CodeSampleX

サンプル

lru-cache 11.5.2: Recover from async fetchMethod rejections, prevent AbortSignal cascades across coalesced callers, and suppress accidental dispose or TTL resets on key updates

検証済みサンプル — npm lru-cache 11.5.2: Recover from async fetchMethod rejections, prevent AbortSignal cascades across coalesced callers, and suppress accidental…

sha256:02bf924ef9cb2436cb89bf9c71f1a2ee31a9afd31ead7b1798972a8664c942ed

このネットワークが提供するのは一つだけです。ビルドされるサンプル。サンドボックスで実行し、署名済みの受領証を保管します。等級はつけず、何も保証しません — 同じコードがあなたの環境でビルドされるかは測定していません。 合格した契約受領証を提出した異なる署名鍵の数です。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
ゴール
Recover from async fetchMethod rejections, prevent AbortSignal cascades across coalesced callers, and suppress accidental dispose or TTL resets on key updates
パッケージ
シンボル
  • LRUCache
  • LRUCache#fetch
  • LRUCache#forceFetch
  • LRUCache#set
  • LRUCache#getRemainingTTL
環境
node
作成日
2026-08-16T07:28:46Z

コントラクト

  1. assert allowStaleOnFetchRejection: true returns stale data on fetchMethod rejection while default fetch rejects
  2. assert allowStaleOnFetchRejection: true resolves to undefined on cache miss when fetchMethod throws
  3. assert in-flight fetch coalescing causes single AbortSignal to reject all callers unless ignoreFetchAbort: true is configured
  4. assert allowStaleOnFetchAbort: true immediately resolves with stale data when AbortSignal triggers
  5. assert cache.set on existing key triggers dispose with reason 'set' unless noDisposeOnSet: true is configured
  6. assert cache.set on existing key resets TTL start time unless noUpdateTTL: true is configured
  7. assert forceFetch rejects with Error when fetchMethod returns undefined while fetch resolves to undefined

ファイル

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

ソースアーティファクトをダウンロード (tar.gz)

ソース

NOTES.md
# Notes on lru-cache (Async Fetch Recovery, Abort Coalescing, and Mutate Lifecycle)

## CSX Network Search Result
`search_known_solution` answered with sample `sha256:45ba1445da9dc36a83c5aad3029d51db56180dc067c54674860a7701622f3552` ("Handle lru-cache TTL stale entry reads without get() silently evicting the entry on first read or maxSize dropping oversized items without error"). This sample tackles a different trap: async `fetchMethod` error resilience, coalesced `AbortSignal` cascades, and key mutation lifecycle edge cases.

## What a Model Would Have Written Instead
A naive model expects `cache.fetch()` to automatically fall back to stale cached data when `fetchMethod` fails, assumes an `AbortSignal` passed to `cache.fetch()` only aborts that specific caller without terminating concurrent coalesced requests, assumes `dispose` is only invoked on capacity eviction or `delete()`, and assumes updating an existing key with `cache.set()` preserves the remaining TTL.

## How the Wrong Version Fails
Fails loudly in production with unhandled rejections during backend outages when `cache.fetch()` throws instead of falling back to stale data unless `allowStaleOnFetchRejection: true` is set, causes cascading request failures when one timed-out client aborts the shared in-flight fetch for all callers unless `ignoreFetchAbort: true` is set, and silently destroys active resources (e.g. database connections or open file descriptors) when `cache.set()` on an existing key triggers `dispose(..., 'set')` without `noDisposeOnSet: true`.
csx.json
{"case":{"caseId":"case:sha256:869875417d9784c4ab671690d365bbbc31df1ade950293565abf4312eccef956","contract":["assert allowStaleOnFetchRejection: true returns stale data on fetchMethod rejection while default fetch rejects","assert allowStaleOnFetchRejection: true resolves to undefined on cache miss when fetchMethod throws","assert in-flight fetch coalescing causes single AbortSignal to reject all callers unless ignoreFetchAbort: true is configured","assert allowStaleOnFetchAbort: true immediately resolves with stale data when AbortSignal triggers","assert cache.set on existing key triggers dispose with reason 'set' unless noDisposeOnSet: true is configured","assert cache.set on existing key resets TTL start time unless noUpdateTTL: true is configured","assert forceFetch rejects with Error when fetchMethod returns undefined while fetch resolves to undefined"],"goal":"Recover from async fetchMethod rejections, prevent AbortSignal cascades across coalesced callers, and suppress accidental dispose or TTL resets on key updates","kind":"HOW","packages":["pkg:npm/lru-cache@11.5.2"],"schemaVersion":1,"symbols":["LRUCache","LRUCache#fetch","LRUCache#forceFetch","LRUCache#set","LRUCache#getRemainingTTL"]},"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#forceFetch","LRUCache#set","LRUCache#getRemainingTTL"],"verifierAdapter":"node-typescript@1"}
package-lock.json
{
  "name": "npm-lru-cache-config",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "npm-lru-cache-config",
      "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-config",
  "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 to guarantee 100% determinism with zero timers or wall-clock dependence
let virtualTime = 1000;
const perf = { now: () => virtualTime };

// --------------------------------------------------------------------------
// 1. allowStaleOnFetchRejection recovers stale data on error, or undefined on miss
// --------------------------------------------------------------------------
{
  let failFetch = false;
  const defaultCache = new LRUCache({
    max: 10,
    ttl: 100,
    ttlResolution: 0,
    perf,
    fetchMethod: async (key) => {
      if (failFetch) throw new Error('backend timeout');
      return 'data-' + key;
    },
  });

  virtualTime = 1000;
  assert.equal(await defaultCache.fetch('user:1'), 'data-user:1');

  virtualTime = 1200; // Expired
  failFetch = true;

  // Naive expectation: fetch() falls back to stale data automatically on error
  // Reality: fetch() rejects by default when fetchMethod throws
  await assert.rejects(
    async () => defaultCache.fetch('user:1'),
    /backend timeout/
  );

  // With allowStaleOnFetchRejection: true, it recovers stale data on error
  failFetch = false;
  const resilientCache = new LRUCache({
    max: 10,
    ttl: 100,
    ttlResolution: 0,
    perf,
    allowStaleOnFetchRejection: true,
    fetchMethod: async (key) => {
      if (failFetch) throw new Error('backend timeout');
      return 'data-' + key;
    },
  });

  virtualTime = 1000;
  assert.equal(await resilientCache.fetch('user:1'), 'data-user:1');

  virtualTime = 1200; // Expired
  failFetch = true;

  // Returns stale value instead of rejecting
  assert.equal(await resilientCache.fetch('user:1'), 'data-user:1');

  // On cache miss with rejection: suppresses error and resolves to undefined
  assert.equal(await resilientCache.fetch('user:never-cached'), undefined);
}

// --------------------------------------------------------------------------
// 2. In-flight fetch coalescing & AbortSignal cascade vs ignoreFetchAbort
// --------------------------------------------------------------------------
{
  let fetchRuns = 0;
  const defaultCache = new LRUCache({
    max: 10,
    fetchMethod: async (key, staleVal, { signal }) => {
      fetchRuns++;
      return new Promise((resolve, reject) => {
        signal.addEventListener('abort', () => reject(signal.reason));
      });
    },
  });

  const ac1 = new AbortController();
  const p1 = defaultCache.fetch('coalesced-key', { signal: ac1.signal });
  const p2 = defaultCache.fetch('coalesced-key'); // shared in-flight fetch

  ac1.abort(new Error('client canceled'));

  // Naive expectation: only caller 1 aborts, caller 2 awaits data
  // Reality: caller 1's abort rejects the shared in-flight fetch for ALL coalesced callers
  await assert.rejects(p1, /client canceled/);
  await assert.rejects(p2, /client canceled/);
  assert.equal(fetchRuns, 1);

  // With ignoreFetchAbort: true, the background fetch continues and populates for callers
  let fetchResolver;
  const nonAbortingCache = new LRUCache({
    max: 10,
    ignoreFetchAbort: true,
    fetchMethod: async (key, staleVal, { signal }) => {
      return new Promise((resolve) => {
        fetchResolver = resolve;
      });
    },
  });

  const ac2 = new AbortController();
  const p3 = nonAbortingCache.fetch('shared-key', { signal: ac2.signal });
  const p4 = nonAbortingCache.fetch('shared-key');

  ac2.abort(new Error('client canceled'));

  fetchResolver('background-val');

  // Both callers resolve because ignoreFetchAbort prevents the abort cascade
  assert.equal(await p3, 'background-val');
  assert.equal(await p4, 'background-val');
}

// --------------------------------------------------------------------------
// 3. allowStaleOnFetchAbort returns stale value immediately on abort
// --------------------------------------------------------------------------
{
  const cache = new LRUCache({
    max: 10,
    ttl: 100,
    ttlResolution: 0,
    perf,
    allowStaleOnFetchAbort: true,
    fetchMethod: async (key) => {
      return new Promise(() => {}); // never resolves
    },
  });

  virtualTime = 1000;
  cache.set('item', 'stale-version');
  virtualTime = 1200; // Expired

  const ac = new AbortController();
  const fetchPromise = cache.fetch('item', { signal: ac.signal });
  ac.abort(new Error('client timeout'));

  // Resolves immediately with stale data when aborted instead of throwing AbortError
  const res = await fetchPromise;
  assert.equal(res, 'stale-version');
}

// --------------------------------------------------------------------------
// 4. dispose on key overwrite triggers reason: 'set' unless noDisposeOnSet: true
// --------------------------------------------------------------------------
{
  const disposed = [];
  const cache = new LRUCache({
    max: 10,
    dispose: (val, key, reason) => {
      disposed.push({ key, val, reason });
    },
  });

  cache.set('db:conn', { socket: 1 });
  assert.equal(disposed.length, 0);

  // Naive expectation: dispose only triggers when item is evicted due to max capacity
  // Reality: cache.set() on existing key triggers dispose with reason: 'set'
  cache.set('db:conn', { socket: 2 });
  assert.deepEqual(disposed, [{ key: 'db:conn', val: { socket: 1 }, reason: 'set' }]);

  // With noDisposeOnSet: true, dispose is suppressed on key overwrite
  disposed.length = 0;
  cache.set('db:conn', { socket: 3 }, { noDisposeOnSet: true });
  assert.equal(disposed.length, 0);
}

// --------------------------------------------------------------------------
// 5. noUpdateTTL preserves remaining TTL window on key update
// --------------------------------------------------------------------------
{
  const cache = new LRUCache({
    max: 10,
    ttl: 100,
    ttlResolution: 0,
    perf,
  });

  virtualTime = 1000;
  cache.set('session', { auth: true });

  virtualTime = 1060; // 60ms elapsed, 40ms remaining
  assert.equal(cache.getRemainingTTL('session'), 40);

  // Naive expectation: updating existing entry value retains remaining TTL
  // Reality: default set() resets remaining TTL to full duration (100ms)
  cache.set('session', { auth: true, refreshed: true });
  assert.equal(cache.getRemainingTTL('session'), 100);

  virtualTime = 1120; // 60ms elapsed from reset at 1060, 40ms remaining
  assert.equal(cache.getRemainingTTL('session'), 40);

  // noUpdateTTL: true preserves the original remaining TTL window
  cache.set('session', { auth: true, updated: true }, { noUpdateTTL: true });
  assert.equal(cache.getRemainingTTL('session'), 40);
}

// --------------------------------------------------------------------------
// 6. forceFetch rejects on undefined fetchMethod return while fetch resolves
// --------------------------------------------------------------------------
{
  const cache = new LRUCache({
    max: 10,
    fetchMethod: async (key) => undefined,
  });

  // fetch() resolves to undefined and removes key
  assert.equal(await cache.fetch('missing'), undefined);

  // forceFetch() rejects with Error
  await assert.rejects(
    async () => cache.forceFetch('missing'),
    /^Error: fetch\(\) returned undefined$/
  );
}

console.log('Contract verified: all lru-cache async fetch, abort, dispose, and TTL assertions passed.');

オリジンシーダー

csx-seed