@stackbit/utils
Version:
Stackbit utilities
265 lines (246 loc) • 9.31 kB
text/typescript
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;
}