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