Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: validate prefetch-sw messages #6942

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lucky-snails-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@builder.io/qwik': patch
---

Prefetch service worker is now robust against unknown messages
46 changes: 23 additions & 23 deletions packages/qwik/src/prefetch-service-worker/index.unit.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { setupServiceWorker } from './setup';
import { expect, describe, it, vi } from 'vitest';
import { createState, type SWStateBase, type SWTask } from './state';
import { processMessage } from './process-message';
import { MsgType, processMessage } from './process-message';
import { addDependencies, directFetch } from './direct-fetch';
import { delay } from '../core/util/promises';

Expand All @@ -24,12 +24,12 @@ describe('service-worker', async () => {
});
});

describe('graph', async () => {
describe(MsgType.Graph, async () => {
const singleGraph = createGraph([['a.js']]);
const graph = createGraph([['a.js', 'b.js', 'c.js'], ['b.js', 'c.js'], ['c.js']]);
it('load single', async () => {
const swState = mockSwState();
await processMessage(swState, ['graph', '/base/', ...singleGraph]);
await processMessage(swState, [MsgType.Graph, '/base/', ...singleGraph]);
expect(swState.$bases$.length).toBe(1);
expect(swState.$bases$[0].$path$).toBe('/base/');
expect(swState.$bases$[0].$graph$).toEqual(singleGraph);
Expand All @@ -38,7 +38,7 @@ describe('service-worker', async () => {

it('load many', async () => {
const swState = mockSwState();
await processMessage(swState, ['graph', '/base/', ...graph]);
await processMessage(swState, [MsgType.Graph, '/base/', ...graph]);
expect(swState.$bases$.length).toBe(1);
expect(swState.$bases$[0].$path$).toBe('/base/');
expect(swState.$bases$[0].$graph$).toEqual(graph);
Expand All @@ -47,8 +47,8 @@ describe('service-worker', async () => {
it('load same base replaces previous', async () => {
const differentGraph = createGraph([['a.js']]);
const swState = mockSwState();
await processMessage(swState, ['graph', '/base/', ...graph]);
await processMessage(swState, ['graph', '/base/', ...differentGraph]);
await processMessage(swState, [MsgType.Graph, '/base/', ...graph]);
await processMessage(swState, [MsgType.Graph, '/base/', ...differentGraph]);
expect(swState.$bases$.length).toBe(1);
expect(swState.$bases$[0].$path$).toBe('/base/');
expect(swState.$bases$[0].$graph$).toEqual(differentGraph);
Expand All @@ -57,7 +57,7 @@ describe('service-worker', async () => {
it('should load graph from network', async () => {
const swState = mockSwState();
const graph = createGraph([['a.js', 'b.js'], ['b.js']]);
const p = processMessage(swState, ['graph-url', '/base/', 'q-graph.json']);
const p = processMessage(swState, [MsgType.GraphURL, '/base/', 'q-graph.json']);
await delay(0);
swState.$fetch$.mock.get('/base/q-graph.json')!.resolve(new Response(JSON.stringify(graph)));
await p;
Expand Down Expand Up @@ -164,7 +164,7 @@ describe('service-worker', async () => {

it('should intercept requests inside base', async () => {
const swState = mockSwState();
await processMessage(swState, ['graph', '/base/']);
await processMessage(swState, [MsgType.Graph, '/base/']);
const responsePromise = directFetch(swState, new URL('http://server/base/unknown.js'));
await delay(0);
swState.$fetch$.mock.get('/base/unknown.js')!.resolve(new Response('RESPONSE'));
Expand All @@ -185,7 +185,7 @@ describe('service-worker', async () => {

it('should not add non 200 response to cache', async () => {
const swState = mockSwState();
await processMessage(swState, ['graph', '/base/']);
await processMessage(swState, [MsgType.Graph, '/base/']);
const responsePromise = directFetch(swState, new URL('http://server/base/unknown.js'));
await delay(0);
swState.$fetch$.mock
Expand All @@ -199,7 +199,7 @@ describe('service-worker', async () => {
it('should cache response', async () => {
const swState = mockSwState();
swState.$put$('/base/abc.js', new Response('RESPONSE'));
await processMessage(swState, ['graph', '/base/', 'abc.js']);
await processMessage(swState, [MsgType.Graph, '/base/', 'abc.js']);
const response = await directFetch(swState, new URL('http://server/base/abc.js'));
expect(response).not.toBeUndefined();
expect(response!.status).toBe(200);
Expand All @@ -209,7 +209,7 @@ describe('service-worker', async () => {
it('should add dependencies to cache', async () => {
const swState = mockSwState();
await processMessage(swState, [
'graph',
MsgType.Graph,
'/base/',
...createGraph([['abc.js', 'def.js'], ['def.js']]),
]);
Expand All @@ -227,8 +227,8 @@ describe('service-worker', async () => {
it('should not have more than X concurrent prefetch requests', async () => {
const swState = mockSwState();
swState.$maxPrefetchRequests$ = 1;
await processMessage(swState, ['graph', '/base/']);
await processMessage(swState, ['prefetch', '/base/', 'a.js', 'b.js', 'c.js']);
await processMessage(swState, [MsgType.Graph, '/base/']);
await processMessage(swState, [MsgType.Prefetch, '/base/', 'a.js', 'b.js', 'c.js']);
await delay(0);
expect(swState.$queue$.length).toBe(3);
expect(swState.$queue$.filter((t) => t.$isFetching$).length).toBe(1);
Expand All @@ -247,8 +247,8 @@ describe('service-worker', async () => {
it('should put direct request at the front of the queue', async () => {
const swState = mockSwState();
swState.$maxPrefetchRequests$ = 1;
await processMessage(swState, ['graph', '/base/']);
await processMessage(swState, ['prefetch', '/base/', 'a.js', 'b.js']);
await processMessage(swState, [MsgType.Graph, '/base/']);
await processMessage(swState, [MsgType.Prefetch, '/base/', 'a.js', 'b.js']);
await delay(0);
expect(swState.$queue$.length).toBe(2);
expect(swState.$queue$.filter((t) => t.$isFetching$).length).toBe(1);
Expand All @@ -274,11 +274,11 @@ describe('service-worker', async () => {
const swState = mockSwState();
swState.$maxPrefetchRequests$ = 1;
await processMessage(swState, [
'graph',
MsgType.Graph,
'/base/',
...createGraph([['a.js', 'b.js'], ['b.js'], ['c.js']]),
]);
await processMessage(swState, ['prefetch', '/base/', 'a.js', 'b.js', 'c.js']);
await processMessage(swState, [MsgType.Prefetch, '/base/', 'a.js', 'b.js', 'c.js']);
await delay(0);
expect(swState.$queue$.filter(areFetching).map(getPathname)).toEqual(['/base/a.js']);
directFetch(swState, new URL('http://server/base/a.js'));
Expand All @@ -303,7 +303,7 @@ describe('service-worker', async () => {
it('should respond from cache', async () => {
const swState = mockSwState();
swState.mockCache.mock.set('/base/abc.js', new Response('RESPONSE'));
await processMessage(swState, ['graph', '/base/', 'abc.js']);
await processMessage(swState, [MsgType.Graph, '/base/', 'abc.js']);
const response = await directFetch(swState, new URL('http://server/base/abc.js'));
expect(response!.status).toBe(200);
expect(await response?.text()).toEqual('RESPONSE');
Expand All @@ -312,8 +312,8 @@ describe('service-worker', async () => {
it('should populate cache from prefetch', async () => {
const swState = mockSwState();
const graph = createGraph([['a.js', 'b.js'], ['b.js', 'c.js'], ['c.js']]);
await processMessage(swState, ['graph', '/base/', ...graph]);
await processMessage(swState, ['prefetch', '/base/', 'a.js']);
await processMessage(swState, [MsgType.Graph, '/base/', ...graph]);
await processMessage(swState, [MsgType.Prefetch, '/base/', 'a.js']);
await delay(0);
swState.$fetch$.mock.get('/base/a.js')!.resolve(new Response('A'));
swState.$fetch$.mock.get('/base/b.js')!.resolve(new Response('B'));
Expand All @@ -330,11 +330,11 @@ describe('service-worker', async () => {
const swState = mockSwState();
swState.mockCache.mock.set('/base/a.js', new Response('A'));
await processMessage(swState, [
'graph',
MsgType.Graph,
'/base/',
...createGraph([['a.js', 'b.js'], ['b.js']]),
]);
await processMessage(swState, ['prefetch', '/base/', 'a.js']);
await processMessage(swState, [MsgType.Prefetch, '/base/', 'a.js']);
await delay(0);
expect(swState.$queue$.length).toBe(1);
expect(swState.$queue$.filter(areFetching).map(getPathname)).toEqual(['/base/b.js']);
Expand All @@ -345,7 +345,7 @@ describe('service-worker', async () => {
const swState = mockSwState();
swState.mockCache.mock.set('/base/a.js', new Response('A'));
swState.mockCache.mock.set('/base/b.js', new Response('B'));
await processMessage(swState, ['graph', '/base/', ...createGraph([['b.js']])]);
await processMessage(swState, [MsgType.Graph, '/base/', ...createGraph([['b.js']])]);
expect(Array.from(swState.mockCache.mock.keys())).toEqual(['/base/b.js']);
});
});
Expand Down
31 changes: 19 additions & 12 deletions packages/qwik/src/prefetch-service-worker/process-message.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { directFetch, enqueueFileAndDependencies, parseBaseFilename } from './direct-fetch';
import type { SWState } from './state';

export enum MsgType {
Graph = 'graph',
GraphURL = 'graph-url',
Prefetch = 'prefetch',
PrefetchAll = 'prefetch-all',
Ping = 'ping',
Verbose = 'verbose',
}

/**
* Initialize the service worker with a bundle graph.
*
Expand All @@ -9,7 +18,7 @@ import type { SWState } from './state';
*/
export type SWMsgBundleGraph = [
/// Message type.
'graph',
MsgType.Graph,
/// Base URL for the bundles
string,
...SWGraph,
Expand All @@ -22,7 +31,7 @@ export type SWMsgBundleGraph = [
*/
export type SWMsgBundleGraphUrl = [
/// Message type.
'graph-url',
MsgType.GraphURL,
/// Base URL for the bundles
string,
/// relative URL to the bundle graph.
Expand All @@ -39,7 +48,7 @@ export type SWGraph = Array<string | number>;

export type SWMsgPrefetch = [
/// Message type.
'prefetch',
MsgType.Prefetch,
/// Base URL for the bundles
string,
/// List of bundles to prefetch.
Expand All @@ -48,7 +57,7 @@ export type SWMsgPrefetch = [

export type SWMsgPrefetchAll = [
/// Message type.
'prefetch-all',
MsgType.PrefetchAll,
/// Base URL for the bundles
string,
];
Expand All @@ -63,22 +72,20 @@ export const log = (...args: any[]) => {
export const processMessage = async (state: SWState, msg: SWMessages) => {
const type = msg[0];
state.$log$('received message:', type, msg[1], msg.slice(2));
if (type === 'graph') {
if (type === MsgType.Graph) {
await processBundleGraph(state, msg[1], msg.slice(2), true);
} else if (type === 'graph-url') {
} else if (type === MsgType.GraphURL) {
await processBundleGraphUrl(state, msg[1], msg[2]);
} else if (type === 'prefetch') {
} else if (type === MsgType.Prefetch) {
await processPrefetch(state, msg[1], msg.slice(2));
} else if (type === 'prefetch-all') {
} else if (type === MsgType.PrefetchAll) {
await processPrefetchAll(state, msg[1]);
} else if (type === 'ping') {
} else if (type === MsgType.Ping) {
// eslint-disable-next-line no-console
log('ping');
} else if (type === 'verbose') {
} else if (type === MsgType.Verbose) {
// eslint-disable-next-line no-console
(state.$log$ = log)('mode: verbose');
} else {
console.error('UNKNOWN MESSAGE:', msg);
}
};

Expand Down
12 changes: 9 additions & 3 deletions packages/qwik/src/prefetch-service-worker/setup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { directFetch } from './direct-fetch';
import { drainMsgQueue } from './process-message';
import { MsgType, drainMsgQueue, type SWMessages } from './process-message';
import { createState, type SWState } from './state';

export const setupServiceWorker = (swScope: ServiceWorkerGlobalScope) => {
Expand All @@ -25,8 +25,10 @@ export const setupServiceWorker = (swScope: ServiceWorkerGlobalScope) => {
}
});
swScope.addEventListener('message', (ev) => {
swState.$msgQueue$.push(ev.data);
drainMsgQueue(swState);
if (isQwikMessages(ev.data)) {
swState.$msgQueue$.push(ev.data);
drainMsgQueue(swState);
}
});
swScope.addEventListener('install', () => {
swScope.skipWaiting();
Expand All @@ -46,3 +48,7 @@ export const setupServiceWorker = (swScope: ServiceWorkerGlobalScope) => {
event.waitUntil(swScope.clients.claim());
});
};

function isQwikMessages(msg: any): msg is SWMessages {
return msg[0] && Object.values(MsgType).includes(msg[0][0]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

every message that comes in you generate an array of all the values in the enum. How about renaming the enum values so they are lowercase and you can do !!MsgType[msg[0][0]]?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes you’re right. Hmm, the tricky part is that some of the enum values can’t be valid enum keys, like graph-url. We could generate the array once outside of the function. I’m not sure what a better solution could be, what do you think?

}
Loading