@lix-js/sdk 0.12.3 → 0.14.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 +40 -10
- package/dist/binding-types.d.ts +37 -9
- package/dist/binding.browser.d.ts +2 -2
- package/dist/binding.browser.js +25 -10
- package/dist/binding.node-wasm.d.ts +2 -2
- package/dist/binding.node-wasm.js +8 -4
- package/dist/binding.node.d.ts +3 -3
- package/dist/binding.node.js +70 -13
- package/dist/browser-wasm-init.d.ts +14 -0
- package/dist/browser-wasm-init.js +16 -0
- 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 +132 -6
- package/dist/open-lix.d.ts +4 -5
- package/dist/open-lix.js +173 -33
- package/dist/remote/client.d.ts +1 -2
- package/dist/remote/client.js +58 -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 +101 -31
- package/dist/value.d.ts +1 -0
- package/dist/value.js +8 -0
- package/dist/wasm/lix_js_sdk.d.ts +119 -24
- package/dist/wasm/lix_js_sdk.js +599 -78
- package/dist/wasm/lix_js_sdk_bg.wasm +0 -0
- package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +47 -6
- package/dist/worker/client.d.ts +27 -5
- package/dist/worker/client.js +458 -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 +2 -1
- package/dist/worker/host.js +283 -37
- package/dist/worker/protocol.d.ts +80 -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);
|
|
130
|
+
}
|
|
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
|
+
});
|
|
67
237
|
}
|
|
68
|
-
|
|
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,16 @@ 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();
|
|
151
371
|
constructor(connection = createWorkerConnection()) {
|
|
152
372
|
this.connection = connection;
|
|
153
373
|
connection.onMessage((message) => this.handleMessage(message));
|
|
@@ -156,12 +376,17 @@ export class LixWorkerClient {
|
|
|
156
376
|
get isDisposed() {
|
|
157
377
|
return this.disposed;
|
|
158
378
|
}
|
|
159
|
-
|
|
379
|
+
allocateSnapshotInputId() {
|
|
380
|
+
return this.nextSnapshotInputId++;
|
|
381
|
+
}
|
|
382
|
+
beginLease(onDisposed, telemetry, syncServer, onProgress) {
|
|
160
383
|
if (this.disposed || this.leased)
|
|
161
384
|
throw workerClosedError();
|
|
162
385
|
this.leased = true;
|
|
163
386
|
this.onDisposed = onDisposed;
|
|
164
387
|
this.telemetry = telemetry;
|
|
388
|
+
this.syncServer = syncServer;
|
|
389
|
+
this.onProgress = onProgress;
|
|
165
390
|
}
|
|
166
391
|
endLease() {
|
|
167
392
|
if (!this.leased)
|
|
@@ -170,9 +395,15 @@ export class LixWorkerClient {
|
|
|
170
395
|
const onDisposed = this.onDisposed;
|
|
171
396
|
this.onDisposed = undefined;
|
|
172
397
|
this.telemetry = undefined;
|
|
398
|
+
this.syncServer = undefined;
|
|
399
|
+
this.onProgress = undefined;
|
|
400
|
+
this.openReport = undefined;
|
|
401
|
+
for (const controller of this.syncFetchControllers.values())
|
|
402
|
+
controller.abort();
|
|
403
|
+
this.syncFetchControllers.clear();
|
|
173
404
|
onDisposed?.();
|
|
174
405
|
}
|
|
175
|
-
request(operation) {
|
|
406
|
+
request(operation, sessionId = 0) {
|
|
176
407
|
if (this.disposed || !this.leased) {
|
|
177
408
|
return Promise.reject(workerClosedError());
|
|
178
409
|
}
|
|
@@ -185,7 +416,12 @@ export class LixWorkerClient {
|
|
|
185
416
|
reject,
|
|
186
417
|
});
|
|
187
418
|
try {
|
|
188
|
-
this.connection.postMessage({
|
|
419
|
+
this.connection.postMessage({
|
|
420
|
+
id,
|
|
421
|
+
sessionId,
|
|
422
|
+
telemetryParent: this.telemetry?.parentContext?.(),
|
|
423
|
+
operation,
|
|
424
|
+
});
|
|
189
425
|
}
|
|
190
426
|
catch (error) {
|
|
191
427
|
this.pending.delete(id);
|
|
@@ -219,12 +455,7 @@ export class LixWorkerClient {
|
|
|
219
455
|
}
|
|
220
456
|
handleMessage(message) {
|
|
221
457
|
if ("kind" in message) {
|
|
222
|
-
|
|
223
|
-
this.telemetry?.onSpan(message.span);
|
|
224
|
-
}
|
|
225
|
-
catch {
|
|
226
|
-
// Telemetry callbacks are isolated from Lix operation results.
|
|
227
|
-
}
|
|
458
|
+
this.handleWorkerEvent(message);
|
|
228
459
|
return;
|
|
229
460
|
}
|
|
230
461
|
const pending = this.pending.get(message.id);
|
|
@@ -238,6 +469,107 @@ export class LixWorkerClient {
|
|
|
238
469
|
else
|
|
239
470
|
pending.reject(deserializeWorkerError(message.error));
|
|
240
471
|
}
|
|
472
|
+
handleWorkerEvent(message) {
|
|
473
|
+
switch (message.kind) {
|
|
474
|
+
case "telemetry":
|
|
475
|
+
try {
|
|
476
|
+
this.telemetry?.onSpan(message.span);
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
// Telemetry callbacks are isolated from Lix operation results.
|
|
480
|
+
}
|
|
481
|
+
break;
|
|
482
|
+
case "open.progress":
|
|
483
|
+
try {
|
|
484
|
+
this.onProgress?.(message.progress);
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
// Open progress is observational and cannot fail repository opening.
|
|
488
|
+
}
|
|
489
|
+
break;
|
|
490
|
+
case "sync.headers":
|
|
491
|
+
void this.resolveSyncHeaders(message.requestId);
|
|
492
|
+
break;
|
|
493
|
+
case "sync.fetch":
|
|
494
|
+
void this.resolveSyncFetch(message.requestId, message.request);
|
|
495
|
+
break;
|
|
496
|
+
case "sync.fetch.cancel":
|
|
497
|
+
this.syncFetchControllers.get(message.requestId)?.abort();
|
|
498
|
+
this.syncFetchControllers.delete(message.requestId);
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async resolveSyncHeaders(requestId) {
|
|
503
|
+
try {
|
|
504
|
+
const source = this.syncServer?.headers;
|
|
505
|
+
const headers = typeof source === "function" ? await source() : source;
|
|
506
|
+
this.notify({
|
|
507
|
+
kind: "sync.headers.result",
|
|
508
|
+
requestId,
|
|
509
|
+
result: { ok: true, headers: headerEntries(headers) },
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
this.notify({
|
|
514
|
+
kind: "sync.headers.result",
|
|
515
|
+
requestId,
|
|
516
|
+
result: { ok: false, error: serializeWorkerError(error) },
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async resolveSyncFetch(requestId, request) {
|
|
521
|
+
const fetcher = this.syncServer?.fetch;
|
|
522
|
+
if (!fetcher) {
|
|
523
|
+
this.notify({
|
|
524
|
+
kind: "sync.fetch.result",
|
|
525
|
+
requestId,
|
|
526
|
+
result: {
|
|
527
|
+
ok: false,
|
|
528
|
+
error: serializeWorkerError(new Error("Sync fetch bridge is unavailable")),
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const controller = new AbortController();
|
|
534
|
+
this.syncFetchControllers.set(requestId, controller);
|
|
535
|
+
try {
|
|
536
|
+
const response = await fetcher(request.url, {
|
|
537
|
+
method: request.method,
|
|
538
|
+
headers: request.headers,
|
|
539
|
+
body: typeof request.body === "string"
|
|
540
|
+
? request.body
|
|
541
|
+
: request.body?.slice().buffer,
|
|
542
|
+
credentials: request.credentials,
|
|
543
|
+
signal: controller.signal,
|
|
544
|
+
});
|
|
545
|
+
const body = await readSyncResponseBody(response, request.responseLimit, controller);
|
|
546
|
+
this.notify({
|
|
547
|
+
kind: "sync.fetch.result",
|
|
548
|
+
requestId,
|
|
549
|
+
result: {
|
|
550
|
+
ok: true,
|
|
551
|
+
response: {
|
|
552
|
+
status: response.status,
|
|
553
|
+
statusText: response.statusText,
|
|
554
|
+
headers: headerEntries(response.headers),
|
|
555
|
+
body,
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
if (!controller.signal.aborted || isSyncResponseTooLarge(error)) {
|
|
562
|
+
this.notify({
|
|
563
|
+
kind: "sync.fetch.result",
|
|
564
|
+
requestId,
|
|
565
|
+
result: { ok: false, error: serializeWorkerError(error) },
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
finally {
|
|
570
|
+
this.syncFetchControllers.delete(requestId);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
241
573
|
handleFatal(error) {
|
|
242
574
|
if (this.disposed)
|
|
243
575
|
return;
|
|
@@ -255,9 +587,90 @@ export class LixWorkerClient {
|
|
|
255
587
|
this.connection.unref();
|
|
256
588
|
}
|
|
257
589
|
}
|
|
590
|
+
async function readSyncResponseBody(response, limit, controller) {
|
|
591
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
592
|
+
throw new TypeError("Browser sync fetch has no valid response limit");
|
|
593
|
+
}
|
|
594
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
595
|
+
if (Number.isFinite(declaredLength) && declaredLength > limit) {
|
|
596
|
+
const error = syncResponseTooLarge(limit);
|
|
597
|
+
controller.abort(error);
|
|
598
|
+
throw error;
|
|
599
|
+
}
|
|
600
|
+
const stream = response.body;
|
|
601
|
+
if (!stream)
|
|
602
|
+
return new Uint8Array();
|
|
603
|
+
const reader = stream.getReader();
|
|
604
|
+
const chunks = [];
|
|
605
|
+
let total = 0;
|
|
606
|
+
try {
|
|
607
|
+
while (true) {
|
|
608
|
+
const { done, value } = await reader.read();
|
|
609
|
+
if (done)
|
|
610
|
+
break;
|
|
611
|
+
total += value.byteLength;
|
|
612
|
+
if (total > limit) {
|
|
613
|
+
const error = syncResponseTooLarge(limit);
|
|
614
|
+
controller.abort(error);
|
|
615
|
+
await reader.cancel(error).catch(() => undefined);
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
618
|
+
chunks.push(value);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
finally {
|
|
622
|
+
reader.releaseLock();
|
|
623
|
+
}
|
|
624
|
+
const body = new Uint8Array(total);
|
|
625
|
+
let offset = 0;
|
|
626
|
+
for (const chunk of chunks) {
|
|
627
|
+
body.set(chunk, offset);
|
|
628
|
+
offset += chunk.byteLength;
|
|
629
|
+
}
|
|
630
|
+
return body;
|
|
631
|
+
}
|
|
632
|
+
function syncResponseTooLarge(limit) {
|
|
633
|
+
const error = new Error(`sync fetch response exceeds ${limit} bytes`);
|
|
634
|
+
error.name = "LixError";
|
|
635
|
+
error.code = "LIX_ERROR_SYNC_RESPONSE_TOO_LARGE";
|
|
636
|
+
return error;
|
|
637
|
+
}
|
|
638
|
+
function isSyncResponseTooLarge(error) {
|
|
639
|
+
return (error instanceof Error &&
|
|
640
|
+
error.code ===
|
|
641
|
+
"LIX_ERROR_SYNC_RESPONSE_TOO_LARGE");
|
|
642
|
+
}
|
|
258
643
|
function workerClosedError() {
|
|
259
644
|
const error = new Error("Lix worker is closed");
|
|
260
645
|
error.name = "LixError";
|
|
261
646
|
error.code = "LIX_ERROR_CLOSED";
|
|
262
647
|
return error;
|
|
263
648
|
}
|
|
649
|
+
function serializeSyncServer(server) {
|
|
650
|
+
if (!server)
|
|
651
|
+
return undefined;
|
|
652
|
+
return {
|
|
653
|
+
url: new URL(server.url).toString(),
|
|
654
|
+
headers: typeof server.headers === "function"
|
|
655
|
+
? undefined
|
|
656
|
+
: headerEntries(server.headers),
|
|
657
|
+
dynamicHeaders: typeof server.headers === "function",
|
|
658
|
+
customFetch: server.fetch !== undefined,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function headerEntries(headers) {
|
|
662
|
+
const entries = [];
|
|
663
|
+
new Headers(headers).forEach((value, name) => entries.push([name, value]));
|
|
664
|
+
return entries;
|
|
665
|
+
}
|
|
666
|
+
async function resolveDirectSyncServer(server) {
|
|
667
|
+
if (!server)
|
|
668
|
+
return undefined;
|
|
669
|
+
const source = server.headers;
|
|
670
|
+
const headers = typeof source === "function" ? await source() : source;
|
|
671
|
+
return {
|
|
672
|
+
url: new URL(server.url).toString(),
|
|
673
|
+
headers: headerEntries(headers),
|
|
674
|
+
fetch: server.fetch,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LixBinding, LixStorageConfig, TelemetryDispatch } from "../binding-types.js";
|
|
1
|
+
import type { LixBinding, LixStorageConfig, TelemetryDispatch, TelemetryParentContext, OpenProgressDispatch, SyncServerBindingOptions } from "../binding-types.js";
|
|
2
2
|
import type { WorkerConnection } from "./protocol.js";
|
|
3
|
-
export declare const openDirectLixBinding: undefined | ((storage: LixStorageConfig, telemetry?: TelemetryDispatch) => Promise<LixBinding>);
|
|
3
|
+
export declare const openDirectLixBinding: undefined | ((storage: LixStorageConfig, telemetry?: TelemetryDispatch, telemetryParent?: TelemetryParentContext, server?: SyncServerBindingOptions, openProgress?: OpenProgressDispatch, snapshot?: ReadableStream<Uint8Array>) => Promise<LixBinding>);
|
|
4
4
|
export declare function createWorkerConnection(): WorkerConnection;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { LixBinding, LixStorageConfig, TelemetryDispatch } from "../binding-types.js";
|
|
1
|
+
import type { LixBinding, LixStorageConfig, SyncServerBindingOptions, TelemetryDispatch, TelemetryParentContext, OpenProgressDispatch } from "../binding-types.js";
|
|
2
2
|
import type { WorkerConnection } from "./protocol.js";
|
|
3
3
|
export declare function createWorkerConnection(): WorkerConnection;
|
|
4
4
|
export declare function workerExecArgv(execArgv: readonly string[]): string[];
|
|
5
|
-
export declare const openDirectLixBinding: (storage: LixStorageConfig, telemetry?: TelemetryDispatch) => Promise<LixBinding | undefined>;
|
|
5
|
+
export declare const openDirectLixBinding: (storage: LixStorageConfig, telemetry?: TelemetryDispatch, telemetryParent?: TelemetryParentContext, server?: SyncServerBindingOptions, openProgress?: OpenProgressDispatch, snapshot?: ReadableStream<Uint8Array>) => Promise<LixBinding | undefined>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Worker } from "node:worker_threads";
|
|
2
|
-
import {
|
|
2
|
+
import { openLixBinding } from "../binding.node.js";
|
|
3
3
|
export function createWorkerConnection() {
|
|
4
4
|
const worker = new Worker(new URL("./entry.node.js", import.meta.url), {
|
|
5
5
|
name: "lix",
|
|
@@ -52,13 +52,6 @@ export function workerExecArgv(execArgv) {
|
|
|
52
52
|
/// Native Lix already owns a dedicated serialized engine actor. Routing it
|
|
53
53
|
/// through a second JavaScript worker adds two message-port hops per query
|
|
54
54
|
/// without adding isolation or concurrency.
|
|
55
|
-
export const openDirectLixBinding = async (storage, telemetry) => {
|
|
56
|
-
|
|
57
|
-
return await openNativeLixBinding(storage, telemetry);
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
if (storage.kind === "memory")
|
|
61
|
-
return undefined;
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
55
|
+
export const openDirectLixBinding = async (storage, telemetry, telemetryParent, server, openProgress, snapshot) => {
|
|
56
|
+
return openLixBinding(storage, telemetry, telemetryParent, server, openProgress, snapshot);
|
|
64
57
|
};
|
package/dist/worker/host.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import { type WorkerHostEndpoint } from "./protocol.js";
|
|
1
|
+
import { type WorkerHostEndpoint, type WorkerSyncFetchResponse } from "./protocol.js";
|
|
2
2
|
export declare function startWorkerHost(endpoint: WorkerHostEndpoint): void;
|
|
3
|
+
export declare function responseFromSyncFetch(resolved: WorkerSyncFetchResponse): Response;
|