@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.
- package/README.md +162 -44
- package/dist/binding-types.d.ts +15 -5
- package/dist/binding.browser.d.ts +2 -0
- package/dist/binding.browser.js +14 -4
- package/dist/binding.node-wasm.d.ts +1 -1
- package/dist/binding.node-wasm.js +5 -3
- package/dist/binding.node.d.ts +2 -0
- package/dist/binding.node.js +34 -8
- package/dist/bundled-plugins/plugin_csv.lixplugin +0 -0
- package/dist/bundled-plugins/plugin_markdown.lixplugin +0 -0
- package/dist/compatibility.d.ts +6 -0
- package/dist/compatibility.js +6 -0
- package/dist/component-host/dispatch.d.ts +12 -0
- package/dist/component-host/dispatch.js +364 -0
- package/dist/component-host/index.d.ts +15 -0
- package/dist/component-host/index.js +84 -0
- package/dist/component-host/instrument.d.ts +6 -0
- package/dist/component-host/instrument.js +260 -0
- package/dist/conversion-provider.d.ts +4 -0
- package/dist/conversion-provider.js +20 -0
- package/dist/hosted-lix.js +1 -1
- package/dist/http-transport.d.ts +25 -0
- package/dist/http-transport.js +162 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/lix.d.ts +14 -9
- package/dist/lix.js +44 -9
- package/dist/migration-binding.browser.d.ts +33 -0
- package/dist/migration-binding.browser.js +46 -0
- package/dist/migration-binding.node.d.ts +6 -0
- package/dist/migration-binding.node.js +5 -0
- package/dist/migration-wasm/lix_js_sdk.d.ts +271 -0
- package/dist/migration-wasm/lix_js_sdk.js +1852 -0
- package/dist/migration-wasm/lix_js_sdk_bg.wasm +4 -0
- package/dist/migration-wasm/lix_js_sdk_bg.wasm.d.ts +101 -0
- package/dist/migration.d.ts +23 -0
- package/dist/migration.js +55 -0
- package/dist/open-lix.js +29 -18
- package/dist/open-progress.d.ts +10 -0
- package/dist/open-progress.js +59 -0
- package/dist/remote/client.d.ts +2 -1
- package/dist/remote/client.js +11 -1
- package/dist/result.d.ts +4 -3
- package/dist/result.js +3 -4
- package/dist/storage-adapter.d.ts +7 -1
- package/dist/storage-ownership.d.ts +5 -0
- package/dist/storage-ownership.js +4 -0
- package/dist/types.d.ts +65 -31
- package/dist/wasm/lix_js_sdk.d.ts +45 -16
- package/dist/wasm/lix_js_sdk.js +214 -60
- package/dist/wasm/lix_js_sdk_bg.wasm +2 -2
- package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +17 -9
- package/dist/worker/client.d.ts +12 -3
- package/dist/worker/client.js +95 -81
- package/dist/worker/durable-local-admission.d.ts +20 -0
- package/dist/worker/durable-local-admission.js +87 -0
- package/dist/worker/entry.shared.browser.d.ts +1 -0
- package/dist/worker/entry.shared.browser.js +211 -0
- package/dist/worker/factory.browser.d.ts +2 -0
- package/dist/worker/factory.browser.js +65 -1
- package/dist/worker/factory.node.d.ts +1 -0
- package/dist/worker/factory.node.js +3 -0
- package/dist/worker/host.d.ts +4 -2
- package/dist/worker/host.js +86 -32
- package/dist/worker/protocol.d.ts +25 -8
- package/dist/worker/protocol.js +25 -7
- package/dist/worker/shared-admission.d.ts +26 -0
- package/dist/worker/shared-admission.js +116 -0
- package/dist/worker/shared-engine.d.ts +34 -0
- package/dist/worker/shared-engine.js +194 -0
- package/package.json +21 -9
|
@@ -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(
|
|
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
|
+
}
|
package/dist/worker/host.d.ts
CHANGED
|
@@ -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):
|
|
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;
|
package/dist/worker/host.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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);
|
|
@@ -236,6 +254,10 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
|
|
|
236
254
|
return requiredLix(sessionId).exportReplicaRecovery(operation.id);
|
|
237
255
|
case "recoverReplica":
|
|
238
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();
|
|
239
261
|
case "activeBranchId":
|
|
240
262
|
return requiredLix(sessionId).activeBranchId();
|
|
241
263
|
case "activeAccountId":
|
|
@@ -328,7 +350,46 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
|
|
|
328
350
|
}
|
|
329
351
|
await input.writer.close();
|
|
330
352
|
}
|
|
331
|
-
|
|
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) {
|
|
332
393
|
if (!server)
|
|
333
394
|
return undefined;
|
|
334
395
|
return {
|
|
@@ -336,48 +397,37 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
|
|
|
336
397
|
headers: server.headers ?? [],
|
|
337
398
|
headerProvider: server.dynamicHeaders
|
|
338
399
|
? () => {
|
|
400
|
+
if (closed)
|
|
401
|
+
throw workerStateError("Worker client disconnected");
|
|
339
402
|
const requestId = nextSyncRequestId++;
|
|
340
403
|
return new Promise((resolve, reject) => {
|
|
341
404
|
pendingSyncHeaders.set(requestId, { resolve, reject });
|
|
342
|
-
endpoint.postMessage({ kind: "sync.headers", requestId });
|
|
405
|
+
endpoint.postMessage({ kind: "sync.headers", requestId, transportScope });
|
|
343
406
|
});
|
|
344
407
|
}
|
|
345
408
|
: undefined,
|
|
346
|
-
|
|
409
|
+
transport: (request) => bridgeFetch(request, transportScope),
|
|
347
410
|
};
|
|
348
411
|
}
|
|
349
|
-
async function bridgeFetch(
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
if (
|
|
354
|
-
(
|
|
355
|
-
!Number.isSafeInteger(responseLimit) ||
|
|
356
|
-
responseLimit <= 0)) {
|
|
357
|
-
throw new TypeError("Browser sync fetch has no valid response limit");
|
|
358
|
-
}
|
|
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");
|
|
359
418
|
const requestId = nextSyncRequestId++;
|
|
360
419
|
const requestBase = {
|
|
361
|
-
url:
|
|
362
|
-
? input
|
|
363
|
-
: input instanceof URL
|
|
364
|
-
? input.toString()
|
|
365
|
-
: input.url,
|
|
420
|
+
url: input,
|
|
366
421
|
method: init?.method ?? "GET",
|
|
367
422
|
headers: headerEntries(init?.headers),
|
|
368
423
|
body: serializableBody(init?.body),
|
|
369
424
|
credentials: init?.credentials,
|
|
425
|
+
cache: init?.cache, redirect: init?.redirect,
|
|
370
426
|
};
|
|
371
|
-
const request =
|
|
372
|
-
? { ...requestBase, responseMode: "stream" }
|
|
373
|
-
: {
|
|
374
|
-
...requestBase,
|
|
375
|
-
responseMode: "buffered",
|
|
376
|
-
responseLimit: responseLimit,
|
|
377
|
-
};
|
|
427
|
+
const request = { ...requestBase, response: policy };
|
|
378
428
|
const response = new Promise((resolve, reject) => {
|
|
379
429
|
pendingSyncFetch.set(requestId, { resolve, reject });
|
|
380
|
-
endpoint.postMessage({ kind: "sync.fetch", requestId, request });
|
|
430
|
+
endpoint.postMessage({ kind: "sync.fetch", requestId, request, transportScope });
|
|
381
431
|
});
|
|
382
432
|
const abort = () => {
|
|
383
433
|
const pending = pendingSyncFetch.get(requestId);
|
|
@@ -469,6 +519,10 @@ export function startWorkerHost(endpoint, openBinding = openLixBinding) {
|
|
|
469
519
|
// serialized finite lane. Each `observe.next` supplies telemetry directly
|
|
470
520
|
// to its observation binding.
|
|
471
521
|
const events = await requiredLix(sessionId).observe(sql, params);
|
|
522
|
+
if (closed) {
|
|
523
|
+
events.close();
|
|
524
|
+
throw workerStateError("Worker client disconnected");
|
|
525
|
+
}
|
|
472
526
|
const observeId = nextObserveId++;
|
|
473
527
|
observations.set(observeId, events);
|
|
474
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
|
-
|
|
17
|
-
|
|
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
|
} | {
|
|
@@ -92,6 +99,13 @@ export type WorkerOperation = {
|
|
|
92
99
|
} | {
|
|
93
100
|
kind: "recoverReplica";
|
|
94
101
|
id: string;
|
|
102
|
+
} | {
|
|
103
|
+
kind: "recoverReplicaWithServer";
|
|
104
|
+
id: string;
|
|
105
|
+
server: WorkerSyncServerOptions;
|
|
106
|
+
transportScope: number;
|
|
107
|
+
} | {
|
|
108
|
+
kind: "syncHealth";
|
|
95
109
|
} | {
|
|
96
110
|
kind: "activeBranchId";
|
|
97
111
|
} | {
|
|
@@ -193,6 +207,7 @@ export type WorkerHostEndpoint = {
|
|
|
193
207
|
onMessage(listener: (message: WorkerInput) => void): void;
|
|
194
208
|
};
|
|
195
209
|
export type SerializedWorkerError = {
|
|
210
|
+
cause?: SerializedWorkerError;
|
|
196
211
|
name: string;
|
|
197
212
|
message: string;
|
|
198
213
|
stack?: string;
|
|
@@ -217,10 +232,12 @@ export type WorkerResponse = {
|
|
|
217
232
|
} | {
|
|
218
233
|
kind: "sync.headers";
|
|
219
234
|
requestId: number;
|
|
235
|
+
transportScope?: number;
|
|
220
236
|
} | {
|
|
221
237
|
kind: "sync.fetch";
|
|
222
238
|
requestId: number;
|
|
223
239
|
request: WorkerSyncFetchRequest;
|
|
240
|
+
transportScope?: number;
|
|
224
241
|
} | {
|
|
225
242
|
kind: "sync.fetch.stream.pull";
|
|
226
243
|
requestId: number;
|
|
@@ -228,6 +245,6 @@ export type WorkerResponse = {
|
|
|
228
245
|
kind: "sync.fetch.cancel";
|
|
229
246
|
requestId: number;
|
|
230
247
|
};
|
|
231
|
-
export declare function serializeWorkerError(error: unknown): SerializedWorkerError;
|
|
248
|
+
export declare function serializeWorkerError(error: unknown, depth?: number): SerializedWorkerError;
|
|
232
249
|
export declare function deserializeWorkerError(error: SerializedWorkerError): Error;
|
|
233
250
|
export {};
|
package/dist/worker/protocol.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
export function serializeWorkerError(error) {
|
|
1
|
+
export function serializeWorkerError(error, depth = 0) {
|
|
2
2
|
if (!(error instanceof Error)) {
|
|
3
|
-
return { name: "Error", message:
|
|
3
|
+
return { name: "Error", message: "Non-error failure" };
|
|
4
4
|
}
|
|
5
5
|
const lixError = error;
|
|
6
6
|
return {
|
|
7
7
|
name: error.name,
|
|
8
|
-
message: error.message,
|
|
9
|
-
stack: error.stack,
|
|
8
|
+
message: redactDiagnostic(error.message),
|
|
9
|
+
stack: error.stack ? redactDiagnostic(error.stack) : undefined,
|
|
10
10
|
code: typeof lixError.code === "string" ? lixError.code : undefined,
|
|
11
|
-
hint: typeof lixError.hint === "string" ? lixError.hint : undefined,
|
|
12
|
-
details: lixError.details,
|
|
11
|
+
hint: typeof lixError.hint === "string" ? redactDiagnostic(lixError.hint) : undefined,
|
|
12
|
+
details: redactDetails(lixError.details),
|
|
13
|
+
cause: depth < 3 && error.cause !== undefined ? serializeWorkerError(error.cause, depth + 1) : undefined,
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
16
|
export function deserializeWorkerError(error) {
|
|
16
|
-
const restored = new Error(error.message);
|
|
17
|
+
const restored = new Error(error.message, error.cause ? { cause: deserializeWorkerError(error.cause) } : undefined);
|
|
17
18
|
restored.name = error.name;
|
|
18
19
|
restored.stack = error.stack;
|
|
19
20
|
restored.code = error.code;
|
|
@@ -21,3 +22,20 @@ export function deserializeWorkerError(error) {
|
|
|
21
22
|
restored.details = error.details;
|
|
22
23
|
return restored;
|
|
23
24
|
}
|
|
25
|
+
function redactDiagnostic(value) {
|
|
26
|
+
return value.slice(0, 4096).replace(/Bearer\s+[^\s,;"']+/gi, "Bearer [redacted]")
|
|
27
|
+
.replace(/((?:authorization|cookie|token|password|secret)\s*[:=]\s*)[^\n]+/gi, "$1[redacted]");
|
|
28
|
+
}
|
|
29
|
+
function redactDetails(value, depth = 0) {
|
|
30
|
+
if (depth > 3)
|
|
31
|
+
return "[truncated]";
|
|
32
|
+
if (typeof value === "string")
|
|
33
|
+
return redactDiagnostic(value);
|
|
34
|
+
if (value === null || typeof value === "number" || typeof value === "boolean" || value === undefined)
|
|
35
|
+
return value;
|
|
36
|
+
if (Array.isArray(value))
|
|
37
|
+
return value.slice(0, 32).map(item => redactDetails(item, depth + 1));
|
|
38
|
+
if (typeof value === "object")
|
|
39
|
+
return Object.fromEntries(Object.entries(value).slice(0, 32).map(([key, item]) => [key, /authorization|cookie|token|password|secret|headers/i.test(key) ? "[redacted]" : redactDetails(item, depth + 1)]));
|
|
40
|
+
return "[unsupported]";
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type HttpTransport } from "../http-transport.js";
|
|
2
|
+
export declare const ADMISSION_PROTOCOL_EPOCH = 17;
|
|
3
|
+
export declare const ADMISSION_STORAGE_EPOCH = 81;
|
|
4
|
+
export type AdmissionIdentity = {
|
|
5
|
+
repositoryId: string;
|
|
6
|
+
principalId: string;
|
|
7
|
+
protocolEpoch: number;
|
|
8
|
+
storageEpoch: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function sharedCredentialKey(url: string, headers: [string, string][]): string;
|
|
11
|
+
export declare function sameAdmission(a: AdmissionIdentity, b: AdmissionIdentity): boolean;
|
|
12
|
+
/** Authorize attachment, awaiting any server-owned repository upgrade. */
|
|
13
|
+
export declare function requestAdmission(url: string, credentials: [string, string][], transport: HttpTransport): Promise<AdmissionIdentity>;
|
|
14
|
+
/** Memory-only proofs grant cached local attachment, never remote authorization. */
|
|
15
|
+
export declare class SharedAdmissionCache {
|
|
16
|
+
private readonly revocations;
|
|
17
|
+
private readonly proofs;
|
|
18
|
+
record(url: string, headers: [string, string][], identity: AdmissionIdentity): void;
|
|
19
|
+
generation(url: string, headers: [string, string][]): number;
|
|
20
|
+
remove(url: string, headers: [string, string][]): void;
|
|
21
|
+
persistLocal(url: string, headers: [string, string][], generation: number, write: () => Promise<void>, remove: () => Promise<void>): Promise<void>;
|
|
22
|
+
verify(url: string, headers: [string, string][], expected: AdmissionIdentity | undefined, probe: () => Promise<AdmissionIdentity>, allowOffline?: boolean): Promise<{
|
|
23
|
+
identity: AdmissionIdentity;
|
|
24
|
+
online: boolean;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { HttpTransportError } from "../http-transport.js";
|
|
2
|
+
export const ADMISSION_PROTOCOL_EPOCH = 17;
|
|
3
|
+
export const ADMISSION_STORAGE_EPOCH = 81;
|
|
4
|
+
const ANONYMOUS_ACCOUNT_ID = "00000000-0000-7000-8000-000000000002";
|
|
5
|
+
export function sharedCredentialKey(url, headers) {
|
|
6
|
+
const entries = [];
|
|
7
|
+
new Headers(headers).forEach((value, key) => entries.push([key, value]));
|
|
8
|
+
return JSON.stringify([url, entries]);
|
|
9
|
+
}
|
|
10
|
+
export function sameAdmission(a, b) {
|
|
11
|
+
return a.repositoryId === b.repositoryId && a.principalId === b.principalId &&
|
|
12
|
+
a.protocolEpoch === b.protocolEpoch && a.storageEpoch === b.storageEpoch;
|
|
13
|
+
}
|
|
14
|
+
/** Authorize attachment, awaiting any server-owned repository upgrade. */
|
|
15
|
+
export async function requestAdmission(url, credentials, transport) {
|
|
16
|
+
const locator = new URL(url);
|
|
17
|
+
const repositoryId = locator.pathname.match(/\/lix\/([0-9a-f-]{36})\/?$/i)?.[1];
|
|
18
|
+
if (!repositoryId || locator.search || locator.hash || locator.username || locator.password) {
|
|
19
|
+
throw new HttpTransportError("LIX_TRANSPORT_CONTRACT", "Admission requires a repository protocol URL");
|
|
20
|
+
}
|
|
21
|
+
const headers = new Headers(credentials);
|
|
22
|
+
headers.set("lix-sync-protocol-version", String(ADMISSION_PROTOCOL_EPOCH));
|
|
23
|
+
locator.pathname = `/lix/v1/${repositoryId}/admission`;
|
|
24
|
+
let response;
|
|
25
|
+
for (;;) {
|
|
26
|
+
response = await transport({ url: locator.toString(),
|
|
27
|
+
init: { method: "GET", headers, signal: AbortSignal.timeout(10_000), cache: "no-store", redirect: "error", credentials: "omit" },
|
|
28
|
+
response: { mode: "buffered", maxBytes: 16 * 1024 } });
|
|
29
|
+
if (response.status !== 503)
|
|
30
|
+
break;
|
|
31
|
+
const body = await response.clone().json().catch(() => null);
|
|
32
|
+
if (body?.error?.code !== "LIX_REPOSITORY_MIGRATING")
|
|
33
|
+
break;
|
|
34
|
+
// The authority owns the migration independently of this request. Poll only
|
|
35
|
+
// its explicit in-progress response; other failures retain their semantics.
|
|
36
|
+
await new Promise(resolve => setTimeout(resolve, 1_000));
|
|
37
|
+
}
|
|
38
|
+
if (response.status === 401 || response.status === 403) {
|
|
39
|
+
throw new HttpTransportError("LIX_ADMISSION_AUTH_REJECTED", "Authority rejected repository admission");
|
|
40
|
+
}
|
|
41
|
+
if (response.status === 409 || response.status === 426) {
|
|
42
|
+
throw new HttpTransportError("LIX_ADMISSION_EPOCH", "Repository is incompatible with this client version");
|
|
43
|
+
}
|
|
44
|
+
if (!response.ok)
|
|
45
|
+
throw new HttpTransportError("LIX_ADMISSION_HTTP", `Authority admission returned HTTP ${response.status}`);
|
|
46
|
+
let result;
|
|
47
|
+
try {
|
|
48
|
+
result = await response.json();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new HttpTransportError("LIX_ADMISSION_PROTOCOL", "Authority returned invalid admission metadata");
|
|
52
|
+
}
|
|
53
|
+
if (!result || result.repositoryId !== repositoryId ||
|
|
54
|
+
typeof result.principalId !== "string" || !/^[\x21-\x7e]{1,255}$/.test(result.principalId)) {
|
|
55
|
+
throw new HttpTransportError("LIX_ADMISSION_PROTOCOL", "Authority admission identity does not match the repository");
|
|
56
|
+
}
|
|
57
|
+
if (result.protocolEpoch !== ADMISSION_PROTOCOL_EPOCH || result.storageEpoch !== ADMISSION_STORAGE_EPOCH) {
|
|
58
|
+
throw new HttpTransportError("LIX_ADMISSION_EPOCH", "Repository is incompatible with this client version");
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
/** Memory-only proofs grant cached local attachment, never remote authorization. */
|
|
63
|
+
export class SharedAdmissionCache {
|
|
64
|
+
revocations = new Map();
|
|
65
|
+
proofs = new Map();
|
|
66
|
+
record(url, headers, identity) {
|
|
67
|
+
// Invisible cookies/custom-fetch identity cannot be a credential proof.
|
|
68
|
+
if (!new Headers(headers).get("authorization") && identity.principalId !== ANONYMOUS_ACCOUNT_ID)
|
|
69
|
+
return;
|
|
70
|
+
if (this.proofs.size >= 64)
|
|
71
|
+
this.proofs.delete(this.proofs.keys().next().value);
|
|
72
|
+
this.proofs.set(sharedCredentialKey(url, headers), { ...identity });
|
|
73
|
+
}
|
|
74
|
+
generation(url, headers) {
|
|
75
|
+
return this.revocations.get(sharedCredentialKey(url, headers)) ?? 0;
|
|
76
|
+
}
|
|
77
|
+
remove(url, headers) {
|
|
78
|
+
const key = sharedCredentialKey(url, headers);
|
|
79
|
+
this.revocations.set(key, this.generation(url, headers) + 1);
|
|
80
|
+
this.proofs.delete(key);
|
|
81
|
+
}
|
|
82
|
+
async persistLocal(url, headers, generation, write, remove) {
|
|
83
|
+
if (this.generation(url, headers) !== generation)
|
|
84
|
+
return;
|
|
85
|
+
await write();
|
|
86
|
+
if (this.generation(url, headers) !== generation)
|
|
87
|
+
await remove();
|
|
88
|
+
}
|
|
89
|
+
async verify(url, headers, expected, probe, allowOffline = true) {
|
|
90
|
+
const generation = this.generation(url, headers);
|
|
91
|
+
let identity;
|
|
92
|
+
let online = true;
|
|
93
|
+
try {
|
|
94
|
+
identity = await probe();
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (!(error instanceof Error) || error.code !== "LIX_TRANSPORT_NETWORK")
|
|
98
|
+
throw error;
|
|
99
|
+
const known = this.proofs.get(sharedCredentialKey(url, headers));
|
|
100
|
+
if (!allowOffline || !known || !expected || !sameAdmission(known, expected)) {
|
|
101
|
+
throw new HttpTransportError("LIX_IDENTITY_UNVERIFIED_OFFLINE", "Repository identity cannot be verified while offline", { cause: error });
|
|
102
|
+
}
|
|
103
|
+
identity = known;
|
|
104
|
+
online = false;
|
|
105
|
+
}
|
|
106
|
+
if (this.generation(url, headers) !== generation) {
|
|
107
|
+
throw new HttpTransportError("LIX_ADMISSION_AUTH_REJECTED", "Credentials were rejected while admission was in flight");
|
|
108
|
+
}
|
|
109
|
+
if (expected && !sameAdmission(identity, expected)) {
|
|
110
|
+
throw new HttpTransportError("LIX_SHARED_ENGINE_IDENTITY_MISMATCH", "Shared engine repository/account does not match this client");
|
|
111
|
+
}
|
|
112
|
+
if (online)
|
|
113
|
+
this.record(url, headers, identity);
|
|
114
|
+
return { identity, online };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { LixBinding, SyncServerBindingOptions, TelemetryDispatch, TelemetryParentContext, OpenProgressDispatch } from "../binding-types.js";
|
|
2
|
+
export type SharedEngineClient = {
|
|
3
|
+
server: SyncServerBindingOptions;
|
|
4
|
+
isDisconnected?(): boolean;
|
|
5
|
+
telemetry?: TelemetryDispatch;
|
|
6
|
+
parent?: TelemetryParentContext;
|
|
7
|
+
progress?: OpenProgressDispatch;
|
|
8
|
+
commitIdentity?(): void | Promise<void>;
|
|
9
|
+
rejectCredentials?(headers: [string, string][]): void | Promise<void>;
|
|
10
|
+
verifyIdentity(): Promise<{
|
|
11
|
+
authorityUrl: string;
|
|
12
|
+
accountId: string;
|
|
13
|
+
headers: [string, string][];
|
|
14
|
+
online?: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
/** One physical owner; ports receive independent sessions, never the root. */
|
|
18
|
+
export declare class SharedEngineOwner {
|
|
19
|
+
private readonly open;
|
|
20
|
+
private root;
|
|
21
|
+
private principalId;
|
|
22
|
+
private state;
|
|
23
|
+
get lifecycleState(): "closed" | "ready" | "closing" | "opening" | "migration-exclusive";
|
|
24
|
+
private readonly clients;
|
|
25
|
+
private queue;
|
|
26
|
+
constructor(open: (server: SyncServerBindingOptions, telemetry: TelemetryDispatch, client: SharedEngineClient) => Promise<LixBinding>);
|
|
27
|
+
private readonly backgroundTelemetry;
|
|
28
|
+
attach(client: SharedEngineClient): Promise<LixBinding>;
|
|
29
|
+
/** Serialize closed-storage conversion with root admission across all ports. */
|
|
30
|
+
convert(client: SharedEngineClient, conversion: (server: SyncServerBindingOptions) => Promise<void>, branchId?: string): Promise<void>;
|
|
31
|
+
deactivate(client: SharedEngineClient): void;
|
|
32
|
+
detach(client: SharedEngineClient): Promise<void>;
|
|
33
|
+
private transport;
|
|
34
|
+
}
|