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 表示只有作者;大于 1 表示还有其他人构建过。密钥是自行生成的,背后没有注册身份,因此计的是密钥而非人。 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