@inkandswitch/patchwork-bootloader 0.0.8 → 0.1.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 +23 -0
- package/dist/externals.js +5 -0
- package/dist/service-worker.js +59 -74
- package/dist/setup.d.ts +2 -4
- package/dist/setup.js +147 -27
- package/dist/site.d.ts +3 -2
- package/dist/site.js +38 -26
- package/dist/types.d.ts +4 -0
- package/package.json +14 -14
- package/src/externals.ts +5 -0
- package/src/service-worker.ts +77 -103
- package/src/setup.ts +165 -33
- package/src/site.ts +54 -32
- package/src/types.ts +10 -0
package/dist/site.js
CHANGED
|
@@ -19,13 +19,19 @@ import * as AutomergeRepo from "@automerge/automerge-repo/slim";
|
|
|
19
19
|
import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
|
|
20
20
|
import { ModuleWatcher } from "@inkandswitch/patchwork-filesystem";
|
|
21
21
|
import { openDocument, registerPatchworkViewElement, } from "@inkandswitch/patchwork-elements";
|
|
22
|
-
import { getRegistry, registerPlugins, resolveAccountHandle, } from "@inkandswitch/patchwork-plugins";
|
|
22
|
+
import { getRegistry, registerPlugins, resolveAccountHandle, unregisterPlugins, } from "@inkandswitch/patchwork-plugins";
|
|
23
23
|
import * as plugins from "@inkandswitch/patchwork-plugins";
|
|
24
24
|
import setupServiceWorker from "./setup.js";
|
|
25
25
|
import { SwLogReader } from "./sw-logger.js";
|
|
26
|
+
import debug from "debug";
|
|
27
|
+
const log = debug("patchwork:bootloader:site");
|
|
26
28
|
const DEFAULT_REMOTE_STORAGE_ID = "3760df37-a4c6-4f66-9ecd-732039a9385d";
|
|
27
29
|
// Legacy big-patchwork hash shape: `slug--<documentId>[?=type]`.
|
|
28
30
|
const BIG_PATCHWORK_HASH_REGEX = /(?<title>[A-Za-z0-9-]+)--(?<docId>[1-9A-HJ-NP-Za-km-z]+)(?<type>\?=[^&?]+)?/;
|
|
31
|
+
const [automergeWasm, subductionWasm] = await Promise.all([
|
|
32
|
+
fetch("/automerge.wasm?main").then((r) => r.bytes()),
|
|
33
|
+
fetch("/subduction.wasm").then((r) => r.bytes()),
|
|
34
|
+
]);
|
|
29
35
|
/**
|
|
30
36
|
* Boot a Patchwork browser site.
|
|
31
37
|
*
|
|
@@ -37,11 +43,8 @@ const BIG_PATCHWORK_HASH_REGEX = /(?<title>[A-Za-z0-9-]+)--(?<docId>[1-9A-HJ-NP-
|
|
|
37
43
|
*/
|
|
38
44
|
export async function bootPatchworkSite(config) {
|
|
39
45
|
const defaultModulesUrl = resolveDefaultModulesUrl(config.defaultModulesUrl);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
fetch("/automerge.wasm").then((r) => r.bytes()),
|
|
43
|
-
fetch("/subduction.wasm").then((r) => r.bytes()),
|
|
44
|
-
]);
|
|
46
|
+
showLoadingAnimation();
|
|
47
|
+
log(`booting`, config);
|
|
45
48
|
await initializeWasm(automergeWasm);
|
|
46
49
|
initSubductionSync(subductionWasm);
|
|
47
50
|
const repo = new Repo({
|
|
@@ -50,21 +53,29 @@ export async function bootPatchworkSite(config) {
|
|
|
50
53
|
return peerId.includes("service-worker");
|
|
51
54
|
},
|
|
52
55
|
enableRemoteHeadsGossiping: true,
|
|
56
|
+
peerId: `${config.titleSuffix}-tab-${crypto.randomUUID()}`,
|
|
53
57
|
});
|
|
54
58
|
repo.subscribeToRemotes(config.remoteStorageIds ?? [DEFAULT_REMOTE_STORAGE_ID]);
|
|
55
59
|
const sw = await setupServiceWorker();
|
|
56
60
|
if (!sw)
|
|
57
61
|
throw new Error("Failed to set up service worker");
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
let activeServiceWorkerPort;
|
|
63
|
+
const connectServiceWorkerPort = async (port) => {
|
|
64
|
+
const previousPort = activeServiceWorkerPort;
|
|
65
|
+
activeServiceWorkerPort = port;
|
|
66
|
+
const net = new MessageChannelNetworkAdapter(port);
|
|
67
|
+
repo.networkSubsystem.addNetworkAdapter(net);
|
|
68
|
+
await net.whenReady();
|
|
69
|
+
previousPort?.close();
|
|
70
|
+
};
|
|
71
|
+
await sw.subscribeToRepoChannel(connectServiceWorkerPort);
|
|
61
72
|
installDevConsoleGlobals(repo);
|
|
62
73
|
registerPatchworkViewElement({ repo });
|
|
63
74
|
// The watcher is started with the site's default-tools bundle alone so that
|
|
64
75
|
// `resolveAccountHandle` below has something to await on (the `account`
|
|
65
76
|
// datatype lives in that bundle today). The user's own module-settings URL
|
|
66
77
|
// is added lazily once it appears on the account doc — see below.
|
|
67
|
-
const moduleWatcher = new ModuleWatcher(repo,
|
|
78
|
+
const moduleWatcher = new ModuleWatcher(repo, { system: defaultModulesUrl }, onModuleLoaded, unregisterPlugins);
|
|
68
79
|
const accountDocHandle = await resolveAccountHandle(repo, {
|
|
69
80
|
storageKey: config.accountStorageKey,
|
|
70
81
|
});
|
|
@@ -78,10 +89,13 @@ export async function bootPatchworkSite(config) {
|
|
|
78
89
|
logToolRegistryWhenLoaded(moduleWatcher);
|
|
79
90
|
window.patchwork = {
|
|
80
91
|
repo,
|
|
81
|
-
|
|
92
|
+
packages: moduleWatcher,
|
|
82
93
|
plugins,
|
|
83
94
|
accountDocHandle,
|
|
84
|
-
sw:
|
|
95
|
+
sw: {
|
|
96
|
+
...buildSwLogApi(),
|
|
97
|
+
subscribeToRepoChannel: sw.subscribeToRepoChannel,
|
|
98
|
+
},
|
|
85
99
|
};
|
|
86
100
|
window.uncache = uncache;
|
|
87
101
|
installHashRouting({
|
|
@@ -111,19 +125,14 @@ function installDevConsoleGlobals(repo) {
|
|
|
111
125
|
window.repo = repo;
|
|
112
126
|
window.Automerge = Automerge;
|
|
113
127
|
window.AutomergeRepo = AutomergeRepo;
|
|
114
|
-
window.getRepoChannel = () => {
|
|
115
|
-
const { port1, port2 } = new MessageChannel();
|
|
116
|
-
navigator.serviceWorker.controller.postMessage({ type: "port" }, [port2]);
|
|
117
|
-
return port1;
|
|
118
|
-
};
|
|
119
128
|
}
|
|
120
129
|
function onModuleLoaded(name, mod) {
|
|
121
130
|
if (Array.isArray(mod.plugins)) {
|
|
122
|
-
|
|
131
|
+
log(`registering ${mod.plugins.length} plugin(s) from ${name.slice(0, 30)}...`, mod.plugins.map((p) => `${p.type}:${p.id}`));
|
|
123
132
|
registerPlugins(mod.plugins, name);
|
|
124
133
|
}
|
|
125
134
|
else {
|
|
126
|
-
console.warn(`
|
|
135
|
+
console.warn(`module ${name.slice(0, 30)}... has no plugins array`, Object.keys(mod));
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
138
|
/**
|
|
@@ -136,7 +145,7 @@ function wireModuleSettingsWhenReady(accountDocHandle, moduleWatcher) {
|
|
|
136
145
|
const url = accountDocHandle.doc()?.moduleSettingsUrl;
|
|
137
146
|
if (!url)
|
|
138
147
|
return;
|
|
139
|
-
void moduleWatcher.addUrl(url);
|
|
148
|
+
void moduleWatcher.addUrl("user", url);
|
|
140
149
|
accountDocHandle.off("change", wire);
|
|
141
150
|
};
|
|
142
151
|
wire();
|
|
@@ -151,7 +160,6 @@ function wireModuleSettingsWhenReady(accountDocHandle, moduleWatcher) {
|
|
|
151
160
|
*/
|
|
152
161
|
function primeRootElement(rootElement, accountDocHandle) {
|
|
153
162
|
rootElement.style.visibility = "hidden";
|
|
154
|
-
showLoadingAnimation();
|
|
155
163
|
const initialParams = new URLSearchParams(location.hash.slice(1));
|
|
156
164
|
if (initialParams.has("frame")) {
|
|
157
165
|
rootElement.setAttribute("tool-id", initialParams.get("frame"));
|
|
@@ -171,10 +179,10 @@ function logToolRegistryWhenLoaded(moduleWatcher) {
|
|
|
171
179
|
.then(() => {
|
|
172
180
|
const toolReg = getRegistry("patchwork:tool");
|
|
173
181
|
const tools = toolReg.all();
|
|
174
|
-
|
|
182
|
+
log(`doneLoading: ${tools.length} tools registered:`, tools.map((t) => t.id));
|
|
175
183
|
})
|
|
176
184
|
.catch((err) => {
|
|
177
|
-
console.error("
|
|
185
|
+
console.error("doneLoading rejected:", err);
|
|
178
186
|
});
|
|
179
187
|
}
|
|
180
188
|
function buildSwLogApi() {
|
|
@@ -184,11 +192,11 @@ function buildSwLogApi() {
|
|
|
184
192
|
for (const e of entries) {
|
|
185
193
|
const prefix = `[${e.ts}] [${e.level}]`;
|
|
186
194
|
if (e.data !== undefined)
|
|
187
|
-
|
|
195
|
+
log(prefix, e.msg, e.data);
|
|
188
196
|
else
|
|
189
|
-
|
|
197
|
+
log(prefix, e.msg);
|
|
190
198
|
}
|
|
191
|
-
|
|
199
|
+
log(`--- ${entries.length} entries ---`);
|
|
192
200
|
},
|
|
193
201
|
tailLogs: (n = 200) => SwLogReader.tail(n),
|
|
194
202
|
exportLogs: () => SwLogReader.exportAll(),
|
|
@@ -218,6 +226,10 @@ function showLoadingAnimation() {
|
|
|
218
226
|
radial-gradient(ellipse 65% 55% at 50% 50%, #f1e6f6, transparent 80%);
|
|
219
227
|
animation: pw-bootloader-pulse 3.5s ease-in-out infinite;
|
|
220
228
|
transition: opacity 0.6s ease-out;
|
|
229
|
+
top: 0;
|
|
230
|
+
left: 0;
|
|
231
|
+
right: 0;
|
|
232
|
+
bottom: 0;
|
|
221
233
|
}
|
|
222
234
|
@media (prefers-color-scheme: dark) {
|
|
223
235
|
#${LOADING_ELEMENT_ID} {
|
package/dist/types.d.ts
CHANGED
|
@@ -5,3 +5,7 @@ export type SetupServiceWorkerOptions = {
|
|
|
5
5
|
*/
|
|
6
6
|
path?: string;
|
|
7
7
|
};
|
|
8
|
+
export type ServiceWorkerRepoChannelListener = (port: MessagePort) => void | Promise<void>;
|
|
9
|
+
export type SetupServiceWorkerResult = {
|
|
10
|
+
subscribeToRepoChannel: (listener: ServiceWorkerRepoChannelListener) => Promise<() => void>;
|
|
11
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inkandswitch/patchwork-bootloader",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"author": "chee",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,27 +40,27 @@
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@automerge/automerge": "3.2.
|
|
44
|
-
"@automerge/automerge-repo": "2.6.0-subduction.
|
|
45
|
-
"@automerge/automerge-subduction": "0.
|
|
46
|
-
"@automerge/automerge-repo-network-messagechannel": "2.6.0-subduction.
|
|
47
|
-
"@automerge/automerge-repo-network-websocket": "2.6.0-subduction.
|
|
48
|
-
"@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.
|
|
49
|
-
"@automerge/vanillajs": "2.6.0-subduction.
|
|
43
|
+
"@automerge/automerge": "3.2.6",
|
|
44
|
+
"@automerge/automerge-repo": "2.6.0-subduction.19",
|
|
45
|
+
"@automerge/automerge-subduction": "0.12.0",
|
|
46
|
+
"@automerge/automerge-repo-network-messagechannel": "2.6.0-subduction.19",
|
|
47
|
+
"@automerge/automerge-repo-network-websocket": "2.6.0-subduction.19",
|
|
48
|
+
"@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.19",
|
|
49
|
+
"@automerge/vanillajs": "2.6.0-subduction.19",
|
|
50
50
|
"@types/debug": "^4.1.12",
|
|
51
51
|
"debug": "^4.4.3",
|
|
52
52
|
"resolve.exports": "^2.0.3",
|
|
53
53
|
"service-worker-types": "npm:@types/serviceworker@^0.0.153",
|
|
54
54
|
"tinyargs": "^0.1.4",
|
|
55
|
-
"@inkandswitch/patchwork-elements": "^0.0.
|
|
56
|
-
"@inkandswitch/patchwork-
|
|
57
|
-
"@inkandswitch/patchwork-
|
|
55
|
+
"@inkandswitch/patchwork-elements": "^0.0.8",
|
|
56
|
+
"@inkandswitch/patchwork-plugins": "^0.0.8",
|
|
57
|
+
"@inkandswitch/patchwork-filesystem": "^0.0.6"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@automerge/automerge": "3.2.
|
|
61
|
-
"@automerge/automerge-repo": "2.6.0-subduction.
|
|
60
|
+
"@automerge/automerge": "3.2.6",
|
|
61
|
+
"@automerge/automerge-repo": "2.6.0-subduction.19",
|
|
62
62
|
"@automerge/automerge-repo-keyhive": "0.2.0-alpha.1d",
|
|
63
|
-
"@automerge/vanillajs": "2.6.0-subduction.
|
|
63
|
+
"@automerge/vanillajs": "2.6.0-subduction.19"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"build": "tsc",
|
package/src/externals.ts
CHANGED
|
@@ -6,7 +6,12 @@ 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",
|
|
12
|
+
"@automerge/automerge-repo-react-hooks",
|
|
13
|
+
"@automerge/automerge-repo-network-messagechannel",
|
|
14
|
+
"@automerge/automerge-repo-storage-indexeddb",
|
|
10
15
|
"@automerge/automerge-subduction",
|
|
11
16
|
"@automerge/automerge-subduction/slim",
|
|
12
17
|
"@keyhive/keyhive",
|
package/src/service-worker.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { SwLogger } from "./sw-logger.js";
|
|
|
8
8
|
// Uses /slim to avoid top-level await (disallowed in service workers).
|
|
9
9
|
// Wasm is fetched from /automerge.wasm (emitted by the vite plugin) instead
|
|
10
10
|
// of bundling the ~3MB base64 string.
|
|
11
|
-
import { initializeWasm } from "@automerge/automerge/slim";
|
|
11
|
+
import { initializeWasm, hasHeads } from "@automerge/automerge/slim";
|
|
12
12
|
// eslint-disable-next-line
|
|
13
13
|
// @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
|
|
14
14
|
import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
|
|
@@ -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";
|
|
@@ -35,8 +31,10 @@ import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websoc
|
|
|
35
31
|
// TEMPORARY: enable debug npm module in SW context (no localStorage available)
|
|
36
32
|
let cachename = "default";
|
|
37
33
|
let debugging = false;
|
|
34
|
+
const workerInstanceId = crypto.randomUUID();
|
|
38
35
|
|
|
39
36
|
const SUBDUCTION_ENDPOINTS = ["wss://subduction.sync.inkandswitch.com"];
|
|
37
|
+
const RESOLVE_TIMEOUT_MS = 30_000;
|
|
40
38
|
|
|
41
39
|
// ── Persistent logger ───────────────────────────────────────────────────
|
|
42
40
|
// Initialized eagerly so it's available for the entire SW lifetime.
|
|
@@ -66,9 +64,7 @@ const slog = SwLogger.open().then((logger) => {
|
|
|
66
64
|
return logger;
|
|
67
65
|
});
|
|
68
66
|
|
|
69
|
-
const cacheableStatuses = [
|
|
70
|
-
200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501,
|
|
71
|
-
];
|
|
67
|
+
const cacheableStatuses = [200, 203, 204, 206];
|
|
72
68
|
|
|
73
69
|
function log(...args: any[]) {
|
|
74
70
|
if (!debugging) return;
|
|
@@ -103,12 +99,13 @@ let repoPromise: Promise<Repo> | null = null;
|
|
|
103
99
|
|
|
104
100
|
function getRepo() {
|
|
105
101
|
if (!repoPromise) {
|
|
106
|
-
|
|
102
|
+
const p: Promise<Repo> = (async () => {
|
|
107
103
|
const logger = await slog;
|
|
104
|
+
logger.info("getRepo: starting");
|
|
108
105
|
|
|
109
106
|
logger.info("fetching wasm modules");
|
|
110
107
|
const [amWasmBuf, sdnWasmBuf] = await Promise.all([
|
|
111
|
-
fetch("/automerge.wasm").then((r) => r.arrayBuffer()),
|
|
108
|
+
fetch("/automerge.wasm?sw").then((r) => r.arrayBuffer()),
|
|
112
109
|
fetch("/subduction.wasm").then((r) => r.arrayBuffer()),
|
|
113
110
|
]);
|
|
114
111
|
initSubductionSync(new Uint8Array(sdnWasmBuf));
|
|
@@ -127,7 +124,7 @@ function getRepo() {
|
|
|
127
124
|
},
|
|
128
125
|
enableRemoteHeadsGossiping: true,
|
|
129
126
|
subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
|
|
130
|
-
network: [new WebSocketClientAdapter("wss://sync3.automerge.org")],
|
|
127
|
+
//network: [new WebSocketClientAdapter("wss://sync3.automerge.org")],
|
|
131
128
|
});
|
|
132
129
|
|
|
133
130
|
(self as any).repo = repo;
|
|
@@ -143,6 +140,13 @@ function getRepo() {
|
|
|
143
140
|
|
|
144
141
|
return repo;
|
|
145
142
|
})();
|
|
143
|
+
// If construction fails (e.g. wasm fetch errors out because the SW was
|
|
144
|
+
// terminated mid-flight), don't permanently cache the rejection — clear
|
|
145
|
+
// the slot so the next caller can retry from scratch.
|
|
146
|
+
p.catch(() => {
|
|
147
|
+
if (repoPromise === p) repoPromise = null;
|
|
148
|
+
});
|
|
149
|
+
repoPromise = p;
|
|
146
150
|
}
|
|
147
151
|
return repoPromise;
|
|
148
152
|
}
|
|
@@ -163,17 +167,39 @@ self.addEventListener("message", async (event) => {
|
|
|
163
167
|
const [pongPort] = event.ports;
|
|
164
168
|
log("ping");
|
|
165
169
|
if (pongPort) {
|
|
166
|
-
pongPort.postMessage({ type: "pong" });
|
|
170
|
+
pongPort.postMessage({ type: "pong", workerInstanceId });
|
|
167
171
|
log("pong");
|
|
168
172
|
pongPort.close();
|
|
169
173
|
} else if (event.source) {
|
|
170
|
-
(event.source as unknown as Client).postMessage({
|
|
174
|
+
(event.source as unknown as Client).postMessage({
|
|
175
|
+
type: "pong",
|
|
176
|
+
workerInstanceId,
|
|
177
|
+
});
|
|
171
178
|
log("pong");
|
|
172
179
|
}
|
|
173
180
|
} else if (event.data.type == "port") {
|
|
174
181
|
log("received messagechannel");
|
|
175
182
|
const [port] = event.ports;
|
|
176
|
-
|
|
183
|
+
const source = event.source as Client | null;
|
|
184
|
+
const id = event.data.id;
|
|
185
|
+
// event.waitUntil keeps the SW alive until the work completes. Without
|
|
186
|
+
// it, the browser can terminate the SW the moment this synchronous block
|
|
187
|
+
// returns, killing the in-flight wasm fetch.
|
|
188
|
+
(event as unknown as FetchEvent).waitUntil(
|
|
189
|
+
connectPort(port).then(
|
|
190
|
+
() => source?.postMessage({ type: "port-ready", id, workerInstanceId }),
|
|
191
|
+
(err) => {
|
|
192
|
+
console.error("connectPort failed", err);
|
|
193
|
+
// Tell the client we failed so it doesn't hang forever.
|
|
194
|
+
source?.postMessage({
|
|
195
|
+
type: "port-failed",
|
|
196
|
+
id,
|
|
197
|
+
error: String(err),
|
|
198
|
+
workerInstanceId,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
);
|
|
177
203
|
} else if (event.data.type == "cachename") {
|
|
178
204
|
const nextCachename = event.data.cachename;
|
|
179
205
|
if (cachename == nextCachename) {
|
|
@@ -190,11 +216,6 @@ self.addEventListener("message", async (event) => {
|
|
|
190
216
|
}
|
|
191
217
|
});
|
|
192
218
|
|
|
193
|
-
interface FileDoc {
|
|
194
|
-
content: string | Uint8Array;
|
|
195
|
-
mimeType?: string;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
219
|
// ── Automerge URL resolution ───────────────────────────────────────────
|
|
199
220
|
|
|
200
221
|
async function resolveAutomergeUrl(automergeURL: URL): Promise<Response> {
|
|
@@ -209,11 +230,11 @@ async function resolveAutomergeUrl(automergeURL: URL): Promise<Response> {
|
|
|
209
230
|
// Trim trailing empty path segment
|
|
210
231
|
if (path.length && !path[path.length - 1]) path.pop();
|
|
211
232
|
|
|
212
|
-
const { heads, documentId } = parseAutomergeUrl(maybeAutomergeUrl);
|
|
233
|
+
const { heads, hexHeads, documentId } = parseAutomergeUrl(maybeAutomergeUrl);
|
|
234
|
+
const signal = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
|
|
213
235
|
|
|
214
236
|
if (!heads) {
|
|
215
|
-
|
|
216
|
-
const folder = await repo.find(maybeAutomergeUrl);
|
|
237
|
+
const folder = await repo.find(maybeAutomergeUrl, { signal });
|
|
217
238
|
const latestHeads = folder.heads();
|
|
218
239
|
const url = stringifyAutomergeUrl({ documentId, heads: latestHeads });
|
|
219
240
|
let location = `/${encodeURIComponent(url)}`;
|
|
@@ -221,93 +242,35 @@ async function resolveAutomergeUrl(automergeURL: URL): Promise<Response> {
|
|
|
221
242
|
return Response.redirect(location, 307);
|
|
222
243
|
}
|
|
223
244
|
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
repo,
|
|
233
|
-
folderHandle,
|
|
234
|
-
path.map(decodeURIComponent)
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
// If not found as a direct path, try resolving as a package subpath export
|
|
238
|
-
// e.g. /automerge%3Adocid/abc → exports["./abc"] → "./dist/abc.js"
|
|
239
|
-
if (!fileHandle) {
|
|
240
|
-
const subpath = "./" + path.map(decodeURIComponent).join("/");
|
|
241
|
-
const pkgFileHandle = await findHandleInFolderHandle<FileDoc>(
|
|
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
|
-
}
|
|
245
|
+
// Load by documentId only so we can verify the requested heads are actually
|
|
246
|
+
// in our local history. repo.find with a heads-bearing URL returns a view
|
|
247
|
+
// at those heads, which silently materializes garbage if we never synced them.
|
|
248
|
+
const baseHandle = await repo.find(stringifyAutomergeUrl({ documentId }), {
|
|
249
|
+
signal,
|
|
250
|
+
});
|
|
251
|
+
if (!hasHeads(baseHandle.doc(), hexHeads ?? [])) {
|
|
252
|
+
return new Response("heads not found", { status: 404 });
|
|
290
253
|
}
|
|
254
|
+
const rootHandle = baseHandle.view(heads);
|
|
255
|
+
|
|
256
|
+
const resolved = await resolvePath(
|
|
257
|
+
repo,
|
|
258
|
+
rootHandle,
|
|
259
|
+
path.map(decodeURIComponent)
|
|
260
|
+
);
|
|
291
261
|
|
|
292
|
-
if (!
|
|
262
|
+
if (!resolved) {
|
|
293
263
|
throw new Error(
|
|
294
264
|
`couldn't resolve ${path.join("/")} in folder at ${maybeAutomergeUrl}`
|
|
295
265
|
);
|
|
296
266
|
}
|
|
297
267
|
|
|
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";
|
|
268
|
+
const body: BodyInit =
|
|
269
|
+
resolved.content instanceof Uint8Array
|
|
270
|
+
? (new Uint8Array(resolved.content) as BlobPart)
|
|
271
|
+
: resolved.content;
|
|
309
272
|
|
|
310
|
-
const headers = new Headers({ "content-type":
|
|
273
|
+
const headers = new Headers({ "content-type": resolved.type });
|
|
311
274
|
headers.set("cross-origin-embedder-policy", "credentialless");
|
|
312
275
|
headers.set("cross-origin-resource-policy", "cross-origin");
|
|
313
276
|
|
|
@@ -353,7 +316,18 @@ self.addEventListener("fetch", (fetchEvent: FetchEvent) => {
|
|
|
353
316
|
});
|
|
354
317
|
}
|
|
355
318
|
|
|
356
|
-
const response = await
|
|
319
|
+
const response = await Promise.race([
|
|
320
|
+
resolveAutomergeUrl(specialURL),
|
|
321
|
+
new Promise<never>((_, reject) =>
|
|
322
|
+
setTimeout(
|
|
323
|
+
() =>
|
|
324
|
+
reject(
|
|
325
|
+
new Error(`resolve timeout after ${RESOLVE_TIMEOUT_MS}ms`)
|
|
326
|
+
),
|
|
327
|
+
RESOLVE_TIMEOUT_MS
|
|
328
|
+
)
|
|
329
|
+
),
|
|
330
|
+
]);
|
|
357
331
|
|
|
358
332
|
if (response.status === 307) {
|
|
359
333
|
return response;
|