Reference solution
The findings below are ordered by priority.
Findings
1. The cache key crosses tenant boundaries
Location: src/profile-cache.ts:25-27,44-50
key uses only userId, so the first tenant to cache user-1 supplies the value returned to every other tenant that asks for the same user ID. This is a direct cross-tenant data disclosure, and invalidation from one tenant also removes another tenant's entry.
Build the key from an unambiguous encoding of both tenantId and userId, and use that same composite identity for reads, refresh tracking, writes, and invalidation.
2. Concurrent stale reads start duplicate origin refreshes
Location: src/profile-cache.ts:36-38,53-64
Every caller that observes a stale entry invokes refresh. A burst of requests therefore produces an equally large burst of origin calls, defeating the required single-flight behavior and increasing load precisely when the origin may already be slow.
Track the in-flight promise per composite cache key. The first stale reader starts it, later readers reuse it while still returning stale data immediately, and completion removes only that same tracked promise.
3. A failed refresh deletes the usable stale value
Location: src/profile-cache.ts:65-69
When the origin rejects, the catch block deletes the entry. A background refresh failure therefore converts a still-valid stale value into a miss, so the next request blocks on the failing origin instead of receiving data allowed through five minutes.
Report the failure while retaining the existing entry. Only a successful refresh should replace it; normal age checks can stop serving it once the stale limit passes.
4. Invalidation can be undone by an older origin request
Location: src/profile-cache.ts:44-46,58-63
invalidate removes the current map entry but does not distinguish generations of work. If a refresh or miss began first, its later response unconditionally writes the profile back and reverses the administrator's invalidation.
Advance a per-key generation during invalidation and capture it when work starts. Commit a response only if its generation is still current; a read after invalidation can deliberately start work in the new generation.
5. The test never enters the stale or invalidation paths
Location: src/profile-cache.test.ts:5-25
The clock never advances and the origin resolves immediately, so the test protects only the uncomplicated fresh hit. It cannot detect tenant collisions, duplicate background refreshes, stale retention after failure, or a late write after invalidation.
Add controlled promises and a mutable monotonic clock. Cover equal user IDs in two tenants, concurrent stale reads, rejected refreshes, expiry beyond five minutes, and invalidation while a fetch is pending.
Reasonable non-findings
- Returning the stale value without awaiting its refresh is the intended stale-while-revalidate behavior.
- The inclusive 60-second and five-minute comparisons match the stated freshness boundaries.
- Catching the detached refresh promise prevents an unhandled rejection; reporting already occurs inside
refresh.