@lix-js/sdk 0.12.3 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +54 -10
  2. package/dist/binding-types.d.ts +37 -9
  3. package/dist/binding.browser.d.ts +2 -2
  4. package/dist/binding.browser.js +20 -13
  5. package/dist/binding.node-wasm.d.ts +2 -2
  6. package/dist/binding.node-wasm.js +9 -11
  7. package/dist/binding.node.d.ts +3 -3
  8. package/dist/binding.node.js +70 -13
  9. package/dist/bundled-plugins/plugin_csv.lixplugin +0 -0
  10. package/dist/bundled-plugins/plugin_markdown.lixplugin +0 -0
  11. package/dist/index.d.ts +4 -4
  12. package/dist/index.js +2 -2
  13. package/dist/lix.d.ts +27 -5
  14. package/dist/lix.js +125 -6
  15. package/dist/open-lix.d.ts +4 -5
  16. package/dist/open-lix.js +174 -33
  17. package/dist/remote/client.d.ts +1 -2
  18. package/dist/remote/client.js +40 -1151
  19. package/dist/remote/server-protocol.d.ts +5 -6
  20. package/dist/remote/server-protocol.js +40 -6
  21. package/dist/result.d.ts +6 -13
  22. package/dist/result.js +9 -35
  23. package/dist/snapshot-restore.d.ts +6 -0
  24. package/dist/snapshot-restore.js +101 -0
  25. package/dist/storage-adapter.d.ts +175 -19
  26. package/dist/storage-adapter.js +21 -0
  27. package/dist/types.d.ts +104 -34
  28. package/dist/value.d.ts +1 -0
  29. package/dist/value.js +8 -0
  30. package/dist/wasm/lix_js_sdk.d.ts +121 -24
  31. package/dist/wasm/lix_js_sdk.js +606 -78
  32. package/dist/wasm/lix_js_sdk_bg.wasm +0 -0
  33. package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +48 -6
  34. package/dist/wasm-init.d.ts +2 -0
  35. package/dist/wasm-init.js +23 -0
  36. package/dist/worker/client.d.ts +31 -5
  37. package/dist/worker/client.js +541 -45
  38. package/dist/worker/factory.browser.d.ts +2 -2
  39. package/dist/worker/factory.node.d.ts +2 -2
  40. package/dist/worker/factory.node.js +3 -10
  41. package/dist/worker/host.d.ts +4 -2
  42. package/dist/worker/host.js +401 -43
  43. package/dist/worker/protocol.d.ts +107 -3
  44. package/package.json +6 -10
  45. package/dist/indexeddb-backend.d.ts +0 -19
  46. package/dist/indexeddb-backend.js +0 -121
  47. package/dist/remote/sse.d.ts +0 -12
  48. package/dist/remote/sse.js +0 -87
  49. package/dist/workerd.d.ts +0 -25
  50. package/dist/workerd.js +0 -35
package/dist/lix.js CHANGED
@@ -15,26 +15,44 @@ const observeFinalizer = new FinalizationRegistry(({ observe, onClose }) => {
15
15
  });
16
16
  export class Lix {
17
17
  binding;
18
+ openReport;
18
19
  closePromise;
19
20
  #activeBranchListeners = new Set();
20
21
  #inFlightOperations = new Set();
21
22
  #observations = new Map();
23
+ #snapshotExports = new Set();
22
24
  #nextObservationId = 0;
23
25
  #transactionsOpening = 0;
24
26
  #activeTransactions = 0;
25
27
  #acceptingOperations = true;
26
28
  constructor(binding) {
27
29
  this.binding = binding;
30
+ const report = binding.openReport?.();
31
+ this.openReport = report
32
+ ? Object.freeze({
33
+ ...report,
34
+ ...(report.migration
35
+ ? { migration: Object.freeze({ ...report.migration }) }
36
+ : {}),
37
+ })
38
+ : undefined;
39
+ }
40
+ /** Opens an independent session over the same repository storage. */
41
+ async openAnotherSession(options = {}) {
42
+ assertOpenAnotherSessionOptions(options);
43
+ return this.#runOperation(async () => new Lix(await this.binding.openAnotherSession(options)));
28
44
  }
29
45
  async execute(sql, params = [], options) {
30
46
  assertExecuteArgs("lix", sql, params, options);
31
- return this.#runOperation(async () => wrapExecuteResult(await this.binding.execute(sql, params.map((param, index) => toNativeValue(normalizeParam(param, index))), options)));
47
+ const { rowMode = "object", ...bindingOptions } = options ?? {};
48
+ return this.#runOperation(async () => wrapExecuteResult(await this.binding.execute(sql, params.map((param, index) => toNativeValue(normalizeParam(param, index))), bindingOptions), rowMode));
32
49
  }
33
50
  async executeBatch(statements, options) {
34
51
  const normalizedStatements = normalizeBatchStatements(statements, options);
52
+ const { rowMode = "object", ...bindingOptions } = options ?? {};
35
53
  return this.#runOperation(async () => {
36
- const results = await this.binding.executeBatch(normalizedStatements, options);
37
- return results.map(wrapExecuteBatchResult);
54
+ const results = await this.binding.executeBatch(normalizedStatements, bindingOptions);
55
+ return results.map((result) => wrapExecuteBatchResult(result, rowMode));
38
56
  });
39
57
  }
40
58
  observe(sql, params = []) {
@@ -82,8 +100,83 @@ export class Lix {
82
100
  async createBranch(options) {
83
101
  return this.#runOperation(() => this.binding.createBranch(options));
84
102
  }
85
- async createCheckpoint() {
86
- return this.#runOperation(() => this.binding.createCheckpoint());
103
+ /** Streams a deterministic snapshot of the complete Lix. */
104
+ exportSnapshot() {
105
+ let snapshot;
106
+ const start = () => {
107
+ if (snapshot)
108
+ return snapshot;
109
+ let finish;
110
+ const completed = new Promise((resolve) => {
111
+ finish = resolve;
112
+ });
113
+ let resolveBinding;
114
+ let rejectBinding;
115
+ const binding = new Promise((resolve, reject) => {
116
+ resolveBinding = resolve;
117
+ rejectBinding = reject;
118
+ });
119
+ const operation = this.#runOperation(async () => {
120
+ try {
121
+ resolveBinding(this.binding.exportSnapshot());
122
+ await completed;
123
+ }
124
+ catch (error) {
125
+ rejectBinding(error);
126
+ throw error;
127
+ }
128
+ });
129
+ // Pull observes setup errors through `binding`; this catch only prevents
130
+ // the lifecycle-tracking promise from becoming an unhandled rejection.
131
+ void operation.catch((error) => rejectBinding(error));
132
+ let finished = false;
133
+ let cancelPromise;
134
+ const active = {
135
+ binding,
136
+ finish: () => {
137
+ if (finished)
138
+ return;
139
+ finished = true;
140
+ this.#snapshotExports.delete(active);
141
+ finish();
142
+ },
143
+ cancel: () => (cancelPromise ??= (async () => {
144
+ try {
145
+ await (await binding).cancel();
146
+ }
147
+ finally {
148
+ active.finish();
149
+ }
150
+ })()),
151
+ };
152
+ snapshot = active;
153
+ this.#snapshotExports.add(active);
154
+ return snapshot;
155
+ };
156
+ return new ReadableStream({
157
+ pull: async (controller) => {
158
+ const active = start();
159
+ const binding = await active.binding;
160
+ try {
161
+ const chunk = await binding.next();
162
+ if (chunk == null) {
163
+ active.finish();
164
+ controller.close();
165
+ return;
166
+ }
167
+ controller.enqueue(chunk);
168
+ }
169
+ catch (error) {
170
+ await active.cancel().catch(() => undefined);
171
+ throw error;
172
+ }
173
+ },
174
+ cancel: async () => {
175
+ if (!snapshot)
176
+ return;
177
+ await snapshot.cancel();
178
+ },
179
+ }, { highWaterMark: 0 });
87
180
  }
88
181
  async undo() {
89
182
  return this.#runOperation(() => this.binding.undo());
@@ -131,6 +224,7 @@ export class Lix {
131
224
  }
132
225
  this.#observations.clear();
133
226
  this.closePromise = (async () => {
227
+ await Promise.allSettled([...this.#snapshotExports].map((snapshot) => snapshot.cancel()));
134
228
  await Promise.allSettled([...this.#inFlightOperations]);
135
229
  const results = await Promise.allSettled([
136
230
  Promise.resolve().then(() => this.binding.close()),
@@ -164,6 +258,20 @@ export class Lix {
164
258
  throw error;
165
259
  }
166
260
  }
261
+ function assertOpenAnotherSessionOptions(options) {
262
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
263
+ throw new TypeError("openAnotherSession() options must be an object");
264
+ }
265
+ for (const [name, value] of [
266
+ ["branchId", options.branchId],
267
+ ["accountId", options.accountId],
268
+ ]) {
269
+ if (value !== undefined &&
270
+ (typeof value !== "string" || value.length === 0)) {
271
+ throw new TypeError(`openAnotherSession() ${name} must be a non-empty string`);
272
+ }
273
+ }
274
+ }
167
275
  export class ObserveEvents {
168
276
  onClose;
169
277
  setup = {};
@@ -218,7 +326,8 @@ export class LixTransaction {
218
326
  }
219
327
  async execute(sql, params = [], options) {
220
328
  assertExecuteArgs("lixTransaction", sql, params, options);
221
- return wrapExecuteResult(await this.binding.execute(sql, params.map((param, index) => toNativeValue(normalizeParam(param, index))), options));
329
+ const { rowMode = "object", ...bindingOptions } = options ?? {};
330
+ return wrapExecuteResult(await this.binding.execute(sql, params.map((param, index) => toNativeValue(normalizeParam(param, index))), bindingOptions), rowMode);
222
331
  }
223
332
  async commit() {
224
333
  return this.finish("transaction.commit");
@@ -271,6 +380,11 @@ function assertExecuteArgs(receiver, sql, params, options) {
271
380
  typeof options.idempotencyKey !== "string") {
272
381
  throw invalidArgument("execute", "options.idempotencyKey", "string", typeof options.idempotencyKey, receiver);
273
382
  }
383
+ if (options.rowMode !== undefined &&
384
+ options.rowMode !== "object" &&
385
+ options.rowMode !== "array") {
386
+ throw invalidArgument("execute", "options.rowMode", '"object" | "array"', typeof options.rowMode, receiver);
387
+ }
274
388
  }
275
389
  function assertSqlArgs(operation, receiver, sql, params) {
276
390
  if (typeof sql !== "string") {
@@ -331,6 +445,11 @@ function assertBatchOptions(options) {
331
445
  typeof options.idempotencyKey !== "string") {
332
446
  throw invalidArgument("executeBatch", "options.idempotencyKey", "string", typeof options.idempotencyKey);
333
447
  }
448
+ if (options.rowMode !== undefined &&
449
+ options.rowMode !== "object" &&
450
+ options.rowMode !== "array") {
451
+ throw invalidArgument("lix.executeBatch", "options.rowMode", '"object" | "array"', typeof options.rowMode);
452
+ }
334
453
  }
335
454
  function withBatchStatementIndex(error, statementIndex) {
336
455
  if (!error || typeof error !== "object")
@@ -1,8 +1,7 @@
1
1
  import { Lix } from "./lix.js";
2
- import type { IndexedDbStorageOptions, OpenLixOptions } from "./types.js";
2
+ import type { OpenLixOptions } from "./types.js";
3
3
  export { Lix, LixTransaction, ObserveEvents } from "./lix.js";
4
- export declare class IndexedDbStorage {
5
- readonly name: string;
6
- constructor(options: IndexedDbStorageOptions);
7
- }
8
4
  export declare function openLix(options?: OpenLixOptions): Promise<Lix>;
5
+ export declare namespace openLix {
6
+ function fromSnapshot(source: ReadableStream<Uint8Array> | Uint8Array, options?: OpenLixOptions): Promise<Lix>;
7
+ }
package/dist/open-lix.js CHANGED
@@ -1,18 +1,11 @@
1
1
  import { Lix } from "./lix.js";
2
- import { isLixStorage } from "./storage-adapter.js";
2
+ import { isJsProviderLixStorage, isLixStorage, } from "./storage-adapter.js";
3
3
  export { Lix, LixTransaction, ObserveEvents } from "./lix.js";
4
4
  const openStorages = new WeakSet();
5
- const openIndexedDbStorageNames = new Set();
6
- export class IndexedDbStorage {
7
- name;
8
- constructor(options) {
9
- if (!options || typeof options.name !== "string" || options.name.length === 0) {
10
- throw new TypeError("IndexedDbStorage requires a non-empty name");
11
- }
12
- this.name = options.name;
13
- }
5
+ export function openLix(options = {}) {
6
+ return openLixInternal(options);
14
7
  }
15
- export async function openLix(options = {}) {
8
+ async function openLixInternal(options, snapshot) {
16
9
  if (!options || typeof options !== "object") {
17
10
  throw new TypeError("openLix() options must be an object");
18
11
  }
@@ -24,16 +17,53 @@ export async function openLix(options = {}) {
24
17
  typeof options.telemetry.onSpan !== "function")) {
25
18
  throw new TypeError("openLix() telemetry requires an onSpan callback");
26
19
  }
20
+ if (options.telemetry?.parentContext !== undefined &&
21
+ typeof options.telemetry.parentContext !== "function") {
22
+ throw new TypeError("openLix() telemetry parentContext must be a context provider function");
23
+ }
24
+ if (options.onProgress !== undefined &&
25
+ typeof options.onProgress !== "function") {
26
+ throw new TypeError("openLix() onProgress must be a function");
27
+ }
27
28
  if (options.server !== undefined) {
28
- const { openRemoteLixBinding } = await import("./remote/client.js");
29
- if ("storage" in options && options.storage !== undefined) {
30
- throw new TypeError("openLix() remote mode does not accept storage");
29
+ if (snapshot) {
30
+ throw new TypeError("openLix.fromSnapshot() does not accept server mode");
31
+ }
32
+ if (options.server.mode === "remote") {
33
+ const { openRemoteLixBinding } = await import("./remote/client.js");
34
+ if ("storage" in options && options.storage !== undefined) {
35
+ throw new TypeError("openLix() remote mode does not accept storage");
36
+ }
37
+ return new Lix(await openRemoteLixBinding(options.server));
38
+ }
39
+ }
40
+ const syncServer = options.server?.mode === "sync"
41
+ ? {
42
+ url: new URL(options.server.url).toString(),
43
+ headers: options.server.headers,
44
+ fetch: options.server.fetch,
31
45
  }
32
- return new Lix(await openRemoteLixBinding(options.server));
46
+ : undefined;
47
+ if (syncServer?.fetch !== undefined && typeof syncServer.fetch !== "function") {
48
+ throw new TypeError("openLix() sync server fetch must be a function");
49
+ }
50
+ if (syncServer?.headers !== undefined && typeof syncServer.headers !== "function") {
51
+ // Validate static headers before opening a worker/native runtime.
52
+ new Headers(syncServer.headers);
53
+ }
54
+ if (options.server !== undefined && syncServer === undefined) {
55
+ throw new TypeError("openLix() server mode must be 'remote' or 'sync'");
56
+ }
57
+ if (syncServer !== undefined && options.storage === undefined) {
58
+ throw new TypeError("openLix() sync mode requires a durability-capable storage adapter");
33
59
  }
34
60
  const { openLixWorkerBinding } = await import("./worker/client.js");
35
61
  if (options.storage === undefined) {
36
- return new Lix(await openLixWorkerBinding({ kind: "memory" }, undefined, options.telemetry));
62
+ const binding = await openLixWorkerBinding({ kind: "memory" }, undefined, options.telemetry, syncServer, options.onProgress, snapshot);
63
+ return new Lix(binding);
64
+ }
65
+ if (isJsProviderLixStorage(options.storage)) {
66
+ return openJsProviderStorage(options.storage, options.telemetry, syncServer, options.onProgress, snapshot);
37
67
  }
38
68
  if (isLixStorage(options.storage)) {
39
69
  const storage = options.storage;
@@ -47,12 +77,13 @@ export async function openLix(options = {}) {
47
77
  openStorages.delete(storage);
48
78
  };
49
79
  try {
50
- binding = await openLixWorkerBinding(storage.lixStorage.config, disconnect, options.telemetry);
80
+ binding = await openLixWorkerBinding(storage.lixStorage.config, disconnect, options.telemetry, syncServer, options.onProgress, snapshot);
81
+ const routed = routeStorageBinding(binding);
51
82
  storage.lixStorage.connect({
52
- importFilesystemPaths: (paths) => binding.importFilesystemPaths(paths),
53
- syncDiskToLix: () => binding.syncDiskToLix(),
83
+ importFilesystemPaths: (paths) => routed.current().importFilesystemPaths(paths),
84
+ syncDiskToLix: () => routed.current().syncDiskToLix(),
54
85
  });
55
- return new Lix(binding);
86
+ return new Lix(routed.binding);
56
87
  }
57
88
  catch (error) {
58
89
  disconnect();
@@ -60,25 +91,135 @@ export async function openLix(options = {}) {
60
91
  throw error;
61
92
  }
62
93
  }
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;
94
+ throw new TypeError("openLix() requires a Lix storage adapter");
95
+ }
96
+ function routeStorageBinding(root) {
97
+ const live = new Set();
98
+ const wrap = (binding) => {
99
+ live.add(binding);
100
+ return new Proxy(binding, {
101
+ get(target, property, receiver) {
102
+ if (property === "openAnotherSession") {
103
+ return async (options) => wrap(await target.openAnotherSession(options));
104
+ }
105
+ if (property === "close") {
106
+ return async () => {
107
+ try {
108
+ await target.close();
109
+ }
110
+ finally {
111
+ live.delete(target);
112
+ }
113
+ };
114
+ }
115
+ const value = Reflect.get(target, property, receiver);
116
+ return typeof value === "function" ? value.bind(target) : value;
117
+ },
118
+ });
119
+ };
120
+ return {
121
+ binding: wrap(root),
122
+ current: () => {
123
+ const bindings = [...live];
124
+ return bindings[bindings.length - 1] ?? root;
125
+ },
126
+ };
127
+ }
128
+ async function openJsProviderStorage(storage, telemetry, syncServer, onProgress, snapshot) {
129
+ const { openLixWorkerBinding } = await import("./worker/client.js");
130
+ if (openStorages.has(storage))
131
+ throw storageAlreadyOpen();
132
+ openStorages.add(storage);
133
+ let binding;
134
+ try {
135
+ const opened = await openLixWorkerBinding({
136
+ kind: "jsStorage",
137
+ moduleUrl: storage.lixStorage.moduleUrl,
138
+ options: storage.lixStorage.options,
139
+ }, () => openStorages.delete(storage), telemetry, syncServer, onProgress, snapshot);
140
+ binding = opened;
141
+ return new Lix(opened);
142
+ }
143
+ catch (error) {
144
+ openStorages.delete(storage);
145
+ await binding?.close().catch(() => undefined);
146
+ throw error;
147
+ }
148
+ }
149
+ (function (openLix) {
150
+ async function fromSnapshot(source, options = {}) {
151
+ const prepared = prepareSnapshotSource(source);
71
152
  try {
72
- binding = await openLixWorkerBinding({ kind: "indexedDb", name: databaseName }, () => openIndexedDbStorageNames.delete(databaseName), options.telemetry);
73
- return new Lix(binding);
153
+ return await openLixInternal(options, prepared.stream);
74
154
  }
75
155
  catch (error) {
76
- openIndexedDbStorageNames.delete(databaseName);
77
- await binding?.close().catch(() => undefined);
156
+ await prepared.cancel(error);
78
157
  throw error;
79
158
  }
80
159
  }
81
- throw new TypeError("openLix() requires a Lix storage adapter or IndexedDbStorage");
160
+ openLix.fromSnapshot = fromSnapshot;
161
+ })(openLix || (openLix = {}));
162
+ function prepareSnapshotSource(source) {
163
+ if (source instanceof Uint8Array) {
164
+ let sent = false;
165
+ const stream = new ReadableStream({
166
+ pull(controller) {
167
+ if (!sent) {
168
+ sent = true;
169
+ controller.enqueue(source);
170
+ }
171
+ controller.close();
172
+ },
173
+ }, { highWaterMark: 0 });
174
+ return {
175
+ stream,
176
+ cancel: async (reason) => {
177
+ await stream.cancel(reason).catch(() => undefined);
178
+ },
179
+ };
180
+ }
181
+ if (!source || typeof source.getReader !== "function") {
182
+ throw new TypeError("openLix.fromSnapshot() requires a ReadableStream<Uint8Array> or Uint8Array");
183
+ }
184
+ // Acquire the caller's stream before openLixInternal can start a native or
185
+ // worker restore. A locked source therefore fails without creating backend work.
186
+ const reader = source.getReader();
187
+ let released = false;
188
+ const release = () => {
189
+ if (released)
190
+ return;
191
+ released = true;
192
+ reader.releaseLock();
193
+ };
194
+ const cancel = async (reason) => {
195
+ if (released)
196
+ return;
197
+ try {
198
+ await reader.cancel(reason);
199
+ }
200
+ finally {
201
+ release();
202
+ }
203
+ };
204
+ const stream = new ReadableStream({
205
+ async pull(controller) {
206
+ try {
207
+ const result = await reader.read();
208
+ if (result.done) {
209
+ release();
210
+ controller.close();
211
+ return;
212
+ }
213
+ controller.enqueue(result.value);
214
+ }
215
+ catch (error) {
216
+ release();
217
+ controller.error(error);
218
+ }
219
+ },
220
+ cancel,
221
+ }, { highWaterMark: 0 });
222
+ return { stream, cancel };
82
223
  }
83
224
  function storageAlreadyOpen() {
84
225
  const error = new Error("openLix() storage is already open; close the existing Lix or create a new storage adapter");
@@ -1,8 +1,7 @@
1
1
  import type { LixBinding } from "../binding-types.js";
2
2
  import type { RemoteLixServerOptions } from "../types.js";
3
- import { type ServerProtocolHandshakeRequest } from "./server-protocol.js";
4
3
  type RemoteLixClientOptions = {
5
- initialActiveBranchId?: ServerProtocolHandshakeRequest["activeBranchId"];
4
+ initialActiveBranchId?: string;
6
5
  };
7
6
  export declare function openRemoteLixBinding(options: RemoteLixServerOptions, clientOptions?: RemoteLixClientOptions): Promise<LixBinding>;
8
7
  export {};