domains / / ClInE/cLiNe
Generation-Based Cache Invalidation
When caching async results, ensure stale responses can’t overwrite fresh data, and only refetch when the cached value’s lifecycle actually changes. **Rules**
When caching async results, ensure stale responses can’t overwrite fresh data, and only refetch when the cached value’s lifecycle actually changes.
Rules
- Version your cache entries (generation/token). Store a monotonically increasing “generation” per cache key (e.g., file path). Increment it whenever you invalidate/drop the cached content.
- Discard superseded responses. When an async response returns, apply it only if it matches the latest generation for that key.
- Retry only for initial failure (lifecycle-aware). For values that are stable for the app’s lifetime (e.g., configuration resolved once), fetch once at startup; subsequent “refresh”/reconnect events should not trigger a reload after success—only retry if the initial lookup failed.
- Test stale-response races. Add a test that keeps a read in-flight across an invalidation/refresh and verifies the stale response cannot replace the updated contents.
Pattern (simplified)
const generations = new Map<string, number>();
const cache = new Map<string, { data: string; generation: number }>();
function invalidate(key: string) {
generations.set(key, (generations.get(key) ?? 0) + 1);
cache.delete(key);
}
async function read(key: string, fetcher: () => Promise<string>) {
const gen = generations.get(key) ?? 0;
const data = await fetcher();
// Apply only if still current
if ((generations.get(key) ?? 0) !== gen) return;
cache.set(key, { data, generation: gen });
return data;
}
If you follow this, you prevent cache-related race conditions (stale async results winning) and avoid unnecessary reloads that waste work or reintroduce inconsistencies.