blob: d309e3b8f9445e5b2c37cbb721d14a661ccd2448 [file]
type CompletionOutcome<T> =
| { status: 'fulfilled'; value: T }
| { status: 'rejected'; error: unknown };
type CompletionWaiter<T> = (outcome: CompletionOutcome<T>) => void;
export type CompletionWaitResult = 'settled' | 'delay' | 'abort';
export type CompletionSignalWaitResult = Exclude<CompletionWaitResult, 'delay'>;
/** A deferred completion whose bounded waits can detach before it settles. */
export class CompletionLatch<T> {
private readonly waiters = new Set<CompletionWaiter<T>>();
private outcome: CompletionOutcome<T> | undefined;
resolve(value: T): void {
this.settle({ status: 'fulfilled', value });
}
reject(error: unknown): void {
this.settle({ status: 'rejected', error });
}
join(): Promise<T> {
if (this.outcome) return outcomeValue(this.outcome);
return new Promise<T>((resolve, reject) => {
this.waiters.add((outcome) => {
if (outcome.status === 'fulfilled') resolve(outcome.value);
else reject(outcome.error);
});
});
}
wait(delayMs: number, signal?: AbortSignal): Promise<CompletionWaitResult> {
return this.waitInternal(delayMs, signal);
}
waitFor(signal?: AbortSignal): Promise<CompletionSignalWaitResult> {
return this.waitInternal(undefined, signal);
}
private waitInternal(
delayMs: undefined,
signal?: AbortSignal,
): Promise<CompletionSignalWaitResult>;
private waitInternal(delayMs: number, signal?: AbortSignal): Promise<CompletionWaitResult>;
private waitInternal(
delayMs: number | undefined,
signal?: AbortSignal,
): Promise<CompletionWaitResult> {
if (this.outcome) return outcomeResult(this.outcome);
if (signal?.aborted) return Promise.resolve('abort');
return new Promise<CompletionWaitResult>((resolve, reject) => {
let settled = false;
let timer: NodeJS.Timeout | undefined;
const cleanup = () => {
if (timer) clearTimeout(timer);
signal?.removeEventListener('abort', onAbort);
this.waiters.delete(onCompletion);
};
const complete = (result: CompletionWaitResult) => {
if (settled) return;
settled = true;
cleanup();
resolve(result);
};
const onAbort = () => complete('abort');
const onCompletion: CompletionWaiter<T> = (outcome) => {
if (settled) return;
settled = true;
cleanup();
if (outcome.status === 'fulfilled') resolve('settled');
else reject(outcome.error);
};
if (delayMs !== undefined) timer = setTimeout(() => complete('delay'), delayMs);
signal?.addEventListener('abort', onAbort, { once: true });
this.waiters.add(onCompletion);
});
}
private settle(outcome: CompletionOutcome<T>): void {
if (this.outcome) return;
this.outcome = outcome;
for (const waiter of this.waiters) waiter(outcome);
this.waiters.clear();
}
}
function outcomeValue<T>(outcome: CompletionOutcome<T>): Promise<T> {
return outcome.status === 'fulfilled'
? Promise.resolve(outcome.value)
: Promise.reject(outcome.error);
}
function outcomeResult<T>(outcome: CompletionOutcome<T>): Promise<CompletionWaitResult> {
return outcome.status === 'fulfilled'
? Promise.resolve('settled')
: Promise.reject(outcome.error);
}