CodeSampleX

샘플

lru-cache 11.5.2: Handle lru-cache TTL stale entry reads without get() silently evicting the entry on first read or maxSize dropping oversized items without error

검증된 샘플 — npm lru-cache 11.5.2: Handle lru-cache TTL stale entry reads without get() silently evicting the entry on first read or maxSize dropping oversized…

sha256:45ba1445da9dc36a83c5aad3029d51db56180dc067c54674860a7701622f3552

이 네트워크가 제공하는 것은 하나입니다. 빌드되는 샘플. 샌드박스에서 돌리고 서명된 영수증을 보관합니다. 등급을 매기지 않고 무엇도 보증하지 않습니다 — 같은 코드가 당신 환경에서 빌드되는지는 측정한 적이 없습니다. 통과한 계약 영수증을 낸 서로 다른 서명 키의 수입니다. 하나면 작성자 혼자이고, 둘 이상이면 다른 사람도 빌드했다는 뜻입니다. 키는 스스로 만드는 것이고 뒤에 등록된 신원이 없으므로, 세는 것은 사람이 아니라 키입니다. 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-15
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
목표
Handle lru-cache TTL stale entry reads without get() silently evicting the entry on first read or maxSize dropping oversized items without error
패키지
심벌
  • LRUCache
  • LRUCache#get
  • LRUCache#has
  • LRUCache#set
  • LRUCache#peek
  • LRUCache#size
  • LRUCache#calculatedSize
환경
node
생성일
2026-08-15T11:02:30Z

컨트랙트

  1. assert allowStale: true alone returns stale value once and immediately deletes entry, requiring noDeleteOnStaleGet: true for repeated reads
  2. assert has() always returns false for expired keys even when allowStale is enabled
  3. assert updateAgeOnGet: true does not revive already-stale entries on get()
  4. assert cache.size retains expired entries until get() or purgeStale() while iterators skip them without purging
  5. assert maxSize without sizeCalculation throws TypeError on set, while oversized items are silently dropped without error

파일

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

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

소스

NOTES.md
# Notes on lru-cache (TTL, Stale Reads, and MaxSize Eviction)

## CSX Network Search Result
`search_known_solution` answered with a MISS for `lru-cache` on npm/Node (returned low-confidence unrelated samples from other ecosystems: Elixir `NimbleOptions`/`NimblePool` and Ruby stdlib).

## What a Model Would Have Written Instead
A naive model would configure `{ ttl, allowStale: true }` expecting stale entries to remain readable across multiple calls, rely on `cache.has(key)` to guard `cache.get(key)`, expect `updateAgeOnGet: true` to revive stale entries upon retrieval, and assume `cache.set()` with `maxSize` either throws on oversized items or defaults entry size to 1 without a custom calculator.

## How the Wrong Version Fails
Fails silently in production with unexpected data loss and cache misses:
- `allowStale: true` alone returns the stale value on the first read and immediately deletes the entry from cache, causing subsequent `cache.get()` calls to silently return `undefined` unless `noDeleteOnStaleGet: true` is explicitly configured.
- `cache.has(key)` silently returns `false` for expired entries even when `allowStale: true` is enabled and `cache.get(key)` would return the stale value.
- When `maxSize` is configured, adding an item larger than `maxSize` silently returns the cache instance without throwing or evicting older items, but discards the entry so `cache.get()` immediately returns `undefined`.
- Calling `cache.set()` with `maxSize` and no `sizeCalculation` or item `size` fails loudly with `TypeError: invalid size value`.
csx.json
{"case":{"caseId":"case:sha256:07b0c1d43071da997b4f3c35b622239d288ad4b7f20cdb95d67da437ec83cdee","constraints":{"runtime":"node"},"contract":["assert allowStale: true alone returns stale value once and immediately deletes entry, requiring noDeleteOnStaleGet: true for repeated reads","assert has() always returns false for expired keys even when allowStale is enabled","assert updateAgeOnGet: true does not revive already-stale entries on get()","assert cache.size retains expired entries until get() or purgeStale() while iterators skip them without purging","assert maxSize without sizeCalculation throws TypeError on set, while oversized items are silently dropped without error"],"goal":"Handle lru-cache TTL stale entry reads without get() silently evicting the entry on first read or maxSize dropping oversized items without error","kind":"HOW","packages":["pkg:npm/lru-cache@11.5.2"],"schemaVersion":1,"symbols":["LRUCache","LRUCache#get","LRUCache#has","LRUCache#set","LRUCache#peek","LRUCache#size","LRUCache#calculatedSize"]},"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#get","LRUCache#has","LRUCache#set","LRUCache#peek","LRUCache#size","LRUCache#calculatedSize"],"verifierAdapter":"node-typescript@1"}
package-lock.json
{
  "name": "npm-lru-cache-ttl",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "npm-lru-cache-ttl",
      "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-ttl",
  "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. allowStale alone deletes on first get(); requires noDeleteOnStaleGet
// --------------------------------------------------------------------------
{
  const cache = new LRUCache({
    max: 10,
    ttl: 100,
    allowStale: true,
    perf,
  });

  virtualTime = 1000;
  cache.set('session', 'data-1');
  virtualTime = 1200; // TTL expired

  // Naive expectation: allowStale keeps returning the cached value across gets
  // Reality: get() returns the stale value ONCE, then immediately deletes it
  assert.equal(cache.get('session'), 'data-1');
  assert.equal(cache.get('session'), undefined);

  // Reality: has() ALWAYS returns false for stale keys, even with allowStale: true
  assert.equal(cache.has('session'), false);
}

{
  // To preserve stale entries across multiple reads, noDeleteOnStaleGet is required
  const cache = new LRUCache({
    max: 10,
    ttl: 100,
    allowStale: true,
    noDeleteOnStaleGet: true,
    perf,
  });

  virtualTime = 1000;
  cache.set('session', 'data-2');
  virtualTime = 1200; // TTL expired

  assert.equal(cache.get('session'), 'data-2');
  assert.equal(cache.get('session'), 'data-2');
  // has() is still false despite repeated successful get() reads
  assert.equal(cache.has('session'), false);
}

// --------------------------------------------------------------------------
// 2. updateAgeOnGet does NOT revive or preserve already-stale entries
// --------------------------------------------------------------------------
{
  const cache = new LRUCache({
    max: 10,
    ttl: 100,
    allowStale: true,
    updateAgeOnGet: true,
    perf,
  });

  virtualTime = 1000;
  cache.set('token', 'abc');
  virtualTime = 1200; // Stale

  // Naive expectation: updateAgeOnGet refreshes the entry's timestamp on access
  // Reality: Stale get still triggers immediate deletion; entry is NOT revived
  assert.equal(cache.get('token'), 'abc');
  assert.equal(cache.get('token'), undefined);
}

// --------------------------------------------------------------------------
// 3. Lazy TTL retention: size vs iteration vs peek vs get
// --------------------------------------------------------------------------
{
  const cache = new LRUCache({
    max: 10,
    ttl: 100,
    perf,
  });

  virtualTime = 1000;
  cache.set('k1', 'v1');
  cache.set('k2', 'v2');
  virtualTime = 1200; // Both expired

  // Naive expectation: cache.size reflects active non-expired keys (0)
  // Reality: size remains 2 because expiration is lazy
  assert.equal(cache.size, 2);

  // Iterators omit stale keys, but DO NOT purge them from storage
  assert.deepEqual(Array.from(cache.keys()), []);
  assert.equal(cache.size, 2);

  // peek() returns undefined without allowStale, but does NOT purge
  assert.equal(cache.peek('k1'), undefined);
  assert.equal(cache.peek('k1', { allowStale: true }), 'v1');
  assert.equal(cache.size, 2);

  // get() with default allowStale: false returns undefined AND deletes entry
  assert.equal(cache.get('k1'), undefined);
  assert.equal(cache.size, 1);
}

// --------------------------------------------------------------------------
// 4. maxSize constraints: missing sizeCalculation throws, oversized silently dropped
// --------------------------------------------------------------------------
{
  // maxSize without sizeCalculation throws TypeError on set()
  const cacheNoCalc = new LRUCache({ maxSize: 50, perf });
  assert.throws(
    () => cacheNoCalc.set('k', 'v'),
    /When maxSize or maxEntrySize is used, sizeCalculation or size must be set/
  );

  const cache = new LRUCache({
    maxSize: 10,
    sizeCalculation: (val) => val.length,
    perf,
  });

  cache.set('fit', '12345'); // size 5
  assert.equal(cache.get('fit'), '12345');
  assert.equal(cache.calculatedSize, 5);

  // Naive expectation: Oversized item either evicts older items or throws
  // Reality: set() returns cache instance without throwing, but item is dropped silently
  const ret = cache.set('oversized', '123456789012345'); // size 15 > maxSize 10
  assert.equal(ret, cache);
  assert.equal(cache.get('oversized'), undefined);
  assert.equal(cache.get('fit'), '12345'); // previous entry intact
  assert.equal(cache.calculatedSize, 5);
}

console.log('Contract verified: all lru-cache TTL, stale get, and maxSize assertions passed.');

오리진 시더

anonymous