Exemple
lru-cache 11.5.2: Configure lru-cache callback signatures, fetchMethod async coalescing, and stale-while-revalidate without silent overwrite disposal or stale return traps
Échantillon vérifié pour npm lru-cache 11.5.2: Configure lru-cache callback signatures, fetchMethod async coalescing, and stale-while-revalidate without…
sha256:f9f588239d71f4d0b4bf38e37c500bf9c7de64348e6cfd115d8d41e7f7ec9f1c
Ce réseau offre une seule chose : un échantillon qui compile. Il l'a exécuté dans un bac à sable et conservé le reçu signé. Il ne note rien et ne garantit rien : si le même code compile chez vous, il ne l'a pas mesuré.
Combien de clés de signature distinctes ont déposé un reçu de contrat réussi. Une seule, c'est l'auteur ; plus d'une signifie que quelqu'un d'autre l'a compilé aussi. Une clé est auto-générée sans identité enregistrée derrière, donc on compte des clés, pas des personnes.
MIT-0
Preuves d'exécution
L'environnement déclaré et les exécutions signées sont séparés, pour que vous voyiez exactement ce que cet échantillon a exécuté et où.
- Base de preuve
- Contrat signé réussi
- Reçus de vérification
- 2
- Clés de signature qui l’ont compilé
- 2
Environnement déclaré
node linux x64 node node npm
Environnements des exécutions de vérification
| Environnement | Contrat | Étapes | Exécution |
|---|---|---|---|
| node 22 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-16 |
| node 22 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-18 |
Cas
HOW- Objectif
- Configure lru-cache callback signatures, fetchMethod async coalescing, and stale-while-revalidate without silent overwrite disposal or stale return traps
- Paquets
- Symboles
-
- LRUCache
- LRUCache#fetch
- LRUCache#set
- LRUCache#pop
- LRUCache#entries
- LRUCache#keys
- Environnement
- node
- Créé
- 2026-08-16T07:28:45Z
Contrat
- assert bare number constructor throws TypeError and legacy maxAge or length options are ignored
- assert dispose and onInsert receive (value, key, reason) with set() overwrites triggering dispose('set') unless noDisposeOnSet: true
- assert fetchMethod receives (key, staleValue, { options, signal, context }) and concurrent fetch() calls coalesce
- assert fetchMethod returning undefined does not cache key
- assert fetch() with allowStale: true immediately returns stale value and triggers background fetchMethod unless forceRefresh is set
- assert set(key, undefined) deletes entry and iterators yield in MRU-to-LRU order
Fichiers
- NOTES.md
- csx.json
- package-lock.json
- package.json
- test/contract.mjs
Code source
# Notes on lru-cache (Lifecycle Callbacks, fetchMethod Signature & SWR Semantics)
## CSX Network Search Result
`search_known_solution` found sample `sha256:45ba1445da9dc36a83c5aad3029d51db56180dc067c54674860a7701622f3552` covering TTL stale entry deletion on `get()` and `maxSize` entry dropping without size calculators. This sample targets a different API surface: callback signatures (`dispose`, `onInsert`), `fetchMethod` argument shape and in-flight deduplication, stale-while-revalidate background execution, and modern constructor / iteration mechanics.
## What a Model Would Have Written Instead
A competent model would assume `fetchMethod` and `memoMethod` receive `(key, context)` or `(key, { context })` as the 2nd parameter, assume `dispose` follows `(key, value, reason)` ordering and only fires on capacity eviction, expect `await cache.fetch(key)` to always await fresh execution, construct caches with `new LRUCache(50)` or `{ maxAge }`, and assume iteration yields in FIFO / insertion order like JavaScript `Map`.
## How the Wrong Version Fails
- **Loudly**: Calling `memoMethod` or `fetchMethod` with `(key, { context })` crashes with `TypeError: Cannot destructure property 'context' of 'undefined' as it is undefined` because the 2nd argument is `staleValue` (`undefined` on initial fetch); calling `new LRUCache(50)` throws `TypeError: At least one of max, maxSize, or ttl is required`.
- **Silently with a green build / runtime bug**: `dispose` runs unintended cleanup logic on every `cache.set()` overwrite because `noDisposeOnSet: true` was omitted; `{ maxAge }` is silently ignored leaving TTL at 0; and `await cache.fetch(key)` with `allowStale: true` immediately returns stale data without awaiting the loader while quietly triggering background revalidation.
{"case":{"caseId":"case:sha256:5e714b73c9632e516fda66a92745b0a4efce1d9d07b4bc8308913d418dbb3307","contract":["assert bare number constructor throws TypeError and legacy maxAge or length options are ignored","assert dispose and onInsert receive (value, key, reason) with set() overwrites triggering dispose('set') unless noDisposeOnSet: true","assert fetchMethod receives (key, staleValue, { options, signal, context }) and concurrent fetch() calls coalesce","assert fetchMethod returning undefined does not cache key","assert fetch() with allowStale: true immediately returns stale value and triggers background fetchMethod unless forceRefresh is set","assert set(key, undefined) deletes entry and iterators yield in MRU-to-LRU order"],"goal":"Configure lru-cache callback signatures, fetchMethod async coalescing, and stale-while-revalidate without silent overwrite disposal or stale return traps","kind":"HOW","packages":["pkg:npm/lru-cache@11.5.2"],"schemaVersion":1,"symbols":["LRUCache","LRUCache#fetch","LRUCache#set","LRUCache#pop","LRUCache#entries","LRUCache#keys"]},"contractCommand":["node","test/contract.mjs"],"environment":{"arch":"x64","ecosystem":"npm","executionContext":"node","language":"node","os":"linux","packageManager":"npm","runtime":"node","schemaVersion":1},"license":"MIT-0","packages":["pkg:npm/lru-cache@11.5.2"],"schemaVersion":1,"symbols":["LRUCache","LRUCache#fetch","LRUCache#set","LRUCache#pop","LRUCache#entries","LRUCache#keys"],"verifierAdapter":"node-typescript@1"}
{
"name": "npm-lru-cache-api",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "npm-lru-cache-api",
"version": "1.0.0",
"dependencies": {
"lru-cache": "^11.5.2"
}
},
"node_modules/lru-cache": {
"version": "11.5.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
}
}
}
{
"name": "npm-lru-cache-api",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"test": "node test/contract.mjs"
},
"dependencies": {
"lru-cache": "^11.5.2"
}
}
import assert from 'node:assert/strict';
import { LRUCache } from 'lru-cache';
// Mock clock for 100% deterministic time control without wall-clock timers
let virtualTime = 1000;
const perf = { now: () => virtualTime };
// --------------------------------------------------------------------------
// 1. Constructor and Legacy Option Traps
// --------------------------------------------------------------------------
{
// Naive expectation: new LRUCache(50) creates a cache with max: 50
// Reality: Constructor requires an options object; bare number throws TypeError
assert.throws(
() => new LRUCache(50),
/At least one of max, maxSize, or ttl is required/
);
// Naive expectation: legacy v6 options { maxAge, length } set TTL and size calculator
// Reality: maxAge and length are silently ignored; ttl defaults to 0 and sizeCalculation is undefined
const legacyCache = new LRUCache({
max: 10,
maxAge: 5000,
length: (val) => val.length,
});
assert.equal(legacyCache.ttl, 0);
assert.equal(legacyCache.sizeCalculation, undefined);
}
// --------------------------------------------------------------------------
// 2. Callback Signatures and Eviction Reasons: dispose, onInsert, and noDisposeOnSet
// --------------------------------------------------------------------------
{
const disposeEvents = [];
const insertEvents = [];
const cache = new LRUCache({
max: 2,
// Naive expectation: dispose receives (key, value, reason) like an entry tuple [k, v]
// Reality: Signature is (value, key, reason) following Map.prototype.forEach order
dispose: (value, key, reason) => {
disposeEvents.push({ key, value, reason });
},
// Signature is (value, key, reason)
onInsert: (value, key, reason) => {
insertEvents.push({ key, value, reason });
},
});
// Insertion of new key: onInsert receives reason 'add'
cache.set('k1', 'val1');
assert.deepEqual(insertEvents.pop(), { key: 'k1', value: 'val1', reason: 'add' });
assert.equal(disposeEvents.length, 0);
// Overwriting an existing key:
// Naive expectation: dispose only runs on capacity eviction ('evict')
// Reality: set() on an existing key triggers dispose with reason 'set' for the old value!
cache.set('k1', 'val1-updated');
assert.deepEqual(disposeEvents.pop(), { key: 'k1', value: 'val1', reason: 'set' });
assert.deepEqual(insertEvents.pop(), { key: 'k1', value: 'val1-updated', reason: 'replace' });
// Capacity eviction: adding k2 then k3 beyond max: 2 evicts older key k1 with reason 'evict'
cache.set('k2', 'val2');
insertEvents.pop();
cache.set('k3', 'val3');
assert.deepEqual(disposeEvents.pop(), { key: 'k1', value: 'val1-updated', reason: 'evict' });
// Explicit deletion: triggers dispose with reason 'delete'
cache.delete('k3');
assert.deepEqual(disposeEvents.pop(), { key: 'k3', value: 'val3', reason: 'delete' });
// cache.clear() disposes all remaining entries with reason 'delete'
cache.clear();
assert.deepEqual(disposeEvents.pop(), { key: 'k2', value: 'val2', reason: 'delete' });
// With noDisposeOnSet: true, overwriting does NOT fire dispose
const safeDisposed = [];
const noDisposeCache = new LRUCache({
max: 5,
noDisposeOnSet: true,
dispose: (value, key, reason) => safeDisposed.push({ key, value, reason }),
});
noDisposeCache.set('x', 10);
noDisposeCache.set('x', 20); // overwrite ignored by dispose
assert.equal(safeDisposed.length, 0);
noDisposeCache.delete('x'); // explicit delete still fires dispose
assert.deepEqual(safeDisposed.pop(), { key: 'x', value: 20, reason: 'delete' });
}
// --------------------------------------------------------------------------
// 3. fetchMethod Argument Shape, Context, and Coalescing
// --------------------------------------------------------------------------
{
let fetchInvocations = 0;
let capturedFetchArgs = null;
const cache = new LRUCache({
max: 5,
// Naive expectation: fetchMethod signature is (key, context) or (key, options)
// Reality: Signature is (key, staleValue, { options, signal, context })
fetchMethod: async (key, staleValue, details) => {
fetchInvocations++;
capturedFetchArgs = { key, staleValue, details };
return `fetched-${key}-${details.context?.reqId}`;
},
});
// Verify fetchMethod parameter shape and context forwarding
const fetchPromise1 = cache.fetch('user:100', { context: { reqId: 'abc-1' } });
const fetchPromise2 = cache.fetch('user:100', { context: { reqId: 'abc-2' } });
// Stampede protection: concurrent calls coalesce into ONE invocation of fetchMethod
const [res1, res2] = await Promise.all([fetchPromise1, fetchPromise2]);
assert.equal(fetchInvocations, 1);
assert.equal(res1, 'fetched-user:100-abc-1');
assert.equal(res2, 'fetched-user:100-abc-1');
// Verify 2nd param is staleValue (undefined for initial fetch) and 3rd param contains context
assert.equal(capturedFetchArgs.key, 'user:100');
assert.equal(capturedFetchArgs.staleValue, undefined);
assert.equal(capturedFetchArgs.details.context.reqId, 'abc-1');
assert.equal(typeof capturedFetchArgs.details.signal.aborted, 'boolean');
// Returning undefined from fetchMethod does NOT store key in cache
const cacheUndef = new LRUCache({
max: 5,
fetchMethod: async () => undefined,
});
const missingRes = await cacheUndef.fetch('nonexistent');
assert.equal(missingRes, undefined);
assert.equal(cacheUndef.has('nonexistent'), false);
assert.equal(cacheUndef.get('nonexistent'), undefined);
}
// --------------------------------------------------------------------------
// 4. Stale-While-Revalidate: fetch() immediate return vs forceRefresh
// --------------------------------------------------------------------------
{
virtualTime = 1000;
let revalidationCount = 0;
const cache = new LRUCache({
max: 10,
ttl: 100,
allowStale: true,
perf,
fetchMethod: async (key, staleVal) => {
revalidationCount++;
return `revalidated-${key}-v${revalidationCount}`;
},
});
// Initial populate
const initial = await cache.fetch('config');
assert.equal(initial, 'revalidated-config-v1');
assert.equal(revalidationCount, 1);
// Advance time past TTL
virtualTime = 1200;
// Naive expectation: await cache.fetch() waits for fetchMethod to produce the new value
// Reality: With allowStale: true, fetch() IMMEDIATELY resolves to the stale value
// and dispatches fetchMethod in the background!
const staleImmediate = await cache.fetch('config');
assert.equal(staleImmediate, 'revalidated-config-v1');
// Yield to microtask/event loop so the background fetchMethod settles
await new Promise((resolve) => setImmediate(resolve));
assert.equal(revalidationCount, 2);
assert.equal(cache.get('config'), 'revalidated-config-v2');
// To bypass stale-while-revalidate and wait for fresh data, forceRefresh + allowStale: false is required
const fresh = await cache.fetch('config', { forceRefresh: true, allowStale: false });
assert.equal(fresh, 'revalidated-config-v3');
assert.equal(revalidationCount, 3);
}
// --------------------------------------------------------------------------
// 5. Method Semantics: set() chaining, set(k, undefined) deletion, pop() & MRU iteration
// --------------------------------------------------------------------------
{
const cache = new LRUCache({ max: 3 });
// set() returns the cache instance (this), enabling fluent chaining
const chainResult = cache.set('a', 1).set('b', 2).set('c', 3);
assert.equal(chainResult, cache);
assert.equal(cache.size, 3);
// set(k, undefined) deletes the key instead of storing undefined
cache.set('a', undefined);
assert.equal(cache.has('a'), false);
assert.equal(cache.get('a'), undefined);
assert.equal(cache.size, 2);
// Recency and pop():
// Touch 'b' to make 'b' the Most Recently Used, making 'c' the Least Recently Used
cache.get('b');
// pop() evicts the LRU item ('c') and returns ONLY its value (3), NOT [key, value]
const poppedValue = cache.pop();
assert.equal(poppedValue, 3);
assert.equal(cache.has('c'), false);
// Iteration order:
// Add 'd' and 'e'
cache.set('d', 4);
cache.set('e', 5);
// Recency order: 'e' (newest / MRU), 'd', 'b' (oldest / LRU)
// Naive expectation: Iterators yield in FIFO / insertion order (like JavaScript Map: 'b', 'd', 'e')
// Reality: LRUCache iterators yield in MRU-to-LRU order (newest first: 'e', 'd', 'b')
const entryKeys = Array.from(cache.keys());
assert.deepEqual(entryKeys, ['e', 'd', 'b']);
const entryTuples = Array.from(cache.entries());
assert.deepEqual(entryTuples, [
['e', 5],
['d', 4],
['b', 2],
]);
}
console.log('Contract verified: all lru-cache API, callback signatures, fetchMethod lifecycle, and MRU iteration assertions passed.');