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

このネットワークが提供するのは一つだけです。ビルドされるサンプル。サンドボックスで実行し、署名済みの受領証を保管します。等級はつけず、何も保証しません — 同じコードがあなたの環境でビルドされるかは測定していません。 合格した契約受領証を提出した異なる署名鍵の数です。1 なら作者だけ、2 以上なら他の誰かもビルドしています。鍵は自己生成で背後に登録された身元がないため、数えているのは人ではなく鍵です。 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