@inkandswitch/patchwork-bootloader 0.0.8 → 0.0.9
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/dist/externals.js +2 -0
- package/package.json +1 -1
- package/src/externals.ts +2 -0
- package/src/service-worker.ts +54 -99
- package/src/setup.ts +42 -1
- package/src/site.ts +32 -21
package/dist/externals.js
CHANGED
|
@@ -6,6 +6,8 @@ const externals = [
|
|
|
6
6
|
"@automerge/automerge/slim",
|
|
7
7
|
"@automerge/automerge-repo",
|
|
8
8
|
"@automerge/automerge-repo/slim",
|
|
9
|
+
"@automerge/automerge-repo-network-messagechannel",
|
|
10
|
+
"@automerge/automerge-repo-storage-indexeddb",
|
|
9
11
|
"@automerge/automerge-repo-keyhive",
|
|
10
12
|
"@automerge/automerge-subduction",
|
|
11
13
|
"@automerge/automerge-subduction/slim",
|
package/package.json
CHANGED
package/src/externals.ts
CHANGED
|
@@ -6,6 +6,8 @@ const externals = [
|
|
|
6
6
|
"@automerge/automerge/slim",
|
|
7
7
|
"@automerge/automerge-repo",
|
|
8
8
|
"@automerge/automerge-repo/slim",
|
|
9
|
+
"@automerge/automerge-repo-network-messagechannel",
|
|
10
|
+
"@automerge/automerge-repo-storage-indexeddb",
|
|
9
11
|
"@automerge/automerge-repo-keyhive",
|
|
10
12
|
"@automerge/automerge-subduction",
|
|
11
13
|
"@automerge/automerge-subduction/slim",
|
package/src/service-worker.ts
CHANGED
|
@@ -21,11 +21,7 @@ import {
|
|
|
21
21
|
stringifyAutomergeUrl,
|
|
22
22
|
type PeerId,
|
|
23
23
|
} from "@automerge/automerge-repo/slim";
|
|
24
|
-
import {
|
|
25
|
-
findHandleInFolderHandle,
|
|
26
|
-
resolvePackageExport,
|
|
27
|
-
type FolderDoc,
|
|
28
|
-
} from "@inkandswitch/patchwork-filesystem";
|
|
24
|
+
import { resolvePath } from "@inkandswitch/patchwork-filesystem";
|
|
29
25
|
|
|
30
26
|
// Small adapters — bundled directly into the SW
|
|
31
27
|
import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
|
|
@@ -37,6 +33,7 @@ let cachename = "default";
|
|
|
37
33
|
let debugging = false;
|
|
38
34
|
|
|
39
35
|
const SUBDUCTION_ENDPOINTS = ["wss://subduction.sync.inkandswitch.com"];
|
|
36
|
+
const RESOLVE_TIMEOUT_MS = 30_000;
|
|
40
37
|
|
|
41
38
|
// ── Persistent logger ───────────────────────────────────────────────────
|
|
42
39
|
// Initialized eagerly so it's available for the entire SW lifetime.
|
|
@@ -66,9 +63,7 @@ const slog = SwLogger.open().then((logger) => {
|
|
|
66
63
|
return logger;
|
|
67
64
|
});
|
|
68
65
|
|
|
69
|
-
const cacheableStatuses = [
|
|
70
|
-
200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501,
|
|
71
|
-
];
|
|
66
|
+
const cacheableStatuses = [200, 203, 204, 206];
|
|
72
67
|
|
|
73
68
|
function log(...args: any[]) {
|
|
74
69
|
if (!debugging) return;
|
|
@@ -103,12 +98,13 @@ let repoPromise: Promise<Repo> | null = null;
|
|
|
103
98
|
|
|
104
99
|
function getRepo() {
|
|
105
100
|
if (!repoPromise) {
|
|
106
|
-
|
|
101
|
+
const p: Promise<Repo> = (async () => {
|
|
107
102
|
const logger = await slog;
|
|
103
|
+
logger.info("getRepo: starting");
|
|
108
104
|
|
|
109
105
|
logger.info("fetching wasm modules");
|
|
110
106
|
const [amWasmBuf, sdnWasmBuf] = await Promise.all([
|
|
111
|
-
fetch("/automerge.wasm").then((r) => r.arrayBuffer()),
|
|
107
|
+
fetch("/automerge.wasm?sw").then((r) => r.arrayBuffer()),
|
|
112
108
|
fetch("/subduction.wasm").then((r) => r.arrayBuffer()),
|
|
113
109
|
]);
|
|
114
110
|
initSubductionSync(new Uint8Array(sdnWasmBuf));
|
|
@@ -127,7 +123,7 @@ function getRepo() {
|
|
|
127
123
|
},
|
|
128
124
|
enableRemoteHeadsGossiping: true,
|
|
129
125
|
subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
|
|
130
|
-
network: [new WebSocketClientAdapter("wss://sync3.automerge.org")],
|
|
126
|
+
//network: [new WebSocketClientAdapter("wss://sync3.automerge.org")],
|
|
131
127
|
});
|
|
132
128
|
|
|
133
129
|
(self as any).repo = repo;
|
|
@@ -143,6 +139,13 @@ function getRepo() {
|
|
|
143
139
|
|
|
144
140
|
return repo;
|
|
145
141
|
})();
|
|
142
|
+
// If construction fails (e.g. wasm fetch errors out because the SW was
|
|
143
|
+
// terminated mid-flight), don't permanently cache the rejection — clear
|
|
144
|
+
// the slot so the next caller can retry from scratch.
|
|
145
|
+
p.catch(() => {
|
|
146
|
+
if (repoPromise === p) repoPromise = null;
|
|
147
|
+
});
|
|
148
|
+
repoPromise = p;
|
|
146
149
|
}
|
|
147
150
|
return repoPromise;
|
|
148
151
|
}
|
|
@@ -173,7 +176,20 @@ self.addEventListener("message", async (event) => {
|
|
|
173
176
|
} else if (event.data.type == "port") {
|
|
174
177
|
log("received messagechannel");
|
|
175
178
|
const [port] = event.ports;
|
|
176
|
-
|
|
179
|
+
const source = event.source as Client | null;
|
|
180
|
+
// event.waitUntil keeps the SW alive until the work completes. Without
|
|
181
|
+
// it, the browser can terminate the SW the moment this synchronous block
|
|
182
|
+
// returns, killing the in-flight wasm fetch.
|
|
183
|
+
(event as unknown as FetchEvent).waitUntil(
|
|
184
|
+
connectPort(port).then(
|
|
185
|
+
() => source?.postMessage({ type: "port-ready" }),
|
|
186
|
+
(err) => {
|
|
187
|
+
console.error("connectPort failed", err);
|
|
188
|
+
// Tell the client we failed so it doesn't hang forever.
|
|
189
|
+
source?.postMessage({ type: "port-failed", error: String(err) });
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
);
|
|
177
193
|
} else if (event.data.type == "cachename") {
|
|
178
194
|
const nextCachename = event.data.cachename;
|
|
179
195
|
if (cachename == nextCachename) {
|
|
@@ -190,11 +206,6 @@ self.addEventListener("message", async (event) => {
|
|
|
190
206
|
}
|
|
191
207
|
});
|
|
192
208
|
|
|
193
|
-
interface FileDoc {
|
|
194
|
-
content: string | Uint8Array;
|
|
195
|
-
mimeType?: string;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
209
|
// ── Automerge URL resolution ───────────────────────────────────────────
|
|
199
210
|
|
|
200
211
|
async function resolveAutomergeUrl(automergeURL: URL): Promise<Response> {
|
|
@@ -210,10 +221,10 @@ async function resolveAutomergeUrl(automergeURL: URL): Promise<Response> {
|
|
|
210
221
|
if (path.length && !path[path.length - 1]) path.pop();
|
|
211
222
|
|
|
212
223
|
const { heads, documentId } = parseAutomergeUrl(maybeAutomergeUrl);
|
|
224
|
+
const signal = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
|
|
213
225
|
|
|
214
226
|
if (!heads) {
|
|
215
|
-
|
|
216
|
-
const folder = await repo.find(maybeAutomergeUrl);
|
|
227
|
+
const folder = await repo.find(maybeAutomergeUrl, { signal });
|
|
217
228
|
const latestHeads = folder.heads();
|
|
218
229
|
const url = stringifyAutomergeUrl({ documentId, heads: latestHeads });
|
|
219
230
|
let location = `/${encodeURIComponent(url)}`;
|
|
@@ -221,93 +232,26 @@ async function resolveAutomergeUrl(automergeURL: URL): Promise<Response> {
|
|
|
221
232
|
return Response.redirect(location, 307);
|
|
222
233
|
}
|
|
223
234
|
|
|
224
|
-
|
|
225
|
-
// e.g. /automerge%3Adocid/abc → resolve "abc" via package.json exports
|
|
226
|
-
const folderHandle = await repo.find<FolderDoc>(maybeAutomergeUrl);
|
|
227
|
-
|
|
228
|
-
let fileHandle;
|
|
229
|
-
if (path.length) {
|
|
230
|
-
// Try direct file navigation first
|
|
231
|
-
fileHandle = await findHandleInFolderHandle<FileDoc>(
|
|
232
|
-
repo,
|
|
233
|
-
folderHandle,
|
|
234
|
-
path.map(decodeURIComponent)
|
|
235
|
-
);
|
|
235
|
+
const rootHandle = await repo.find(maybeAutomergeUrl, { signal });
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
repo,
|
|
243
|
-
folderHandle,
|
|
244
|
-
["package.json"]
|
|
245
|
-
);
|
|
246
|
-
if (pkgFileHandle) {
|
|
247
|
-
const pkgDoc = pkgFileHandle.doc() as FileDoc | undefined;
|
|
248
|
-
if (pkgDoc?.content) {
|
|
249
|
-
const pkgJson = JSON.parse(String(pkgDoc.content));
|
|
250
|
-
try {
|
|
251
|
-
const resolved = resolvePackageExport(pkgJson, subpath);
|
|
252
|
-
if (resolved) {
|
|
253
|
-
const resolvedPath = resolved.replace(/^\.\//, "").split("/");
|
|
254
|
-
fileHandle = await findHandleInFolderHandle<FileDoc>(
|
|
255
|
-
repo,
|
|
256
|
-
folderHandle,
|
|
257
|
-
resolvedPath
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
} catch {
|
|
261
|
-
// not a valid export subpath, fall through to error
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
} else {
|
|
267
|
-
// No path — resolve the root export (like "." in package.json)
|
|
268
|
-
const pkgFileHandle = await findHandleInFolderHandle<FileDoc>(
|
|
269
|
-
repo,
|
|
270
|
-
folderHandle,
|
|
271
|
-
["package.json"]
|
|
272
|
-
);
|
|
273
|
-
if (pkgFileHandle) {
|
|
274
|
-
const pkgDoc = pkgFileHandle.doc() as FileDoc | undefined;
|
|
275
|
-
if (pkgDoc?.content) {
|
|
276
|
-
const pkgJson = JSON.parse(String(pkgDoc.content));
|
|
277
|
-
try {
|
|
278
|
-
const resolved = resolvePackageExport(pkgJson);
|
|
279
|
-
if (resolved) {
|
|
280
|
-
const resolvedPath = resolved.replace(/^\.\//, "").split("/");
|
|
281
|
-
fileHandle = await findHandleInFolderHandle<FileDoc>(
|
|
282
|
-
repo,
|
|
283
|
-
folderHandle,
|
|
284
|
-
resolvedPath
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
} catch {}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
237
|
+
const resolved = await resolvePath(
|
|
238
|
+
repo,
|
|
239
|
+
rootHandle,
|
|
240
|
+
path.map(decodeURIComponent)
|
|
241
|
+
);
|
|
291
242
|
|
|
292
|
-
if (!
|
|
243
|
+
if (!resolved) {
|
|
293
244
|
throw new Error(
|
|
294
245
|
`couldn't resolve ${path.join("/")} in folder at ${maybeAutomergeUrl}`
|
|
295
246
|
);
|
|
296
247
|
}
|
|
297
248
|
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
let body: BodyInit =
|
|
305
|
-
content instanceof Uint8Array
|
|
306
|
-
? (new Uint8Array(content) as BlobPart)
|
|
307
|
-
: String(content);
|
|
308
|
-
const mimeType = fileDoc.mimeType ?? "text/plain";
|
|
249
|
+
const body: BodyInit =
|
|
250
|
+
resolved.content instanceof Uint8Array
|
|
251
|
+
? (new Uint8Array(resolved.content) as BlobPart)
|
|
252
|
+
: resolved.content;
|
|
309
253
|
|
|
310
|
-
const headers = new Headers({ "content-type":
|
|
254
|
+
const headers = new Headers({ "content-type": resolved.type });
|
|
311
255
|
headers.set("cross-origin-embedder-policy", "credentialless");
|
|
312
256
|
headers.set("cross-origin-resource-policy", "cross-origin");
|
|
313
257
|
|
|
@@ -353,7 +297,18 @@ self.addEventListener("fetch", (fetchEvent: FetchEvent) => {
|
|
|
353
297
|
});
|
|
354
298
|
}
|
|
355
299
|
|
|
356
|
-
const response = await
|
|
300
|
+
const response = await Promise.race([
|
|
301
|
+
resolveAutomergeUrl(specialURL),
|
|
302
|
+
new Promise<never>((_, reject) =>
|
|
303
|
+
setTimeout(
|
|
304
|
+
() =>
|
|
305
|
+
reject(
|
|
306
|
+
new Error(`resolve timeout after ${RESOLVE_TIMEOUT_MS}ms`)
|
|
307
|
+
),
|
|
308
|
+
RESOLVE_TIMEOUT_MS
|
|
309
|
+
)
|
|
310
|
+
),
|
|
311
|
+
]);
|
|
357
312
|
|
|
358
313
|
if (response.status === 307) {
|
|
359
314
|
return response;
|
package/src/setup.ts
CHANGED
|
@@ -73,6 +73,11 @@ export default async function setupServiceWorker(
|
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
const path = options?.path ?? "/service-worker.js";
|
|
76
|
+
// No controller at this point means the page loaded without a service
|
|
77
|
+
// worker — i.e. this is a first-time install (or a hard reload). We'll
|
|
78
|
+
// reload after activation so the page boots with the SW in control of its
|
|
79
|
+
// initial fetches.
|
|
80
|
+
//const isFirstInstall = !navigator.serviceWorker.controller;
|
|
76
81
|
const reg = await navigator.serviceWorker.register(path, { type: "module" });
|
|
77
82
|
|
|
78
83
|
// If there's an update waiting or installing, wait for it to activate
|
|
@@ -94,9 +99,45 @@ export default async function setupServiceWorker(
|
|
|
94
99
|
});
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
// Send a MessagePort so the SW's repo can sync with clients
|
|
102
|
+
// Send a MessagePort so the SW's repo can sync with clients, and wait for
|
|
103
|
+
// the SW to confirm its repo is constructed before returning. The
|
|
104
|
+
// MessageChannel adapter's whenReady() force-resolves after 100ms regardless
|
|
105
|
+
// of the other end's state, so it can't be used as a real readiness signal
|
|
106
|
+
// on first install (when the SW still has to fetch wasm and build its repo).
|
|
98
107
|
const { port1, port2 } = new MessageChannel();
|
|
108
|
+
const swReady = new Promise<void>((resolve, reject) => {
|
|
109
|
+
let timeout: ReturnType<typeof setTimeout>;
|
|
110
|
+
const cleanup = () => {
|
|
111
|
+
clearTimeout(timeout);
|
|
112
|
+
navigator.serviceWorker.removeEventListener("message", listener);
|
|
113
|
+
};
|
|
114
|
+
const listener = (event: MessageEvent) => {
|
|
115
|
+
if (event.data?.type === "port-ready") {
|
|
116
|
+
cleanup();
|
|
117
|
+
resolve();
|
|
118
|
+
} else if (event.data?.type === "port-failed") {
|
|
119
|
+
cleanup();
|
|
120
|
+
reject(new Error(`service worker init failed: ${event.data.error}`));
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
navigator.serviceWorker.addEventListener("message", listener);
|
|
124
|
+
// Failsafe: don't block boot forever if the SW never replies. Surface the
|
|
125
|
+
// issue and let the rest of the site come up rather than hanging on a
|
|
126
|
+
// blank page.
|
|
127
|
+
timeout = setTimeout(() => {
|
|
128
|
+
cleanup();
|
|
129
|
+
reject(new Error("service worker port-ready timeout"));
|
|
130
|
+
}, 30_000);
|
|
131
|
+
});
|
|
99
132
|
navigator.serviceWorker.controller!.postMessage({ type: "port" }, [port2]);
|
|
133
|
+
try {
|
|
134
|
+
await swReady;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.warn(
|
|
137
|
+
"proceeding without SW ready ack:",
|
|
138
|
+
err instanceof Error ? err.message : err
|
|
139
|
+
);
|
|
140
|
+
}
|
|
100
141
|
|
|
101
142
|
// Keepalive — Chromium idles out service workers after ~30s of inactivity,
|
|
102
143
|
// which tears down the in-memory Repo and forces a cold restart on the next
|
package/src/site.ts
CHANGED
|
@@ -44,11 +44,14 @@ import {
|
|
|
44
44
|
getRegistry,
|
|
45
45
|
registerPlugins,
|
|
46
46
|
resolveAccountHandle,
|
|
47
|
+
unregisterPlugins,
|
|
47
48
|
} from "@inkandswitch/patchwork-plugins";
|
|
48
49
|
import * as plugins from "@inkandswitch/patchwork-plugins";
|
|
49
50
|
|
|
50
51
|
import setupServiceWorker from "./setup.js";
|
|
51
52
|
import { SwLogReader } from "./sw-logger.js";
|
|
53
|
+
import debug from "debug";
|
|
54
|
+
const log = debug("patchwork:bootloader:site");
|
|
52
55
|
|
|
53
56
|
declare global {
|
|
54
57
|
interface Window {
|
|
@@ -59,7 +62,7 @@ declare global {
|
|
|
59
62
|
getRepoChannel: () => MessagePort;
|
|
60
63
|
patchwork: {
|
|
61
64
|
repo: Repo;
|
|
62
|
-
|
|
65
|
+
packages: ModuleWatcher;
|
|
63
66
|
plugins: typeof plugins;
|
|
64
67
|
accountDocHandle: DocHandle<AccountDoc>;
|
|
65
68
|
sw: {
|
|
@@ -125,6 +128,11 @@ const DEFAULT_REMOTE_STORAGE_ID =
|
|
|
125
128
|
const BIG_PATCHWORK_HASH_REGEX =
|
|
126
129
|
/(?<title>[A-Za-z0-9-]+)--(?<docId>[1-9A-HJ-NP-Za-km-z]+)(?<type>\?=[^&?]+)?/;
|
|
127
130
|
|
|
131
|
+
const [automergeWasm, subductionWasm] = await Promise.all([
|
|
132
|
+
fetch("/automerge.wasm?main").then((r) => r.bytes()),
|
|
133
|
+
fetch("/subduction.wasm").then((r) => r.bytes()),
|
|
134
|
+
]);
|
|
135
|
+
|
|
128
136
|
/**
|
|
129
137
|
* Boot a Patchwork browser site.
|
|
130
138
|
*
|
|
@@ -138,12 +146,8 @@ export async function bootPatchworkSite(
|
|
|
138
146
|
config: SiteConfig
|
|
139
147
|
): Promise<BootResult> {
|
|
140
148
|
const defaultModulesUrl = resolveDefaultModulesUrl(config.defaultModulesUrl);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const [automergeWasm, subductionWasm] = await Promise.all([
|
|
144
|
-
fetch("/automerge.wasm").then((r) => r.bytes()),
|
|
145
|
-
fetch("/subduction.wasm").then((r) => r.bytes()),
|
|
146
|
-
]);
|
|
149
|
+
showLoadingAnimation();
|
|
150
|
+
log(`booting`, config);
|
|
147
151
|
await initializeWasm(automergeWasm);
|
|
148
152
|
initSubductionSync(subductionWasm);
|
|
149
153
|
|
|
@@ -153,7 +157,10 @@ export async function bootPatchworkSite(
|
|
|
153
157
|
return peerId.includes("service-worker");
|
|
154
158
|
},
|
|
155
159
|
enableRemoteHeadsGossiping: true,
|
|
160
|
+
peerId:
|
|
161
|
+
`${config.titleSuffix}-tab-${crypto.randomUUID()}` as AutomergeRepo.PeerId,
|
|
156
162
|
});
|
|
163
|
+
|
|
157
164
|
repo.subscribeToRemotes(
|
|
158
165
|
config.remoteStorageIds ?? [DEFAULT_REMOTE_STORAGE_ID]
|
|
159
166
|
);
|
|
@@ -173,8 +180,9 @@ export async function bootPatchworkSite(
|
|
|
173
180
|
// is added lazily once it appears on the account doc — see below.
|
|
174
181
|
const moduleWatcher = new ModuleWatcher(
|
|
175
182
|
repo,
|
|
176
|
-
|
|
177
|
-
onModuleLoaded
|
|
183
|
+
{ system: defaultModulesUrl },
|
|
184
|
+
onModuleLoaded,
|
|
185
|
+
unregisterPlugins
|
|
178
186
|
);
|
|
179
187
|
|
|
180
188
|
const accountDocHandle = await resolveAccountHandle(repo, {
|
|
@@ -197,7 +205,7 @@ export async function bootPatchworkSite(
|
|
|
197
205
|
|
|
198
206
|
window.patchwork = {
|
|
199
207
|
repo,
|
|
200
|
-
|
|
208
|
+
packages: moduleWatcher,
|
|
201
209
|
plugins,
|
|
202
210
|
accountDocHandle,
|
|
203
211
|
sw: buildSwLogApi(),
|
|
@@ -247,14 +255,14 @@ function installDevConsoleGlobals(repo: Repo): void {
|
|
|
247
255
|
|
|
248
256
|
function onModuleLoaded(name: string, mod: any): void {
|
|
249
257
|
if (Array.isArray(mod.plugins)) {
|
|
250
|
-
|
|
251
|
-
`
|
|
258
|
+
log(
|
|
259
|
+
`registering ${mod.plugins.length} plugin(s) from ${name.slice(0, 30)}...`,
|
|
252
260
|
mod.plugins.map((p: any) => `${p.type}:${p.id}`)
|
|
253
261
|
);
|
|
254
262
|
registerPlugins(mod.plugins, name);
|
|
255
263
|
} else {
|
|
256
264
|
console.warn(
|
|
257
|
-
`
|
|
265
|
+
`module ${name.slice(0, 30)}... has no plugins array`,
|
|
258
266
|
Object.keys(mod)
|
|
259
267
|
);
|
|
260
268
|
}
|
|
@@ -272,7 +280,7 @@ function wireModuleSettingsWhenReady(
|
|
|
272
280
|
const wire = () => {
|
|
273
281
|
const url = accountDocHandle.doc()?.moduleSettingsUrl;
|
|
274
282
|
if (!url) return;
|
|
275
|
-
void moduleWatcher.addUrl(url);
|
|
283
|
+
void moduleWatcher.addUrl("user", url);
|
|
276
284
|
accountDocHandle.off("change", wire);
|
|
277
285
|
};
|
|
278
286
|
wire();
|
|
@@ -291,7 +299,6 @@ function primeRootElement(
|
|
|
291
299
|
accountDocHandle: DocHandle<AccountDoc>
|
|
292
300
|
): void {
|
|
293
301
|
rootElement.style.visibility = "hidden";
|
|
294
|
-
showLoadingAnimation();
|
|
295
302
|
|
|
296
303
|
const initialParams = new URLSearchParams(location.hash.slice(1));
|
|
297
304
|
if (initialParams.has("frame")) {
|
|
@@ -312,13 +319,13 @@ function logToolRegistryWhenLoaded(moduleWatcher: ModuleWatcher): void {
|
|
|
312
319
|
.then(() => {
|
|
313
320
|
const toolReg = getRegistry("patchwork:tool");
|
|
314
321
|
const tools = toolReg.all();
|
|
315
|
-
|
|
316
|
-
`
|
|
322
|
+
log(
|
|
323
|
+
`doneLoading: ${tools.length} tools registered:`,
|
|
317
324
|
tools.map((t: any) => t.id)
|
|
318
325
|
);
|
|
319
326
|
})
|
|
320
327
|
.catch((err: unknown) => {
|
|
321
|
-
console.error("
|
|
328
|
+
console.error("doneLoading rejected:", err);
|
|
322
329
|
});
|
|
323
330
|
}
|
|
324
331
|
|
|
@@ -328,10 +335,10 @@ function buildSwLogApi(): Window["patchwork"]["sw"] {
|
|
|
328
335
|
const entries = await SwLogReader.tail(n);
|
|
329
336
|
for (const e of entries) {
|
|
330
337
|
const prefix = `[${e.ts}] [${e.level}]`;
|
|
331
|
-
if (e.data !== undefined)
|
|
332
|
-
else
|
|
338
|
+
if (e.data !== undefined) log(prefix, e.msg, e.data);
|
|
339
|
+
else log(prefix, e.msg);
|
|
333
340
|
}
|
|
334
|
-
|
|
341
|
+
log(`--- ${entries.length} entries ---`);
|
|
335
342
|
},
|
|
336
343
|
tailLogs: (n = 200) => SwLogReader.tail(n),
|
|
337
344
|
exportLogs: () => SwLogReader.exportAll(),
|
|
@@ -363,6 +370,10 @@ function showLoadingAnimation(): void {
|
|
|
363
370
|
radial-gradient(ellipse 65% 55% at 50% 50%, #f1e6f6, transparent 80%);
|
|
364
371
|
animation: pw-bootloader-pulse 3.5s ease-in-out infinite;
|
|
365
372
|
transition: opacity 0.6s ease-out;
|
|
373
|
+
top: 0;
|
|
374
|
+
left: 0;
|
|
375
|
+
right: 0;
|
|
376
|
+
bottom: 0;
|
|
366
377
|
}
|
|
367
378
|
@media (prefers-color-scheme: dark) {
|
|
368
379
|
#${LOADING_ELEMENT_ID} {
|