mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
fix(sw): leave rendered HTML pages to the network
The fetch handler skipped only /api/, so a popped-out /html-view/ page (a top-level navigation the worker intercepts) was put into CacheStorage despite its `Cache-Control: no-store`, and the offline fallback could serve it after logout had revoked the capability. /html-view/ is now skipped like /api/. The new test runs the shipped sw.js against a stub worker scope and drives its real fetch listener. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
06f1e5ddc8
commit
7dd78d9d71
2 files changed
+61
No files matched your search
@@ -101,6 +101,10 @@ self.addEventListener('fetch', (event) => {
|
||||
if (request.headers.get('upgrade') === 'websocket') return;
|
||||
if (request.headers.get('accept') === 'text/event-stream') return;
|
||||
if (request.url.includes('/api/')) return;
|
||||
// A rendered HTML page is served `no-store` behind a capability that logout
|
||||
// revokes; caching a popped-out page here would let the offline fallback
|
||||
// serve it after the capability is gone.
|
||||
if (request.url.includes('/html-view/')) return;
|
||||
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @fileoverview Which requests the service worker leaves to the network.
|
||||
*
|
||||
* The fetch handler caches every OK GET it answers, and the offline fallback
|
||||
* serves from that cache. A rendered HTML page (`/html-view/<cap>/...`) is
|
||||
* served `no-store` behind a capability that logout revokes, so a popped-out
|
||||
* page must never land there: it is skipped like `/api/`.
|
||||
*
|
||||
* Runs the shipped sw.js in a `vm` context with a stub ServiceWorkerGlobalScope
|
||||
* and drives its real fetch listener.
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const swJs = readFileSync(resolve(import.meta.dirname, '../src/web/public/sw.js'), 'utf8');
|
||||
|
||||
function loadFetchListener(): (event: unknown) => void {
|
||||
const listeners = new Map<string, (event: unknown) => void>();
|
||||
const self = {
|
||||
location: { pathname: '/sw.js', origin: 'http://localhost' },
|
||||
addEventListener: (type: string, fn: (event: unknown) => void) => listeners.set(type, fn),
|
||||
};
|
||||
// A fetch that never settles: the listener only has to decide whether to answer.
|
||||
const fetch = vi.fn(() => new Promise(() => {}));
|
||||
vm.runInContext(swJs, vm.createContext({ self, console, caches: {}, fetch, URL }), { filename: 'sw.js' });
|
||||
const listener = listeners.get('fetch');
|
||||
expect(listener, 'sw.js registers a fetch listener').toBeDefined();
|
||||
return listener!;
|
||||
}
|
||||
|
||||
function fetchEvent(path: string) {
|
||||
return {
|
||||
request: { method: 'GET', url: `http://localhost${path}`, headers: { get: () => null } },
|
||||
respondWith: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe('service worker fetch skip list', () => {
|
||||
it('leaves rendered HTML pages and the API to the network', () => {
|
||||
const onFetch = loadFetchListener();
|
||||
for (const path of ['/html-view/AAAAAAAAAAAAAAAAAAAAAAAA/index.html', '/api/status']) {
|
||||
const event = fetchEvent(path);
|
||||
onFetch(event);
|
||||
expect(event.respondWith, path).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('still answers an app asset', () => {
|
||||
const onFetch = loadFetchListener();
|
||||
const event = fetchEvent('/app.js');
|
||||
onFetch(event);
|
||||
expect(event.respondWith).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user