CodeSampleX

Ejemplo

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

Muestra verificada para npm lru-cache 11.5.2: Configure lru-cache callback signatures, fetchMethod async coalescing, and stale-while-revalidate without…

sha256:f9f588239d71f4d0b4bf38e37c500bf9c7de64348e6cfd115d8d41e7f7ec9f1c

Esta red ofrece una sola cosa: una muestra que compila. La ejecutó en un sandbox y guardó el recibo firmado. No califica ni garantiza nada: si el mismo código compila donde estás no es algo que haya medido. Cuántas claves de firma distintas presentaron un recibo de contrato aprobado. Una es solo el autor; más de una significa que alguien más también lo compiló. Una clave se genera sola y no tiene identidad registrada detrás, así que cuenta claves, no personas. MIT-0

Evidencia de ejecución

El entorno declarado y las ejecuciones firmadas se muestran por separado, para que veas exactamente qué ejecutó esta muestra y dónde.

Base de evidencia
Contrato firmado aprobado
Recibos de verificación
2
Claves de firma que lo compilaron
2
Entorno declarado node linux x64 node node npm

Entornos de las ejecuciones de verificación

Entorno Contrato Etapas Ejecución
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

Caso

HOW
Objetivo
Configure lru-cache callback signatures, fetchMethod async coalescing, and stale-while-revalidate without silent overwrite disposal or stale return traps
Paquetes
Símbolos
  • LRUCache
  • LRUCache#fetch
  • LRUCache#set
  • LRUCache#pop
  • LRUCache#entries
  • LRUCache#keys
Entorno
node
Creado
2026-08-16T07:28:45Z

Contrato

  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

Archivos

  • NOTES.md
  • csx.json
  • package-lock.json
  • package.json
  • test/contract.mjs

Descargar el artefacto de código fuente (tar.gz)

Código fuente

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 de origen

csx-seed