@inkandswitch/patchwork-bootloader 0.2.8 → 0.3.1
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 +14 -0
- package/dist/automerge-worker.js +137 -40
- package/dist/externals.js +2 -2
- package/dist/module-loader-worker.d.ts +1 -0
- package/dist/module-loader-worker.js +55 -0
- package/dist/module-loader.d.ts +13 -0
- package/dist/module-loader.js +72 -0
- package/dist/service-worker.js +61 -7
- package/dist/setup.d.ts +2 -0
- package/dist/setup.js +134 -2
- package/dist/site.d.ts +24 -13
- package/dist/site.js +154 -53
- package/dist/types.d.ts +8 -0
- package/dist/types.js +6 -0
- package/dist/vite/service-worker-plugin.js +4 -0
- package/package.json +19 -15
- package/src/automerge-worker.ts +148 -49
- package/src/externals.ts +2 -2
- package/src/module-loader-worker.ts +68 -0
- package/src/module-loader.ts +85 -0
- package/src/service-worker.ts +81 -7
- package/src/setup.ts +141 -7
- package/src/site.ts +212 -72
- package/src/types.ts +9 -0
- package/src/vite/service-worker-plugin.ts +4 -0
package/dist/setup.js
CHANGED
|
@@ -2,6 +2,37 @@ import { readClassicSyncServer, DEFAULT_CLASSIC_SYNC_SERVER, } from "./sync-conf
|
|
|
2
2
|
import debug from "debug";
|
|
3
3
|
const serviceWorkerDebugging = debug.enabled("patchwork:serviceworker");
|
|
4
4
|
const workerDebugging = debug.enabled("patchwork:automergeworker");
|
|
5
|
+
// Diagnostic [lifecycle] logging, on by default. Disable via
|
|
6
|
+
// localStorage["patchwork:lifecycle-logs"] = "off". Read live at log time.
|
|
7
|
+
const LIFECYCLE_LOG_KEY = "patchwork:lifecycle-logs";
|
|
8
|
+
export function lifecycleLoggingEnabled() {
|
|
9
|
+
try {
|
|
10
|
+
const v = globalThis.localStorage?.getItem(LIFECYCLE_LOG_KEY);
|
|
11
|
+
return v !== "off" && v !== "false" && v !== "0" && v !== "no";
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// The SW can't read localStorage, so it always emits [lifecycle] markers and
|
|
18
|
+
// forwards them as `sw-lifecycle`; gate rendering here on the live toggle.
|
|
19
|
+
let swLifecycleListenerInstalled = false;
|
|
20
|
+
function installServiceWorkerLogForwarding() {
|
|
21
|
+
if (swLifecycleListenerInstalled)
|
|
22
|
+
return;
|
|
23
|
+
if (typeof navigator === "undefined" || !navigator.serviceWorker)
|
|
24
|
+
return;
|
|
25
|
+
swLifecycleListenerInstalled = true;
|
|
26
|
+
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
27
|
+
const data = event.data;
|
|
28
|
+
if (data?.type !== "sw-lifecycle")
|
|
29
|
+
return;
|
|
30
|
+
if (!lifecycleLoggingEnabled())
|
|
31
|
+
return;
|
|
32
|
+
const fn = console[data.level] ?? console.log;
|
|
33
|
+
fn(`[service-worker] ${data.msg}`);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
5
36
|
const key = "patchworkServiceWorkerCacheVersion";
|
|
6
37
|
let nextRepoChannelId = 0;
|
|
7
38
|
function bumpServiceWorkerCacheVersion() {
|
|
@@ -48,7 +79,7 @@ function configureServiceWorker(sw) {
|
|
|
48
79
|
// port; it talks to the service worker over a BroadcastChannel.
|
|
49
80
|
let automergeWorkerPath = "/automerge-worker.js";
|
|
50
81
|
let automergeWorker;
|
|
51
|
-
function getAutomergeWorker() {
|
|
82
|
+
export function getAutomergeWorker() {
|
|
52
83
|
if (!automergeWorker) {
|
|
53
84
|
automergeWorker = new SharedWorker(automergeWorkerPath, {
|
|
54
85
|
name: "patchwork-automerge",
|
|
@@ -57,10 +88,99 @@ function getAutomergeWorker() {
|
|
|
57
88
|
// Control replies (port-ready &c) come back on this port, so it needs
|
|
58
89
|
// start() — we listen with addEventListener, not onmessage.
|
|
59
90
|
automergeWorker.port.start();
|
|
91
|
+
// Surface the SharedWorker's console output and uncaught errors in this
|
|
92
|
+
// tab's console (it has its own console that's awkward to find otherwise).
|
|
93
|
+
automergeWorker.port.addEventListener("message", (event) => {
|
|
94
|
+
if (event.data?.type !== "console")
|
|
95
|
+
return;
|
|
96
|
+
const { level, args } = event.data;
|
|
97
|
+
// Gate forwarded [lifecycle] logs on the toggle too.
|
|
98
|
+
if (!lifecycleLoggingEnabled() &&
|
|
99
|
+
typeof args?.[0] === "string" &&
|
|
100
|
+
args[0].includes("[lifecycle]")) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const fn = console[level] ?? console.log;
|
|
104
|
+
// The worker's logs (debug library, the worker's own log()) carry %c
|
|
105
|
+
// format directives in args[0] with CSS in the following args. Prefix
|
|
106
|
+
// the tag into the format string rather than as a separate positional,
|
|
107
|
+
// or the %c would no longer be in arg 0 and the CSS would print raw.
|
|
108
|
+
if (typeof args[0] === "string") {
|
|
109
|
+
fn(`[automerge-worker] ${args[0]}`, ...args.slice(1));
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
fn("[automerge-worker]", ...args);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
60
115
|
automergeWorker.port.postMessage({ type: "debug", debug: workerDebugging });
|
|
116
|
+
installWorkerDeathDetection(automergeWorker);
|
|
61
117
|
}
|
|
62
118
|
return automergeWorker;
|
|
63
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Detect when the automerge SharedWorker dies or restarts: control-port close,
|
|
122
|
+
* worker error, changed instance id, or an unanswered heartbeat while the tab
|
|
123
|
+
* is visible (a miss while hidden is more likely suspension). [lifecycle]-tagged.
|
|
124
|
+
*/
|
|
125
|
+
function installWorkerDeathDetection(worker) {
|
|
126
|
+
const stamp = () => new Date().toISOString();
|
|
127
|
+
const warn = (msg) => {
|
|
128
|
+
if (lifecycleLoggingEnabled())
|
|
129
|
+
console.warn(`[lifecycle] ${stamp()} ${msg}`);
|
|
130
|
+
};
|
|
131
|
+
const info = (msg) => {
|
|
132
|
+
if (lifecycleLoggingEnabled())
|
|
133
|
+
console.info(`[lifecycle] ${stamp()} ${msg}`);
|
|
134
|
+
};
|
|
135
|
+
let instanceId;
|
|
136
|
+
let lastPongAt = Date.now();
|
|
137
|
+
let warnedUnresponsive = false;
|
|
138
|
+
worker.port.addEventListener("message", (event) => {
|
|
139
|
+
const data = event.data;
|
|
140
|
+
if (data?.type !== "hello" && data?.type !== "pong")
|
|
141
|
+
return;
|
|
142
|
+
if (data.type === "pong") {
|
|
143
|
+
lastPongAt = Date.now();
|
|
144
|
+
warnedUnresponsive = false;
|
|
145
|
+
}
|
|
146
|
+
if (instanceId === undefined) {
|
|
147
|
+
instanceId = data.instanceId;
|
|
148
|
+
info(`automerge SharedWorker instance ${data.instanceId} (via ${data.type})`);
|
|
149
|
+
}
|
|
150
|
+
else if (data.instanceId && data.instanceId !== instanceId) {
|
|
151
|
+
warn(`automerge SharedWorker RESTARTED (instance ${data.instanceId}, ` +
|
|
152
|
+
`was ${instanceId}) — fresh peerId + cold state; docs need re-subscribe`);
|
|
153
|
+
instanceId = data.instanceId;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
// Fires when the SharedWorker is destroyed (where supported).
|
|
157
|
+
worker.port.addEventListener("close", () => {
|
|
158
|
+
warn("automerge SharedWorker control port CLOSED — worker terminated");
|
|
159
|
+
});
|
|
160
|
+
worker.addEventListener("error", event => {
|
|
161
|
+
warn(`automerge SharedWorker error: ${event.message || event}`);
|
|
162
|
+
});
|
|
163
|
+
// A missed pong while the tab is visible means the worker likely died (an
|
|
164
|
+
// active tab keeps it alive); a miss while hidden is more likely suspension.
|
|
165
|
+
const HEARTBEAT_MS = 10_000;
|
|
166
|
+
const HEARTBEAT_TIMEOUT_MS = 25_000;
|
|
167
|
+
let seq = 0;
|
|
168
|
+
setInterval(() => {
|
|
169
|
+
try {
|
|
170
|
+
worker.port.postMessage({ type: "ping", id: ++seq });
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Port already torn down — the "close" handler covers that case.
|
|
174
|
+
}
|
|
175
|
+
const silentMs = Date.now() - lastPongAt;
|
|
176
|
+
const visible = typeof document === "undefined" || document.visibilityState === "visible";
|
|
177
|
+
if (silentMs > HEARTBEAT_TIMEOUT_MS && visible && !warnedUnresponsive) {
|
|
178
|
+
warnedUnresponsive = true;
|
|
179
|
+
warn(`automerge SharedWorker UNRESPONSIVE ~${Math.round(silentMs / 1000)}s ` +
|
|
180
|
+
`while tab visible — likely died/crashed`);
|
|
181
|
+
}
|
|
182
|
+
}, HEARTBEAT_MS);
|
|
183
|
+
}
|
|
64
184
|
export function connectClassicSync(server = readClassicSyncServer()) {
|
|
65
185
|
const url = server.trim() || DEFAULT_CLASSIC_SYNC_SERVER;
|
|
66
186
|
if (!/^wss?:\/\//.test(url)) {
|
|
@@ -155,11 +275,15 @@ function getRepoChannel() {
|
|
|
155
275
|
return port1;
|
|
156
276
|
}
|
|
157
277
|
export default async function setupServiceWorker(options) {
|
|
278
|
+
// Attach the SW→tab [lifecycle] log bridge as early as possible so boot /
|
|
279
|
+
// install / activate markers from the controlling worker are rendered here.
|
|
280
|
+
installServiceWorkerLogForwarding();
|
|
158
281
|
if (options?.workerPath)
|
|
159
282
|
automergeWorkerPath = options.workerPath;
|
|
160
283
|
// Start the automerge worker right away so it boots (wasm, repo) while the
|
|
161
284
|
// service worker installs.
|
|
162
|
-
getAutomergeWorker();
|
|
285
|
+
const shared = getAutomergeWorker();
|
|
286
|
+
// todo delete
|
|
163
287
|
const path = options?.path ?? "/service-worker.js";
|
|
164
288
|
// No controller at this point means the page loaded without a service
|
|
165
289
|
// worker — i.e. this is a first-time install (or a hard reload). Wait for
|
|
@@ -183,7 +307,15 @@ export default async function setupServiceWorker(options) {
|
|
|
183
307
|
configureServiceWorker(navigator.serviceWorker.controller);
|
|
184
308
|
});
|
|
185
309
|
console.log("service worker alive, loading %c patchwork system ", "background: #fcf2f0; color: #333; border: 2px solid; border-radius: 4px");
|
|
310
|
+
// todon't
|
|
311
|
+
window.killsw = () => {
|
|
312
|
+
if (automergeWorker) {
|
|
313
|
+
automergeWorker.port.close();
|
|
314
|
+
automergeWorker = undefined;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
186
317
|
return {
|
|
318
|
+
shared,
|
|
187
319
|
connectClassicSync,
|
|
188
320
|
getRepoChannel,
|
|
189
321
|
async subscribeToRepoChannel(listener) {
|
package/dist/site.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* site's `main.ts`. Non-UI consumers should import the package default (which
|
|
12
12
|
* only does SW registration and the automerge-worker handoff).
|
|
13
13
|
*/
|
|
14
|
-
import { type DocHandle, Repo, type AutomergeUrl
|
|
14
|
+
import { type DocHandle, Repo, type AutomergeUrl } from "@automerge/vanillajs/slim";
|
|
15
15
|
import { type AutomergeRepoKeyhive } from "@automerge/automerge-repo-keyhive";
|
|
16
16
|
import { ModuleWatcher } from "@inkandswitch/patchwork-filesystem";
|
|
17
17
|
import { type AccountDoc } from "@inkandswitch/patchwork-plugins";
|
|
@@ -40,16 +40,32 @@ declare global {
|
|
|
40
40
|
}
|
|
41
41
|
export interface SiteConfig {
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
43
|
+
* The site's default tool bundle — the tools every user of this site gets
|
|
44
|
+
* out of the box. Must collectively contribute at least a
|
|
45
|
+
* `patchwork:datatype` registration for `"account"` (typically the one
|
|
46
|
+
* supplied by `@inkandswitch/patchwork-frame`).
|
|
47
|
+
*
|
|
48
|
+
* Each entry is a *module-list source* and may be either:
|
|
49
|
+
* - an Automerge module-settings doc URL (`automerge:...`), which is
|
|
50
|
+
* live-reloaded, or
|
|
51
|
+
* - an HTTP(S) URL (absolute or site-relative, e.g. `/modules.json`) to a
|
|
52
|
+
* static JSON manifest of the shape `{ modules: string[], branches? }`,
|
|
53
|
+
* fetched once at boot.
|
|
54
|
+
*
|
|
55
|
+
* The module URLs *inside* either kind of source may themselves be Automerge
|
|
56
|
+
* folder docs or plain HTTP(S) bundles, so deployment targets can be freely
|
|
57
|
+
* mixed.
|
|
47
58
|
*
|
|
48
59
|
* Can be overridden at runtime by setting `localStorage.defaultToolsUrl` to
|
|
49
|
-
* another automerge
|
|
50
|
-
* unpublished tool set.
|
|
60
|
+
* another `automerge:` URL or manifest URL — useful for local development
|
|
61
|
+
* against an unpublished tool set.
|
|
62
|
+
*/
|
|
63
|
+
defaultModules?: string | string[];
|
|
64
|
+
/**
|
|
65
|
+
* @deprecated Use {@link SiteConfig.defaultModules}. Retained for backwards
|
|
66
|
+
* compatibility with existing sites.
|
|
51
67
|
*/
|
|
52
|
-
defaultModulesUrl
|
|
68
|
+
defaultModulesUrl?: AutomergeUrl;
|
|
53
69
|
/**
|
|
54
70
|
* `localStorage` key under which this site remembers which account document
|
|
55
71
|
* belongs to the current user. Sites sharing an origin MUST use distinct
|
|
@@ -66,11 +82,6 @@ export interface SiteConfig {
|
|
|
66
82
|
* Defaults to `"root"`.
|
|
67
83
|
*/
|
|
68
84
|
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
85
|
/**
|
|
75
86
|
* When true, initialize keyhive for access control.
|
|
76
87
|
* The Repo will use keyhive's network adapter, peerId, and idFactory
|
package/dist/site.js
CHANGED
|
@@ -11,23 +11,25 @@
|
|
|
11
11
|
* site's `main.ts`. Non-UI consumers should import the package default (which
|
|
12
12
|
* only does SW registration and the automerge-worker handoff).
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import { initializeWasm, isValidAutomergeUrl, isValidDocumentId, MessageChannelNetworkAdapter, parseAutomergeUrl, Repo, stringifyAutomergeUrl, } from "@automerge/vanillajs/slim";
|
|
15
|
+
import { IndexedDBWorkerStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb/IndexedDBWorkerStorageAdapter";
|
|
15
16
|
import * as Automerge from "@automerge/automerge/slim";
|
|
16
17
|
import * as AutomergeRepo from "@automerge/automerge-repo/slim";
|
|
17
|
-
import { initKeyhiveWasm,
|
|
18
|
+
import { initKeyhiveWasm, initializeAutomergeRepoKeyhiveWithRepo, } from "@automerge/automerge-repo-keyhive";
|
|
18
19
|
// eslint-disable-next-line
|
|
19
20
|
// @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
|
|
20
21
|
import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
|
|
21
22
|
const siteName = typeof __SITE_NAME__ !== "undefined" ? __SITE_NAME__ : "tiny-patchwork";
|
|
23
|
+
const useKeyhiveSyncServer = typeof __KEYHIVE_SYNC_SERVER__ !== "undefined" && __KEYHIVE_SYNC_SERVER__;
|
|
22
24
|
import { ModuleWatcher } from "@inkandswitch/patchwork-filesystem";
|
|
25
|
+
import { importAutomergeModuleViaWorker } from "./module-loader.js";
|
|
23
26
|
import { openDocument, registerPatchworkViewElement, } from "@inkandswitch/patchwork-elements";
|
|
24
27
|
import { registerRepoProviderElement } from "@inkandswitch/patchwork-providers";
|
|
25
28
|
import { getRegistry, registerPlugins, resolveAccountHandle, unregisterPlugins, } from "@inkandswitch/patchwork-plugins";
|
|
26
29
|
import * as plugins from "@inkandswitch/patchwork-plugins";
|
|
27
|
-
import setupServiceWorker from "./setup.js";
|
|
30
|
+
import setupServiceWorker, { lifecycleLoggingEnabled, } from "./setup.js";
|
|
28
31
|
import debug from "debug";
|
|
29
32
|
const log = debug("patchwork:bootloader:site");
|
|
30
|
-
const DEFAULT_REMOTE_STORAGE_ID = "3760df37-a4c6-4f66-9ecd-732039a9385d";
|
|
31
33
|
// Legacy big-patchwork hash shape: `slug--<documentId>[?=type]`.
|
|
32
34
|
const BIG_PATCHWORK_HASH_REGEX = /(?<title>[A-Za-z0-9-]+)--(?<docId>[1-9A-HJ-NP-Za-km-z]+)(?<type>\?=[^&?]+)?/;
|
|
33
35
|
const [automergeWasm, subductionWasm] = await Promise.all([
|
|
@@ -44,53 +46,79 @@ const [automergeWasm, subductionWasm] = await Promise.all([
|
|
|
44
46
|
* do additional wiring after boot.
|
|
45
47
|
*/
|
|
46
48
|
export async function bootPatchworkSite(config) {
|
|
47
|
-
const
|
|
49
|
+
const defaultModuleSources = resolveDefaultModules(config);
|
|
48
50
|
showLoadingAnimation();
|
|
49
51
|
log(`booting`, config);
|
|
52
|
+
installLifecycleLogging();
|
|
50
53
|
await initializeWasm(automergeWasm);
|
|
51
54
|
initSubductionSync(subductionWasm);
|
|
55
|
+
log("enabling workers");
|
|
52
56
|
const sw = await setupServiceWorker();
|
|
53
57
|
if (!sw)
|
|
54
58
|
throw new Error("Failed to set up service worker");
|
|
59
|
+
log("workers ready");
|
|
55
60
|
let hive;
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
initKeyhiveWasm();
|
|
66
|
-
hive = await initializeAutomergeRepoKeyhive({
|
|
67
|
-
storage: new IndexedDBStorageAdapter(`${siteName}-keyhive`),
|
|
68
|
-
peerIdSuffix: siteName + Math.random().toString(36).slice(2),
|
|
69
|
-
networkAdapter: new MessageChannelNetworkAdapter(workerPort),
|
|
70
|
-
automaticArchiveIngestion: true,
|
|
71
|
-
cachingMode: "periodic",
|
|
72
|
-
onlyShareWithHardcodedServerPeerId: false,
|
|
73
|
-
});
|
|
61
|
+
let repo;
|
|
62
|
+
// If a Repo is already on `window` — an embedding context provided one before
|
|
63
|
+
// this entry ran — reuse it and its keyhive instead of standing up a fresh
|
|
64
|
+
// realm-local Repo, so we share the same documents and sync/keyhive context.
|
|
65
|
+
// Otherwise create our own below.
|
|
66
|
+
if (window.repo) {
|
|
67
|
+
log("using existing Repo from window");
|
|
68
|
+
repo = window.repo;
|
|
69
|
+
hive = window.hive;
|
|
74
70
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
idFactory: hive.idFactory,
|
|
82
|
-
})
|
|
83
|
-
: new Repo({
|
|
84
|
-
network: [new MessageChannelNetworkAdapter(workerPort)],
|
|
85
|
-
storage: new IndexedDBStorageAdapter(),
|
|
86
|
-
async sharePolicy(peerId) {
|
|
87
|
-
return peerId.includes("automerge-worker");
|
|
88
|
-
},
|
|
89
|
-
enableRemoteHeadsGossiping: true,
|
|
90
|
-
peerId: `${config.titleSuffix}-tab-${crypto.randomUUID()}`,
|
|
71
|
+
else {
|
|
72
|
+
// Get the initial automerge-worker port via subscribeToRepoChannel,
|
|
73
|
+
// then pass it to keyhive init which wraps it in its own network adapter.
|
|
74
|
+
let resolvePort;
|
|
75
|
+
const portPromise = new Promise((r) => {
|
|
76
|
+
resolvePort = r;
|
|
91
77
|
});
|
|
92
|
-
|
|
78
|
+
log("subscribing to repo channel");
|
|
79
|
+
await sw.subscribeToRepoChannel(resolvePort);
|
|
80
|
+
log("repo channel subscribed");
|
|
81
|
+
const workerPort = await portPromise;
|
|
82
|
+
if (config.keyhive) {
|
|
83
|
+
log("setting up keyhive");
|
|
84
|
+
initKeyhiveWasm();
|
|
85
|
+
({ hive, repo } = await initializeAutomergeRepoKeyhiveWithRepo({
|
|
86
|
+
createRepo: (config) => new Repo(config),
|
|
87
|
+
storage: new IndexedDBWorkerStorageAdapter(`${siteName}-keyhive`),
|
|
88
|
+
peerIdSuffix: siteName + Math.random().toString(36).slice(2),
|
|
89
|
+
networkAdapter: new MessageChannelNetworkAdapter(workerPort),
|
|
90
|
+
automaticArchiveIngestion: true,
|
|
91
|
+
cachingMode: "periodic",
|
|
92
|
+
onlyShareWithHardcodedServerPeerId: false,
|
|
93
|
+
// ARK selects the relay via `syncServer` ("keyhive" | "subduction").
|
|
94
|
+
// Defaults to "subduction".
|
|
95
|
+
...(useKeyhiveSyncServer ? { syncServer: "keyhive" } : {}),
|
|
96
|
+
repo: {
|
|
97
|
+
storage: new IndexedDBWorkerStorageAdapter(),
|
|
98
|
+
enableRemoteHeadsGossiping: true,
|
|
99
|
+
},
|
|
100
|
+
}));
|
|
101
|
+
log("keyhive setup complete");
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
log("creating repo");
|
|
105
|
+
repo = new Repo({
|
|
106
|
+
network: [new MessageChannelNetworkAdapter(workerPort)],
|
|
107
|
+
storage: new IndexedDBWorkerStorageAdapter(),
|
|
108
|
+
async sharePolicy(peerId) {
|
|
109
|
+
return peerId.includes("automerge-worker");
|
|
110
|
+
},
|
|
111
|
+
enableRemoteHeadsGossiping: true,
|
|
112
|
+
peerId: `${config.titleSuffix}-tab-${crypto.randomUUID()}`,
|
|
113
|
+
});
|
|
114
|
+
log("repo created");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
log("popping repo on window");
|
|
118
|
+
window.repo = repo;
|
|
119
|
+
log("await repo.networkSubsystem.whenReady()");
|
|
93
120
|
await repo.networkSubsystem.whenReady();
|
|
121
|
+
log("networkSubsystem ready");
|
|
94
122
|
if (hive) {
|
|
95
123
|
hive.networkAdapter.syncKeyhive?.();
|
|
96
124
|
}
|
|
@@ -110,7 +138,10 @@ export async function bootPatchworkSite(config) {
|
|
|
110
138
|
// `resolveAccountHandle` below has something to await on (the `account`
|
|
111
139
|
// datatype lives in that bundle today). The user's own module-settings URL
|
|
112
140
|
// is added lazily once it appears on the account doc — see below.
|
|
113
|
-
const moduleWatcher = new ModuleWatcher(repo,
|
|
141
|
+
const moduleWatcher = new ModuleWatcher(repo, buildSystemSources(defaultModuleSources), onModuleLoaded, unregisterPlugins,
|
|
142
|
+
// Discover an Automerge package's plugin descriptors off the main thread;
|
|
143
|
+
// each plugin's load() re-imports the package (at heads) on this thread.
|
|
144
|
+
importAutomergeModuleViaWorker);
|
|
114
145
|
const accountDocHandle = (await resolveAccountHandle(repo, {
|
|
115
146
|
storageKey: config.accountStorageKey,
|
|
116
147
|
hive,
|
|
@@ -142,18 +173,54 @@ export async function bootPatchworkSite(config) {
|
|
|
142
173
|
return { repo, moduleWatcher, accountDocHandle };
|
|
143
174
|
}
|
|
144
175
|
// ─── Internals ──────────────────────────────────────────────────────────
|
|
145
|
-
|
|
176
|
+
/**
|
|
177
|
+
* A module-list source is valid if it is an Automerge URL or looks like an
|
|
178
|
+
* HTTP(S)/site-relative manifest URL.
|
|
179
|
+
*/
|
|
180
|
+
function isValidModuleSource(source) {
|
|
181
|
+
if (isValidAutomergeUrl(source))
|
|
182
|
+
return true;
|
|
183
|
+
return (source.startsWith("/") ||
|
|
184
|
+
source.startsWith("http://") ||
|
|
185
|
+
source.startsWith("https://") ||
|
|
186
|
+
source.startsWith("./"));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Resolve the site's default module-list sources, honouring the
|
|
190
|
+
* `localStorage.defaultToolsUrl` dev override (which replaces the entire
|
|
191
|
+
* built-in default bundle).
|
|
192
|
+
*/
|
|
193
|
+
function resolveDefaultModules(config) {
|
|
194
|
+
const builtin = config.defaultModules ?? config.defaultModulesUrl ?? [];
|
|
195
|
+
const builtinList = (Array.isArray(builtin) ? builtin : [builtin]).filter(Boolean);
|
|
146
196
|
const override = globalThis.localStorage?.getItem("defaultToolsUrl");
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
197
|
+
if (override) {
|
|
198
|
+
if (isValidModuleSource(override)) {
|
|
199
|
+
if (!builtinList.includes(override)) {
|
|
200
|
+
console.info(`using defaultToolsUrl override from localStorage: ${override}`);
|
|
201
|
+
}
|
|
202
|
+
return [override];
|
|
152
203
|
}
|
|
153
|
-
|
|
204
|
+
console.warn(`ignoring invalid defaultToolsUrl in localStorage: ${override}; using built-in default`);
|
|
205
|
+
}
|
|
206
|
+
if (builtinList.length === 0) {
|
|
207
|
+
throw new Error("bootPatchworkSite: no default module sources configured (set `defaultModules`)");
|
|
154
208
|
}
|
|
155
|
-
|
|
156
|
-
|
|
209
|
+
return builtinList;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Turn an ordered list of module-list sources into the name-keyed map the
|
|
213
|
+
* ModuleWatcher expects. The first source keeps the canonical `system` name;
|
|
214
|
+
* additional sources get suffixed names. None may be `user` (reserved for the
|
|
215
|
+
* per-account settings doc, which has branch-override precedence).
|
|
216
|
+
*/
|
|
217
|
+
function buildSystemSources(sources) {
|
|
218
|
+
const map = {};
|
|
219
|
+
sources.forEach((source, index) => {
|
|
220
|
+
const name = index === 0 ? "system" : `system-${index}`;
|
|
221
|
+
map[name] = source;
|
|
222
|
+
});
|
|
223
|
+
return map;
|
|
157
224
|
}
|
|
158
225
|
function installDevConsoleGlobals(repo, hive, getRepoChannel) {
|
|
159
226
|
window.repo = repo;
|
|
@@ -164,6 +231,33 @@ function installDevConsoleGlobals(repo, hive, getRepoChannel) {
|
|
|
164
231
|
}
|
|
165
232
|
window.getRepoChannel = getRepoChannel;
|
|
166
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Log this tab's Page Lifecycle + connectivity transitions (visibility,
|
|
236
|
+
* freeze, bfcache, online/offline) so they line up against the SharedWorker's
|
|
237
|
+
* sync-socket reaps. [lifecycle]-tagged, on by default.
|
|
238
|
+
*/
|
|
239
|
+
function installLifecycleLogging() {
|
|
240
|
+
if (typeof document === "undefined")
|
|
241
|
+
return;
|
|
242
|
+
const opts = { capture: true };
|
|
243
|
+
const note = (label, extra) => {
|
|
244
|
+
if (!lifecycleLoggingEnabled())
|
|
245
|
+
return;
|
|
246
|
+
const msg = `[lifecycle] ${new Date().toISOString()} ${label}`;
|
|
247
|
+
if (extra === undefined)
|
|
248
|
+
console.info(msg);
|
|
249
|
+
else
|
|
250
|
+
console.info(msg, extra);
|
|
251
|
+
};
|
|
252
|
+
document.addEventListener("visibilitychange", () => note(`visibilitychange → ${document.visibilityState}`), opts);
|
|
253
|
+
document.addEventListener("freeze", () => note("freeze (tab suspended)"), opts);
|
|
254
|
+
document.addEventListener("resume", () => note("resume (tab unsuspended)"), opts);
|
|
255
|
+
window.addEventListener("pageshow", e => note("pageshow", { persisted: e.persisted }), opts);
|
|
256
|
+
window.addEventListener("pagehide", e => note("pagehide", { persisted: e.persisted }), opts);
|
|
257
|
+
window.addEventListener("online", () => note("online"), opts);
|
|
258
|
+
window.addEventListener("offline", () => note("offline"), opts);
|
|
259
|
+
note(`lifecycle logging installed (visibilityState=${document.visibilityState}, hasFocus=${document.hasFocus()})`);
|
|
260
|
+
}
|
|
167
261
|
function onModuleLoaded(name, mod) {
|
|
168
262
|
if (Array.isArray(mod.plugins)) {
|
|
169
263
|
log(`registering ${mod.plugins.length} plugin(s) from ${name.slice(0, 30)}...`, mod.plugins.map((p) => `${p.type}:${p.id}`));
|
|
@@ -202,7 +296,7 @@ function primeRootElement(rootElement, accountDocHandle) {
|
|
|
202
296
|
const initialParams = new URLSearchParams(location.hash.slice(1));
|
|
203
297
|
if (initialParams.has("frame")) {
|
|
204
298
|
rootElement.setAttribute("tool-id", initialParams.get("frame"));
|
|
205
|
-
const docId = initialParams.get("doc");
|
|
299
|
+
const docId = initialParams.get("doc")?.replace(/^automerge:/, "");
|
|
206
300
|
const docUrl = docId
|
|
207
301
|
? stringifyAutomergeUrl({ documentId: docId })
|
|
208
302
|
: accountDocHandle.url;
|
|
@@ -368,15 +462,22 @@ function installHashRouting(params) {
|
|
|
368
462
|
}
|
|
369
463
|
return;
|
|
370
464
|
}
|
|
465
|
+
// Bare automerge URL in hash: /#automerge:<documentId>
|
|
466
|
+
if (isValidAutomergeUrl(hash)) {
|
|
467
|
+
const { documentId, heads } = parseAutomergeUrl(hash);
|
|
468
|
+
window.location.hash = "";
|
|
469
|
+
openDocument(rootElement, stringifyAutomergeUrl({ documentId, heads }));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
371
472
|
const params = new URLSearchParams(hash);
|
|
372
|
-
const documentId = params.get("doc");
|
|
473
|
+
const documentId = params.get("doc")?.replace(/^automerge:/, "");
|
|
373
474
|
const heads = params.get("heads")?.split("|");
|
|
374
475
|
const toolId = params.get("tool");
|
|
375
476
|
const title = params.get("title");
|
|
376
477
|
const type = params.get("type");
|
|
377
478
|
const frame = params.get("frame");
|
|
378
479
|
if (frame) {
|
|
379
|
-
const docUrl = params.get("doc") ?? accountDocHandle.url;
|
|
480
|
+
const docUrl = params.get("doc")?.replace(/^automerge:/, "") ?? accountDocHandle.url;
|
|
380
481
|
if (rootElement.getAttribute("tool-id") !== frame ||
|
|
381
482
|
rootElement.getAttribute("doc-url") !== docUrl) {
|
|
382
483
|
rootElement.setAttribute("tool-id", frame);
|
package/dist/types.d.ts
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* reintroduced when either of them restarts — and so tabs can listen in.
|
|
6
6
|
*/
|
|
7
7
|
export declare const HANDOFF_CHANNEL = "@patchwork/handoff";
|
|
8
|
+
/**
|
|
9
|
+
* BroadcastChannel on which the automerge shared worker announces remote
|
|
10
|
+
* heads it learns about from the sync server. Any tab can listen to stay
|
|
11
|
+
* informed of sync progress without repo-to-repo gossiping.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SYNCSTATE_CHANNEL = "@patchwork/syncstate";
|
|
8
14
|
/**
|
|
9
15
|
* The special URL to resolve, plus enough of the {@link Request} the service
|
|
10
16
|
* worker is holding that the automerge worker can construct one that
|
|
@@ -93,6 +99,8 @@ export type SetupServiceWorkerOptions = {
|
|
|
93
99
|
};
|
|
94
100
|
export type ServiceWorkerRepoChannelListener = (port: MessagePort) => void | Promise<void>;
|
|
95
101
|
export type SetupServiceWorkerResult = {
|
|
102
|
+
shared?: SharedWorker;
|
|
103
|
+
kill?: () => void;
|
|
96
104
|
/** Open a classic Automerge sync WebSocket from the automerge worker. */
|
|
97
105
|
connectClassicSync: (server?: string) => Promise<void>;
|
|
98
106
|
subscribeToRepoChannel: (listener: ServiceWorkerRepoChannelListener) => Promise<() => void>;
|
package/dist/types.js
CHANGED
|
@@ -5,3 +5,9 @@
|
|
|
5
5
|
* reintroduced when either of them restarts — and so tabs can listen in.
|
|
6
6
|
*/
|
|
7
7
|
export const HANDOFF_CHANNEL = "@patchwork/handoff";
|
|
8
|
+
/**
|
|
9
|
+
* BroadcastChannel on which the automerge shared worker announces remote
|
|
10
|
+
* heads it learns about from the sync server. Any tab can listen to stay
|
|
11
|
+
* informed of sync progress without repo-to-repo gossiping.
|
|
12
|
+
*/
|
|
13
|
+
export const SYNCSTATE_CHANNEL = "@patchwork/syncstate";
|
|
@@ -12,6 +12,10 @@ const workers = [
|
|
|
12
12
|
specifier: "@inkandswitch/patchwork-bootloader/automerge-worker",
|
|
13
13
|
fileName: "automerge-worker.js",
|
|
14
14
|
},
|
|
15
|
+
{
|
|
16
|
+
specifier: "@inkandswitch/patchwork-bootloader/module-loader-worker",
|
|
17
|
+
fileName: "module-loader-worker.js",
|
|
18
|
+
},
|
|
15
19
|
];
|
|
16
20
|
export function serviceworker() {
|
|
17
21
|
const entryIds = new Set();
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inkandswitch/patchwork-bootloader",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"author": "chee",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"devDependencies": {
|
|
8
|
-
"@automerge/automerge-repo-keyhive": "0.3.0-alpha.sub.
|
|
8
|
+
"@automerge/automerge-repo-keyhive": "0.3.0-alpha.sub.8b",
|
|
9
9
|
"esbuild": "^0.23.1",
|
|
10
10
|
"rollup": "^4.61.1"
|
|
11
11
|
},
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"import": "./dist/automerge-worker.js",
|
|
31
31
|
"types": "./dist/automerge-worker.d.ts"
|
|
32
32
|
},
|
|
33
|
+
"./module-loader-worker": {
|
|
34
|
+
"import": "./dist/module-loader-worker.js",
|
|
35
|
+
"types": "./dist/module-loader-worker.d.ts"
|
|
36
|
+
},
|
|
33
37
|
"./site": {
|
|
34
38
|
"import": "./dist/site.js",
|
|
35
39
|
"types": "./dist/site.d.ts"
|
|
@@ -41,28 +45,28 @@
|
|
|
41
45
|
},
|
|
42
46
|
"dependencies": {
|
|
43
47
|
"@automerge/automerge": "3.3.0-fragments.1",
|
|
44
|
-
"@automerge/automerge-repo": "2.6.0-subduction.
|
|
45
|
-
"@automerge/automerge-repo-network-messagechannel": "2.6.0-subduction.
|
|
46
|
-
"@automerge/automerge-repo-network-websocket": "2.6.0-subduction.
|
|
47
|
-
"@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.
|
|
48
|
-
"@automerge/automerge-subduction": "0.
|
|
49
|
-
"@automerge/vanillajs": "2.6.0-subduction.
|
|
50
|
-
"@keyhive/keyhive": "0.
|
|
48
|
+
"@automerge/automerge-repo": "2.6.0-subduction.39",
|
|
49
|
+
"@automerge/automerge-repo-network-messagechannel": "2.6.0-subduction.39",
|
|
50
|
+
"@automerge/automerge-repo-network-websocket": "2.6.0-subduction.39",
|
|
51
|
+
"@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.39",
|
|
52
|
+
"@automerge/automerge-subduction": "0.16.0",
|
|
53
|
+
"@automerge/vanillajs": "2.6.0-subduction.39",
|
|
54
|
+
"@keyhive/keyhive": "0.1.0-alpha.5",
|
|
51
55
|
"@types/debug": "^4.1.13",
|
|
52
56
|
"debug": "^4.4.3",
|
|
53
57
|
"resolve.exports": "^2.0.3",
|
|
54
58
|
"service-worker-types": "npm:@types/serviceworker@^0.0.153",
|
|
55
59
|
"tinyargs": "^0.1.4",
|
|
56
|
-
"@inkandswitch/patchwork-filesystem": "^0.
|
|
57
|
-
"@inkandswitch/patchwork-
|
|
60
|
+
"@inkandswitch/patchwork-filesystem": "^0.1.1",
|
|
61
|
+
"@inkandswitch/patchwork-elements": "^2.0.0",
|
|
58
62
|
"@inkandswitch/patchwork-providers": "^0.3.0",
|
|
59
|
-
"@inkandswitch/patchwork-
|
|
63
|
+
"@inkandswitch/patchwork-plugins": "^0.0.11"
|
|
60
64
|
},
|
|
61
65
|
"peerDependencies": {
|
|
62
66
|
"@automerge/automerge": "3.3.0-fragments.1",
|
|
63
|
-
"@automerge/automerge-repo": "2.6.0-subduction.
|
|
64
|
-
"@automerge/automerge-repo-keyhive": "0.3.0-alpha.sub.
|
|
65
|
-
"@automerge/vanillajs": "2.6.0-subduction.
|
|
67
|
+
"@automerge/automerge-repo": "2.6.0-subduction.39",
|
|
68
|
+
"@automerge/automerge-repo-keyhive": "0.3.0-alpha.sub.8b",
|
|
69
|
+
"@automerge/vanillajs": "2.6.0-subduction.39"
|
|
66
70
|
},
|
|
67
71
|
"scripts": {
|
|
68
72
|
"build": "tsc",
|