@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.
Files changed (50) hide show
  1. package/README.md +54 -10
  2. package/dist/binding-types.d.ts +37 -9
  3. package/dist/binding.browser.d.ts +2 -2
  4. package/dist/binding.browser.js +20 -13
  5. package/dist/binding.node-wasm.d.ts +2 -2
  6. package/dist/binding.node-wasm.js +9 -11
  7. package/dist/binding.node.d.ts +3 -3
  8. package/dist/binding.node.js +70 -13
  9. package/dist/bundled-plugins/plugin_csv.lixplugin +0 -0
  10. package/dist/bundled-plugins/plugin_markdown.lixplugin +0 -0
  11. package/dist/index.d.ts +4 -4
  12. package/dist/index.js +2 -2
  13. package/dist/lix.d.ts +27 -5
  14. package/dist/lix.js +125 -6
  15. package/dist/open-lix.d.ts +4 -5
  16. package/dist/open-lix.js +174 -33
  17. package/dist/remote/client.d.ts +1 -2
  18. package/dist/remote/client.js +40 -1151
  19. package/dist/remote/server-protocol.d.ts +5 -6
  20. package/dist/remote/server-protocol.js +40 -6
  21. package/dist/result.d.ts +6 -13
  22. package/dist/result.js +9 -35
  23. package/dist/snapshot-restore.d.ts +6 -0
  24. package/dist/snapshot-restore.js +101 -0
  25. package/dist/storage-adapter.d.ts +175 -19
  26. package/dist/storage-adapter.js +21 -0
  27. package/dist/types.d.ts +104 -34
  28. package/dist/value.d.ts +1 -0
  29. package/dist/value.js +8 -0
  30. package/dist/wasm/lix_js_sdk.d.ts +121 -24
  31. package/dist/wasm/lix_js_sdk.js +606 -78
  32. package/dist/wasm/lix_js_sdk_bg.wasm +0 -0
  33. package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +48 -6
  34. package/dist/wasm-init.d.ts +2 -0
  35. package/dist/wasm-init.js +23 -0
  36. package/dist/worker/client.d.ts +31 -5
  37. package/dist/worker/client.js +541 -45
  38. package/dist/worker/factory.browser.d.ts +2 -2
  39. package/dist/worker/factory.node.d.ts +2 -2
  40. package/dist/worker/factory.node.js +3 -10
  41. package/dist/worker/host.d.ts +4 -2
  42. package/dist/worker/host.js +401 -43
  43. package/dist/worker/protocol.d.ts +107 -3
  44. package/package.json +6 -10
  45. package/dist/indexeddb-backend.d.ts +0 -19
  46. package/dist/indexeddb-backend.js +0 -121
  47. package/dist/remote/sse.d.ts +0 -12
  48. package/dist/remote/sse.js +0 -87
  49. package/dist/workerd.d.ts +0 -25
  50. package/dist/workerd.js +0 -35
@@ -1,30 +1,109 @@
1
1
  import { createWorkerConnection, openDirectLixBinding } from "#worker-factory";
2
- import { deserializeWorkerError, } from "./protocol.js";
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
- await client.request({
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 binding;
43
- let disposed = false;
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 workerBinding(client) {
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
- await request({ kind: "close" });
111
- closed = true;
112
- await releaseWorker(client);
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
- beginLease(onDisposed, telemetry) {
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({ id, operation });
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
- try {
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
+ }