@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.
- package/README.md +187 -22
- package/dist/binding-types.d.ts +20 -6
- 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 +20 -10
- package/dist/lix.js +68 -23
- 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 -5
- package/dist/storage-ownership.d.ts +5 -0
- package/dist/storage-ownership.js +4 -0
- package/dist/types.d.ts +132 -22
- package/dist/wasm/lix_js_sdk.d.ts +57 -16
- package/dist/wasm/lix_js_sdk.js +268 -60
- package/dist/wasm/lix_js_sdk_bg.wasm +2 -2
- package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +23 -9
- package/dist/worker/client.d.ts +12 -3
- package/dist/worker/client.js +98 -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 +92 -32
- package/dist/worker/protocol.d.ts +33 -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 -13
- package/dist/remote/server-protocol.d.ts +0 -185
- 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(
|
|
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);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
409
|
+
transport: (request) => bridgeFetch(request, transportScope),
|
|
341
410
|
};
|
|
342
411
|
}
|
|
343
|
-
async function bridgeFetch(
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
if (
|
|
348
|
-
(
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
} | {
|
|
@@ -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 {};
|