@lix-js/sdk 0.12.3 → 0.15.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/README.md +54 -10
- package/dist/binding-types.d.ts +37 -9
- package/dist/binding.browser.d.ts +2 -2
- package/dist/binding.browser.js +20 -13
- package/dist/binding.node-wasm.d.ts +2 -2
- package/dist/binding.node-wasm.js +9 -11
- package/dist/binding.node.d.ts +3 -3
- package/dist/binding.node.js +70 -13
- package/dist/bundled-plugins/plugin_csv.lixplugin +0 -0
- package/dist/bundled-plugins/plugin_markdown.lixplugin +0 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/lix.d.ts +27 -5
- package/dist/lix.js +125 -6
- package/dist/open-lix.d.ts +4 -5
- package/dist/open-lix.js +174 -33
- package/dist/remote/client.d.ts +1 -2
- package/dist/remote/client.js +40 -1151
- package/dist/remote/server-protocol.d.ts +5 -6
- package/dist/remote/server-protocol.js +40 -6
- package/dist/result.d.ts +6 -13
- package/dist/result.js +9 -35
- package/dist/snapshot-restore.d.ts +6 -0
- package/dist/snapshot-restore.js +101 -0
- package/dist/storage-adapter.d.ts +175 -19
- package/dist/storage-adapter.js +21 -0
- package/dist/types.d.ts +104 -34
- package/dist/value.d.ts +1 -0
- package/dist/value.js +8 -0
- package/dist/wasm/lix_js_sdk.d.ts +121 -24
- package/dist/wasm/lix_js_sdk.js +606 -78
- package/dist/wasm/lix_js_sdk_bg.wasm +0 -0
- package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +48 -6
- package/dist/wasm-init.d.ts +2 -0
- package/dist/wasm-init.js +23 -0
- package/dist/worker/client.d.ts +31 -5
- package/dist/worker/client.js +541 -45
- package/dist/worker/factory.browser.d.ts +2 -2
- package/dist/worker/factory.node.d.ts +2 -2
- package/dist/worker/factory.node.js +3 -10
- package/dist/worker/host.d.ts +4 -2
- package/dist/worker/host.js +401 -43
- package/dist/worker/protocol.d.ts +107 -3
- package/package.json +6 -10
- package/dist/indexeddb-backend.d.ts +0 -19
- package/dist/indexeddb-backend.js +0 -121
- package/dist/remote/sse.d.ts +0 -12
- package/dist/remote/sse.js +0 -87
- package/dist/workerd.d.ts +0 -25
- package/dist/workerd.js +0 -35
package/dist/worker/client.js
CHANGED
|
@@ -1,30 +1,109 @@
|
|
|
1
1
|
import { createWorkerConnection, openDirectLixBinding } from "#worker-factory";
|
|
2
|
-
import {
|
|
2
|
+
import { ownedSnapshotRestoreChunks } from "../snapshot-restore.js";
|
|
3
|
+
import { deserializeWorkerError, serializeWorkerError, } from "./protocol.js";
|
|
3
4
|
const MAX_IDLE_WORKERS = 1;
|
|
4
5
|
// The common serial reopen path retains one worker so its prepared plugin cache
|
|
5
6
|
// survives close(). Concurrent opens still receive isolated workers.
|
|
6
7
|
const idleWorkers = [];
|
|
7
|
-
export async function openLixWorker(storage, onDisposed, telemetry) {
|
|
8
|
+
export async function openLixWorker(storage, onDisposed, telemetry, server, onProgress, snapshot) {
|
|
8
9
|
let client = idleWorkers.pop();
|
|
9
10
|
while (client?.isDisposed)
|
|
10
11
|
client = idleWorkers.pop();
|
|
11
12
|
client ??= new LixWorkerClient();
|
|
12
|
-
client.beginLease(onDisposed, telemetry);
|
|
13
|
+
client.beginLease(onDisposed, telemetry, server, onProgress);
|
|
14
|
+
let snapshotReader;
|
|
15
|
+
let snapshotPumpStarted = false;
|
|
13
16
|
try {
|
|
14
|
-
|
|
17
|
+
// Lock the source before telling the worker to start a restore. A locked or
|
|
18
|
+
// otherwise unusable stream therefore cannot leave a worker open pending input.
|
|
19
|
+
snapshotReader = snapshot?.getReader();
|
|
20
|
+
const snapshotId = snapshotReader
|
|
21
|
+
? client.allocateSnapshotInputId()
|
|
22
|
+
: undefined;
|
|
23
|
+
const open = client.request({
|
|
15
24
|
kind: "open",
|
|
16
25
|
storage,
|
|
17
26
|
telemetryEnabled: telemetry !== undefined,
|
|
18
|
-
|
|
27
|
+
progressEnabled: onProgress !== undefined,
|
|
28
|
+
snapshotId,
|
|
29
|
+
server: serializeSyncServer(server),
|
|
30
|
+
}, 0);
|
|
31
|
+
if (snapshotReader && snapshotId !== undefined) {
|
|
32
|
+
snapshotPumpStarted = true;
|
|
33
|
+
await pumpSnapshotToWorker(client, snapshotReader, snapshotId, open);
|
|
34
|
+
}
|
|
35
|
+
client.openReport = await open;
|
|
19
36
|
return client;
|
|
20
37
|
}
|
|
21
38
|
catch (error) {
|
|
39
|
+
if (snapshotReader && !snapshotPumpStarted) {
|
|
40
|
+
await snapshotReader.cancel(error).catch(() => undefined);
|
|
41
|
+
snapshotReader.releaseLock();
|
|
42
|
+
}
|
|
22
43
|
await client.terminate();
|
|
23
44
|
throw error;
|
|
24
45
|
}
|
|
25
46
|
}
|
|
47
|
+
export async function pumpSnapshotToWorker(client, reader, snapshotId, open) {
|
|
48
|
+
let producerError = false;
|
|
49
|
+
// Convert rejection into data immediately so an early worker-open failure is
|
|
50
|
+
// always observed, even while or after a source read is pending.
|
|
51
|
+
const openCompletion = open.then((value) => ({ kind: "open-complete", value }), (error) => ({ kind: "open-error", error }));
|
|
52
|
+
try {
|
|
53
|
+
while (true) {
|
|
54
|
+
const outcome = await Promise.race([
|
|
55
|
+
reader.read().then((result) => ({ kind: "source", result }), (error) => ({ kind: "source-error", error })),
|
|
56
|
+
openCompletion,
|
|
57
|
+
]);
|
|
58
|
+
if (outcome.kind === "open-error")
|
|
59
|
+
throw outcome.error;
|
|
60
|
+
if (outcome.kind === "open-complete") {
|
|
61
|
+
// Success cannot normally precede EOF, but if a binding completes early,
|
|
62
|
+
// stop consuming the now-unneeded producer without reporting an error.
|
|
63
|
+
await reader.cancel().catch(() => undefined);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (outcome.kind === "source-error") {
|
|
67
|
+
producerError = true;
|
|
68
|
+
throw outcome.error;
|
|
69
|
+
}
|
|
70
|
+
const read = outcome.result;
|
|
71
|
+
if (read.done)
|
|
72
|
+
break;
|
|
73
|
+
if (!(read.value instanceof Uint8Array)) {
|
|
74
|
+
producerError = true;
|
|
75
|
+
throw new TypeError("snapshot stream chunks must be Uint8Array values");
|
|
76
|
+
}
|
|
77
|
+
for (const chunk of ownedSnapshotRestoreChunks(read.value)) {
|
|
78
|
+
await client.request({
|
|
79
|
+
kind: "openSnapshot.write",
|
|
80
|
+
snapshotId,
|
|
81
|
+
chunk,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
await client.request({ kind: "openSnapshot.finish", snapshotId });
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
await reader.cancel(error).catch(() => undefined);
|
|
89
|
+
client.notify({ kind: "openSnapshot.cancel", snapshotId });
|
|
90
|
+
try {
|
|
91
|
+
await open;
|
|
92
|
+
}
|
|
93
|
+
catch (openError) {
|
|
94
|
+
// Decoder errors are more useful than the restore-lane write/close
|
|
95
|
+
// rejection. Producer errors remain authoritative.
|
|
96
|
+
if (!producerError)
|
|
97
|
+
throw openError;
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
reader.releaseLock();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
26
105
|
/** Opens the local worker transport behind the semantic Lix binding. */
|
|
27
|
-
export async function openLixWorkerBinding(storage, onDisposed, telemetry) {
|
|
106
|
+
export async function openLixWorkerBinding(storage, onDisposed, telemetry, server, onProgress, snapshot) {
|
|
28
107
|
if (openDirectLixBinding) {
|
|
29
108
|
const telemetryDispatch = telemetry
|
|
30
109
|
? (span) => {
|
|
@@ -36,47 +115,149 @@ export async function openLixWorkerBinding(storage, onDisposed, telemetry) {
|
|
|
36
115
|
}
|
|
37
116
|
}
|
|
38
117
|
: undefined;
|
|
39
|
-
const binding = await openDirectLixBinding(storage, telemetryDispatch);
|
|
118
|
+
const binding = await openDirectLixBinding(storage, telemetryDispatch, telemetry?.parentContext?.(), await resolveDirectSyncServer(server), onProgress, snapshot);
|
|
40
119
|
if (binding) {
|
|
120
|
+
const operationAwareBinding = telemetry?.parentContext
|
|
121
|
+
? wrapTelemetryParentBinding(binding, telemetry.parentContext)
|
|
122
|
+
: binding;
|
|
41
123
|
if (!onDisposed)
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
return new Proxy(binding, {
|
|
45
|
-
get(target, property, receiver) {
|
|
46
|
-
if (property === "close") {
|
|
47
|
-
return async () => {
|
|
48
|
-
try {
|
|
49
|
-
await target.close();
|
|
50
|
-
}
|
|
51
|
-
finally {
|
|
52
|
-
if (!disposed) {
|
|
53
|
-
disposed = true;
|
|
54
|
-
onDisposed();
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
const value = Reflect.get(target, property, receiver);
|
|
60
|
-
return typeof value === "function" ? value.bind(target) : value;
|
|
61
|
-
}
|
|
62
|
-
});
|
|
124
|
+
return operationAwareBinding;
|
|
125
|
+
return wrapDirectBinding(operationAwareBinding, new BindingLease(onDisposed));
|
|
63
126
|
}
|
|
64
127
|
}
|
|
65
|
-
const client = await openLixWorker(storage, onDisposed, telemetry);
|
|
66
|
-
return workerBinding(client);
|
|
128
|
+
const client = await openLixWorker(storage, onDisposed, telemetry, server, onProgress, snapshot);
|
|
129
|
+
return workerBinding(client, new BindingLease(() => releaseWorker(client)), 0);
|
|
67
130
|
}
|
|
68
|
-
function
|
|
131
|
+
function wrapTelemetryParentBinding(binding, parentContext) {
|
|
132
|
+
const prepareOperation = () => binding.setTelemetryParent(parentContext());
|
|
133
|
+
return new Proxy(binding, {
|
|
134
|
+
get(target, property, receiver) {
|
|
135
|
+
if (property === "setTelemetryParent") {
|
|
136
|
+
return target.setTelemetryParent.bind(target);
|
|
137
|
+
}
|
|
138
|
+
if (property === "openAnotherSession") {
|
|
139
|
+
return async (options) => {
|
|
140
|
+
prepareOperation();
|
|
141
|
+
return wrapTelemetryParentBinding(await target.openAnotherSession(options), parentContext);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (property === "observe") {
|
|
145
|
+
return async (sql, params) => {
|
|
146
|
+
prepareOperation();
|
|
147
|
+
return wrapTelemetryParentObserve(await target.observe(sql, params), parentContext);
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (property === "beginTransaction") {
|
|
151
|
+
return async () => {
|
|
152
|
+
prepareOperation();
|
|
153
|
+
return wrapTelemetryParentTransaction(await target.beginTransaction(), target, parentContext);
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const value = Reflect.get(target, property, receiver);
|
|
157
|
+
if (typeof value !== "function")
|
|
158
|
+
return value;
|
|
159
|
+
return (...args) => {
|
|
160
|
+
prepareOperation();
|
|
161
|
+
return Reflect.apply(value, target, args);
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function wrapTelemetryParentObserve(events, parentContext) {
|
|
167
|
+
return {
|
|
168
|
+
setTelemetryParent: (parent) => events.setTelemetryParent(parent),
|
|
169
|
+
next: () => {
|
|
170
|
+
events.setTelemetryParent(parentContext());
|
|
171
|
+
return events.next();
|
|
172
|
+
},
|
|
173
|
+
close: () => events.close(),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function wrapTelemetryParentTransaction(transaction, binding, parentContext) {
|
|
177
|
+
const prepareOperation = () => binding.setTelemetryParent(parentContext());
|
|
178
|
+
return {
|
|
179
|
+
execute: (sql, params, options) => {
|
|
180
|
+
prepareOperation();
|
|
181
|
+
return transaction.execute(sql, params, options);
|
|
182
|
+
},
|
|
183
|
+
commit: () => {
|
|
184
|
+
prepareOperation();
|
|
185
|
+
return transaction.commit();
|
|
186
|
+
},
|
|
187
|
+
rollback: () => {
|
|
188
|
+
prepareOperation();
|
|
189
|
+
return transaction.rollback();
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** @internal Exported only for worker lifecycle tests. */
|
|
194
|
+
export class BindingLease {
|
|
195
|
+
releaseLast;
|
|
196
|
+
references = 1;
|
|
197
|
+
constructor(releaseLast) {
|
|
198
|
+
this.releaseLast = releaseLast;
|
|
199
|
+
}
|
|
200
|
+
retain() {
|
|
201
|
+
this.references += 1;
|
|
202
|
+
}
|
|
203
|
+
async release() {
|
|
204
|
+
this.references -= 1;
|
|
205
|
+
if (this.references === 0)
|
|
206
|
+
await this.releaseLast();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function wrapDirectBinding(binding, lease) {
|
|
210
|
+
let closed = false;
|
|
211
|
+
return new Proxy(binding, {
|
|
212
|
+
get(target, property, receiver) {
|
|
213
|
+
if (property === "openAnotherSession") {
|
|
214
|
+
return async (options) => {
|
|
215
|
+
const opened = await target.openAnotherSession(options);
|
|
216
|
+
lease.retain();
|
|
217
|
+
return wrapDirectBinding(opened, lease);
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (property === "close") {
|
|
221
|
+
return async () => {
|
|
222
|
+
if (closed)
|
|
223
|
+
return;
|
|
224
|
+
try {
|
|
225
|
+
await target.close();
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
closed = true;
|
|
229
|
+
await lease.release();
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const value = Reflect.get(target, property, receiver);
|
|
234
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/** @internal Exported only for worker lifecycle tests. */
|
|
239
|
+
export function workerBinding(client, lease, sessionId) {
|
|
69
240
|
let closed = false;
|
|
70
241
|
const request = (operation) => {
|
|
71
242
|
if (closed)
|
|
72
243
|
return Promise.reject(workerClosedError());
|
|
73
|
-
return client.request(operation);
|
|
244
|
+
return client.request(operation, sessionId);
|
|
74
245
|
};
|
|
75
246
|
const notify = (notification) => {
|
|
76
247
|
if (!closed)
|
|
77
248
|
client.notify(notification);
|
|
78
249
|
};
|
|
79
250
|
return {
|
|
251
|
+
openReport: () => client.openReport,
|
|
252
|
+
setTelemetryParent: () => { },
|
|
253
|
+
openAnotherSession: async (options) => {
|
|
254
|
+
const openedSessionId = await request({
|
|
255
|
+
kind: "openAnotherSession",
|
|
256
|
+
options,
|
|
257
|
+
});
|
|
258
|
+
lease.retain();
|
|
259
|
+
return workerBinding(client, lease, openedSessionId);
|
|
260
|
+
},
|
|
80
261
|
execute: (sql, params, options) => request({ kind: "execute", sql, params, options }),
|
|
81
262
|
executeBatch: (statements, options) => request({ kind: "executeBatch", statements, options }),
|
|
82
263
|
observe: async (sql, params) => {
|
|
@@ -96,7 +277,6 @@ function workerBinding(client) {
|
|
|
96
277
|
activeBranchId: () => request({ kind: "activeBranchId" }),
|
|
97
278
|
activeAccountId: () => request({ kind: "activeAccountId" }),
|
|
98
279
|
createBranch: (options) => request({ kind: "createBranch", options }),
|
|
99
|
-
createCheckpoint: () => request({ kind: "createCheckpoint" }),
|
|
100
280
|
undo: () => request({ kind: "undo" }),
|
|
101
281
|
redo: () => request({ kind: "redo" }),
|
|
102
282
|
switchBranch: (options) => request({ kind: "switchBranch", options }),
|
|
@@ -104,12 +284,46 @@ function workerBinding(client) {
|
|
|
104
284
|
mergeBranchPreview: (options) => request({ kind: "mergeBranchPreview", options }),
|
|
105
285
|
mergeBranch: (options) => request({ kind: "mergeBranch", options }),
|
|
106
286
|
syncDiskToLix: () => request({ kind: "syncDiskToLix" }),
|
|
287
|
+
exportSnapshot: () => {
|
|
288
|
+
const exportId = request({ kind: "exportSnapshot" });
|
|
289
|
+
let canceled = false;
|
|
290
|
+
return {
|
|
291
|
+
next: async () => {
|
|
292
|
+
if (canceled)
|
|
293
|
+
return undefined;
|
|
294
|
+
return request({
|
|
295
|
+
kind: "exportSnapshot.next",
|
|
296
|
+
exportId: await exportId,
|
|
297
|
+
});
|
|
298
|
+
},
|
|
299
|
+
cancel: async () => {
|
|
300
|
+
if (canceled)
|
|
301
|
+
return;
|
|
302
|
+
canceled = true;
|
|
303
|
+
await request({
|
|
304
|
+
kind: "exportSnapshot.cancel",
|
|
305
|
+
exportId: await exportId,
|
|
306
|
+
});
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
},
|
|
107
310
|
close: async () => {
|
|
108
311
|
if (closed)
|
|
109
312
|
return;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
313
|
+
try {
|
|
314
|
+
await request({ kind: "close" });
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
// A failed semantic close may leave the worker host retaining its OPFS
|
|
318
|
+
// session. Terminating the worker is the ownership backstop before an
|
|
319
|
+
// outer caller can safely release its cross-tab lock.
|
|
320
|
+
await client.terminate().catch(() => undefined);
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
closed = true;
|
|
325
|
+
await lease.release();
|
|
326
|
+
}
|
|
113
327
|
},
|
|
114
328
|
};
|
|
115
329
|
}
|
|
@@ -128,6 +342,7 @@ function workerTransactionBinding(request, transactionId) {
|
|
|
128
342
|
}
|
|
129
343
|
function workerObserveBinding(request, notify, observeId) {
|
|
130
344
|
return {
|
|
345
|
+
setTelemetryParent: () => { },
|
|
131
346
|
next: () => request({ kind: "observe.next", observeId }),
|
|
132
347
|
close: () => notify({ kind: "observe.close", observeId }),
|
|
133
348
|
};
|
|
@@ -143,11 +358,17 @@ async function releaseWorker(client) {
|
|
|
143
358
|
export class LixWorkerClient {
|
|
144
359
|
connection;
|
|
145
360
|
nextRequestId = 1;
|
|
361
|
+
nextSnapshotInputId = 1;
|
|
146
362
|
pending = new Map();
|
|
147
363
|
disposed = false;
|
|
148
364
|
leased = false;
|
|
149
365
|
onDisposed;
|
|
150
366
|
telemetry;
|
|
367
|
+
syncServer;
|
|
368
|
+
onProgress;
|
|
369
|
+
openReport;
|
|
370
|
+
syncFetchControllers = new Map();
|
|
371
|
+
syncFetchStreams = new Map();
|
|
151
372
|
constructor(connection = createWorkerConnection()) {
|
|
152
373
|
this.connection = connection;
|
|
153
374
|
connection.onMessage((message) => this.handleMessage(message));
|
|
@@ -156,12 +377,17 @@ export class LixWorkerClient {
|
|
|
156
377
|
get isDisposed() {
|
|
157
378
|
return this.disposed;
|
|
158
379
|
}
|
|
159
|
-
|
|
380
|
+
allocateSnapshotInputId() {
|
|
381
|
+
return this.nextSnapshotInputId++;
|
|
382
|
+
}
|
|
383
|
+
beginLease(onDisposed, telemetry, syncServer, onProgress) {
|
|
160
384
|
if (this.disposed || this.leased)
|
|
161
385
|
throw workerClosedError();
|
|
162
386
|
this.leased = true;
|
|
163
387
|
this.onDisposed = onDisposed;
|
|
164
388
|
this.telemetry = telemetry;
|
|
389
|
+
this.syncServer = syncServer;
|
|
390
|
+
this.onProgress = onProgress;
|
|
165
391
|
}
|
|
166
392
|
endLease() {
|
|
167
393
|
if (!this.leased)
|
|
@@ -170,9 +396,19 @@ export class LixWorkerClient {
|
|
|
170
396
|
const onDisposed = this.onDisposed;
|
|
171
397
|
this.onDisposed = undefined;
|
|
172
398
|
this.telemetry = undefined;
|
|
399
|
+
this.syncServer = undefined;
|
|
400
|
+
this.onProgress = undefined;
|
|
401
|
+
this.openReport = undefined;
|
|
402
|
+
for (const controller of this.syncFetchControllers.values())
|
|
403
|
+
controller.abort();
|
|
404
|
+
this.syncFetchControllers.clear();
|
|
405
|
+
for (const reader of this.syncFetchStreams.values()) {
|
|
406
|
+
void reader?.cancel().catch(() => undefined);
|
|
407
|
+
}
|
|
408
|
+
this.syncFetchStreams.clear();
|
|
173
409
|
onDisposed?.();
|
|
174
410
|
}
|
|
175
|
-
request(operation) {
|
|
411
|
+
request(operation, sessionId = 0) {
|
|
176
412
|
if (this.disposed || !this.leased) {
|
|
177
413
|
return Promise.reject(workerClosedError());
|
|
178
414
|
}
|
|
@@ -185,7 +421,12 @@ export class LixWorkerClient {
|
|
|
185
421
|
reject,
|
|
186
422
|
});
|
|
187
423
|
try {
|
|
188
|
-
this.connection.postMessage({
|
|
424
|
+
this.connection.postMessage({
|
|
425
|
+
id,
|
|
426
|
+
sessionId,
|
|
427
|
+
telemetryParent: this.telemetry?.parentContext?.(),
|
|
428
|
+
operation,
|
|
429
|
+
});
|
|
189
430
|
}
|
|
190
431
|
catch (error) {
|
|
191
432
|
this.pending.delete(id);
|
|
@@ -219,12 +460,7 @@ export class LixWorkerClient {
|
|
|
219
460
|
}
|
|
220
461
|
handleMessage(message) {
|
|
221
462
|
if ("kind" in message) {
|
|
222
|
-
|
|
223
|
-
this.telemetry?.onSpan(message.span);
|
|
224
|
-
}
|
|
225
|
-
catch {
|
|
226
|
-
// Telemetry callbacks are isolated from Lix operation results.
|
|
227
|
-
}
|
|
463
|
+
this.handleWorkerEvent(message);
|
|
228
464
|
return;
|
|
229
465
|
}
|
|
230
466
|
const pending = this.pending.get(message.id);
|
|
@@ -238,6 +474,185 @@ export class LixWorkerClient {
|
|
|
238
474
|
else
|
|
239
475
|
pending.reject(deserializeWorkerError(message.error));
|
|
240
476
|
}
|
|
477
|
+
handleWorkerEvent(message) {
|
|
478
|
+
switch (message.kind) {
|
|
479
|
+
case "telemetry":
|
|
480
|
+
try {
|
|
481
|
+
this.telemetry?.onSpan(message.span);
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// Telemetry callbacks are isolated from Lix operation results.
|
|
485
|
+
}
|
|
486
|
+
break;
|
|
487
|
+
case "open.progress":
|
|
488
|
+
try {
|
|
489
|
+
this.onProgress?.(message.progress);
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
// Open progress is observational and cannot fail repository opening.
|
|
493
|
+
}
|
|
494
|
+
break;
|
|
495
|
+
case "sync.headers":
|
|
496
|
+
void this.resolveSyncHeaders(message.requestId);
|
|
497
|
+
break;
|
|
498
|
+
case "sync.fetch":
|
|
499
|
+
void this.resolveSyncFetch(message.requestId, message.request);
|
|
500
|
+
break;
|
|
501
|
+
case "sync.fetch.stream.pull":
|
|
502
|
+
void this.resolveSyncFetchStreamPull(message.requestId);
|
|
503
|
+
break;
|
|
504
|
+
case "sync.fetch.cancel":
|
|
505
|
+
this.cancelSyncFetch(message.requestId);
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async resolveSyncHeaders(requestId) {
|
|
510
|
+
try {
|
|
511
|
+
const source = this.syncServer?.headers;
|
|
512
|
+
const headers = typeof source === "function" ? await source() : source;
|
|
513
|
+
this.notify({
|
|
514
|
+
kind: "sync.headers.result",
|
|
515
|
+
requestId,
|
|
516
|
+
result: { ok: true, headers: headerEntries(headers) },
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
this.notify({
|
|
521
|
+
kind: "sync.headers.result",
|
|
522
|
+
requestId,
|
|
523
|
+
result: { ok: false, error: serializeWorkerError(error) },
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async resolveSyncFetch(requestId, request) {
|
|
528
|
+
const fetcher = this.syncServer?.fetch;
|
|
529
|
+
if (!fetcher) {
|
|
530
|
+
this.notify({
|
|
531
|
+
kind: "sync.fetch.result",
|
|
532
|
+
requestId,
|
|
533
|
+
result: {
|
|
534
|
+
ok: false,
|
|
535
|
+
error: serializeWorkerError(new Error("Sync fetch bridge is unavailable")),
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const controller = new AbortController();
|
|
541
|
+
this.syncFetchControllers.set(requestId, controller);
|
|
542
|
+
let retainedStream = false;
|
|
543
|
+
try {
|
|
544
|
+
const response = await fetcher(request.url, {
|
|
545
|
+
method: request.method,
|
|
546
|
+
headers: request.headers,
|
|
547
|
+
body: typeof request.body === "string"
|
|
548
|
+
? request.body
|
|
549
|
+
: request.body?.slice().buffer,
|
|
550
|
+
credentials: request.credentials,
|
|
551
|
+
signal: controller.signal,
|
|
552
|
+
});
|
|
553
|
+
if (this.syncFetchControllers.get(requestId) !== controller ||
|
|
554
|
+
controller.signal.aborted) {
|
|
555
|
+
await response.body?.cancel().catch(() => undefined);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (request.responseMode === "stream") {
|
|
559
|
+
this.syncFetchStreams.set(requestId, response.body?.getReader());
|
|
560
|
+
retainedStream = true;
|
|
561
|
+
this.notify({
|
|
562
|
+
kind: "sync.fetch.result",
|
|
563
|
+
requestId,
|
|
564
|
+
result: {
|
|
565
|
+
ok: true,
|
|
566
|
+
response: {
|
|
567
|
+
status: response.status,
|
|
568
|
+
statusText: response.statusText,
|
|
569
|
+
headers: headerEntries(response.headers),
|
|
570
|
+
streaming: true,
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const body = await readSyncResponseBody(response, request.responseLimit, controller);
|
|
577
|
+
this.notify({
|
|
578
|
+
kind: "sync.fetch.result",
|
|
579
|
+
requestId,
|
|
580
|
+
result: {
|
|
581
|
+
ok: true,
|
|
582
|
+
response: {
|
|
583
|
+
status: response.status,
|
|
584
|
+
statusText: response.statusText,
|
|
585
|
+
headers: headerEntries(response.headers),
|
|
586
|
+
body,
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
if (!controller.signal.aborted || isSyncResponseTooLarge(error)) {
|
|
593
|
+
this.notify({
|
|
594
|
+
kind: "sync.fetch.result",
|
|
595
|
+
requestId,
|
|
596
|
+
result: { ok: false, error: serializeWorkerError(error) },
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
finally {
|
|
601
|
+
if (!retainedStream)
|
|
602
|
+
this.syncFetchControllers.delete(requestId);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async resolveSyncFetchStreamPull(requestId) {
|
|
606
|
+
if (!this.syncFetchStreams.has(requestId))
|
|
607
|
+
return;
|
|
608
|
+
const reader = this.syncFetchStreams.get(requestId);
|
|
609
|
+
try {
|
|
610
|
+
const result = reader ? await reader.read() : { done: true };
|
|
611
|
+
if (!this.syncFetchStreams.has(requestId) ||
|
|
612
|
+
this.syncFetchStreams.get(requestId) !== reader ||
|
|
613
|
+
this.syncFetchControllers.get(requestId)?.signal.aborted) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
this.notify({
|
|
617
|
+
kind: "sync.fetch.stream.result",
|
|
618
|
+
requestId,
|
|
619
|
+
result: result.done
|
|
620
|
+
? { ok: true, done: true }
|
|
621
|
+
: { ok: true, done: false, chunk: result.value.slice() },
|
|
622
|
+
});
|
|
623
|
+
if (result.done) {
|
|
624
|
+
reader?.releaseLock();
|
|
625
|
+
this.finishSyncFetchStream(requestId);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
const controller = this.syncFetchControllers.get(requestId);
|
|
630
|
+
if (!controller?.signal.aborted) {
|
|
631
|
+
this.notify({
|
|
632
|
+
kind: "sync.fetch.stream.result",
|
|
633
|
+
requestId,
|
|
634
|
+
result: { ok: false, error: serializeWorkerError(error) },
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
reader?.releaseLock();
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
// A cancellation can leave the read pending until its rejection settles.
|
|
642
|
+
}
|
|
643
|
+
this.finishSyncFetchStream(requestId);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
cancelSyncFetch(requestId) {
|
|
647
|
+
this.syncFetchControllers.get(requestId)?.abort();
|
|
648
|
+
const reader = this.syncFetchStreams.get(requestId);
|
|
649
|
+
void reader?.cancel().catch(() => undefined);
|
|
650
|
+
this.finishSyncFetchStream(requestId);
|
|
651
|
+
}
|
|
652
|
+
finishSyncFetchStream(requestId) {
|
|
653
|
+
this.syncFetchStreams.delete(requestId);
|
|
654
|
+
this.syncFetchControllers.delete(requestId);
|
|
655
|
+
}
|
|
241
656
|
handleFatal(error) {
|
|
242
657
|
if (this.disposed)
|
|
243
658
|
return;
|
|
@@ -255,9 +670,90 @@ export class LixWorkerClient {
|
|
|
255
670
|
this.connection.unref();
|
|
256
671
|
}
|
|
257
672
|
}
|
|
673
|
+
async function readSyncResponseBody(response, limit, controller) {
|
|
674
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
675
|
+
throw new TypeError("Browser sync fetch has no valid response limit");
|
|
676
|
+
}
|
|
677
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
678
|
+
if (Number.isFinite(declaredLength) && declaredLength > limit) {
|
|
679
|
+
const error = syncResponseTooLarge(limit);
|
|
680
|
+
controller.abort(error);
|
|
681
|
+
throw error;
|
|
682
|
+
}
|
|
683
|
+
const stream = response.body;
|
|
684
|
+
if (!stream)
|
|
685
|
+
return new Uint8Array();
|
|
686
|
+
const reader = stream.getReader();
|
|
687
|
+
const chunks = [];
|
|
688
|
+
let total = 0;
|
|
689
|
+
try {
|
|
690
|
+
while (true) {
|
|
691
|
+
const { done, value } = await reader.read();
|
|
692
|
+
if (done)
|
|
693
|
+
break;
|
|
694
|
+
total += value.byteLength;
|
|
695
|
+
if (total > limit) {
|
|
696
|
+
const error = syncResponseTooLarge(limit);
|
|
697
|
+
controller.abort(error);
|
|
698
|
+
await reader.cancel(error).catch(() => undefined);
|
|
699
|
+
throw error;
|
|
700
|
+
}
|
|
701
|
+
chunks.push(value);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
finally {
|
|
705
|
+
reader.releaseLock();
|
|
706
|
+
}
|
|
707
|
+
const body = new Uint8Array(total);
|
|
708
|
+
let offset = 0;
|
|
709
|
+
for (const chunk of chunks) {
|
|
710
|
+
body.set(chunk, offset);
|
|
711
|
+
offset += chunk.byteLength;
|
|
712
|
+
}
|
|
713
|
+
return body;
|
|
714
|
+
}
|
|
715
|
+
function syncResponseTooLarge(limit) {
|
|
716
|
+
const error = new Error(`sync fetch response exceeds ${limit} bytes`);
|
|
717
|
+
error.name = "LixError";
|
|
718
|
+
error.code = "LIX_ERROR_SYNC_RESPONSE_TOO_LARGE";
|
|
719
|
+
return error;
|
|
720
|
+
}
|
|
721
|
+
function isSyncResponseTooLarge(error) {
|
|
722
|
+
return (error instanceof Error &&
|
|
723
|
+
error.code ===
|
|
724
|
+
"LIX_ERROR_SYNC_RESPONSE_TOO_LARGE");
|
|
725
|
+
}
|
|
258
726
|
function workerClosedError() {
|
|
259
727
|
const error = new Error("Lix worker is closed");
|
|
260
728
|
error.name = "LixError";
|
|
261
729
|
error.code = "LIX_ERROR_CLOSED";
|
|
262
730
|
return error;
|
|
263
731
|
}
|
|
732
|
+
function serializeSyncServer(server) {
|
|
733
|
+
if (!server)
|
|
734
|
+
return undefined;
|
|
735
|
+
return {
|
|
736
|
+
url: new URL(server.url).toString(),
|
|
737
|
+
headers: typeof server.headers === "function"
|
|
738
|
+
? undefined
|
|
739
|
+
: headerEntries(server.headers),
|
|
740
|
+
dynamicHeaders: typeof server.headers === "function",
|
|
741
|
+
customFetch: server.fetch !== undefined,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function headerEntries(headers) {
|
|
745
|
+
const entries = [];
|
|
746
|
+
new Headers(headers).forEach((value, name) => entries.push([name, value]));
|
|
747
|
+
return entries;
|
|
748
|
+
}
|
|
749
|
+
async function resolveDirectSyncServer(server) {
|
|
750
|
+
if (!server)
|
|
751
|
+
return undefined;
|
|
752
|
+
const source = server.headers;
|
|
753
|
+
const headers = typeof source === "function" ? await source() : source;
|
|
754
|
+
return {
|
|
755
|
+
url: new URL(server.url).toString(),
|
|
756
|
+
headers: headerEntries(headers),
|
|
757
|
+
fetch: server.fetch,
|
|
758
|
+
};
|
|
759
|
+
}
|