diff --git a/packages/core/src/files.ccmod.ts b/packages/core/src/files.ccmod.ts index f3d5973..faae30e 100644 --- a/packages/core/src/files.ccmod.ts +++ b/packages/core/src/files.ccmod.ts @@ -1,5 +1,5 @@ import { type Unzipped, unzipSync } from 'fflate/browser'; -import { addFetchHandler } from './service-worker-bridge'; +import { setFetchHandler } from './service-worker-bridge'; import * as files from './files'; const fileMap = new Map(); @@ -62,6 +62,8 @@ export async function loadCCMods(ccmods: string[]): Promise { }), ); + // from my limited testing unzipSync is about 50ms faster (unzipSync takes 150ms) + // compared to asynchronous unzip ¯\_(ツ)_/¯ // console.time('uncompress'); const uncompressed = ccmodArrayBuffers.map((buf) => unzipSync(buf)); // console.timeEnd('uncompress'); @@ -72,5 +74,5 @@ export async function loadCCMods(ccmods: string[]): Promise { fileMap.set(modDir, buf); } - addFetchHandler(ccmods, readFile); + setFetchHandler(ccmods, readFile); } diff --git a/packages/core/src/service-worker-bridge.ts b/packages/core/src/service-worker-bridge.ts index 7a9b477..697c879 100644 --- a/packages/core/src/service-worker-bridge.ts +++ b/packages/core/src/service-worker-bridge.ts @@ -1,3 +1,72 @@ +export namespace ServiceWorker { + // Messages send to the service worker + export namespace Outgoing { + export interface ValidPathPrefixesPacket { + type: 'ValidPathPrefixes'; + validPathPrefixes: string[]; + } + + export interface DataPacket { + type: 'Data'; + path: string; + data: BodyInit | null; + } + + export type Packet = ValidPathPrefixesPacket | DataPacket; + } + // Messages coming from the service worker + export namespace Incoming { + export interface PathPacket { + type: 'Path'; + path: string; + } + export interface ValidPathPrefixesRequestPacket { + type: 'ValidPathPrefixesRequest'; + } + + export type Packet = PathPacket | ValidPathPrefixesRequestPacket; + } +} + +export type FetchHandler = (path: string) => Promise; + +let fetchHandler: FetchHandler | undefined; +let validPathPrefixes: string[] = []; + +function sendServiceWorkerMessage(packet: ServiceWorker.Outgoing.Packet): void { + const { controller } = window.navigator.serviceWorker; + controller?.postMessage(packet); +} + +export function setFetchHandler(pathPrefixes: string[], handler: FetchHandler): void { + fetchHandler = handler; + validPathPrefixes = pathPrefixes.map((path) => `/${path}/`); + sendServiceWorkerMessage({ type: 'ValidPathPrefixes', validPathPrefixes }); +} + +function setMessageHandling(): void { + navigator.serviceWorker.onmessage = async (event) => { + const packet: ServiceWorker.Incoming.Packet = event.data; + let responsePacket: ServiceWorker.Outgoing.Packet; + + if (packet.type === 'Path') { + const { path } = packet; + responsePacket = { + type: 'Data', + path, + data: + (await fetchHandler?.(path).catch((e) => { + console.error(`error while handing fetch of ${path}:`, e); + })) ?? null, + }; + } else { + responsePacket = { type: 'ValidPathPrefixes', validPathPrefixes }; + } + + sendServiceWorkerMessage(responsePacket); + }; +} + export async function loadServiceWorker(): Promise { const currentRegistration = await window.navigator.serviceWorker.getRegistration(); if (currentRegistration) { @@ -20,53 +89,7 @@ export async function loadServiceWorker(): Promise { } setMessageHandling(); - updateServiceWorkerValidPathPrefixes(); + sendServiceWorkerMessage({ type: 'ValidPathPrefixes', validPathPrefixes }); return controller; } - -function sendServiceWorkerMessage(packet: unknown): void { - const { controller } = window.navigator.serviceWorker; - controller?.postMessage(packet); -} - -export type FetchHandler = (path: string) => Promise; -const fetchHandlers: FetchHandler[] = []; -const validPathPrefixes: string[] = []; - -function updateServiceWorkerValidPathPrefixes(): void { - sendServiceWorkerMessage(validPathPrefixes.map((path) => `/${path}`)); -} - -export function addFetchHandler(pathPrefixes: string[], handler: FetchHandler): void { - fetchHandlers.unshift(handler); - validPathPrefixes.push(...pathPrefixes); - updateServiceWorkerValidPathPrefixes(); -} - -export interface ServiceWorkerPacket { - path: string; - data: BodyInit | null; -} - -function setMessageHandling(): void { - navigator.serviceWorker.onmessage = async (event) => { - const path: string = event.data; - - let data: ArrayBufferLike | null = null; - for (const handler of fetchHandlers) { - try { - data = await handler(path); - } catch (e) { - console.error(`error while handing fetch of ${path}:`, e); - } - if (data) break; - } - - const packet: ServiceWorkerPacket = { - path, - data, - }; - sendServiceWorkerMessage(packet); - }; -} diff --git a/packages/core/src/service-worker.ts b/packages/core/src/service-worker.ts index aa4d3c5..3b72429 100644 --- a/packages/core/src/service-worker.ts +++ b/packages/core/src/service-worker.ts @@ -1,4 +1,4 @@ -import type { ServiceWorkerPacket } from './service-worker-bridge'; +import type { ServiceWorker } from './service-worker-bridge'; self.addEventListener('activate', () => { void self.clients.claim(); @@ -23,24 +23,64 @@ function contentType(url: string): string { return CONTENT_TYPES[url.substring(url.lastIndexOf('.') + 1)] || 'text/plain'; } -async function post(data: unknown): Promise { +const cacheName = 'ccmod-service-worker-cache'; + +function getCacheKey(key: string): string { + return `https://${key}`; +} + +async function storeInCache(key: string, value: T): Promise { + const cache = await caches.open(cacheName); + const response = new Response(JSON.stringify(value), { + headers: { 'Content-Type': 'application/json' }, + }); + await cache.put(getCacheKey(key), response); +} + +async function getFromCache(key: string): Promise { + const cache = await caches.open(cacheName); + const response = await cache.match(getCacheKey(key)); + if (!response) return null; + return await response.json(); +} + +const validPathPrefixesCacheKey = 'validPathPrefixes'; + +async function post(data: ServiceWorker.Incoming.Packet): Promise { const clients = await self.clients.matchAll(); const client = clients[0]; client.postMessage(data); } -const waitingFor = new Map void>(); +const waitingFor = new Map void>(); -async function requestContents(path: string): Promise { - let resolve!: (packet: ServiceWorkerPacket) => void; - const promise = new Promise((res) => { - resolve = res; +async function requestAndAwaitAck( + packet: ServiceWorker.Incoming.PathPacket, +): Promise { + return new Promise((resolve) => { + waitingFor.set(packet.path, resolve); + void post(packet); }); - await post(path); +} + +let validPathPrefixes: string[] | null; + +self.addEventListener('message', (event) => { + const packet: ServiceWorker.Outgoing.Packet = event.data; + + if (packet.type === 'ValidPathPrefixes') { + validPathPrefixes = packet.validPathPrefixes; + + void storeInCache(validPathPrefixesCacheKey, validPathPrefixes); + } else { + waitingFor.get(packet.path)?.(packet); + waitingFor.delete(packet.path); + } +}); - waitingFor.set(path, resolve); +async function requestContents(path: string): Promise { + const { data } = await requestAndAwaitAck({ type: 'Path', path }); - const { data } = await promise; if (!data) { return new Response(null, { status: 404 }); } @@ -54,32 +94,19 @@ async function requestContents(path: string): Promise { }); } -let validPathPrefixes: string[] | undefined; +async function respond(event: FetchEvent): Promise { + const { request } = event; + const path = decodeURI(new URL(request.url).pathname); -self.addEventListener('message', (event) => { - if (Array.isArray(event.data)) { - validPathPrefixes = event.data; + validPathPrefixes ??= await getFromCache(validPathPrefixesCacheKey); + + if (validPathPrefixes?.some((pathPrefix) => path.startsWith(pathPrefix))) { + return requestContents(path); } else { - const packet: ServiceWorkerPacket = event.data; - const resolve = waitingFor.get(packet.path)!; - resolve(packet); - waitingFor.delete(packet.path); + return fetch(request); } -}); +} self.addEventListener('fetch', (event: FetchEvent) => { - if (!validPathPrefixes) { - return; - } - - const { request } = event; - const path = decodeURI(new URL(request.url).pathname); - - if ( - validPathPrefixes.some( - (pathPrefix) => path.length > pathPrefix.length && path.startsWith(pathPrefix), - ) - ) { - event.respondWith(requestContents(path)); - } + event.respondWith(respond(event)); });