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 GuardParams = {\n key: string;\n ttlMs: number;\n handler: () => Promise;\n};\n\ntype CacheEntry =\n | { status: 'pending'; promise: Promise }\n | { status: 'success'; result: T; expiresAt: number };\n\nconst cache = new Map<string, CacheEntry>();\n\nexport async function createIdempotencyGuard(params: GuardParams): Promise {\n const { key, ttlMs, handler } = params;\n const now = Date.now();\n const existing = cache.get(key) as CacheEntry | undefined;\n\n if (existing) {\n if (existing.status === 'pending') {\n return existing.promise;\n }\n if (existing.status === 'success' && existing.expiresAt > now) {\n return existing.result;\n }\n // expired success entry — fall through and re-run\n }\n\n const promise = handler().then(\n (result) => {\n cache.set(key, {\n status: 'success',\n result,\n expiresAt: Date.now() + ttlMs,\n });\n return result;\n },\n (error) => {\n // Do not cache failures; allow future retries\n cache.delete(key);\n throw error;\n }\n );\n\n // Synchronous insert so concurrent callers join the same in-flight Promise\n cache.set(key, { status: 'pending', promise });\n return promise;\n}\n\n/** @internal test helper */\nexport function _resetIdempotencyGuardCacheForTests(): void {\n cache.clear();\n}", "explanation": "createIdempotencyGuard keeps a module-level Map from key to either an in-flight Promise or a successful result with an expiry timestamp. On each call it (1) returns the cached result if still within ttlMs, (2) returns the same pending Promise if the key is already running, or (3) starts handler, stores the pending Promise synchronously (safe under JS single-threading), then on resolve replaces the entry with the result and expiry, or on reject deletes the entry so the next call retries. No external libraries are used.", "complexity": { "time": "O(1) per call — Map get/set/delete and Date.now() are constant time. handler runtime is external.", "space": "O(n) where n is the number of distinct keys retained in the Map (pending or unexpired successes). Expired entries are removed lazily on next access for that key; they are not proactively swept." }, "tests": [ "// 1) Concurrency: parallel calls with the same key share one in-flight handler\n_resetIdempotencyGuardCacheForTests();\nlet runs = 0;\nconst handler = async () => { runs++; await new Promise(r => setTimeout(r, 50)); return 'ok'; };\nconst [a, b, c] = await Promise.all([\n createIdempotencyGuard({ key: 'k1', ttlMs: 1000, handler }),\n createIdempotencyGuard({ key: 'k1', ttlMs: 1000, handler }),\n createIdempotencyGuard({ key: 'k1', ttlMs: 1000, handler }),\n]);\nconsole.assert(a === 'ok' && b === 'ok' && c === 'ok');\nconsole.assert(runs === 1, 'handler must run once under concurrency');", "// 2) Caching: subsequent call within TTL returns cached value without re-running handler\n_resetIdempotencyGuardCacheForTests();\nlet runs2 = 0;\nconst h2 = async () => { runs2++; return 42; };\nconst r1 = await createIdempotencyGuard({ key: 'k2', ttlMs: 5000, handler: h2 });\nconst r2 = await createIdempotencyGuard({ key: 'k2', ttlMs: 5000, handler: h2 });\nconsole.assert(r1 === 42 && r2 === 42);\nconsole.assert(runs2 === 1, 'second call must hit cache');", "// 3) Expiry: after ttlMs the handler runs again\n_resetIdempotencyGuardCacheForTests();\nlet runs3 = 0;\nconst h3 = async () => { runs3++; return runs3; };\nconst e1 = await createIdempotencyGuard({ key: 'k3', ttlMs: 30, handler: h3 });\nawait new Promise(r => setTimeout(r, 50));\nconst e2 = await createIdempotencyGuard({ key: 'k3', ttlMs: 30, handler: h3 });\nconsole.assert(e1 === 1 && e2 === 2, 'must re-execute after TTL');\nconsole.assert(runs3 === 2);", "// 4) Errors are not cached; a later call retries the handler\n_resetIdempotencyGuardCacheForTests();\nlet attempts = 0;\nconst h4 = async () => {\n attempts++;\n if (attempts === 1) throw new Error('fail once');\n return 'recovered';\n};\nlet threw = false;\ntry {\n await createIdempotencyGuard({ key: 'k4', ttlMs: 5000, handler: h4 });\n} catch {\n threw = true;\n}\nconsole.assert(threw, 'first call must reject');\nconst ok = await createIdempotencyGuard({ key: 'k4', ttlMs: 5000, handler: h4 });\nconsole.assert(ok === 'recovered');\nconsole.assert(attempts === 2, 'failure must not be cached');", "// 5) Different keys are isolated; error on one key does not affect another\n_resetIdempotencyGuardCacheForTests();\nconst slow = async () => { await new Promise(r => setTimeout(r, 40)); return 'A'; };\nconst fail = async () => { throw new Error('B failed'); };\nconst pA = createIdempotencyGuard({ key: 'keyA', ttlMs: 1000, handler: slow });\nlet failErr = false;\ntry {\n await createIdempotencyGuard({ key: 'keyB', ttlMs: 1000, handler: fail });\n} catch {\n failErr = true;\n}\nconst valA = await pA;\nconsole.assert(failErr && valA === 'A', 'keys must not interfere');\n// 6) Concurrent waiters all reject on failure, then a fresh call can succeed\n_resetIdempotencyGuardCacheForTests();\nlet n = 0;\nconst flaky = async () => { n++; await new Promise(r => setTimeout(r, 20)); throw new Error('x'); };\nconst results = await Promise.allSettled([\n createIdempotencyGuard({ key: 'k6', ttlMs: 1000, handler: flaky }),\n createIdempotencyGuard({ key: 'k6', ttlMs: 1000, handler: flaky }),\n]);\nconsole.assert(results.every(r => r.status === 'rejected'));\nconsole.assert(n === 1, 'failed in-flight work still deduped');\nconst after = await createIdempotencyGuard({ key: 'k6', ttlMs: 1000, handler: async () => 'done' });\nconsole.assert(after === 'done');" ] }
8/8 checks passed