UNPKG

@lodestar/utils

Version:

Utilities required across multiple lodestar packages

164 lines (147 loc) 4.27 kB
import {ErrorAborted, TimeoutError} from "./errors.js"; import {sleep} from "./sleep.js"; import {ArrayToTuple, NonEmptyArray} from "./types.js"; /** * While promise t is not finished, call function `fn` per `interval` */ export async function callFnWhenAwait<T>( p: Promise<T>, fn: () => void, interval: number, signal?: AbortSignal ): Promise<T> { let done = false; const logFn = async (): Promise<void> => { while (!done) { await sleep(interval, signal); if (!done) fn(); } }; const t = await Promise.race([p, logFn()]).finally(() => { done = true; }); return t as T; } /** * Create a deferred promise */ export function defer<T>() { let resolve!: (v: T) => void; let reject!: (e: unknown) => void; const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej; }); return {promise, resolve, reject}; } export type PromiseResult<T> = { promise: Promise<T>; } & ( | { status: "pending"; } | { status: "fulfilled"; value: T; durationMs: number; } | { status: "rejected"; reason: Error; durationMs: number; } ); export type PromiseFulfilledResult<T> = PromiseResult<T> & {status: "fulfilled"}; export type PromiseRejectedResult<T> = PromiseResult<T> & {status: "rejected"}; /** * Wrap a promise to an object to track the status and value of the promise */ export function wrapPromise<T>(promise: PromiseLike<T>): PromiseResult<T> { const startedAt = Date.now(); const result = { promise: promise.then( (value) => { result.status = "fulfilled"; (result as PromiseFulfilledResult<T>).value = value; (result as PromiseFulfilledResult<T>).durationMs = Date.now() - startedAt; return value; }, (reason: unknown) => { result.status = "rejected"; (result as PromiseRejectedResult<T>).reason = reason as Error; (result as PromiseRejectedResult<T>).durationMs = Date.now() - startedAt; throw reason; } ), status: "pending", } as PromiseResult<T>; return result; } type ReturnPromiseWithTuple<Tuple extends NonEmptyArray<PromiseLike<unknown>>> = { [Index in keyof ArrayToTuple<Tuple>]: PromiseResult<Awaited<Tuple[Index]>>; }; /** * Two phased approach for resolving promises: * - first wait `resolveTimeoutMs` or until all promises settle * - then wait `raceTimeoutMs - resolveTimeoutMs` or until at least a single promise resolves * * Returns a list of promise results, see `PromiseResult` */ export async function resolveOrRacePromises<T extends NonEmptyArray<PromiseLike<unknown>>>( promises: T, { resolveTimeoutMs, raceTimeoutMs, signal, }: { resolveTimeoutMs: number; raceTimeoutMs: number; signal?: AbortSignal; } ): Promise<ReturnPromiseWithTuple<T>> | never { if (raceTimeoutMs <= resolveTimeoutMs) { throw new Error("Race time must be greater than resolve time"); } const resolveTimeoutError = new TimeoutError( `Given promises can't be resolved within resolveTimeoutMs=${resolveTimeoutMs}` ); const raceTimeoutError = new TimeoutError( `Not a any single promise be resolved in given raceTimeoutMs=${raceTimeoutMs}` ); const promiseResults = promises.map((p) => wrapPromise(p)) as ReturnPromiseWithTuple<T>; // We intentionally want an array of promises here promises = (promiseResults as PromiseResult<T>[]).map((p) => p.promise) as unknown as T; try { await Promise.race([ Promise.allSettled(promises), sleep(resolveTimeoutMs, signal).then(() => { throw resolveTimeoutError; }), ]); return promiseResults; } catch (err) { if (err instanceof ErrorAborted) { return promiseResults; } if (err !== resolveTimeoutError) { throw err; } } try { await Promise.race([ Promise.any(promises), sleep(raceTimeoutMs - resolveTimeoutMs, signal).then(() => { throw raceTimeoutError; }), ]); return promiseResults; } catch (err) { if (err instanceof ErrorAborted) { return promiseResults; } if (err !== raceTimeoutError && !(err instanceof AggregateError)) { throw err; } } return promiseResults; }