UNPKG

@stackbit/utils

Version:
265 lines (246 loc) 9.31 kB
export function forEachPromise<T>(array: T[], callback: (value: T, index: number, array: T[]) => Promise<any>, thisArg?: any): Promise<void> { return new Promise((resolve, reject) => { function next(index: number) { if (index < array.length) { callback .call(thisArg, array[index] as T, index, array) .then((result) => { if (result === false) { resolve(); } else { next(index + 1); } }) .catch((error) => { reject(error); }); } else { resolve(); } } next(0); }); } export function mapPromise<T, U>(array: T[], callback: (value: T, index: number, array: T[]) => Promise<U>, thisArg?: any): Promise<U[]> { return new Promise((resolve, reject) => { const results: U[] = []; function next(index: number) { if (index < array.length) { callback .call(thisArg, array[index] as T, index, array) .then((result) => { results[index] = result; next(index + 1); }) .catch((error) => { reject(error); }); } else { resolve(results); } } next(0); }); } export async function mapValuesPromise<T extends object, U>( object: T, callback: (value: T[keyof T], key: string, object: T) => Promise<U>, thisArg?: any ): Promise<Record<string, U>> { const results: Record<string, U> = {}; for (const [key, value] of Object.entries(object)) { results[key] = await callback.call(thisArg, value, key, object); } return results; } export function reducePromise<T, U>( array: T[], callback: (accumulator: U, currentValue: T, currentIndex: number, array: T[]) => Promise<U>, initialValue: U, thisArg?: any ): Promise<U> { return new Promise((resolve, reject) => { function next(index: number, accumulator: U) { if (index < array.length) { callback .call(thisArg, accumulator, array[index] as T, index, array) .then((result) => { next(index + 1, result); }) .catch((error) => { reject(error); }); } else { resolve(accumulator); } } next(0, initialValue); }); } export function findPromise<T>(array: T[], callback: (value: T, index: number, array: T[]) => Promise<boolean>, thisArg?: any): Promise<T | undefined> { return new Promise((resolve, reject) => { function next(index: number) { if (index < array.length) { callback .call(thisArg, array[index] as T, index, array) .then((result) => { if (result) { resolve(array[index]); } else { next(index + 1); } }) .catch((error) => { reject(error); }); } else { resolve(undefined); } } next(0); }); } export interface DeferredPromise<T> { resolve: (value: T | PromiseLike<T>) => void; reject: (reason?: any) => void; promise: Promise<T>; } export function deferredPromise<T>(): DeferredPromise<T> { let _resolve: (value: T | PromiseLike<T>) => void; let _reject: (reason?: any) => void; const promise = new Promise<T>((resolve, reject) => { _resolve = resolve; _reject = reject; }); // The executor function is called before the Promise constructor returns: // https://262.ecma-international.org/6.0/#sec-promise-executor // so it is safe to use Non-null Assertion Operator "!" return { promise: promise, resolve: _resolve!, reject: _reject! }; } /** * Creates a function that is restricted to invoking `func` serially. Subsequent * calls to the function while the previous `func` call is being resolved, defer * invoking the `func` until the previous call is resolved. The deferred `func` * is invoked with the last arguments provided to the created function. All * subsequent calls to the function are resolved simultaneously with the result * of the last `func` invocation. * * The `options.groupResolver` function allows separating deferred calls into * groups based on the arguments provided to the created function. * * The `options.argsResolver` function allows controlling the arguments passed * to the deferred `func`. * * @example * const defFunc = deferOnceWhileRunning(origFunc); * * defFunc(z) * defFunc(y) ↓ * defFunc(x) ↓ o---------------------------● * ↓ o---------------------------------------● * o--------------------------------● ↑ * ↓ ↑ ↑ * -----o================================●-o================●-----> * ↑ ↑ * origFunc(x) origFunc(z) */ export function deferWhileRunning<R, T extends (...args: any) => Promise<R>>( func: T, options: { thisArg?: any; groupResolver?: (...args: Parameters<T>) => string; argsResolver?: ({ nextArgs, prevArgs }: { nextArgs: Parameters<T>; prevArgs: Parameters<T> | null }) => Parameters<T>; debounceDelay?: number; debounceMaxDelay?: number; } = {} ): T { const groupResolver = options.groupResolver ?? (() => 'defaultGroup'); const argsResolver = options.argsResolver ?? (({ nextArgs }) => nextArgs); const thisArg = options.thisArg; const debounceDelay = options.debounceDelay; const debounceMaxDelay = options.debounceMaxDelay; type DeferGroup = { startWaiting: number | null; timeout: NodeJS.Timeout | null; isInvoking: boolean; deferred: DeferredPromise<R> | null; nextArgs: Parameters<T> | null; }; const deferGroups: Record<string, DeferGroup> = {}; const invoke = async (group: DeferGroup, args: Parameters<T>): Promise<R> => { try { group.isInvoking = true; return await func.apply(thisArg, args); } finally { group.isInvoking = false; if (group.deferred !== null) { const nextArgsCopy = group.nextArgs!; const deferredCopy = group.deferred; group.nextArgs = null; group.deferred = null; try { deferredCopy.resolve(invoke(group, nextArgsCopy)); } catch (e) { deferredCopy.reject(e); } } } }; const debounce = (group: DeferGroup, args: Parameters<T>, debounceWait: number): Promise<R> => { const now = new Date().getTime(); if (!group.deferred) { group.startWaiting = now; group.deferred = deferredPromise(); } if (group.timeout) { clearTimeout(group.timeout); } group.nextArgs = argsResolver({ nextArgs: args, prevArgs: group.nextArgs }); const wait = debounceMaxDelay ? Math.min(Math.max(0, debounceMaxDelay - (now - group.startWaiting!)), debounceWait) : debounceWait; group.timeout = setTimeout(() => { const nextArgsCopy = group.nextArgs!; const deferredCopy = group.deferred!; group.nextArgs = null; group.deferred = null; group.timeout = null; group.startWaiting = null; try { deferredCopy.resolve(invoke(group, nextArgsCopy)); } catch (e) { deferredCopy.reject(e); } }, wait); return group.deferred.promise; }; return (async (...args: Parameters<T>): Promise<R> => { const groupId = groupResolver(...args); let group: DeferGroup; if (groupId in deferGroups) { group = deferGroups[groupId]!; } else { group = { startWaiting: null, timeout: null, isInvoking: false, deferred: null, nextArgs: null }; deferGroups[groupId] = group; } if (!group.isInvoking) { if (typeof debounceDelay !== 'undefined') { return debounce(group, args, debounceDelay); } return invoke(group, args); } if (!group.deferred) { group.deferred = deferredPromise(); } group.nextArgs = argsResolver({ nextArgs: args, prevArgs: group.nextArgs }); return group.deferred.promise; }) as T; }