@lix-js/sdk 0.10.0 → 0.12.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 +49 -78
- package/dist/binding-types.d.ts +9 -18
- package/dist/binding.browser.js +18 -5
- package/dist/binding.node.js +5 -11
- package/dist/bundled-plugins/plugin_csv.lixplugin +0 -0
- package/dist/bundled-plugins/plugin_markdown.lixplugin +0 -0
- package/dist/errors.d.ts +0 -2
- package/dist/errors.js +0 -13
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/indexeddb-backend.d.ts +19 -0
- package/dist/indexeddb-backend.js +121 -0
- package/dist/lix.d.ts +3 -6
- package/dist/lix.js +28 -54
- package/dist/open-lix.d.ts +4 -13
- package/dist/open-lix.js +45 -152
- package/dist/remote/client.d.ts +2 -3
- package/dist/remote/client.js +50 -23
- package/dist/remote/{protocol.d.ts → server-protocol.d.ts} +40 -32
- package/dist/remote/{protocol.js → server-protocol.js} +36 -17
- package/dist/result.d.ts +2 -1
- package/dist/result.js +12 -0
- package/dist/storage-adapter.d.ts +30 -0
- package/dist/storage-adapter.js +12 -0
- package/dist/types.d.ts +25 -27
- package/dist/value.d.ts +2 -1
- package/dist/value.js +23 -10
- package/dist/wasm/lix_js_sdk.d.ts +5 -10
- package/dist/wasm/lix_js_sdk.js +33 -45
- package/dist/wasm/lix_js_sdk_bg.wasm +0 -0
- package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +3 -6
- package/dist/worker/client.d.ts +1 -12
- package/dist/worker/client.js +0 -161
- package/dist/worker/host.js +0 -23
- package/dist/worker/protocol.d.ts +1 -15
- package/package.json +8 -12
- package/dist/client-state.d.ts +0 -53
- package/dist/client-state.js +0 -318
- package/dist/local-storage-adapter.d.ts +0 -26
- package/dist/local-storage-adapter.js +0 -117
- package/dist/snapshot-persistence.d.ts +0 -7
- package/dist/snapshot-persistence.js +0 -26
package/dist/lix.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { invalidArgument } from "./errors.js";
|
|
2
|
-
import {
|
|
3
|
-
import { normalizeOptionals, wrapExecuteResult } from "./result.js";
|
|
4
|
-
import { isSnapshotPersistenceAfterCommitError } from "./snapshot-persistence.js";
|
|
2
|
+
import { normalizeOptionals, wrapExecuteBatchResult, wrapExecuteResult, } from "./result.js";
|
|
5
3
|
import { normalizeParam, toNativeValue } from "./value.js";
|
|
6
4
|
const transactionFinalizer = new FinalizationRegistry(({ transaction, onFinish }) => {
|
|
7
5
|
void transaction
|
|
@@ -17,9 +15,7 @@ const observeFinalizer = new FinalizationRegistry(({ observe, onClose }) => {
|
|
|
17
15
|
});
|
|
18
16
|
export class Lix {
|
|
19
17
|
binding;
|
|
20
|
-
managedClientState;
|
|
21
18
|
closePromise;
|
|
22
|
-
clientState;
|
|
23
19
|
#activeBranchListeners = new Set();
|
|
24
20
|
#inFlightOperations = new Set();
|
|
25
21
|
#observations = new Map();
|
|
@@ -27,20 +23,8 @@ export class Lix {
|
|
|
27
23
|
#transactionsOpening = 0;
|
|
28
24
|
#activeTransactions = 0;
|
|
29
25
|
#acceptingOperations = true;
|
|
30
|
-
constructor(binding
|
|
26
|
+
constructor(binding) {
|
|
31
27
|
this.binding = binding;
|
|
32
|
-
this.managedClientState = managedClientState;
|
|
33
|
-
this.clientState = managedClientState
|
|
34
|
-
? {
|
|
35
|
-
get: (key) => managedClientState.get(key),
|
|
36
|
-
set: (key, value) => this.#runOperation(() => managedClientState.set(key, value)),
|
|
37
|
-
delete: (key) => this.#runOperation(() => managedClientState.delete(key)),
|
|
38
|
-
subscribe: (listener) => {
|
|
39
|
-
this.#assertAcceptingOperations();
|
|
40
|
-
return managedClientState.subscribe(listener);
|
|
41
|
-
},
|
|
42
|
-
}
|
|
43
|
-
: unavailableClientState();
|
|
44
28
|
}
|
|
45
29
|
async execute(sql, params = [], options) {
|
|
46
30
|
assertExecuteArgs("lix", sql, params, options);
|
|
@@ -50,7 +34,7 @@ export class Lix {
|
|
|
50
34
|
const normalizedStatements = normalizeBatchStatements(statements, options);
|
|
51
35
|
return this.#runOperation(async () => {
|
|
52
36
|
const results = await this.binding.executeBatch(normalizedStatements, options);
|
|
53
|
-
return results.map(
|
|
37
|
+
return results.map(wrapExecuteBatchResult);
|
|
54
38
|
});
|
|
55
39
|
}
|
|
56
40
|
observe(sql, params = []) {
|
|
@@ -110,16 +94,6 @@ export class Lix {
|
|
|
110
94
|
async switchBranch(options) {
|
|
111
95
|
return this.#runOperation(async () => {
|
|
112
96
|
const receipt = await this.binding.switchBranch(options);
|
|
113
|
-
try {
|
|
114
|
-
if (this.managedClientState) {
|
|
115
|
-
await this.managedClientState.set(ACTIVE_BRANCH_CLIENT_STATE_KEY, receipt.branchId);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
// The remote branch switch already committed. Client persistence is a
|
|
120
|
-
// best-effort reopen preference and cannot turn that success into a
|
|
121
|
-
// rejected switch with ambiguous branch state.
|
|
122
|
-
}
|
|
123
97
|
for (const listener of [...this.#activeBranchListeners]) {
|
|
124
98
|
try {
|
|
125
99
|
listener();
|
|
@@ -155,9 +129,13 @@ export class Lix {
|
|
|
155
129
|
this.#observations.clear();
|
|
156
130
|
this.closePromise = (async () => {
|
|
157
131
|
await Promise.allSettled([...this.#inFlightOperations]);
|
|
158
|
-
await
|
|
159
|
-
|
|
132
|
+
const results = await Promise.allSettled([
|
|
133
|
+
Promise.resolve().then(() => this.binding.close()),
|
|
134
|
+
]);
|
|
160
135
|
this.#activeBranchListeners.clear();
|
|
136
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
137
|
+
if (failure)
|
|
138
|
+
throw failure.reason;
|
|
161
139
|
})();
|
|
162
140
|
}
|
|
163
141
|
await this.closePromise;
|
|
@@ -256,31 +234,22 @@ export class LixTransaction {
|
|
|
256
234
|
throw transactionClosedError();
|
|
257
235
|
if (!this.finishPromise) {
|
|
258
236
|
this.finishPromise = (async () => {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
237
|
+
try {
|
|
238
|
+
if (kind === "transaction.commit")
|
|
239
|
+
await this.binding.commit();
|
|
240
|
+
else
|
|
241
|
+
await this.binding.rollback();
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
// A terminal binding call consumes the underlying transaction even
|
|
245
|
+
// when its durable commit or rollback reports an error.
|
|
246
|
+
this.finished = true;
|
|
247
|
+
transactionFinalizer.unregister(this);
|
|
248
|
+
this.onFinish();
|
|
249
|
+
}
|
|
266
250
|
})();
|
|
267
251
|
}
|
|
268
|
-
|
|
269
|
-
await this.finishPromise;
|
|
270
|
-
}
|
|
271
|
-
catch (error) {
|
|
272
|
-
if (isSnapshotPersistenceAfterCommitError(error)) {
|
|
273
|
-
// The transaction finished in Rust; only durable snapshot saving
|
|
274
|
-
// failed. Release the transaction lifecycle while reporting that
|
|
275
|
-
// durability failure to the caller.
|
|
276
|
-
this.finished = true;
|
|
277
|
-
transactionFinalizer.unregister(this);
|
|
278
|
-
this.onFinish();
|
|
279
|
-
throw error;
|
|
280
|
-
}
|
|
281
|
-
this.finishPromise = undefined;
|
|
282
|
-
throw error;
|
|
283
|
-
}
|
|
252
|
+
await this.finishPromise;
|
|
284
253
|
}
|
|
285
254
|
}
|
|
286
255
|
function transactionClosedError() {
|
|
@@ -336,9 +305,14 @@ function normalizeBatchStatements(statements, options) {
|
|
|
336
305
|
if (!Array.isArray(params)) {
|
|
337
306
|
throw invalidArgument("executeBatch", `statements[${statementIndex}].params`, "array", typeof params);
|
|
338
307
|
}
|
|
308
|
+
if (statement.label !== undefined &&
|
|
309
|
+
typeof statement.label !== "string") {
|
|
310
|
+
throw invalidArgument("executeBatch", `statements[${statementIndex}].label`, "string", typeof statement.label);
|
|
311
|
+
}
|
|
339
312
|
return {
|
|
340
313
|
sql: statement.sql,
|
|
341
314
|
params: params.map((param, parameterIndex) => toNativeValue(normalizeParam(param, parameterIndex))),
|
|
315
|
+
...(statement.label === undefined ? {} : { label: statement.label }),
|
|
342
316
|
};
|
|
343
317
|
}
|
|
344
318
|
catch (error) {
|
package/dist/open-lix.d.ts
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
import { Lix } from "./lix.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { IndexedDbStorageOptions, OpenLixOptions } from "./types.js";
|
|
3
3
|
export { Lix, LixTransaction, ObserveEvents } from "./lix.js";
|
|
4
|
-
export declare class
|
|
5
|
-
readonly
|
|
6
|
-
constructor(options:
|
|
7
|
-
}
|
|
8
|
-
export declare class LocalFilesystem {
|
|
9
|
-
readonly path: string;
|
|
10
|
-
readonly lixDir: string | undefined;
|
|
11
|
-
readonly syncAllFiles: boolean;
|
|
12
|
-
constructor(options: LocalFilesystemOptions);
|
|
13
|
-
importPaths(paths: readonly string[]): Promise<void>;
|
|
14
|
-
syncDiskToLix(): Promise<void>;
|
|
15
|
-
private client;
|
|
4
|
+
export declare class IndexedDbStorage {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
constructor(options: IndexedDbStorageOptions);
|
|
16
7
|
}
|
|
17
8
|
export declare function openLix(options?: OpenLixOptions): Promise<Lix>;
|
package/dist/open-lix.js
CHANGED
|
@@ -1,63 +1,15 @@
|
|
|
1
|
-
import { localFilesystemAlreadyOpen, localFilesystemNotOpen, } from "./errors.js";
|
|
2
|
-
import { ACTIVE_ACCOUNT_CLIENT_STATE_KEY, ACTIVE_BRANCH_CLIENT_STATE_KEY, openClientState, openStoredClientState, } from "./client-state.js";
|
|
3
1
|
import { Lix } from "./lix.js";
|
|
2
|
+
import { isLixStorage } from "./storage-adapter.js";
|
|
4
3
|
export { Lix, LixTransaction, ObserveEvents } from "./lix.js";
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
const openStorages = new WeakSet();
|
|
5
|
+
const openIndexedDbStorageNames = new Set();
|
|
6
|
+
export class IndexedDbStorage {
|
|
7
|
+
name;
|
|
7
8
|
constructor(options) {
|
|
8
|
-
if (!options ||
|
|
9
|
-
|
|
10
|
-
options.path.length === 0) {
|
|
11
|
-
throw new TypeError("SQLite requires a non-empty path");
|
|
9
|
+
if (!options || typeof options.name !== "string" || options.name.length === 0) {
|
|
10
|
+
throw new TypeError("IndexedDbStorage requires a non-empty name");
|
|
12
11
|
}
|
|
13
|
-
this.
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
const openLocalFilesystems = new WeakMap();
|
|
17
|
-
export class LocalFilesystem {
|
|
18
|
-
path;
|
|
19
|
-
lixDir;
|
|
20
|
-
syncAllFiles;
|
|
21
|
-
constructor(options) {
|
|
22
|
-
if (!options ||
|
|
23
|
-
typeof options.path !== "string" ||
|
|
24
|
-
options.path.length === 0) {
|
|
25
|
-
throw new TypeError("LocalFilesystem requires a non-empty path");
|
|
26
|
-
}
|
|
27
|
-
if (options.lixDir !== undefined &&
|
|
28
|
-
(typeof options.lixDir !== "string" || options.lixDir.length === 0)) {
|
|
29
|
-
throw new TypeError("LocalFilesystem lixDir must be a non-empty string");
|
|
30
|
-
}
|
|
31
|
-
if (typeof options.syncAllFiles !== "boolean") {
|
|
32
|
-
throw new TypeError("LocalFilesystem syncAllFiles must be a boolean");
|
|
33
|
-
}
|
|
34
|
-
this.path = options.path;
|
|
35
|
-
this.lixDir = options.lixDir;
|
|
36
|
-
this.syncAllFiles = options.syncAllFiles;
|
|
37
|
-
}
|
|
38
|
-
async importPaths(paths) {
|
|
39
|
-
if (!Array.isArray(paths)) {
|
|
40
|
-
throw new TypeError("importPaths() paths must be an array");
|
|
41
|
-
}
|
|
42
|
-
for (const path of paths) {
|
|
43
|
-
if (typeof path !== "string" || path.length === 0) {
|
|
44
|
-
throw new TypeError("importPaths() paths must contain non-empty strings");
|
|
45
|
-
}
|
|
46
|
-
if (path.endsWith("/")) {
|
|
47
|
-
throw new TypeError("importPaths() paths must not end with a trailing slash");
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
await this.client("importPaths").importFilesystemPaths([...paths]);
|
|
51
|
-
}
|
|
52
|
-
async syncDiskToLix() {
|
|
53
|
-
return this.client("syncDiskToLix").syncDiskToLix();
|
|
54
|
-
}
|
|
55
|
-
client(operation) {
|
|
56
|
-
const client = openLocalFilesystems.get(this);
|
|
57
|
-
if (!client) {
|
|
58
|
-
throw localFilesystemNotOpen(operation);
|
|
59
|
-
}
|
|
60
|
-
return client;
|
|
12
|
+
this.name = options.name;
|
|
61
13
|
}
|
|
62
14
|
}
|
|
63
15
|
export async function openLix(options = {}) {
|
|
@@ -74,122 +26,63 @@ export async function openLix(options = {}) {
|
|
|
74
26
|
}
|
|
75
27
|
if (options.server !== undefined) {
|
|
76
28
|
const { openRemoteLixBinding } = await import("./remote/client.js");
|
|
77
|
-
if (options.storage
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
assertSnapshotStorage(options.storage);
|
|
81
|
-
const clientState = await openStoredClientState({
|
|
82
|
-
storage: options.storage,
|
|
83
|
-
namespace: remoteClientStateNamespace(options.server.url),
|
|
84
|
-
});
|
|
85
|
-
const restoredBranchId = clientState.get(ACTIVE_BRANCH_CLIENT_STATE_KEY);
|
|
86
|
-
const restoredAccountId = clientState.get(ACTIVE_ACCOUNT_CLIENT_STATE_KEY);
|
|
87
|
-
let remoteBinding;
|
|
88
|
-
try {
|
|
89
|
-
try {
|
|
90
|
-
remoteBinding = await openRemoteLixBinding(options.server, {
|
|
91
|
-
initialActiveBranchId: restoredBranchId,
|
|
92
|
-
initialActiveAccountId: restoredAccountId,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
if (!restoredBranchId || !isBranchNotFoundError(error))
|
|
97
|
-
throw error;
|
|
98
|
-
remoteBinding = await openRemoteLixBinding(options.server, {
|
|
99
|
-
initialActiveAccountId: restoredAccountId,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
const activeBranchId = await remoteBinding.activeBranchId();
|
|
103
|
-
const activeAccountId = await remoteBinding.activeAccountId();
|
|
104
|
-
if (activeBranchId !== restoredBranchId) {
|
|
105
|
-
await clientState.set(ACTIVE_BRANCH_CLIENT_STATE_KEY, activeBranchId);
|
|
106
|
-
}
|
|
107
|
-
if (activeAccountId !== restoredAccountId) {
|
|
108
|
-
await clientState.set(ACTIVE_ACCOUNT_CLIENT_STATE_KEY, activeAccountId);
|
|
109
|
-
}
|
|
110
|
-
return new Lix(remoteBinding, clientState);
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
await remoteBinding?.close().catch(() => undefined);
|
|
114
|
-
await clientState.close().catch(() => undefined);
|
|
115
|
-
throw error;
|
|
29
|
+
if ("storage" in options && options.storage !== undefined) {
|
|
30
|
+
throw new TypeError("openLix() remote mode does not accept storage");
|
|
116
31
|
}
|
|
32
|
+
return new Lix(await openRemoteLixBinding(options.server));
|
|
117
33
|
}
|
|
118
34
|
const { openLixWorkerBinding } = await import("./worker/client.js");
|
|
119
35
|
if (options.storage === undefined) {
|
|
120
36
|
return new Lix(await openLixWorkerBinding({ kind: "memory" }, undefined, options.telemetry));
|
|
121
37
|
}
|
|
122
|
-
if (options.storage
|
|
123
|
-
return new Lix(await openLixWorkerBinding({
|
|
124
|
-
kind: "sqlite",
|
|
125
|
-
path: options.storage.path,
|
|
126
|
-
}, undefined, options.telemetry));
|
|
127
|
-
}
|
|
128
|
-
if (options.storage instanceof LocalFilesystem) {
|
|
38
|
+
if (isLixStorage(options.storage)) {
|
|
129
39
|
const storage = options.storage;
|
|
130
|
-
if (
|
|
131
|
-
throw
|
|
40
|
+
if (openStorages.has(storage)) {
|
|
41
|
+
throw storageAlreadyOpen();
|
|
132
42
|
}
|
|
133
|
-
|
|
43
|
+
openStorages.add(storage);
|
|
44
|
+
let binding;
|
|
45
|
+
const disconnect = () => {
|
|
46
|
+
storage.lixStorage.connect(undefined);
|
|
47
|
+
openStorages.delete(storage);
|
|
48
|
+
};
|
|
134
49
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}, () => openLocalFilesystems.delete(storage), options.telemetry);
|
|
141
|
-
openLocalFilesystems.set(storage, binding);
|
|
50
|
+
binding = await openLixWorkerBinding(storage.lixStorage.config, disconnect, options.telemetry);
|
|
51
|
+
storage.lixStorage.connect({
|
|
52
|
+
importFilesystemPaths: (paths) => binding.importFilesystemPaths(paths),
|
|
53
|
+
syncDiskToLix: () => binding.syncDiskToLix(),
|
|
54
|
+
});
|
|
142
55
|
return new Lix(binding);
|
|
143
56
|
}
|
|
144
57
|
catch (error) {
|
|
145
|
-
|
|
58
|
+
disconnect();
|
|
59
|
+
await binding?.close().catch(() => undefined);
|
|
146
60
|
throw error;
|
|
147
61
|
}
|
|
148
62
|
}
|
|
149
|
-
if (
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
63
|
+
if (options.storage instanceof IndexedDbStorage) {
|
|
64
|
+
const storage = options.storage;
|
|
65
|
+
const databaseName = storage.name;
|
|
66
|
+
if (openIndexedDbStorageNames.has(databaseName)) {
|
|
67
|
+
throw new Error("IndexedDbStorage is already open");
|
|
68
|
+
}
|
|
69
|
+
openIndexedDbStorageNames.add(databaseName);
|
|
70
|
+
let binding;
|
|
156
71
|
try {
|
|
157
|
-
|
|
158
|
-
return new Lix(binding
|
|
72
|
+
binding = await openLixWorkerBinding({ kind: "indexedDb", name: databaseName }, () => openIndexedDbStorageNames.delete(databaseName), options.telemetry);
|
|
73
|
+
return new Lix(binding);
|
|
159
74
|
}
|
|
160
75
|
catch (error) {
|
|
161
|
-
|
|
76
|
+
openIndexedDbStorageNames.delete(databaseName);
|
|
77
|
+
await binding?.close().catch(() => undefined);
|
|
162
78
|
throw error;
|
|
163
79
|
}
|
|
164
80
|
}
|
|
165
|
-
throw new TypeError("openLix() requires
|
|
166
|
-
}
|
|
167
|
-
function isSnapshotStorage(value) {
|
|
168
|
-
return (typeof value === "object" &&
|
|
169
|
-
value !== null &&
|
|
170
|
-
typeof value.load === "function" &&
|
|
171
|
-
typeof value.save === "function");
|
|
172
|
-
}
|
|
173
|
-
function assertSnapshotStorage(value) {
|
|
174
|
-
if (!isSnapshotStorage(value)) {
|
|
175
|
-
throw new TypeError("openLix() remote storage must implement load() and save()");
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
function remoteClientStateNamespace(value) {
|
|
179
|
-
let url;
|
|
180
|
-
try {
|
|
181
|
-
url = new URL(value);
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
throw new TypeError("openLix() remote server url must be an absolute URL");
|
|
185
|
-
}
|
|
186
|
-
url.pathname = url.pathname.replace(/\/$/, "");
|
|
187
|
-
url.search = "";
|
|
188
|
-
url.hash = "";
|
|
189
|
-
return `remote:${url.href}`;
|
|
81
|
+
throw new TypeError("openLix() requires a Lix storage adapter or IndexedDbStorage");
|
|
190
82
|
}
|
|
191
|
-
function
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
83
|
+
function storageAlreadyOpen() {
|
|
84
|
+
const error = new Error("openLix() storage is already open; close the existing Lix or create a new storage adapter");
|
|
85
|
+
error.name = "LixError";
|
|
86
|
+
error.code = "LIX_STORAGE_IN_USE";
|
|
87
|
+
return error;
|
|
195
88
|
}
|
package/dist/remote/client.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { LixBinding } from "../binding-types.js";
|
|
2
2
|
import type { RemoteLixServerOptions } from "../types.js";
|
|
3
|
-
import { type
|
|
3
|
+
import { type ServerProtocolHandshakeRequest } from "./server-protocol.js";
|
|
4
4
|
type RemoteLixClientOptions = {
|
|
5
|
-
initialActiveBranchId?:
|
|
6
|
-
initialActiveAccountId?: RemoteHandshakeRequest["activeAccountId"];
|
|
5
|
+
initialActiveBranchId?: ServerProtocolHandshakeRequest["activeBranchId"];
|
|
7
6
|
};
|
|
8
7
|
export declare function openRemoteLixBinding(options: RemoteLixServerOptions, clientOptions?: RemoteLixClientOptions): Promise<LixBinding>;
|
|
9
8
|
export {};
|
package/dist/remote/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeExecuteResult, decodeHandshake, decodeObserveEvent, encodeWireValue, errorFromResponseBody, protocolError, record,
|
|
1
|
+
import { decodeExecuteBatchResult, decodeExecuteResult, decodeHandshake, decodeObserveEvent, encodeWireValue, errorFromResponseBody, protocolError, record, SERVER_PROTOCOL_PATH, remoteError, } from "./server-protocol.js";
|
|
2
2
|
import { readSseEvents } from "./sse.js";
|
|
3
3
|
const OBSERVE_RETRY_BASE_MS = 100;
|
|
4
4
|
const OBSERVE_RETRY_MAX_MS = 5_000;
|
|
@@ -32,7 +32,6 @@ class RemoteLixBinding {
|
|
|
32
32
|
#fetch;
|
|
33
33
|
#headers;
|
|
34
34
|
#initialActiveBranchId;
|
|
35
|
-
#initialActiveAccountId;
|
|
36
35
|
#observationHub;
|
|
37
36
|
#requestBlobBases = new Map();
|
|
38
37
|
#sessionId;
|
|
@@ -66,13 +65,9 @@ class RemoteLixBinding {
|
|
|
66
65
|
throw new TypeError("initialActiveBranchId must be a non-empty string");
|
|
67
66
|
}
|
|
68
67
|
this.#initialActiveBranchId = clientOptions.initialActiveBranchId;
|
|
69
|
-
if (clientOptions.initialActiveAccountId !== undefined &&
|
|
70
|
-
clientOptions.initialActiveAccountId.length === 0) {
|
|
71
|
-
throw new TypeError("initialActiveAccountId must be a non-empty string");
|
|
72
|
-
}
|
|
73
|
-
this.#initialActiveAccountId = clientOptions.initialActiveAccountId;
|
|
74
68
|
this.#observationHub = new RemoteObservationHub({
|
|
75
69
|
openStream: (subscriptions, signal) => this.#requestObserveStream(subscriptions, signal),
|
|
70
|
+
refreshObservation: (subscription) => this.#refreshObservation(subscription),
|
|
76
71
|
});
|
|
77
72
|
}
|
|
78
73
|
async open() {
|
|
@@ -80,9 +75,6 @@ class RemoteLixBinding {
|
|
|
80
75
|
if (this.#initialActiveBranchId !== undefined) {
|
|
81
76
|
query.set("activeBranchId", this.#initialActiveBranchId);
|
|
82
77
|
}
|
|
83
|
-
if (this.#initialActiveAccountId !== undefined) {
|
|
84
|
-
query.set("activeAccountId", this.#initialActiveAccountId);
|
|
85
|
-
}
|
|
86
78
|
const path = query.size === 0 ? "" : `?${query}`;
|
|
87
79
|
const handshake = decodeHandshake(await this.#requestJson(path, { method: "GET" }));
|
|
88
80
|
this.#sessionId = handshake.sessionId;
|
|
@@ -119,10 +111,12 @@ class RemoteLixBinding {
|
|
|
119
111
|
const snapshot = statements.map((statement) => ({
|
|
120
112
|
sql: statement.sql,
|
|
121
113
|
params: snapshotParams(statement.params),
|
|
114
|
+
...(statement.label === undefined ? {} : { label: statement.label }),
|
|
122
115
|
}));
|
|
123
116
|
return this.#enqueue(async () => {
|
|
124
117
|
const preparedStatements = await Promise.all(snapshot.map(async (statement, statementIndex) => ({
|
|
125
118
|
sql: statement.sql,
|
|
119
|
+
label: statement.label,
|
|
126
120
|
prepared: await this.#prepareParams(statement.params, (paramIndex) => requestBlobSlot("batch", statement.sql, paramIndex, statementIndex)),
|
|
127
121
|
})));
|
|
128
122
|
const cacheBlobs = preparedStatements.some((statement) => statement.prepared.cacheBlobs);
|
|
@@ -133,6 +127,9 @@ class RemoteLixBinding {
|
|
|
133
127
|
body: JSON.stringify({
|
|
134
128
|
statements: preparedStatements.map((statement) => ({
|
|
135
129
|
sql: statement.sql,
|
|
130
|
+
...(statement.label === undefined
|
|
131
|
+
? {}
|
|
132
|
+
: { label: statement.label }),
|
|
136
133
|
params: full
|
|
137
134
|
? statement.prepared.fullParams()
|
|
138
135
|
: statement.prepared.params,
|
|
@@ -145,7 +142,7 @@ class RemoteLixBinding {
|
|
|
145
142
|
if (!Array.isArray(value)) {
|
|
146
143
|
throw protocolError("execute batch response must be an array");
|
|
147
144
|
}
|
|
148
|
-
const results = value.map(
|
|
145
|
+
const results = value.map(decodeExecuteBatchResult);
|
|
149
146
|
this.#commitRequestBlobBases(preparedStatements.flatMap((statement) => statement.prepared.cacheUpdates));
|
|
150
147
|
return results;
|
|
151
148
|
});
|
|
@@ -219,7 +216,7 @@ class RemoteLixBinding {
|
|
|
219
216
|
if (this.#activeBranchId === undefined) {
|
|
220
217
|
const handshake = decodeHandshake(await this.#requestJson("", { method: "GET" }));
|
|
221
218
|
if (handshake.sessionId !== this.#sessionId) {
|
|
222
|
-
throw protocolError("
|
|
219
|
+
throw protocolError("Lix Server Protocol handshake changed sessionId");
|
|
223
220
|
}
|
|
224
221
|
this.#activeBranchId = handshake.activeBranchId;
|
|
225
222
|
}
|
|
@@ -232,7 +229,7 @@ class RemoteLixBinding {
|
|
|
232
229
|
if (this.#activeAccountId === undefined) {
|
|
233
230
|
const handshake = decodeHandshake(await this.#requestJson("", { method: "GET" }));
|
|
234
231
|
if (handshake.sessionId !== this.#sessionId) {
|
|
235
|
-
throw protocolError("
|
|
232
|
+
throw protocolError("Lix Server Protocol handshake changed sessionId");
|
|
236
233
|
}
|
|
237
234
|
this.#activeAccountId = handshake.activeAccountId;
|
|
238
235
|
}
|
|
@@ -442,6 +439,18 @@ class RemoteLixBinding {
|
|
|
442
439
|
throw remoteError("LIX_REMOTE_UNAVAILABLE", "The remote Lix observation stream is unavailable", { details: { cause: errorMessage(cause) } });
|
|
443
440
|
}
|
|
444
441
|
}
|
|
442
|
+
async #refreshObservation(subscription) {
|
|
443
|
+
return this.#enqueue(async () => {
|
|
444
|
+
const value = await this.#requestJson("execute", {
|
|
445
|
+
method: "POST",
|
|
446
|
+
body: JSON.stringify({
|
|
447
|
+
sql: subscription.sql,
|
|
448
|
+
params: subscription.params,
|
|
449
|
+
}),
|
|
450
|
+
});
|
|
451
|
+
return decodeExecuteResult(value);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
445
454
|
async #prepareParams(params, slot) {
|
|
446
455
|
const prepared = await Promise.all(params.map(async (param, index) => {
|
|
447
456
|
if (param.kind !== "blob" ||
|
|
@@ -661,6 +670,7 @@ function copyArrayBuffer(bytes) {
|
|
|
661
670
|
}
|
|
662
671
|
class RemoteObservationHub {
|
|
663
672
|
#openStream;
|
|
673
|
+
#refreshObservation;
|
|
664
674
|
#observations = new Map();
|
|
665
675
|
#nextObservationId = 0;
|
|
666
676
|
#controller;
|
|
@@ -672,6 +682,7 @@ class RemoteObservationHub {
|
|
|
672
682
|
#closed = false;
|
|
673
683
|
constructor(options) {
|
|
674
684
|
this.#openStream = options.openStream;
|
|
685
|
+
this.#refreshObservation = options.refreshObservation;
|
|
675
686
|
}
|
|
676
687
|
observe(sql, params) {
|
|
677
688
|
const id = `observe-${++this.#nextObservationId}`;
|
|
@@ -743,6 +754,7 @@ class RemoteObservationHub {
|
|
|
743
754
|
if (!this.#isCurrent(generation, controller))
|
|
744
755
|
return;
|
|
745
756
|
streamOpened = true;
|
|
757
|
+
const initialSubscriptions = new Set(this.#observations.keys());
|
|
746
758
|
if (!response.ok) {
|
|
747
759
|
if (isRetryableObserveStatus(response.status)) {
|
|
748
760
|
void response.body?.cancel();
|
|
@@ -781,7 +793,21 @@ class RemoteObservationHub {
|
|
|
781
793
|
const transportDelta = payload.delta !== undefined;
|
|
782
794
|
const event = decodeObserveEvent(payload, transportBases.get(subscriptionId));
|
|
783
795
|
transportBases.set(subscriptionId, event);
|
|
784
|
-
|
|
796
|
+
if (initialSubscriptions.delete(subscriptionId)) {
|
|
797
|
+
// The first frame after opening (including a reconnect) is a
|
|
798
|
+
// synchronization point, not an authoritative snapshot. The
|
|
799
|
+
// remote runtime may have observed the stream before its
|
|
800
|
+
// external-storage watcher caught up. Reconcile through the
|
|
801
|
+
// normal execute endpoint before publishing it to consumers.
|
|
802
|
+
const rows = await this.#refreshObservation(observation.request());
|
|
803
|
+
observation.accept({
|
|
804
|
+
...event,
|
|
805
|
+
rows,
|
|
806
|
+
}, false);
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
observation.accept(event, transportDelta);
|
|
810
|
+
}
|
|
785
811
|
this.#retryAttempt = 0;
|
|
786
812
|
}
|
|
787
813
|
catch (error) {
|
|
@@ -1057,7 +1083,7 @@ function isRetryableObserveError(error) {
|
|
|
1057
1083
|
function asObserveProtocolError(error, event) {
|
|
1058
1084
|
if (error instanceof Error &&
|
|
1059
1085
|
"code" in error &&
|
|
1060
|
-
error.code === "
|
|
1086
|
+
error.code === "LIX_SERVER_PROTOCOL_ERROR") {
|
|
1061
1087
|
return error;
|
|
1062
1088
|
}
|
|
1063
1089
|
return protocolError(`remote observe ${event} event contains invalid data: ${errorMessage(error)}`);
|
|
@@ -1084,8 +1110,8 @@ function nativeValuesEqual(left, right) {
|
|
|
1084
1110
|
return (right.kind === "blob" &&
|
|
1085
1111
|
left.blob.length === right.blob.length &&
|
|
1086
1112
|
left.blob.every((byte, index) => byte === right.blob[index]));
|
|
1087
|
-
case "
|
|
1088
|
-
return right.kind === "
|
|
1113
|
+
case "jsonb":
|
|
1114
|
+
return right.kind === "jsonb" && jsonValuesEqual(left.value, right.value);
|
|
1089
1115
|
default:
|
|
1090
1116
|
return left.value === right.value;
|
|
1091
1117
|
}
|
|
@@ -1117,21 +1143,22 @@ function stringArraysEqual(left, right) {
|
|
|
1117
1143
|
left.every((value, index) => value === right[index]));
|
|
1118
1144
|
}
|
|
1119
1145
|
function protocolBaseUrl(value) {
|
|
1120
|
-
let
|
|
1146
|
+
let repositoryUrl;
|
|
1121
1147
|
try {
|
|
1122
|
-
|
|
1148
|
+
repositoryUrl = new URL(value);
|
|
1123
1149
|
}
|
|
1124
1150
|
catch {
|
|
1125
1151
|
throw new TypeError("openLix() remote server url must be an absolute URL");
|
|
1126
1152
|
}
|
|
1127
|
-
if (
|
|
1153
|
+
if (repositoryUrl.protocol !== "http:" &&
|
|
1154
|
+
repositoryUrl.protocol !== "https:") {
|
|
1128
1155
|
throw new TypeError("openLix() remote server url must use http or https");
|
|
1129
1156
|
}
|
|
1130
|
-
if (
|
|
1157
|
+
if (repositoryUrl.search || repositoryUrl.hash) {
|
|
1131
1158
|
throw new TypeError("openLix() remote server url must not contain a query or fragment");
|
|
1132
1159
|
}
|
|
1133
|
-
|
|
1134
|
-
return
|
|
1160
|
+
repositoryUrl.pathname = `${repositoryUrl.pathname.replace(/\/$/, "")}${SERVER_PROTOCOL_PATH}`;
|
|
1161
|
+
return repositoryUrl;
|
|
1135
1162
|
}
|
|
1136
1163
|
function unsupportedRemoteOperation(operation) {
|
|
1137
1164
|
return remoteError("LIX_UNSUPPORTED_REMOTE_OPERATION", `${operation} is not supported in remote mode`, { details: { operation } });
|