From a07cb72106c8cac0f162e2379caea2ba415975cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B3=96=E9=A5=BC?= Date: Wed, 18 Sep 2024 23:53:38 +0800 Subject: [PATCH] feat: added `ignoreVary` option --- .changeset/hungry-wombats-clean.md | 5 + src/cache-key.test.ts | 172 +++++++-------------- src/cache-key.ts | 15 +- src/cache.ts | 233 ++++++++++++++++++----------- src/fetch.ts | 39 ++--- src/types.ts | 44 +++--- 6 files changed, 247 insertions(+), 261 deletions(-) create mode 100644 .changeset/hungry-wombats-clean.md diff --git a/.changeset/hungry-wombats-clean.md b/.changeset/hungry-wombats-clean.md new file mode 100644 index 0000000..fe33855 --- /dev/null +++ b/.changeset/hungry-wombats-clean.md @@ -0,0 +1,5 @@ +--- +"@web-widget/shared-cache": minor +--- + +Added `ignoreVary` option. diff --git a/src/cache-key.test.ts b/src/cache-key.test.ts index c06c8de..18587fb 100644 --- a/src/cache-key.test.ts +++ b/src/cache-key.test.ts @@ -8,11 +8,9 @@ import { test('base: host + pathname + search', async () => { const keyGenerator = createCacheKeyGenerator(); const key = await keyGenerator(new Request('http://localhost/?a=1'), { - cacheKeyRules: { - host: true, - pathname: true, - search: true, - }, + host: true, + pathname: true, + search: true, }); expect(key).toBe('localhost/?a=1'); }); @@ -30,16 +28,14 @@ test('should support built-in rules', async () => { }, }), { - cacheKeyRules: { - cookie: true, - device: true, - header: { - include: ['x-id'], - }, - host: true, - pathname: true, - search: true, + cookie: true, + device: true, + header: { + include: ['x-id'], }, + host: true, + pathname: true, + search: true, } ); expect(key).toBe('localhost/?a=1#a=356a19:desktop:x-id=a9993e'); @@ -55,14 +51,12 @@ test('should support filtering', async () => { }, }), { - cacheKeyRules: { - host: { - include: ['localhost'], - }, - pathname: true, - search: { include: ['a'] }, - header: { include: ['x-id'] }, + host: { + include: ['localhost'], }, + pathname: true, + search: { include: ['a'] }, + header: { include: ['x-id'] }, } ); expect(key).toBe('localhost/?a=1#x-id=a9993e'); @@ -71,11 +65,9 @@ test('should support filtering', async () => { test('should support presence or absence without including its actual value', async () => { const keyGenerator = createCacheKeyGenerator(); const key = await keyGenerator(new Request('http://localhost/?a=1&b=2'), { - cacheKeyRules: { - host: true, - pathname: true, - search: { include: ['a', 'b'], checkPresence: ['a'] }, - }, + host: true, + pathname: true, + search: { include: ['a', 'b'], checkPresence: ['a'] }, }); expect(key).toBe('localhost/?a&b=2'); }); @@ -84,11 +76,9 @@ describe('should support cacheName', () => { test('"default" value should be overridden to empty', async () => { const keyGenerator = createCacheKeyGenerator('default'); const key = await keyGenerator(new Request('http://localhost/?a=1&b=2'), { - cacheKeyRules: { - host: true, - pathname: true, - search: { include: ['a', 'b'], checkPresence: ['a'] }, - }, + host: true, + pathname: true, + search: { include: ['a', 'b'], checkPresence: ['a'] }, }); expect(key).toBe('localhost/?a&b=2'); }); @@ -96,11 +86,9 @@ describe('should support cacheName', () => { test('cacheName should appear in the prefix', async () => { const keyGenerator = createCacheKeyGenerator('custom'); const key = await keyGenerator(new Request('http://localhost/?a=1&b=2'), { - cacheKeyRules: { - host: true, - pathname: true, - search: { include: ['a', 'b'], checkPresence: ['a'] }, - }, + host: true, + pathname: true, + search: { include: ['a', 'b'], checkPresence: ['a'] }, }); expect(key).toBe('custom/localhost/?a&b=2'); }); @@ -116,9 +104,7 @@ describe('should support cookie', () => { }, }), { - cacheKeyRules: { - cookie: true, - }, + cookie: true, } ); expect(key).toBe('#a=aaf4c6'); @@ -133,9 +119,7 @@ describe('should support cookie', () => { }, }), { - cacheKeyRules: { - cookie: true, - }, + cookie: true, } ); expect(key).toBe('#a=356a19&b=da4b92&c=77de68'); @@ -150,9 +134,7 @@ describe('should support cookie', () => { }, }), { - cacheKeyRules: { - cookie: { include: ['a'] }, - }, + cookie: { include: ['a'] }, } ) ).toBe('#a=356a19'); @@ -165,9 +147,7 @@ describe('should support cookie', () => { }, }), { - cacheKeyRules: { - cookie: { exclude: ['a'] }, - }, + cookie: { exclude: ['a'] }, } ) ).toBe('#b=da4b92&c=77de68'); @@ -182,9 +162,7 @@ describe('should support cookie', () => { }, }), { - cacheKeyRules: { - cookie: { include: ['a', 'b', 'c'], checkPresence: ['a'] }, - }, + cookie: { include: ['a', 'b', 'c'], checkPresence: ['a'] }, } ); expect(key).toBe('#a&b=da4b92&c=77de68'); @@ -195,9 +173,7 @@ describe('should support device', () => { test('default device type', async () => { const keyGenerator = createCacheKeyGenerator(); const key = await keyGenerator(new Request('http://localhost/'), { - cacheKeyRules: { - device: true, - }, + device: true, }); expect(key).toBe('#desktop'); }); @@ -212,9 +188,7 @@ describe('should support device', () => { }, }), { - cacheKeyRules: { - device: true, - }, + device: true, } ); expect(key).toBe('#desktop'); @@ -230,9 +204,7 @@ describe('should support device', () => { }, }), { - cacheKeyRules: { - device: true, - }, + device: true, } ); expect(key).toBe('#mobile'); @@ -248,9 +220,7 @@ describe('should support device', () => { }, }), { - cacheKeyRules: { - device: true, - }, + device: true, } ); expect(key).toBe('#tablet'); @@ -267,9 +237,7 @@ describe('should support header', () => { }, }), { - cacheKeyRules: { - header: true, - }, + header: true, } ); expect(key).toBe('#a=aaf4c6'); @@ -286,9 +254,7 @@ describe('should support header', () => { }, }), { - cacheKeyRules: { - header: true, - }, + header: true, } ); expect(key).toBe('#a=356a19&b=da4b92&c=77de68'); @@ -305,9 +271,7 @@ describe('should support header', () => { }, }), { - cacheKeyRules: { - header: { include: ['a'] }, - }, + header: { include: ['a'] }, } ) ).toBe('#a=356a19'); @@ -322,9 +286,7 @@ describe('should support header', () => { }, }), { - cacheKeyRules: { - header: { exclude: ['a'] }, - }, + header: { exclude: ['a'] }, } ) ).toBe('#b=da4b92&c=77de68'); @@ -341,9 +303,7 @@ describe('should support header', () => { }, }), { - cacheKeyRules: { - header: { include: ['a', 'b', 'c'], checkPresence: ['a'] }, - }, + header: { include: ['a', 'b', 'c'], checkPresence: ['a'] }, } ); expect(key).toBe('#a&b=da4b92&c=77de68'); @@ -359,9 +319,7 @@ describe('should support header', () => { }, }), { - cacheKeyRules: { - header: true, - }, + header: true, } ); expect(key).toBe('#a=ca9fd0&x-id=a9993e'); @@ -377,9 +335,7 @@ describe('should support header', () => { }, }), { - cacheKeyRules: { - header: { include: [key] }, - }, + header: { include: [key] }, } ) ).rejects.toThrow(`Cannot include header: ${key}`); @@ -391,9 +347,7 @@ describe('should support host', () => { test('basic', async () => { const keyGenerator = createCacheKeyGenerator(); const key = await keyGenerator(new Request('http://localhost/'), { - cacheKeyRules: { - host: true, - }, + host: true, }); expect(key).toBe('localhost'); }); @@ -401,9 +355,7 @@ describe('should support host', () => { test('should support filtering', async () => { const keyGenerator = createCacheKeyGenerator(); const key = await keyGenerator(new Request('http://localhost:8080/'), { - cacheKeyRules: { - host: { include: ['localhost'] }, - }, + host: { include: ['localhost'] }, }); expect(key).toBe(''); }); @@ -413,9 +365,7 @@ describe('should support pathname', () => { test('basic', async () => { const keyGenerator = createCacheKeyGenerator(); const key = await keyGenerator(new Request('http://localhost/a/b/c'), { - cacheKeyRules: { - pathname: true, - }, + pathname: true, }); expect(key).toBe('/a/b/c'); }); @@ -423,9 +373,7 @@ describe('should support pathname', () => { test('should support filtering', async () => { const keyGenerator = createCacheKeyGenerator(); const key = await keyGenerator(new Request('http://localhost:8080/a/b/c'), { - cacheKeyRules: { - pathname: { include: ['/a/b/c'] }, - }, + pathname: { include: ['/a/b/c'] }, }); expect(key).toBe('/a/b/c'); }); @@ -437,9 +385,7 @@ describe('should support search', () => { const key = await keyGenerator( new Request('http://localhost/?b=2&a=1&c=3'), { - cacheKeyRules: { - search: true, - }, + search: true, } ); expect(key).toBe('?a=1&b=2&c=3'); @@ -448,9 +394,7 @@ describe('should support search', () => { test('question marks should not be generated if there are no query parameters', async () => { const keyGenerator = createCacheKeyGenerator(); const key = await keyGenerator(new Request('http://localhost/'), { - cacheKeyRules: { - search: true, - }, + search: true, }); expect(key).toBe(''); }); @@ -460,9 +404,7 @@ describe('should support search', () => { const key = await keyGenerator( new Request('http://localhost/?a=1&b=2&c=3'), { - cacheKeyRules: { - search: { include: ['a'] }, - }, + search: { include: ['a'] }, } ); expect(key).toBe('?a=1'); @@ -473,9 +415,7 @@ describe('should support search', () => { const key = await keyGenerator( new Request('http://localhost/?a=1&b=2&c=3'), { - cacheKeyRules: { - search: { include: ['a', 'b', 'c'], checkPresence: ['a'] }, - }, + search: { include: ['a', 'b', 'c'], checkPresence: ['a'] }, } ); expect(key).toBe('?a&b=2&c=3'); @@ -494,9 +434,7 @@ describe('should support custom key', () => { }, }), { - cacheKeyRules: { - foo: true, - }, + foo: true, } ); expect(key).toBe('#custom'); @@ -506,9 +444,7 @@ describe('should support custom key', () => { const keyGenerator = createCacheKeyGenerator(); await expect(() => keyGenerator(new Request('http://localhost/'), { - cacheKeyRules: { - foo: true, - }, + foo: true, }) ).rejects.toThrow('Unknown custom part: "foo".'); }); @@ -524,11 +460,9 @@ describe('should support custom key', () => { }, }), { - cacheKeyRules: { - foo: true, - header: { - include: ['x-id'], - }, + foo: true, + header: { + include: ['x-id'], }, } ); diff --git a/src/cache-key.ts b/src/cache-key.ts index 2563444..548f93d 100644 --- a/src/cache-key.ts +++ b/src/cache-key.ts @@ -218,14 +218,8 @@ export function createCacheKeyGenerator( ) { return async function cacheKeyGenerator( request: Request, - options: { - cacheKeyRules?: SharedCacheKeyRules; - } & CacheQueryOptions = {} + cacheKeyRules: SharedCacheKeyRules = DEFAULT_CACHE_KEY_RULES ): Promise { - notImplemented(options, 'ignoreVary'); - notImplemented(options, 'ignoreSearch'); - - const { cacheKeyRules = DEFAULT_CACHE_KEY_RULES } = options; const { host, pathname, search, ...fragmentRules } = cacheKeyRules; const prefix = cacheName ? cacheName === 'default' @@ -282,10 +276,3 @@ export function createCacheKeyGenerator( : `${prefix}${urlPart.join('')}`; }; } - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function notImplemented(options: any, name: string) { - if (name in options) { - throw new Error(`Not implemented: "${name}" option.`); - } -} diff --git a/src/cache.ts b/src/cache.ts index b34c711..1755f5a 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -8,8 +8,12 @@ import type { SharedCacheStatus, Logger, } from './types'; -import { createCacheKeyGenerator, vary as getVary } from './cache-key'; -import type { SharedCacheKeyRules, FilterOptions } from './cache-key'; +import { + createCacheKeyGenerator, + DEFAULT_CACHE_KEY_RULES, + vary as getVary, +} from './cache-key'; +import type { FilterOptions } from './cache-key'; import { CACHE_STATUS_HEADERS_NAME, EXPIRED, @@ -19,11 +23,7 @@ import { } from './constants'; export class SharedCache implements Cache { - #cacheKeyGenerator: ( - request: Request, - options?: SharedCacheQueryOptions - ) => Promise; - #cacheKeyRules?: SharedCacheKeyRules; + #cacheKeyGenerator: (request: Request) => Promise; #fetch?: typeof fetch; #logger?: Logger; #storage: KVStorage; @@ -41,11 +41,16 @@ export class SharedCache implements Cache { ...options, }; - this.#cacheKeyGenerator = createCacheKeyGenerator( + const cacheKeyGenerator = createCacheKeyGenerator( resolveOptions._cacheName, resolveOptions.cacheKeyPartDefiners ); - this.#cacheKeyRules = resolveOptions.cacheKeyRules; + this.#cacheKeyGenerator = async (request) => + cacheKeyGenerator(request, { + ...DEFAULT_CACHE_KEY_RULES, + ...resolveOptions.cacheKeyRules, + ...request.sharedCache?.cacheKeyRules, + }); this.#fetch = resolveOptions.fetch; this.#logger = resolveOptions.logger; this.#storage = storage; @@ -75,7 +80,7 @@ export class SharedCache implements Cache { */ async delete( request: RequestInfo, - options?: SharedCacheQueryOptions + options?: CacheQueryOptions ): Promise { // 1. let r: Request | null = null; @@ -96,18 +101,21 @@ export class SharedCache implements Cache { r = r!; - const cacheKey = await this.#cacheKeyGenerator(r, { - cacheKeyRules: this.#cacheKeyRules, - ...options, - }); + this.#verifyCacheQueryOptions(options); + const cacheKey = await this.#cacheKeyGenerator(r); - return deleteCacheItem(r, this.#storage, cacheKey); + return deleteCacheItem( + r, + this.#storage, + cacheKey, + options?.ignoreVary ?? false + ); } /** @private */ async keys( _request?: RequestInfo, - _options?: SharedCacheQueryOptions + _options?: CacheQueryOptions ): Promise { throw new Error('Not implemented.'); } @@ -124,7 +132,7 @@ export class SharedCache implements Cache { */ async match( request: RequestInfo, - options?: SharedCacheQueryOptions + options?: CacheQueryOptions ): Promise { // 1. let r: Request | null = null; @@ -147,11 +155,15 @@ export class SharedCache implements Cache { r = r!; - const cacheKey = await this.#cacheKeyGenerator(r, { - cacheKeyRules: this.#cacheKeyRules, - ...options, - }); - const cacheItem = await getCacheItem(r, this.#storage, cacheKey); + this.#verifyCacheQueryOptions(options); + const cacheKey = await this.#cacheKeyGenerator(r); + const ignoreVary = options?.ignoreVary ?? false; + const cacheItem = await getCacheItem( + r, + this.#storage, + cacheKey, + ignoreVary + ); if (!cacheItem) { return; @@ -171,7 +183,7 @@ export class SharedCache implements Cache { if ( !policy.satisfiesWithoutRevalidation(r, { - ignoreRequestCacheControl: options?.ignoreRequestCacheControl, + ignoreRequestCacheControl: options?._ignoreRequestCacheControl, ignoreMethod: true, ignoreSearch: true, ignoreVary: true, @@ -190,6 +202,7 @@ export class SharedCache implements Cache { response: response.clone(), policy, }, + ignoreVary, cacheKey, fetch, options @@ -204,6 +217,7 @@ export class SharedCache implements Cache { response, policy, }, + ignoreVary, cacheKey, fetch, options @@ -218,7 +232,7 @@ export class SharedCache implements Cache { /** @private */ async matchAll( _request?: RequestInfo, - _options?: SharedCacheQueryOptions + _options?: CacheQueryOptions ): Promise { throw new Error('Not implemented.'); } @@ -228,14 +242,9 @@ export class SharedCache implements Cache { * to the current Cache object. * @param request The Request object or URL that you want to add to the cache. * @param response The Response you want to match up to the request. - * @param options An object that sets options for the put operation. */ - async put( - request: RequestInfo, - response: Response, - options?: SharedCacheQueryOptions - ): Promise { - return this.#putWithCustomCacheKey(request, response, options).catch( + async put(request: RequestInfo, response: Response): Promise { + return this.#putWithCustomCacheKey(request, response, false).catch( (error) => { this.#logger?.error('Cache.put: Failed to cache response.', { url: request instanceof Request ? request.url : request, @@ -249,6 +258,7 @@ export class SharedCache implements Cache { async #putWithCustomCacheKey( request: RequestInfo, response: Response, + ignoreVary: boolean, cacheKey?: string | SharedCacheQueryOptions ): Promise { // 1. @@ -324,10 +334,7 @@ export class SharedCache implements Cache { }; if (typeof cacheKey !== 'string') { - cacheKey = await this.#cacheKeyGenerator(innerRequest, { - cacheKeyRules: this.#cacheKeyRules, - ...cacheKey, - }); + cacheKey = await this.#cacheKeyGenerator(innerRequest); } await setCacheItem( @@ -336,20 +343,22 @@ export class SharedCache implements Cache { cacheItem, ttl, innerRequest, - clonedResponse + clonedResponse, + ignoreVary ); } async #revalidate( request: Request, resolveCacheItem: PolicyResponse, + ignoreVary: boolean, cacheKey: string, fetch: typeof globalThis.fetch, options: SharedCacheQueryOptions | undefined ): Promise { const revalidationRequest = new Request(request, { headers: resolveCacheItem.policy.revalidationHeaders(request, { - ignoreRequestCacheControl: options?.ignoreRequestCacheControl, + ignoreRequestCacheControl: options?._ignoreRequestCacheControl, ignoreMethod: true, ignoreSearch: true, ignoreVary: true, @@ -385,7 +394,7 @@ export class SharedCache implements Cache { ? revalidationResponse : resolveCacheItem.response; - await this.#putWithCustomCacheKey(request, response, cacheKey); + await this.#putWithCustomCacheKey(request, response, ignoreVary, cacheKey); const clonedResponse = new Response(response.body, { status: response.status, @@ -405,42 +414,50 @@ export class SharedCache implements Cache { #setCacheStatus(response: Response, status: SharedCacheStatus) { response.headers.set(CACHE_STATUS_HEADERS_NAME, status); } + + #verifyCacheQueryOptions(options: CacheQueryOptions | undefined) { + if (options) { + if ('ignoreSearch' in options) { + throw new Error(`Not implemented: "ignoreSearch" option.`); + } + } + } } async function getCacheItem( request: Request, storage: KVStorage, - customCacheKey: string + customCacheKey: string, + ignoreVary: boolean ): Promise { - const varyKey = getVaryCacheKey(customCacheKey); - const varyFilterOptions = (await storage.get(varyKey)) as - | FilterOptions - | undefined; - const varyPart = varyFilterOptions - ? await getVary(request, varyFilterOptions) - : undefined; - const cacheKey = varyPart ? `${customCacheKey}:${varyPart}` : customCacheKey; - const cacheItem = (await storage.get(cacheKey)) as CacheItem | undefined; - return cacheItem; + let cacheKey = customCacheKey; + + if (!ignoreVary) { + cacheKey = await getEffectiveCacheKey(request, storage, customCacheKey); + } + + return (await storage.get(cacheKey)) as CacheItem | undefined; } async function deleteCacheItem( request: Request, storage: KVStorage, - customCacheKey: string + customCacheKey: string, + ignoreVary: boolean ): Promise { - const varyKey = getVaryCacheKey(customCacheKey); - const varyFilterOptions = (await storage.get(varyKey)) as - | FilterOptions - | undefined; - const varyPart = varyFilterOptions - ? await getVary(request, varyFilterOptions) - : undefined; - const cacheKey = varyPart ? `${customCacheKey}:${varyPart}` : customCacheKey; - - return varyFilterOptions - ? (await storage.delete(varyKey)) && (await storage.delete(cacheKey)) - : storage.delete(cacheKey); + let cacheKey = customCacheKey; + + if (!ignoreVary) { + cacheKey = await getEffectiveCacheKey(request, storage, customCacheKey); + } + + if (cacheKey === customCacheKey) { + return storage.delete(cacheKey); + } else { + return ( + (await storage.delete(cacheKey)) && (await storage.delete(customCacheKey)) + ); + } } async function setCacheItem( @@ -449,46 +466,84 @@ async function setCacheItem( cacheItem: CacheItem, ttl: number, request: Request, - response: Response + response: Response, + ignoreVary: boolean ): Promise { - const vary = response.headers.get('vary'); - if (vary) { - const varyKey = getVaryCacheKey(customCacheKey); - const varyFilterOptions: FilterOptions | undefined = - vary === '*' - ? undefined - : { include: vary.split(',').map((field) => field.trim()) }; - const varyPart = await getVary(request, varyFilterOptions); - const cacheKey = `${customCacheKey}:${varyPart}`; - await storage.set(varyKey, varyFilterOptions, ttl); - await storage.set(cacheKey, cacheItem, ttl); - } else { - await storage.set(customCacheKey, cacheItem, ttl); + let cacheKey = customCacheKey; + if (!ignoreVary) { + const vary = response.headers.get('vary'); + const varyFilterOptions = await getAndSaveVaryFilterOptions( + storage, + customCacheKey, + ttl, + vary + ); + cacheKey = await getVaryCacheKey( + request, + customCacheKey, + varyFilterOptions + ); + } + + await storage.set(cacheKey, cacheItem, ttl); +} + +async function getEffectiveCacheKey( + request: Request, + storage: KVStorage, + customCacheKey: string +): Promise { + const varyFilterOptions = await getVaryFilterOptions(storage, customCacheKey); + return getVaryCacheKey(request, customCacheKey, varyFilterOptions); +} + +async function getVaryFilterOptions( + storage: KVStorage, + customCacheKey: string +): Promise { + const varyKey = `${customCacheKey}:vary`; + return (await storage.get(varyKey)) as FilterOptions | undefined; +} + +async function getAndSaveVaryFilterOptions( + storage: KVStorage, + customCacheKey: string, + ttl: number, + vary: string | null +): Promise { + if (!vary || vary === '*') { + return; } + const varyKey = `${customCacheKey}:vary`; + const varyFilterOptions: FilterOptions = { + include: vary.split(',').map((field) => field.trim()), + }; + await storage.set(varyKey, varyFilterOptions, ttl); + return varyFilterOptions; } -function getVaryCacheKey(customCacheKey: string) { - return `${customCacheKey}:vary`; +async function getVaryCacheKey( + request: Request, + customCacheKey: string, + varyFilterOptions: FilterOptions | undefined +): Promise { + if (!varyFilterOptions) { + return customCacheKey; + } + const varyPart = await getVary(request, varyFilterOptions); + return varyPart ? `${customCacheKey}:${varyPart}` : customCacheKey; } /** * @see https://fetch.spec.whatwg.org/#http-scheme */ -function urlIsHttpHttpsScheme(url: string) { +function urlIsHttpHttpsScheme(url: string): boolean { return /^https?:/.test(url); } /** * @see https://github.com/chromium/chromium/blob/694d20d134cb553d8d89e5500b9148012b1ba299/content/browser/cache_storage/cache_storage_cache.cc#L260-L262 */ -function getFieldValues(header: string) { - const values = []; - - for (let value of header.split(',')) { - value = value.trim(); - - values.push(value); - } - - return values; +function getFieldValues(header: string): string[] { + return header.split(',').map((value) => value.trim()); } diff --git a/src/fetch.ts b/src/fetch.ts index 32e6a02..a0718f0 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -8,11 +8,7 @@ import { HIT, MISS, } from './constants'; -import { - SharedCacheStatus, - SharedCacheFetch, - SharedCacheRequestInitProperties, -} from './types'; +import { SharedCacheStatus, SharedCacheFetch } from './types'; const ORIGINAL_FETCH = globalThis.fetch; @@ -38,20 +34,28 @@ export function createSharedCacheFetch( const request = new Request(input, init); const requestCache = getRequestCacheMode(request, init?.cache); - const sharedCache = init?.sharedCache; + const sharedCacheOptions = (request.sharedCache = { + ...request.sharedCache, + ...init?.sharedCache, + }); + + const interceptor = createInterceptor( + fetcher, + sharedCacheOptions.cacheControlOverride, + sharedCacheOptions.varyOverride + ); const ignoreRequestCacheControl = - sharedCache?.ignoreRequestCacheControl ?? true; - const interceptor = createInterceptor(fetcher, sharedCache); + sharedCacheOptions?.ignoreRequestCacheControl ?? true; if (requestCache && requestCache !== 'default') { throw new Error(`Not implemented: "cache" option.`); } const cachedResponse = await cache.match(request, { - ...sharedCache, - _fetch: interceptor, ignoreMethod: request.method === 'HEAD', - ignoreRequestCacheControl, + ignoreVary: sharedCacheOptions?.ignoreVary, + _ignoreRequestCacheControl: ignoreRequestCacheControl, + _fetch: interceptor, }); if (cachedResponse) { @@ -66,7 +70,7 @@ export function createSharedCacheFetch( if (bypassCache(cacheControl)) { setCacheStatus(fetchedResponse, BYPASS); } else { - const ok = await cache.put(request, fetchedResponse, sharedCache).then( + const ok = await cache.put(request, fetchedResponse).then( () => true, () => false ); @@ -91,17 +95,18 @@ function setCacheStatus(response: Response, status: SharedCacheStatus) { function createInterceptor( fetcher: typeof fetch, - sharedCache?: SharedCacheRequestInitProperties + cacheControlOverride: string | undefined, + varyOverride: string | undefined ): typeof fetch { return async function fetch(...args) { const response = await fetcher(...args); const headers = response.headers; if (response.ok) { - if (sharedCache?.cacheControlOverride) { - cacheControl(headers, sharedCache.cacheControlOverride); + if (cacheControlOverride) { + cacheControl(headers, cacheControlOverride); } - if (sharedCache?.varyOverride) { - vary(headers, sharedCache.varyOverride); + if (varyOverride) { + vary(headers, varyOverride); } } return response; diff --git a/src/types.ts b/src/types.ts index 93ecd7c..d2a4ca3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -69,29 +69,29 @@ export type SharedCacheStatus = | 'REVALIDATED' | 'DYNAMIC'; -export type SharedCacheQueryOptions = { - cacheKeyRules?: SharedCacheKeyRules; - ignoreRequestCacheControl?: boolean; - ignoreMethod?: boolean; - /** @private */ - ignoreSearch?: never; - /** @private */ - ignoreVary?: never; - /** - * Method to initiate a request after cache expiration. - * @private - */ - _fetch?: typeof fetch; -} & CacheQueryOptions; +export type SharedCacheQueryOptions = CacheQueryOptions; -export type SharedCacheFetch = ( - input: RequestInfo | URL, - init?: { - sharedCache?: SharedCacheRequestInitProperties; - } & RequestInit -) => Promise; +export type SharedCacheFetch = typeof fetch; -export type SharedCacheRequestInitProperties = { +export interface SharedCacheRequestInitProperties { cacheControlOverride?: string; + cacheKeyRules?: SharedCacheKeyRules; + ignoreRequestCacheControl?: boolean; + ignoreVary?: boolean; varyOverride?: string; -} & SharedCacheQueryOptions; +} + +declare global { + interface Request { + sharedCache?: SharedCacheRequestInitProperties; + } + interface RequestInit { + sharedCache?: SharedCacheRequestInitProperties; + } + interface CacheQueryOptions { + /** @private */ + _ignoreRequestCacheControl?: boolean; + /** @private */ + _fetch?: typeof fetch; + } +}