Loading cell…
Loading cell…
Write a TypeScript function named createIdempotencyGuard.
Requirements:
key, ttlMs, and an async handler function.Output format: { "code": "...", "explanation": "...", "complexity": { "time": "...", "space": "..." }, "tests": ["...", "...", "...", "...", "..."] }
{ "code": "type IdempotencyRequest = {\n key: string;\n ttlMs: number;\n handler: () => Promise;\n};\n\ntype PendingEntry = {\n state: "pending";\n promise: Promise;\n};\n\ntype FulfilledEntry = {\n state: "fulfilled";\n promise: Promise;\n value: T;\n expiresAt: number;\n};\n\ntype Entry = PendingEntry | FulfilledEntry;\n\nconst entries = new Map<string, Entry>();\n\nexport function createIdempotencyGuard({\n key,\n ttlMs,\n handler,\n}: IdempotencyRequest): Promise {\n const existing = entries.get(key) as Entry | undefined;\n\n if (existing?.state === "pending") {\n return existing.promise;\n }\n\n if (existing?.state === "fulfilled") {\n if (Date.now() < existing.expiresAt) {\n return Promise.resolve(existing.value);\n }\n\n entries.delete(key);\n }\n\n let pendingEntry: PendingEntry;\n\n const promise = Promise.resolve()\n .then(handler)\n .then(\n (value) => {\n if (entries.get(key) === pendingEntry) {\n entries.set(key, {\n state: "fulfilled",\n promise,\n value,\n expiresAt: Date.now() + ttlMs,\n });\n }\n return value;\n },\n (error) => {\n if (entries.get(key) === pendingEntry) {\n entries.delete(key);\n }\n throw error;\n },\n );\n\n pendingEntry = {\n state: "pending",\n promise,\n };\n\n entries.set(key, pendingEntry);\n return promise;\n}\n", "explanation": "The module-level Map stores one entry per key. A pending entry contains the in-flight Promise, so concurrent calls return that exact Promise. After successful completion, the entry is replaced with a fulfilled entry containing the result and its expiration time. Expired entries are removed lazily when the key is accessed. If the handler rejects, the entry is deleted, allowing a later call to retry. The handler is started through Promise.resolve().then(handler), which also converts synchronous throws into rejected Promises.", "complexity": { "time": "O(1) average time per call, excluding the time spent executing the handler. Expiration checks and Map operations are constant time on average.", "space": "O(K), where K is the number of distinct keys retained in the Map. Expired entries are removed when accessed; a separate cleanup timer would be required to eagerly remove expired unused keys." }, "tests": [ "let calls = 0; let release!: () => void; const gate = new Promise(resolve => { release = resolve; }); const handler = async () => { calls++; await gate; return "done"; }; const p1 = createIdempotencyGuard({ key: "concurrent", ttlMs: 1000, handler }); const p2 = createIdempotencyGuard({ key: "concurrent", ttlMs: 1000, handler }); assert(p1 === p2); release(); assert((await p1) === "done"); assert((await p2) === "done"); assert(calls === 1);", "let calls = 0; const handler = async () => ++calls; const first = await createIdempotencyGuard({ key: "cache", ttlMs: 1000, handler }); const second = await createIdempotencyGuard({ key: "cache", ttlMs: 1000, handler }); assert(first === 1); assert(second === 1); assert(calls === 1);", "const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); let calls = 0; const handler = async () => ++calls; const first = await createIdempotencyGuard({ key: "expiry", ttlMs: 20, handler }); await sleep(30); const second = await createIdempotencyGuard({ key: "expiry", ttlMs: 20, handler }); assert(first === 1); assert(second === 2); assert(calls === 2);", "let calls = 0; const handler = async () => { calls++; if (calls === 1) throw new Error("temporary failure"); return "ok"; }; await assertRejects(() => createIdempotencyGuard({ key: "retry", ttlMs: 1000, handler })); const result = await createIdempotencyGuard({ key: "retry", ttlMs: 1000, handler }); assert(result === "ok"); assert(calls === 2);", "let calls = 0; let reject!: (error: Error) => void; const pendingFailure = new Promise((_, r) => { reject = r; }); const handler = async () => { calls++; return pendingFailure; }; const p1 = createIdempotencyGuard({ key: "shared-error", ttlMs: 1000, handler }); const p2 = createIdempotencyGuard({ key: "shared-error", ttlMs: 1000, handler }); assert(p1 === p2); reject(new Error("failure")); await assertRejects(() => p1); await assertRejects(() => p2); assert(calls === 1);" ] }
8/8 checks passed