@lix-js/sdk 0.16.1 → 0.17.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 (71) hide show
  1. package/README.md +162 -44
  2. package/dist/binding-types.d.ts +15 -5
  3. package/dist/binding.browser.d.ts +2 -0
  4. package/dist/binding.browser.js +14 -4
  5. package/dist/binding.node-wasm.d.ts +1 -1
  6. package/dist/binding.node-wasm.js +5 -3
  7. package/dist/binding.node.d.ts +2 -0
  8. package/dist/binding.node.js +34 -8
  9. package/dist/bundled-plugins/plugin_csv.lixplugin +0 -0
  10. package/dist/bundled-plugins/plugin_markdown.lixplugin +0 -0
  11. package/dist/compatibility.d.ts +6 -0
  12. package/dist/compatibility.js +6 -0
  13. package/dist/component-host/dispatch.d.ts +12 -0
  14. package/dist/component-host/dispatch.js +364 -0
  15. package/dist/component-host/index.d.ts +15 -0
  16. package/dist/component-host/index.js +84 -0
  17. package/dist/component-host/instrument.d.ts +6 -0
  18. package/dist/component-host/instrument.js +260 -0
  19. package/dist/conversion-provider.d.ts +4 -0
  20. package/dist/conversion-provider.js +20 -0
  21. package/dist/hosted-lix.js +1 -1
  22. package/dist/http-transport.d.ts +25 -0
  23. package/dist/http-transport.js +162 -0
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +1 -0
  26. package/dist/lix.d.ts +14 -9
  27. package/dist/lix.js +44 -9
  28. package/dist/migration-binding.browser.d.ts +33 -0
  29. package/dist/migration-binding.browser.js +46 -0
  30. package/dist/migration-binding.node.d.ts +6 -0
  31. package/dist/migration-binding.node.js +5 -0
  32. package/dist/migration-wasm/lix_js_sdk.d.ts +271 -0
  33. package/dist/migration-wasm/lix_js_sdk.js +1852 -0
  34. package/dist/migration-wasm/lix_js_sdk_bg.wasm +4 -0
  35. package/dist/migration-wasm/lix_js_sdk_bg.wasm.d.ts +101 -0
  36. package/dist/migration.d.ts +23 -0
  37. package/dist/migration.js +55 -0
  38. package/dist/open-lix.js +29 -18
  39. package/dist/open-progress.d.ts +10 -0
  40. package/dist/open-progress.js +59 -0
  41. package/dist/remote/client.d.ts +2 -1
  42. package/dist/remote/client.js +11 -1
  43. package/dist/result.d.ts +4 -3
  44. package/dist/result.js +3 -4
  45. package/dist/storage-adapter.d.ts +7 -1
  46. package/dist/storage-ownership.d.ts +5 -0
  47. package/dist/storage-ownership.js +4 -0
  48. package/dist/types.d.ts +65 -31
  49. package/dist/wasm/lix_js_sdk.d.ts +45 -16
  50. package/dist/wasm/lix_js_sdk.js +214 -60
  51. package/dist/wasm/lix_js_sdk_bg.wasm +2 -2
  52. package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +17 -9
  53. package/dist/worker/client.d.ts +12 -3
  54. package/dist/worker/client.js +95 -81
  55. package/dist/worker/durable-local-admission.d.ts +20 -0
  56. package/dist/worker/durable-local-admission.js +87 -0
  57. package/dist/worker/entry.shared.browser.d.ts +1 -0
  58. package/dist/worker/entry.shared.browser.js +211 -0
  59. package/dist/worker/factory.browser.d.ts +2 -0
  60. package/dist/worker/factory.browser.js +65 -1
  61. package/dist/worker/factory.node.d.ts +1 -0
  62. package/dist/worker/factory.node.js +3 -0
  63. package/dist/worker/host.d.ts +4 -2
  64. package/dist/worker/host.js +86 -32
  65. package/dist/worker/protocol.d.ts +25 -8
  66. package/dist/worker/protocol.js +25 -7
  67. package/dist/worker/shared-admission.d.ts +26 -0
  68. package/dist/worker/shared-admission.js +116 -0
  69. package/dist/worker/shared-engine.d.ts +34 -0
  70. package/dist/worker/shared-engine.js +194 -0
  71. package/package.json +21 -9
@@ -1,4 +1,7 @@
1
- import { createWorkerConnection, openDirectLixBinding } from "#worker-factory";
1
+ import { ADMISSION_PROTOCOL_EPOCH, ADMISSION_STORAGE_EPOCH } from "./shared-admission.js";
2
+ import { observeOpenProgress, emitOpenProgress } from "../open-progress.js";
3
+ import { fetchTransport } from "../http-transport.js";
4
+ import { createWorkerConnection, createSharedWorkerConnection, openDirectLixBinding } from "#worker-factory";
2
5
  import { ownedSnapshotRestoreChunks } from "../snapshot-restore.js";
3
6
  import { deserializeWorkerError, serializeWorkerError, } from "./protocol.js";
4
7
  const MAX_IDLE_WORKERS = 1;
@@ -6,7 +9,12 @@ const MAX_IDLE_WORKERS = 1;
6
9
  // survives close(). Concurrent opens still receive isolated workers.
7
10
  const idleWorkers = [];
8
11
  export async function openLixWorker(storage, onDisposed, telemetry, server, onProgress, snapshot) {
9
- let client = idleWorkers.pop();
12
+ const providerOptions = storage.kind === "jsStorage" ? storage.options : undefined;
13
+ const sharedKey = !snapshot && server && providerOptions && typeof providerOptions === "object"
14
+ && "sharedEngineKey" in providerOptions && typeof providerOptions.sharedEngineKey === "string"
15
+ && providerOptions.sharedEngineKey.startsWith("lix:opfs:") ? providerOptions.sharedEngineKey : undefined;
16
+ const sharedConnection = sharedKey ? createSharedWorkerConnection(`${sharedKey}:protocol-${ADMISSION_PROTOCOL_EPOCH}:storage-${ADMISSION_STORAGE_EPOCH}`) : undefined;
17
+ let client = sharedConnection ? new LixWorkerClient(sharedConnection, false) : idleWorkers.pop();
10
18
  while (client?.isDisposed)
11
19
  client = idleWorkers.pop();
12
20
  client ??= new LixWorkerClient();
@@ -104,6 +112,20 @@ export async function pumpSnapshotToWorker(client, reader, snapshotId, open) {
104
112
  }
105
113
  /** Opens the local worker transport behind the semantic Lix binding. */
106
114
  export async function openLixWorkerBinding(storage, onDisposed, telemetry, server, onProgress, snapshot) {
115
+ const progress = observeOpenProgress(onProgress);
116
+ const routed = server && onProgress ? { ...server,
117
+ transport: progress.transport(server.transport ?? fetchTransport(server.fetch)),
118
+ } : server;
119
+ try {
120
+ const binding = await openLixWorkerBindingInner(storage, onDisposed, telemetry, routed, onProgress ? value => emitOpenProgress(onProgress, value) : undefined, snapshot);
121
+ progress.complete();
122
+ return binding;
123
+ }
124
+ finally {
125
+ progress.stop();
126
+ }
127
+ }
128
+ async function openLixWorkerBindingInner(storage, onDisposed, telemetry, server, onProgress, snapshot) {
107
129
  if (openDirectLixBinding) {
108
130
  const telemetryDispatch = telemetry
109
131
  ? (span) => {
@@ -277,6 +299,8 @@ export function workerBinding(client, lease, sessionId) {
277
299
  replicaRecoverySources: () => request({ kind: "replicaRecoverySources" }),
278
300
  exportReplicaRecovery: (id) => request({ kind: "exportReplicaRecovery", id }),
279
301
  recoverReplica: (id) => request({ kind: "recoverReplica", id }),
302
+ recoverReplicaWithServer: (id, server) => client.withRecoveryServer(server, (transportScope, serialized) => request({ kind: "recoverReplicaWithServer", id, server: serialized, transportScope })),
303
+ syncHealth: () => request({ kind: "syncHealth" }),
280
304
  activeBranchId: () => request({ kind: "activeBranchId" }),
281
305
  activeAccountId: () => request({ kind: "activeAccountId" }),
282
306
  createBranch: (options) => request({ kind: "createBranch", options }),
@@ -353,7 +377,7 @@ function workerObserveBinding(request, notify, observeId) {
353
377
  }
354
378
  async function releaseWorker(client) {
355
379
  client.endLease();
356
- if (!client.isDisposed && idleWorkers.length < MAX_IDLE_WORKERS) {
380
+ if (client.reusable && !client.isDisposed && idleWorkers.length < MAX_IDLE_WORKERS) {
357
381
  idleWorkers.push(client);
358
382
  return;
359
383
  }
@@ -361,6 +385,7 @@ async function releaseWorker(client) {
361
385
  }
362
386
  export class LixWorkerClient {
363
387
  connection;
388
+ reusable;
364
389
  nextRequestId = 1;
365
390
  nextSnapshotInputId = 1;
366
391
  pending = new Map();
@@ -369,12 +394,26 @@ export class LixWorkerClient {
369
394
  onDisposed;
370
395
  telemetry;
371
396
  syncServer;
397
+ scopedServers = new Map();
398
+ nextTransportScope = 1;
399
+ async withRecoveryServer(server, operation) {
400
+ const scope = this.nextTransportScope++;
401
+ const runtime = { url: server.url, headers: server.headerProvider ?? server.headers, transport: server.transport };
402
+ this.scopedServers.set(scope, runtime);
403
+ try {
404
+ return await operation(scope, serializeSyncServer(runtime));
405
+ }
406
+ finally {
407
+ this.scopedServers.delete(scope);
408
+ }
409
+ }
372
410
  onProgress;
373
411
  openReport;
374
412
  syncFetchControllers = new Map();
375
413
  syncFetchStreams = new Map();
376
- constructor(connection = createWorkerConnection()) {
414
+ constructor(connection = createWorkerConnection(), reusable = true) {
377
415
  this.connection = connection;
416
+ this.reusable = reusable;
378
417
  connection.onMessage((message) => this.handleMessage(message));
379
418
  connection.onFatal((error) => this.handleFatal(error));
380
419
  }
@@ -497,10 +536,10 @@ export class LixWorkerClient {
497
536
  }
498
537
  break;
499
538
  case "sync.headers":
500
- void this.resolveSyncHeaders(message.requestId);
539
+ void this.resolveSyncHeaders(message.requestId, message.transportScope);
501
540
  break;
502
541
  case "sync.fetch":
503
- void this.resolveSyncFetch(message.requestId, message.request);
542
+ void this.resolveSyncFetch(message.requestId, message.request, message.transportScope);
504
543
  break;
505
544
  case "sync.fetch.stream.pull":
506
545
  void this.resolveSyncFetchStreamPull(message.requestId);
@@ -510,9 +549,11 @@ export class LixWorkerClient {
510
549
  break;
511
550
  }
512
551
  }
513
- async resolveSyncHeaders(requestId) {
552
+ async resolveSyncHeaders(requestId, transportScope) {
514
553
  try {
515
- const source = this.syncServer?.headers;
554
+ if (transportScope !== undefined && !this.scopedServers.has(transportScope))
555
+ throw workerClosedError();
556
+ const source = (transportScope === undefined ? this.syncServer : this.scopedServers.get(transportScope))?.headers;
516
557
  const headers = typeof source === "function" ? await source() : source;
517
558
  this.notify({
518
559
  kind: "sync.headers.result",
@@ -528,38 +569,33 @@ export class LixWorkerClient {
528
569
  });
529
570
  }
530
571
  }
531
- async resolveSyncFetch(requestId, request) {
532
- const fetcher = this.syncServer?.fetch;
533
- if (!fetcher) {
534
- this.notify({
535
- kind: "sync.fetch.result",
536
- requestId,
537
- result: {
538
- ok: false,
539
- error: serializeWorkerError(new Error("Sync fetch bridge is unavailable")),
540
- },
541
- });
572
+ async resolveSyncFetch(requestId, request, transportScope) {
573
+ const server = transportScope === undefined ? this.syncServer : this.scopedServers.get(transportScope);
574
+ if (!server) {
575
+ this.notify({ kind: "sync.fetch.result", requestId, result: { ok: false, error: serializeWorkerError(workerClosedError()) } });
542
576
  return;
543
577
  }
578
+ const transport = server.transport ?? fetchTransport(server.fetch);
544
579
  const controller = new AbortController();
545
580
  this.syncFetchControllers.set(requestId, controller);
546
581
  let retainedStream = false;
547
582
  try {
548
- const response = await fetcher(request.url, {
549
- method: request.method,
550
- headers: request.headers,
551
- body: typeof request.body === "string"
552
- ? request.body
553
- : request.body?.slice().buffer,
554
- credentials: request.credentials,
555
- signal: controller.signal,
556
- });
583
+ const response = await transport({ url: request.url, response: request.response, init: {
584
+ method: request.method,
585
+ headers: request.headers,
586
+ body: typeof request.body === "string"
587
+ ? request.body
588
+ : request.body?.slice().buffer,
589
+ credentials: request.credentials,
590
+ signal: controller.signal,
591
+ cache: request.cache, redirect: request.redirect,
592
+ } });
557
593
  if (this.syncFetchControllers.get(requestId) !== controller ||
558
594
  controller.signal.aborted) {
559
595
  await response.body?.cancel().catch(() => undefined);
560
596
  return;
561
597
  }
562
- if (request.responseMode === "stream") {
598
+ if (request.response.mode === "streaming") {
563
599
  this.syncFetchStreams.set(requestId, response.body?.getReader());
564
600
  retainedStream = true;
565
601
  this.notify({
@@ -577,7 +613,7 @@ export class LixWorkerClient {
577
613
  });
578
614
  return;
579
615
  }
580
- const body = await readSyncResponseBody(response, request.responseLimit, controller);
616
+ const body = new Uint8Array(await response.arrayBuffer());
581
617
  this.notify({
582
618
  kind: "sync.fetch.result",
583
619
  requestId,
@@ -593,6 +629,8 @@ export class LixWorkerClient {
593
629
  });
594
630
  }
595
631
  catch (error) {
632
+ if (isSyncResponseTooLarge(error))
633
+ controller.abort(error);
596
634
  if (!controller.signal.aborted || isSyncResponseTooLarge(error)) {
597
635
  this.notify({
598
636
  kind: "sync.fetch.result",
@@ -674,58 +712,10 @@ export class LixWorkerClient {
674
712
  this.connection.unref();
675
713
  }
676
714
  }
677
- async function readSyncResponseBody(response, limit, controller) {
678
- if (!Number.isSafeInteger(limit) || limit <= 0) {
679
- throw new TypeError("Browser sync fetch has no valid response limit");
680
- }
681
- const declaredLength = Number(response.headers.get("content-length"));
682
- if (Number.isFinite(declaredLength) && declaredLength > limit) {
683
- const error = syncResponseTooLarge(limit);
684
- controller.abort(error);
685
- throw error;
686
- }
687
- const stream = response.body;
688
- if (!stream)
689
- return new Uint8Array();
690
- const reader = stream.getReader();
691
- const chunks = [];
692
- let total = 0;
693
- try {
694
- while (true) {
695
- const { done, value } = await reader.read();
696
- if (done)
697
- break;
698
- total += value.byteLength;
699
- if (total > limit) {
700
- const error = syncResponseTooLarge(limit);
701
- controller.abort(error);
702
- await reader.cancel(error).catch(() => undefined);
703
- throw error;
704
- }
705
- chunks.push(value);
706
- }
707
- }
708
- finally {
709
- reader.releaseLock();
710
- }
711
- const body = new Uint8Array(total);
712
- let offset = 0;
713
- for (const chunk of chunks) {
714
- body.set(chunk, offset);
715
- offset += chunk.byteLength;
716
- }
717
- return body;
718
- }
719
- function syncResponseTooLarge(limit) {
720
- const error = new Error(`sync fetch response exceeds ${limit} bytes`);
721
- error.name = "LixError";
722
- error.code = "LIX_ERROR_SYNC_RESPONSE_TOO_LARGE";
723
- return error;
724
- }
725
715
  function isSyncResponseTooLarge(error) {
726
716
  return (error instanceof Error &&
727
717
  error.code ===
728
- "LIX_ERROR_SYNC_RESPONSE_TOO_LARGE");
718
+ "LIX_TRANSPORT_RESPONSE_LIMIT");
729
719
  }
730
720
  function workerClosedError() {
731
721
  const error = new Error("Lix worker is closed");
@@ -742,7 +732,6 @@ function serializeSyncServer(server) {
742
732
  ? undefined
743
733
  : headerEntries(server.headers),
744
734
  dynamicHeaders: typeof server.headers === "function",
745
- customFetch: server.fetch !== undefined,
746
735
  };
747
736
  }
748
737
  function headerEntries(headers) {
@@ -758,7 +747,7 @@ async function resolveDirectSyncServer(server) {
758
747
  return {
759
748
  url: new URL(server.url).toString(),
760
749
  headers: headerEntries(headers),
761
- fetch: server.fetch,
750
+ transport: server.transport ?? (server.fetch ? fetchTransport(server.fetch) : undefined),
762
751
  };
763
752
  }
764
753
  export async function hostedLixWorkerOperation(operation) {
@@ -771,3 +760,28 @@ export async function hostedLixWorkerOperation(operation) {
771
760
  await client.terminate();
772
761
  }
773
762
  }
763
+ export async function convertReplicaWorkerOperation(storage, server, branchId) {
764
+ const providerOptions = storage.kind === "jsStorage" ? storage.options : undefined;
765
+ const sharedKey = providerOptions && typeof providerOptions === "object"
766
+ && "sharedEngineKey" in providerOptions && typeof providerOptions.sharedEngineKey === "string"
767
+ && providerOptions.sharedEngineKey.startsWith("lix:opfs:") ? providerOptions.sharedEngineKey : undefined;
768
+ const connection = sharedKey ? createSharedWorkerConnection(`${sharedKey}:protocol-${ADMISSION_PROTOCOL_EPOCH}:storage-${ADMISSION_STORAGE_EPOCH}`) : undefined;
769
+ const client = connection ? new LixWorkerClient(connection, false) : new LixWorkerClient();
770
+ client.beginLease(undefined, undefined, server);
771
+ try {
772
+ await client.request({ kind: "replica.convert", storage, server: serializeSyncServer(server), branchId }, 0);
773
+ }
774
+ finally {
775
+ await client.terminate();
776
+ }
777
+ }
778
+ export async function retryReplicaMigrationCleanupWorkerOperation(storage, server) {
779
+ const client = new LixWorkerClient();
780
+ client.beginLease(undefined, undefined, server);
781
+ try {
782
+ return await client.request({ kind: "replica.cleanup", storage, server: serializeSyncServer(server) }, 0);
783
+ }
784
+ finally {
785
+ await client.terminate();
786
+ }
787
+ }
@@ -0,0 +1,20 @@
1
+ import { type AdmissionIdentity } from "./shared-admission.js";
2
+ export interface DurableLocalAdmissionStore {
3
+ get(key: string): Promise<unknown>;
4
+ put(key: string, value: unknown): Promise<void>;
5
+ delete(key: string): Promise<void>;
6
+ }
7
+ /** Durable local routing evidence only. Never grants or restores a remote lease. */
8
+ export declare class DurableLocalAdmission {
9
+ private readonly scope;
10
+ private readonly store;
11
+ private readonly url;
12
+ private readonly repositoryId;
13
+ constructor(scope: string, url: string, store?: DurableLocalAdmissionStore);
14
+ private key;
15
+ private validIdentity;
16
+ read(headers: [string, string][]): Promise<AdmissionIdentity | undefined>;
17
+ /** Call only after actual storage/account validation, never after metadata admission alone. */
18
+ record(headers: [string, string][], identity: AdmissionIdentity): Promise<void>;
19
+ remove(headers: [string, string][]): Promise<void>;
20
+ }
@@ -0,0 +1,87 @@
1
+ import { ADMISSION_PROTOCOL_EPOCH, ADMISSION_STORAGE_EPOCH } from "./shared-admission.js";
2
+ const anonymous = "00000000-0000-7000-8000-000000000002";
3
+ const databaseName = "lix-local-admission-v1";
4
+ const storeName = "proofs";
5
+ /** Each operation closes its connection and resolves only after transaction commit. */
6
+ class IndexedDbProofStore {
7
+ async operation(key, mode, value) {
8
+ const db = await new Promise((resolve, reject) => {
9
+ const request = indexedDB.open(databaseName, 1);
10
+ request.onupgradeneeded = () => request.result.createObjectStore(storeName);
11
+ request.onsuccess = () => resolve(request.result);
12
+ request.onerror = () => reject(request.error);
13
+ });
14
+ try {
15
+ return await new Promise((resolve, reject) => {
16
+ const transaction = db.transaction(storeName, mode === "get" ? "readonly" : "readwrite");
17
+ const store = transaction.objectStore(storeName);
18
+ const request = mode === "get" ? store.get(key) : mode === "put" ? store.put(value, key) : store.delete(key);
19
+ transaction.oncomplete = () => resolve(request.result);
20
+ transaction.onabort = () => reject(transaction.error ?? new Error("Local admission transaction aborted"));
21
+ transaction.onerror = () => reject(transaction.error ?? new Error("Local admission transaction failed"));
22
+ });
23
+ }
24
+ finally {
25
+ db.close();
26
+ }
27
+ }
28
+ get(key) { return this.operation(key, "get"); }
29
+ async put(key, value) { await this.operation(key, "put", value); }
30
+ async delete(key) { await this.operation(key, "delete"); }
31
+ }
32
+ /** Durable local routing evidence only. Never grants or restores a remote lease. */
33
+ export class DurableLocalAdmission {
34
+ scope;
35
+ store;
36
+ url;
37
+ repositoryId;
38
+ constructor(scope, url, store = new IndexedDbProofStore()) {
39
+ this.scope = scope;
40
+ this.store = store;
41
+ const parsed = new URL(url);
42
+ const id = parsed.pathname.match(/^\/lix\/([0-9a-f-]{36})\/?$/i)?.[1];
43
+ if (!scope || scope.length > 4096 || !id || !["https:", "http:"].includes(parsed.protocol) || parsed.search || parsed.hash || parsed.username || parsed.password) {
44
+ throw new Error("Invalid durable local admission scope");
45
+ }
46
+ parsed.pathname = `/lix/${id}`;
47
+ this.url = parsed.toString();
48
+ this.repositoryId = id;
49
+ }
50
+ async key(headers) {
51
+ const entries = [];
52
+ new Headers(headers).forEach((value, name) => entries.push([name, value]));
53
+ const bytes = new TextEncoder().encode(JSON.stringify([1, this.scope, this.url, entries, ADMISSION_PROTOCOL_EPOCH, ADMISSION_STORAGE_EPOCH]));
54
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
55
+ return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, "0")).join("");
56
+ }
57
+ validIdentity(value) {
58
+ if (!value || typeof value !== "object" || Array.isArray(value))
59
+ return false;
60
+ const v = value;
61
+ return Object.keys(v).sort().join(",") === "principalId,protocolEpoch,repositoryId,storageEpoch" &&
62
+ v.repositoryId === this.repositoryId && typeof v.principalId === "string" && /^[\x21-\x7e]{1,255}$/.test(v.principalId) &&
63
+ v.protocolEpoch === ADMISSION_PROTOCOL_EPOCH && v.storageEpoch === ADMISSION_STORAGE_EPOCH;
64
+ }
65
+ async read(headers) {
66
+ const key = await this.key(headers);
67
+ const value = await this.store.get(key);
68
+ if (!value || typeof value !== "object" || Array.isArray(value))
69
+ return undefined;
70
+ const record = value;
71
+ if (Object.keys(record).sort().join(",") !== "identity,key,schemaVersion" || record.schemaVersion !== 1 || record.key !== key || !this.validIdentity(record.identity))
72
+ return undefined;
73
+ if (!new Headers(headers).get("authorization") && record.identity.principalId !== anonymous)
74
+ return undefined;
75
+ return { ...record.identity };
76
+ }
77
+ /** Call only after actual storage/account validation, never after metadata admission alone. */
78
+ async record(headers, identity) {
79
+ if (!this.validIdentity(identity))
80
+ throw new Error("Invalid durable local admission identity");
81
+ if (!new Headers(headers).get("authorization") && identity.principalId !== anonymous)
82
+ return;
83
+ const key = await this.key(headers);
84
+ await this.store.put(key, { schemaVersion: 1, key, identity: { ...identity } });
85
+ }
86
+ async remove(headers) { await this.store.delete(await this.key(headers)); }
87
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,211 @@
1
+ /// <reference lib="webworker" />
2
+ import { openLixBinding, convertReplicaBinding } from "#binding";
3
+ import { fetchTransport, HttpTransportError } from "../http-transport.js";
4
+ import { SharedAdmissionCache, requestAdmission, sharedCredentialKey, sameAdmission, } from "./shared-admission.js";
5
+ import { DurableLocalAdmission } from "./durable-local-admission.js";
6
+ import { startWorkerHost } from "./host.js";
7
+ import { SharedEngineOwner } from "./shared-engine.js";
8
+ import { serializeWorkerError } from "./protocol.js";
9
+ const scope = globalThis;
10
+ let owner;
11
+ let configuration;
12
+ // Same-origin callers own the local store. Durable routing proofs can reopen
13
+ // its cached data after worker shutdown, but never authorize remote requests.
14
+ // Raw credentials remain in memory only.
15
+ const admitted = new SharedAdmissionCache();
16
+ let rootIdentity;
17
+ scope.onconnect = (event) => {
18
+ const port = event.ports[0];
19
+ let client;
20
+ let disconnected = false;
21
+ let input;
22
+ const prepareClient = async (...args) => {
23
+ const [storage, telemetry, parent, server, progress, snapshot] = args;
24
+ if (!server || snapshot)
25
+ throw new Error("Shared partial engines require a server and existing storage");
26
+ const config = JSON.stringify([storage, server.url]);
27
+ if (configuration !== undefined && config !== configuration)
28
+ throw new Error("Shared engine configuration mismatch");
29
+ configuration = config;
30
+ const raw = server;
31
+ const readHeaders = async () => raw.headerProvider ? await raw.headerProvider() : raw.headers;
32
+ let verifiedKey;
33
+ let verifiedGeneration;
34
+ let candidateIdentity;
35
+ let candidateHeaders;
36
+ let candidateOnline = false;
37
+ let candidateGeneration = 0;
38
+ let candidateCredentialGeneration = 0;
39
+ const providerOptions = storage.kind === "jsStorage" ? storage.options : undefined;
40
+ const physicalScope = providerOptions && typeof providerOptions === "object" &&
41
+ "sharedEngineKey" in providerOptions && typeof providerOptions.sharedEngineKey === "string"
42
+ ? providerOptions.sharedEngineKey : undefined;
43
+ if (!physicalScope?.startsWith("lix:opfs:"))
44
+ throw new Error("Missing physical shared storage identity");
45
+ const localAdmission = new DurableLocalAdmission(physicalScope, raw.url);
46
+ const transport = raw.transport ?? fetchTransport();
47
+ const authenticate = async (headers, allowOffline) => {
48
+ const credentialGeneration = admitted.generation(raw.url, headers);
49
+ let result;
50
+ try {
51
+ result = await admitted.verify(raw.url, headers, rootIdentity, () => requestAdmission(raw.url, headers, transport), allowOffline);
52
+ }
53
+ catch (error) {
54
+ const code = error?.code;
55
+ if (code === "LIX_ADMISSION_AUTH_REJECTED") {
56
+ if (candidateHeaders && sharedCredentialKey(raw.url, candidateHeaders) === sharedCredentialKey(raw.url, headers)) {
57
+ candidateOnline = false;
58
+ candidateGeneration++;
59
+ }
60
+ admitted.remove(raw.url, headers);
61
+ await localAdmission.remove(headers).catch(() => undefined);
62
+ }
63
+ if (!allowOffline || code !== "LIX_IDENTITY_UNVERIFIED_OFFLINE")
64
+ throw error;
65
+ const local = await localAdmission.read(headers).catch(() => undefined);
66
+ if (!local)
67
+ throw error;
68
+ if (rootIdentity && !sameAdmission(local, rootIdentity)) {
69
+ throw new HttpTransportError("LIX_SHARED_ENGINE_IDENTITY_MISMATCH", "Cached local repository/account does not match this owner");
70
+ }
71
+ result = { identity: local, online: false };
72
+ }
73
+ if (admitted.generation(raw.url, headers) !== credentialGeneration) {
74
+ throw new HttpTransportError("LIX_ADMISSION_AUTH_REJECTED", "Credentials were rejected while local admission was in flight");
75
+ }
76
+ if (result.online) {
77
+ verifiedKey = sharedCredentialKey(raw.url, headers);
78
+ verifiedGeneration = credentialGeneration;
79
+ }
80
+ candidateCredentialGeneration = credentialGeneration;
81
+ candidateGeneration++;
82
+ candidateIdentity = result.identity;
83
+ candidateHeaders = headers.map(([name, value]) => [name, value]);
84
+ candidateOnline = result.online;
85
+ return result;
86
+ };
87
+ const routed = {
88
+ ...raw,
89
+ transport: async (request) => {
90
+ try {
91
+ const response = await transport(request);
92
+ return response;
93
+ }
94
+ catch (error) {
95
+ verifiedKey = undefined;
96
+ throw error;
97
+ }
98
+ },
99
+ headerProvider: async () => {
100
+ const headers = await readHeaders();
101
+ if (sharedCredentialKey(raw.url, headers) !== verifiedKey || admitted.generation(raw.url, headers) !== verifiedGeneration) {
102
+ // Failure only suspends this remote lease; local sessions survive.
103
+ verifiedKey = undefined;
104
+ await authenticate(headers, false);
105
+ }
106
+ return headers;
107
+ },
108
+ };
109
+ if (!owner) {
110
+ owner = new SharedEngineOwner(async (transport, backgroundTelemetry, opener) => {
111
+ return openLixBinding(storage, backgroundTelemetry, opener.parent, transport, opener.progress);
112
+ });
113
+ }
114
+ client = {
115
+ server: routed,
116
+ isDisconnected: () => disconnected,
117
+ telemetry,
118
+ parent,
119
+ progress,
120
+ rejectCredentials: async (headers) => {
121
+ const rejectedKey = sharedCredentialKey(raw.url, headers);
122
+ if (verifiedKey === rejectedKey)
123
+ verifiedKey = undefined;
124
+ if (candidateHeaders && sharedCredentialKey(raw.url, candidateHeaders) === rejectedKey) {
125
+ candidateOnline = false;
126
+ candidateGeneration++;
127
+ }
128
+ admitted.remove(raw.url, headers);
129
+ await localAdmission.remove(headers).catch(() => undefined);
130
+ },
131
+ commitIdentity: async () => {
132
+ if (!candidateIdentity)
133
+ throw new Error("Missing verified owner identity");
134
+ rootIdentity ??= candidateIdentity;
135
+ if (candidateOnline && candidateHeaders) {
136
+ // The owner has checked the actual stored account before this call.
137
+ // Failure to cache only disables later offline reopening.
138
+ const generation = candidateGeneration;
139
+ const headers = candidateHeaders;
140
+ const identity = candidateIdentity;
141
+ await admitted.persistLocal(raw.url, headers, candidateCredentialGeneration, () => localAdmission.record(headers, identity), () => localAdmission.remove(headers)).catch(() => undefined);
142
+ // A rejection during the asynchronous write must not resurrect its proof.
143
+ if (generation !== candidateGeneration)
144
+ await localAdmission.remove(headers).catch(() => undefined);
145
+ }
146
+ },
147
+ verifyIdentity: async () => {
148
+ const headers = await readHeaders();
149
+ const { identity, online } = await authenticate(headers, true);
150
+ return { authorityUrl: raw.url, accountId: identity.principalId, headers, online };
151
+ },
152
+ };
153
+ return { owner, client };
154
+ };
155
+ const controller = startWorkerHost({
156
+ postMessage: (message) => port.postMessage(message),
157
+ onMessage: (listener) => { input = listener; },
158
+ }, async (...args) => {
159
+ const prepared = await prepareClient(...args);
160
+ const binding = await prepared.owner.attach(prepared.client);
161
+ if (disconnected) {
162
+ await binding.close();
163
+ throw new Error("Shared engine client disconnected during open");
164
+ }
165
+ return binding;
166
+ }, async (storage, server, branchId) => {
167
+ const prepared = await prepareClient(storage, undefined, undefined, server);
168
+ await prepared.owner.convert(prepared.client, transport => convertReplicaBinding(storage, transport, branchId), branchId);
169
+ });
170
+ const disconnect = async () => {
171
+ if (disconnected)
172
+ return;
173
+ disconnected = true;
174
+ if (client)
175
+ owner?.deactivate(client);
176
+ let failure;
177
+ try {
178
+ await controller.close();
179
+ }
180
+ catch (error) {
181
+ failure = error;
182
+ }
183
+ try {
184
+ if (client)
185
+ await owner?.detach(client);
186
+ }
187
+ catch (error) {
188
+ failure ??= error;
189
+ }
190
+ try {
191
+ port.postMessage({ kind: "shared.disconnected", error: failure === undefined ? undefined : serializeWorkerError(failure) });
192
+ }
193
+ finally {
194
+ port.close();
195
+ }
196
+ };
197
+ port.onmessage = (event) => {
198
+ const message = event.data;
199
+ if (message?.kind === "shared.disconnect") {
200
+ void disconnect();
201
+ return;
202
+ }
203
+ if (message?.kind === "shared.clientLease") {
204
+ // The page holds this lock. Acquisition means it closed or crashed.
205
+ void navigator.locks.request(message.name, () => disconnect());
206
+ return;
207
+ }
208
+ input?.(message);
209
+ };
210
+ port.start();
211
+ };
@@ -2,3 +2,5 @@ import type { LixBinding, LixStorageConfig, TelemetryDispatch, TelemetryParentCo
2
2
  import type { WorkerConnection } from "./protocol.js";
3
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;
5
+ /** Package-private provider identity selects one engine, not one engine per tab. */
6
+ export declare function createSharedWorkerConnection(key: string): WorkerConnection;