@lix-js/sdk 0.16.0 → 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 (73) hide show
  1. package/README.md +187 -22
  2. package/dist/binding-types.d.ts +20 -6
  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 +20 -10
  27. package/dist/lix.js +68 -23
  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 -5
  46. package/dist/storage-ownership.d.ts +5 -0
  47. package/dist/storage-ownership.js +4 -0
  48. package/dist/types.d.ts +132 -22
  49. package/dist/wasm/lix_js_sdk.d.ts +57 -16
  50. package/dist/wasm/lix_js_sdk.js +268 -60
  51. package/dist/wasm/lix_js_sdk_bg.wasm +2 -2
  52. package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +23 -9
  53. package/dist/worker/client.d.ts +12 -3
  54. package/dist/worker/client.js +98 -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 +92 -32
  65. package/dist/worker/protocol.d.ts +33 -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 -13
  72. package/dist/remote/server-protocol.d.ts +0 -185
  73. package/dist/remote/server-protocol.js +0 -417
@@ -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;
@@ -1,4 +1,5 @@
1
1
  /// <reference lib="webworker" />
2
+ import { deserializeWorkerError } from "./protocol.js";
2
3
  // Browser/Wasm execution stays off the main thread.
3
4
  export const openDirectLixBinding = undefined;
4
5
  export function createWorkerConnection() {
@@ -14,7 +15,7 @@ export function createWorkerConnection() {
14
15
  worker.onmessage = (event) => listener(event.data);
15
16
  },
16
17
  onFatal(listener) {
17
- worker.onerror = (event) => listener(event.error ?? new Error(event.message ?? "Lix worker failed"));
18
+ worker.onerror = (event) => listener(workerFailure(event));
18
19
  },
19
20
  ref() { },
20
21
  unref() { },
@@ -23,3 +24,66 @@ export function createWorkerConnection() {
23
24
  },
24
25
  };
25
26
  }
27
+ /** Package-private provider identity selects one engine, not one engine per tab. */
28
+ export function createSharedWorkerConnection(key) {
29
+ if (typeof SharedWorker === "undefined" || !navigator.locks)
30
+ throw new Error("Shared partial replicas require SharedWorker and Web Locks");
31
+ const worker = new SharedWorker(new URL("./entry.shared.browser.js", import.meta.url), { type: "module", name: key });
32
+ const port = worker.port;
33
+ const leaseName = `lix:shared-client:${crypto.randomUUID()}`;
34
+ let release;
35
+ const lifetime = new Promise(resolve => { release = resolve; });
36
+ let ready;
37
+ const acquired = new Promise(resolve => { ready = resolve; });
38
+ let failure;
39
+ void navigator.locks.request(leaseName, async () => { ready(); await lifetime; }).catch(error => failure?.(error));
40
+ void acquired.then(() => port.postMessage({ kind: "shared.clientLease", name: leaseName }));
41
+ port.start();
42
+ let closed = false;
43
+ let termination;
44
+ let disconnected;
45
+ const detached = new Promise((resolve, reject) => { disconnected = error => error ? reject(error) : resolve(); });
46
+ return {
47
+ postMessage(message) { if (closed)
48
+ throw new Error("Shared engine connection closed"); port.postMessage(message); },
49
+ onMessage(listener) {
50
+ port.onmessage = event => {
51
+ if (event.data?.kind === "shared.disconnected") {
52
+ const error = event.data.error;
53
+ disconnected(error ? deserializeWorkerError(error) : undefined);
54
+ }
55
+ else
56
+ listener(event.data);
57
+ };
58
+ },
59
+ onFatal(listener) { failure = listener; worker.onerror = event => listener(workerFailure(event)); },
60
+ ref() { }, unref() { },
61
+ terminate() {
62
+ if (termination)
63
+ return termination;
64
+ termination = (async () => {
65
+ closed = true;
66
+ port.postMessage({ kind: "shared.disconnect" });
67
+ release();
68
+ let timeout;
69
+ try {
70
+ await Promise.race([detached, new Promise((_, reject) => {
71
+ timeout = setTimeout(() => reject(Object.assign(new Error("Shared engine close was not acknowledged"), { code: "LIX_SHARED_ENGINE_CLOSE_UNCONFIRMED" })), 10000);
72
+ })]);
73
+ }
74
+ finally {
75
+ if (timeout !== undefined)
76
+ clearTimeout(timeout);
77
+ port.close();
78
+ }
79
+ })();
80
+ return termination;
81
+ },
82
+ };
83
+ }
84
+ function workerFailure(event) {
85
+ if (event.error instanceof Error)
86
+ return event.error;
87
+ const location = event.filename ? ` (${event.filename}:${event.lineno ?? 0}:${event.colno ?? 0})` : "";
88
+ return Object.assign(new Error(`${event.message || "Lix worker failed to load or execute"}${location}`), { code: "LIX_WORKER_FAILED" });
89
+ }
@@ -3,3 +3,4 @@ import type { WorkerConnection } from "./protocol.js";
3
3
  export declare function createWorkerConnection(): WorkerConnection;
4
4
  export declare function workerExecArgv(execArgv: readonly string[]): string[];
5
5
  export declare const openDirectLixBinding: (storage: LixStorageConfig, telemetry?: TelemetryDispatch, telemetryParent?: TelemetryParentContext, server?: SyncServerBindingOptions, openProgress?: OpenProgressDispatch, snapshot?: ReadableStream<Uint8Array>) => Promise<LixBinding | undefined>;
6
+ export declare function createSharedWorkerConnection(_key: string): WorkerConnection | undefined;
@@ -55,3 +55,6 @@ export function workerExecArgv(execArgv) {
55
55
  export const openDirectLixBinding = async (storage, telemetry, telemetryParent, server, openProgress, snapshot) => {
56
56
  return openLixBinding(storage, telemetry, telemetryParent, server, openProgress, snapshot);
57
57
  };
58
+ export function createSharedWorkerConnection(_key) {
59
+ return undefined;
60
+ }
@@ -1,4 +1,6 @@
1
- import { openLixBinding } from "#binding";
1
+ import { openLixBinding, convertReplicaBinding } from "#binding";
2
2
  import { type WorkerHostEndpoint, type WorkerSyncFetchResponse } from "./protocol.js";
3
- export declare function startWorkerHost(endpoint: WorkerHostEndpoint, openBinding?: typeof openLixBinding): void;
3
+ export declare function startWorkerHost(endpoint: WorkerHostEndpoint, openBinding?: typeof openLixBinding, convertBinding?: typeof convertReplicaBinding): {
4
+ close(): Promise<void>;
5
+ };
4
6
  export declare function responseFromSyncFetch(resolved: WorkerSyncFetchResponse): Response;
@@ -1,6 +1,8 @@
1
- import { openLixBinding, createHostedBinding, deleteHostedBinding, } from "#binding";
1
+ import { validateHttpRequest } from "../http-transport.js";
2
+ import { openLixBinding, convertReplicaBinding, retryReplicaMigrationCleanupBinding, createHostedBinding, deleteHostedBinding, } from "#binding";
2
3
  import { deserializeWorkerError, serializeWorkerError, } from "./protocol.js";
3
- export function startWorkerHost(endpoint, openBinding = openLixBinding) {
4
+ export function startWorkerHost(endpoint, openBinding = openLixBinding, convertBinding = convertReplicaBinding) {
5
+ let closed = false;
4
6
  const sessions = new Map();
5
7
  let nextSessionId = 1;
6
8
  let nextTransactionId = 1;
@@ -16,7 +18,10 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
16
18
  const pendingSyncStreamPulls = new Map();
17
19
  const syncStreamCleanup = new Map();
18
20
  let finiteQueue = Promise.resolve();
21
+ const registrations = new Set();
19
22
  endpoint.onMessage((message) => {
23
+ if (closed && "id" in message)
24
+ return;
20
25
  if (!("id" in message)) {
21
26
  handleNotification(message);
22
27
  return;
@@ -54,15 +59,21 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
54
59
  // finite-operation queue lets a long-running operation block a newly
55
60
  // mounted query, including one that needs lazy history hydration.
56
61
  // The live `next()` lane is already independent for the same reason.
57
- void respond(message, () => handleObserveRegistration(message.sessionId, operation.sql, operation.params));
62
+ const registration = respond(message, () => handleObserveRegistration(message.sessionId, operation.sql, operation.params));
63
+ registrations.add(registration);
64
+ void registration.finally(() => registrations.delete(registration));
58
65
  return;
59
66
  }
60
67
  finiteQueue = finiteQueue.then(async () => {
61
68
  try {
62
69
  await respond(message, async () => {
70
+ if (closed)
71
+ throw workerStateError("Worker client disconnected");
63
72
  if (message.operation.kind !== "open" &&
64
73
  message.operation.kind !== "hosted.create" &&
65
- message.operation.kind !== "hosted.delete") {
74
+ message.operation.kind !== "hosted.delete" &&
75
+ message.operation.kind !== "replica.convert" &&
76
+ message.operation.kind !== "replica.cleanup") {
66
77
  requiredLix(message.sessionId).setTelemetryParent(message.telemetryParent);
67
78
  }
68
79
  return handleFiniteOperation(message.sessionId, message.operation, message.telemetryParent);
@@ -165,6 +176,14 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
165
176
  }
166
177
  async function handleFiniteOperation(sessionId, operation, telemetryParent) {
167
178
  switch (operation.kind) {
179
+ case "replica.cleanup":
180
+ if (sessions.size > 0)
181
+ throw workerStateError("Migration cleanup requires closed storage");
182
+ return retryReplicaMigrationCleanupBinding(operation.storage, createSyncServerBridge(operation.server));
183
+ case "replica.convert":
184
+ if (sessions.size > 0)
185
+ throw workerStateError("Conversion requires closed storage");
186
+ return convertBinding(operation.storage, createSyncServerBridge(operation.server), operation.branchId);
168
187
  case "hosted.create":
169
188
  return createHostedBinding(operation.server);
170
189
  case "hosted.delete":
@@ -221,8 +240,7 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
221
240
  case "transaction.commit": {
222
241
  const transaction = requiredTransaction(operation.transactionId);
223
242
  transactions.delete(operation.transactionId);
224
- await transaction.commit();
225
- return undefined;
243
+ return await transaction.commit();
226
244
  }
227
245
  case "transaction.rollback": {
228
246
  const transaction = requiredTransaction(operation.transactionId);
@@ -230,6 +248,16 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
230
248
  await transaction.rollback();
231
249
  return undefined;
232
250
  }
251
+ case "replicaRecoverySources":
252
+ return requiredLix(sessionId).replicaRecoverySources();
253
+ case "exportReplicaRecovery":
254
+ return requiredLix(sessionId).exportReplicaRecovery(operation.id);
255
+ case "recoverReplica":
256
+ return requiredLix(sessionId).recoverReplica(operation.id);
257
+ case "recoverReplicaWithServer":
258
+ return requiredLix(sessionId).recoverReplicaWithServer(operation.id, createSyncServerBridge(operation.server, operation.transportScope));
259
+ case "syncHealth":
260
+ return requiredLix(sessionId).syncHealth();
233
261
  case "activeBranchId":
234
262
  return requiredLix(sessionId).activeBranchId();
235
263
  case "activeAccountId":
@@ -322,7 +350,46 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
322
350
  }
323
351
  await input.writer.close();
324
352
  }
325
- function createSyncServerBridge(server) {
353
+ return { async close() {
354
+ if (closed)
355
+ return;
356
+ closed = true;
357
+ const failure = workerStateError("Worker client disconnected");
358
+ for (const pending of pendingSyncHeaders.values())
359
+ pending.reject(failure);
360
+ pendingSyncHeaders.clear();
361
+ for (const pending of pendingSyncFetch.values())
362
+ pending.reject(failure);
363
+ pendingSyncFetch.clear();
364
+ for (const cleanup of syncStreamCleanup.values())
365
+ cleanup();
366
+ syncStreamCleanup.clear();
367
+ for (const pending of pendingSyncStreamPulls.values()) {
368
+ pending.controller.error(failure);
369
+ pending.reject(failure);
370
+ }
371
+ pendingSyncStreamPulls.clear();
372
+ for (const observation of observations.values())
373
+ observation.close();
374
+ observations.clear();
375
+ for (const snapshot of snapshotExports.values())
376
+ await Promise.resolve(snapshot.cancel()).catch(() => undefined);
377
+ snapshotExports.clear();
378
+ await finiteQueue.catch(() => undefined);
379
+ await Promise.allSettled(registrations);
380
+ // The active finite operation has finished; never roll back a handle
381
+ // concurrently with its execute/commit operation.
382
+ for (const transaction of transactions.values())
383
+ await transaction.rollback().catch(() => undefined);
384
+ transactions.clear();
385
+ for (const snapshot of snapshotExports.values())
386
+ await Promise.resolve(snapshot.cancel()).catch(() => undefined);
387
+ snapshotExports.clear();
388
+ for (const session of sessions.values())
389
+ await session.close();
390
+ sessions.clear();
391
+ } };
392
+ function createSyncServerBridge(server, transportScope) {
326
393
  if (!server)
327
394
  return undefined;
328
395
  return {
@@ -330,48 +397,37 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
330
397
  headers: server.headers ?? [],
331
398
  headerProvider: server.dynamicHeaders
332
399
  ? () => {
400
+ if (closed)
401
+ throw workerStateError("Worker client disconnected");
333
402
  const requestId = nextSyncRequestId++;
334
403
  return new Promise((resolve, reject) => {
335
404
  pendingSyncHeaders.set(requestId, { resolve, reject });
336
- endpoint.postMessage({ kind: "sync.headers", requestId });
405
+ endpoint.postMessage({ kind: "sync.headers", requestId, transportScope });
337
406
  });
338
407
  }
339
408
  : undefined,
340
- fetch: server.customFetch ? bridgeFetch : undefined,
409
+ transport: (request) => bridgeFetch(request, transportScope),
341
410
  };
342
411
  }
343
- async function bridgeFetch(input, init) {
344
- const extension = init;
345
- const streaming = extension?.lixResponseStream === true;
346
- const responseLimit = extension?.lixResponseLimit;
347
- if (!streaming &&
348
- (typeof responseLimit !== "number" ||
349
- !Number.isSafeInteger(responseLimit) ||
350
- responseLimit <= 0)) {
351
- throw new TypeError("Browser sync fetch has no valid response limit");
352
- }
412
+ async function bridgeFetch(httpRequest, transportScope) {
413
+ validateHttpRequest(httpRequest);
414
+ const { url: input, init, response: policy } = httpRequest;
415
+ const streaming = policy.mode === "streaming";
416
+ if (closed)
417
+ throw workerStateError("Worker client disconnected");
353
418
  const requestId = nextSyncRequestId++;
354
419
  const requestBase = {
355
- url: typeof input === "string"
356
- ? input
357
- : input instanceof URL
358
- ? input.toString()
359
- : input.url,
420
+ url: input,
360
421
  method: init?.method ?? "GET",
361
422
  headers: headerEntries(init?.headers),
362
423
  body: serializableBody(init?.body),
363
424
  credentials: init?.credentials,
425
+ cache: init?.cache, redirect: init?.redirect,
364
426
  };
365
- const request = streaming
366
- ? { ...requestBase, responseMode: "stream" }
367
- : {
368
- ...requestBase,
369
- responseMode: "buffered",
370
- responseLimit: responseLimit,
371
- };
427
+ const request = { ...requestBase, response: policy };
372
428
  const response = new Promise((resolve, reject) => {
373
429
  pendingSyncFetch.set(requestId, { resolve, reject });
374
- endpoint.postMessage({ kind: "sync.fetch", requestId, request });
430
+ endpoint.postMessage({ kind: "sync.fetch", requestId, request, transportScope });
375
431
  });
376
432
  const abort = () => {
377
433
  const pending = pendingSyncFetch.get(requestId);
@@ -463,6 +519,10 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
463
519
  // serialized finite lane. Each `observe.next` supplies telemetry directly
464
520
  // to its observation binding.
465
521
  const events = await requiredLix(sessionId).observe(sql, params);
522
+ if (closed) {
523
+ events.close();
524
+ throw workerStateError("Worker client disconnected");
525
+ }
466
526
  const observeId = nextObserveId++;
467
527
  observations.set(observeId, events);
468
528
  return observeId;
@@ -1,10 +1,10 @@
1
+ import type { HttpResponsePolicy } from "../http-transport.js";
1
2
  import type { BindingBatchStatement, BindingParam, LixStorageConfig } from "../binding-types.js";
2
3
  import type { CreateBranchOptions, ExecuteOptions, LixBatchOptions, MergeBranchOptions, SwitchBranchOptions, LixTelemetrySpan, LixTelemetryParentContext, LixOpenProgress, OpenAnotherSessionOptions } from "../types.js";
3
4
  export type WorkerSyncServerOptions = {
4
5
  url: string;
5
6
  headers?: [string, string][];
6
7
  dynamicHeaders: boolean;
7
- customFetch: boolean;
8
8
  };
9
9
  export type WorkerSyncFetchRequest = {
10
10
  url: string;
@@ -12,12 +12,10 @@ export type WorkerSyncFetchRequest = {
12
12
  headers: [string, string][];
13
13
  body?: string | Uint8Array;
14
14
  credentials?: RequestCredentials;
15
- } & ({
16
- responseMode: "buffered";
17
- responseLimit: number;
18
- } | {
19
- responseMode: "stream";
20
- });
15
+ cache?: RequestCache;
16
+ redirect?: RequestRedirect;
17
+ response: HttpResponsePolicy;
18
+ };
21
19
  type WorkerSyncFetchResponseHead = {
22
20
  status: number;
23
21
  statusText: string;
@@ -36,6 +34,15 @@ export type WorkerRequest = {
36
34
  operation: WorkerOperation;
37
35
  };
38
36
  export type WorkerOperation = {
37
+ kind: "replica.cleanup";
38
+ storage: LixStorageConfig;
39
+ server: WorkerSyncServerOptions;
40
+ } | {
41
+ kind: "replica.convert";
42
+ storage: LixStorageConfig;
43
+ server: WorkerSyncServerOptions;
44
+ branchId?: string;
45
+ } | {
39
46
  kind: "hosted.create";
40
47
  server: import("../binding-types.js").HostedServerBindingOptions;
41
48
  } | {
@@ -84,6 +91,21 @@ export type WorkerOperation = {
84
91
  } | {
85
92
  kind: "transaction.rollback";
86
93
  transactionId: number;
94
+ } | {
95
+ kind: "replicaRecoverySources";
96
+ } | {
97
+ kind: "exportReplicaRecovery";
98
+ id: string;
99
+ } | {
100
+ kind: "recoverReplica";
101
+ id: string;
102
+ } | {
103
+ kind: "recoverReplicaWithServer";
104
+ id: string;
105
+ server: WorkerSyncServerOptions;
106
+ transportScope: number;
107
+ } | {
108
+ kind: "syncHealth";
87
109
  } | {
88
110
  kind: "activeBranchId";
89
111
  } | {
@@ -185,6 +207,7 @@ export type WorkerHostEndpoint = {
185
207
  onMessage(listener: (message: WorkerInput) => void): void;
186
208
  };
187
209
  export type SerializedWorkerError = {
210
+ cause?: SerializedWorkerError;
188
211
  name: string;
189
212
  message: string;
190
213
  stack?: string;
@@ -209,10 +232,12 @@ export type WorkerResponse = {
209
232
  } | {
210
233
  kind: "sync.headers";
211
234
  requestId: number;
235
+ transportScope?: number;
212
236
  } | {
213
237
  kind: "sync.fetch";
214
238
  requestId: number;
215
239
  request: WorkerSyncFetchRequest;
240
+ transportScope?: number;
216
241
  } | {
217
242
  kind: "sync.fetch.stream.pull";
218
243
  requestId: number;
@@ -220,6 +245,6 @@ export type WorkerResponse = {
220
245
  kind: "sync.fetch.cancel";
221
246
  requestId: number;
222
247
  };
223
- export declare function serializeWorkerError(error: unknown): SerializedWorkerError;
248
+ export declare function serializeWorkerError(error: unknown, depth?: number): SerializedWorkerError;
224
249
  export declare function deserializeWorkerError(error: SerializedWorkerError): Error;
225
250
  export {};