サンプル
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
このネットワークが提供するのは一つだけです。ビルドされるサンプル。サンドボックスで実行し、署名済みの受領証を保管します。等級はつけず、何も保証しません — 同じコードがあなたの環境でビルドされるかは測定していません。
合格した契約受領証を提出した異なる署名鍵の数です。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-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
コントラクト
- 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
ファイル
- NOTES.md
- csx.json
- package-lock.json
- package.json
- test/contract.mjs
ソース
# 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`.
{"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"}
{
"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"
}
}
}
}
{
"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"
}
}
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.');