@lix-js/sdk 0.12.3 → 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.
Files changed (50) hide show
  1. package/README.md +40 -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 +25 -10
  5. package/dist/binding.node-wasm.d.ts +2 -2
  6. package/dist/binding.node-wasm.js +8 -4
  7. package/dist/binding.node.d.ts +3 -3
  8. package/dist/binding.node.js +70 -13
  9. package/dist/browser-wasm-init.d.ts +14 -0
  10. package/dist/browser-wasm-init.js +16 -0
  11. package/dist/bundled-plugins/plugin_csv.lixplugin +0 -0
  12. package/dist/bundled-plugins/plugin_markdown.lixplugin +0 -0
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +2 -2
  15. package/dist/lix.d.ts +27 -5
  16. package/dist/lix.js +132 -6
  17. package/dist/open-lix.d.ts +4 -5
  18. package/dist/open-lix.js +173 -33
  19. package/dist/remote/client.d.ts +1 -2
  20. package/dist/remote/client.js +58 -1151
  21. package/dist/remote/server-protocol.d.ts +5 -6
  22. package/dist/remote/server-protocol.js +40 -6
  23. package/dist/result.d.ts +6 -13
  24. package/dist/result.js +9 -35
  25. package/dist/snapshot-restore.d.ts +6 -0
  26. package/dist/snapshot-restore.js +101 -0
  27. package/dist/storage-adapter.d.ts +175 -19
  28. package/dist/storage-adapter.js +21 -0
  29. package/dist/types.d.ts +101 -31
  30. package/dist/value.d.ts +1 -0
  31. package/dist/value.js +8 -0
  32. package/dist/wasm/lix_js_sdk.d.ts +119 -24
  33. package/dist/wasm/lix_js_sdk.js +599 -78
  34. package/dist/wasm/lix_js_sdk_bg.wasm +0 -0
  35. package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +47 -6
  36. package/dist/worker/client.d.ts +27 -5
  37. package/dist/worker/client.js +458 -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 +2 -1
  42. package/dist/worker/host.js +283 -37
  43. package/dist/worker/protocol.d.ts +80 -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,90 @@ 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
+ const exportSnapshot = this.binding.exportSnapshot;
122
+ if (!exportSnapshot) {
123
+ const error = new Error("snapshot export is not available for remote Lix handles");
124
+ error.name = "LixError";
125
+ error.code = "LIX_UNSUPPORTED_STORAGE";
126
+ throw error;
127
+ }
128
+ resolveBinding(exportSnapshot.call(this.binding));
129
+ await completed;
130
+ }
131
+ catch (error) {
132
+ rejectBinding(error);
133
+ throw error;
134
+ }
135
+ });
136
+ // Pull observes setup errors through `binding`; this catch only prevents
137
+ // the lifecycle-tracking promise from becoming an unhandled rejection.
138
+ void operation.catch((error) => rejectBinding(error));
139
+ let finished = false;
140
+ let cancelPromise;
141
+ const active = {
142
+ binding,
143
+ finish: () => {
144
+ if (finished)
145
+ return;
146
+ finished = true;
147
+ this.#snapshotExports.delete(active);
148
+ finish();
149
+ },
150
+ cancel: () => (cancelPromise ??= (async () => {
151
+ try {
152
+ await (await binding).cancel();
153
+ }
154
+ finally {
155
+ active.finish();
156
+ }
157
+ })()),
158
+ };
159
+ snapshot = active;
160
+ this.#snapshotExports.add(active);
161
+ return snapshot;
162
+ };
163
+ return new ReadableStream({
164
+ pull: async (controller) => {
165
+ const active = start();
166
+ const binding = await active.binding;
167
+ try {
168
+ const chunk = await binding.next();
169
+ if (chunk == null) {
170
+ active.finish();
171
+ controller.close();
172
+ return;
173
+ }
174
+ controller.enqueue(chunk);
175
+ }
176
+ catch (error) {
177
+ await active.cancel().catch(() => undefined);
178
+ throw error;
179
+ }
180
+ },
181
+ cancel: async () => {
182
+ if (!snapshot)
183
+ return;
184
+ await snapshot.cancel();
185
+ },
186
+ }, { highWaterMark: 0 });
87
187
  }
88
188
  async undo() {
89
189
  return this.#runOperation(() => this.binding.undo());
@@ -131,6 +231,7 @@ export class Lix {
131
231
  }
132
232
  this.#observations.clear();
133
233
  this.closePromise = (async () => {
234
+ await Promise.allSettled([...this.#snapshotExports].map((snapshot) => snapshot.cancel()));
134
235
  await Promise.allSettled([...this.#inFlightOperations]);
135
236
  const results = await Promise.allSettled([
136
237
  Promise.resolve().then(() => this.binding.close()),
@@ -164,6 +265,20 @@ export class Lix {
164
265
  throw error;
165
266
  }
166
267
  }
268
+ function assertOpenAnotherSessionOptions(options) {
269
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
270
+ throw new TypeError("openAnotherSession() options must be an object");
271
+ }
272
+ for (const [name, value] of [
273
+ ["branchId", options.branchId],
274
+ ["accountId", options.accountId],
275
+ ]) {
276
+ if (value !== undefined &&
277
+ (typeof value !== "string" || value.length === 0)) {
278
+ throw new TypeError(`openAnotherSession() ${name} must be a non-empty string`);
279
+ }
280
+ }
281
+ }
167
282
  export class ObserveEvents {
168
283
  onClose;
169
284
  setup = {};
@@ -218,7 +333,8 @@ export class LixTransaction {
218
333
  }
219
334
  async execute(sql, params = [], options) {
220
335
  assertExecuteArgs("lixTransaction", sql, params, options);
221
- return wrapExecuteResult(await this.binding.execute(sql, params.map((param, index) => toNativeValue(normalizeParam(param, index))), options));
336
+ const { rowMode = "object", ...bindingOptions } = options ?? {};
337
+ return wrapExecuteResult(await this.binding.execute(sql, params.map((param, index) => toNativeValue(normalizeParam(param, index))), bindingOptions), rowMode);
222
338
  }
223
339
  async commit() {
224
340
  return this.finish("transaction.commit");
@@ -271,6 +387,11 @@ function assertExecuteArgs(receiver, sql, params, options) {
271
387
  typeof options.idempotencyKey !== "string") {
272
388
  throw invalidArgument("execute", "options.idempotencyKey", "string", typeof options.idempotencyKey, receiver);
273
389
  }
390
+ if (options.rowMode !== undefined &&
391
+ options.rowMode !== "object" &&
392
+ options.rowMode !== "array") {
393
+ throw invalidArgument("execute", "options.rowMode", '"object" | "array"', typeof options.rowMode, receiver);
394
+ }
274
395
  }
275
396
  function assertSqlArgs(operation, receiver, sql, params) {
276
397
  if (typeof sql !== "string") {
@@ -331,6 +452,11 @@ function assertBatchOptions(options) {
331
452
  typeof options.idempotencyKey !== "string") {
332
453
  throw invalidArgument("executeBatch", "options.idempotencyKey", "string", typeof options.idempotencyKey);
333
454
  }
455
+ if (options.rowMode !== undefined &&
456
+ options.rowMode !== "object" &&
457
+ options.rowMode !== "array") {
458
+ throw invalidArgument("lix.executeBatch", "options.rowMode", '"object" | "array"', typeof options.rowMode);
459
+ }
334
460
  }
335
461
  function withBatchStatementIndex(error, statementIndex) {
336
462
  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,52 @@ 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
+ return new Lix(await openLixWorkerBinding({ kind: "memory" }, undefined, options.telemetry, syncServer, options.onProgress, snapshot));
63
+ }
64
+ if (isJsProviderLixStorage(options.storage)) {
65
+ return openJsProviderStorage(options.storage, options.telemetry, syncServer, options.onProgress, snapshot);
37
66
  }
38
67
  if (isLixStorage(options.storage)) {
39
68
  const storage = options.storage;
@@ -47,12 +76,13 @@ export async function openLix(options = {}) {
47
76
  openStorages.delete(storage);
48
77
  };
49
78
  try {
50
- binding = await openLixWorkerBinding(storage.lixStorage.config, disconnect, options.telemetry);
79
+ binding = await openLixWorkerBinding(storage.lixStorage.config, disconnect, options.telemetry, syncServer, options.onProgress, snapshot);
80
+ const routed = routeStorageBinding(binding);
51
81
  storage.lixStorage.connect({
52
- importFilesystemPaths: (paths) => binding.importFilesystemPaths(paths),
53
- syncDiskToLix: () => binding.syncDiskToLix(),
82
+ importFilesystemPaths: (paths) => routed.current().importFilesystemPaths(paths),
83
+ syncDiskToLix: () => routed.current().syncDiskToLix(),
54
84
  });
55
- return new Lix(binding);
85
+ return new Lix(routed.binding);
56
86
  }
57
87
  catch (error) {
58
88
  disconnect();
@@ -60,25 +90,135 @@ export async function openLix(options = {}) {
60
90
  throw error;
61
91
  }
62
92
  }
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;
93
+ throw new TypeError("openLix() requires a Lix storage adapter");
94
+ }
95
+ function routeStorageBinding(root) {
96
+ const live = new Set();
97
+ const wrap = (binding) => {
98
+ live.add(binding);
99
+ return new Proxy(binding, {
100
+ get(target, property, receiver) {
101
+ if (property === "openAnotherSession") {
102
+ return async (options) => wrap(await target.openAnotherSession(options));
103
+ }
104
+ if (property === "close") {
105
+ return async () => {
106
+ try {
107
+ await target.close();
108
+ }
109
+ finally {
110
+ live.delete(target);
111
+ }
112
+ };
113
+ }
114
+ const value = Reflect.get(target, property, receiver);
115
+ return typeof value === "function" ? value.bind(target) : value;
116
+ },
117
+ });
118
+ };
119
+ return {
120
+ binding: wrap(root),
121
+ current: () => {
122
+ const bindings = [...live];
123
+ return bindings[bindings.length - 1] ?? root;
124
+ },
125
+ };
126
+ }
127
+ async function openJsProviderStorage(storage, telemetry, syncServer, onProgress, snapshot) {
128
+ const { openLixWorkerBinding } = await import("./worker/client.js");
129
+ if (openStorages.has(storage))
130
+ throw storageAlreadyOpen();
131
+ openStorages.add(storage);
132
+ let binding;
133
+ try {
134
+ const opened = await openLixWorkerBinding({
135
+ kind: "jsStorage",
136
+ moduleUrl: storage.lixStorage.moduleUrl,
137
+ options: storage.lixStorage.options,
138
+ }, () => openStorages.delete(storage), telemetry, syncServer, onProgress, snapshot);
139
+ binding = opened;
140
+ return new Lix(opened);
141
+ }
142
+ catch (error) {
143
+ openStorages.delete(storage);
144
+ await binding?.close().catch(() => undefined);
145
+ throw error;
146
+ }
147
+ }
148
+ (function (openLix) {
149
+ async function fromSnapshot(source, options = {}) {
150
+ const prepared = prepareSnapshotSource(source);
71
151
  try {
72
- binding = await openLixWorkerBinding({ kind: "indexedDb", name: databaseName }, () => openIndexedDbStorageNames.delete(databaseName), options.telemetry);
73
- return new Lix(binding);
152
+ return await openLixInternal(options, prepared.stream);
74
153
  }
75
154
  catch (error) {
76
- openIndexedDbStorageNames.delete(databaseName);
77
- await binding?.close().catch(() => undefined);
155
+ await prepared.cancel(error);
78
156
  throw error;
79
157
  }
80
158
  }
81
- throw new TypeError("openLix() requires a Lix storage adapter or IndexedDbStorage");
159
+ openLix.fromSnapshot = fromSnapshot;
160
+ })(openLix || (openLix = {}));
161
+ function prepareSnapshotSource(source) {
162
+ if (source instanceof Uint8Array) {
163
+ let sent = false;
164
+ const stream = new ReadableStream({
165
+ pull(controller) {
166
+ if (!sent) {
167
+ sent = true;
168
+ controller.enqueue(source);
169
+ }
170
+ controller.close();
171
+ },
172
+ }, { highWaterMark: 0 });
173
+ return {
174
+ stream,
175
+ cancel: async (reason) => {
176
+ await stream.cancel(reason).catch(() => undefined);
177
+ },
178
+ };
179
+ }
180
+ if (!source || typeof source.getReader !== "function") {
181
+ throw new TypeError("openLix.fromSnapshot() requires a ReadableStream<Uint8Array> or Uint8Array");
182
+ }
183
+ // Acquire the caller's stream before openLixInternal can start a native or
184
+ // worker restore. A locked source therefore fails without creating backend work.
185
+ const reader = source.getReader();
186
+ let released = false;
187
+ const release = () => {
188
+ if (released)
189
+ return;
190
+ released = true;
191
+ reader.releaseLock();
192
+ };
193
+ const cancel = async (reason) => {
194
+ if (released)
195
+ return;
196
+ try {
197
+ await reader.cancel(reason);
198
+ }
199
+ finally {
200
+ release();
201
+ }
202
+ };
203
+ const stream = new ReadableStream({
204
+ async pull(controller) {
205
+ try {
206
+ const result = await reader.read();
207
+ if (result.done) {
208
+ release();
209
+ controller.close();
210
+ return;
211
+ }
212
+ controller.enqueue(result.value);
213
+ }
214
+ catch (error) {
215
+ release();
216
+ controller.error(error);
217
+ }
218
+ },
219
+ cancel,
220
+ }, { highWaterMark: 0 });
221
+ return { stream, cancel };
82
222
  }
83
223
  function storageAlreadyOpen() {
84
224
  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 {};