diff --git a/packages/playground/remote/src/lib/offline-mode-cache.spec.ts b/packages/playground/remote/src/lib/offline-mode-cache.spec.ts new file mode 100644 index 00000000000..1de7b7c55d4 --- /dev/null +++ b/packages/playground/remote/src/lib/offline-mode-cache.spec.ts @@ -0,0 +1,47 @@ +describe('offline mode cache', () => { + beforeEach(() => { + vi.resetModules(); + vi.stubGlobal('self', {}); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it.each([ + ['cache first', 'cacheFirstFetch'], + ['network first', 'networkFirstFetch'], + ] as const)( + 'settles failed background writes for the %s strategy', + async (_strategy, fetchFunctionName) => { + const cacheWriteError = new DOMException( + 'Cache.put() encountered a network error', + 'NetworkError' + ); + const cacheWriteCatch = vi.fn((onRejected) => + Promise.resolve(onRejected(cacheWriteError)) + ); + const cachePut = vi.fn().mockReturnValue({ + catch: cacheWriteCatch, + }); + vi.stubGlobal('caches', { + open: vi.fn().mockResolvedValue({ + match: vi.fn().mockResolvedValue(undefined), + put: cachePut, + }), + }); + const response = new Response('response body'); + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(response)); + const offlineModeCache = await import('./offline-mode-cache'); + + await expect( + offlineModeCache[fetchFunctionName]( + new Request('https://playground.wordpress.net/asset.js') + ) + ).resolves.toBe(response); + + expect(cachePut).toHaveBeenCalledOnce(); + expect(cacheWriteCatch).toHaveBeenCalledOnce(); + } + ); +}); diff --git a/packages/playground/remote/src/lib/offline-mode-cache.ts b/packages/playground/remote/src/lib/offline-mode-cache.ts index 069c365f692..9ad1466ffb1 100644 --- a/packages/playground/remote/src/lib/offline-mode-cache.ts +++ b/packages/playground/remote/src/lib/offline-mode-cache.ts @@ -48,8 +48,12 @@ export async function cacheFirstFetch(request: Request): Promise { ) { // Intentionally do not await writing to the cache so the response // promise can be returned immediately and observed for progress events. + // A failed write only reduces offline availability, so settle it here + // instead of surfacing an unhandled rejection after the response returns. // NOTE: This is a race condition for simultaneous requests for the same asset. - offlineModeCache.put(requestWithoutRangeHeader, response.clone()); + void offlineModeCache + .put(requestWithoutRangeHeader, response.clone()) + .catch(() => undefined); } } @@ -91,8 +95,12 @@ export async function networkFirstFetch(request: Request): Promise { if (response.ok) { // Intentionally do not await writing to the cache so the response // promise can be returned immediately and observed for progress events. + // A failed write only reduces offline availability, so settle it here + // instead of surfacing an unhandled rejection after the response returns. // NOTE: This is a race condition for simultaneous requests for the same asset. - offlineModeCache.put(request, response.clone()); + void offlineModeCache + .put(request, response.clone()) + .catch(() => undefined); return response; }