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 AsyncHandler = () => Promise;\n\ntype Entry = {\n promise: Promise;\n expiresAt: number;\n timer?: ReturnType;\n};\n\nconst idempotencyEntries = new Map<string, Entry>();\n\nexport function createIdempotencyGuard({\n key,\n ttlMs,\n handler,\n}: {\n key: string;\n ttlMs: number;\n handler: AsyncHandler;\n}): Promise {\n if (!Number.isFinite(ttlMs) || ttlMs < 0) {\n throw new TypeError('ttlMs must be a finite, non-negative number');\n }\n\n const existing = idempotencyEntries.get(key) as Entry | undefined;\n const now = Date.now();\n\n if (existing) {\n if (existing.expiresAt > now) {\n return existing.promise;\n }\n\n if (existing.timer !== undefined) {\n clearTimeout(existing.timer);\n }\n idempotencyEntries.delete(key);\n }\n\n let promise!: Promise;\n\n promise = Promise.resolve()\n .then(() => handler())\n .then(\n (result) => {\n const current = idempotencyEntries.get(key) as Entry | undefined;\n\n // Do not overwrite a newer entry that may have replaced this one.\n if (current?.promise === promise) {\n const cachedEntry: Entry = {\n promise,\n expiresAt: Date.now() + ttlMs,\n };\n\n cachedEntry.timer = setTimeout(() => {\n const latest = idempotencyEntries.get(key) as Entry | undefined;\n if (latest?.promise === promise) {\n idempotencyEntries.delete(key);\n }\n }, ttlMs);\n\n idempotencyEntries.set(key, cachedEntry);\n }\n\n return result;\n },\n (error) => {\n const current = idempotencyEntries.get(key) as Entry | undefined;\n if (current?.promise === promise) {\n idempotencyEntries.delete(key);\n }\n throw error;\n },\n );\n\n idempotencyEntries.set(key, {\n promise,\n expiresAt: Number.POSITIVE_INFINITY,\n });\n\n return promise;\n}", "explanation": "The map stores either an in-flight promise or a successfully completed promise. Calls using a key whose entry is still valid receive the exact same promise. The handler is invoked only after the entry has been inserted, so concurrent calls cannot start duplicate work. On success, the entry receives an expiration time and is removed after ttlMs. On failure, the entry is deleted immediately so a later call retries. The identity checks prevent an older request from deleting or overwriting a newer request for the same key.", "complexity": { "time": "Expected O(1) per call, excluding the time spent executing handler, because Map operations and expiration checks are constant time.", "space": "O(k) for k distinct active or cached keys, plus the memory retained by cached results and timer callbacks. Entries are removed when they expire or when their handler fails." }, "tests": [ "Concurrency: set a counter to 0 and use a handler that waits briefly before incrementing it. Call createIdempotencyGuard({ key: 'concurrent', ttlMs: 1000, handler }) twice without awaiting either call. Assert both returned values are equal, the two returned promises are strictly equal, and the counter is 1.", "Caching: call createIdempotencyGuard({ key: 'cached', ttlMs: 1000, handler }) twice sequentially with a handler that increments a counter and returns the counter. Assert both results are 1 and the counter remains 1.", "Expiry: call the guard with a short TTL such as 20 milliseconds, await the result, wait at least 30 milliseconds, and call it again. Assert the handler ran twice and the second call produced a fresh result.", "Error retry: use a handler that throws on its first invocation and succeeds on its second. Assert the first call rejects, the second call succeeds, and the invocation count is 2.", "Concurrent error retry: make two concurrent calls with a handler that rejects after a delay. Assert both calls reject and the handler runs once. Then call the guard again with a succeeding handler and assert it runs successfully, proving the failure was not cached.", "Different keys: call the guard concurrently with keys 'a' and 'b' and separate handlers. Assert both handlers execute independently and each key returns its own result." ] }
8/8 checks passed