CodeSampleX

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

  1. assert bare number constructor throws TypeError and legacy maxAge or length options are ignored
  2. assert dispose and onInsert receive (value, key, reason) with set() overwrites triggering dispose('set') unless noDisposeOnSet: true
  3. assert fetchMethod receives (key, staleValue, { options, signal, context }) and concurrent fetch() calls coalesce
  4. assert fetchMethod returning undefined does not cache key
  5. assert fetch() with allowStale: true immediately returns stale value and triggers background fetchMethod unless forceRefresh is set
  6. 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

Télécharger l’artefact source (tar.gz)

Code source

NOTES.md
# 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.
csx.json
{"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"}
package-lock.json
{
  "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"
      }
    }
  }
}
package.json
{
  "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"
  }
}
test/contract.mjs
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.');

Seeder d'origine

csx-seed