/** * Per-key serialization. NocoDB has no atomic increment, so all read-modify-write * operations for a given ticket code must run one at a time. Operations on * different codes run concurrently. * * IMPORTANT: correctness depends on a SINGLE backend instance. Never scale this * service to multiple replicas — the queue is in-process only. */ export class RedeemQueue { private chains = new Map>(); private readonly timeoutMs: number; constructor(timeoutMs = 10_000) { this.timeoutMs = timeoutMs; } /** Run `fn` after any in-flight op for `key` completes. */ run(key: string, fn: () => Promise): Promise { const prior = this.chains.get(key) ?? Promise.resolve(); // Chain regardless of whether the prior op resolved or rejected. const next = prior.catch(() => undefined).then(() => this.withTimeout(fn)); this.chains.set(key, next); // Clean up the map entry once this is the tail of the chain. next.finally(() => { if (this.chains.get(key) === next) this.chains.delete(key); }).catch(() => undefined); return next; } private withTimeout(fn: () => Promise): Promise { return new Promise((resolve, reject) => { const timer = setTimeout( () => reject(new Error("redeem operation timed out")), this.timeoutMs, ); fn().then( (v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); }, ); }); } }