@inkandswitch/patchwork-bootloader 0.4.3 → 0.5.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 +29 -0
- package/dist/automerge-worker.js +533 -771
- package/dist/externals-list.d.ts +5 -0
- package/dist/externals-list.js +41 -0
- package/dist/externals.d.ts +14 -3
- package/dist/externals.js +50 -38
- package/dist/global.css +30 -0
- package/dist/module-loader.d.ts +0 -5
- package/dist/module-loader.js +0 -6
- package/dist/service-worker.js +178 -205
- package/dist/setup.d.ts +2 -1
- package/dist/setup.js +251 -375
- package/dist/sync-config.d.ts +1 -1
- package/dist/sync-config.js +3 -1
- package/package.json +15 -14
- package/src/automerge-worker.ts +634 -839
- package/src/externals-list.ts +43 -0
- package/src/externals.ts +64 -38
- package/src/global.css +30 -0
- package/src/module-loader.ts +0 -6
- package/src/service-worker.ts +224 -238
- package/src/setup.ts +287 -401
- package/src/sync-config.ts +6 -1
- package/dist/site.d.ts +0 -111
- package/dist/site.js +0 -602
- package/dist/vite/importmap-plugin.d.ts +0 -4
- package/dist/vite/importmap-plugin.js +0 -105
- package/dist/vite/patchwork-plugin.d.ts +0 -14
- package/dist/vite/patchwork-plugin.js +0 -8
- package/dist/vite/service-worker-plugin.d.ts +0 -2
- package/dist/vite/service-worker-plugin.js +0 -42
- package/src/site.ts +0 -859
- package/src/vite/importmap-plugin.ts +0 -133
- package/src/vite/patchwork-plugin.ts +0 -20
- package/src/vite/service-worker-plugin.ts +0 -46
package/src/site.ts
DELETED
|
@@ -1,859 +0,0 @@
|
|
|
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 automerge-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 the automerge-worker handoff).
|
|
13
|
-
*/
|
|
14
|
-
import {
|
|
15
|
-
type DocHandle,
|
|
16
|
-
initializeWasm,
|
|
17
|
-
isValidAutomergeUrl,
|
|
18
|
-
isValidDocumentId,
|
|
19
|
-
MessageChannelNetworkAdapter,
|
|
20
|
-
Repo,
|
|
21
|
-
stringifyAutomergeUrl,
|
|
22
|
-
type AutomergeUrl,
|
|
23
|
-
type DocumentId,
|
|
24
|
-
} from "@automerge/vanillajs/slim";
|
|
25
|
-
import { IndexedDBWorkerStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb/IndexedDBWorkerStorageAdapter";
|
|
26
|
-
import * as Automerge from "@automerge/automerge/slim";
|
|
27
|
-
import * as AutomergeRepo from "@automerge/automerge-repo/slim";
|
|
28
|
-
import {
|
|
29
|
-
initKeyhiveWasm,
|
|
30
|
-
initializeAutomergeRepoKeyhiveWithRepo,
|
|
31
|
-
type AutomergeRepoKeyhive,
|
|
32
|
-
} from "@automerge/automerge-repo-keyhive";
|
|
33
|
-
// eslint-disable-next-line
|
|
34
|
-
// @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
|
|
35
|
-
import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
|
|
36
|
-
import { MemorySigner } from "@automerge/automerge-subduction/slim";
|
|
37
|
-
|
|
38
|
-
declare const __SITE_NAME__: string;
|
|
39
|
-
const siteName =
|
|
40
|
-
typeof __SITE_NAME__ !== "undefined" ? __SITE_NAME__ : "patchwork.inkandswitch.com";
|
|
41
|
-
|
|
42
|
-
// Sync-server selection for keyhive. Defaults to "subduction". Build with
|
|
43
|
-
// KEYHIVE_SYNC_SERVER=true to target keyhive.sync.automerge.org. This must match
|
|
44
|
-
// the automerge-worker (SharedWorker) selection so the tab and the SW grant relay
|
|
45
|
-
// access to the same server.
|
|
46
|
-
declare const __KEYHIVE_SYNC_SERVER__: boolean;
|
|
47
|
-
const useKeyhiveSyncServer =
|
|
48
|
-
typeof __KEYHIVE_SYNC_SERVER__ !== "undefined" && __KEYHIVE_SYNC_SERVER__;
|
|
49
|
-
|
|
50
|
-
import { ModuleWatcher } from "@inkandswitch/patchwork-filesystem";
|
|
51
|
-
import { importAutomergePackageViaWorker } from "./module-loader.js";
|
|
52
|
-
import {
|
|
53
|
-
openDocument,
|
|
54
|
-
registerPatchworkViewElement,
|
|
55
|
-
} from "@inkandswitch/patchwork-elements";
|
|
56
|
-
import { registerRepoProviderElement } from "@inkandswitch/patchwork-providers";
|
|
57
|
-
import {
|
|
58
|
-
type AccountDoc,
|
|
59
|
-
type DatatypeDescription,
|
|
60
|
-
type DatatypeImplementation,
|
|
61
|
-
getRegistry,
|
|
62
|
-
registerPlugins,
|
|
63
|
-
resolveAccountHandle,
|
|
64
|
-
unregisterPlugins,
|
|
65
|
-
} from "@inkandswitch/patchwork-plugins";
|
|
66
|
-
import * as plugins from "@inkandswitch/patchwork-plugins";
|
|
67
|
-
|
|
68
|
-
import setupServiceWorker, { lifecycleLoggingEnabled } from "./setup.js";
|
|
69
|
-
import type {
|
|
70
|
-
ServiceWorkerRepoChannelListener,
|
|
71
|
-
SyncStateDocMessage,
|
|
72
|
-
} from "./types.js";
|
|
73
|
-
import debug from "debug";
|
|
74
|
-
const log = debug("patchwork:bootloader:site");
|
|
75
|
-
|
|
76
|
-
declare global {
|
|
77
|
-
interface Window {
|
|
78
|
-
accountDocHandle: DocHandle<AccountDoc>;
|
|
79
|
-
Automerge: typeof import("@automerge/automerge");
|
|
80
|
-
AutomergeRepo: typeof import("@automerge/automerge-repo");
|
|
81
|
-
repo: Repo;
|
|
82
|
-
hive?: AutomergeRepoKeyhive;
|
|
83
|
-
getRepoChannel: () => MessagePort;
|
|
84
|
-
patchwork: {
|
|
85
|
-
repo: Repo;
|
|
86
|
-
packages: ModuleWatcher;
|
|
87
|
-
plugins: typeof plugins;
|
|
88
|
-
accountDocHandle: DocHandle<AccountDoc>;
|
|
89
|
-
signer?: {
|
|
90
|
-
peerId: string;
|
|
91
|
-
verifyingKey: string;
|
|
92
|
-
};
|
|
93
|
-
sw: {
|
|
94
|
-
connectClassicSync: (server?: string) => Promise<void>;
|
|
95
|
-
subscribeToRepoChannel: (
|
|
96
|
-
listener: ServiceWorkerRepoChannelListener
|
|
97
|
-
) => Promise<() => void>;
|
|
98
|
-
subscribeSyncState: (
|
|
99
|
-
documentId: string,
|
|
100
|
-
listener: (update: SyncStateDocMessage) => void
|
|
101
|
-
) => () => void;
|
|
102
|
-
};
|
|
103
|
-
};
|
|
104
|
-
uncache: (match: string) => Promise<void>;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface SiteConfig {
|
|
109
|
-
/**
|
|
110
|
-
* The site's default tool bundle — the tools every user of this site gets
|
|
111
|
-
* out of the box. Must collectively contribute at least a
|
|
112
|
-
* `patchwork:datatype` registration for `"account"` (typically the one
|
|
113
|
-
* supplied by `@inkandswitch/patchwork-frame`).
|
|
114
|
-
*
|
|
115
|
-
* Each entry is a *module-list source* and may be either:
|
|
116
|
-
* - an Automerge module-settings doc URL (`automerge:...`), which is
|
|
117
|
-
* live-reloaded, or
|
|
118
|
-
* - an HTTP(S) URL (absolute or site-relative, e.g. `/modules.json`) to a
|
|
119
|
-
* static JSON manifest of the shape `{ modules: string[], branches? }`,
|
|
120
|
-
* fetched once at boot.
|
|
121
|
-
*
|
|
122
|
-
* The module URLs *inside* either kind of source may themselves be Automerge
|
|
123
|
-
* folder docs or plain HTTP(S) bundles, so deployment targets can be freely
|
|
124
|
-
* mixed.
|
|
125
|
-
*
|
|
126
|
-
* Can be overridden at runtime by setting `localStorage.systemPackageListURL` to
|
|
127
|
-
* another `automerge:` URL or manifest URL — useful for local development
|
|
128
|
-
* against an unpublished tool set.
|
|
129
|
-
*/
|
|
130
|
-
defaultModules?: string | string[];
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* @deprecated Use {@link SiteConfig.defaultModules}. Retained for backwards
|
|
134
|
-
* compatibility with existing sites.
|
|
135
|
-
*/
|
|
136
|
-
defaultModulesUrl?: AutomergeUrl;
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* `localStorage` key under which this site remembers which account document
|
|
140
|
-
* belongs to the current user. Sites sharing an origin MUST use distinct
|
|
141
|
-
* keys so they do not clobber each other's accounts.
|
|
142
|
-
*/
|
|
143
|
-
accountStorageKey: string;
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Brand word appended to the document title as `"<doc> | <titleSuffix>"`
|
|
147
|
-
* when a document is open. The separator is provided for you.
|
|
148
|
-
*/
|
|
149
|
-
titleSuffix: string;
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* DOM id of the `<patchwork-view>` element that will host the root tool.
|
|
153
|
-
* Defaults to `"root"`.
|
|
154
|
-
*/
|
|
155
|
-
rootElementId?: string;
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* When true, initialize keyhive for access control.
|
|
159
|
-
* The Repo will use keyhive's network adapter, peerId, and idFactory
|
|
160
|
-
* instead of a sharePolicy.
|
|
161
|
-
*/
|
|
162
|
-
keyhive?: boolean;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export interface BootResult {
|
|
166
|
-
repo: Repo;
|
|
167
|
-
moduleWatcher: ModuleWatcher;
|
|
168
|
-
accountDocHandle: DocHandle<AccountDoc>;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Legacy big-patchwork hash shape: `<slug>--<documentId>[?…]`. The slug can
|
|
172
|
-
// contain characters we don't otherwise permit (e.g. `drawing-(branch-1)`), so
|
|
173
|
-
// we anchor on the `--` before the base58 document id and allow any non-query
|
|
174
|
-
// characters ahead of it rather than a strict slug charset.
|
|
175
|
-
const BIG_PATCHWORK_HASH_REGEX =
|
|
176
|
-
/^(?<title>[^=&?/#]*)--(?<docId>[1-9A-HJ-NP-Za-km-z]+)/;
|
|
177
|
-
|
|
178
|
-
const [automergeWasm, subductionWasm] = await Promise.all([
|
|
179
|
-
fetch("/automerge.wasm").then((r) => r.bytes()),
|
|
180
|
-
fetch("/subduction.wasm").then((r) => r.bytes()),
|
|
181
|
-
]);
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Boot a Patchwork browser site.
|
|
185
|
-
*
|
|
186
|
-
* Performs the full application-shell setup: service worker + port, Repo,
|
|
187
|
-
* plugin/module loading, account resolution, URL-hash routing, and dev-console
|
|
188
|
-
* globals (`window.repo`, `window.patchwork`, `window.uncache`). Returns the
|
|
189
|
-
* constructed Repo, ModuleWatcher and account handle for sites that want to
|
|
190
|
-
* do additional wiring after boot.
|
|
191
|
-
*/
|
|
192
|
-
export async function bootPatchworkSite(
|
|
193
|
-
config: SiteConfig
|
|
194
|
-
): Promise<BootResult> {
|
|
195
|
-
const defaultModuleSources = resolveDefaultModules(config);
|
|
196
|
-
showLoadingAnimation();
|
|
197
|
-
log(`booting`, config);
|
|
198
|
-
installLifecycleLogging();
|
|
199
|
-
await initializeWasm(automergeWasm);
|
|
200
|
-
initSubductionSync(subductionWasm);
|
|
201
|
-
|
|
202
|
-
log("enabling workers");
|
|
203
|
-
const sw = await setupServiceWorker();
|
|
204
|
-
if (!sw) throw new Error("Failed to set up service worker");
|
|
205
|
-
log("workers ready");
|
|
206
|
-
|
|
207
|
-
let hive: AutomergeRepoKeyhive | undefined;
|
|
208
|
-
let repo: Repo;
|
|
209
|
-
let tabSignerIdentity: { peerId: string; verifyingKey: string } | undefined;
|
|
210
|
-
|
|
211
|
-
// Called with a fresh port when the automerge worker dies and is recreated
|
|
212
|
-
// (see recoverAutomergeWorker in setup.ts); assigned once the repo exists.
|
|
213
|
-
let onWorkerPortRenewed: ((port: MessagePort) => void) | undefined;
|
|
214
|
-
|
|
215
|
-
// If a Repo is already on `window` — an embedding context provided one before
|
|
216
|
-
// this entry ran — reuse it and its keyhive instead of standing up a fresh
|
|
217
|
-
// realm-local Repo, so we share the same documents and sync/keyhive context.
|
|
218
|
-
// Otherwise create our own below.
|
|
219
|
-
if (window.repo) {
|
|
220
|
-
log("using existing Repo from window");
|
|
221
|
-
repo = window.repo;
|
|
222
|
-
hive = window.hive;
|
|
223
|
-
} else {
|
|
224
|
-
// Get the initial automerge-worker port via subscribeToRepoChannel,
|
|
225
|
-
// then pass it to keyhive init which wraps it in its own network adapter.
|
|
226
|
-
// The listener is called again with a fresh port if the worker is ever
|
|
227
|
-
// recreated after dying.
|
|
228
|
-
let resolveFirstPort!: (port: MessagePort) => void;
|
|
229
|
-
const firstPortPromise = new Promise<MessagePort>((r) => {
|
|
230
|
-
resolveFirstPort = r;
|
|
231
|
-
});
|
|
232
|
-
let seenFirstPort = false;
|
|
233
|
-
log("subscribing to repo channel");
|
|
234
|
-
// Deliberately not awaited: subscribeToRepoChannel resolves only after
|
|
235
|
-
// the boot channel's port-ready handshake, which can take its full 30s
|
|
236
|
-
// timeout against a stranded worker connection. Boot should block on the
|
|
237
|
-
// first *delivered* port instead — if the boot channel stalls, worker
|
|
238
|
-
// recovery hands the listener a good port long before that timeout.
|
|
239
|
-
void sw.subscribeToRepoChannel((port) => {
|
|
240
|
-
if (!seenFirstPort) {
|
|
241
|
-
seenFirstPort = true;
|
|
242
|
-
resolveFirstPort(port);
|
|
243
|
-
} else if (onWorkerPortRenewed) {
|
|
244
|
-
onWorkerPortRenewed(port);
|
|
245
|
-
} else {
|
|
246
|
-
console.warn(
|
|
247
|
-
"automerge worker port renewed before the repo existed; dropping it"
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
const workerPort = await firstPortPromise;
|
|
252
|
-
log("repo channel subscribed");
|
|
253
|
-
let workerAdapter = new MessageChannelNetworkAdapter(workerPort);
|
|
254
|
-
|
|
255
|
-
if (config.keyhive) {
|
|
256
|
-
log("setting up keyhive");
|
|
257
|
-
initKeyhiveWasm();
|
|
258
|
-
|
|
259
|
-
({ hive, repo } = await initializeAutomergeRepoKeyhiveWithRepo({
|
|
260
|
-
createRepo: (config) => new Repo(config),
|
|
261
|
-
storage: new IndexedDBWorkerStorageAdapter(`${siteName}-keyhive`),
|
|
262
|
-
peerIdSuffix: siteName + Math.random().toString(36).slice(2),
|
|
263
|
-
networkAdapter: workerAdapter,
|
|
264
|
-
automaticArchiveIngestion: true,
|
|
265
|
-
cachingMode: "periodic",
|
|
266
|
-
onlyShareWithHardcodedServerPeerId: false,
|
|
267
|
-
// ARK selects the relay via `syncServer` ("keyhive" | "subduction").
|
|
268
|
-
// Defaults to "subduction".
|
|
269
|
-
...(useKeyhiveSyncServer ? { syncServer: "keyhive" as const } : {}),
|
|
270
|
-
repo: {
|
|
271
|
-
storage: new IndexedDBWorkerStorageAdapter(),
|
|
272
|
-
enableRemoteHeadsGossiping: true,
|
|
273
|
-
},
|
|
274
|
-
}));
|
|
275
|
-
log("keyhive setup complete");
|
|
276
|
-
} else {
|
|
277
|
-
log("creating repo");
|
|
278
|
-
// Pass an explicit signer (instead of the Repo's internal default) so we
|
|
279
|
-
// can expose tab signer identity on window.patchwork for dev inspection.
|
|
280
|
-
// The tab never connects via Subduction (no endpoints/adapters), so this
|
|
281
|
-
// id never goes on the wire.
|
|
282
|
-
const tabSigner = new MemorySigner();
|
|
283
|
-
repo = new Repo({
|
|
284
|
-
network: [workerAdapter],
|
|
285
|
-
storage: new IndexedDBWorkerStorageAdapter(),
|
|
286
|
-
signer: tabSigner,
|
|
287
|
-
async sharePolicy(peerId) {
|
|
288
|
-
return peerId.includes("automerge-worker");
|
|
289
|
-
},
|
|
290
|
-
enableRemoteHeadsGossiping: true,
|
|
291
|
-
peerId:
|
|
292
|
-
`${config.titleSuffix}-tab-${crypto.randomUUID()}` as AutomergeRepo.PeerId,
|
|
293
|
-
});
|
|
294
|
-
tabSignerIdentity = {
|
|
295
|
-
peerId: tabSigner.peerId().toString(),
|
|
296
|
-
verifyingKey: (
|
|
297
|
-
tabSigner.verifyingKey() as Uint8Array<ArrayBufferLike> & {
|
|
298
|
-
toHex(): string;
|
|
299
|
-
}
|
|
300
|
-
).toHex(),
|
|
301
|
-
};
|
|
302
|
-
console.log("[patchwork] tab subduction identity:", tabSignerIdentity);
|
|
303
|
-
log("repo created");
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// The worker was recreated with cold state: wire the repo onto the fresh
|
|
307
|
-
// port and drop the adapter stranded on the dead one. `repo`/`hive` are
|
|
308
|
-
// settled by the time this can fire (recovery needs a missed heartbeat).
|
|
309
|
-
const bootHive = hive;
|
|
310
|
-
onWorkerPortRenewed = (port) => {
|
|
311
|
-
const fresh = new MessageChannelNetworkAdapter(port);
|
|
312
|
-
// Mirror the boot wiring: a keyhive repo talks to the worker through a
|
|
313
|
-
// keyhive adapter wrapped around the message channel (same parameters
|
|
314
|
-
// the worker uses for its side of the pair).
|
|
315
|
-
const registered = bootHive
|
|
316
|
-
? bootHive.createKeyhiveNetworkAdapter(fresh, false, false, 2000)
|
|
317
|
-
: fresh;
|
|
318
|
-
repo.networkSubsystem.addNetworkAdapter(registered as any);
|
|
319
|
-
for (const adapter of [...repo.networkSubsystem.adapters]) {
|
|
320
|
-
if (adapter === (registered as any)) continue;
|
|
321
|
-
// The keyhive wrapper keeps the wrapped adapter on `.networkAdapter`.
|
|
322
|
-
const base = (adapter as any).networkAdapter ?? adapter;
|
|
323
|
-
if (base !== workerAdapter) continue;
|
|
324
|
-
try {
|
|
325
|
-
repo.networkSubsystem.removeNetworkAdapter(adapter as any);
|
|
326
|
-
} catch (err) {
|
|
327
|
-
console.error("failed to remove stale worker network adapter", err);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
workerAdapter = fresh;
|
|
331
|
-
console.warn(
|
|
332
|
-
`[lifecycle] ${new Date().toISOString()} repo re-wired to the ` +
|
|
333
|
-
`recreated automerge worker`
|
|
334
|
-
);
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
log("popping repo on window");
|
|
338
|
-
window.repo = repo;
|
|
339
|
-
log("await repo.networkSubsystem.whenReady()");
|
|
340
|
-
|
|
341
|
-
await repo.networkSubsystem.whenReady();
|
|
342
|
-
log("networkSubsystem ready");
|
|
343
|
-
|
|
344
|
-
if (hive) {
|
|
345
|
-
(hive.networkAdapter as any).syncKeyhive?.();
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
installDevConsoleGlobals(repo, hive, sw.getRepoChannel);
|
|
349
|
-
|
|
350
|
-
registerRepoProviderElement(repo as any);
|
|
351
|
-
|
|
352
|
-
const rootElement = document.getElementById(config.rootElementId ?? "root");
|
|
353
|
-
if (!rootElement) {
|
|
354
|
-
throw new Error(
|
|
355
|
-
`bootPatchworkSite: no element with id="${config.rootElementId ?? "root"}"`
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
// `<repo-provider>` sits above the root and answers `repo:handle-descriptor`
|
|
359
|
-
// for any view outside a remapper (resolving to the requested url unchanged).
|
|
360
|
-
const repoProvider = document.createElement("repo-provider");
|
|
361
|
-
rootElement.parentElement!.insertBefore(repoProvider, rootElement);
|
|
362
|
-
repoProvider.appendChild(rootElement);
|
|
363
|
-
|
|
364
|
-
registerPatchworkViewElement({ hive, repo });
|
|
365
|
-
|
|
366
|
-
// The watcher is started with the site's default-tools bundle alone so that
|
|
367
|
-
// `resolveAccountHandle` below has something to await on (the `account`
|
|
368
|
-
// datatype lives in that bundle today). The user's own module-settings URL
|
|
369
|
-
// is added lazily once it appears on the account doc — see below.
|
|
370
|
-
const moduleWatcher = new ModuleWatcher(
|
|
371
|
-
repo,
|
|
372
|
-
buildSystemSources(defaultModuleSources),
|
|
373
|
-
onModuleLoaded,
|
|
374
|
-
unregisterPlugins,
|
|
375
|
-
// Discover an Automerge package's plugin descriptors off the main thread;
|
|
376
|
-
// each plugin's load() re-imports the package (at heads) on this thread.
|
|
377
|
-
importAutomergePackageViaWorker
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
const accountDocHandle = (await resolveAccountHandle(repo, {
|
|
381
|
-
storageKey: config.accountStorageKey,
|
|
382
|
-
hive,
|
|
383
|
-
})) as DocHandle<AccountDoc>;
|
|
384
|
-
// TODO: something we (Orion & pvh) changed in the types made this necessary
|
|
385
|
-
// fix this before merging to main!
|
|
386
|
-
|
|
387
|
-
window.accountDocHandle = accountDocHandle;
|
|
388
|
-
|
|
389
|
-
wireModuleSettingsWhenReady(accountDocHandle, moduleWatcher);
|
|
390
|
-
|
|
391
|
-
primeRootElement(rootElement, accountDocHandle);
|
|
392
|
-
logToolRegistryWhenLoaded(moduleWatcher);
|
|
393
|
-
|
|
394
|
-
window.patchwork = {
|
|
395
|
-
repo,
|
|
396
|
-
packages: moduleWatcher,
|
|
397
|
-
plugins,
|
|
398
|
-
accountDocHandle,
|
|
399
|
-
...(tabSignerIdentity ? { signer: tabSignerIdentity } : {}),
|
|
400
|
-
sw: {
|
|
401
|
-
connectClassicSync: sw.connectClassicSync,
|
|
402
|
-
subscribeToRepoChannel: sw.subscribeToRepoChannel,
|
|
403
|
-
subscribeSyncState: sw.subscribeSyncState,
|
|
404
|
-
},
|
|
405
|
-
};
|
|
406
|
-
window.uncache = uncache;
|
|
407
|
-
|
|
408
|
-
installHashRouting({
|
|
409
|
-
rootElement,
|
|
410
|
-
repo,
|
|
411
|
-
accountDocHandle,
|
|
412
|
-
titleSuffix: config.titleSuffix,
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
return { repo, moduleWatcher, accountDocHandle };
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// ─── Internals ──────────────────────────────────────────────────────────
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* A module-list source is valid if it is an Automerge URL or looks like an
|
|
422
|
-
* HTTP(S)/site-relative manifest URL.
|
|
423
|
-
*/
|
|
424
|
-
function isValidModuleSource(source: string): boolean {
|
|
425
|
-
if (isValidAutomergeUrl(source)) return true;
|
|
426
|
-
return (
|
|
427
|
-
source.startsWith("/") ||
|
|
428
|
-
source.startsWith("http://") ||
|
|
429
|
-
source.startsWith("https://") ||
|
|
430
|
-
source.startsWith("./")
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Resolve the site's default module-list sources, honouring the
|
|
436
|
-
* `localStorage.systemPackageListURL` dev override (which replaces the entire
|
|
437
|
-
* built-in default bundle).
|
|
438
|
-
*/
|
|
439
|
-
function resolveDefaultModules(config: SiteConfig): string[] {
|
|
440
|
-
const builtin = config.defaultModules ?? config.defaultModulesUrl ?? [];
|
|
441
|
-
const builtinList = (Array.isArray(builtin) ? builtin : [builtin]).filter(
|
|
442
|
-
Boolean
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
const storage = globalThis.localStorage;
|
|
446
|
-
// `defaultToolsUrl` is the pre-rename key, still honoured for existing browsers.
|
|
447
|
-
const override =
|
|
448
|
-
storage?.getItem("systemPackageListURL") ??
|
|
449
|
-
storage?.getItem("defaultToolsUrl");
|
|
450
|
-
if (override) {
|
|
451
|
-
if (isValidModuleSource(override)) {
|
|
452
|
-
if (!builtinList.includes(override)) {
|
|
453
|
-
console.info(
|
|
454
|
-
`using systemPackageListURL override from localStorage: ${override}`
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
return [override];
|
|
458
|
-
}
|
|
459
|
-
console.warn(
|
|
460
|
-
`ignoring invalid systemPackageListURL in localStorage: ${override}; using built-in default`
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (builtinList.length === 0) {
|
|
465
|
-
throw new Error(
|
|
466
|
-
"bootPatchworkSite: no default module sources configured (set `defaultModules`)"
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
return builtinList;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Turn an ordered list of module-list sources into the name-keyed map the
|
|
474
|
-
* ModuleWatcher expects. The first source keeps the canonical `system` name;
|
|
475
|
-
* additional sources get suffixed names. None may be `user` (reserved for the
|
|
476
|
-
* per-account settings doc, which has branch-override precedence).
|
|
477
|
-
*/
|
|
478
|
-
function buildSystemSources(sources: string[]): Record<string, string> {
|
|
479
|
-
const map: Record<string, string> = {};
|
|
480
|
-
sources.forEach((source, index) => {
|
|
481
|
-
const name = index === 0 ? "system" : `system-${index}`;
|
|
482
|
-
map[name] = source;
|
|
483
|
-
});
|
|
484
|
-
return map;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function installDevConsoleGlobals(
|
|
488
|
-
repo: Repo,
|
|
489
|
-
hive: AutomergeRepoKeyhive | undefined,
|
|
490
|
-
getRepoChannel: () => MessagePort
|
|
491
|
-
): void {
|
|
492
|
-
window.repo = repo;
|
|
493
|
-
window.Automerge = Automerge;
|
|
494
|
-
window.AutomergeRepo = AutomergeRepo;
|
|
495
|
-
if (hive) {
|
|
496
|
-
window.hive = hive;
|
|
497
|
-
}
|
|
498
|
-
window.getRepoChannel = getRepoChannel;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Log this tab's Page Lifecycle + connectivity transitions (visibility,
|
|
503
|
-
* freeze, bfcache, online/offline) so they line up against the SharedWorker's
|
|
504
|
-
* sync-socket reaps. [lifecycle]-tagged, on by default.
|
|
505
|
-
*/
|
|
506
|
-
function installLifecycleLogging(): void {
|
|
507
|
-
if (typeof document === "undefined") return;
|
|
508
|
-
const opts = { capture: true } as const;
|
|
509
|
-
const note = (label: string, extra?: unknown) => {
|
|
510
|
-
if (!lifecycleLoggingEnabled()) return;
|
|
511
|
-
const msg = `[lifecycle] ${new Date().toISOString()} ${label}`;
|
|
512
|
-
if (extra === undefined) console.info(msg);
|
|
513
|
-
else console.info(msg, extra);
|
|
514
|
-
};
|
|
515
|
-
|
|
516
|
-
document.addEventListener(
|
|
517
|
-
"visibilitychange",
|
|
518
|
-
() => note(`visibilitychange → ${document.visibilityState}`),
|
|
519
|
-
opts
|
|
520
|
-
);
|
|
521
|
-
document.addEventListener(
|
|
522
|
-
"freeze",
|
|
523
|
-
() => note("freeze (tab suspended)"),
|
|
524
|
-
opts
|
|
525
|
-
);
|
|
526
|
-
document.addEventListener(
|
|
527
|
-
"resume",
|
|
528
|
-
() => note("resume (tab unsuspended)"),
|
|
529
|
-
opts
|
|
530
|
-
);
|
|
531
|
-
window.addEventListener(
|
|
532
|
-
"pageshow",
|
|
533
|
-
(e) =>
|
|
534
|
-
note("pageshow", { persisted: (e as PageTransitionEvent).persisted }),
|
|
535
|
-
opts
|
|
536
|
-
);
|
|
537
|
-
window.addEventListener(
|
|
538
|
-
"pagehide",
|
|
539
|
-
(e) =>
|
|
540
|
-
note("pagehide", { persisted: (e as PageTransitionEvent).persisted }),
|
|
541
|
-
opts
|
|
542
|
-
);
|
|
543
|
-
window.addEventListener("online", () => note("online"), opts);
|
|
544
|
-
window.addEventListener("offline", () => note("offline"), opts);
|
|
545
|
-
|
|
546
|
-
note(
|
|
547
|
-
`lifecycle logging installed (visibilityState=${document.visibilityState}, hasFocus=${document.hasFocus()})`
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function onModuleLoaded(name: string, mod: any): void {
|
|
552
|
-
if (Array.isArray(mod.plugins)) {
|
|
553
|
-
log(
|
|
554
|
-
`registering ${mod.plugins.length} plugin(s) from ${name.slice(0, 30)}...`,
|
|
555
|
-
mod.plugins.map((p: any) => `${p.type}:${p.id}`)
|
|
556
|
-
);
|
|
557
|
-
registerPlugins(mod.plugins, name);
|
|
558
|
-
} else {
|
|
559
|
-
console.warn(
|
|
560
|
-
`module ${name.slice(0, 30)}... has no plugins array`,
|
|
561
|
-
Object.keys(mod)
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* The frame lazy-creates `moduleSettingsUrl` on first mount. Watch for it to
|
|
568
|
-
* appear on the account doc and feed it into the ModuleWatcher so the user's
|
|
569
|
-
* own tool bundle loads alongside the site default. Idempotent.
|
|
570
|
-
*/
|
|
571
|
-
function wireModuleSettingsWhenReady(
|
|
572
|
-
accountDocHandle: DocHandle<AccountDoc>,
|
|
573
|
-
moduleWatcher: ModuleWatcher
|
|
574
|
-
): void {
|
|
575
|
-
const wire = () => {
|
|
576
|
-
const url = accountDocHandle.doc()?.moduleSettingsUrl;
|
|
577
|
-
if (!url) return;
|
|
578
|
-
void moduleWatcher.addUrl("user", url);
|
|
579
|
-
accountDocHandle.off("change", wire);
|
|
580
|
-
};
|
|
581
|
-
wire();
|
|
582
|
-
if (!accountDocHandle.doc()?.moduleSettingsUrl) {
|
|
583
|
-
accountDocHandle.on("change", wire);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* Set initial `tool-id` / `doc-url` attributes on the root
|
|
589
|
-
* `<patchwork-view>` based on the URL hash (if it specifies a frame
|
|
590
|
-
* override) or the account doc's configured frame tool + the account doc
|
|
591
|
-
* itself.
|
|
592
|
-
*/
|
|
593
|
-
function primeRootElement(
|
|
594
|
-
rootElement: HTMLElement,
|
|
595
|
-
accountDocHandle: DocHandle<AccountDoc>
|
|
596
|
-
): void {
|
|
597
|
-
rootElement.style.visibility = "hidden";
|
|
598
|
-
|
|
599
|
-
const initialParams = new URLSearchParams(location.hash.slice(1));
|
|
600
|
-
if (initialParams.has("frame")) {
|
|
601
|
-
rootElement.setAttribute("tool-id", initialParams.get("frame")!);
|
|
602
|
-
const docUrl =
|
|
603
|
-
docParamToUrl(initialParams.get("doc")) ?? accountDocHandle.url;
|
|
604
|
-
rootElement.setAttribute("doc-url", docUrl);
|
|
605
|
-
} else {
|
|
606
|
-
rootElement.setAttribute("tool-id", accountDocHandle.doc().frameToolId);
|
|
607
|
-
rootElement.setAttribute("doc-url", accountDocHandle.url);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
function logToolRegistryWhenLoaded(moduleWatcher: ModuleWatcher): void {
|
|
612
|
-
moduleWatcher.doneLoading
|
|
613
|
-
.then(() => {
|
|
614
|
-
const toolReg = getRegistry("patchwork:tool");
|
|
615
|
-
const tools = toolReg.all();
|
|
616
|
-
log(
|
|
617
|
-
`doneLoading: ${tools.length} tools registered:`,
|
|
618
|
-
tools.map((t: any) => t.id)
|
|
619
|
-
);
|
|
620
|
-
})
|
|
621
|
-
.catch((err: unknown) => {
|
|
622
|
-
console.error("doneLoading rejected:", err);
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
const LOADING_STYLE_ID = "pw-bootloader-loading-styles";
|
|
627
|
-
const LOADING_ELEMENT_ID = "pw-bootloader-loading";
|
|
628
|
-
|
|
629
|
-
function showLoadingAnimation(): void {
|
|
630
|
-
if (!document.getElementById(LOADING_STYLE_ID)) {
|
|
631
|
-
const style = document.createElement("style");
|
|
632
|
-
style.id = LOADING_STYLE_ID;
|
|
633
|
-
style.textContent = `
|
|
634
|
-
@keyframes pw-bootloader-pulse {
|
|
635
|
-
0%, 100% { opacity: 0.25; }
|
|
636
|
-
50% { opacity: 0.95; }
|
|
637
|
-
}
|
|
638
|
-
#${LOADING_ELEMENT_ID} {
|
|
639
|
-
position: fixed;
|
|
640
|
-
inset: 0;
|
|
641
|
-
z-index: 0;
|
|
642
|
-
pointer-events: none;
|
|
643
|
-
background-color: #fff;
|
|
644
|
-
background-image:
|
|
645
|
-
radial-gradient(ellipse 55% 45% at 28% 35%, #fde4ec, transparent 70%),
|
|
646
|
-
radial-gradient(ellipse 50% 55% at 72% 65%, #e0f0fb, transparent 70%),
|
|
647
|
-
radial-gradient(ellipse 65% 55% at 50% 50%, #f1e6f6, transparent 80%);
|
|
648
|
-
animation: pw-bootloader-pulse 3.5s ease-in-out infinite;
|
|
649
|
-
transition: opacity 0.6s ease-out;
|
|
650
|
-
top: 0;
|
|
651
|
-
left: 0;
|
|
652
|
-
right: 0;
|
|
653
|
-
bottom: 0;
|
|
654
|
-
}
|
|
655
|
-
@media (prefers-color-scheme: dark) {
|
|
656
|
-
#${LOADING_ELEMENT_ID} {
|
|
657
|
-
background-color: #000;
|
|
658
|
-
background-image:
|
|
659
|
-
radial-gradient(ellipse 55% 45% at 28% 35%, #2a1d33, transparent 70%),
|
|
660
|
-
radial-gradient(ellipse 50% 55% at 72% 65%, #1a2738, transparent 70%),
|
|
661
|
-
radial-gradient(ellipse 65% 55% at 50% 50%, #221a2e, transparent 80%);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
#${LOADING_ELEMENT_ID}.pw-bootloader-fading {
|
|
665
|
-
opacity: 0;
|
|
666
|
-
animation: none;
|
|
667
|
-
}
|
|
668
|
-
`;
|
|
669
|
-
document.head.appendChild(style);
|
|
670
|
-
}
|
|
671
|
-
if (document.getElementById(LOADING_ELEMENT_ID)) return;
|
|
672
|
-
const el = document.createElement("div");
|
|
673
|
-
el.id = LOADING_ELEMENT_ID;
|
|
674
|
-
document.body.appendChild(el);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function hideLoadingAnimation(): void {
|
|
678
|
-
const el = document.getElementById(LOADING_ELEMENT_ID);
|
|
679
|
-
if (!el) return;
|
|
680
|
-
el.classList.add("pw-bootloader-fading");
|
|
681
|
-
setTimeout(() => el.remove(), 700);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
async function uncache(match: string): Promise<void> {
|
|
685
|
-
for (const name of await caches.keys()) {
|
|
686
|
-
const cache = await caches.open(name);
|
|
687
|
-
for (const request of await cache.keys()) {
|
|
688
|
-
if (request.url.includes(match)) {
|
|
689
|
-
cache.delete(request);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
// The `doc=` value is an automerge URL — we keep its `:` (and any `#`/`|`
|
|
696
|
-
// heads) literal rather than percent-encoding it so links stay readable.
|
|
697
|
-
const RAW_HASH_KEYS = new Set(["doc"]);
|
|
698
|
-
// Emit hash params in a stable order so re-serializing the same logical params
|
|
699
|
-
// yields a byte-identical string (avoids spurious `hashchange` round-trips).
|
|
700
|
-
const HASH_KEY_ORDER = ["doc", "tool", "type", "title", "frame"];
|
|
701
|
-
|
|
702
|
-
function serializeHashParams(params: URLSearchParams): string {
|
|
703
|
-
const emitted = new Set<string>();
|
|
704
|
-
const parts: string[] = [];
|
|
705
|
-
const emit = (key: string) => {
|
|
706
|
-
if (emitted.has(key)) return;
|
|
707
|
-
const value = params.get(key);
|
|
708
|
-
if (!value) return;
|
|
709
|
-
emitted.add(key);
|
|
710
|
-
parts.push(
|
|
711
|
-
`${key}=${RAW_HASH_KEYS.has(key) ? value : encodeURIComponent(value)}`
|
|
712
|
-
);
|
|
713
|
-
};
|
|
714
|
-
for (const key of HASH_KEY_ORDER) emit(key);
|
|
715
|
-
for (const key of params.keys()) emit(key);
|
|
716
|
-
return parts.join("&");
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* Coerce a `doc=` hash param to a full automerge URL. Accepts a full URL
|
|
721
|
-
* (`automerge:<id>[#heads]`) or a bare document id for backwards compatibility
|
|
722
|
-
* with older links.
|
|
723
|
-
*/
|
|
724
|
-
function docParamToUrl(docParam: string | null): AutomergeUrl | undefined {
|
|
725
|
-
if (!docParam) return undefined;
|
|
726
|
-
if (isValidAutomergeUrl(docParam as AutomergeUrl)) {
|
|
727
|
-
return docParam as AutomergeUrl;
|
|
728
|
-
}
|
|
729
|
-
const documentId = docParam.replace(/^automerge:/, "");
|
|
730
|
-
if (isValidDocumentId(documentId)) {
|
|
731
|
-
return stringifyAutomergeUrl({ documentId: documentId as DocumentId });
|
|
732
|
-
}
|
|
733
|
-
return undefined;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
interface HashRoutingParams {
|
|
737
|
-
rootElement: HTMLElement;
|
|
738
|
-
repo: Repo;
|
|
739
|
-
accountDocHandle: DocHandle<AccountDoc>;
|
|
740
|
-
titleSuffix: string;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
function installHashRouting(params: HashRoutingParams): void {
|
|
744
|
-
const { rootElement, repo, accountDocHandle, titleSuffix } = params;
|
|
745
|
-
|
|
746
|
-
rootElement.addEventListener("patchwork:open-document", async (event) => {
|
|
747
|
-
const params = new URLSearchParams(window.location.hash.slice(1));
|
|
748
|
-
const { url, toolId, type, title } = event.detail as {
|
|
749
|
-
url: AutomergeUrl;
|
|
750
|
-
toolId?: string;
|
|
751
|
-
type?: string;
|
|
752
|
-
title?: string;
|
|
753
|
-
};
|
|
754
|
-
// `doc` is now the full automerge URL — heads, if any, live inside it, so
|
|
755
|
-
// the separate `heads=` param is gone.
|
|
756
|
-
params.delete("heads");
|
|
757
|
-
params.set("doc", url);
|
|
758
|
-
if (toolId) params.set("tool", toolId);
|
|
759
|
-
else params.delete("tool");
|
|
760
|
-
if (title) params.set("title", title);
|
|
761
|
-
else params.delete("title");
|
|
762
|
-
if (type) params.set("type", type);
|
|
763
|
-
else params.delete("type");
|
|
764
|
-
window.location.hash = serializeHashParams(params);
|
|
765
|
-
|
|
766
|
-
try {
|
|
767
|
-
const docHandle = await repo.find<{ "@patchwork"?: { type?: string } }>(
|
|
768
|
-
url
|
|
769
|
-
);
|
|
770
|
-
const doc = docHandle.doc();
|
|
771
|
-
const docType = type || doc?.["@patchwork"]?.type;
|
|
772
|
-
if (!docType) return;
|
|
773
|
-
const registry = getRegistry<DatatypeDescription>("patchwork:datatype");
|
|
774
|
-
const datatype = await registry.load(docType);
|
|
775
|
-
if (!datatype) return;
|
|
776
|
-
const docTitle = (datatype.module as DatatypeImplementation).getTitle(
|
|
777
|
-
doc
|
|
778
|
-
);
|
|
779
|
-
if (docTitle) {
|
|
780
|
-
document.title = `${docTitle} | ${titleSuffix}`;
|
|
781
|
-
}
|
|
782
|
-
} catch (e) {
|
|
783
|
-
console.error("Failed to update document title", e);
|
|
784
|
-
}
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
let firstMount = true;
|
|
788
|
-
const reveal = () => {
|
|
789
|
-
if (!firstMount) return;
|
|
790
|
-
firstMount = false;
|
|
791
|
-
rootElement.style.visibility = "visible";
|
|
792
|
-
hideLoadingAnimation();
|
|
793
|
-
};
|
|
794
|
-
|
|
795
|
-
rootElement.addEventListener("patchwork:mounted", (event) => {
|
|
796
|
-
handleHashChange();
|
|
797
|
-
if (event.target !== rootElement) return;
|
|
798
|
-
console.info("root element mounted");
|
|
799
|
-
reveal();
|
|
800
|
-
// Re-resolve routing after a beat so deep-links from freshly-loaded tools
|
|
801
|
-
// get a second chance to render.
|
|
802
|
-
setTimeout(handleHashChange, 1000);
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
// Failsafe: if nothing ever mounts, reveal the element anyway after 12s so
|
|
806
|
-
// the user sees *something* rather than a blank page.
|
|
807
|
-
setTimeout(reveal, 12_000);
|
|
808
|
-
|
|
809
|
-
const handleHashChange = async () => {
|
|
810
|
-
const hash = window.location.hash.slice(1);
|
|
811
|
-
|
|
812
|
-
// Legacy big-patchwork link (`<slug>--<docId>?…`): if the hash carries a
|
|
813
|
-
// `--` followed by a valid document id, normalize it to the canonical
|
|
814
|
-
// `#doc=automerge:<docId>` form and let routing re-run on the hashchange.
|
|
815
|
-
const legacyDocId = BIG_PATCHWORK_HASH_REGEX.exec(hash)?.groups?.docId;
|
|
816
|
-
if (legacyDocId && isValidDocumentId(legacyDocId)) {
|
|
817
|
-
window.location.hash = serializeHashParams(
|
|
818
|
-
new URLSearchParams({
|
|
819
|
-
doc: stringifyAutomergeUrl({ documentId: legacyDocId as DocumentId }),
|
|
820
|
-
})
|
|
821
|
-
);
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Bare automerge URL in hash: /#automerge:<documentId>
|
|
826
|
-
if (isValidAutomergeUrl(hash as AutomergeUrl)) {
|
|
827
|
-
const url = hash as AutomergeUrl;
|
|
828
|
-
window.location.hash = "";
|
|
829
|
-
openDocument(rootElement, url);
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
const params = new URLSearchParams(hash);
|
|
834
|
-
const docUrl = docParamToUrl(params.get("doc"));
|
|
835
|
-
const toolId = params.get("tool");
|
|
836
|
-
const title = params.get("title");
|
|
837
|
-
const type = params.get("type");
|
|
838
|
-
const frame = params.get("frame");
|
|
839
|
-
if (frame) {
|
|
840
|
-
const frameDocUrl = docUrl ?? accountDocHandle.url;
|
|
841
|
-
if (
|
|
842
|
-
rootElement.getAttribute("tool-id") !== frame ||
|
|
843
|
-
rootElement.getAttribute("doc-url") !== frameDocUrl
|
|
844
|
-
) {
|
|
845
|
-
rootElement.setAttribute("tool-id", frame);
|
|
846
|
-
rootElement.setAttribute("doc-url", frameDocUrl);
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
if (docUrl) {
|
|
850
|
-
rootElement.dispatchEvent(
|
|
851
|
-
new CustomEvent("patchwork:open-document", {
|
|
852
|
-
detail: { url: docUrl, toolId, title, type },
|
|
853
|
-
})
|
|
854
|
-
);
|
|
855
|
-
}
|
|
856
|
-
};
|
|
857
|
-
|
|
858
|
-
window.addEventListener("hashchange", handleHashChange);
|
|
859
|
-
}
|