UNPKG

15.6 kBJavaScriptView Raw
1import { getWindow } from './dom/getWindow';
2/**
3 * Bugs often appear in async code when stuff gets disposed, but async operations don't get canceled.
4 * This Async helper class solves these issues by tying async code to the lifetime of a disposable object.
5 *
6 * Usage: Anything class extending from BaseModel can access this helper via this.async. Otherwise create a
7 * new instance of the class and remember to call dispose() during your code's dispose handler.
8 *
9 * @public
10 */
11var Async = /** @class */ (function () {
12 // eslint-disable-next-line @typescript-eslint/no-explicit-any
13 function Async(parent, onError) {
14 this._timeoutIds = null;
15 this._immediateIds = null;
16 this._intervalIds = null;
17 this._animationFrameIds = null;
18 this._isDisposed = false;
19 this._parent = parent || null;
20 this._onErrorHandler = onError;
21 this._noop = function () {
22 /* do nothing */
23 };
24 }
25 /**
26 * Dispose function, clears all async operations.
27 */
28 Async.prototype.dispose = function () {
29 var id;
30 this._isDisposed = true;
31 this._parent = null;
32 // Clear timeouts.
33 if (this._timeoutIds) {
34 for (id in this._timeoutIds) {
35 if (this._timeoutIds.hasOwnProperty(id)) {
36 this.clearTimeout(parseInt(id, 10));
37 }
38 }
39 this._timeoutIds = null;
40 }
41 // Clear immediates.
42 if (this._immediateIds) {
43 for (id in this._immediateIds) {
44 if (this._immediateIds.hasOwnProperty(id)) {
45 this.clearImmediate(parseInt(id, 10));
46 }
47 }
48 this._immediateIds = null;
49 }
50 // Clear intervals.
51 if (this._intervalIds) {
52 for (id in this._intervalIds) {
53 if (this._intervalIds.hasOwnProperty(id)) {
54 this.clearInterval(parseInt(id, 10));
55 }
56 }
57 this._intervalIds = null;
58 }
59 // Clear animation frames.
60 if (this._animationFrameIds) {
61 for (id in this._animationFrameIds) {
62 if (this._animationFrameIds.hasOwnProperty(id)) {
63 this.cancelAnimationFrame(parseInt(id, 10));
64 }
65 }
66 this._animationFrameIds = null;
67 }
68 };
69 /**
70 * SetTimeout override, which will auto cancel the timeout during dispose.
71 * @param callback - Callback to execute.
72 * @param duration - Duration in milliseconds.
73 * @returns The setTimeout id.
74 */
75 Async.prototype.setTimeout = function (callback, duration) {
76 var _this = this;
77 var timeoutId = 0;
78 if (!this._isDisposed) {
79 if (!this._timeoutIds) {
80 this._timeoutIds = {};
81 }
82 timeoutId = setTimeout(function () {
83 // Time to execute the timeout, enqueue it as a foreground task to be executed.
84 try {
85 // Now delete the record and call the callback.
86 if (_this._timeoutIds) {
87 delete _this._timeoutIds[timeoutId];
88 }
89 callback.apply(_this._parent);
90 }
91 catch (e) {
92 if (_this._onErrorHandler) {
93 _this._onErrorHandler(e);
94 }
95 }
96 }, duration);
97 this._timeoutIds[timeoutId] = true;
98 }
99 return timeoutId;
100 };
101 /**
102 * Clears the timeout.
103 * @param id - Id to cancel.
104 */
105 Async.prototype.clearTimeout = function (id) {
106 if (this._timeoutIds && this._timeoutIds[id]) {
107 clearTimeout(id);
108 delete this._timeoutIds[id];
109 }
110 };
111 /**
112 * SetImmediate override, which will auto cancel the immediate during dispose.
113 * @param callback - Callback to execute.
114 * @param targetElement - Optional target element to use for identifying the correct window.
115 * @returns The setTimeout id.
116 */
117 Async.prototype.setImmediate = function (callback, targetElement) {
118 var _this = this;
119 var immediateId = 0;
120 var win = getWindow(targetElement);
121 if (!this._isDisposed) {
122 if (!this._immediateIds) {
123 this._immediateIds = {};
124 }
125 var setImmediateCallback = function () {
126 // Time to execute the timeout, enqueue it as a foreground task to be executed.
127 try {
128 // Now delete the record and call the callback.
129 if (_this._immediateIds) {
130 delete _this._immediateIds[immediateId];
131 }
132 callback.apply(_this._parent);
133 }
134 catch (e) {
135 _this._logError(e);
136 }
137 };
138 immediateId = win.setTimeout(setImmediateCallback, 0);
139 this._immediateIds[immediateId] = true;
140 }
141 return immediateId;
142 };
143 /**
144 * Clears the immediate.
145 * @param id - Id to cancel.
146 * @param targetElement - Optional target element to use for identifying the correct window.
147 */
148 Async.prototype.clearImmediate = function (id, targetElement) {
149 var win = getWindow(targetElement);
150 if (this._immediateIds && this._immediateIds[id]) {
151 win.clearTimeout(id);
152 delete this._immediateIds[id];
153 }
154 };
155 /**
156 * SetInterval override, which will auto cancel the timeout during dispose.
157 * @param callback - Callback to execute.
158 * @param duration - Duration in milliseconds.
159 * @returns The setTimeout id.
160 */
161 Async.prototype.setInterval = function (callback, duration) {
162 var _this = this;
163 var intervalId = 0;
164 if (!this._isDisposed) {
165 if (!this._intervalIds) {
166 this._intervalIds = {};
167 }
168 intervalId = setInterval(function () {
169 // Time to execute the interval callback, enqueue it as a foreground task to be executed.
170 try {
171 callback.apply(_this._parent);
172 }
173 catch (e) {
174 _this._logError(e);
175 }
176 }, duration);
177 this._intervalIds[intervalId] = true;
178 }
179 return intervalId;
180 };
181 /**
182 * Clears the interval.
183 * @param id - Id to cancel.
184 */
185 Async.prototype.clearInterval = function (id) {
186 if (this._intervalIds && this._intervalIds[id]) {
187 clearInterval(id);
188 delete this._intervalIds[id];
189 }
190 };
191 /**
192 * Creates a function that, when executed, will only call the func function at most once per
193 * every wait milliseconds. Provide an options object to indicate that func should be invoked
194 * on the leading and/or trailing edge of the wait timeout. Subsequent calls to the throttled
195 * function will return the result of the last func call.
196 *
197 * Note: If leading and trailing options are true func will be called on the trailing edge of
198 * the timeout only if the throttled function is invoked more than once during the wait timeout.
199 *
200 * @param func - The function to throttle.
201 * @param wait - The number of milliseconds to throttle executions to. Defaults to 0.
202 * @param options - The options object.
203 * @returns The new throttled function.
204 */
205 // eslint-disable-next-line @typescript-eslint/no-explicit-any
206 Async.prototype.throttle = function (func, wait, options) {
207 var _this = this;
208 if (this._isDisposed) {
209 return this._noop;
210 }
211 var waitMS = wait || 0;
212 var leading = true;
213 var trailing = true;
214 var lastExecuteTime = 0;
215 var lastResult;
216 // eslint-disable-next-line @typescript-eslint/no-explicit-any
217 var lastArgs;
218 var timeoutId = null;
219 if (options && typeof options.leading === 'boolean') {
220 leading = options.leading;
221 }
222 if (options && typeof options.trailing === 'boolean') {
223 trailing = options.trailing;
224 }
225 var callback = function (userCall) {
226 var now = Date.now();
227 var delta = now - lastExecuteTime;
228 var waitLength = leading ? waitMS - delta : waitMS;
229 if (delta >= waitMS && (!userCall || leading)) {
230 lastExecuteTime = now;
231 if (timeoutId) {
232 _this.clearTimeout(timeoutId);
233 timeoutId = null;
234 }
235 lastResult = func.apply(_this._parent, lastArgs);
236 }
237 else if (timeoutId === null && trailing) {
238 timeoutId = _this.setTimeout(callback, waitLength);
239 }
240 return lastResult;
241 };
242 // eslint-disable-next-line @typescript-eslint/no-explicit-any
243 var resultFunction = (function () {
244 var args = [];
245 for (var _i = 0; _i < arguments.length; _i++) {
246 args[_i] = arguments[_i];
247 }
248 lastArgs = args;
249 return callback(true);
250 });
251 return resultFunction;
252 };
253 /**
254 * Creates a function that will delay the execution of func until after wait milliseconds have
255 * elapsed since the last time it was invoked. Provide an options object to indicate that func
256 * should be invoked on the leading and/or trailing edge of the wait timeout. Subsequent calls
257 * to the debounced function will return the result of the last func call.
258 *
259 * Note: If leading and trailing options are true func will be called on the trailing edge of
260 * the timeout only if the debounced function is invoked more than once during the wait
261 * timeout.
262 *
263 * @param func - The function to debounce.
264 * @param wait - The number of milliseconds to delay.
265 * @param options - The options object.
266 * @returns The new debounced function.
267 */
268 // eslint-disable-next-line @typescript-eslint/no-explicit-any
269 Async.prototype.debounce = function (func, wait, options) {
270 var _this = this;
271 if (this._isDisposed) {
272 var noOpFunction = (function () {
273 /** Do nothing */
274 });
275 noOpFunction.cancel = function () {
276 return;
277 };
278 noOpFunction.flush = (function () { return null; });
279 noOpFunction.pending = function () { return false; };
280 return noOpFunction;
281 }
282 var waitMS = wait || 0;
283 var leading = false;
284 var trailing = true;
285 var maxWait = null;
286 var lastCallTime = 0;
287 var lastExecuteTime = Date.now();
288 var lastResult;
289 // eslint-disable-next-line @typescript-eslint/no-explicit-any
290 var lastArgs;
291 var timeoutId = null;
292 if (options && typeof options.leading === 'boolean') {
293 leading = options.leading;
294 }
295 if (options && typeof options.trailing === 'boolean') {
296 trailing = options.trailing;
297 }
298 if (options && typeof options.maxWait === 'number' && !isNaN(options.maxWait)) {
299 maxWait = options.maxWait;
300 }
301 var markExecuted = function (time) {
302 if (timeoutId) {
303 _this.clearTimeout(timeoutId);
304 timeoutId = null;
305 }
306 lastExecuteTime = time;
307 };
308 var invokeFunction = function (time) {
309 markExecuted(time);
310 lastResult = func.apply(_this._parent, lastArgs);
311 };
312 var callback = function (userCall) {
313 var now = Date.now();
314 var executeImmediately = false;
315 if (userCall) {
316 if (leading && now - lastCallTime >= waitMS) {
317 executeImmediately = true;
318 }
319 lastCallTime = now;
320 }
321 var delta = now - lastCallTime;
322 var waitLength = waitMS - delta;
323 var maxWaitDelta = now - lastExecuteTime;
324 var maxWaitExpired = false;
325 if (maxWait !== null) {
326 // maxWait only matters when there is a pending callback
327 if (maxWaitDelta >= maxWait && timeoutId) {
328 maxWaitExpired = true;
329 }
330 else {
331 waitLength = Math.min(waitLength, maxWait - maxWaitDelta);
332 }
333 }
334 if (delta >= waitMS || maxWaitExpired || executeImmediately) {
335 invokeFunction(now);
336 }
337 else if ((timeoutId === null || !userCall) && trailing) {
338 timeoutId = _this.setTimeout(callback, waitLength);
339 }
340 return lastResult;
341 };
342 var pending = function () {
343 return !!timeoutId;
344 };
345 var cancel = function () {
346 if (pending()) {
347 // Mark the debounced function as having executed
348 markExecuted(Date.now());
349 }
350 };
351 var flush = function () {
352 if (pending()) {
353 invokeFunction(Date.now());
354 }
355 return lastResult;
356 };
357 // eslint-disable-next-line @typescript-eslint/no-explicit-any
358 var resultFunction = (function () {
359 var args = [];
360 for (var _i = 0; _i < arguments.length; _i++) {
361 args[_i] = arguments[_i];
362 }
363 lastArgs = args;
364 return callback(true);
365 });
366 resultFunction.cancel = cancel;
367 resultFunction.flush = flush;
368 resultFunction.pending = pending;
369 return resultFunction;
370 };
371 Async.prototype.requestAnimationFrame = function (callback, targetElement) {
372 var _this = this;
373 var animationFrameId = 0;
374 var win = getWindow(targetElement);
375 if (!this._isDisposed) {
376 if (!this._animationFrameIds) {
377 this._animationFrameIds = {};
378 }
379 var animationFrameCallback = function () {
380 try {
381 // Now delete the record and call the callback.
382 if (_this._animationFrameIds) {
383 delete _this._animationFrameIds[animationFrameId];
384 }
385 callback.apply(_this._parent);
386 }
387 catch (e) {
388 _this._logError(e);
389 }
390 };
391 animationFrameId = win.requestAnimationFrame
392 ? win.requestAnimationFrame(animationFrameCallback)
393 : win.setTimeout(animationFrameCallback, 0);
394 this._animationFrameIds[animationFrameId] = true;
395 }
396 return animationFrameId;
397 };
398 Async.prototype.cancelAnimationFrame = function (id, targetElement) {
399 var win = getWindow(targetElement);
400 if (this._animationFrameIds && this._animationFrameIds[id]) {
401 win.cancelAnimationFrame ? win.cancelAnimationFrame(id) : win.clearTimeout(id);
402 delete this._animationFrameIds[id];
403 }
404 };
405 // eslint-disable-next-line @typescript-eslint/no-explicit-any
406 Async.prototype._logError = function (e) {
407 if (this._onErrorHandler) {
408 this._onErrorHandler(e);
409 }
410 };
411 return Async;
412}());
413export { Async };
414//# sourceMappingURL=Async.js.map
\No newline at end of file