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": "interface IdempotencyGuardOptions {\n key: string;\n ttlMs: number;\n handler: () => Promise;\n}\n\ntype InFlightEntry = {\n promise: Promise;\n};\n\ntype CachedEntry = {\n result: T;\n expiry: number;\n};\n\nexport function createIdempotencyGuard() {\n const inFlight = new Map<string, InFlightEntry>();\n const cache = new Map<string, CachedEntry>();\n\n return async function execute({ key, ttlMs, handler }: IdempotencyGuardOptions): Promise {\n // Check if there is a cached result that hasn't expired\n const cached = cache.get(key);\n if (cached && Date.now() < cached.expiry) {\n return cached.result;\n }\n\n // Check if there is an in-flight promise for the same key\n const inflight = inFlight.get(key);\n if (inflight) {\n return inflight.promise;\n }\n\n // No cached valid result and no in-flight; start new handler\n const promise = handler().then(\n (result) => {\n // On success: cache result with expiry, remove from inflight\n cache.set(key, { result, expiry: Date.now() + ttlMs });\n inFlight.delete(key);\n return result;\n },\n (error) => {\n // On failure: remove from inflight, do not cache\n inFlight.delete(key);\n throw error; // rethrow\n }\n );\n\n // Store the in-flight promise\n inFlight.set(key, { promise });\n\n return promise;\n };\n}",
"explanation": "The function createIdempotencyGuard returns a closure that maintains two internal Maps: inFlight for currently executing promises and cache for successfully completed results with their expiration timestamps. On each call with a given key, it first checks the cache (if the entry exists and has not expired). If valid, it returns the cached result. Otherwise, it checks the inFlight map; if a promise for that key is already pending, it returns the same promise to avoid duplicate execution. If neither condition holds, it invokes the handler, stores the resulting promise in inFlight, and attaches .then and .catch handlers. On success, the result is cached with an expiry time of Date.now() + ttlMs and the in-flight entry is removed. On failure, the in-flight entry is removed and no caching occurs, ensuring a future call retries the handler. The design uses only plain JavaScript Maps and Promises, with no external libraries.",
"complexity": {
"time": "O(1) per operation – Map lookups, insertions, and deletions are constant time.",
"space": "O(n) where n is the total number of distinct keys that are either currently in-flight or cached (with expired entries eventually being reclaimed by the next call)."
},
"tests": [
"// Test 1: Basic caching\n// Two sequential calls with the same key and a short TTL should return the same result without re-executing the handler.\nconst guard = createIdempotencyGuard();\nlet callCount = 0;\nconst handler = async () => { callCount++; return 42; };\nconst result1 = await guard({ key: 'a', ttlMs: 1000, handler });\nconst result2 = await guard({ key: 'a', ttlMs: 1000, handler });\nassert.strictEqual(result1, 42);\nassert.strictEqual(result2, 42);\nassert.strictEqual(callCount, 1);",
"// Test 2: Concurrency\n// Two simultaneous calls with the same key should receive the same promise (handler executed once).\nconst guard = createIdempotencyGuard();\nlet callCount = 0;\nconst handler = async () => { callCount++; await delay(100); return 99; };\nconst [res1, res2] = await Promise.all([\n guard({ key: 'x', ttlMs: 1000, handler }),\n guard({ key: 'x', ttlMs: 1000, handler })\n]);\nassert.strictEqual(res1, 99);\nassert.strictEqual(res2, 99);\nassert.strictEqual(callCount, 1);",
"// Test 3: Expiry\n// After the TTL expires, the next call should re-execute the handler.\nconst guard = createIdempotencyGuard();\nlet callCount = 0;\nconst handler = async () => { callCount++; return Date.now(); };\nconst first = await guard({ key: 'b', ttlMs: 50, handler });\nawait delay(60);\nconst second = await guard({ key: 'b', ttlMs: 50, handler });\nassert.notStrictEqual(first, second);\nassert.strictEqual(callCount, 2);",
"// Test 4: Error handling\n// If the handler throws, the error is propagated and no caching occurs; the next call retries.\nconst guard = createIdempotencyGuard();\nlet callCount = 0;\nconst handler = async () => {\n callCount++;\n if (callCount === 1) throw new Error('fail');\n return 10;\n};\nawait assert.rejects(\n guard({ key: 'c', ttlMs: 1000, handler }),\n /fail/\n);\nconst result = await guard({ key: 'c', ttlMs: 1000, handler });\nassert.strictEqual(result, 10);\nassert.strictEqual(callCount, 2);",
"// Test 5: Multiple keys\n// Different keys should be handled independently.\nconst guard = createIdempotencyGuard();\nlet callCount = 0;\nconst handler = async () => { callCount++; return callCount; };\nconst [r1, r2] = await Promise.all([\n guard({ key: 'k1', ttlMs: 1000, handler }),\n guard({ key: 'k2', ttlMs: 1000, handler })\n]);\nassert.strictEqual(r1, 1);\nassert.strictEqual(r2, 2);\nassert.strictEqual(callCount, 2);"
]
}
8/8 checks passed