Delay Queue With Scheduled Jobs
Schedule async jobs for future delivery with cancellable timers, early-clock rescheduling and pending inspection.
Code
export interface DelayedItem<T> {
id: string;
value: T;
dueAt: number;
}
export class DelayQueue<T> {
private readonly items = new Map<string, DelayedItem<T>>();
private readonly timers = new Map<string, ReturnType<typeof setTimeout>>();
private readonly handler: (value: T, id: string) => void | Promise<void>;
private readonly now: () => number;
private sequence = 0;
private lastFailure: unknown;
constructor(
handler: (value: T, id: string) => void | Promise<void>,
now: () => number = Date.now,
) {
this.handler = handler;
this.now = now;
}
schedule(value: T, delayMs: number, id = "job-" + (this.sequence += 1)): string {
if (!Number.isFinite(delayMs) || delayMs < 0) {
throw new RangeError("delayMs must be a non-negative finite number");
}
this.cancel(id);
const dueAt = this.now() + delayMs;
this.items.set(id, { id, value, dueAt });
const timer = setTimeout(() => {
void this.deliver(id);
}, delayMs);
const handle = timer as unknown as { unref?: () => void };
handle.unref?.();
this.timers.set(id, timer);
return id;
}
cancel(id: string): boolean {
const timer = this.timers.get(id);
if (timer !== undefined) clearTimeout(timer);
this.timers.delete(id);
return this.items.delete(id);
}
async deliver(id: string): Promise<void> {
const item = this.items.get(id);
if (!item) return;
const remaining = item.dueAt - this.now();
if (remaining > 0) {
const timer = setTimeout(() => {
void this.deliver(id);
}, remaining);
const handle = timer as unknown as { unref?: () => void };
handle.unref?.();
this.timers.set(id, timer);
return;
}
this.items.delete(id);
this.timers.delete(id);
try {
await this.handler(item.value, id);
} catch (error) {
this.lastFailure = error;
}
}
get size(): number {
return this.items.size;
}
get lastError(): unknown {
return this.lastFailure;
}
pending(): DelayedItem<T>[] {
return [...this.items.values()]
.sort((left, right) => left.dueAt - right.dueAt)
.map((item) => ({ ...item }));
}
clear(): void {
for (const id of [...this.items.keys()]) this.cancel(id);
}
}Report a problem with this exampleOpens a pre-filled issue on GitHub.