Files
tech-log-frontend/src/adapters/service-worker/service-worker-entry.ts
T
DongHyeonkaandClaude Opus 5 4bff9ca151 chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed
on it through 43 template commits, so it was missing all three rounds of
adapter remediation — including files it never had, such as the shared
`abortable-operation` primitive and the `exact-snapshot` decoder that
later fixes are written against. Taking only the newest round was not
possible for that reason: the delta is coherent only as a whole.

The product had not touched `src/adapters` at all since materialization,
so the 140-file delta applied with a three-way merge and no conflicts.
`package.json` was the single overlap and merged cleanly: the product owns
`name`, the template contributed `check:adapter-inventory`,
`check:remediation-ledger` and the image-resolve-signal type fixture.
All 24 product-owned files — README, index.html, CI workflow, i18n
catalog, home page, generated schemas, evidence scripts, component and
visual snapshots — are byte-identical to `main`.

`template.lock.json` now pins the synced revision and tree.

Verified in this repository, not inherited from the template: six type
projects, lint, nine gates (adapter inventory, remediation ledger,
registries, diagnostics, realtime boundaries, architecture, browser
file/storage boundaries, optional recipes, documentation), the production
build, and 2,054 of 2,073 tests. The 19 failures are all in
`tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing
sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template
records; four suites that failed once under parallel load pass in
isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:04:58 +09:00

172 lines
5.7 KiB
TypeScript

/// <reference lib="webworker" />
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,
});
}
}),
);
});
}