An idempotency-key queue guarantees that a task tied to a caller-supplied key runs at most once within a time window, and that every duplicate request — one that overlaps the first call or one that arrives long after it finished — receives the same result without re-running the work. This is the client-side shape of Stripe-style idempotency keys: a user double-clicks Pay, or a flaky network silently retries the same request, and you must charge the card exactly once and hand the same charge back to every duplicate.
Implement idempotencyKeyQueue(options). The options object carries ttl — the number of milliseconds a completed result stays cached (default Infinity). It returns an object with a run(key, task) method plus has(key) and a size count. The first run for a key invokes task() and stores the in-flight promise; a duplicate while that promise is pending shares it; a duplicate after it resolved returns the cached value; and a rejected task is evicted so a retry runs the task again.
function idempotencyKeyQueue(options?: { ttl?: number }): IdempotencyQueue;
interface IdempotencyQueue {
// Runs task() for `key` at most once per ttl window. A concurrent duplicate
// shares the in-flight promise; a later duplicate returns the cached result;
// a failure evicts the key so the next call retries.
run<T>(key: string, task: () => Promise<T>): Promise<T>;
has(key: string): boolean; // is the key in-flight or cached?
size: number; // count of in-flight plus cached keys
}
const q = idempotencyKeyQueue({ ttl: 60000 });
const charge = () => api.charge(cart); // side effect: money moves
const a = q.run('checkout-42', charge);
const b = q.run('checkout-42', charge); // duplicate while in flight
a === b; // true — charge() ran once; both callers await the one call
await a;
q.run('checkout-42', charge); // duplicate after success → cached result, no charge
const q = idempotencyKeyQueue({ ttl: 30 });
await q.run('k', task); // miss → runs task
await q.run('k', task); // hit → cached, task not run
// ...30ms later...
await q.run('k', task); // window closed → runs task again
// failures are never cached:
await q.run('save', failing).catch(() => {});
await q.run('save', save); // key was evicted → runs the task again
run(key, task) never invokes task again; every duplicate resolves from the shared work.ttl milliseconds (default Infinity); after it expires, the next call re-runs the task.Map per queue is enough here.