Dead Letter Queue for Failed Jobs
Retry failed jobs up to a limit, then move them to an inspectable dead-letter queue with the error message and failure time.
Code
export interface Job<T> {
id: string;
payload: T;
attempts: number;
}
export interface DeadLetterEntry<T> {
job: Job<T>;
error: string;
failedAt: number;
}
export class RetryQueue<T> {
private readonly pending: Job<T>[] = [];
private readonly deadLetters: DeadLetterEntry<T>[] = [];
private readonly maxAttempts: number;
private readonly now: () => number;
constructor(maxAttempts = 3, now: () => number = () => Date.now()) {
if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
throw new RangeError("maxAttempts must be a positive integer");
}
if (typeof now !== "function") throw new TypeError("now must be a function");
this.maxAttempts = maxAttempts;
this.now = now;
}
enqueue(id: string, payload: T): void {
if (typeof id !== "string" || id.length === 0) {
throw new TypeError("id must be a non-empty string");
}
this.pending.push({ id, payload, attempts: 0 });
}
async drain(
handler: (payload: T, job: Job<T>) => Promise<void>,
): Promise<{ processed: number; deadLettered: number }> {
if (typeof handler !== "function") throw new TypeError("handler must be a function");
let processed = 0;
let deadLettered = 0;
while (this.pending.length > 0) {
const job = this.pending.shift();
if (!job) break;
job.attempts += 1;
try {
await handler(job.payload, job);
processed += 1;
} catch (error) {
if (job.attempts < this.maxAttempts) {
this.pending.push(job);
} else {
this.deadLetters.push({
job,
error: error instanceof Error ? error.message : String(error),
failedAt: this.now(),
});
deadLettered += 1;
}
}
}
return { processed, deadLettered };
}
get deadLetterCount(): number {
return this.deadLetters.length;
}
listDeadLetters(): readonly DeadLetterEntry<T>[] {
return [...this.deadLetters];
}
}Report a problem with this exampleOpens a pre-filled issue on GitHub.