Async
import { KeyedSerializer, Serializer, SingleFlight, wait, withTimeout } from '@adecore/utils/async';Helpers for time and for the order of async work. They run in a page and in Node.
Waiting
wait(ms, signal) resolves after ms. An abort resolves it early instead of rejecting, because the loops that wait check the signal on their next turn:
while (!signal.aborted) {
await poll();
await wait(5_000, signal);
}Each wait takes its abort listener back when it ends, so one long-lived signal can serve many waits.
Deadlines
withTimeout(promise, ms, message) rejects with message once ms passed, and clears its timer either way. A call that answers at once does not hold the process open until its deadline.
const status = await withTimeout(fetchStatus(), 10_000, 'The status took too long.');withDeadline(promise, ms, message, timer) is the same with the timer passed in. The DeadlineTimer it takes has setTimeout and clearTimeout, so a test can drive the clock.
Timers
A Timer runs a callback after a delay and returns how to cancel it. Code that takes a Timer instead of calling setTimeout lets a test pass a fake one and skip the wait.
cancellableTimeoutkeeps the process alive until it fires.unrefTimeoutdoes not, for a timer that should never be the reason a process stays up.
function scheduleRetry(timer: Timer, retry: () => void): () => void {
return timer(1_000, retry);
}
const cancel = scheduleRetry(unrefTimeout, reconnect);One at a time
Serializer runs one piece of work at a time. The next starts once the one before settled, kept or broken, and every caller gets its own result. Two changes in a row never read the same file and drop one of them.
const writes = new Serializer();
export function saveSettings(settings: Settings): Promise<void> {
return writes.run(() => writeAtomic(SETTINGS_FILE, JSON.stringify(settings)));
}idle() resolves when the queue is empty, without adding work of its own. Await it before the process exits.
KeyedSerializer does the same per key, for a store whose records are written one by one and never wait on each other. It drops a key once its last work settled, and size reports how many keys still have work:
const writes = new KeyedSerializer();
export function saveRecord(id: string, write: () => Promise<void>): Promise<void> {
return writes.run(id, () => withTimeout(write(), 10_000, `Saving ${id} took too long.`));
}Once per key
SingleFlight runs work once per key at a time. A caller that asks while the work runs gets the same promise, and a key that settled leaves nothing behind, so the next call runs the work again:
const loads = new SingleFlight<Project>();
export function loadProject(path: string): Promise<Project> {
return loads.run(path, () => readProject(path));
}get(key) returns the run in flight, and has(key) says whether there is one.