When caching data fetched from an external source that can change out-of-band (polling, file system, git status, etc.), avoid two common pitfalls: invalidating based on a fragile derived signature, and clobbering fresh data with a late in-flight response.
stale: true rather than deleting it outright.Example pattern:
type Cached<T> = T & { stale?: boolean };
function markStale<T extends { stale?: boolean }>(cache: Map<string, T>) {
const next = new Map<string, T>();
for (const [key, entry] of cache) next.set(key, { ...entry, stale: true });
return next;
}
const generation = new Map<string, number>();
function invalidate(keys: Iterable<string>) {
for (const key of keys) {
generation.set(key, (generation.get(key) ?? 0) + 1);
}
}
async function read(key: string) {
const gen = generation.get(key) ?? 0;
const result = await fetchData(key);
if ((generation.get(key) ?? 0) !== gen) {
// stale response raced a newer invalidation — refetch once, don't apply
return read(key);
}
applyResult(key, result);
}
Cover this behavior with a test that holds a read in flight across an invalidation/poll cycle and asserts the late response is discarded (or applied correctly) and exactly one follow-up read is issued.