diff --git a/README.md b/README.md index 2778d5c..c3d0f20 100644 --- a/README.md +++ b/README.md @@ -264,8 +264,32 @@ Set to true to return a stale value from the cache when the event, whether user-triggered, or due to internal cache behavior. Unless `ignoreFetchAbort` is also set, the underlying -`fetchMethod` will still be considered canceled, and its return -value will be ignored and not cached. +`fetchMethod` will still be considered canceled, and any value +it returns will be ignored and not cached. + +Caveat: since fetches are aborted when a new value is explicitly +set in the cache, this can lead to fetch returning a stale value, +since that was the fallback value _at the moment the `fetch()` was +initiated_, even though the new updated value is now present in +the cache. + +For example: + +```ts +const cache = new LRUCache({ + ttl: 100, + fetchMethod: async (url, oldValue, { signal }) => { + const res = await fetch(url, { signal }) + return await res.json() + } +}) +cache.set('https://example.com/', { some: 'data' }) +// 100ms go by... +const result = cache.fetch('https://example.com/') +cache.set('https://example.com/', { other: 'thing' }) +console.log(await result) // { some: 'data' } +console.log(cache.get('https://example.com/')) // { other: 'thing' } +``` ### `ignoreFetchAbort` @@ -569,7 +593,7 @@ For the usage of the `status` option, see **Status Tracking** below. If the value is `undefined`, then this is an alias for -`cache.delete(key)`. `undefined` is never stored in the cache. +`cache.delete(key)`. `undefined` is never stored in the cache. See **Storing Undefined Values** below. ### `get(key, { updateAgeOnGet, allowStale, status } = {}) => value` @@ -1028,7 +1052,7 @@ internally in a few places to indicate that a key is not in the cache. You may call `cache.set(key, undefined)`, but this is just an -an alias for `cache.delete(key)`. Note that this has the effect +an alias for `cache.delete(key)`. Note that this has the effect that `cache.has(key)` will return _false_ after setting it to undefined. diff --git a/package-lock.json b/package-lock.json index 2d60267..7eb26de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "devDependencies": { "@size-limit/preset-small-lib": "^7.0.8", - "@types/node": "^17.0.31", + "@types/node": "^20.2.5", "@types/tap": "^15.0.6", "benchmark": "^2.1.4", "c8": "^7.11.2", @@ -1228,9 +1228,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "17.0.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", - "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "version": "20.2.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz", + "integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==", "dev": true }, "node_modules/@types/tap": { diff --git a/package.json b/package.json index bba5a70..9e926f2 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "repository": "git://github.com/isaacs/node-lru-cache.git", "devDependencies": { "@size-limit/preset-small-lib": "^7.0.8", - "@types/node": "^17.0.31", + "@types/node": "^20.2.5", "@types/tap": "^15.0.6", "benchmark": "^2.1.4", "c8": "^7.11.2", diff --git a/src/index.ts b/src/index.ts index 9060ffb..029c654 100644 --- a/src/index.ts +++ b/src/index.ts @@ -712,8 +712,32 @@ export namespace LRUCache { * event, whether user-triggered, or due to internal cache behavior. * * Unless {@link OptionsBase.ignoreFetchAbort} is also set, the underlying - * {@link OptionsBase.fetchMethod} will still be considered canceled, and its return - * value will be ignored and not cached. + * {@link OptionsBase.fetchMethod} will still be considered canceled, and + * any value it returns will be ignored and not cached. + * + * Caveat: since fetches are aborted when a new value is explicitly + * set in the cache, this can lead to fetch returning a stale value, + * since that was the fallback value _at the moment the `fetch()` was + * initiated_, even though the new updated value is now present in + * the cache. + * + * For example: + * + * ```ts + * const cache = new LRUCache({ + * ttl: 100, + * fetchMethod: async (url, oldValue, { signal }) => { + * const res = await fetch(url, { signal }) + * return await res.json() + * } + * }) + * cache.set('https://example.com/', { some: 'data' }) + * // 100ms go by... + * const result = cache.fetch('https://example.com/') + * cache.set('https://example.com/', { other: 'thing' }) + * console.log(await result) // { some: 'data' } + * console.log(cache.get('https://example.com/')) // { other: 'thing' } + * ``` */ allowStaleOnFetchAbort?: boolean diff --git a/test/fetch.ts b/test/fetch.ts index f02e715..b0fa33e 100644 --- a/test/fetch.ts +++ b/test/fetch.ts @@ -647,7 +647,8 @@ t.test('abort, but then keep on fetching anyway', async t => { return new Promise(res => setTimeout(() => { resolved = true - res(returnUndefined ? undefined : k) + if (returnUndefined) res() + else res(k) }, 100) ) }, @@ -706,7 +707,10 @@ t.test('allowStaleOnFetchAbort', async t => { fetchMethod: async (k, _, { signal }) => { return new Promise(res => { const t = setTimeout(() => res(k), 100) - signal.addEventListener('abort', () => clearTimeout(t)) + signal.addEventListener('abort', () => { + clearTimeout(t) + res() + }) }) }, }) @@ -716,7 +720,11 @@ t.test('allowStaleOnFetchAbort', async t => { const p = c.fetch(1, { signal: ac.signal }) ac.abort(new Error('gimme the stale value')) t.equal(await p, 10) - t.equal(c.get(1, { allowStale: true }), 10) + t.equal(c.get(1, { allowStale: true, noDeleteOnStaleGet: true }), 10) + const p2 = c.fetch(1) + c.set(1, 100) + t.equal(await p2, 10) + t.equal(c.get(1), 100) }) t.test('background update on timeout, return stale', async t => { diff --git a/tsconfig-base.json b/tsconfig-base.json index 71c24f2..4e8b9da 100644 --- a/tsconfig-base.json +++ b/tsconfig-base.json @@ -9,7 +9,6 @@ "isolatedModules": true, "moduleResolution": "node", "resolveJsonModule": true, - "skipLibCheck": true, "sourceMap": true, "inlineSources": true, "strict": true,