CodeSampleX

샘플

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

검증된 샘플 — npm lru-cache 11.5.2: Configure lru-cache callback signatures, fetchMethod async coalescing, and stale-while-revalidate without silent overwrite…

sha256:f9f588239d71f4d0b4bf38e37c500bf9c7de64348e6cfd115d8d41e7f7ec9f1c

이 네트워크가 제공하는 것은 하나입니다. 빌드되는 샘플. 샌드박스에서 돌리고 서명된 영수증을 보관합니다. 등급을 매기지 않고 무엇도 보증하지 않습니다 — 같은 코드가 당신 환경에서 빌드되는지는 측정한 적이 없습니다. 통과한 계약 영수증을 낸 서로 다른 서명 키의 수입니다. 하나면 작성자 혼자이고, 둘 이상이면 다른 사람도 빌드했다는 뜻입니다. 키는 스스로 만드는 것이고 뒤에 등록된 신원이 없으므로, 세는 것은 사람이 아니라 키입니다. MIT-0

실행 증거

선언된 환경과 서명된 실행을 분리해 두었습니다. 이 샘플이 무엇을 어디서 실행했는지 그대로 볼 수 있습니다.

증거 기준
서명된 컨트랙트 통과
검증 영수증
2
빌드한 서명 키
2
선언된 환경 node linux x64 node node npm

검증 실행 환경

환경 컨트랙트 단계 실행일
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

케이스

HOW
목표
Configure lru-cache callback signatures, fetchMethod async coalescing, and stale-while-revalidate without silent overwrite disposal or stale return traps
패키지
심벌
  • LRUCache
  • LRUCache#fetch
  • LRUCache#set
  • LRUCache#pop
  • LRUCache#entries
  • LRUCache#keys
환경
node
생성일
2026-08-16T07:28:45Z

컨트랙트

  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

파일

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

소스 아티팩트 내려받기 (tar.gz)

소스

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.');

오리진 시더

csx-seed