libaribhtml5 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 huggy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # libaribhtml5
2
+
3
+ [日本語](#日本語) | [English](#english)
4
+
5
+ ## 日本語
6
+
7
+ ブラウザ上で動作する ARIB HTML5 受信機ランタイムのプロトタイプです。初期デモでは、
8
+ 展開済みの BS4K アプリケーションを放送時のパスで配信し、アプリケーションの
9
+ スクリプトが実行される前に受信機 API のポリフィルを注入します。
10
+
11
+ ```sh
12
+ pnpm install
13
+ pnpm dev
14
+ ```
15
+
16
+ Vite が出力した URL を開いてください。放送による自動起動アプリケーションには
17
+ **透明起動ページ**を、表示ページを直接確認するには**表示ページ**を使用します。
18
+
19
+ ### 注意事項
20
+
21
+ - 放送アプリケーションが利用する一部のオンラインデータは、ブラウザから放送局の
22
+ サーバーへ直接リクエストされます。現在のデモでは、安全のため、このような外部への
23
+ ネットワークリクエストをすべてブロックしています。
24
+ - ACAS のシリアル番号は `0721 0721 0721 0724 9674` に固定してエミュレートしています。
25
+
26
+ ### SDK バンドル
27
+
28
+ npm から利用する場合は、パッケージをインストールして ESM エントリを import します。
29
+
30
+ ```sh
31
+ npm install libaribhtml5
32
+ ```
33
+
34
+ ```ts
35
+ import { AribReceiverHost, installRuntime } from 'libaribhtml5'
36
+ ```
37
+
38
+ 従来の `<script>` 向け IIFE は `libaribhtml5/iife`、Service Worker ファイルは
39
+ `libaribhtml5/arib-vfs-sw.js` として公開されます。
40
+
41
+ ```sh
42
+ pnpm build:sdk
43
+ ```
44
+
45
+ このコマンドは `dist/sdk/libaribhtml5.js` と、ブラウザ VFS 用の
46
+ `dist/sdk/arib-vfs-sw.js` を生成します。SDK は `AribReceiverHost`、
47
+ `ServiceWorkerBroadcastVfs`、`installRuntime` を持つ `window.ARIBHTML5` を公開します。受信機内蔵音は
48
+ `src/runtime/romsound/` 以下で個別の MP3 ファイルとして管理され、SDK のビルド時に
49
+ すべてデータ URL として単一の JavaScript バンドルへ埋め込まれます。
50
+
51
+ プレーヤーへ統合する際のメディアスロット、adapter、レイヤー、ライフサイクル、字幕の契約は
52
+ [メディアプレーン共通統合ガイド](docs/media-plane-integration.md)を参照してください。
53
+ データ放送へ入る時は `host.loadApplication()`、通常プレーヤーへ戻す時は
54
+ `host.exitApplication()` を使用します。
55
+
56
+ 録画再生ではシステム時計ではなく、ストリームの NTP と再生位置を
57
+ `host.setBroadcastClock()` へ渡します。変換例は
58
+ [録画再生時の放送時計](docs/broadcast-clock.md)を参照してください。
59
+
60
+ EIT 由来の番組情報と受信機固有情報を host から渡す契約は
61
+ [receiver host contracts](docs/receiver-host-contracts.md)を参照してください。
62
+
63
+ Service Worker の `/data-broadcast/` ルーティング、データ放送リソースの
64
+ store/update/release ライフサイクル、および ARIB 追加記号フォントは
65
+ [放送リソースとフォント](docs/broadcast-resources-and-fonts.md)を参照してください。
66
+
67
+ Android/WebView の「放送予定」検索結果から受信機所有の番組表・番組詳細を開く場合は、
68
+ [`onOpenProgramGuide` とブラウザ fallback](docs/android-program-guide.md)を参照してください。
69
+
70
+ ## English
71
+
72
+ Browser-hosted ARIB HTML5 receiver-runtime prototype. The initial demo serves the
73
+ extracted BS4K application under its broadcast path and injects receiver API
74
+ polyfills before the application scripts execute.
75
+
76
+ ```sh
77
+ pnpm install
78
+ pnpm dev
79
+ ```
80
+
81
+ Open the URL printed by Vite. Use **透明起動ページ** for the broadcast autostart
82
+ application and **表示ページ** to inspect the visible page directly.
83
+
84
+ ### Notes
85
+
86
+ - Some online data used by broadcast applications is requested directly from the
87
+ browser to the broadcaster's servers. For safety, the current demo blocks all
88
+ such external network requests.
89
+ - The ACAS serial number is emulated with the fixed value
90
+ `0721 0721 0721 0724 9674`.
91
+
92
+ ### SDK bundle
93
+
94
+ Install the package to use its typed ESM entry point:
95
+
96
+ ```sh
97
+ npm install libaribhtml5
98
+ ```
99
+
100
+ ```ts
101
+ import { AribReceiverHost, installRuntime } from 'libaribhtml5'
102
+ ```
103
+
104
+ The browser-global IIFE remains available as `libaribhtml5/iife`, and the
105
+ Service Worker is exported as `libaribhtml5/arib-vfs-sw.js`.
106
+
107
+ ```sh
108
+ pnpm build:sdk
109
+ ```
110
+
111
+ This produces `dist/sdk/libaribhtml5.js` plus `dist/sdk/arib-vfs-sw.js` for the
112
+ optional browser VFS, and exposes `window.ARIBHTML5` with `AribReceiverHost`,
113
+ `ServiceWorkerBroadcastVfs`, and `installRuntime`. Receiver built-in sounds are maintained
114
+ as individual MP3 files under `src/runtime/romsound/`; the SDK build inlines all
115
+ of them as data URLs in the single JavaScript bundle.
116
+
117
+ See the [common media-plane integration guide](docs/media-plane-integration.md)
118
+ for the shared video layering, geometry, lifecycle, and caption contract.
119
+ See [broadcast resources, Worker routing, and fonts](docs/broadcast-resources-and-fonts.md)
120
+ for the `/data-broadcast/` namespace and cache/font lifecycle.
121
+ See [Android program-guide integration](docs/android-program-guide.md) for handing a
122
+ future-program result to a receiver-owned WebView/native EPG with a browser fallback.
123
+ See [receiver host contracts](docs/receiver-host-contracts.md) for program metadata
124
+ and receiver-owned system information.
@@ -0,0 +1,24 @@
1
+ # Third-party notices
2
+
3
+ `src/runtime/romsound/*.mp3` contains receiver built-in sound data restored from
4
+ [web-bml](https://github.com/otya128/web-bml), used under the MIT License.
5
+
6
+ Copyright (c) 2022 otya
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
@@ -0,0 +1,336 @@
1
+ (function() {
2
+ "use strict";
3
+ function deferRomSoundMarkup(source) {
4
+ return source.replace(
5
+ /\s+src\s*=\s*(?:(["'])(romsound:\/\/\d+)\1|(romsound:\/\/\d+)(?=[\s/>]))/gi,
6
+ (_match, _quote, quoted, bare) => ` data-arib-romsound="${quoted ?? bare}"`
7
+ );
8
+ }
9
+ function normalizeBasePath(value) {
10
+ const base = value || "/data-broadcast/";
11
+ return `/${base.replace(/^\/+|\/+$/g, "")}/`;
12
+ }
13
+ function rewriteBroadcastObjectMarkup(source) {
14
+ return source.replace(/<object\b[^>]*>/gi, (tag) => {
15
+ const broadcast = /\btype\s*=\s*(?:["']video\/x-arib2-broadcast["']|video\/x-arib2-broadcast)(?:\s|\/?>)/i.test(tag);
16
+ if (!broadcast) return tag;
17
+ return tag.replace(
18
+ /\s+type\s*=\s*(?:["']video\/x-arib2-broadcast["']|video\/x-arib2-broadcast)/i,
19
+ ' data-arib-type="video/x-arib2-broadcast"'
20
+ ).replace(/\s+data\s*=\s*(["'])(.*?)\1/i, " data-arib-data=$1$2$1");
21
+ });
22
+ }
23
+ function receiverPath(path, base, scope) {
24
+ if (path.startsWith(scope)) return path;
25
+ const mount = base.startsWith(scope) ? base.slice(scope.length).replace(/^\/+|\/+$/g, "") : "";
26
+ const requested = path.replace(/^\/+/, "");
27
+ if (mount && (requested === mount || requested.startsWith(`${mount}/`))) {
28
+ return `${scope}${requested}`;
29
+ }
30
+ return `${base}${requested}`;
31
+ }
32
+ function prefixBroadcastRootAttributes(source, basePath, scopePath) {
33
+ const base = normalizeBasePath(basePath);
34
+ const scope = normalizeBasePath(scopePath ?? basePath);
35
+ return source.replace(
36
+ /(\b(?:href|src|action|poster)\s*=\s*)(?:(['"])(\/[^/][^"']*)\2|(\/[^\s>]*))/gi,
37
+ (match, name, quote, quotedPath, barePath) => {
38
+ const path = quotedPath ?? barePath;
39
+ if (!path || path.startsWith(base)) return match;
40
+ const value = receiverPath(path, base, scope);
41
+ return quote ? `${name}${quote}${value}${quote}` : `${name}${value}`;
42
+ }
43
+ );
44
+ }
45
+ function prepareBroadcastStylesheet(source, options = {}) {
46
+ const base = normalizeBasePath(options.basePath);
47
+ const scope = normalizeBasePath(options.scopePath ?? options.basePath);
48
+ return source.replace(
49
+ /url\(\s*(["']?)(\/[^/)][^)]*)\1\s*\)/gi,
50
+ (match, quote, path) => path.startsWith(base) ? match : `url(${quote}${receiverPath(path, base, scope)}${quote})`
51
+ ).replace(
52
+ /(@import\s+)(["'])(\/[^/][^"']*)\2/gi,
53
+ (match, keyword, quote, path) => path.startsWith(base) ? match : `${keyword}${quote}${receiverPath(path, base, scope)}${quote}`
54
+ );
55
+ }
56
+ function prepareBroadcastHtml(source, options = {}) {
57
+ const prepared = prefixBroadcastRootAttributes(
58
+ deferRomSoundMarkup(rewriteBroadcastObjectMarkup(source)),
59
+ options.basePath,
60
+ options.scopePath
61
+ );
62
+ if (!options.bootstrap) return prepared;
63
+ const head = /<head(?:\s[^>]*)?>/i.exec(prepared);
64
+ if (!head) return `${options.bootstrap}${prepared}`;
65
+ const offset = head.index + head[0].length;
66
+ return `${prepared.slice(0, offset)}${options.bootstrap}${prepared.slice(offset)}`;
67
+ }
68
+ const scopeUrl = new URL(self.registration.scope);
69
+ const VFS_PREFIX = scopeUrl.pathname.endsWith("/") ? scopeUrl.pathname : `${scopeUrl.pathname}/`;
70
+ const CACHE_NAME = `libaribhtml5-arib-vfs-v1:${VFS_PREFIX}`;
71
+ const CACHE_RESOURCE_PATH = `${VFS_PREFIX}.libaribhtml5-arib-vfs-resource`;
72
+ const CACHE_SESSION_PATH = `${VFS_PREFIX}.libaribhtml5-arib-vfs-session`;
73
+ const RUNTIME_BOOTSTRAP = "<script>parent.__ARIB_HTML5_INSTALL__?.(window)<\/script>";
74
+ const resources = /* @__PURE__ */ new Map();
75
+ const waiters = /* @__PURE__ */ new Map();
76
+ let enabled = false;
77
+ let uniqueBasenameFallback = false;
78
+ let restorePromise = null;
79
+ function cacheRequest(path) {
80
+ const url = new URL(CACHE_RESOURCE_PATH, self.location.origin);
81
+ url.searchParams.set("path", path);
82
+ return new Request(url.href);
83
+ }
84
+ function sessionRequest() {
85
+ return new Request(new URL(CACHE_SESSION_PATH, self.location.origin).href);
86
+ }
87
+ async function beginPersistentSession() {
88
+ await caches.delete(CACHE_NAME);
89
+ const cache = await caches.open(CACHE_NAME);
90
+ await cache.put(sessionRequest(), new Response("", {
91
+ headers: {
92
+ "X-Arib-VFS-Session": "active",
93
+ "X-Arib-VFS-Unique-Basename-Fallback": uniqueBasenameFallback ? "1" : "0"
94
+ }
95
+ }));
96
+ restorePromise = null;
97
+ }
98
+ async function persistResource(path, resource) {
99
+ const cache = await caches.open(CACHE_NAME);
100
+ await cache.put(cacheRequest(path), new Response(resource.data.slice(), {
101
+ headers: {
102
+ "Content-Type": resource.contentType || "application/octet-stream",
103
+ "X-Arib-VFS-Path": path
104
+ }
105
+ }));
106
+ }
107
+ async function restorePersistentResources() {
108
+ if (enabled) return true;
109
+ if (restorePromise) return restorePromise;
110
+ restorePromise = (async () => {
111
+ const cache = await caches.open(CACHE_NAME);
112
+ const session = await cache.match(sessionRequest());
113
+ if (!session) return false;
114
+ uniqueBasenameFallback = session.headers.get("X-Arib-VFS-Unique-Basename-Fallback") === "1";
115
+ for (const request of await cache.keys()) {
116
+ const response = await cache.match(request);
117
+ const path = response?.headers.get("X-Arib-VFS-Path");
118
+ if (!path) continue;
119
+ resources.set(path, {
120
+ data: new Uint8Array(await response.arrayBuffer()),
121
+ contentType: response.headers.get("Content-Type") || ""
122
+ });
123
+ }
124
+ enabled = true;
125
+ return true;
126
+ })();
127
+ try {
128
+ return await restorePromise;
129
+ } finally {
130
+ restorePromise = null;
131
+ }
132
+ }
133
+ function normalizePath(value) {
134
+ let pathname;
135
+ try {
136
+ pathname = decodeURIComponent(new URL(String(value), self.location.origin).pathname);
137
+ } catch {
138
+ return null;
139
+ }
140
+ if (pathname.startsWith(VFS_PREFIX)) pathname = pathname.slice(VFS_PREFIX.length);
141
+ const parts = pathname.split("/").filter(Boolean);
142
+ if (!parts.length || parts.some((part) => part === "." || part === "..")) return null;
143
+ return parts.join("/");
144
+ }
145
+ function uniqueBasenameMatch(path) {
146
+ if (!uniqueBasenameFallback) return null;
147
+ const slash = path.lastIndexOf("/");
148
+ const basename = slash >= 0 ? path.slice(slash + 1) : path;
149
+ if (!basename) return null;
150
+ let match = null;
151
+ for (const candidate of resources.keys()) {
152
+ if (candidate !== basename && !candidate.endsWith(`/${basename}`)) continue;
153
+ if (match !== null) return null;
154
+ match = candidate;
155
+ }
156
+ return match;
157
+ }
158
+ function hasBroadcastRoot(path) {
159
+ const slash = path.indexOf("/");
160
+ const root = slash < 0 ? path : path.slice(0, slash);
161
+ for (const candidate of resources.keys()) {
162
+ if (candidate === root || candidate.startsWith(`${root}/`)) return true;
163
+ }
164
+ return false;
165
+ }
166
+ function hasResourceCandidate(value) {
167
+ const path = normalizePath(value);
168
+ return Boolean(path && (resources.has(path) || hasBroadcastRoot(path) || uniqueBasenameMatch(path) !== null));
169
+ }
170
+ function contentType(path, supplied) {
171
+ if (supplied) return supplied;
172
+ const extension = path.slice(path.lastIndexOf(".")).toLowerCase();
173
+ return (/* @__PURE__ */ new Map([
174
+ [".html", "text/html; charset=utf-8"],
175
+ [".htm", "text/html; charset=utf-8"],
176
+ [".css", "text/css; charset=utf-8"],
177
+ [".js", "text/javascript; charset=utf-8"],
178
+ [".json", "application/json; charset=utf-8"],
179
+ [".svg", "image/svg+xml"],
180
+ [".png", "image/png"],
181
+ [".jpg", "image/jpeg"],
182
+ [".jpeg", "image/jpeg"],
183
+ [".gif", "image/gif"],
184
+ [".webp", "image/webp"],
185
+ [".woff", "font/woff"],
186
+ [".woff2", "font/woff2"]
187
+ ])).get(extension) || "application/octet-stream";
188
+ }
189
+ function broadcastRootPath(path) {
190
+ const separator = path.indexOf("/");
191
+ return separator <= 0 ? VFS_PREFIX : `${VFS_PREFIX}${path.slice(0, separator)}/`;
192
+ }
193
+ function wake(path, resource) {
194
+ const exact = waiters.get(path);
195
+ if (exact) {
196
+ waiters.delete(path);
197
+ for (const resolve of exact) resolve({ path, resource });
198
+ }
199
+ if (!uniqueBasenameFallback) return;
200
+ for (const [requestedPath, pending] of [...waiters]) {
201
+ const fallback = uniqueBasenameMatch(requestedPath);
202
+ if (!fallback) continue;
203
+ const fallbackResource = resources.get(fallback);
204
+ if (!fallbackResource) continue;
205
+ waiters.delete(requestedPath);
206
+ for (const resolve of pending) resolve({ path: fallback, resource: fallbackResource });
207
+ }
208
+ }
209
+ function waitFor(path, timeout = 3e4) {
210
+ const current = resources.get(path);
211
+ if (current) return Promise.resolve({ path, resource: current });
212
+ const fallback = uniqueBasenameMatch(path);
213
+ if (fallback) return Promise.resolve({ path: fallback, resource: resources.get(fallback) });
214
+ return new Promise((resolve) => {
215
+ const pending = waiters.get(path) || /* @__PURE__ */ new Set();
216
+ pending.add(resolve);
217
+ waiters.set(path, pending);
218
+ setTimeout(() => {
219
+ pending.delete(resolve);
220
+ if (!pending.size) waiters.delete(path);
221
+ resolve(null);
222
+ }, timeout);
223
+ });
224
+ }
225
+ async function hasAvailableResource(value) {
226
+ await restorePersistentResources();
227
+ const path = normalizePath(`/${value || ""}`);
228
+ return Boolean(enabled && path && resources.has(path));
229
+ }
230
+ async function serve(request) {
231
+ const path = normalizePath(request.url);
232
+ if (!path) return new Response("Bad broadcast path", { status: 400 });
233
+ const resolved = await waitFor(path);
234
+ if (!resolved) return new Response("Broadcast resource is not available", { status: 404 });
235
+ if (resolved.path !== path) {
236
+ return Response.redirect(new URL(`${VFS_PREFIX}${resolved.path}`, self.location.origin), 302);
237
+ }
238
+ const resource = resolved.resource;
239
+ const type = contentType(path, resource.contentType);
240
+ let body = resource.data.slice(0);
241
+ if (/^text\/html(?:;|$)/i.test(type)) {
242
+ const source = prepareBroadcastHtml(new TextDecoder().decode(resource.data), {
243
+ basePath: broadcastRootPath(resolved.path),
244
+ scopePath: VFS_PREFIX,
245
+ bootstrap: RUNTIME_BOOTSTRAP
246
+ });
247
+ body = new TextEncoder().encode(source);
248
+ } else if (/^text\/css(?:;|$)/i.test(type)) {
249
+ const source = prepareBroadcastStylesheet(new TextDecoder().decode(resource.data), {
250
+ basePath: broadcastRootPath(resolved.path),
251
+ scopePath: VFS_PREFIX
252
+ });
253
+ body = new TextEncoder().encode(source);
254
+ }
255
+ return new Response(request.method === "HEAD" ? null : body, {
256
+ status: 200,
257
+ headers: {
258
+ "Cache-Control": "no-store",
259
+ "Content-Type": type,
260
+ "Content-Security-Policy": "default-src 'self' data: blob:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; object-src 'none'; frame-src 'none'",
261
+ "X-Content-Type-Options": "nosniff"
262
+ }
263
+ });
264
+ }
265
+ self.addEventListener("install", (event) => {
266
+ event.waitUntil(self.skipWaiting());
267
+ });
268
+ self.addEventListener("activate", (event) => {
269
+ event.waitUntil(self.clients.claim());
270
+ });
271
+ self.addEventListener("message", (event) => {
272
+ const message = event.data || {};
273
+ const reply = (value) => event.ports[0]?.postMessage(value);
274
+ const handle = async () => {
275
+ if (message.type === "arib-vfs-probe") {
276
+ reply({ ok: true, available: await hasAvailableResource(message.path) });
277
+ return;
278
+ }
279
+ if (message.type === "arib-vfs-begin") {
280
+ enabled = true;
281
+ uniqueBasenameFallback = Boolean(message.uniqueBasenameFallback);
282
+ resources.clear();
283
+ for (const pending of waiters.values()) {
284
+ for (const resolve of pending) resolve(null);
285
+ }
286
+ waiters.clear();
287
+ await beginPersistentSession();
288
+ reply({ ok: true });
289
+ return;
290
+ }
291
+ if (message.type === "arib-vfs-put") {
292
+ const path = normalizePath(`/${message.path || ""}`);
293
+ if (!path || !(message.data instanceof ArrayBuffer)) {
294
+ reply({ ok: false, error: "invalid resource" });
295
+ return;
296
+ }
297
+ const resource = {
298
+ data: new Uint8Array(message.data),
299
+ contentType: String(message.contentType || "")
300
+ };
301
+ enabled = true;
302
+ resources.set(path, resource);
303
+ await persistResource(path, resource);
304
+ wake(path, resource);
305
+ reply({ ok: true });
306
+ return;
307
+ }
308
+ if (message.type === "arib-vfs-reset") {
309
+ enabled = false;
310
+ uniqueBasenameFallback = false;
311
+ resources.clear();
312
+ restorePromise = null;
313
+ for (const pending of waiters.values()) {
314
+ for (const resolve of pending) resolve(null);
315
+ }
316
+ waiters.clear();
317
+ await caches.delete(CACHE_NAME);
318
+ reply({ ok: true });
319
+ return;
320
+ }
321
+ reply({ ok: false, error: "unsupported VFS message" });
322
+ };
323
+ event.waitUntil(handle().catch((error) => {
324
+ reply({ ok: false, error: error?.message || String(error) });
325
+ }));
326
+ });
327
+ self.addEventListener("fetch", (event) => {
328
+ const url = new URL(event.request.url);
329
+ if (url.origin !== self.location.origin || !url.pathname.startsWith(VFS_PREFIX) || event.request.method !== "GET" && event.request.method !== "HEAD") return;
330
+ event.respondWith((async () => {
331
+ await restorePersistentResources();
332
+ if (!enabled || !hasResourceCandidate(url.pathname)) return fetch(event.request);
333
+ return serve(event.request);
334
+ })());
335
+ });
336
+ })();