@inkandswitch/patchwork-bootloader 0.0.4 → 0.0.6
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/dist/externals.js +10 -0
- package/dist/service-worker.js +232 -84
- package/dist/setup.d.ts +4 -2
- package/dist/setup.js +57 -45
- package/dist/site.d.ts +89 -0
- package/dist/site.js +365 -0
- package/dist/sw-logger.d.ts +105 -0
- package/dist/sw-logger.js +366 -0
- package/dist/types.d.ts +0 -27
- package/dist/vite/importmap-plugin.js +26 -18
- package/dist/vite/service-worker-plugin.d.ts +1 -1
- package/dist/vite/service-worker-plugin.js +14 -31
- package/package.json +25 -12
- package/src/externals.ts +11 -0
- package/src/service-worker.ts +305 -89
- package/src/setup.ts +70 -77
- package/src/site.ts +530 -0
- package/src/sw-logger.ts +463 -0
- package/src/types.ts +0 -35
- package/src/vite/importmap-plugin.ts +29 -18
- package/src/vite/service-worker-plugin.ts +18 -39
package/src/service-worker.ts
CHANGED
|
@@ -1,10 +1,75 @@
|
|
|
1
1
|
/// <reference types="service-worker-types" />
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { SwLogger } from "./sw-logger.js";
|
|
4
4
|
|
|
5
|
+
// Heavy imports — marked external by the service-worker vite plugin,
|
|
6
|
+
// resolved to /packages/... URLs at build time. The SW is registered with
|
|
7
|
+
// type:"module" so the browser fetches these as regular network requests.
|
|
8
|
+
// Uses /slim to avoid top-level await (disallowed in service workers).
|
|
9
|
+
// Wasm is fetched from /automerge.wasm (emitted by the vite plugin) instead
|
|
10
|
+
// of bundling the ~3MB base64 string.
|
|
11
|
+
import { initializeWasm } from "@automerge/automerge/slim";
|
|
12
|
+
// eslint-disable-next-line
|
|
13
|
+
// @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
|
|
14
|
+
import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
|
|
15
|
+
import { WebCryptoSigner } from "@automerge/automerge-subduction/slim";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
Repo,
|
|
19
|
+
isValidAutomergeUrl,
|
|
20
|
+
parseAutomergeUrl,
|
|
21
|
+
stringifyAutomergeUrl,
|
|
22
|
+
type PeerId,
|
|
23
|
+
} from "@automerge/automerge-repo/slim";
|
|
24
|
+
import {
|
|
25
|
+
findHandleInFolderHandle,
|
|
26
|
+
resolvePackageExport,
|
|
27
|
+
type FolderDoc,
|
|
28
|
+
} from "@inkandswitch/patchwork-filesystem";
|
|
29
|
+
|
|
30
|
+
// Small adapters — bundled directly into the SW
|
|
31
|
+
import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
|
|
32
|
+
import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel";
|
|
33
|
+
import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
|
|
34
|
+
|
|
35
|
+
// TEMPORARY: enable debug npm module in SW context (no localStorage available)
|
|
5
36
|
let cachename = "default";
|
|
6
37
|
let debugging = false;
|
|
7
38
|
|
|
39
|
+
const SUBDUCTION_ENDPOINTS = ["wss://subduction.sync.inkandswitch.com"];
|
|
40
|
+
|
|
41
|
+
// ── Persistent logger ───────────────────────────────────────────────────
|
|
42
|
+
// Initialized eagerly so it's available for the entire SW lifetime.
|
|
43
|
+
// Access from the SW inspector console via self.printLogs(), self.tailLogs(),
|
|
44
|
+
// self.exportLogs(), self.clearLogs().
|
|
45
|
+
const slog = SwLogger.open().then((logger) => {
|
|
46
|
+
(self as any).slog = logger;
|
|
47
|
+
|
|
48
|
+
(self as any).printLogs = async (n = 200) => {
|
|
49
|
+
const entries = await logger.tail(n);
|
|
50
|
+
for (const e of entries) {
|
|
51
|
+
const prefix = `[${e.ts}] [${e.level}]`;
|
|
52
|
+
if (e.data !== undefined) {
|
|
53
|
+
console.log(prefix, e.msg, e.data);
|
|
54
|
+
} else {
|
|
55
|
+
console.log(prefix, e.msg);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
console.log(`--- ${entries.length} entries ---`);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
(self as any).tailLogs = (n = 200) => logger.tail(n);
|
|
62
|
+
(self as any).exportLogs = () => logger.exportAll();
|
|
63
|
+
(self as any).clearLogs = () => logger.clear();
|
|
64
|
+
|
|
65
|
+
logger.info("sw-logger initialized");
|
|
66
|
+
return logger;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const cacheableStatuses = [
|
|
70
|
+
200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501,
|
|
71
|
+
];
|
|
72
|
+
|
|
8
73
|
function log(...args: any[]) {
|
|
9
74
|
if (!debugging) return;
|
|
10
75
|
console.log.call(
|
|
@@ -34,38 +99,81 @@ self.addEventListener("activate", async () => {
|
|
|
34
99
|
clients.claim();
|
|
35
100
|
});
|
|
36
101
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
102
|
+
let repoPromise: Promise<Repo> | null = null;
|
|
103
|
+
|
|
104
|
+
function getRepo() {
|
|
105
|
+
if (!repoPromise) {
|
|
106
|
+
repoPromise = (async () => {
|
|
107
|
+
const logger = await slog;
|
|
108
|
+
|
|
109
|
+
logger.info("fetching wasm modules");
|
|
110
|
+
const [amWasmBuf, sdnWasmBuf] = await Promise.all([
|
|
111
|
+
fetch("/automerge.wasm").then((r) => r.arrayBuffer()),
|
|
112
|
+
fetch("/subduction.wasm").then((r) => r.arrayBuffer()),
|
|
113
|
+
]);
|
|
114
|
+
initSubductionSync(new Uint8Array(sdnWasmBuf));
|
|
115
|
+
await initializeWasm(new Uint8Array(amWasmBuf));
|
|
116
|
+
logger.info("wasm initialized");
|
|
117
|
+
|
|
118
|
+
const signer = await WebCryptoSigner.setup();
|
|
119
|
+
|
|
120
|
+
const repo = new Repo({
|
|
121
|
+
storage: new IndexedDBStorageAdapter(),
|
|
122
|
+
signer,
|
|
123
|
+
peerId: ("service-worker-" +
|
|
124
|
+
(Math.random() * 10000).toString(36).slice(2)) as PeerId,
|
|
125
|
+
async sharePolicy(peerId) {
|
|
126
|
+
return peerId.includes("storage-server");
|
|
127
|
+
},
|
|
128
|
+
enableRemoteHeadsGossiping: true,
|
|
129
|
+
subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
|
|
130
|
+
network: [new WebSocketClientAdapter("wss://sync3.automerge.org")],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
(self as any).repo = repo;
|
|
134
|
+
logger.info("repo constructed, waiting for network subsystem");
|
|
135
|
+
|
|
136
|
+
// Don't block getRepo() on whenReady() — the network subsystem starts
|
|
137
|
+
// with only the subduction adapter, and the MessageChannel adapter is
|
|
138
|
+
// added later via connectPort (which awaits getRepo). Blocking here
|
|
139
|
+
// would deadlock that path and starve the fetch handler.
|
|
140
|
+
repo.networkSubsystem.whenReady().then(() => {
|
|
141
|
+
logger.info("repo network subsystem ready");
|
|
142
|
+
});
|
|
42
143
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (!responseItem) {
|
|
46
|
-
return console.warn(`No read response found for id ${message.id}`);
|
|
144
|
+
return repo;
|
|
145
|
+
})();
|
|
47
146
|
}
|
|
48
|
-
return
|
|
147
|
+
return repoPromise;
|
|
49
148
|
}
|
|
50
149
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
})
|
|
150
|
+
// Connect client MessagePorts to the repo for sync
|
|
151
|
+
async function connectPort(port: MessagePort) {
|
|
152
|
+
const repo = await getRepo();
|
|
153
|
+
repo.networkSubsystem.addNetworkAdapter(
|
|
154
|
+
new MessageChannelNetworkAdapter(port, { useWeakRef: true })
|
|
155
|
+
);
|
|
156
|
+
}
|
|
56
157
|
|
|
57
|
-
// when we receive a `response` req, we resolve the promise with that id
|
|
58
158
|
self.addEventListener("message", async (event) => {
|
|
59
|
-
if (event.data.type == "
|
|
60
|
-
|
|
159
|
+
if (event.data.type == "ping") {
|
|
160
|
+
// Keepalive — Chromium idles out service workers after ~30s of inactivity.
|
|
161
|
+
// Reply via the provided port if any; the message event itself also resets
|
|
162
|
+
// the idle timer.
|
|
163
|
+
const [pongPort] = event.ports;
|
|
164
|
+
log("ping");
|
|
165
|
+
if (pongPort) {
|
|
166
|
+
pongPort.postMessage({ type: "pong" });
|
|
167
|
+
log("pong");
|
|
168
|
+
pongPort.close();
|
|
169
|
+
} else if (event.source) {
|
|
170
|
+
(event.source as unknown as Client).postMessage({ type: "pong" });
|
|
171
|
+
log("pong");
|
|
172
|
+
}
|
|
61
173
|
} else if (event.data.type == "port") {
|
|
62
|
-
log("
|
|
174
|
+
log("received messagechannel");
|
|
63
175
|
const [port] = event.ports;
|
|
64
|
-
port
|
|
65
|
-
if (event.data.type == "response") {
|
|
66
|
-
accept(event.data);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
176
|
+
connectPort(port);
|
|
69
177
|
} else if (event.data.type == "cachename") {
|
|
70
178
|
const nextCachename = event.data.cachename;
|
|
71
179
|
if (cachename == nextCachename) {
|
|
@@ -82,14 +190,139 @@ self.addEventListener("message", async (event) => {
|
|
|
82
190
|
}
|
|
83
191
|
});
|
|
84
192
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
193
|
+
interface FileDoc {
|
|
194
|
+
content: string | Uint8Array;
|
|
195
|
+
mimeType?: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Automerge URL resolution ───────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
async function resolveAutomergeUrl(automergeURL: URL): Promise<Response> {
|
|
201
|
+
const repo = await getRepo();
|
|
202
|
+
const href = automergeURL.href;
|
|
203
|
+
const [maybeAutomergeUrl, ...path] = href.split("/");
|
|
204
|
+
|
|
205
|
+
if (!isValidAutomergeUrl(maybeAutomergeUrl)) {
|
|
206
|
+
return new Response("invalid automerge url", { status: 400 });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Trim trailing empty path segment
|
|
210
|
+
if (path.length && !path[path.length - 1]) path.pop();
|
|
211
|
+
|
|
212
|
+
const { heads, documentId } = parseAutomergeUrl(maybeAutomergeUrl);
|
|
213
|
+
|
|
214
|
+
if (!heads) {
|
|
215
|
+
// Redirect to pinned-heads URL
|
|
216
|
+
const folder = await repo.find(maybeAutomergeUrl);
|
|
217
|
+
const latestHeads = folder.heads();
|
|
218
|
+
const url = stringifyAutomergeUrl({ documentId, heads: latestHeads });
|
|
219
|
+
let location = `/${encodeURIComponent(url)}`;
|
|
220
|
+
if (path.length) location += `/${path.join("/")}`;
|
|
221
|
+
return Response.redirect(location, 307);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// If no path, check if this is a package with exports to resolve
|
|
225
|
+
// e.g. /automerge%3Adocid/abc → resolve "abc" via package.json exports
|
|
226
|
+
const folderHandle = await repo.find<FolderDoc>(maybeAutomergeUrl);
|
|
227
|
+
|
|
228
|
+
let fileHandle;
|
|
229
|
+
if (path.length) {
|
|
230
|
+
// Try direct file navigation first
|
|
231
|
+
fileHandle = await findHandleInFolderHandle<FileDoc>(
|
|
232
|
+
repo,
|
|
233
|
+
folderHandle,
|
|
234
|
+
path.map(decodeURIComponent)
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// If not found as a direct path, try resolving as a package subpath export
|
|
238
|
+
// e.g. /automerge%3Adocid/abc → exports["./abc"] → "./dist/abc.js"
|
|
239
|
+
if (!fileHandle) {
|
|
240
|
+
const subpath = "./" + path.map(decodeURIComponent).join("/");
|
|
241
|
+
const pkgFileHandle = await findHandleInFolderHandle<FileDoc>(
|
|
242
|
+
repo,
|
|
243
|
+
folderHandle,
|
|
244
|
+
["package.json"]
|
|
245
|
+
);
|
|
246
|
+
if (pkgFileHandle) {
|
|
247
|
+
const pkgDoc = pkgFileHandle.doc() as FileDoc | undefined;
|
|
248
|
+
if (pkgDoc?.content) {
|
|
249
|
+
const pkgJson = JSON.parse(String(pkgDoc.content));
|
|
250
|
+
try {
|
|
251
|
+
const resolved = resolvePackageExport(pkgJson, subpath);
|
|
252
|
+
if (resolved) {
|
|
253
|
+
const resolvedPath = resolved.replace(/^\.\//, "").split("/");
|
|
254
|
+
fileHandle = await findHandleInFolderHandle<FileDoc>(
|
|
255
|
+
repo,
|
|
256
|
+
folderHandle,
|
|
257
|
+
resolvedPath
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
// not a valid export subpath, fall through to error
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
// No path — resolve the root export (like "." in package.json)
|
|
268
|
+
const pkgFileHandle = await findHandleInFolderHandle<FileDoc>(
|
|
269
|
+
repo,
|
|
270
|
+
folderHandle,
|
|
271
|
+
["package.json"]
|
|
272
|
+
);
|
|
273
|
+
if (pkgFileHandle) {
|
|
274
|
+
const pkgDoc = pkgFileHandle.doc() as FileDoc | undefined;
|
|
275
|
+
if (pkgDoc?.content) {
|
|
276
|
+
const pkgJson = JSON.parse(String(pkgDoc.content));
|
|
277
|
+
try {
|
|
278
|
+
const resolved = resolvePackageExport(pkgJson);
|
|
279
|
+
if (resolved) {
|
|
280
|
+
const resolvedPath = resolved.replace(/^\.\//, "").split("/");
|
|
281
|
+
fileHandle = await findHandleInFolderHandle<FileDoc>(
|
|
282
|
+
repo,
|
|
283
|
+
folderHandle,
|
|
284
|
+
resolvedPath
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
} catch {}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!fileHandle) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
`couldn't resolve ${path.join("/")} in folder at ${maybeAutomergeUrl}`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const fileDoc = fileHandle.doc() as unknown as FileDoc;
|
|
299
|
+
const content = fileDoc?.content;
|
|
300
|
+
if (!content) {
|
|
301
|
+
throw new Error(`file at ${href} has no content`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let body: BodyInit =
|
|
305
|
+
content instanceof Uint8Array
|
|
306
|
+
? (new Uint8Array(content) as BlobPart)
|
|
307
|
+
: String(content);
|
|
308
|
+
const mimeType = fileDoc.mimeType ?? "text/plain";
|
|
309
|
+
|
|
310
|
+
const headers = new Headers({ "content-type": mimeType });
|
|
311
|
+
headers.set("cross-origin-embedder-policy", "credentialless");
|
|
312
|
+
headers.set("cross-origin-resource-policy", "cross-origin");
|
|
313
|
+
|
|
314
|
+
return new Response(body, { status: 200, headers });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── Fetch handler ──────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
self.addEventListener("fetch", (fetchEvent: FetchEvent) => {
|
|
320
|
+
log("fetch event", fetchEvent.request.url);
|
|
88
321
|
const request = fetchEvent.request;
|
|
89
322
|
if (request.method !== "GET") return fetchEvent.respondWith(fetch(request));
|
|
90
323
|
const url = new URL(fetchEvent.request.url);
|
|
91
324
|
|
|
92
|
-
let
|
|
325
|
+
let specialURL: URL | undefined;
|
|
93
326
|
|
|
94
327
|
if (
|
|
95
328
|
url.hostname == self.location.hostname &&
|
|
@@ -97,10 +330,8 @@ self.addEventListener("fetch", async (fetchEvent: FetchEvent) => {
|
|
|
97
330
|
url.protocol == self.location.protocol
|
|
98
331
|
) {
|
|
99
332
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
handoffURL = new URL(decodeURIComponent(url.pathname.slice(1)));
|
|
103
|
-
log(`received handoff request ${handoffURL}`);
|
|
333
|
+
specialURL = new URL(decodeURIComponent(url.pathname.slice(1)));
|
|
334
|
+
log(`received special request ${specialURL}`);
|
|
104
335
|
} catch {}
|
|
105
336
|
}
|
|
106
337
|
|
|
@@ -110,66 +341,42 @@ self.addEventListener("fetch", async (fetchEvent: FetchEvent) => {
|
|
|
110
341
|
const match = await cache.match(request);
|
|
111
342
|
|
|
112
343
|
try {
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
responseResolvers.set(reqid, resolvers);
|
|
123
|
-
|
|
124
|
-
// i don't think this can happen
|
|
125
|
-
if (!client) {
|
|
126
|
-
throw new Error(
|
|
127
|
-
`the client has gone missing!!! ${fetchEvent.clientId}. i have NO IDEA what to do`
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
const message = {
|
|
131
|
-
id: reqid,
|
|
132
|
-
type: "request",
|
|
133
|
-
cache: cachename,
|
|
134
|
-
request: {
|
|
135
|
-
url: handoffURL.href,
|
|
136
|
-
headers: Object.fromEntries(request.headers.entries()),
|
|
137
|
-
method: request.method,
|
|
138
|
-
destination: request.destination,
|
|
139
|
-
referrer: request.referrer,
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
log("sending handoff request", message);
|
|
143
|
-
// send request event to main thread to ask them how to handle it
|
|
144
|
-
client.postMessage(message);
|
|
145
|
-
// this'll finish when the main thread gets back to us
|
|
146
|
-
fetchEvent.waitUntil(resolvers.promise);
|
|
147
|
-
const handoffResponse = await resolvers.promise;
|
|
148
|
-
log("received handoff response", handoffResponse);
|
|
149
|
-
if (handoffResponse) {
|
|
150
|
-
const response = new Response(handoffResponse.body, {
|
|
151
|
-
status: handoffResponse.status,
|
|
152
|
-
headers: handoffResponse.headers,
|
|
344
|
+
if (specialURL) {
|
|
345
|
+
if (match) {
|
|
346
|
+
log(`serving ${specialURL} from cache ${cachename}`);
|
|
347
|
+
const headers = new Headers(match.headers);
|
|
348
|
+
headers.set("cross-origin-embedder-policy", "credentialless");
|
|
349
|
+
headers.set("cross-origin-resource-policy", "cross-origin");
|
|
350
|
+
return new Response(match.body, {
|
|
351
|
+
status: match.status,
|
|
352
|
+
headers,
|
|
153
353
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const response = await resolveAutomergeUrl(specialURL);
|
|
357
|
+
|
|
358
|
+
if (response.status === 307) {
|
|
160
359
|
return response;
|
|
161
360
|
}
|
|
162
361
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
362
|
+
if (cacheableStatuses.includes(response.status)) {
|
|
363
|
+
log(`caching ${specialURL}`);
|
|
364
|
+
await cache.put(request, response.clone());
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return response;
|
|
167
368
|
} else {
|
|
168
|
-
|
|
169
|
-
const response = await fetch(request);
|
|
369
|
+
const response = await fetch(request).catch(() => null);
|
|
170
370
|
if (response) {
|
|
171
|
-
if (
|
|
371
|
+
if (
|
|
372
|
+
cacheableStatuses.includes(response.status) &&
|
|
373
|
+
response.url.match(/^https?\:/)
|
|
374
|
+
) {
|
|
172
375
|
await cache.put(request, response.clone());
|
|
376
|
+
} else {
|
|
377
|
+
log(
|
|
378
|
+
`skipping uncacheable response code from cache: ${response.status} for ${response.url}`
|
|
379
|
+
);
|
|
173
380
|
}
|
|
174
381
|
return response;
|
|
175
382
|
}
|
|
@@ -177,10 +384,19 @@ self.addEventListener("fetch", async (fetchEvent: FetchEvent) => {
|
|
|
177
384
|
return new Response("couldnt fetch and no stale", { status: 503 });
|
|
178
385
|
}
|
|
179
386
|
} catch (error) {
|
|
387
|
+
const message =
|
|
388
|
+
error instanceof Error
|
|
389
|
+
? `${error.message}\n\n${error.stack}`
|
|
390
|
+
: String(error);
|
|
391
|
+
console.error(
|
|
392
|
+
`service worker error resolving ${request.url}${specialURL ? ` (for: ${specialURL})` : ""}.\n${message}`
|
|
393
|
+
);
|
|
180
394
|
if (match) return match;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
395
|
+
|
|
396
|
+
return new Response(message, {
|
|
397
|
+
status: 500,
|
|
398
|
+
headers: { "content-type": "text/plain" },
|
|
399
|
+
});
|
|
184
400
|
}
|
|
185
401
|
})()
|
|
186
402
|
);
|
package/src/setup.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
HandoffHandler,
|
|
3
|
-
HandoffRequestMessage,
|
|
4
|
-
HandoffResponse,
|
|
5
|
-
SetupServiceWorkerOptions,
|
|
6
|
-
} from "./types.js";
|
|
1
|
+
import type { SetupServiceWorkerOptions } from "./types.js";
|
|
7
2
|
import debug from "debug";
|
|
8
3
|
|
|
9
4
|
const debugging = debug.enabled("patchwork:serviceworker");
|
|
@@ -43,89 +38,87 @@ export function bumpServiceWorkerCache(
|
|
|
43
38
|
setServiceWorkerCacheName(sw);
|
|
44
39
|
}
|
|
45
40
|
|
|
41
|
+
(window as any).bumpServiceWorkerCache = bumpServiceWorkerCache;
|
|
42
|
+
|
|
43
|
+
/** Wait for a registration to have an active worker */
|
|
44
|
+
function waitForActive(reg: ServiceWorkerRegistration): Promise<ServiceWorker> {
|
|
45
|
+
if (reg.active) return Promise.resolve(reg.active);
|
|
46
|
+
const worker = reg.installing || reg.waiting;
|
|
47
|
+
if (!worker)
|
|
48
|
+
return Promise.reject(new Error("no service worker in registration"));
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
worker.addEventListener("statechange", () => {
|
|
51
|
+
if (worker.state === "activated") resolve(worker);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
export default async function setupServiceWorker(
|
|
47
|
-
handler: HandoffHandler,
|
|
48
57
|
options?: SetupServiceWorkerOptions
|
|
49
58
|
) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
// Backwards compat: if an old service worker sends handoff "request" messages,
|
|
60
|
+
// immediately reject them so its fetch handler doesn't hang forever.
|
|
61
|
+
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
62
|
+
if (event.data?.type === "request" && event.data.id != null) {
|
|
63
|
+
navigator.serviceWorker.controller?.postMessage({
|
|
64
|
+
type: "response",
|
|
65
|
+
id: event.data.id,
|
|
66
|
+
response: {
|
|
67
|
+
body: "service worker upgraded, please refresh",
|
|
68
|
+
status: 503,
|
|
69
|
+
headers: { "content-type": "text/plain" },
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
57
73
|
});
|
|
58
74
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const requestMessage: HandoffRequestMessage = event.data;
|
|
62
|
-
const source = event.source;
|
|
63
|
-
|
|
64
|
-
if (!source) {
|
|
65
|
-
throw new TypeError("can't operate without a source");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function send(
|
|
69
|
-
response: HandoffResponse,
|
|
70
|
-
transfer?: Transferable[]
|
|
71
|
-
) {
|
|
72
|
-
source!.postMessage(
|
|
73
|
-
{
|
|
74
|
-
id: requestMessage.id,
|
|
75
|
-
type: "response",
|
|
76
|
-
response,
|
|
77
|
-
},
|
|
78
|
-
{ transfer }
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const handoffResponse = await handler(
|
|
83
|
-
requestMessage.request.url,
|
|
84
|
-
requestMessage.request
|
|
85
|
-
);
|
|
75
|
+
const path = options?.path ?? "/service-worker.js";
|
|
76
|
+
const reg = await navigator.serviceWorker.register(path, { type: "module" });
|
|
86
77
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
// If there's an update waiting or installing, wait for it to activate
|
|
79
|
+
if (reg.installing || reg.waiting) {
|
|
80
|
+
await waitForActive(reg);
|
|
81
|
+
}
|
|
90
82
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
83
|
+
const active = reg.active!;
|
|
84
|
+
active.postMessage({ type: "debug", debug: debugging });
|
|
94
85
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
86
|
+
// Wait for the controller to be available
|
|
87
|
+
if (!navigator.serviceWorker.controller) {
|
|
88
|
+
await new Promise<void>((resolve) => {
|
|
89
|
+
navigator.serviceWorker.addEventListener(
|
|
90
|
+
"controllerchange",
|
|
91
|
+
() => resolve(),
|
|
92
|
+
{ once: true }
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
// Send a MessagePort so the SW's repo can sync with clients
|
|
98
|
+
const { port1, port2 } = new MessageChannel();
|
|
99
|
+
navigator.serviceWorker.controller!.postMessage({ type: "port" }, [port2]);
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
// Keepalive — Chromium idles out service workers after ~30s of inactivity,
|
|
102
|
+
// which tears down the in-memory Repo and forces a cold restart on the next
|
|
103
|
+
// fetch. Send a ping every 20s while the page is visible to keep it warm.
|
|
104
|
+
setInterval(() => {
|
|
105
|
+
navigator.serviceWorker.controller?.postMessage({ type: "ping" });
|
|
106
|
+
}, 20_000);
|
|
102
107
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
// Reload on future SW updates (added after setup so the initial
|
|
109
|
+
// activation doesn't trigger a reload loop).
|
|
110
|
+
navigator.serviceWorker.addEventListener("controllerchange", function () {
|
|
111
|
+
console.info(
|
|
112
|
+
"%cnew service worker took control, reloading...",
|
|
113
|
+
"color: pink; font-weight: bold"
|
|
114
|
+
);
|
|
115
|
+
location.reload();
|
|
108
116
|
});
|
|
109
117
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.then(async (sw) => {
|
|
115
|
-
sw.active?.postMessage({
|
|
116
|
-
type: "debug",
|
|
117
|
-
debug: debugging,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
if (!existingSw?.active) {
|
|
121
|
-
bumpServiceWorkerCache(sw.installing);
|
|
122
|
-
queueMicrotask(() => location.reload());
|
|
123
|
-
return sw.active!;
|
|
124
|
-
}
|
|
118
|
+
console.log(
|
|
119
|
+
"service worker alive, loading %c patchwork system ",
|
|
120
|
+
"background: #fcf2f0; color: #333; border: 2px solid; border-radius: 4px"
|
|
121
|
+
);
|
|
125
122
|
|
|
126
|
-
|
|
127
|
-
"service worker alive, loading %c patchwork system ",
|
|
128
|
-
"background: #fff8f0; border: 1px solid; border-radius: 4px"
|
|
129
|
-
);
|
|
130
|
-
});
|
|
123
|
+
return { port: port1 };
|
|
131
124
|
}
|