@lix-js/sdk 0.12.2 → 0.14.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 +40 -10
- package/dist/binding-types.d.ts +37 -9
- package/dist/binding.browser.d.ts +2 -2
- package/dist/binding.browser.js +25 -10
- package/dist/binding.node-wasm.d.ts +2 -2
- package/dist/binding.node-wasm.js +8 -4
- package/dist/binding.node.d.ts +3 -3
- package/dist/binding.node.js +70 -13
- package/dist/browser-wasm-init.d.ts +14 -0
- package/dist/browser-wasm-init.js +16 -0
- package/dist/bundled-plugins/plugin_csv.lixplugin +0 -0
- package/dist/bundled-plugins/plugin_markdown.lixplugin +0 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/lix.d.ts +27 -5
- package/dist/lix.js +135 -12
- package/dist/open-lix.d.ts +4 -5
- package/dist/open-lix.js +173 -33
- package/dist/remote/client.d.ts +1 -2
- package/dist/remote/client.js +58 -1151
- package/dist/remote/server-protocol.d.ts +5 -6
- package/dist/remote/server-protocol.js +40 -9
- package/dist/result.d.ts +6 -13
- package/dist/result.js +9 -35
- package/dist/snapshot-restore.d.ts +6 -0
- package/dist/snapshot-restore.js +101 -0
- package/dist/storage-adapter.d.ts +175 -19
- package/dist/storage-adapter.js +21 -0
- package/dist/types.d.ts +101 -31
- package/dist/value.d.ts +1 -0
- package/dist/value.js +8 -0
- package/dist/wasm/lix_js_sdk.d.ts +119 -24
- package/dist/wasm/lix_js_sdk.js +599 -78
- package/dist/wasm/lix_js_sdk_bg.wasm +0 -0
- package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +47 -6
- package/dist/worker/client.d.ts +27 -5
- package/dist/worker/client.js +458 -45
- package/dist/worker/factory.browser.d.ts +2 -2
- package/dist/worker/factory.node.d.ts +3 -2
- package/dist/worker/factory.node.js +18 -12
- package/dist/worker/host.d.ts +2 -1
- package/dist/worker/host.js +283 -37
- package/dist/worker/protocol.d.ts +80 -3
- package/package.json +6 -10
- package/dist/indexeddb-backend.d.ts +0 -19
- package/dist/indexeddb-backend.js +0 -121
- package/dist/remote/sse.d.ts +0 -12
- package/dist/remote/sse.js +0 -87
- package/dist/workerd.d.ts +0 -25
- package/dist/workerd.js +0 -35
package/dist/remote/client.js
CHANGED
|
@@ -1,1167 +1,77 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const MAX_COMPRESSION_SAMPLE_RATIO = 0.7;
|
|
24
|
-
const MAX_COMPRESSED_BODY_RATIO = 0.9;
|
|
25
|
-
export async function openRemoteLixBinding(options, clientOptions = {}) {
|
|
26
|
-
const client = new RemoteLixBinding(options, clientOptions);
|
|
27
|
-
await client.open();
|
|
28
|
-
return client;
|
|
29
|
-
}
|
|
30
|
-
class RemoteLixBinding {
|
|
31
|
-
#baseUrl;
|
|
32
|
-
#fetch;
|
|
33
|
-
#headers;
|
|
34
|
-
#initialActiveBranchId;
|
|
35
|
-
#observationHub;
|
|
36
|
-
#requestBlobBases = new Map();
|
|
37
|
-
#sessionId;
|
|
38
|
-
#activeBranchId;
|
|
39
|
-
#activeAccountId;
|
|
40
|
-
#requestBlobBaseBytes = 0;
|
|
41
|
-
#acceptingOperations = true;
|
|
42
|
-
#operationQueue = Promise.resolve();
|
|
43
|
-
#closePromise;
|
|
44
|
-
constructor(options, clientOptions) {
|
|
45
|
-
if (!options || typeof options !== "object") {
|
|
46
|
-
throw new TypeError("openLix() remote server must be an object");
|
|
47
|
-
}
|
|
48
|
-
if (options.mode !== "remote") {
|
|
49
|
-
throw new TypeError("openLix() remote server mode must be 'remote'");
|
|
50
|
-
}
|
|
51
|
-
this.#baseUrl = protocolBaseUrl(options.url);
|
|
52
|
-
const remoteFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
53
|
-
if (typeof remoteFetch !== "function") {
|
|
54
|
-
throw new TypeError("openLix() remote mode requires fetch");
|
|
55
|
-
}
|
|
56
|
-
this.#fetch = remoteFetch;
|
|
57
|
-
if (options.headers !== undefined &&
|
|
58
|
-
typeof options.headers !== "function" &&
|
|
59
|
-
!isHeadersInit(options.headers)) {
|
|
60
|
-
throw new TypeError("openLix() remote server headers must be HeadersInit or a function");
|
|
61
|
-
}
|
|
62
|
-
this.#headers = options.headers;
|
|
63
|
-
if (clientOptions.initialActiveBranchId !== undefined &&
|
|
64
|
-
clientOptions.initialActiveBranchId.length === 0) {
|
|
65
|
-
throw new TypeError("initialActiveBranchId must be a non-empty string");
|
|
66
|
-
}
|
|
67
|
-
this.#initialActiveBranchId = clientOptions.initialActiveBranchId;
|
|
68
|
-
this.#observationHub = new RemoteObservationHub({
|
|
69
|
-
openStream: (subscriptions, signal) => this.#requestObserveStream(subscriptions, signal),
|
|
70
|
-
refreshObservation: (subscription) => this.#refreshObservation(subscription),
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
async open() {
|
|
74
|
-
const query = new URLSearchParams();
|
|
75
|
-
if (this.#initialActiveBranchId !== undefined) {
|
|
76
|
-
query.set("activeBranchId", this.#initialActiveBranchId);
|
|
77
|
-
}
|
|
78
|
-
const path = query.size === 0 ? "" : `?${query}`;
|
|
79
|
-
const handshake = decodeHandshake(await this.#requestJson(path, { method: "GET" }));
|
|
80
|
-
this.#sessionId = handshake.sessionId;
|
|
81
|
-
this.#activeBranchId = handshake.activeBranchId;
|
|
82
|
-
this.#activeAccountId = handshake.activeAccountId;
|
|
83
|
-
}
|
|
84
|
-
async execute(sql, params, options) {
|
|
85
|
-
this.#assertOpen();
|
|
86
|
-
const snapshot = snapshotParams(params);
|
|
87
|
-
const idempotencyKey = idempotencyKeyFor(options?.idempotencyKey);
|
|
88
|
-
const requestOptions = remoteExecuteOptions(options);
|
|
89
|
-
return this.#enqueue(async () => {
|
|
90
|
-
const prepared = await this.#prepareParams(snapshot, (index) => requestBlobSlot("execute", sql, index));
|
|
91
|
-
const request = (params) => this.#requestJson("execute", {
|
|
92
|
-
method: "POST",
|
|
93
|
-
headers: { [IDEMPOTENCY_KEY_HEADER]: idempotencyKey },
|
|
94
|
-
body: JSON.stringify({
|
|
95
|
-
sql,
|
|
96
|
-
params,
|
|
97
|
-
...(requestOptions === undefined ? {} : { options: requestOptions }),
|
|
98
|
-
...(prepared.cacheBlobs ? { cacheBlobs: true } : {}),
|
|
99
|
-
}),
|
|
100
|
-
});
|
|
101
|
-
const value = await requestWithFullBlobFallback(() => request(prepared.params), prepared.hasDelta ? () => request(prepared.fullParams()) : undefined);
|
|
102
|
-
const result = decodeExecuteResult(value);
|
|
103
|
-
this.#commitRequestBlobBases(prepared.cacheUpdates);
|
|
104
|
-
return result;
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
async executeBatch(statements, options) {
|
|
108
|
-
this.#assertOpen();
|
|
109
|
-
const idempotencyKey = idempotencyKeyFor(options?.idempotencyKey);
|
|
110
|
-
const requestOptions = remoteExecuteOptions(options);
|
|
111
|
-
const snapshot = statements.map((statement) => ({
|
|
112
|
-
sql: statement.sql,
|
|
113
|
-
params: snapshotParams(statement.params),
|
|
114
|
-
...(statement.label === undefined ? {} : { label: statement.label }),
|
|
115
|
-
}));
|
|
116
|
-
return this.#enqueue(async () => {
|
|
117
|
-
const preparedStatements = await Promise.all(snapshot.map(async (statement, statementIndex) => ({
|
|
118
|
-
sql: statement.sql,
|
|
119
|
-
label: statement.label,
|
|
120
|
-
prepared: await this.#prepareParams(statement.params, (paramIndex) => requestBlobSlot("batch", statement.sql, paramIndex, statementIndex)),
|
|
121
|
-
})));
|
|
122
|
-
const cacheBlobs = preparedStatements.some((statement) => statement.prepared.cacheBlobs);
|
|
123
|
-
const hasDelta = preparedStatements.some((statement) => statement.prepared.hasDelta);
|
|
124
|
-
const request = (full) => this.#requestJson("execute-batch", {
|
|
125
|
-
method: "POST",
|
|
126
|
-
headers: { [IDEMPOTENCY_KEY_HEADER]: idempotencyKey },
|
|
127
|
-
body: JSON.stringify({
|
|
128
|
-
statements: preparedStatements.map((statement) => ({
|
|
129
|
-
sql: statement.sql,
|
|
130
|
-
...(statement.label === undefined
|
|
131
|
-
? {}
|
|
132
|
-
: { label: statement.label }),
|
|
133
|
-
params: full
|
|
134
|
-
? statement.prepared.fullParams()
|
|
135
|
-
: statement.prepared.params,
|
|
136
|
-
})),
|
|
137
|
-
...(requestOptions === undefined ? {} : { options: requestOptions }),
|
|
138
|
-
...(cacheBlobs ? { cacheBlobs: true } : {}),
|
|
139
|
-
}),
|
|
140
|
-
});
|
|
141
|
-
const value = await requestWithFullBlobFallback(() => request(false), hasDelta ? () => request(true) : undefined);
|
|
142
|
-
if (!Array.isArray(value)) {
|
|
143
|
-
throw protocolError("execute batch response must be an array");
|
|
144
|
-
}
|
|
145
|
-
const results = value.map(decodeExecuteBatchResult);
|
|
146
|
-
this.#commitRequestBlobBases(preparedStatements.flatMap((statement) => statement.prepared.cacheUpdates));
|
|
147
|
-
return results;
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
async observe(sql, params) {
|
|
151
|
-
this.#assertOpen();
|
|
152
|
-
return this.#observationHub.observe(sql, params.map(encodeWireValue));
|
|
153
|
-
}
|
|
154
|
-
async beginTransaction() {
|
|
155
|
-
this.#assertOpen();
|
|
156
|
-
return this.#enqueue(async () => {
|
|
157
|
-
const begun = record(await this.#requestJson("transaction/begin", { method: "POST" }), "begin transaction response");
|
|
158
|
-
if (typeof begun.transactionId !== "string") {
|
|
159
|
-
throw protocolError("begin transaction response.transactionId must be a string");
|
|
160
|
-
}
|
|
161
|
-
const transactionId = begun.transactionId;
|
|
162
|
-
let active = true;
|
|
163
|
-
const assertActive = () => {
|
|
164
|
-
if (!active) {
|
|
165
|
-
throw remoteError("LIX_INVALID_TRANSACTION_STATE", "Lix transaction is closed");
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
return {
|
|
169
|
-
execute: async (sql, params, options) => {
|
|
170
|
-
assertActive();
|
|
171
|
-
const snapshot = snapshotParams(params);
|
|
172
|
-
const requestOptions = remoteExecuteOptions(options);
|
|
173
|
-
return this.#enqueue(async () => {
|
|
174
|
-
const value = await this.#requestJson("transaction/execute", {
|
|
175
|
-
method: "POST",
|
|
176
|
-
headers: { [REMOTE_TRANSACTION_HEADER]: transactionId },
|
|
177
|
-
body: JSON.stringify({
|
|
178
|
-
sql,
|
|
179
|
-
params: snapshot.map(encodeWireValue),
|
|
180
|
-
...(requestOptions === undefined
|
|
181
|
-
? {}
|
|
182
|
-
: { options: requestOptions }),
|
|
183
|
-
}),
|
|
184
|
-
});
|
|
185
|
-
return decodeExecuteResult(value);
|
|
186
|
-
});
|
|
187
|
-
},
|
|
188
|
-
commit: async () => {
|
|
189
|
-
assertActive();
|
|
190
|
-
return this.#enqueue(async () => {
|
|
191
|
-
assertActive();
|
|
192
|
-
await this.#requestJson("transaction/commit", {
|
|
193
|
-
method: "POST",
|
|
194
|
-
headers: { [REMOTE_TRANSACTION_HEADER]: transactionId },
|
|
195
|
-
}, "empty");
|
|
196
|
-
active = false;
|
|
197
|
-
});
|
|
198
|
-
},
|
|
199
|
-
rollback: async () => {
|
|
200
|
-
assertActive();
|
|
201
|
-
return this.#enqueue(async () => {
|
|
202
|
-
assertActive();
|
|
203
|
-
await this.#requestJson("transaction/rollback", {
|
|
204
|
-
method: "POST",
|
|
205
|
-
headers: { [REMOTE_TRANSACTION_HEADER]: transactionId },
|
|
206
|
-
}, "empty");
|
|
207
|
-
active = false;
|
|
208
|
-
});
|
|
209
|
-
},
|
|
210
|
-
};
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
async activeBranchId() {
|
|
214
|
-
this.#assertOpen();
|
|
215
|
-
return this.#enqueue(async () => {
|
|
216
|
-
if (this.#activeBranchId === undefined) {
|
|
217
|
-
const handshake = decodeHandshake(await this.#requestJson("", { method: "GET" }));
|
|
218
|
-
if (handshake.sessionId !== this.#sessionId) {
|
|
219
|
-
throw protocolError("Lix Server Protocol handshake changed sessionId");
|
|
220
|
-
}
|
|
221
|
-
this.#activeBranchId = handshake.activeBranchId;
|
|
222
|
-
}
|
|
223
|
-
return this.#activeBranchId;
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
async activeAccountId() {
|
|
227
|
-
this.#assertOpen();
|
|
228
|
-
return this.#enqueue(async () => {
|
|
229
|
-
if (this.#activeAccountId === undefined) {
|
|
230
|
-
const handshake = decodeHandshake(await this.#requestJson("", { method: "GET" }));
|
|
231
|
-
if (handshake.sessionId !== this.#sessionId) {
|
|
232
|
-
throw protocolError("Lix Server Protocol handshake changed sessionId");
|
|
233
|
-
}
|
|
234
|
-
this.#activeAccountId = handshake.activeAccountId;
|
|
235
|
-
}
|
|
236
|
-
return this.#activeAccountId;
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
async createBranch(options) {
|
|
240
|
-
this.#assertOpen();
|
|
241
|
-
return this.#enqueue(async () => {
|
|
242
|
-
const value = record(await this.#requestJson("branch/create", {
|
|
243
|
-
method: "POST",
|
|
244
|
-
body: JSON.stringify(options),
|
|
245
|
-
}), "create branch response");
|
|
246
|
-
if (typeof value.id !== "string" ||
|
|
247
|
-
typeof value.name !== "string" ||
|
|
248
|
-
typeof value.hidden !== "boolean" ||
|
|
249
|
-
typeof value.commitId !== "string") {
|
|
250
|
-
throw protocolError("create branch response is invalid");
|
|
251
|
-
}
|
|
252
|
-
return {
|
|
253
|
-
id: value.id,
|
|
254
|
-
name: value.name,
|
|
255
|
-
hidden: value.hidden,
|
|
256
|
-
commitId: value.commitId,
|
|
257
|
-
};
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
async createCheckpoint() {
|
|
261
|
-
this.#assertOpen();
|
|
262
|
-
return this.#enqueue(async () => {
|
|
263
|
-
const value = record(await this.#requestJson("checkpoint/create", { method: "POST" }), "create checkpoint response");
|
|
264
|
-
if (typeof value.commitId !== "string" ||
|
|
265
|
-
value.commitId.length === 0) {
|
|
266
|
-
throw protocolError("create checkpoint response is invalid");
|
|
267
|
-
}
|
|
268
|
-
return { commitId: value.commitId };
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
async undo() {
|
|
272
|
-
this.#assertOpen();
|
|
273
|
-
return this.#enqueue(async () => {
|
|
274
|
-
const value = record(await this.#requestJson("undo", { method: "POST" }), "undo response");
|
|
275
|
-
if (typeof value.branchId !== "string" ||
|
|
276
|
-
typeof value.targetCommitId !== "string" ||
|
|
277
|
-
typeof value.inverseCommitId !== "string") {
|
|
278
|
-
throw protocolError("undo response is invalid");
|
|
279
|
-
}
|
|
280
|
-
return {
|
|
281
|
-
branchId: value.branchId,
|
|
282
|
-
targetCommitId: value.targetCommitId,
|
|
283
|
-
inverseCommitId: value.inverseCommitId,
|
|
284
|
-
};
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
async redo() {
|
|
288
|
-
this.#assertOpen();
|
|
289
|
-
return this.#enqueue(async () => {
|
|
290
|
-
const value = record(await this.#requestJson("redo", { method: "POST" }), "redo response");
|
|
291
|
-
if (typeof value.branchId !== "string" ||
|
|
292
|
-
typeof value.targetCommitId !== "string" ||
|
|
293
|
-
typeof value.replayCommitId !== "string") {
|
|
294
|
-
throw protocolError("redo response is invalid");
|
|
295
|
-
}
|
|
296
|
-
return {
|
|
297
|
-
branchId: value.branchId,
|
|
298
|
-
targetCommitId: value.targetCommitId,
|
|
299
|
-
replayCommitId: value.replayCommitId,
|
|
300
|
-
};
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
async switchBranch(options) {
|
|
304
|
-
this.#assertOpen();
|
|
305
|
-
return this.#enqueue(async () => {
|
|
306
|
-
let requestAttempted = false;
|
|
307
|
-
try {
|
|
308
|
-
const value = record(await this.#requestJson("branch/switch", {
|
|
309
|
-
method: "POST",
|
|
310
|
-
body: JSON.stringify(options),
|
|
311
|
-
}, "json", () => {
|
|
312
|
-
requestAttempted = true;
|
|
313
|
-
}), "switch branch response");
|
|
314
|
-
if (value.branchId !== options.branchId) {
|
|
315
|
-
throw protocolError("switch branch response is invalid");
|
|
316
|
-
}
|
|
317
|
-
this.#activeBranchId = options.branchId;
|
|
318
|
-
this.#observationHub.restart();
|
|
319
|
-
return { branchId: options.branchId };
|
|
320
|
-
}
|
|
321
|
-
catch (error) {
|
|
322
|
-
if (requestAttempted && !isDefinitiveClientError(error)) {
|
|
323
|
-
this.#activeBranchId = undefined;
|
|
324
|
-
this.#observationHub.restart();
|
|
325
|
-
}
|
|
326
|
-
throw error;
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
async importFilesystemPaths(_paths) {
|
|
331
|
-
this.#assertOpen();
|
|
332
|
-
throw unsupportedRemoteOperation("importFilesystemPaths");
|
|
333
|
-
}
|
|
334
|
-
async mergeBranchPreview(_options) {
|
|
335
|
-
this.#assertOpen();
|
|
336
|
-
throw unsupportedRemoteOperation("mergeBranchPreview");
|
|
337
|
-
}
|
|
338
|
-
async mergeBranch(_options) {
|
|
339
|
-
this.#assertOpen();
|
|
340
|
-
throw unsupportedRemoteOperation("mergeBranch");
|
|
341
|
-
}
|
|
342
|
-
async syncDiskToLix() {
|
|
343
|
-
this.#assertOpen();
|
|
344
|
-
throw unsupportedRemoteOperation("syncDiskToLix");
|
|
345
|
-
}
|
|
346
|
-
async close() {
|
|
347
|
-
if (this.#closePromise !== undefined)
|
|
348
|
-
return this.#closePromise;
|
|
349
|
-
this.#acceptingOperations = false;
|
|
350
|
-
this.#observationHub.close();
|
|
351
|
-
this.#closePromise = this.#enqueue(async () => {
|
|
352
|
-
this.#observationHub.close();
|
|
353
|
-
await this.#requestJson("session", { method: "DELETE" }, "empty");
|
|
354
|
-
});
|
|
355
|
-
return this.#closePromise;
|
|
356
|
-
}
|
|
357
|
-
async #requestJson(path, init, responseKind = "json", onRequestAttempt) {
|
|
358
|
-
const headers = new Headers(await resolveHeaders(this.#headers));
|
|
359
|
-
new Headers(init.headers).forEach((value, name) => headers.set(name, value));
|
|
360
|
-
if (this.#sessionId === undefined)
|
|
361
|
-
headers.delete(REMOTE_SESSION_HEADER);
|
|
362
|
-
else
|
|
363
|
-
headers.set(REMOTE_SESSION_HEADER, this.#sessionId);
|
|
364
|
-
headers.set("accept", "application/json");
|
|
365
|
-
headers.delete("content-encoding");
|
|
366
|
-
let requestInit = init;
|
|
367
|
-
if (init.body !== undefined) {
|
|
368
|
-
headers.set("content-type", "application/json");
|
|
369
|
-
if (typeof init.body === "string" &&
|
|
370
|
-
init.body.length >= MIN_COMPRESSIBLE_JSON_BYTES) {
|
|
371
|
-
const prepared = await prepareJsonRequestBody(init.body);
|
|
372
|
-
requestInit = { ...init, body: prepared.body };
|
|
373
|
-
if (prepared.compressed)
|
|
374
|
-
headers.set("content-encoding", "gzip");
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
let response;
|
|
378
|
-
const url = new URL(path, this.#baseUrl);
|
|
379
|
-
const fetchInit = {
|
|
380
|
-
...requestInit,
|
|
381
|
-
headers,
|
|
382
|
-
};
|
|
383
|
-
// A custom fetch may transmit before throwing, so failures become
|
|
384
|
-
// ambiguous only once control is handed to it.
|
|
385
|
-
onRequestAttempt?.();
|
|
386
|
-
try {
|
|
387
|
-
response = await this.#fetch(url, fetchInit);
|
|
388
|
-
}
|
|
389
|
-
catch (cause) {
|
|
390
|
-
throw remoteError("LIX_REMOTE_UNAVAILABLE", "The remote Lix server is unavailable", { details: { cause: errorMessage(cause) } });
|
|
391
|
-
}
|
|
392
|
-
if (!response.ok)
|
|
393
|
-
throw await errorFromHttpResponse(response);
|
|
394
|
-
if (responseKind === "empty" || response.status === 204)
|
|
395
|
-
return undefined;
|
|
396
|
-
const text = await response.text();
|
|
397
|
-
try {
|
|
398
|
-
return JSON.parse(text);
|
|
399
|
-
}
|
|
400
|
-
catch {
|
|
401
|
-
throw protocolError(`remote response ${response.status} did not contain valid JSON`);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
async #requestObserveStream(subscriptions, signal) {
|
|
405
|
-
let headers;
|
|
406
|
-
try {
|
|
407
|
-
headers = new Headers(await resolveHeaders(this.#headers));
|
|
408
|
-
}
|
|
409
|
-
catch (cause) {
|
|
410
|
-
throw remoteError("LIX_REMOTE_CONFIGURATION_ERROR", "Remote Lix observation headers could not be resolved", { details: { cause: errorMessage(cause) } });
|
|
411
|
-
}
|
|
412
|
-
signal.throwIfAborted();
|
|
413
|
-
if (this.#sessionId === undefined) {
|
|
414
|
-
throw protocolError("remote observation started without a session");
|
|
415
|
-
}
|
|
416
|
-
headers.set(REMOTE_SESSION_HEADER, this.#sessionId);
|
|
417
|
-
headers.set("accept", "text/event-stream");
|
|
418
|
-
headers.set("content-type", "application/json");
|
|
419
|
-
headers.delete("content-encoding");
|
|
420
|
-
const observeBody = JSON.stringify({ subscriptions });
|
|
421
|
-
const prepared = observeBody.length < MIN_COMPRESSIBLE_JSON_BYTES
|
|
422
|
-
? { body: observeBody, compressed: false }
|
|
423
|
-
: await prepareJsonRequestBody(observeBody);
|
|
424
|
-
if (prepared.compressed) {
|
|
425
|
-
headers.set("content-encoding", "gzip");
|
|
426
|
-
}
|
|
427
|
-
signal.throwIfAborted();
|
|
428
|
-
try {
|
|
429
|
-
return await this.#fetch(new URL("observe/multiplex", this.#baseUrl), {
|
|
430
|
-
method: "POST",
|
|
431
|
-
headers,
|
|
432
|
-
body: prepared.body,
|
|
433
|
-
signal,
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
catch (cause) {
|
|
437
|
-
if (signal.aborted)
|
|
438
|
-
throw cause;
|
|
439
|
-
throw remoteError("LIX_REMOTE_UNAVAILABLE", "The remote Lix observation stream is unavailable", { details: { cause: errorMessage(cause) } });
|
|
440
|
-
}
|
|
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
|
-
}
|
|
454
|
-
async #prepareParams(params, slot) {
|
|
455
|
-
const prepared = await Promise.all(params.map(async (param, index) => {
|
|
456
|
-
if (param.kind !== "blob" ||
|
|
457
|
-
param.blob.byteLength < REQUEST_BLOB_DELTA_MIN_BYTES ||
|
|
458
|
-
param.blob.byteLength > REQUEST_BLOB_BASE_MAX_BYTES) {
|
|
459
|
-
return fullRequestParam(param);
|
|
460
|
-
}
|
|
461
|
-
const resultSha256 = await sha256Hex(param.blob);
|
|
462
|
-
if (resultSha256 === undefined)
|
|
463
|
-
return fullRequestParam(param);
|
|
464
|
-
const cacheSlot = slot(index);
|
|
465
|
-
const cacheUpdate = {
|
|
466
|
-
slot: cacheSlot,
|
|
467
|
-
base: {
|
|
468
|
-
sha256: resultSha256,
|
|
469
|
-
bytes: param.blob,
|
|
470
|
-
},
|
|
471
|
-
};
|
|
472
|
-
const base = this.#requestBlobBases.get(cacheSlot);
|
|
473
|
-
if (base === undefined) {
|
|
474
|
-
return { ...fullRequestParam(param), cacheUpdate };
|
|
475
|
-
}
|
|
476
|
-
const delta = planBlobSplice(base, param.blob, resultSha256);
|
|
477
|
-
if (!blobSpliceIsAtLeastTenPercentSmaller(delta, param.blob)) {
|
|
478
|
-
return { ...fullRequestParam(param), cacheUpdate };
|
|
479
|
-
}
|
|
480
|
-
return {
|
|
481
|
-
value: encodeBlobSplice(delta),
|
|
482
|
-
full: () => encodeWireValue(param),
|
|
483
|
-
cacheUpdate,
|
|
484
|
-
};
|
|
485
|
-
}));
|
|
486
|
-
return {
|
|
487
|
-
params: prepared.map((param) => param.value),
|
|
488
|
-
fullParams: () => prepared.map((param) => param.full()),
|
|
489
|
-
cacheUpdates: prepared.flatMap((param) => param.cacheUpdate === undefined ? [] : [param.cacheUpdate]),
|
|
490
|
-
cacheBlobs: prepared.some((param) => param.cacheUpdate !== undefined),
|
|
491
|
-
hasDelta: prepared.some((param) => param.value.kind === "blob-splice"),
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
#commitRequestBlobBases(updates) {
|
|
495
|
-
for (const update of updates) {
|
|
496
|
-
const previous = this.#requestBlobBases.get(update.slot);
|
|
497
|
-
if (previous !== undefined) {
|
|
498
|
-
this.#requestBlobBaseBytes -= previous.bytes.byteLength;
|
|
499
|
-
this.#requestBlobBases.delete(update.slot);
|
|
500
|
-
}
|
|
501
|
-
if (update.base.bytes.byteLength > REQUEST_BLOB_BASE_MAX_BYTES)
|
|
502
|
-
continue;
|
|
503
|
-
while (this.#requestBlobBases.size >= REQUEST_BLOB_BASE_MAX_ENTRIES ||
|
|
504
|
-
this.#requestBlobBaseBytes + update.base.bytes.byteLength >
|
|
505
|
-
REQUEST_BLOB_BASE_MAX_BYTES) {
|
|
506
|
-
const oldest = this.#requestBlobBases.entries().next().value;
|
|
507
|
-
if (oldest === undefined)
|
|
508
|
-
break;
|
|
509
|
-
this.#requestBlobBases.delete(oldest[0]);
|
|
510
|
-
this.#requestBlobBaseBytes -= oldest[1].bytes.byteLength;
|
|
511
|
-
}
|
|
512
|
-
this.#requestBlobBases.set(update.slot, update.base);
|
|
513
|
-
this.#requestBlobBaseBytes += update.base.bytes.byteLength;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
#assertOpen() {
|
|
517
|
-
if (!this.#acceptingOperations) {
|
|
518
|
-
throw remoteError("LIX_ERROR_CLOSED", "Lix is closed");
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
#enqueue(operation) {
|
|
522
|
-
const result = this.#operationQueue.then(operation, operation);
|
|
523
|
-
this.#operationQueue = result.then(() => undefined, () => undefined);
|
|
524
|
-
return result;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
function fullRequestParam(param) {
|
|
528
|
-
const full = encodeWireValue(param);
|
|
529
|
-
return { value: full, full: () => full };
|
|
530
|
-
}
|
|
531
|
-
function snapshotParams(params) {
|
|
532
|
-
return params.map((param) => param.kind === "blob"
|
|
533
|
-
? { kind: "blob", value: null, blob: new Uint8Array(param.blob) }
|
|
534
|
-
: param);
|
|
535
|
-
}
|
|
536
|
-
function requestBlobSlot(kind, sql, paramIndex, statementIndex) {
|
|
537
|
-
return JSON.stringify([kind, statementIndex, sql, paramIndex]);
|
|
538
|
-
}
|
|
539
|
-
async function sha256Hex(bytes) {
|
|
540
|
-
const subtle = globalThis.crypto?.subtle;
|
|
541
|
-
if (subtle === undefined)
|
|
542
|
-
return undefined;
|
|
543
|
-
try {
|
|
544
|
-
const input = bytes.buffer instanceof ArrayBuffer &&
|
|
545
|
-
bytes.byteOffset === 0 &&
|
|
546
|
-
bytes.byteLength === bytes.buffer.byteLength
|
|
547
|
-
? bytes.buffer
|
|
548
|
-
: copyArrayBuffer(bytes);
|
|
549
|
-
const digest = await subtle.digest("SHA-256", input);
|
|
550
|
-
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
551
|
-
}
|
|
552
|
-
catch {
|
|
553
|
-
return undefined;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
function planBlobSplice(base, result, resultSha256) {
|
|
557
|
-
const baseView = new DataView(base.bytes.buffer, base.bytes.byteOffset, base.bytes.byteLength);
|
|
558
|
-
const resultView = new DataView(result.buffer, result.byteOffset, result.byteLength);
|
|
559
|
-
let prefixBytes = 0;
|
|
560
|
-
const prefixLimit = Math.min(base.bytes.byteLength, result.byteLength);
|
|
561
|
-
while (prefixLimit - prefixBytes >= REQUEST_BLOB_COMPARE_WORD_BYTES &&
|
|
562
|
-
baseView.getUint32(prefixBytes) === resultView.getUint32(prefixBytes) &&
|
|
563
|
-
baseView.getUint32(prefixBytes + 4) ===
|
|
564
|
-
resultView.getUint32(prefixBytes + 4)) {
|
|
565
|
-
prefixBytes += REQUEST_BLOB_COMPARE_WORD_BYTES;
|
|
566
|
-
}
|
|
567
|
-
while (prefixBytes < prefixLimit &&
|
|
568
|
-
base.bytes[prefixBytes] === result[prefixBytes]) {
|
|
569
|
-
prefixBytes += 1;
|
|
570
|
-
}
|
|
571
|
-
let suffixBytes = 0;
|
|
572
|
-
const suffixLimit = Math.min(base.bytes.byteLength - prefixBytes, result.byteLength - prefixBytes);
|
|
573
|
-
while (suffixLimit - suffixBytes >= REQUEST_BLOB_COMPARE_WORD_BYTES) {
|
|
574
|
-
const baseOffset = base.bytes.byteLength - suffixBytes - REQUEST_BLOB_COMPARE_WORD_BYTES;
|
|
575
|
-
const resultOffset = result.byteLength - suffixBytes - REQUEST_BLOB_COMPARE_WORD_BYTES;
|
|
576
|
-
if (baseView.getUint32(baseOffset) !== resultView.getUint32(resultOffset) ||
|
|
577
|
-
baseView.getUint32(baseOffset + 4) !==
|
|
578
|
-
resultView.getUint32(resultOffset + 4)) {
|
|
579
|
-
break;
|
|
580
|
-
}
|
|
581
|
-
suffixBytes += REQUEST_BLOB_COMPARE_WORD_BYTES;
|
|
582
|
-
}
|
|
583
|
-
while (suffixBytes < suffixLimit &&
|
|
584
|
-
base.bytes[base.bytes.byteLength - suffixBytes - 1] ===
|
|
585
|
-
result[result.byteLength - suffixBytes - 1]) {
|
|
586
|
-
suffixBytes += 1;
|
|
587
|
-
}
|
|
588
|
-
const insert = result.subarray(prefixBytes, result.byteLength - suffixBytes);
|
|
589
|
-
return {
|
|
590
|
-
baseSha256: base.sha256,
|
|
591
|
-
resultSha256,
|
|
592
|
-
prefixBytes,
|
|
593
|
-
suffixBytes,
|
|
594
|
-
insert,
|
|
595
|
-
};
|
|
596
|
-
}
|
|
597
|
-
function encodeBlobSplice(plan) {
|
|
598
|
-
const encodedInsert = encodeWireValue({
|
|
599
|
-
kind: "blob",
|
|
600
|
-
value: null,
|
|
601
|
-
blob: plan.insert,
|
|
602
|
-
});
|
|
603
|
-
if (encodedInsert.kind !== "blob") {
|
|
604
|
-
throw protocolError("request blob splice insert could not be encoded");
|
|
605
|
-
}
|
|
606
|
-
return {
|
|
607
|
-
kind: "blob-splice",
|
|
608
|
-
baseSha256: plan.baseSha256,
|
|
609
|
-
resultSha256: plan.resultSha256,
|
|
610
|
-
prefixBytes: plan.prefixBytes,
|
|
611
|
-
suffixBytes: plan.suffixBytes,
|
|
612
|
-
insertBase64: encodedInsert.base64,
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
function blobSpliceIsAtLeastTenPercentSmaller(delta, full) {
|
|
616
|
-
const deltaEnvelopeBytes = JSON.stringify({
|
|
617
|
-
kind: "blob-splice",
|
|
618
|
-
baseSha256: delta.baseSha256,
|
|
619
|
-
resultSha256: delta.resultSha256,
|
|
620
|
-
prefixBytes: delta.prefixBytes,
|
|
621
|
-
suffixBytes: delta.suffixBytes,
|
|
622
|
-
insertBase64: "",
|
|
623
|
-
}).length;
|
|
624
|
-
const deltaBytes = deltaEnvelopeBytes + base64EncodedLength(delta.insert.byteLength);
|
|
625
|
-
const fullBytes = WIRE_BLOB_JSON_ENVELOPE_BYTES + base64EncodedLength(full.byteLength);
|
|
626
|
-
return (deltaBytes < fullBytes * REQUEST_BLOB_DELTA_MIN_WIRE_RATIO);
|
|
627
|
-
}
|
|
628
|
-
function base64EncodedLength(byteLength) {
|
|
629
|
-
return 4 * Math.ceil(byteLength / 3);
|
|
630
|
-
}
|
|
631
|
-
function remoteExecuteOptions(options) {
|
|
632
|
-
if (options?.originKey === undefined)
|
|
633
|
-
return undefined;
|
|
634
|
-
return { originKey: options.originKey };
|
|
635
|
-
}
|
|
636
|
-
function idempotencyKeyFor(provided) {
|
|
637
|
-
if (provided !== undefined && typeof provided !== "string") {
|
|
638
|
-
throw new TypeError("options.idempotencyKey must be a string");
|
|
639
|
-
}
|
|
640
|
-
const key = provided ?? globalThis.crypto.randomUUID();
|
|
641
|
-
if (key.length === 0 ||
|
|
642
|
-
key.length > 255 ||
|
|
643
|
-
![...key].every((character) => character.length === 1 &&
|
|
644
|
-
character.charCodeAt(0) >= 0x21 &&
|
|
645
|
-
character.charCodeAt(0) <= 0x7e)) {
|
|
646
|
-
throw new TypeError("options.idempotencyKey must contain 1 to 255 visible ASCII characters");
|
|
647
|
-
}
|
|
648
|
-
return key;
|
|
649
|
-
}
|
|
650
|
-
async function requestWithFullBlobFallback(request, fullFallback) {
|
|
651
|
-
try {
|
|
652
|
-
return await request();
|
|
653
|
-
}
|
|
654
|
-
catch (error) {
|
|
655
|
-
if (fullFallback !== undefined && errorCode(error) === REMOTE_BLOB_BASE_MISSING) {
|
|
656
|
-
return await fullFallback();
|
|
657
|
-
}
|
|
658
|
-
throw error;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
function errorCode(error) {
|
|
662
|
-
return error instanceof Error && "code" in error
|
|
663
|
-
? error.code
|
|
664
|
-
: undefined;
|
|
665
|
-
}
|
|
666
|
-
function copyArrayBuffer(bytes) {
|
|
667
|
-
const copy = new Uint8Array(bytes.byteLength);
|
|
668
|
-
copy.set(bytes);
|
|
669
|
-
return copy.buffer;
|
|
670
|
-
}
|
|
671
|
-
class RemoteObservationHub {
|
|
672
|
-
#openStream;
|
|
673
|
-
#refreshObservation;
|
|
674
|
-
#observations = new Map();
|
|
675
|
-
#nextObservationId = 0;
|
|
676
|
-
#controller;
|
|
677
|
-
#retryTimer;
|
|
678
|
-
#retryAttempt = 0;
|
|
679
|
-
#serverRetryMs;
|
|
680
|
-
#generation = 0;
|
|
681
|
-
#startQueued = false;
|
|
682
|
-
#closed = false;
|
|
683
|
-
constructor(options) {
|
|
684
|
-
this.#openStream = options.openStream;
|
|
685
|
-
this.#refreshObservation = options.refreshObservation;
|
|
686
|
-
}
|
|
687
|
-
observe(sql, params) {
|
|
688
|
-
const id = `observe-${++this.#nextObservationId}`;
|
|
689
|
-
const observation = new RemoteObservation({
|
|
690
|
-
id,
|
|
691
|
-
sql,
|
|
692
|
-
params,
|
|
693
|
-
onClose: () => {
|
|
694
|
-
this.#observations.delete(id);
|
|
695
|
-
this.#restartStream();
|
|
696
|
-
},
|
|
697
|
-
});
|
|
698
|
-
this.#observations.set(id, observation);
|
|
699
|
-
this.#restartStream();
|
|
700
|
-
return observation;
|
|
701
|
-
}
|
|
702
|
-
close() {
|
|
703
|
-
if (!this.#closed) {
|
|
704
|
-
this.#closed = true;
|
|
705
|
-
this.#stopStream();
|
|
706
|
-
}
|
|
707
|
-
for (const observation of [...this.#observations.values()]) {
|
|
708
|
-
observation.close();
|
|
709
|
-
}
|
|
710
|
-
this.#observations.clear();
|
|
711
|
-
}
|
|
712
|
-
restart() {
|
|
713
|
-
if (!this.#closed)
|
|
714
|
-
this.#restartStream();
|
|
715
|
-
}
|
|
716
|
-
#restartStream() {
|
|
717
|
-
this.#stopStream();
|
|
718
|
-
if (this.#startQueued)
|
|
719
|
-
return;
|
|
720
|
-
this.#startQueued = true;
|
|
721
|
-
queueMicrotask(() => {
|
|
722
|
-
this.#startQueued = false;
|
|
723
|
-
this.#startStream();
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
#startStream() {
|
|
727
|
-
if (this.#closed ||
|
|
728
|
-
this.#observations.size === 0 ||
|
|
729
|
-
this.#controller !== undefined ||
|
|
730
|
-
this.#retryTimer !== undefined) {
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
const generation = this.#generation;
|
|
734
|
-
const controller = new AbortController();
|
|
735
|
-
this.#controller = controller;
|
|
736
|
-
void this.#consume(generation, controller);
|
|
737
|
-
}
|
|
738
|
-
#stopStream() {
|
|
739
|
-
this.#generation += 1;
|
|
740
|
-
this.#controller?.abort();
|
|
741
|
-
this.#controller = undefined;
|
|
742
|
-
if (this.#retryTimer !== undefined)
|
|
743
|
-
clearTimeout(this.#retryTimer);
|
|
744
|
-
this.#retryTimer = undefined;
|
|
745
|
-
this.#retryAttempt = 0;
|
|
746
|
-
this.#serverRetryMs = undefined;
|
|
747
|
-
}
|
|
748
|
-
async #consume(generation, controller) {
|
|
749
|
-
let reconnect = false;
|
|
750
|
-
let streamOpened = false;
|
|
751
|
-
const transportBases = new Map();
|
|
752
|
-
try {
|
|
753
|
-
const response = await this.#openStream([...this.#observations.values()].map((observation) => observation.request()), controller.signal);
|
|
754
|
-
if (!this.#isCurrent(generation, controller))
|
|
755
|
-
return;
|
|
756
|
-
streamOpened = true;
|
|
757
|
-
const initialSubscriptions = new Set(this.#observations.keys());
|
|
758
|
-
if (!response.ok) {
|
|
759
|
-
if (isRetryableObserveStatus(response.status)) {
|
|
760
|
-
void response.body?.cancel();
|
|
761
|
-
reconnect = true;
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
const error = await errorFromHttpResponse(response);
|
|
765
|
-
if (this.#isCurrent(generation, controller)) {
|
|
766
|
-
this.#failStream(error, controller);
|
|
767
|
-
}
|
|
768
|
-
return;
|
|
769
|
-
}
|
|
770
|
-
if (!response.body) {
|
|
771
|
-
this.#failStream(protocolError("remote observe response has no body"), controller);
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
775
|
-
if (contentType.split(";", 1)[0]?.trim().toLowerCase() !==
|
|
776
|
-
"text/event-stream") {
|
|
777
|
-
this.#failStream(protocolError("remote observe response must be text/event-stream"), controller);
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
for await (const frame of readSseEvents(response.body)) {
|
|
781
|
-
if (!this.#isCurrent(generation, controller))
|
|
782
|
-
return;
|
|
783
|
-
if (frame.retry !== undefined)
|
|
784
|
-
this.#serverRetryMs = frame.retry;
|
|
785
|
-
if (frame.event === "next") {
|
|
786
|
-
try {
|
|
787
|
-
const payload = record(JSON.parse(frame.data), "remote multiplex observe next event");
|
|
788
|
-
const subscriptionId = payload.subscriptionId;
|
|
789
|
-
if (typeof subscriptionId !== "string") {
|
|
790
|
-
throw protocolError("remote observe event requires subscriptionId");
|
|
791
|
-
}
|
|
792
|
-
const observation = this.#observation(subscriptionId);
|
|
793
|
-
const transportDelta = payload.delta !== undefined;
|
|
794
|
-
const event = decodeObserveEvent(payload, transportBases.get(subscriptionId));
|
|
795
|
-
transportBases.set(subscriptionId, event);
|
|
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
|
-
}
|
|
811
|
-
this.#retryAttempt = 0;
|
|
812
|
-
}
|
|
813
|
-
catch (error) {
|
|
814
|
-
this.#failStream(asObserveProtocolError(error, "next"), controller);
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
else if (frame.event === "error") {
|
|
819
|
-
try {
|
|
820
|
-
const payload = record(JSON.parse(frame.data), "remote multiplex observe error event");
|
|
821
|
-
if (payload.retryable !== undefined &&
|
|
822
|
-
typeof payload.retryable !== "boolean") {
|
|
823
|
-
throw protocolError("remote observe error retryable must be a boolean");
|
|
824
|
-
}
|
|
825
|
-
const error = errorFromResponseBody(payload);
|
|
826
|
-
const subscriptionId = payload.subscriptionId;
|
|
827
|
-
if (subscriptionId !== undefined) {
|
|
828
|
-
if (typeof subscriptionId !== "string") {
|
|
829
|
-
throw protocolError("remote observe event requires subscriptionId");
|
|
830
|
-
}
|
|
831
|
-
const observation = this.#observation(subscriptionId);
|
|
832
|
-
if (payload.retryable === true) {
|
|
833
|
-
observation.recover(error);
|
|
834
|
-
reconnect = true;
|
|
835
|
-
controller.abort();
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
observation.fail(error);
|
|
839
|
-
this.#observations.delete(subscriptionId);
|
|
840
|
-
continue;
|
|
841
|
-
}
|
|
842
|
-
if (payload.retryable === true) {
|
|
843
|
-
for (const observation of this.#observations.values()) {
|
|
844
|
-
observation.recover(error);
|
|
845
|
-
}
|
|
846
|
-
reconnect = true;
|
|
847
|
-
controller.abort();
|
|
848
|
-
}
|
|
849
|
-
else {
|
|
850
|
-
this.#failStream(error, controller);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
catch (error) {
|
|
854
|
-
this.#failStream(asObserveProtocolError(error, "error"), controller);
|
|
855
|
-
}
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
else if (frame.event !== "message" || frame.data.length > 0) {
|
|
859
|
-
this.#failStream(protocolError(`unknown remote observe event: ${frame.event}`), controller);
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
if (this.#isCurrent(generation, controller))
|
|
864
|
-
reconnect = true;
|
|
865
|
-
}
|
|
866
|
-
catch (error) {
|
|
867
|
-
if (!this.#isCurrent(generation, controller) ||
|
|
868
|
-
controller.signal.aborted) {
|
|
869
|
-
return;
|
|
870
|
-
}
|
|
871
|
-
if (streamOpened || isRetryableObserveError(error))
|
|
872
|
-
reconnect = true;
|
|
873
|
-
else
|
|
874
|
-
this.#failStream(error, controller);
|
|
875
|
-
}
|
|
876
|
-
finally {
|
|
877
|
-
if (this.#isCurrent(generation, controller)) {
|
|
878
|
-
this.#controller = undefined;
|
|
879
|
-
if (reconnect)
|
|
880
|
-
this.#scheduleReconnect(generation);
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
#observation(id) {
|
|
885
|
-
if (typeof id !== "string" || id.length === 0) {
|
|
886
|
-
throw protocolError("remote observe event requires subscriptionId");
|
|
887
|
-
}
|
|
888
|
-
const observation = this.#observations.get(id);
|
|
889
|
-
if (!observation) {
|
|
890
|
-
throw protocolError(`unknown remote observe subscription: ${id}`);
|
|
891
|
-
}
|
|
892
|
-
return observation;
|
|
893
|
-
}
|
|
894
|
-
#failAll(error) {
|
|
895
|
-
for (const observation of this.#observations.values()) {
|
|
896
|
-
observation.fail(error);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
#failStream(error, controller) {
|
|
900
|
-
this.#failAll(error);
|
|
901
|
-
controller.abort();
|
|
902
|
-
}
|
|
903
|
-
#scheduleReconnect(generation) {
|
|
904
|
-
if (this.#closed ||
|
|
905
|
-
this.#observations.size === 0 ||
|
|
906
|
-
generation !== this.#generation ||
|
|
907
|
-
this.#controller !== undefined ||
|
|
908
|
-
this.#retryTimer !== undefined) {
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
const delay = this.#serverRetryMs === undefined
|
|
912
|
-
? Math.min(OBSERVE_RETRY_BASE_MS * 2 ** this.#retryAttempt, OBSERVE_RETRY_MAX_MS)
|
|
913
|
-
: Math.min(Math.max(this.#serverRetryMs, OBSERVE_RETRY_BASE_MS), OBSERVE_RETRY_MAX_MS);
|
|
914
|
-
this.#retryAttempt += 1;
|
|
915
|
-
this.#retryTimer = setTimeout(() => {
|
|
916
|
-
this.#retryTimer = undefined;
|
|
917
|
-
if (generation === this.#generation)
|
|
918
|
-
this.#startStream();
|
|
919
|
-
}, delay);
|
|
920
|
-
}
|
|
921
|
-
#isCurrent(generation, controller) {
|
|
922
|
-
return (!this.#closed &&
|
|
923
|
-
generation === this.#generation &&
|
|
924
|
-
controller === this.#controller);
|
|
925
|
-
}
|
|
1
|
+
import { initializeBrowserWasm } from "../browser-wasm-init.js";
|
|
2
|
+
// Generated before TypeScript compilation and emitted beside the JS SDK.
|
|
3
|
+
// @ts-ignore Generated by build:wasm and absent in source-only checks.
|
|
4
|
+
import initWasm, { openRemote } from "../wasm/lix_js_sdk.js";
|
|
5
|
+
let wasmInitialized;
|
|
6
|
+
function initializeWasm() {
|
|
7
|
+
if (wasmInitialized !== undefined)
|
|
8
|
+
return wasmInitialized;
|
|
9
|
+
let initialization;
|
|
10
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
11
|
+
initialization = import("node:fs/promises").then(({ readFile }) => initWasm({
|
|
12
|
+
module_or_path: readFile(new URL("../wasm/lix_js_sdk_bg.wasm", import.meta.url)),
|
|
13
|
+
}), (error) => {
|
|
14
|
+
wasmInitialized = undefined;
|
|
15
|
+
throw error;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
initialization = initializeBrowserWasm(initWasm, new URL("../wasm/lix_js_sdk_bg.wasm", import.meta.url));
|
|
20
|
+
}
|
|
21
|
+
wasmInitialized = initialization;
|
|
22
|
+
return wasmInitialized;
|
|
926
23
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
#params;
|
|
931
|
-
#onClose;
|
|
932
|
-
#outcomes = [];
|
|
933
|
-
#waiters = [];
|
|
934
|
-
#terminalError;
|
|
935
|
-
#lastRows;
|
|
936
|
-
#lastSequence = -1;
|
|
937
|
-
#closed = false;
|
|
938
|
-
constructor(options) {
|
|
939
|
-
this.#id = options.id;
|
|
940
|
-
this.#sql = options.sql;
|
|
941
|
-
this.#params = options.params;
|
|
942
|
-
this.#onClose = options.onClose;
|
|
943
|
-
}
|
|
944
|
-
request() {
|
|
945
|
-
return { id: this.#id, sql: this.#sql, params: this.#params };
|
|
946
|
-
}
|
|
947
|
-
next() {
|
|
948
|
-
const outcome = this.#outcomes.shift();
|
|
949
|
-
if (outcome?.ok)
|
|
950
|
-
return Promise.resolve(outcome.event);
|
|
951
|
-
if (outcome)
|
|
952
|
-
return Promise.reject(outcome.error);
|
|
953
|
-
if (this.#terminalError !== undefined) {
|
|
954
|
-
return Promise.reject(this.#terminalError);
|
|
955
|
-
}
|
|
956
|
-
if (this.#closed)
|
|
957
|
-
return Promise.resolve(undefined);
|
|
958
|
-
return new Promise((resolve, reject) => {
|
|
959
|
-
this.#waiters.push({ resolve, reject });
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
close() {
|
|
963
|
-
if (this.#closed)
|
|
964
|
-
return;
|
|
965
|
-
this.#closed = true;
|
|
966
|
-
this.#outcomes = [];
|
|
967
|
-
this.#terminalError = undefined;
|
|
968
|
-
for (const waiter of this.#waiters.splice(0))
|
|
969
|
-
waiter.resolve(undefined);
|
|
970
|
-
this.#onClose();
|
|
971
|
-
}
|
|
972
|
-
accept(event, transportDelta = false) {
|
|
973
|
-
if (this.#closed || this.#terminalError !== undefined)
|
|
974
|
-
return;
|
|
975
|
-
if (!transportDelta &&
|
|
976
|
-
this.#lastRows !== undefined &&
|
|
977
|
-
executeResultsEqual(this.#lastRows, event.rows)) {
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
const normalized = {
|
|
981
|
-
sequence: this.#lastSequence + 1,
|
|
982
|
-
mutationSequence: event.mutationSequence,
|
|
983
|
-
rows: event.rows,
|
|
984
|
-
};
|
|
985
|
-
this.#lastRows = event.rows;
|
|
986
|
-
this.#lastSequence = normalized.sequence;
|
|
987
|
-
const waiter = this.#waiters.shift();
|
|
988
|
-
if (waiter)
|
|
989
|
-
waiter.resolve(normalized);
|
|
990
|
-
else {
|
|
991
|
-
this.#outcomes = this.#outcomes.filter((outcome) => !outcome.ok);
|
|
992
|
-
this.#outcomes.push({ ok: true, event: normalized });
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
recover(error) {
|
|
996
|
-
if (this.#closed || this.#terminalError !== undefined)
|
|
997
|
-
return;
|
|
998
|
-
const waiter = this.#waiters.shift();
|
|
999
|
-
if (waiter)
|
|
1000
|
-
waiter.reject(error);
|
|
1001
|
-
else if (!this.#outcomes.some((outcome) => !outcome.ok)) {
|
|
1002
|
-
this.#outcomes.push({ ok: false, error });
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
fail(error) {
|
|
1006
|
-
if (this.#closed || this.#terminalError !== undefined)
|
|
1007
|
-
return;
|
|
1008
|
-
this.#terminalError = error;
|
|
1009
|
-
for (const waiter of this.#waiters.splice(0))
|
|
1010
|
-
waiter.reject(error);
|
|
24
|
+
export async function openRemoteLixBinding(options, clientOptions = {}) {
|
|
25
|
+
if (!options || typeof options !== "object") {
|
|
26
|
+
throw new TypeError("openLix() remote server must be an object");
|
|
1011
27
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
const bytes = new TextEncoder().encode(body);
|
|
1015
|
-
if (bytes.byteLength < MIN_COMPRESSIBLE_JSON_BYTES) {
|
|
1016
|
-
return { body, compressed: false };
|
|
28
|
+
if (options.mode !== "remote") {
|
|
29
|
+
throw new TypeError("openLix() remote server mode must be 'remote'");
|
|
1017
30
|
}
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
sample.byteLength * MAX_COMPRESSION_SAMPLE_RATIO) {
|
|
1022
|
-
return { body, compressed: false };
|
|
31
|
+
const remoteFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
32
|
+
if (typeof remoteFetch !== "function") {
|
|
33
|
+
throw new TypeError("openLix() remote mode requires fetch");
|
|
1023
34
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
35
|
+
if (options.headers !== undefined &&
|
|
36
|
+
typeof options.headers !== "function" &&
|
|
37
|
+
!isHeadersInit(options.headers)) {
|
|
38
|
+
throw new TypeError("openLix() remote server headers must be HeadersInit or a function");
|
|
1027
39
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
}
|
|
1032
|
-
async function gzipBytes(bytes) {
|
|
1033
|
-
const CompressionStreamConstructor = globalThis.CompressionStream;
|
|
1034
|
-
if (typeof CompressionStreamConstructor === "function") {
|
|
1035
|
-
const stream = new CompressionStreamConstructor("gzip");
|
|
1036
|
-
const output = new Response(stream.readable).arrayBuffer();
|
|
1037
|
-
const writer = stream.writable.getWriter();
|
|
1038
|
-
await writer.write(bytes);
|
|
1039
|
-
await writer.close();
|
|
1040
|
-
return new Uint8Array(await output);
|
|
40
|
+
if (clientOptions.initialActiveBranchId !== undefined &&
|
|
41
|
+
clientOptions.initialActiveBranchId.length === 0) {
|
|
42
|
+
throw new TypeError("initialActiveBranchId must be a non-empty string");
|
|
1041
43
|
}
|
|
1042
|
-
const
|
|
1043
|
-
|
|
44
|
+
const protocolLocator = connectionUrl(options.url).toString();
|
|
45
|
+
await initializeWasm();
|
|
46
|
+
return openRemote(protocolLocator, remoteFetch, options.headers, clientOptions.initialActiveBranchId);
|
|
1044
47
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
}
|
|
1048
|
-
async function errorFromHttpResponse(response) {
|
|
1049
|
-
const text = await response.text();
|
|
48
|
+
function connectionUrl(value) {
|
|
49
|
+
let locator;
|
|
1050
50
|
try {
|
|
1051
|
-
|
|
1052
|
-
}
|
|
1053
|
-
catch (error) {
|
|
1054
|
-
if (error instanceof Error &&
|
|
1055
|
-
"status" in error &&
|
|
1056
|
-
error.status === response.status) {
|
|
1057
|
-
return error;
|
|
1058
|
-
}
|
|
1059
|
-
return remoteError("LIX_REMOTE_REQUEST_FAILED", `Remote Lix request failed with status ${response.status}`, {
|
|
1060
|
-
status: response.status,
|
|
1061
|
-
details: text.length === 0 ? undefined : { body: text.slice(0, 1000) },
|
|
1062
|
-
});
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
function isDefinitiveClientError(error) {
|
|
1066
|
-
if (!(error instanceof Error) || !("status" in error))
|
|
1067
|
-
return false;
|
|
1068
|
-
const status = error.status;
|
|
1069
|
-
return (typeof status === "number" &&
|
|
1070
|
-
status >= 400 &&
|
|
1071
|
-
status < 500 &&
|
|
1072
|
-
status !== 408 &&
|
|
1073
|
-
status !== 429);
|
|
1074
|
-
}
|
|
1075
|
-
function isRetryableObserveStatus(status) {
|
|
1076
|
-
return status === 408 || status === 429 || status >= 500;
|
|
1077
|
-
}
|
|
1078
|
-
function isRetryableObserveError(error) {
|
|
1079
|
-
return (error instanceof Error &&
|
|
1080
|
-
"code" in error &&
|
|
1081
|
-
error.code === "LIX_REMOTE_UNAVAILABLE");
|
|
1082
|
-
}
|
|
1083
|
-
function asObserveProtocolError(error, event) {
|
|
1084
|
-
if (error instanceof Error &&
|
|
1085
|
-
"code" in error &&
|
|
1086
|
-
error.code === "LIX_SERVER_PROTOCOL_ERROR") {
|
|
1087
|
-
return error;
|
|
1088
|
-
}
|
|
1089
|
-
return protocolError(`remote observe ${event} event contains invalid data: ${errorMessage(error)}`);
|
|
1090
|
-
}
|
|
1091
|
-
function executeResultsEqual(left, right) {
|
|
1092
|
-
return (left.rowsAffected === right.rowsAffected &&
|
|
1093
|
-
stringArraysEqual(left.columns, right.columns) &&
|
|
1094
|
-
left.rows.length === right.rows.length &&
|
|
1095
|
-
left.rows.every((row, rowIndex) => row.length === right.rows[rowIndex]?.length &&
|
|
1096
|
-
row.every((value, valueIndex) => nativeValuesEqual(value, right.rows[rowIndex]?.[valueIndex]))) &&
|
|
1097
|
-
left.notices.length === right.notices.length &&
|
|
1098
|
-
left.notices.every((notice, index) => {
|
|
1099
|
-
const other = right.notices[index];
|
|
1100
|
-
return (notice.code === other?.code &&
|
|
1101
|
-
notice.message === other.message &&
|
|
1102
|
-
notice.hint === other.hint);
|
|
1103
|
-
}));
|
|
1104
|
-
}
|
|
1105
|
-
function nativeValuesEqual(left, right) {
|
|
1106
|
-
if (!right || left.kind !== right.kind)
|
|
1107
|
-
return false;
|
|
1108
|
-
switch (left.kind) {
|
|
1109
|
-
case "blob":
|
|
1110
|
-
return (right.kind === "blob" &&
|
|
1111
|
-
left.blob.length === right.blob.length &&
|
|
1112
|
-
left.blob.every((byte, index) => byte === right.blob[index]));
|
|
1113
|
-
case "jsonb":
|
|
1114
|
-
return right.kind === "jsonb" && jsonValuesEqual(left.value, right.value);
|
|
1115
|
-
default:
|
|
1116
|
-
return left.value === right.value;
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
function jsonValuesEqual(left, right) {
|
|
1120
|
-
if (left === right)
|
|
1121
|
-
return true;
|
|
1122
|
-
if (Array.isArray(left) || Array.isArray(right)) {
|
|
1123
|
-
return (Array.isArray(left) &&
|
|
1124
|
-
Array.isArray(right) &&
|
|
1125
|
-
left.length === right.length &&
|
|
1126
|
-
left.every((value, index) => jsonValuesEqual(value, right[index])));
|
|
1127
|
-
}
|
|
1128
|
-
if (!left ||
|
|
1129
|
-
!right ||
|
|
1130
|
-
typeof left !== "object" ||
|
|
1131
|
-
typeof right !== "object") {
|
|
1132
|
-
return false;
|
|
1133
|
-
}
|
|
1134
|
-
const leftRecord = left;
|
|
1135
|
-
const rightRecord = right;
|
|
1136
|
-
const leftKeys = Object.keys(leftRecord).sort();
|
|
1137
|
-
const rightKeys = Object.keys(rightRecord).sort();
|
|
1138
|
-
return (stringArraysEqual(leftKeys, rightKeys) &&
|
|
1139
|
-
leftKeys.every((key) => jsonValuesEqual(leftRecord[key], rightRecord[key])));
|
|
1140
|
-
}
|
|
1141
|
-
function stringArraysEqual(left, right) {
|
|
1142
|
-
return (left.length === right.length &&
|
|
1143
|
-
left.every((value, index) => value === right[index]));
|
|
1144
|
-
}
|
|
1145
|
-
function protocolBaseUrl(value) {
|
|
1146
|
-
let repositoryUrl;
|
|
1147
|
-
try {
|
|
1148
|
-
repositoryUrl = new URL(value);
|
|
51
|
+
locator = new URL(value);
|
|
1149
52
|
}
|
|
1150
53
|
catch {
|
|
1151
54
|
throw new TypeError("openLix() remote server url must be an absolute URL");
|
|
1152
55
|
}
|
|
1153
|
-
if (
|
|
1154
|
-
|
|
1155
|
-
throw new TypeError("openLix() remote server url must use http
|
|
56
|
+
if (locator.protocol !== "https:" &&
|
|
57
|
+
!(locator.protocol === "http:" && isLoopbackHost(locator.hostname))) {
|
|
58
|
+
throw new TypeError("openLix() remote server url must use https (http is allowed only for loopback development)");
|
|
1156
59
|
}
|
|
1157
|
-
if (
|
|
60
|
+
if (locator.search || locator.hash) {
|
|
1158
61
|
throw new TypeError("openLix() remote server url must not contain a query or fragment");
|
|
1159
62
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
63
|
+
if (locator.username || locator.password) {
|
|
64
|
+
throw new TypeError("openLix() remote server url must not contain credentials");
|
|
65
|
+
}
|
|
66
|
+
if (!/^\/lix\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(locator.pathname)) {
|
|
67
|
+
throw new TypeError("openLix() remote server url path must be exactly /lix/{uuid}");
|
|
68
|
+
}
|
|
69
|
+
return locator;
|
|
1162
70
|
}
|
|
1163
|
-
function
|
|
1164
|
-
return
|
|
71
|
+
function isLoopbackHost(hostname) {
|
|
72
|
+
return (hostname === "localhost" ||
|
|
73
|
+
hostname === "[::1]" ||
|
|
74
|
+
/^127(?:\.[0-9]{1,3}){3}$/.test(hostname));
|
|
1165
75
|
}
|
|
1166
76
|
function isHeadersInit(value) {
|
|
1167
77
|
try {
|
|
@@ -1172,6 +82,3 @@ function isHeadersInit(value) {
|
|
|
1172
82
|
return false;
|
|
1173
83
|
}
|
|
1174
84
|
}
|
|
1175
|
-
function errorMessage(value) {
|
|
1176
|
-
return value instanceof Error ? value.message : String(value);
|
|
1177
|
-
}
|