@lodestar/utils
Version:
Utilities required across multiple lodestar packages
164 lines (147 loc) • 4.27 kB
text/typescript
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;
}