@inkandswitch/patchwork-bootloader 0.0.3 → 0.0.5
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/CHANGELOG.md +6 -0
- 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/dist/generate.d.ts +0 -170
- package/dist/generate.js +0 -83
- package/dist/patchwork-vite-plugin.d.ts +0 -40
- package/dist/patchwork-vite-plugin.js +0 -192
package/CHANGELOG.md
CHANGED
package/dist/externals.js
CHANGED
|
@@ -7,6 +7,8 @@ const externals = [
|
|
|
7
7
|
"@automerge/automerge-repo",
|
|
8
8
|
"@automerge/automerge-repo/slim",
|
|
9
9
|
"@automerge/automerge-repo-keyhive",
|
|
10
|
+
"@automerge/automerge-subduction",
|
|
11
|
+
"@automerge/automerge-subduction/slim",
|
|
10
12
|
"@keyhive/keyhive",
|
|
11
13
|
"@keyhive/keyhive/slim",
|
|
12
14
|
"@inkandswitch/patchwork-bootloader",
|
|
@@ -16,5 +18,13 @@ const externals = [
|
|
|
16
18
|
// sad
|
|
17
19
|
"@codemirror/state",
|
|
18
20
|
"@codemirror/view",
|
|
21
|
+
"@codemirror/language",
|
|
22
|
+
// rip
|
|
23
|
+
"solid-js",
|
|
24
|
+
"solid-js/html",
|
|
25
|
+
"solid-js/web",
|
|
26
|
+
"solid-js/h",
|
|
27
|
+
"solid-js/store",
|
|
28
|
+
"solid-js/jsx-runtime",
|
|
19
29
|
];
|
|
20
30
|
export default externals;
|
package/dist/service-worker.js
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
/// <reference types="service-worker-types" />
|
|
2
|
+
import { SwLogger } from "./sw-logger.js";
|
|
3
|
+
// Heavy imports — marked external by the service-worker vite plugin,
|
|
4
|
+
// resolved to /packages/... URLs at build time. The SW is registered with
|
|
5
|
+
// type:"module" so the browser fetches these as regular network requests.
|
|
6
|
+
// Uses /slim to avoid top-level await (disallowed in service workers).
|
|
7
|
+
// Wasm is fetched from /automerge.wasm (emitted by the vite plugin) instead
|
|
8
|
+
// of bundling the ~3MB base64 string.
|
|
9
|
+
import { initializeWasm } from "@automerge/automerge/slim";
|
|
10
|
+
// eslint-disable-next-line
|
|
11
|
+
// @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
|
|
12
|
+
import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
|
|
13
|
+
import { WebCryptoSigner } from "@automerge/automerge-subduction/slim";
|
|
14
|
+
import { Repo, isValidAutomergeUrl, parseAutomergeUrl, stringifyAutomergeUrl, } from "@automerge/automerge-repo/slim";
|
|
15
|
+
import { findHandleInFolderHandle, resolvePackageExport, } from "@inkandswitch/patchwork-filesystem";
|
|
16
|
+
// Small adapters — bundled directly into the SW
|
|
17
|
+
import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
|
|
18
|
+
import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel";
|
|
19
|
+
import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
|
|
20
|
+
// TEMPORARY: enable debug npm module in SW context (no localStorage available)
|
|
2
21
|
let cachename = "default";
|
|
3
22
|
let debugging = false;
|
|
23
|
+
const SUBDUCTION_ENDPOINTS = ["wss://subduction.sync.inkandswitch.com"];
|
|
24
|
+
// ── Persistent logger ───────────────────────────────────────────────────
|
|
25
|
+
// Initialized eagerly so it's available for the entire SW lifetime.
|
|
26
|
+
// Access from the SW inspector console via self.printLogs(), self.tailLogs(),
|
|
27
|
+
// self.exportLogs(), self.clearLogs().
|
|
28
|
+
const slog = SwLogger.open().then((logger) => {
|
|
29
|
+
self.slog = logger;
|
|
30
|
+
self.printLogs = async (n = 200) => {
|
|
31
|
+
const entries = await logger.tail(n);
|
|
32
|
+
for (const e of entries) {
|
|
33
|
+
const prefix = `[${e.ts}] [${e.level}]`;
|
|
34
|
+
if (e.data !== undefined) {
|
|
35
|
+
console.log(prefix, e.msg, e.data);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.log(prefix, e.msg);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
console.log(`--- ${entries.length} entries ---`);
|
|
42
|
+
};
|
|
43
|
+
self.tailLogs = (n = 200) => logger.tail(n);
|
|
44
|
+
self.exportLogs = () => logger.exportAll();
|
|
45
|
+
self.clearLogs = () => logger.clear();
|
|
46
|
+
logger.info("sw-logger initialized");
|
|
47
|
+
return logger;
|
|
48
|
+
});
|
|
49
|
+
const cacheableStatuses = [
|
|
50
|
+
200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501,
|
|
51
|
+
];
|
|
4
52
|
function log(...args) {
|
|
5
53
|
if (!debugging)
|
|
6
54
|
return;
|
|
@@ -21,33 +69,72 @@ self.addEventListener("activate", async () => {
|
|
|
21
69
|
await clearOldCaches();
|
|
22
70
|
clients.claim();
|
|
23
71
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
72
|
+
let repoPromise = null;
|
|
73
|
+
function getRepo() {
|
|
74
|
+
if (!repoPromise) {
|
|
75
|
+
repoPromise = (async () => {
|
|
76
|
+
const logger = await slog;
|
|
77
|
+
logger.info("fetching wasm modules");
|
|
78
|
+
const [amWasmBuf, sdnWasmBuf] = await Promise.all([
|
|
79
|
+
fetch("/automerge.wasm").then((r) => r.arrayBuffer()),
|
|
80
|
+
fetch("/subduction.wasm").then((r) => r.arrayBuffer()),
|
|
81
|
+
]);
|
|
82
|
+
initSubductionSync(new Uint8Array(sdnWasmBuf));
|
|
83
|
+
await initializeWasm(new Uint8Array(amWasmBuf));
|
|
84
|
+
logger.info("wasm initialized");
|
|
85
|
+
const signer = await WebCryptoSigner.setup();
|
|
86
|
+
const repo = new Repo({
|
|
87
|
+
storage: new IndexedDBStorageAdapter(),
|
|
88
|
+
signer,
|
|
89
|
+
peerId: ("service-worker-" +
|
|
90
|
+
(Math.random() * 10000).toString(36).slice(2)),
|
|
91
|
+
async sharePolicy(peerId) {
|
|
92
|
+
return peerId.includes("storage-server");
|
|
93
|
+
},
|
|
94
|
+
enableRemoteHeadsGossiping: true,
|
|
95
|
+
subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
|
|
96
|
+
network: [new WebSocketClientAdapter("wss://sync3.automerge.org")],
|
|
97
|
+
});
|
|
98
|
+
self.repo = repo;
|
|
99
|
+
logger.info("repo constructed, waiting for network subsystem");
|
|
100
|
+
// Don't block getRepo() on whenReady() — the network subsystem starts
|
|
101
|
+
// with only the subduction adapter, and the MessageChannel adapter is
|
|
102
|
+
// added later via connectPort (which awaits getRepo). Blocking here
|
|
103
|
+
// would deadlock that path and starve the fetch handler.
|
|
104
|
+
repo.networkSubsystem.whenReady().then(() => {
|
|
105
|
+
logger.info("repo network subsystem ready");
|
|
106
|
+
});
|
|
107
|
+
return repo;
|
|
108
|
+
})();
|
|
30
109
|
}
|
|
31
|
-
return
|
|
110
|
+
return repoPromise;
|
|
111
|
+
}
|
|
112
|
+
// Connect client MessagePorts to the repo for sync
|
|
113
|
+
async function connectPort(port) {
|
|
114
|
+
const repo = await getRepo();
|
|
115
|
+
repo.networkSubsystem.addNetworkAdapter(new MessageChannelNetworkAdapter(port, { useWeakRef: true }));
|
|
32
116
|
}
|
|
33
|
-
const bc = new BroadcastChannel("@patchwork/handoff");
|
|
34
|
-
bc.addEventListener("message", (event) => {
|
|
35
|
-
if (event.data.type == "response")
|
|
36
|
-
accept(event.data);
|
|
37
|
-
});
|
|
38
|
-
// when we receive a `response` req, we resolve the promise with that id
|
|
39
117
|
self.addEventListener("message", async (event) => {
|
|
40
|
-
if (event.data.type == "
|
|
41
|
-
|
|
118
|
+
if (event.data.type == "ping") {
|
|
119
|
+
// Keepalive — Chromium idles out service workers after ~30s of inactivity.
|
|
120
|
+
// Reply via the provided port if any; the message event itself also resets
|
|
121
|
+
// the idle timer.
|
|
122
|
+
const [pongPort] = event.ports;
|
|
123
|
+
log("ping");
|
|
124
|
+
if (pongPort) {
|
|
125
|
+
pongPort.postMessage({ type: "pong" });
|
|
126
|
+
log("pong");
|
|
127
|
+
pongPort.close();
|
|
128
|
+
}
|
|
129
|
+
else if (event.source) {
|
|
130
|
+
event.source.postMessage({ type: "pong" });
|
|
131
|
+
log("pong");
|
|
132
|
+
}
|
|
42
133
|
}
|
|
43
134
|
else if (event.data.type == "port") {
|
|
44
|
-
log("
|
|
135
|
+
log("received messagechannel");
|
|
45
136
|
const [port] = event.ports;
|
|
46
|
-
port
|
|
47
|
-
if (event.data.type == "response") {
|
|
48
|
-
accept(event.data);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
137
|
+
connectPort(port);
|
|
51
138
|
}
|
|
52
139
|
else if (event.data.type == "cachename") {
|
|
53
140
|
const nextCachename = event.data.cachename;
|
|
@@ -63,22 +150,107 @@ self.addEventListener("message", async (event) => {
|
|
|
63
150
|
log("serviceworker debugging enabled");
|
|
64
151
|
}
|
|
65
152
|
});
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
153
|
+
// ── Automerge URL resolution ───────────────────────────────────────────
|
|
154
|
+
async function resolveAutomergeUrl(automergeURL) {
|
|
155
|
+
const repo = await getRepo();
|
|
156
|
+
const href = automergeURL.href;
|
|
157
|
+
const [maybeAutomergeUrl, ...path] = href.split("/");
|
|
158
|
+
if (!isValidAutomergeUrl(maybeAutomergeUrl)) {
|
|
159
|
+
return new Response("invalid automerge url", { status: 400 });
|
|
160
|
+
}
|
|
161
|
+
// Trim trailing empty path segment
|
|
162
|
+
if (path.length && !path[path.length - 1])
|
|
163
|
+
path.pop();
|
|
164
|
+
const { heads, documentId } = parseAutomergeUrl(maybeAutomergeUrl);
|
|
165
|
+
if (!heads) {
|
|
166
|
+
// Redirect to pinned-heads URL
|
|
167
|
+
const folder = await repo.find(maybeAutomergeUrl);
|
|
168
|
+
const latestHeads = folder.heads();
|
|
169
|
+
const url = stringifyAutomergeUrl({ documentId, heads: latestHeads });
|
|
170
|
+
let location = `/${encodeURIComponent(url)}`;
|
|
171
|
+
if (path.length)
|
|
172
|
+
location += `/${path.join("/")}`;
|
|
173
|
+
return Response.redirect(location, 307);
|
|
174
|
+
}
|
|
175
|
+
// If no path, check if this is a package with exports to resolve
|
|
176
|
+
// e.g. /automerge%3Adocid/abc → resolve "abc" via package.json exports
|
|
177
|
+
const folderHandle = await repo.find(maybeAutomergeUrl);
|
|
178
|
+
let fileHandle;
|
|
179
|
+
if (path.length) {
|
|
180
|
+
// Try direct file navigation first
|
|
181
|
+
fileHandle = await findHandleInFolderHandle(repo, folderHandle, path.map(decodeURIComponent));
|
|
182
|
+
// If not found as a direct path, try resolving as a package subpath export
|
|
183
|
+
// e.g. /automerge%3Adocid/abc → exports["./abc"] → "./dist/abc.js"
|
|
184
|
+
if (!fileHandle) {
|
|
185
|
+
const subpath = "./" + path.map(decodeURIComponent).join("/");
|
|
186
|
+
const pkgFileHandle = await findHandleInFolderHandle(repo, folderHandle, ["package.json"]);
|
|
187
|
+
if (pkgFileHandle) {
|
|
188
|
+
const pkgDoc = pkgFileHandle.doc();
|
|
189
|
+
if (pkgDoc?.content) {
|
|
190
|
+
const pkgJson = JSON.parse(String(pkgDoc.content));
|
|
191
|
+
try {
|
|
192
|
+
const resolved = resolvePackageExport(pkgJson, subpath);
|
|
193
|
+
if (resolved) {
|
|
194
|
+
const resolvedPath = resolved.replace(/^\.\//, "").split("/");
|
|
195
|
+
fileHandle = await findHandleInFolderHandle(repo, folderHandle, resolvedPath);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// not a valid export subpath, fall through to error
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// No path — resolve the root export (like "." in package.json)
|
|
207
|
+
const pkgFileHandle = await findHandleInFolderHandle(repo, folderHandle, ["package.json"]);
|
|
208
|
+
if (pkgFileHandle) {
|
|
209
|
+
const pkgDoc = pkgFileHandle.doc();
|
|
210
|
+
if (pkgDoc?.content) {
|
|
211
|
+
const pkgJson = JSON.parse(String(pkgDoc.content));
|
|
212
|
+
try {
|
|
213
|
+
const resolved = resolvePackageExport(pkgJson);
|
|
214
|
+
if (resolved) {
|
|
215
|
+
const resolvedPath = resolved.replace(/^\.\//, "").split("/");
|
|
216
|
+
fileHandle = await findHandleInFolderHandle(repo, folderHandle, resolvedPath);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch { }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!fileHandle) {
|
|
224
|
+
throw new Error(`couldn't resolve ${path.join("/")} in folder at ${maybeAutomergeUrl}`);
|
|
225
|
+
}
|
|
226
|
+
const fileDoc = fileHandle.doc();
|
|
227
|
+
const content = fileDoc?.content;
|
|
228
|
+
if (!content) {
|
|
229
|
+
throw new Error(`file at ${href} has no content`);
|
|
230
|
+
}
|
|
231
|
+
let body = content instanceof Uint8Array
|
|
232
|
+
? new Uint8Array(content)
|
|
233
|
+
: String(content);
|
|
234
|
+
const mimeType = fileDoc.mimeType ?? "text/plain";
|
|
235
|
+
const headers = new Headers({ "content-type": mimeType });
|
|
236
|
+
headers.set("cross-origin-embedder-policy", "credentialless");
|
|
237
|
+
headers.set("cross-origin-resource-policy", "cross-origin");
|
|
238
|
+
return new Response(body, { status: 200, headers });
|
|
239
|
+
}
|
|
240
|
+
// ── Fetch handler ──────────────────────────────────────────────────────
|
|
241
|
+
self.addEventListener("fetch", (fetchEvent) => {
|
|
242
|
+
log("fetch event", fetchEvent.request.url);
|
|
69
243
|
const request = fetchEvent.request;
|
|
70
244
|
if (request.method !== "GET")
|
|
71
245
|
return fetchEvent.respondWith(fetch(request));
|
|
72
246
|
const url = new URL(fetchEvent.request.url);
|
|
73
|
-
let
|
|
247
|
+
let specialURL;
|
|
74
248
|
if (url.hostname == self.location.hostname &&
|
|
75
249
|
url.port == self.location.port &&
|
|
76
250
|
url.protocol == self.location.protocol) {
|
|
77
251
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
handoffURL = new URL(decodeURIComponent(url.pathname.slice(1)));
|
|
81
|
-
log(`received handoff request ${handoffURL}`);
|
|
252
|
+
specialURL = new URL(decodeURIComponent(url.pathname.slice(1)));
|
|
253
|
+
log(`received special request ${specialURL}`);
|
|
82
254
|
}
|
|
83
255
|
catch { }
|
|
84
256
|
}
|
|
@@ -86,65 +258,37 @@ self.addEventListener("fetch", async (fetchEvent) => {
|
|
|
86
258
|
const cache = await caches.open(cachename);
|
|
87
259
|
const match = await cache.match(request);
|
|
88
260
|
try {
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
responseResolvers.set(reqid, resolvers);
|
|
99
|
-
// i don't think this can happen
|
|
100
|
-
if (!client) {
|
|
101
|
-
throw new Error(`the client has gone missing!!! ${fetchEvent.clientId}. i have NO IDEA what to do`);
|
|
102
|
-
}
|
|
103
|
-
const message = {
|
|
104
|
-
id: reqid,
|
|
105
|
-
type: "request",
|
|
106
|
-
cache: cachename,
|
|
107
|
-
request: {
|
|
108
|
-
url: handoffURL.href,
|
|
109
|
-
headers: Object.fromEntries(request.headers.entries()),
|
|
110
|
-
method: request.method,
|
|
111
|
-
destination: request.destination,
|
|
112
|
-
referrer: request.referrer,
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
log("sending handoff request", message);
|
|
116
|
-
// send request event to main thread to ask them how to handle it
|
|
117
|
-
client.postMessage(message);
|
|
118
|
-
// this'll finish when the main thread gets back to us
|
|
119
|
-
fetchEvent.waitUntil(resolvers.promise);
|
|
120
|
-
const handoffResponse = await resolvers.promise;
|
|
121
|
-
log("received handoff response", handoffResponse);
|
|
122
|
-
if (handoffResponse) {
|
|
123
|
-
const response = new Response(handoffResponse.body, {
|
|
124
|
-
status: handoffResponse.status,
|
|
125
|
-
headers: handoffResponse.headers,
|
|
261
|
+
if (specialURL) {
|
|
262
|
+
if (match) {
|
|
263
|
+
log(`serving ${specialURL} from cache ${cachename}`);
|
|
264
|
+
const headers = new Headers(match.headers);
|
|
265
|
+
headers.set("cross-origin-embedder-policy", "credentialless");
|
|
266
|
+
headers.set("cross-origin-resource-policy", "cross-origin");
|
|
267
|
+
return new Response(match.body, {
|
|
268
|
+
status: match.status,
|
|
269
|
+
headers,
|
|
126
270
|
});
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
log(`caching disabled on ${handoffURL}`);
|
|
133
|
-
}
|
|
271
|
+
}
|
|
272
|
+
const response = await resolveAutomergeUrl(specialURL);
|
|
273
|
+
if (response.status === 307) {
|
|
134
274
|
return response;
|
|
135
275
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
276
|
+
if (cacheableStatuses.includes(response.status)) {
|
|
277
|
+
log(`caching ${specialURL}`);
|
|
278
|
+
await cache.put(request, response.clone());
|
|
279
|
+
}
|
|
280
|
+
return response;
|
|
140
281
|
}
|
|
141
282
|
else {
|
|
142
|
-
|
|
143
|
-
const response = await fetch(request);
|
|
283
|
+
const response = await fetch(request).catch(() => null);
|
|
144
284
|
if (response) {
|
|
145
|
-
if (
|
|
285
|
+
if (cacheableStatuses.includes(response.status) &&
|
|
286
|
+
response.url.match(/^https?\:/)) {
|
|
146
287
|
await cache.put(request, response.clone());
|
|
147
288
|
}
|
|
289
|
+
else {
|
|
290
|
+
log(`skipping uncacheable response code from cache: ${response.status} for ${response.url}`);
|
|
291
|
+
}
|
|
148
292
|
return response;
|
|
149
293
|
}
|
|
150
294
|
if (match)
|
|
@@ -153,12 +297,16 @@ self.addEventListener("fetch", async (fetchEvent) => {
|
|
|
153
297
|
}
|
|
154
298
|
}
|
|
155
299
|
catch (error) {
|
|
300
|
+
const message = error instanceof Error
|
|
301
|
+
? `${error.message}\n\n${error.stack}`
|
|
302
|
+
: String(error);
|
|
303
|
+
console.error(`service worker error resolving ${request.url}${specialURL ? ` (for: ${specialURL})` : ""}.\n${message}`);
|
|
156
304
|
if (match)
|
|
157
305
|
return match;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
306
|
+
return new Response(message, {
|
|
307
|
+
status: 500,
|
|
308
|
+
headers: { "content-type": "text/plain" },
|
|
309
|
+
});
|
|
161
310
|
}
|
|
162
311
|
})());
|
|
163
312
|
});
|
|
164
|
-
export {};
|
package/dist/setup.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SetupServiceWorkerOptions } from "./types.js";
|
|
2
2
|
export declare function bumpServiceWorkerCache(sw?: ServiceWorker | null): void;
|
|
3
|
-
export default function setupServiceWorker(
|
|
3
|
+
export default function setupServiceWorker(options?: SetupServiceWorkerOptions): Promise<{
|
|
4
|
+
port: MessagePort;
|
|
5
|
+
}>;
|
package/dist/setup.js
CHANGED
|
@@ -28,54 +28,66 @@ export function bumpServiceWorkerCache(sw = navigator.serviceWorker.controller)
|
|
|
28
28
|
bumpServiceWorkerCacheVersion();
|
|
29
29
|
setServiceWorkerCacheName(sw);
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
window.bumpServiceWorkerCache = bumpServiceWorkerCache;
|
|
32
|
+
/** Wait for a registration to have an active worker */
|
|
33
|
+
function waitForActive(reg) {
|
|
34
|
+
if (reg.active)
|
|
35
|
+
return Promise.resolve(reg.active);
|
|
36
|
+
const worker = reg.installing || reg.waiting;
|
|
37
|
+
if (!worker)
|
|
38
|
+
return Promise.reject(new Error("no service worker in registration"));
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
worker.addEventListener("statechange", () => {
|
|
41
|
+
if (worker.state === "activated")
|
|
42
|
+
resolve(worker);
|
|
43
|
+
});
|
|
36
44
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!handoffResponse) {
|
|
53
|
-
return source?.postMessage({ id: requestMessage.id, type: "response" });
|
|
54
|
-
}
|
|
55
|
-
if (typeof handoffResponse == "string") {
|
|
56
|
-
return send({ body: handoffResponse }, [handoffResponse]);
|
|
57
|
-
}
|
|
58
|
-
if (handoffResponse instanceof Uint8Array) {
|
|
59
|
-
return send({ body: handoffResponse }, [handoffResponse.buffer]);
|
|
60
|
-
}
|
|
61
|
-
const { body: handoffBody, headers, status, cache } = handoffResponse;
|
|
62
|
-
const body = handoffBody;
|
|
63
|
-
send({ body, headers, status, cache }, body instanceof Uint8Array ? [body.buffer] : undefined);
|
|
45
|
+
}
|
|
46
|
+
export default async function setupServiceWorker(options) {
|
|
47
|
+
// Backwards compat: if an old service worker sends handoff "request" messages,
|
|
48
|
+
// immediately reject them so its fetch handler doesn't hang forever.
|
|
49
|
+
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
50
|
+
if (event.data?.type === "request" && event.data.id != null) {
|
|
51
|
+
navigator.serviceWorker.controller?.postMessage({
|
|
52
|
+
type: "response",
|
|
53
|
+
id: event.data.id,
|
|
54
|
+
response: {
|
|
55
|
+
body: "service worker upgraded, please refresh",
|
|
56
|
+
status: 503,
|
|
57
|
+
headers: { "content-type": "text/plain" },
|
|
58
|
+
},
|
|
59
|
+
});
|
|
64
60
|
}
|
|
65
61
|
});
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
62
|
+
const path = options?.path ?? "/service-worker.js";
|
|
63
|
+
const reg = await navigator.serviceWorker.register(path, { type: "module" });
|
|
64
|
+
// If there's an update waiting or installing, wait for it to activate
|
|
65
|
+
if (reg.installing || reg.waiting) {
|
|
66
|
+
await waitForActive(reg);
|
|
67
|
+
}
|
|
68
|
+
const active = reg.active;
|
|
69
|
+
active.postMessage({ type: "debug", debug: debugging });
|
|
70
|
+
// Wait for the controller to be available
|
|
71
|
+
if (!navigator.serviceWorker.controller) {
|
|
72
|
+
await new Promise((resolve) => {
|
|
73
|
+
navigator.serviceWorker.addEventListener("controllerchange", () => resolve(), { once: true });
|
|
73
74
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
}
|
|
76
|
+
// Send a MessagePort so the SW's repo can sync with clients
|
|
77
|
+
const { port1, port2 } = new MessageChannel();
|
|
78
|
+
navigator.serviceWorker.controller.postMessage({ type: "port" }, [port2]);
|
|
79
|
+
// Keepalive — Chromium idles out service workers after ~30s of inactivity,
|
|
80
|
+
// which tears down the in-memory Repo and forces a cold restart on the next
|
|
81
|
+
// fetch. Send a ping every 20s while the page is visible to keep it warm.
|
|
82
|
+
setInterval(() => {
|
|
83
|
+
navigator.serviceWorker.controller?.postMessage({ type: "ping" });
|
|
84
|
+
}, 20_000);
|
|
85
|
+
// Reload on future SW updates (added after setup so the initial
|
|
86
|
+
// activation doesn't trigger a reload loop).
|
|
87
|
+
navigator.serviceWorker.addEventListener("controllerchange", function () {
|
|
88
|
+
console.info("%cnew service worker took control, reloading...", "color: pink; font-weight: bold");
|
|
89
|
+
location.reload();
|
|
80
90
|
});
|
|
91
|
+
console.log("service worker alive, loading %c patchwork system ", "background: #fcf2f0; color: #333; border: 2px solid; border-radius: 4px");
|
|
92
|
+
return { port: port1 };
|
|
81
93
|
}
|
package/dist/site.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level browser-app boot sequence for a Patchwork site.
|
|
3
|
+
*
|
|
4
|
+
* Layers on top of {@link setupServiceWorker} (the package default export) to
|
|
5
|
+
* construct the Repo, wire up the service-worker port, load plugins via the
|
|
6
|
+
* ModuleWatcher, resolve the user's account document, and hand control to the
|
|
7
|
+
* configured root tool.
|
|
8
|
+
*
|
|
9
|
+
* This entry point pulls in DOM- and plugin-layer dependencies (patchwork
|
|
10
|
+
* elements, plugins, filesystem) and is intended for use only from a browser
|
|
11
|
+
* site's `main.ts`. Non-UI consumers should import the package default (which
|
|
12
|
+
* only does SW registration and port handoff).
|
|
13
|
+
*/
|
|
14
|
+
import { type DocHandle, Repo, type AutomergeUrl, type StorageId } from "@automerge/vanillajs/slim";
|
|
15
|
+
import { ModuleWatcher } from "@inkandswitch/patchwork-filesystem";
|
|
16
|
+
import { type AccountDoc } from "@inkandswitch/patchwork-plugins";
|
|
17
|
+
import * as plugins from "@inkandswitch/patchwork-plugins";
|
|
18
|
+
import { SwLogReader } from "./sw-logger.js";
|
|
19
|
+
declare global {
|
|
20
|
+
interface Window {
|
|
21
|
+
accountDocHandle: DocHandle<AccountDoc>;
|
|
22
|
+
Automerge: typeof import("@automerge/automerge");
|
|
23
|
+
AutomergeRepo: typeof import("@automerge/automerge-repo");
|
|
24
|
+
repo: Repo;
|
|
25
|
+
getRepoChannel: () => MessagePort;
|
|
26
|
+
patchwork: {
|
|
27
|
+
repo: Repo;
|
|
28
|
+
modules: ModuleWatcher;
|
|
29
|
+
plugins: typeof plugins;
|
|
30
|
+
accountDocHandle: DocHandle<AccountDoc>;
|
|
31
|
+
sw: {
|
|
32
|
+
printLogs: (n?: number) => Promise<void>;
|
|
33
|
+
tailLogs: (n?: number) => ReturnType<typeof SwLogReader.tail>;
|
|
34
|
+
exportLogs: () => Promise<string>;
|
|
35
|
+
clearLogs: () => Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
uncache: (match: string) => Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export interface SiteConfig {
|
|
42
|
+
/**
|
|
43
|
+
* Automerge URL of the site's default module-settings document — the bundle
|
|
44
|
+
* of tools every user of this site gets out of the box. Must contribute at
|
|
45
|
+
* least a `patchwork:datatype` registration for `"account"` (typically the
|
|
46
|
+
* one supplied by `@inkandswitch/patchwork-frame`).
|
|
47
|
+
*
|
|
48
|
+
* Can be overridden at runtime by setting `localStorage.defaultToolsUrl` to
|
|
49
|
+
* another automerge: URL — useful for local development against an
|
|
50
|
+
* unpublished tool set.
|
|
51
|
+
*/
|
|
52
|
+
defaultModulesUrl: AutomergeUrl;
|
|
53
|
+
/**
|
|
54
|
+
* `localStorage` key under which this site remembers which account document
|
|
55
|
+
* belongs to the current user. Sites sharing an origin MUST use distinct
|
|
56
|
+
* keys so they do not clobber each other's accounts.
|
|
57
|
+
*/
|
|
58
|
+
accountStorageKey: string;
|
|
59
|
+
/**
|
|
60
|
+
* Brand word appended to the document title as `"<doc> | <titleSuffix>"`
|
|
61
|
+
* when a document is open. The separator is provided for you.
|
|
62
|
+
*/
|
|
63
|
+
titleSuffix: string;
|
|
64
|
+
/**
|
|
65
|
+
* DOM id of the `<patchwork-view>` element that will host the root tool.
|
|
66
|
+
* Defaults to `"root"`.
|
|
67
|
+
*/
|
|
68
|
+
rootElementId?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Storage IDs to subscribe to for remote-heads gossiping. Defaults to
|
|
71
|
+
* Ink & Switch's production Subduction storage.
|
|
72
|
+
*/
|
|
73
|
+
remoteStorageIds?: StorageId[];
|
|
74
|
+
}
|
|
75
|
+
export interface BootResult {
|
|
76
|
+
repo: Repo;
|
|
77
|
+
moduleWatcher: ModuleWatcher;
|
|
78
|
+
accountDocHandle: DocHandle<AccountDoc>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Boot a Patchwork browser site.
|
|
82
|
+
*
|
|
83
|
+
* Performs the full application-shell setup: service worker + port, Repo,
|
|
84
|
+
* plugin/module loading, account resolution, URL-hash routing, and dev-console
|
|
85
|
+
* globals (`window.repo`, `window.patchwork`, `window.uncache`). Returns the
|
|
86
|
+
* constructed Repo, ModuleWatcher and account handle for sites that want to
|
|
87
|
+
* do additional wiring after boot.
|
|
88
|
+
*/
|
|
89
|
+
export declare function bootPatchworkSite(config: SiteConfig): Promise<BootResult>;
|