diff --git a/README.md b/README.md index bb1d4a43..8d3b72e9 100644 --- a/README.md +++ b/README.md @@ -508,14 +508,28 @@ can be confusing when setting values specifically to `undefined`, as in `cache.set(key, undefined)`. Use `cache.has()` to determine whether a key is present in the cache at all. -### `async fetch(key, { updateAgeOnGet, allowStale, size, sizeCalculation, ttl, noDisposeOnSet, forceRefresh } = {}) => Promise` +### `async fetch(key, options = {}) => Promise` + +The following options are supported: + +* `updateAgeOnGet` +* `allowStale` +* `size` +* `sizeCalculation` +* `ttl` +* `noDisposeOnSet` +* `forceRefresh` +* `signal` - AbortSignal can be used to cancel the `fetch()` +* `fetchContext` - sets the `context` option passed to the + underlying `fetchMethod`. If the value is in the cache and not stale, then the returned Promise resolves to the value. If not in the cache, or beyond its TTL staleness, then -`fetchMethod(key, staleValue, options)` is called, and the value -returned will be added to the cache once resolved. +`fetchMethod(key, staleValue, { options, signal, context })` is +called, and the value returned will be added to the cache once +resolved. If called with `allowStale`, and an asynchronous fetch is currently in progress to reload a stale value, then the former @@ -539,6 +553,15 @@ When the fetch method resolves to a value, if the fetch has not been aborted due to deletion, eviction, or being overwritten, then it is added to the cache using the options provided. +If the key is evicted or deleted before the `fetchMethod` +resolves, then the AbortSignal passed to the `fetchMethod` will +receive an `abort` event, and the promise returned by `fetch()` +will reject with the reason for the abort. + +If a `signal` is passed to the `fetch()` call, then aborting the +signal will abort the fetch and cause the `fetch()` promise to +reject with the reason provided. + ### `peek(key, { allowStale } = {}) => value` Like `get()` but doesn't update recency or delete stale items. diff --git a/index.d.ts b/index.d.ts index e8af7c4d..47c02bae 100644 --- a/index.d.ts +++ b/index.d.ts @@ -645,6 +645,7 @@ declare namespace LRUCache { interface FetchOptions extends FetcherFetchOptions { forceRefresh?: boolean fetchContext?: any + signal?: AbortSignal } interface FetcherOptions { diff --git a/index.js b/index.js index f4be3476..082fd10e 100644 --- a/index.js +++ b/index.js @@ -742,6 +742,11 @@ class LRUCache { return v } const ac = new AC() + if (options.signal) { + options.signal.addEventListener('abort', () => + ac.abort(options.signal.reason) + ) + } const fetchOpts = { signal: ac.signal, options, diff --git a/test/fetch.ts b/test/fetch.ts index 0defe7f4..93eac791 100644 --- a/test/fetch.ts +++ b/test/fetch.ts @@ -68,7 +68,7 @@ t.test('asynchronous fetching', async t => { t.matchSnapshot(JSON.stringify(dump), 'safe to stringify dump') t.equal(e.isBackgroundFetch(v), true) - t.equal(e.backgroundFetch('key', 0), v) + t.equal(e.backgroundFetch('key', 0, {}), v) await v const v7 = await c.fetch('key', { allowStale: true, @@ -626,3 +626,36 @@ t.test( t.equal([...c].length, 2) } ) + +t.test('send a signal', async t => { + let aborted: Error | undefined = undefined + let resolved: boolean = false + const c = new LRU({ + max: 10, + fetchMethod: async (k, _, { signal }) => { + signal.addEventListener('abort', () => { + aborted = signal.reason + }) + return new Promise(res => + setTimeout(() => { + resolved = true + res(k) + }, 100) + ) + }, + }) + const ac = new AbortController() + const p = c.fetch(1, { signal: ac.signal }) + const er = new Error('custom abort signal') + const testp = t.rejects(p, er) + ac.abort(er) + await testp + t.equal( + resolved, + false, + 'should have aborted before fetchMethod resolved' + ) + t.equal(aborted, er) + t.equal(ac.signal.reason, er) + t.equal(c.get(1), undefined) +}) diff --git a/test/fixtures/expose.ts b/test/fixtures/expose.ts index 345c87b4..f66e4b6d 100644 --- a/test/fixtures/expose.ts +++ b/test/fixtures/expose.ts @@ -8,7 +8,12 @@ export const exposeStatics = (LRU: typeof LRUCache) => { export const expose = (cache: LRUCache) => { return cache as unknown as { isBackgroundFetch: (v: any) => boolean - backgroundFetch: (v: any, index: number) => Promise + backgroundFetch: ( + v: any, + index: number, + options: { [k: string]: any }, + context?: any + ) => Promise valList: any[] keyList: any[] free: number[]