PASS9.5
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 Handler<T> = () => Promise<T>;\n\ninterface IdempotencyGuardOptions<T> {\n key: string;\n ttlMs: number;\n handler: Handler<T>;\n}\n\ninterface CacheEntry<T> {\n value: T;\n expiresAt: number;\n}\n\ninterface InFlightEntry<T> {\n promise: Promise<T>;\n}\n\n// Module-level stores shared across all calls\nconst cache = new Map<string, CacheEntry<unknown>>();\nconst inFlight = new Map<string, InFlightEntry<unknown>>();\n\nasync function createIdempotencyGuard<T>({\n key,\n ttlMs,\n handler,\n}: IdempotencyGuardOptions<T>): Promise<T> {\n const now = Date.now();\n\n // 1. Check if a valid cached result exists\n const cached = cache.get(key) as CacheEntry<T> | undefined;\n if (cached && cached.expiresAt > now) {\n return cached.value;\n } else if (cached) {\n // Expired entry — evict it\n cache.delete(key);\n }\n\n // 2. Check if there is an in-flight request for this key\n const existing = inFlight.get(key) as InFlightEntry<T> | undefined;\n if (existing) {\n return existing.promise;\n }\n\n // 3. Start a new request\n const promise: Promise<T> = (async () => {\n try {\n const result = await handler();\n // Cache the successful result\n cache.set(key, { value: result, expiresAt: Date.now() + ttlMs });\n return result;\n } catch (err) {\n // Do NOT cache failures; remove in-flight so next call retries\n throw err;\n } finally {\n // Always clean up the in-flight entry\n inFlight.delete(key);\n }\n })();\n\n inFlight.set(key, { promise });\n return promise;\n}\n\n// Exported for testing\nexport { createIdempotencyGuard, cache, inFlight };\nexport type { IdempotencyGuardOptions };",
"explanation": "The function uses two module-level Maps: `cache` stores successful results with an expiry timestamp, and `inFlight` stores Promises that are currently executing. On each call:\n1. **Cache hit (valid TTL):** Return the cached value immediately without calling the handler.\n2. **Cache miss or expired:** Evict any stale entry and proceed.\n3. **In-flight deduplication:** If another call with the same key is already running, return the *same* Promise so all callers share the result and the handler runs exactly once.\n4. **New execution:** Wrap the handler in an async IIFE, register it in `inFlight`, and on success write to `cache`. On failure, skip caching so the next caller retries. The `finally` block always removes the in-flight entry to avoid leaks.\n\nThis design ensures:\n- **Idempotency under concurrency:** Concurrent calls with the same key share one Promise.\n- **Result caching:** Subsequent calls within `ttlMs` skip the handler entirely.\n- **Error transparency:** Errors propagate to all waiting callers, and the next call after failure starts fresh.\n- **No external dependencies:** Only native `Map`, `Date`, and `Promise` primitives are used.",
"complexity": {
"time": "O(1) per call for cache lookup, in-flight lookup, and insertion — all Map operations are O(1) average. The handler itself may take arbitrary time, but the guard overhead is constant. Expired-entry eviction is also O(1) (single key lookup and delete).",
"space": "O(K) where K is the number of distinct keys that have been processed. At any moment the cache holds at most one entry per unique key (evicted on expiry or overwritten on retry), and inFlight holds at most one entry per key currently executing. No historical accumulation occurs beyond live entries."
},
"tests": [
"// Test 1 — Concurrency: same key called twice simultaneously shares one in-flight Promise\nasync function testConcurrency() {\n // Reset stores\n cache.clear(); inFlight.clear();\n let callCount = 0;\n const handler = async () => { callCount++; await new Promise(r => setTimeout(r, 50)); return 'result'; };\n const [r1, r2] = await Promise.all([\n createIdempotencyGuard({ key: 'c1', ttlMs: 1000, handler }),\n createIdempotencyGuard({ key: 'c1', ttlMs: 1000, handler }),\n ]);\n console.assert(callCount === 1, 'Handler should be called only once');\n console.assert(r1 === 'result' && r2 === 'result', 'Both callers should receive the result');\n console.log('Test 1 passed');\n}",
"// Test 2 — Caching: second call within TTL returns cached value without invoking handler\nasync function testCaching() {\n cache.clear(); inFlight.clear();\n let callCount = 0;\n const handler = async () => { callCount++; return 42; };\n const r1 = await createIdempotencyGuard({ key: 'c2', ttlMs: 5000, handler });\n const r2 = await createIdempotencyGuard({ key: 'c2', ttlMs: 5000, handler });\n console.assert(callCount === 1, 'Handler should be called only once within TTL');\n console.assert(r1 === 42 && r2 === 42, 'Both calls should return 42');\n console.log('Test 2 passed');\n}",
"// Test 3 — Expiry: after TTL expires, handler is called again\nasync function testExpiry() {\n cache.clear(); inFlight.clear();\n let callCount = 0;\n const handler = async () => { callCount++; return callCount; };\n const r1 = await createIdempotencyGuard({ key: 'c3', ttlMs: 50, handler });\n await new Promise(r => setTimeout(r, 100)); // wait for TTL to expire\n const r2 = await createIdempotencyGuard({ key: 'c3', ttlMs: 50, handler });\n console.assert(callCount === 2, 'Handler should be called again after TTL expiry');\n console.assert(r1 === 1 && r2 === 2, 'Results should differ after expiry');\n console.log('Test 3 passed');\n}",
"// Test 4 — Error handling: failure is not cached; next call retries\nasync function testErrorRetry() {\n cache.clear(); inFlight.clear();\n let callCount = 0;\n const handler = async () => {\n callCount++;\n if (callCount === 1) throw new Error('transient failure');\n return 'ok';\n };\n let threw = false;\n try {\n await createIdempotencyGuard({ key: 'c4', ttlMs: 5000, handler });\n } catch {\n threw = true;\n }\n console.assert(threw, 'First call should throw');\n console.assert(!cache.has('c4'), 'Failed result must not be cached');\n const r2 = await createIdempotencyGuard({ key: 'c4', ttlMs: 5000, handler });\n console.assert(callCount === 2, 'Handler should be retried after failure');\n console.assert(r2 === 'ok', 'Second call should succeed');\n console.log('Test 4 passed');\n}",
"// Test 5 — Error propagation to concurrent callers: all waiters receive the error\nasync function testConcurrentErrorPropagation() {\n cache.clear(); inFlight.clear();\n let callCount = 0;\n const handler = async () => {\n callCount++;\n await new Promise(r => setTimeout(r, 30));\n throw new Error('boom');\n };\n const results = await Promise.allSettled([\n createIdempotencyGuard({ key: 'c5', ttlMs: 5000, handler }),\n createIdempotencyGuard({ key: 'c5', ttlMs: 5000, handler }),\n createIdempotencyGuard({ key: 'c5', ttlMs: 5000, handler }),\n ]);\n console.assert(callCount === 1, 'Handler should only run once despite 3 concurrent callers');\n const allRejected = results.every(r => r.status === 'rejected');\n console.assert(allRejected, 'All concurrent callers should receive the rejection');\n console.assert(!cache.has('c5'), 'Error must not be cached');\n // Verify retry works after shared failure\n let retryCount = 0;\n const retryHandler = async () => { retryCount++; return 'recovered'; };\n const r = await createIdempotencyGuard({ key: 'c5', ttlMs: 5000, handler: retryHandler });\n console.assert(r === 'recovered', 'Retry after shared failure should succeed');\n console.log('Test 5 passed');\n}"
]
}
8/8 checks passed