Skip to content

Commit 03b80fb

Browse files
authored
Fix validPathPrefixes unloading in serivce worker resulting in ccmod files failing to fetch (#26)
* Fix validPathPrefixes unloading in serivce worker resulting in ccmod files failing to fetch * Add comment about ccmod unzip times * Small service worker changes that will hopefully make it more consistent
1 parent b503b45 commit 03b80fb

3 files changed

Lines changed: 134 additions & 82 deletions

File tree

packages/core/src/files.ccmod.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type Unzipped, unzipSync } from 'fflate/browser';
2-
import { addFetchHandler } from './service-worker-bridge';
2+
import { setFetchHandler } from './service-worker-bridge';
33
import * as files from './files';
44

55
const fileMap = new Map<string, Unzipped>();
@@ -62,6 +62,8 @@ export async function loadCCMods(ccmods: string[]): Promise<void> {
6262
}),
6363
);
6464

65+
// from my limited testing unzipSync is about 50ms faster (unzipSync takes 150ms)
66+
// compared to asynchronous unzip ¯\_(ツ)_/¯
6567
// console.time('uncompress');
6668
const uncompressed = ccmodArrayBuffers.map((buf) => unzipSync(buf));
6769
// console.timeEnd('uncompress');
@@ -72,5 +74,5 @@ export async function loadCCMods(ccmods: string[]): Promise<void> {
7274
fileMap.set(modDir, buf);
7375
}
7476

75-
addFetchHandler(ccmods, readFile);
77+
setFetchHandler(ccmods, readFile);
7678
}

packages/core/src/service-worker-bridge.ts

Lines changed: 70 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,72 @@
1+
export namespace ServiceWorker {
2+
// Messages send to the service worker
3+
export namespace Outgoing {
4+
export interface ValidPathPrefixesPacket {
5+
type: 'ValidPathPrefixes';
6+
validPathPrefixes: string[];
7+
}
8+
9+
export interface DataPacket {
10+
type: 'Data';
11+
path: string;
12+
data: BodyInit | null;
13+
}
14+
15+
export type Packet = ValidPathPrefixesPacket | DataPacket;
16+
}
17+
// Messages coming from the service worker
18+
export namespace Incoming {
19+
export interface PathPacket {
20+
type: 'Path';
21+
path: string;
22+
}
23+
export interface ValidPathPrefixesRequestPacket {
24+
type: 'ValidPathPrefixesRequest';
25+
}
26+
27+
export type Packet = PathPacket | ValidPathPrefixesRequestPacket;
28+
}
29+
}
30+
31+
export type FetchHandler = (path: string) => Promise<ArrayBufferLike | null>;
32+
33+
let fetchHandler: FetchHandler | undefined;
34+
let validPathPrefixes: string[] = [];
35+
36+
function sendServiceWorkerMessage(packet: ServiceWorker.Outgoing.Packet): void {
37+
const { controller } = window.navigator.serviceWorker;
38+
controller?.postMessage(packet);
39+
}
40+
41+
export function setFetchHandler(pathPrefixes: string[], handler: FetchHandler): void {
42+
fetchHandler = handler;
43+
validPathPrefixes = pathPrefixes.map((path) => `/${path}/`);
44+
sendServiceWorkerMessage({ type: 'ValidPathPrefixes', validPathPrefixes });
45+
}
46+
47+
function setMessageHandling(): void {
48+
navigator.serviceWorker.onmessage = async (event) => {
49+
const packet: ServiceWorker.Incoming.Packet = event.data;
50+
let responsePacket: ServiceWorker.Outgoing.Packet;
51+
52+
if (packet.type === 'Path') {
53+
const { path } = packet;
54+
responsePacket = {
55+
type: 'Data',
56+
path,
57+
data:
58+
(await fetchHandler?.(path).catch((e) => {
59+
console.error(`error while handing fetch of ${path}:`, e);
60+
})) ?? null,
61+
};
62+
} else {
63+
responsePacket = { type: 'ValidPathPrefixes', validPathPrefixes };
64+
}
65+
66+
sendServiceWorkerMessage(responsePacket);
67+
};
68+
}
69+
170
export async function loadServiceWorker(): Promise<ServiceWorker> {
271
const currentRegistration = await window.navigator.serviceWorker.getRegistration();
372
if (currentRegistration) {
@@ -20,53 +89,7 @@ export async function loadServiceWorker(): Promise<ServiceWorker> {
2089
}
2190

2291
setMessageHandling();
23-
updateServiceWorkerValidPathPrefixes();
92+
sendServiceWorkerMessage({ type: 'ValidPathPrefixes', validPathPrefixes });
2493

2594
return controller;
2695
}
27-
28-
function sendServiceWorkerMessage(packet: unknown): void {
29-
const { controller } = window.navigator.serviceWorker;
30-
controller?.postMessage(packet);
31-
}
32-
33-
export type FetchHandler = (path: string) => Promise<ArrayBufferLike | null>;
34-
const fetchHandlers: FetchHandler[] = [];
35-
const validPathPrefixes: string[] = [];
36-
37-
function updateServiceWorkerValidPathPrefixes(): void {
38-
sendServiceWorkerMessage(validPathPrefixes.map((path) => `/${path}`));
39-
}
40-
41-
export function addFetchHandler(pathPrefixes: string[], handler: FetchHandler): void {
42-
fetchHandlers.unshift(handler);
43-
validPathPrefixes.push(...pathPrefixes);
44-
updateServiceWorkerValidPathPrefixes();
45-
}
46-
47-
export interface ServiceWorkerPacket {
48-
path: string;
49-
data: BodyInit | null;
50-
}
51-
52-
function setMessageHandling(): void {
53-
navigator.serviceWorker.onmessage = async (event) => {
54-
const path: string = event.data;
55-
56-
let data: ArrayBufferLike | null = null;
57-
for (const handler of fetchHandlers) {
58-
try {
59-
data = await handler(path);
60-
} catch (e) {
61-
console.error(`error while handing fetch of ${path}:`, e);
62-
}
63-
if (data) break;
64-
}
65-
66-
const packet: ServiceWorkerPacket = {
67-
path,
68-
data,
69-
};
70-
sendServiceWorkerMessage(packet);
71-
};
72-
}
Lines changed: 60 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ServiceWorkerPacket } from './service-worker-bridge';
1+
import type { ServiceWorker } from './service-worker-bridge';
22

33
self.addEventListener('activate', () => {
44
void self.clients.claim();
@@ -23,24 +23,64 @@ function contentType(url: string): string {
2323
return CONTENT_TYPES[url.substring(url.lastIndexOf('.') + 1)] || 'text/plain';
2424
}
2525

26-
async function post(data: unknown): Promise<void> {
26+
const cacheName = 'ccmod-service-worker-cache';
27+
28+
function getCacheKey(key: string): string {
29+
return `https://${key}`;
30+
}
31+
32+
async function storeInCache<T extends object>(key: string, value: T): Promise<void> {
33+
const cache = await caches.open(cacheName);
34+
const response = new Response(JSON.stringify(value), {
35+
headers: { 'Content-Type': 'application/json' },
36+
});
37+
await cache.put(getCacheKey(key), response);
38+
}
39+
40+
async function getFromCache<T extends object>(key: string): Promise<T | null> {
41+
const cache = await caches.open(cacheName);
42+
const response = await cache.match(getCacheKey(key));
43+
if (!response) return null;
44+
return await response.json();
45+
}
46+
47+
const validPathPrefixesCacheKey = 'validPathPrefixes';
48+
49+
async function post(data: ServiceWorker.Incoming.Packet): Promise<void> {
2750
const clients = await self.clients.matchAll();
2851
const client = clients[0];
2952
client.postMessage(data);
3053
}
3154

32-
const waitingFor = new Map<string, (packet: ServiceWorkerPacket) => void>();
55+
const waitingFor = new Map<string, (packet: ServiceWorker.Outgoing.DataPacket) => void>();
3356

34-
async function requestContents(path: string): Promise<Response> {
35-
let resolve!: (packet: ServiceWorkerPacket) => void;
36-
const promise = new Promise<ServiceWorkerPacket>((res) => {
37-
resolve = res;
57+
async function requestAndAwaitAck(
58+
packet: ServiceWorker.Incoming.PathPacket,
59+
): Promise<ServiceWorker.Outgoing.DataPacket> {
60+
return new Promise<ServiceWorker.Outgoing.DataPacket>((resolve) => {
61+
waitingFor.set(packet.path, resolve);
62+
void post(packet);
3863
});
39-
await post(path);
64+
}
65+
66+
let validPathPrefixes: string[] | null;
67+
68+
self.addEventListener('message', (event) => {
69+
const packet: ServiceWorker.Outgoing.Packet = event.data;
70+
71+
if (packet.type === 'ValidPathPrefixes') {
72+
validPathPrefixes = packet.validPathPrefixes;
73+
74+
void storeInCache(validPathPrefixesCacheKey, validPathPrefixes);
75+
} else {
76+
waitingFor.get(packet.path)?.(packet);
77+
waitingFor.delete(packet.path);
78+
}
79+
});
4080

41-
waitingFor.set(path, resolve);
81+
async function requestContents(path: string): Promise<Response> {
82+
const { data } = await requestAndAwaitAck({ type: 'Path', path });
4283

43-
const { data } = await promise;
4484
if (!data) {
4585
return new Response(null, { status: 404 });
4686
}
@@ -54,32 +94,19 @@ async function requestContents(path: string): Promise<Response> {
5494
});
5595
}
5696

57-
let validPathPrefixes: string[] | undefined;
97+
async function respond(event: FetchEvent): Promise<Response> {
98+
const { request } = event;
99+
const path = decodeURI(new URL(request.url).pathname);
58100

59-
self.addEventListener('message', (event) => {
60-
if (Array.isArray(event.data)) {
61-
validPathPrefixes = event.data;
101+
validPathPrefixes ??= await getFromCache<string[]>(validPathPrefixesCacheKey);
102+
103+
if (validPathPrefixes?.some((pathPrefix) => path.startsWith(pathPrefix))) {
104+
return requestContents(path);
62105
} else {
63-
const packet: ServiceWorkerPacket = event.data;
64-
const resolve = waitingFor.get(packet.path)!;
65-
resolve(packet);
66-
waitingFor.delete(packet.path);
106+
return fetch(request);
67107
}
68-
});
108+
}
69109

70110
self.addEventListener('fetch', (event: FetchEvent) => {
71-
if (!validPathPrefixes) {
72-
return;
73-
}
74-
75-
const { request } = event;
76-
const path = decodeURI(new URL(request.url).pathname);
77-
78-
if (
79-
validPathPrefixes.some(
80-
(pathPrefix) => path.length > pathPrefix.length && path.startsWith(pathPrefix),
81-
)
82-
) {
83-
event.respondWith(requestContents(path));
84-
}
111+
event.respondWith(respond(event));
85112
});

0 commit comments

Comments
 (0)