From 0bb5d01326a691393feb362bf9b50cae1e16c3d4 Mon Sep 17 00:00:00 2001 From: isaacs Date: Wed, 29 Jun 2022 15:41:45 -0700 Subject: [PATCH] add fetchContext option --- README.md | 16 ++++++++++++++-- index.d.ts | 30 ++++++++++++++++++++++-------- index.js | 16 +++++++++++++--- test/fetch.ts | 27 +++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 902c7838..c8441bcf 100644 --- a/README.md +++ b/README.md @@ -141,8 +141,8 @@ Deprecated alias: `length` ### `fetchMethod` Function that is used to make background asynchronous fetches. -Called with `fetchMethod(key, staleValue, { signal, options })`. -May return a Promise. +Called with `fetchMethod(key, staleValue, { signal, options, +context })`. May return a Promise. If `fetchMethod` is not provided, then `cache.fetch(key)` is equivalent to `Promise.resolve(cache.get(key))`. @@ -164,6 +164,18 @@ value is resolved. For example, a DNS cache may update the TTL based on the value returned from a remote DNS server by changing `options.ttl` in the `fetchMethod`. +### `fetchContext` + +Arbitrary data that can be passed to the `fetchMethod` as the +`context` option. + +Note that this will only be relevant when the `cache.fetch()` +call needs to call `fetchMethod()`. Thus, any data which will +meaningfully vary the fetch response needs to be present in the +key. This is primarily intended for including `x-request-id` +headers and the like for debugging purposes, which do not affect +the `fetchMethod()` response. + ### `noDeleteOnFetchRejection` If a `fetchMethod` throws an error or returns a rejected promise, diff --git a/index.d.ts b/index.d.ts index ef8435ab..b9375a8b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -496,6 +496,15 @@ declare namespace LRUCache { * @since 7.10.0 */ noDeleteOnFetchRejection?: boolean + + /** + * Set to any value in the constructor or fetch() options to + * pass arbitrary data to the fetch() method in the options.context + * field. + * + * @since 7.12.0 + */ + fetchContext?: any } type Options = SharedOptions & @@ -545,13 +554,7 @@ declare namespace LRUCache { allowStale?: boolean } - /** - * options which override the options set in the LRUCache constructor - * when making `cache.fetch()` calls. - * This is the union of GetOptions and SetOptions, plus the - * `noDeleteOnFetchRejection` boolean. - */ - interface FetchOptions { + interface FetcherFetchOptions { allowStale?: boolean updateAgeOnGet?: boolean noDeleteOnStaleGet?: boolean @@ -563,9 +566,20 @@ declare namespace LRUCache { noDeleteOnFetchRejection?: boolean } + /** + * options which override the options set in the LRUCache constructor + * when making `cache.fetch()` calls. + * This is the union of GetOptions and SetOptions, plus the + * `noDeleteOnFetchRejection` and `fetchContext` fields. + */ + interface FetchOptions extends FetcherFetchOptions { + fetchContext?: any + } + interface FetcherOptions { signal: AbortSignal - options: FetchOptions + options: FetcherFetchOptions + context: any } interface Entry { diff --git a/index.js b/index.js index ee2f207b..479ffc86 100644 --- a/index.js +++ b/index.js @@ -159,6 +159,7 @@ class LRUCache { maxSize = 0, sizeCalculation, fetchMethod, + fetchContext, noDeleteOnFetchRejection, noDeleteOnStaleGet, } = options @@ -198,6 +199,13 @@ class LRUCache { ) } + this.fetchContext = fetchContext + if (!this.fetchMethod && fetchContext !== undefined) { + throw new TypeError( + 'cannot set fetchContext without fetchMethod' + ) + } + this.keyMap = new Map() this.keyList = new Array(max).fill(null) this.valList = new Array(max).fill(null) @@ -676,7 +684,7 @@ class LRUCache { } } - backgroundFetch(k, index, options) { + backgroundFetch(k, index, options, context) { const v = index === undefined ? undefined : this.valList[index] if (this.isBackgroundFetch(v)) { return v @@ -685,6 +693,7 @@ class LRUCache { const fetchOpts = { signal: ac.signal, options, + context, } const cb = v => { if (!ac.signal.aborted) { @@ -753,6 +762,7 @@ class LRUCache { noUpdateTTL = this.noUpdateTTL, // fetch exclusive options noDeleteOnFetchRejection = this.noDeleteOnFetchRejection, + fetchContext = this.fetchContext, } = {} ) { if (!this.fetchMethod) { @@ -773,7 +783,7 @@ class LRUCache { let index = this.keyMap.get(k) if (index === undefined) { - const p = this.backgroundFetch(k, index, options) + const p = this.backgroundFetch(k, index, options, fetchContext) return (p.__returned = p) } else { // in cache, maybe already fetching @@ -794,7 +804,7 @@ class LRUCache { // ok, it is stale, and not already fetching // refresh the cache. - const p = this.backgroundFetch(k, index, options) + const p = this.backgroundFetch(k, index, options, fetchContext) return allowStale && p.__staleWhileFetching !== undefined ? p.__staleWhileFetching : (p.__returned = p) diff --git a/test/fetch.ts b/test/fetch.ts index fde92a8b..5ca8a82e 100644 --- a/test/fetch.ts +++ b/test/fetch.ts @@ -135,6 +135,10 @@ t.test('fetchMethod must be a function', async t => { t.throws(() => new LRU({ fetchMethod: true, max: 2 })) }) +t.test('no fetchContext without fetchMethod', async t => { + t.throws(() => new LRU({ fetchContext: true, max: 2 })) +}) + t.test('fetch without fetch method', async t => { const c = new LRU({ max: 3 }) c.set(0, 0) @@ -468,3 +472,26 @@ t.test( t.equal(e.valList[1], null, 'not in cache') } ) + +t.test('fetchContext', async t => { + const cache = new LRU({ + max: 10, + ttl: 10, + allowStale: true, + noDeleteOnFetchRejection: true, + fetchContext: 'default context', + fetchMethod: async (k, _, { context, options }) => { + //@ts-expect-error + t.equal(options.fetchContext, undefined) + t.equal(context, expectContext) + return [k, context] + }, + }) + + let expectContext = 'default context' + t.strictSame(await cache.fetch('x'), ['x', 'default context']) + expectContext = 'overridden' + t.strictSame(await cache.fetch('y', { fetchContext: 'overridden' }), ['y', 'overridden']) + // if still in cache, doesn't call fetchMethod again + t.strictSame(await cache.fetch('x', { fetchContext: 'ignored' }), ['x', 'default context']) +})