Implement an async memoization utility that caches resolved results while safely managing in-flight concurrency to avoid duplicate work.
Unlike synchronous memoization, asynchronous requests introduce complex concurrency states. If multiple calls with the exact same arguments are made before the first request settles, only one underlying request should be made to prevent duplicated network/IO operations (request deduplication).
In-Flight Deduplication: Concurrent requests with identical arguments must share the same Promise until settled.
Success Caching: When a promise resolves, its result becomes permanently cached. Subsequent calls return the cached value directly.
Failure Recovery: Rejected promises must not be cached. If an in-flight request fails, the cache entry must be cleared so the next consumer can trigger a fresh execution.
Key Generation: Use JSON.stringify(args) to generate cache keys for strict argument matching.
Examples
const memoized =memoizeAsync(async(id)=>{const res =awaitfetch(`/api/user/${id}`);return res.json();});// These three calls trigger only ONE network requestconst[u1, u2, u3]=awaitPromise.all([memoized(101),memoized(101),memoized(101)]);// Subsequent calls return the cached user object immediatelyconst u4 =awaitmemoized(101);
Constraints
Time complexity per cached hit must be O(1) (excluding serialization cost).
Space complexity must be bounded by O(K) where K is the number of distinct argument signatures.
Must not leak memory from rejected promises; tracking entries must be cleaned up on failure.
The returned function must preserve the asynchronous nature (always return a Promise).