/// import { OFFLINE_SYNC_TAG } from "../../contracts/offline-command.ts"; import type { ServiceWorkerHandlerId, ServiceWorkerProtocolIdentity, StaticAssetManifestV1, } from "../../contracts/service-worker.ts"; import { createServiceWorkerRuntime, type WorkerScopeLike, } from "./service-worker-lifecycle.ts"; import { parseServiceWorkerMessage } from "./service-worker-protocol.ts"; /** * §17.1. The one physical worker entry for this scope. * * PWA lifecycle, verified static asset fetch, Web Push and the optional sync * wake-up are all handler factories inside this single entry. A second * registration for any of them is prohibited. * * This module is compiled only by `vite.service-worker.config.ts` when the * static selection is `ACTIVE`; it is never part of the page bundle. */ declare const self: ServiceWorkerGlobalScope; // Build-time virtual modules (§18.3). They resolve through the Service Worker // Vite config only, so the page bundle can never import a worker asset list. declare const __CA_SERVICE_WORKER_BUILD_INFO__: ServiceWorkerProtocolIdentity; declare const __CA_SERVICE_WORKER_ASSETS__: StaticAssetManifestV1 | null; declare const __CA_SERVICE_WORKER_HANDLERS__: readonly ServiceWorkerHandlerId[]; declare const __CA_RUNTIME_CONFIG_URL__: string; declare const __CA_RELEASE_MANIFEST_URL__: string; const identity = __CA_SERVICE_WORKER_BUILD_INFO__; const handlers = __CA_SERVICE_WORKER_HANDLERS__; const scope: WorkerScopeLike = { caches: { open: (name) => caches.open(name), keys: () => caches.keys(), delete: (name) => caches.delete(name), // SW-01. No CacheStorage-wide match: only the current release cache may // answer a verified static request. }, clients: { matchAll: (options) => self.clients.matchAll( options as { type?: "window"; includeUncontrolled?: boolean }, ) as Promise< readonly { id: string; url: string; postMessage(m: unknown): void; }[] >, }, registrationScope: self.registration.scope, skipWaiting: () => self.skipWaiting(), fetcher: (input: RequestInfo | URL, init?: RequestInit) => fetch(input, init), async digest(bytes) { const buffer = await crypto.subtle.digest( "SHA-256", bytes.slice().buffer as ArrayBuffer, ); let hex = ""; for (const byte of new Uint8Array(buffer)) { hex += byte.toString(16).padStart(2, "0"); } return `sha256:${hex}`; }, }; const runtime = createServiceWorkerRuntime(scope, { identity, handlers, manifest: __CA_SERVICE_WORKER_ASSETS__, runtimeConfigUrl: __CA_RUNTIME_CONFIG_URL__, releaseManifestUrl: __CA_RELEASE_MANIFEST_URL__, }); self.addEventListener("install", (event) => { // §17.10. Install never calls skipWaiting(); activation is a page handshake. event.waitUntil(runtime.onInstall()); }); self.addEventListener("activate", (event) => { // §17.12. No clients.claim() in the baseline. event.waitUntil(runtime.onActivate()); }); self.addEventListener("fetch", (event) => { const request = event.request; event.respondWith( runtime .onFetch({ method: request.method, url: request.url, mode: request.mode, }) .then((cached) => cached ?? fetch(request)), ); }); self.addEventListener("message", (event) => { const parsed = parseServiceWorkerMessage(event.data); if (!parsed.ok) return; if (parsed.message.kind === "ACTIVATE_REQUEST") { event.waitUntil(runtime.onActivateRequest(event.data)); return; } if ( parsed.message.kind === "CLIENT_DRAINED" || parsed.message.kind === "ACTIVATE_REJECTED" ) { const source = event.source; if (source && "id" in source && typeof source.id === "string") { runtime.onClientMessage(event.data, source.id); } return; } if (parsed.message.kind === "CACHE_RESET_REQUEST") { const source = event.source; if ( source && "id" in source && typeof source.id === "string" && "postMessage" in source && typeof source.postMessage === "function" ) { event.waitUntil(runtime.onCacheResetRequest(event.data, source)); } } }); // §17.1 WEB_PUSH composition point. // // The Web Push runtime is a handler factory inside this one entry, never a // second registration. It is not wired here because the template cannot supply // the two product-owned inputs it needs: a PushAssociationFenceStore over the // product push control repository, and a WebPushNotificationRegistry of exact // notification types with the same-origin routes their clicks may open // (§21.11). Selecting WEB_PUSH means adding, inside a // `handlers.includes("WEB_PUSH")` guard: import // createWebPushServiceWorkerRuntime and createServiceWorkerScopeHost from the // sibling web-push adapter, then call the runtime with the scope host built // from `self` plus the product fence store and notification registry. // // Keeping the import out of the baseline entry is also what lets the realtime // and Web Push runtime be removed as a pure file deletion (§24.12). if (handlers.includes("OFFLINE_SYNC_WAKEUP")) { // §19.18. Wake-up only: the handler records that a sync fired and notifies // controlled clients. It never sends an authenticated command (§19.20). self.addEventListener("sync", (rawEvent: Event) => { const event = rawEvent as ExtendableEvent & { tag?: string }; if (event.tag !== OFFLINE_SYNC_TAG) return; event.waitUntil( self.clients.matchAll({ type: "window" }).then((clients) => { for (const client of clients) { client.postMessage({ protocolVersion: 1, kind: "SYNC_WAKE_OBSERVED", messageId: crypto.randomUUID(), sourceBuildId: identity.buildId, }); } }), ); }); }