monkeytypegame / monkeytypegame/monkeytype
Bug (backend): cacheWithTTL marks cache fresh before fetch resolves — failed fetch serves stale data for full TTL, no in-flight dedup
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 20.7k
- Forks
- 3.3k
- PR merge metrics
- No merged PRs in 30d
Description
Did you clear cache before opening an issue?
- I have cleared my cache
Is there an existing issue for this?
- I have searched the existing open and closed issues
Does the issue happen when logged in?
N/A
Does the issue happen when logged out?
N/A (backend source bug)
Does the issue happen in incognito mode when logged in?
N/A
Does the issue happen in incognito mode when logged out?
N/A
Issue details
Current Behavior
cacheWithTTL in backend/src/utils/ttl-cache.ts marks the cache as fresh before the fetch resolves:
// backend/src/utils/ttl-cache.ts:20-26
return async () => {
if (lastFetchTime < Date.now() - ttlMs) {
lastFetchTime = Date.now(); // updated before await completes
cache = await fn();
}
return cache;
};
Consequences:
- Failed fetch poisons the cache for the full TTL. If
fn()rejects, the rejection propagates to that caller, butlastFetchTimehas already been advanced — every subsequent call within the TTL returns stale cached data instead of retrying. This utility backs the PSA endpoint (controllers/psa.ts), so one failed upstream fetch serves stale content for the whole TTL window. - No in-flight promise dedup. When the TTL expires under concurrent requests, all callers run
fn()simultaneously (thundering herd) since nothing records the pending promise.
Expected Behavior
Only advance lastFetchTime after a successful fetch, and dedupe concurrent calls by caching the promise itself:
let lastFetchTime = 0;
let cache: T | undefined;
let inflight: Promise<T> | undefined;
return async () => {
if (lastFetchTime < Date.now() - ttlMs) {
inflight ??= fn()
.then((result) => {
cache = result;
lastFetchTime = Date.now();
return result;
})
.finally(() => {
inflight = undefined;
});
return inflight;
}
return cache;
};
Steps To Reproduce
- Call a
cacheWithTTL-wrapped getter whosefnthrows. - Call again within the TTL — stale data is returned with no retry until TTL expiry.
Environment
- Backend,
master@ 91bd24bb8
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in backend/src/utils/ttl-cache.ts and review how cacheWithTTL is used by controllers/psa.ts. Verify the behavior for failed fetches and concurrent calls, then update the utility so successful fetches refresh the TTL, failures remain retryable, and concurrent requests share one in-flight fetch. Done means stale data is not served after a failed refresh and the upstream function runs only once for concurrent expired-cache calls.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100