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 fastRafWithFallback(fn: F) { return fastRaf(fn, false, true); } export function fastRafPrimaryWithFallback(fn: F) { return fastRaf(fn, true, true); } export function throttleWithRafFallback(fn: F) { return throttleWith(fastRafWithFallback, fn); } export function throttleWithPrimaryRafFallback(fn: F) { return throttleWith(fastRafPrimaryWithFallback, fn); } export function throttleWithRaf(fn: F) { return throttleWith(fastRaf, fn); } export function throttleWithPrimaryRaf(fn: F) { return throttleWith(fastRafPrimary, fn); } export function throttleWithTickEnd(fn: F) { return throttleWith(onTickEnd, fn); } export function throttleWithPrimaryTickEnd(fn: F) { return throttleWith(onTickEndPrimary, 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); }); } let fastRafCallbacks: NoneToVoidFunction[] | undefined; let fastRafPrimaryCallbacks: NoneToVoidFunction[] | undefined; let timeoutCallbacks: NoneToVoidFunction[] | undefined; let timeoutPrimaryCallbacks: NoneToVoidFunction[] | undefined; let timeout: NodeJS.Timeout | undefined; const FAST_RAF_TIMEOUT_FALLBACK_MS = 300; // 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, isPrimary = false, withTimeoutFallback = false) { if (!fastRafCallbacks) { fastRafCallbacks = !withTimeoutFallback && !isPrimary ? [callback] : []; fastRafPrimaryCallbacks = !withTimeoutFallback && isPrimary ? [callback] : []; timeoutCallbacks = withTimeoutFallback && !isPrimary ? [callback] : []; timeoutPrimaryCallbacks = withTimeoutFallback && isPrimary ? [callback] : []; requestAnimationFrame(() => { const currentCallbacks = fastRafCallbacks!; const currentPrimaryCallbacks = fastRafPrimaryCallbacks!; const currentTimeoutCallbacks = timeoutCallbacks!; const currentTimeoutPrimaryCallbacks = timeoutPrimaryCallbacks!; if (timeout) clearTimeout(timeout); timeout = undefined; fastRafCallbacks = undefined; fastRafPrimaryCallbacks = undefined; timeoutCallbacks = undefined; timeoutPrimaryCallbacks = undefined; currentPrimaryCallbacks.forEach((cb) => cb()); currentTimeoutPrimaryCallbacks.forEach((cb) => cb()); currentCallbacks.forEach((cb) => cb()); currentTimeoutCallbacks.forEach((cb) => cb()); }); } else if (isPrimary) { if (withTimeoutFallback) { timeoutPrimaryCallbacks!.push(callback); } else { fastRafPrimaryCallbacks!.push(callback); } } else if (withTimeoutFallback) { timeoutCallbacks!.push(callback); } else { fastRafCallbacks.push(callback); } if (!timeout && withTimeoutFallback) { timeout = setTimeout(() => { const currentTimeoutCallbacks = timeoutCallbacks!; const currentTimeoutPrimaryCallbacks = timeoutPrimaryCallbacks!; if (timeout) clearTimeout(timeout); timeout = undefined; timeoutCallbacks = []; timeoutPrimaryCallbacks = []; currentTimeoutPrimaryCallbacks.forEach((cb) => cb()); currentTimeoutCallbacks.forEach((cb) => cb()); }, FAST_RAF_TIMEOUT_FALLBACK_MS); } } export function fastRafPrimary(callback: NoneToVoidFunction) { fastRaf(callback, true); } let onTickEndCallbacks: NoneToVoidFunction[] | undefined; let onTickEndPrimaryCallbacks: NoneToVoidFunction[] | undefined; export function onTickEnd(callback: NoneToVoidFunction, isPrimary = false) { if (!onTickEndCallbacks) { onTickEndCallbacks = isPrimary ? [] : [callback]; onTickEndPrimaryCallbacks = isPrimary ? [callback] : []; Promise.resolve().then(() => { const currentCallbacks = onTickEndCallbacks!; const currentPrimaryCallbacks = onTickEndPrimaryCallbacks!; onTickEndCallbacks = undefined; onTickEndPrimaryCallbacks = undefined; currentPrimaryCallbacks.forEach((cb) => cb()); currentCallbacks.forEach((cb) => cb()); }); } else if (isPrimary) { onTickEndPrimaryCallbacks!.push(callback); } else { onTickEndCallbacks.push(callback); } } export function onTickEndPrimary(callback: NoneToVoidFunction) { onTickEnd(callback, true); } 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); }; }