TelegramPWA/src/util/schedulers.ts
2023-10-27 12:52:06 +02:00

200 lines
5.0 KiB
TypeScript

export type Scheduler = typeof requestAnimationFrame | typeof onTickEnd;
export function debounce<F extends AnyToVoidFunction>(
fn: F,
ms: number,
shouldRunFirst = true,
shouldRunLast = true,
) {
let waitingTimeout: number | undefined;
return (...args: Parameters<F>) => {
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<F extends AnyToVoidFunction>(
fn: F,
ms: number,
shouldRunFirst = true,
) {
let interval: number | undefined;
let isPending: boolean;
let args: Parameters<F>;
return (..._args: Parameters<F>) => {
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<F extends AnyToVoidFunction>(fn: F) {
return throttleWith(onTickEnd, fn);
}
export function throttleWith<F extends AnyToVoidFunction>(schedulerFn: Scheduler, fn: F) {
let waiting = false;
let args: Parameters<F>;
return (..._args: Parameters<F>) => {
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<void>((resolve) => {
setTimeout(() => resolve(), ms);
});
export function rafPromise() {
return new Promise<void>((resolve) => {
fastRaf(resolve);
});
}
const FAST_RAF_TIMEOUT_FALLBACK_MS = 35; // < 30 FPS
let fastRafCallbacks: Set<NoneToVoidFunction> | undefined;
let fastRafFallbackCallbacks: Set<NoneToVoidFunction> | 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);
};
}