@inkandswitch/patchwork-bootloader 0.6.2 → 0.7.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/CHANGELOG.md +44 -0
- package/dist/automerge-protocol-handler-worker.js +315 -0
- package/dist/externals-list.js +1 -5
- package/dist/setup.d.ts +4 -7
- package/dist/setup.js +22 -395
- package/dist/shared-worker-lifecycle.d.ts +30 -0
- package/dist/shared-worker-lifecycle.js +242 -0
- package/dist/siblings.d.ts +20 -0
- package/dist/siblings.js +28 -0
- package/dist/types.d.ts +1 -75
- package/dist/types.js +0 -6
- package/dist/worker-control.d.ts +8 -0
- package/dist/worker-control.js +111 -0
- package/package.json +25 -20
- package/src/automerge-protocol-handler-worker.ts +421 -0
- package/src/externals-list.ts +1 -5
- package/src/setup.ts +34 -439
- package/src/shared-worker-lifecycle.ts +289 -0
- package/src/siblings.ts +32 -0
- package/src/types.ts +2 -108
- package/src/worker-control.ts +130 -0
- package/dist/automerge-worker.js +0 -811
- package/src/automerge-worker.ts +0 -1035
- /package/dist/{automerge-worker.d.ts → automerge-protocol-handler-worker.d.ts} +0 -0
package/dist/setup.js
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import { readClassicSyncServer, DEFAULT_CLASSIC_SYNC_SERVER, } from "./sync-config.js";
|
|
2
2
|
import debug from "debug";
|
|
3
|
-
import {
|
|
3
|
+
import { forwardWorkerConsole, lifecycleLog, sharedWorkerHandle, } from "./shared-worker-lifecycle.js";
|
|
4
|
+
export { lifecycleLog };
|
|
4
5
|
const serviceWorkerDebugging = debug.enabled("patchwork:serviceworker");
|
|
5
6
|
const workerDebugging = debug.enabled("patchwork:automergeworker");
|
|
6
|
-
export const lifecycleLog = debug("patchwork:lifecycle");
|
|
7
|
-
function describeErrorEvent(event) {
|
|
8
|
-
const error = event;
|
|
9
|
-
const where = error.filename
|
|
10
|
-
? ` (${error.filename}:${error.lineno}:${error.colno})`
|
|
11
|
-
: "";
|
|
12
|
-
return `${error.message || String(event)}${where}`;
|
|
13
|
-
}
|
|
14
7
|
// The version is cleared on every boot, so the steady state is
|
|
15
8
|
// DEFAULT_CACHE_NAME. bumpServiceWorkerCache is a dev escape hatch: it moves
|
|
16
9
|
// the worker to a throwaway cache now, and the next boot both reverts the name
|
|
@@ -47,315 +40,26 @@ function installServiceWorkerLogForwarding() {
|
|
|
47
40
|
lifecycleLog("[service-worker] %s", event.data.msg);
|
|
48
41
|
});
|
|
49
42
|
}
|
|
50
|
-
// The automerge
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const RECOVERY_MIN_INTERVAL_MS = 15_000;
|
|
65
|
-
let nextRepoChannelId = 0;
|
|
66
|
-
// Chrome can't spawn workers inside a SharedWorker, so each tab offers this
|
|
67
|
-
// proxy's port to the automerge worker, which requests one via its port
|
|
68
|
-
// provider. Being a SharedWorker itself, the proxy — and the donated
|
|
69
|
-
// worker↔worker port — outlives the donor tab.
|
|
70
|
-
const SUBDUCTION_IO_WORKER_URL = "/packages/@automerge/automerge-repo/subduction-websocket-worker-shared.js";
|
|
71
|
-
export function getAutomergeWorker() {
|
|
72
|
-
if (automergeWorker)
|
|
73
|
-
return automergeWorker;
|
|
74
|
-
workerGeneration++;
|
|
75
|
-
const worker = new SharedWorker(automergeWorkerPath, {
|
|
76
|
-
name: "patchwork-automerge",
|
|
77
|
-
type: "module",
|
|
78
|
-
});
|
|
79
|
-
automergeWorker = worker;
|
|
80
|
-
// Fires when a message can't be structured-deserialized. Silent otherwise:
|
|
81
|
-
// the message is dropped, which looks identical to a worker that never
|
|
82
|
-
// replied.
|
|
83
|
-
worker.port.addEventListener("messageerror", (event) => {
|
|
84
|
-
console.error("[automerge-worker] undeserializable message from worker:", event);
|
|
85
|
-
});
|
|
86
|
-
// Control replies come back on this port, and we listen with
|
|
87
|
-
// addEventListener rather than onmessage, so it needs start().
|
|
88
|
-
worker.port.start();
|
|
89
|
-
worker.port.addEventListener("message", handleWorkerMessage);
|
|
90
|
-
worker.port.postMessage({ type: "debug", debug: workerDebugging });
|
|
91
|
-
donatePort(worker.port, createSubductionIoPort);
|
|
92
|
-
disposeWorkerDeathDetection = installWorkerDeathDetection(worker);
|
|
93
|
-
return worker;
|
|
94
|
-
}
|
|
95
|
-
function handleWorkerMessage(event) {
|
|
96
|
-
const data = event.data;
|
|
97
|
-
if (data?.type === "sync-state") {
|
|
98
|
-
dispatchSyncState(data);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
// Crash/skew reports relayed from the subduction io proxy (e.g. a protocol
|
|
102
|
-
// mismatch from a stale SW-cached worker chunk). These otherwise only exist
|
|
103
|
-
// in chrome://inspect.
|
|
104
|
-
if (isWorkerErrorMessage(data)) {
|
|
105
|
-
console.error("[subduction-io]", data);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
if (data?.type !== "console")
|
|
109
|
-
return;
|
|
110
|
-
const { level, args } = data;
|
|
111
|
-
if (!lifecycleLog.enabled &&
|
|
112
|
-
typeof args?.[0] === "string" &&
|
|
113
|
-
args[0].includes("[lifecycle]")) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const write = console[level] ?? console.log;
|
|
117
|
-
// The worker's logs carry %c directives in args[0] with CSS in the following
|
|
118
|
-
// args, so the tag has to go inside the format string or the CSS prints raw.
|
|
119
|
-
if (typeof args[0] === "string") {
|
|
120
|
-
write(`[automerge-worker] ${args[0]}`, ...args.slice(1));
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
write("[automerge-worker]", ...args);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function createSubductionIoPort() {
|
|
127
|
-
const io = new SharedWorker(SUBDUCTION_IO_WORKER_URL, {
|
|
128
|
-
type: "module",
|
|
129
|
-
name: "subduction-websocket",
|
|
130
|
-
});
|
|
131
|
-
// This worker carries the websocket to the sync server, so a load failure
|
|
132
|
-
// stops sync with no other symptom.
|
|
133
|
-
io.addEventListener("error", (event) => {
|
|
134
|
-
console.error(`[subduction-io] failed to load/run ${SUBDUCTION_IO_WORKER_URL}:`, describeErrorEvent(event));
|
|
135
|
-
});
|
|
136
|
-
io.port.addEventListener("messageerror", (event) => {
|
|
137
|
-
console.error("[subduction-io] undeserializable message:", event);
|
|
138
|
-
});
|
|
139
|
-
return io.port;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Build a replacement worker and re-wire everything a live tab holds against
|
|
143
|
-
* it: console forwarding and port donation (both re-done by
|
|
144
|
-
* getAutomergeWorker), the per-doc sync-state subscriptions, and every
|
|
145
|
-
* subscriber's repo port. The new instance boots with cold state.
|
|
146
|
-
*/
|
|
147
|
-
async function recoverAutomergeWorker(reason, deadWorker) {
|
|
148
|
-
if (deadWorker !== automergeWorker)
|
|
149
|
-
return;
|
|
150
|
-
if (recoveringWorker)
|
|
151
|
-
return;
|
|
152
|
-
const now = Date.now();
|
|
153
|
-
if (now - lastWorkerRecoveryAt < RECOVERY_MIN_INTERVAL_MS)
|
|
154
|
-
return;
|
|
155
|
-
recoveringWorker = true;
|
|
156
|
-
lastWorkerRecoveryAt = now;
|
|
157
|
-
lifecycleLog("recreating the automerge SharedWorker (%s)", reason);
|
|
158
|
-
try {
|
|
159
|
-
disposeWorkerDeathDetection?.();
|
|
160
|
-
disposeWorkerDeathDetection = undefined;
|
|
161
|
-
automergeWorker = undefined;
|
|
162
|
-
try {
|
|
163
|
-
deadWorker.port.close();
|
|
164
|
-
}
|
|
165
|
-
catch { }
|
|
166
|
-
const fresh = getAutomergeWorker();
|
|
167
|
-
for (const documentId of syncStateListeners.keys()) {
|
|
168
|
-
fresh.port.postMessage({ type: "sync-sub", documentId });
|
|
169
|
-
}
|
|
170
|
-
for (const listener of repoChannelListeners) {
|
|
171
|
-
try {
|
|
172
|
-
const generation = workerGeneration;
|
|
173
|
-
const port = await openRepoChannel();
|
|
174
|
-
// Replaced again while we waited — the newer recovery re-delivers.
|
|
175
|
-
if (generation !== workerGeneration)
|
|
176
|
-
break;
|
|
177
|
-
await listener(port);
|
|
178
|
-
}
|
|
179
|
-
catch (err) {
|
|
180
|
-
console.error("failed to re-wire a repo channel after worker recovery", err);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
finally {
|
|
185
|
-
recoveringWorker = false;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
// A silent port is not proof of death: the worker may still be evaluating its
|
|
189
|
-
// module graph, or be busy with wasm/sync work. In both cases every queued
|
|
190
|
-
// message — including the repo ports the network adapters ride on — is
|
|
191
|
-
// delivered once it catches up, and tearing the port down would lose them. So
|
|
192
|
-
// silence only starts a non-destructive probe: a second connection to the same
|
|
193
|
-
// instance. Only if the probe gets a `hello` while this port stays silent do we
|
|
194
|
-
// know the instance is alive but our port is stranded, and recover.
|
|
195
|
-
const HEARTBEAT_MS = 5_000;
|
|
196
|
-
const HEARTBEAT_TIMEOUT_MS = 25_000;
|
|
197
|
-
// An idle worker hellos within milliseconds of connecting, so before first
|
|
198
|
-
// contact the budget is tighter — probing early rescues stranded boots fast.
|
|
199
|
-
const FIRST_CONTACT_TIMEOUT_MS = 4_000;
|
|
200
|
-
// After a slow boot both connections hello at roughly the same moment and
|
|
201
|
-
// cross-port delivery order isn't guaranteed, so give the suspect this long to
|
|
202
|
-
// also speak before concluding it's stranded.
|
|
203
|
-
const PROBE_GRACE_MS = 500;
|
|
204
|
-
function installWorkerDeathDetection(worker) {
|
|
205
|
-
let instanceId;
|
|
206
|
-
let lastHeardAt = Date.now();
|
|
207
|
-
let warnedUnresponsive = false;
|
|
208
|
-
let warnedSendFailed = false;
|
|
209
|
-
let disposed = false;
|
|
210
|
-
let probe;
|
|
211
|
-
let seq = 0;
|
|
212
|
-
const closeProbe = () => {
|
|
213
|
-
if (!probe)
|
|
214
|
-
return;
|
|
215
|
-
try {
|
|
216
|
-
probe.port.close();
|
|
217
|
-
}
|
|
218
|
-
catch { }
|
|
219
|
-
probe = undefined;
|
|
220
|
-
};
|
|
221
|
-
worker.port.addEventListener("message", (event) => {
|
|
222
|
-
const data = event.data;
|
|
223
|
-
if (data?.type !== "hello" && data?.type !== "pong")
|
|
224
|
-
return;
|
|
225
|
-
lastHeardAt = Date.now();
|
|
226
|
-
warnedUnresponsive = false;
|
|
227
|
-
closeProbe();
|
|
228
|
-
if (instanceId === undefined) {
|
|
229
|
-
instanceId = data.instanceId;
|
|
230
|
-
lifecycleLog("automerge SharedWorker instance %s (via %s)", data.instanceId, data.type);
|
|
231
|
-
}
|
|
232
|
-
else if (data.instanceId && data.instanceId !== instanceId) {
|
|
233
|
-
lifecycleLog("automerge SharedWorker instance changed (instance %s, was %s)", data.instanceId, instanceId);
|
|
234
|
-
instanceId = data.instanceId;
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
worker.port.addEventListener("close", () => {
|
|
238
|
-
if (disposed)
|
|
239
|
-
return;
|
|
240
|
-
lifecycleLog("automerge SharedWorker control port closed");
|
|
241
|
-
void recoverAutomergeWorker("control port closed", worker);
|
|
242
|
-
});
|
|
243
|
-
// Not gated on the debug namespace: a worker that fails to load never replies
|
|
244
|
-
// to anything, and this is the only signal that says so.
|
|
245
|
-
worker.addEventListener("error", (event) => {
|
|
246
|
-
console.error("automerge SharedWorker error:", describeErrorEvent(event));
|
|
247
|
-
});
|
|
248
|
-
const startProbe = (reason) => {
|
|
249
|
-
if (probe || disposed)
|
|
250
|
-
return;
|
|
251
|
-
lifecycleLog("automerge SharedWorker %s; probing with a second connection", reason);
|
|
252
|
-
const startedAt = Date.now();
|
|
253
|
-
const p = new SharedWorker(automergeWorkerPath, {
|
|
254
|
-
name: "patchwork-automerge",
|
|
255
|
-
type: "module",
|
|
256
|
-
});
|
|
257
|
-
probe = p;
|
|
258
|
-
p.port.start();
|
|
259
|
-
p.port.addEventListener("message", (event) => {
|
|
260
|
-
if (event.data?.type !== "hello")
|
|
261
|
-
return;
|
|
262
|
-
setTimeout(() => {
|
|
263
|
-
if (disposed || probe !== p)
|
|
264
|
-
return;
|
|
265
|
-
closeProbe();
|
|
266
|
-
// The suspect spoke while the probe ran: it was merely busy, and
|
|
267
|
-
// everything queued on it has been delivered.
|
|
268
|
-
if (lastHeardAt >= startedAt)
|
|
269
|
-
return;
|
|
270
|
-
void recoverAutomergeWorker(`port unresponsive on a live worker (${reason}; probe confirmed)`, worker);
|
|
271
|
-
}, PROBE_GRACE_MS);
|
|
272
|
-
});
|
|
273
|
-
// No hello on the probe means the instance is loading or busy. The probe
|
|
274
|
-
// waits indefinitely rather than tearing anything down on a timer.
|
|
275
|
-
};
|
|
276
|
-
const heartbeat = setInterval(() => {
|
|
277
|
-
try {
|
|
278
|
-
worker.port.postMessage({ type: "ping", id: ++seq });
|
|
279
|
-
}
|
|
280
|
-
catch (error) {
|
|
281
|
-
// Without this a failed send is indistinguishable from a dead worker.
|
|
282
|
-
if (!warnedSendFailed) {
|
|
283
|
-
warnedSendFailed = true;
|
|
284
|
-
console.error("automerge SharedWorker ping send threw", error);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
const neverHeard = instanceId === undefined;
|
|
288
|
-
const silentMs = Date.now() - lastHeardAt;
|
|
289
|
-
const timeoutMs = neverHeard
|
|
290
|
-
? FIRST_CONTACT_TIMEOUT_MS
|
|
291
|
-
: HEARTBEAT_TIMEOUT_MS;
|
|
292
|
-
if (silentMs <= timeoutMs)
|
|
293
|
-
return;
|
|
294
|
-
// First contact probes regardless of visibility: SharedWorkers don't
|
|
295
|
-
// suspend with the tab, and the probe destroys nothing. Post-contact
|
|
296
|
-
// silence defers to visibility, since a hidden page's throttling can fake
|
|
297
|
-
// it.
|
|
298
|
-
const visible = typeof document === "undefined" || document.visibilityState === "visible";
|
|
299
|
-
if (!neverHeard && !visible)
|
|
300
|
-
return;
|
|
301
|
-
const seconds = Math.round(silentMs / 1000);
|
|
302
|
-
const reason = neverHeard
|
|
303
|
-
? `no hello ~${seconds}s after connecting`
|
|
304
|
-
: `no pong for ~${seconds}s`;
|
|
305
|
-
if (!warnedUnresponsive) {
|
|
306
|
-
warnedUnresponsive = true;
|
|
307
|
-
lifecycleLog("automerge SharedWorker %s (tab visible)", reason);
|
|
308
|
-
}
|
|
309
|
-
startProbe(reason);
|
|
310
|
-
}, HEARTBEAT_MS);
|
|
311
|
-
return () => {
|
|
312
|
-
disposed = true;
|
|
313
|
-
clearInterval(heartbeat);
|
|
314
|
-
closeProbe();
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
const syncStateListeners = new Map();
|
|
318
|
-
function dispatchSyncState(update) {
|
|
319
|
-
for (const listener of syncStateListeners.get(update.documentId) ?? []) {
|
|
320
|
-
try {
|
|
321
|
-
listener(update);
|
|
322
|
-
}
|
|
323
|
-
catch (err) {
|
|
324
|
-
console.error("sync-state listener threw", err);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
export function subscribeSyncState(documentId, listener) {
|
|
329
|
-
const worker = getAutomergeWorker();
|
|
330
|
-
let listeners = syncStateListeners.get(documentId);
|
|
331
|
-
if (!listeners) {
|
|
332
|
-
syncStateListeners.set(documentId, (listeners = new Set()));
|
|
333
|
-
worker.port.postMessage({ type: "sync-sub", documentId });
|
|
334
|
-
}
|
|
335
|
-
listeners.add(listener);
|
|
336
|
-
let active = true;
|
|
337
|
-
return () => {
|
|
338
|
-
if (!active)
|
|
339
|
-
return;
|
|
340
|
-
active = false;
|
|
341
|
-
const set = syncStateListeners.get(documentId);
|
|
342
|
-
if (!set)
|
|
343
|
-
return;
|
|
344
|
-
set.delete(listener);
|
|
345
|
-
if (set.size > 0)
|
|
346
|
-
return;
|
|
347
|
-
syncStateListeners.delete(documentId);
|
|
348
|
-
// Unsubscribe from whichever instance is current: recovery replays
|
|
349
|
-
// subscriptions onto a new worker, so it may not be the one captured above.
|
|
350
|
-
automergeWorker?.port.postMessage({ type: "sync-unsub", documentId });
|
|
351
|
-
};
|
|
43
|
+
// ── The automerge worker ───────────────────────────────────────────────
|
|
44
|
+
// A SharedWorker holding the Repo that resolves `automerge:` URLs for the
|
|
45
|
+
// service worker. Tabs don't sync through it — each tab is its own node — but
|
|
46
|
+
// each tab keeps it alive and heartbeats it, so it's here rather than in the
|
|
47
|
+
// service worker, which can't own one.
|
|
48
|
+
let automergeProtocolHandlerWorkerPath = "/automerge-protocol-handler-worker.js";
|
|
49
|
+
const automergeProtocolHandlerWorker = sharedWorkerHandle("patchwork-automerge-protocol-handler", () => automergeProtocolHandlerWorkerPath, {
|
|
50
|
+
debugging: workerDebugging,
|
|
51
|
+
onMessage(event) {
|
|
52
|
+
forwardWorkerConsole("automerge-protocol-handler-worker", event.data);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
export function getAutomergeProtocolHandlerWorker() {
|
|
56
|
+
return automergeProtocolHandlerWorker.get();
|
|
352
57
|
}
|
|
353
58
|
export function connectClassicSync(server = readClassicSyncServer()) {
|
|
354
59
|
const url = server.trim() || DEFAULT_CLASSIC_SYNC_SERVER;
|
|
355
60
|
if (!/^wss?:\/\//.test(url)) {
|
|
356
61
|
return Promise.reject(new Error(`invalid classic sync server URL: ${server}`));
|
|
357
62
|
}
|
|
358
|
-
const worker = getAutomergeWorker();
|
|
359
63
|
const { port1, port2 } = new MessageChannel();
|
|
360
64
|
return new Promise((resolve, reject) => {
|
|
361
65
|
const timeout = setTimeout(() => {
|
|
@@ -370,65 +74,10 @@ export function connectClassicSync(server = readClassicSyncServer()) {
|
|
|
370
74
|
else
|
|
371
75
|
reject(new Error(event.data?.error ?? "connect-classic-sync failed"));
|
|
372
76
|
};
|
|
373
|
-
|
|
374
|
-
port2,
|
|
375
|
-
]);
|
|
77
|
+
automergeProtocolHandlerWorker.post({ type: "connect-classic-sync", server: url }, [port2]);
|
|
376
78
|
});
|
|
377
79
|
}
|
|
378
|
-
|
|
379
|
-
const { port1, port2 } = new MessageChannel();
|
|
380
|
-
getAutomergeWorker().port.postMessage({ type: "port", id }, [port2]);
|
|
381
|
-
return port1;
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Wait for the worker to confirm its repo is constructed. The MessageChannel
|
|
385
|
-
* adapter's whenReady() force-resolves after 100ms regardless of the other
|
|
386
|
-
* end's state, so it can't serve as a readiness signal on first boot, when the
|
|
387
|
-
* worker still has to fetch wasm and build its repo.
|
|
388
|
-
*/
|
|
389
|
-
function awaitPortReady(control, id) {
|
|
390
|
-
return new Promise((resolve, reject) => {
|
|
391
|
-
const cleanup = () => {
|
|
392
|
-
clearTimeout(timeout);
|
|
393
|
-
control.removeEventListener("message", listener);
|
|
394
|
-
};
|
|
395
|
-
const listener = (event) => {
|
|
396
|
-
if (event.data?.id !== id)
|
|
397
|
-
return;
|
|
398
|
-
if (event.data.type === "port-ready") {
|
|
399
|
-
cleanup();
|
|
400
|
-
resolve();
|
|
401
|
-
}
|
|
402
|
-
else if (event.data.type === "port-failed") {
|
|
403
|
-
cleanup();
|
|
404
|
-
reject(new Error(`automerge worker init failed: ${event.data.error}`));
|
|
405
|
-
}
|
|
406
|
-
};
|
|
407
|
-
control.addEventListener("message", listener);
|
|
408
|
-
const timeout = setTimeout(() => {
|
|
409
|
-
cleanup();
|
|
410
|
-
reject(new Error("automerge worker port-ready timeout"));
|
|
411
|
-
}, 30_000);
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
async function openRepoChannel() {
|
|
415
|
-
const id = ++nextRepoChannelId;
|
|
416
|
-
const ready = awaitPortReady(getAutomergeWorker().port, id);
|
|
417
|
-
const port = sendRepoPort(id);
|
|
418
|
-
try {
|
|
419
|
-
await ready;
|
|
420
|
-
}
|
|
421
|
-
catch (err) {
|
|
422
|
-
// Surface the problem and let the rest of the site come up rather than
|
|
423
|
-
// hanging on a blank page.
|
|
424
|
-
console.warn("proceeding without worker ready ack:", err instanceof Error ? err.message : err);
|
|
425
|
-
}
|
|
426
|
-
return port;
|
|
427
|
-
}
|
|
428
|
-
/** Open a fresh repo sync port to the automerge worker (dev console). */
|
|
429
|
-
function getRepoChannel() {
|
|
430
|
-
return sendRepoPort(++nextRepoChannelId);
|
|
431
|
-
}
|
|
80
|
+
// ── Boot ───────────────────────────────────────────────────────────────
|
|
432
81
|
function waitForActive(reg) {
|
|
433
82
|
if (reg.active)
|
|
434
83
|
return Promise.resolve(reg.active);
|
|
@@ -458,10 +107,9 @@ export default async function setupServiceWorker(options) {
|
|
|
458
107
|
// default eviction.
|
|
459
108
|
void navigator.storage?.persist?.().catch(() => { });
|
|
460
109
|
if (options?.workerPath)
|
|
461
|
-
|
|
462
|
-
// Start
|
|
463
|
-
|
|
464
|
-
const shared = getAutomergeWorker();
|
|
110
|
+
automergeProtocolHandlerWorkerPath = options.workerPath;
|
|
111
|
+
// Start it now so it boots wasm while the service worker installs.
|
|
112
|
+
const shared = automergeProtocolHandlerWorker.get();
|
|
465
113
|
const reg = await navigator.serviceWorker.register(options?.path ?? "/service-worker.js", { type: "module" });
|
|
466
114
|
const active = reg.installing || reg.waiting ? await waitForActive(reg) : reg.active;
|
|
467
115
|
configureServiceWorker(active);
|
|
@@ -479,27 +127,6 @@ export default async function setupServiceWorker(options) {
|
|
|
479
127
|
configureServiceWorker(navigator.serviceWorker.controller);
|
|
480
128
|
});
|
|
481
129
|
console.log("service worker alive, loading %c patchwork system ", "background: #fcf2f0; color: #333; border: 2px solid; border-radius: 4px");
|
|
482
|
-
return {
|
|
483
|
-
shared,
|
|
484
|
-
connectClassicSync,
|
|
485
|
-
getRepoChannel,
|
|
486
|
-
subscribeSyncState,
|
|
487
|
-
// Called once with the boot port. If the automerge worker later dies and is
|
|
488
|
-
// recreated, the listener is called again with a fresh port — treat every
|
|
489
|
-
// call as "(re)wire your repo's sync onto this port".
|
|
490
|
-
async subscribeToRepoChannel(listener) {
|
|
491
|
-
repoChannelListeners.add(listener);
|
|
492
|
-
const generation = workerGeneration;
|
|
493
|
-
const port = await openRepoChannel();
|
|
494
|
-
// If the worker was replaced while this channel was opening, recovery has
|
|
495
|
-
// already delivered a good port to this listener — drop the stale one
|
|
496
|
-
// rather than wiring the repo to a dead channel.
|
|
497
|
-
if (generation === workerGeneration)
|
|
498
|
-
await listener(port);
|
|
499
|
-
return () => {
|
|
500
|
-
repoChannelListeners.delete(listener);
|
|
501
|
-
};
|
|
502
|
-
},
|
|
503
|
-
};
|
|
130
|
+
return { shared, connectClassicSync };
|
|
504
131
|
}
|
|
505
132
|
window.bumpServiceWorkerCache = bumpServiceWorkerCache;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import debug from "debug";
|
|
2
|
+
export declare const lifecycleLog: debug.Debugger;
|
|
3
|
+
export type SharedWorkerHandle = {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
/** The current instance, spawning one if there isn't a live one. */
|
|
6
|
+
get(): SharedWorker;
|
|
7
|
+
/** Send on the current instance's control port. */
|
|
8
|
+
post(message: unknown, transfer?: Transferable[]): void;
|
|
9
|
+
/**
|
|
10
|
+
* The worker died and was replaced. Anything held against the old instance —
|
|
11
|
+
* a port, a subscription — is stranded; the new one boots with cold state.
|
|
12
|
+
*/
|
|
13
|
+
onRecreated(listener: () => void): () => void;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A SharedWorker a tab keeps alive: spawned on demand, heartbeated, and rebuilt
|
|
17
|
+
* if the browser kills it (which it may, under memory pressure).
|
|
18
|
+
*/
|
|
19
|
+
export declare function sharedWorkerHandle(name: string,
|
|
20
|
+
/** Read on every spawn, so a site can set the path after this is built. */
|
|
21
|
+
path: () => string, { debugging, onMessage, onSpawn, }: {
|
|
22
|
+
debugging: boolean;
|
|
23
|
+
onMessage: (event: MessageEvent) => void;
|
|
24
|
+
onSpawn?: (worker: SharedWorker) => void;
|
|
25
|
+
}): SharedWorkerHandle;
|
|
26
|
+
/**
|
|
27
|
+
* Mirror a worker's forwarded console output into this tab's console, since a
|
|
28
|
+
* SharedWorker's own console is only visible in chrome://inspect.
|
|
29
|
+
*/
|
|
30
|
+
export declare function forwardWorkerConsole(name: string, data: any): boolean;
|