export type Scheduler = typeof requestAnimationFrame | typeof onTickEnd; export function debounce( fn: F, ms: number, shouldRunFirst = true, shouldRunLast = true, ) { let waitingTimeout: number | undefined; return (...args: Parameters) => { if (waitingTimeout) { clearTimeout(waitingTimeout); waitingTimeout = undefined; } else if (shouldRunFirst) { fn(...args); } // eslint-disable-next-line no-restricted-globals waitingTimeout = self.setTimeout(() => { if (shouldRunLast) { fn(...args); } waitingTimeout = undefined; }, ms); }; } export function throttle( fn: F, ms: number, shouldRunFirst = true, ) { let interval: number | undefined; let isPending: boolean; let args: Parameters; return (..._args: Parameters) => { isPending = true; args = _args; if (!interval) { if (shouldRunFirst) { isPending = false; fn(...args); } // eslint-disable-next-line no-restricted-globals interval = self.setInterval(() => { if (!isPending) { // eslint-disable-next-line no-restricted-globals self.clearInterval(interval!); interval = undefined; return; } isPending = false; fn(...args); }, ms); } }; } export function throttleWithTickEnd(fn: F) { return throttleWith(onTickEnd, fn); } export function throttleWith(schedulerFn: Scheduler, fn: F) { let waiting = false; let args: Parameters; return (..._args: Parameters) => { args = _args; if (!waiting) { waiting = true; schedulerFn(() => { waiting = false; fn(...args); }); } }; } export function onIdle(cb: NoneToVoidFunction, timeout?: number) { // eslint-disable-next-line no-restricted-globals if (self.requestIdleCallback) { // eslint-disable-next-line no-restricted-globals self.requestIdleCallback(cb, { timeout }); } else { onTickEnd(cb); } } export const pause = (ms: number) => new Promise((resolve) => { setTimeout(() => resolve(), ms); }); export function rafPromise() { return new Promise((resolve) => { fastRaf(resolve); }); } const FAST_RAF_TIMEOUT_FALLBACK_MS = 35; // < 30 FPS let fastRafCallbacks: Set | undefined; let fastRafFallbackCallbacks: Set | undefined; let fastRafFallbackTimeout: number | undefined; // May result in an immediate execution if called from another RAF callback which was scheduled // (and therefore is executed) earlier than RAF callback scheduled by `fastRaf` export function fastRaf(callback: NoneToVoidFunction, withTimeoutFallback = false) { if (!fastRafCallbacks) { fastRafCallbacks = new Set([callback]); requestAnimationFrame(() => { const currentCallbacks = fastRafCallbacks!; fastRafCallbacks = undefined; fastRafFallbackCallbacks = undefined; if (fastRafFallbackTimeout) { clearTimeout(fastRafFallbackTimeout); fastRafFallbackTimeout = undefined; } currentCallbacks.forEach((cb) => cb()); }); } else { fastRafCallbacks.add(callback); } if (withTimeoutFallback) { if (!fastRafFallbackCallbacks) { fastRafFallbackCallbacks = new Set([callback]); } else { fastRafFallbackCallbacks.add(callback); } if (!fastRafFallbackTimeout) { fastRafFallbackTimeout = window.setTimeout(() => { const currentTimeoutCallbacks = fastRafFallbackCallbacks!; if (fastRafCallbacks) { currentTimeoutCallbacks.forEach(fastRafCallbacks.delete, fastRafCallbacks); } fastRafFallbackCallbacks = undefined; if (fastRafFallbackTimeout) { clearTimeout(fastRafFallbackTimeout); fastRafFallbackTimeout = undefined; } currentTimeoutCallbacks.forEach((cb) => cb()); }, FAST_RAF_TIMEOUT_FALLBACK_MS); } } } let onTickEndCallbacks: NoneToVoidFunction[] | undefined; export function onTickEnd(callback: NoneToVoidFunction) { if (!onTickEndCallbacks) { onTickEndCallbacks = [callback]; Promise.resolve().then(() => { const currentCallbacks = onTickEndCallbacks!; onTickEndCallbacks = undefined; currentCallbacks.forEach((cb) => cb()); }); } else { onTickEndCallbacks.push(callback); } } let beforeUnloadCallbacks: NoneToVoidFunction[] | undefined; export function onBeforeUnload(callback: NoneToVoidFunction, isLast = false) { if (!beforeUnloadCallbacks) { beforeUnloadCallbacks = []; // eslint-disable-next-line no-restricted-globals self.addEventListener('beforeunload', () => { beforeUnloadCallbacks!.forEach((cb) => cb()); }); } if (isLast) { beforeUnloadCallbacks.push(callback); } else { beforeUnloadCallbacks.unshift(callback); } return () => { beforeUnloadCallbacks = beforeUnloadCallbacks!.filter((cb) => cb !== callback); }; }