@lix-js/sdk 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +48 -76
  2. package/dist/binding-types.d.ts +6 -15
  3. package/dist/binding.browser.js +18 -5
  4. package/dist/binding.node.js +5 -6
  5. package/dist/bundled-plugins/plugin_csv.lixplugin +0 -0
  6. package/dist/bundled-plugins/plugin_markdown.lixplugin +0 -0
  7. package/dist/errors.d.ts +0 -2
  8. package/dist/errors.js +0 -13
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.js +1 -1
  11. package/dist/indexeddb-backend.d.ts +19 -0
  12. package/dist/indexeddb-backend.js +121 -0
  13. package/dist/lix.d.ts +1 -4
  14. package/dist/lix.js +21 -52
  15. package/dist/open-lix.d.ts +4 -9
  16. package/dist/open-lix.js +45 -135
  17. package/dist/remote/client.d.ts +2 -3
  18. package/dist/remote/client.js +13 -21
  19. package/dist/remote/{protocol.d.ts → server-protocol.d.ts} +35 -33
  20. package/dist/remote/{protocol.js → server-protocol.js} +24 -17
  21. package/dist/storage-adapter.d.ts +30 -0
  22. package/dist/storage-adapter.js +12 -0
  23. package/dist/types.d.ts +14 -24
  24. package/dist/value.d.ts +2 -1
  25. package/dist/value.js +23 -10
  26. package/dist/wasm/lix_js_sdk.d.ts +5 -10
  27. package/dist/wasm/lix_js_sdk.js +33 -45
  28. package/dist/wasm/lix_js_sdk_bg.wasm +0 -0
  29. package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +3 -6
  30. package/dist/worker/client.d.ts +1 -12
  31. package/dist/worker/client.js +0 -161
  32. package/dist/worker/host.js +0 -23
  33. package/dist/worker/protocol.d.ts +1 -15
  34. package/package.json +8 -12
  35. package/dist/client-state.d.ts +0 -53
  36. package/dist/client-state.js +0 -315
  37. package/dist/local-storage-adapter.d.ts +0 -26
  38. package/dist/local-storage-adapter.js +0 -117
  39. package/dist/snapshot-persistence.d.ts +0 -7
  40. package/dist/snapshot-persistence.js +0 -26
package/README.md CHANGED
@@ -22,28 +22,28 @@ console.log(result.rows[0]?.get("message"));
22
22
  await lix.close();
23
23
  ```
24
24
 
25
- ## Remote workspaces
25
+ ## Remote repositories
26
26
 
27
- Use the same Lix client as a thin client against a hosted workspace:
27
+ Use the same Lix client as a thin client against a hosted repository:
28
28
 
29
29
  ```ts
30
30
  const lix = await openLix({
31
- server: {
32
- mode: "remote",
33
- url: "https://lixray.com/@namespace/workspace",
34
- headers: async () => ({
35
- Authorization: `Bearer ${await accessToken()}`,
36
- }),
37
- },
31
+ server: {
32
+ mode: "remote",
33
+ url: "https://example.com/repositories/acme",
34
+ headers: async () => ({
35
+ Authorization: `Bearer ${await accessToken()}`,
36
+ }),
37
+ },
38
38
  });
39
39
 
40
40
  const files = lix.observe("SELECT path FROM lix_file ORDER BY path");
41
41
  const initial = await files.next();
42
42
 
43
- await lix.execute(
44
- "INSERT INTO lix_file (path, content) VALUES ($1, $2)",
45
- ["/hello.txt", new TextEncoder().encode("hello")],
46
- );
43
+ await lix.execute("INSERT INTO lix_file (path, content) VALUES ($1, $2)", [
44
+ "/hello.txt",
45
+ new TextEncoder().encode("hello"),
46
+ ]);
47
47
  const update = await files.next();
48
48
 
49
49
  files.close();
@@ -55,59 +55,29 @@ open a local engine. Dynamic headers are resolved for every request and
55
55
  observation reconnect. An injected `fetch` can route requests through a service
56
56
  binding or another authorized server-side transport.
57
57
 
58
- Browser clients can opt into private, durable client state with the local
59
- storage adapter:
60
-
61
- ```ts
62
- import { openLix } from "@lix-js/sdk";
63
- import { LocalStorage } from "@lix-js/sdk/local-storage-adapter";
64
-
65
- const lix = await openLix({
66
- server: {
67
- mode: "remote",
68
- url: "https://lixray.com/@namespace/workspace",
69
- },
70
- storage: new LocalStorage(),
71
- });
72
-
73
- const previousUiState = lix.clientState.get("atelier-ui");
74
- await lix.clientState.set("atelier-ui", { sidebar: "history" });
75
- ```
76
-
77
- `lix.clientState` is hydrated before `openLix()` resolves, so reads are
78
- synchronous. Its JSON values and the client's active branch are stored in a
79
- private local Lix snapshot; workspace SQL continues to execute only on the
80
- server. Reopening the same remote URL with the same storage restores both. Each
81
- remote server session is branch-pinned, so switching one client does not switch
82
- another client.
83
-
84
- After a remote branch switch succeeds, saving that branch as the next-reopen
85
- preference is best effort: a client-storage failure does not turn the completed
86
- server switch into a rejected operation. Explicit `lix.clientState.set()` and
87
- `.delete()` calls report durability failures to their caller. Because the local
88
- Rust transaction has already committed, `get()` continues to expose that live
89
- session value; a later successful snapshot save can make it durable.
58
+ Remote server sessions are branch-pinned, so switching one client does not
59
+ switch another client. Browser-local application state belongs to the
60
+ application rather than the remote Lix handle.
90
61
 
91
62
  Filesystem sync uses native Node.js dependencies:
92
63
 
93
64
  ```ts
94
- import { LocalFilesystem, openLix } from "@lix-js/sdk";
65
+ import { openLix } from "@lix-js/sdk";
66
+ import { FilesystemStorage } from "@lix-js/storage-filesystem";
95
67
 
96
68
  const lix = await openLix({
97
- storage: new LocalFilesystem({
98
- path: "./workspace",
99
- syncAllFiles: true,
100
- }),
69
+ storage: new FilesystemStorage({ path: "./repository" }),
101
70
  });
102
71
 
103
72
  await lix.execute(
104
- "INSERT INTO lix_file (path, content) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET content = excluded.content",
105
- ["/hello.txt", new TextEncoder().encode("world")],
73
+ "INSERT INTO lix_file (path, content) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET content = excluded.content",
74
+ ["/hello.txt", new TextEncoder().encode("world")],
106
75
  );
107
76
 
108
- const result = await lix.execute("SELECT content FROM lix_file WHERE path = $1", [
109
- "/hello.txt",
110
- ]);
77
+ const result = await lix.execute(
78
+ "SELECT content FROM lix_file WHERE path = $1",
79
+ ["/hello.txt"],
80
+ );
111
81
  const bytes = result.rows[0]?.value("content").asBytes();
112
82
 
113
83
  console.log(bytes && new TextDecoder().decode(bytes));
@@ -140,8 +110,8 @@ const draft = await lix.createBranch({ name: "Draft" });
140
110
 
141
111
  await lix.switchBranch({ branchId: draft.id });
142
112
  await lix.execute(
143
- "INSERT INTO lix_file (path, content) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET content = excluded.content",
144
- ["/status.txt", new TextEncoder().encode("draft")],
113
+ "INSERT INTO lix_file (path, content) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET content = excluded.content",
114
+ ["/status.txt", new TextEncoder().encode("draft")],
145
115
  );
146
116
 
147
117
  await lix.switchBranch({ branchId: main });
@@ -155,30 +125,31 @@ const merge = await lix.mergeBranch({ sourceBranchId: draft.id });
155
125
  const tx = await lix.beginTransaction();
156
126
 
157
127
  try {
158
- await tx.execute(
159
- "INSERT INTO lix_file (path, content) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET content = excluded.content",
160
- ["/a.txt", new TextEncoder().encode("1")],
161
- );
162
- await tx.execute(
163
- "INSERT INTO lix_file (path, content) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET content = excluded.content",
164
- ["/b.txt", new TextEncoder().encode("2")],
165
- );
166
- await tx.commit();
128
+ await tx.execute(
129
+ "INSERT INTO lix_file (path, content) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET content = excluded.content",
130
+ ["/a.txt", new TextEncoder().encode("1")],
131
+ );
132
+ await tx.execute(
133
+ "INSERT INTO lix_file (path, content) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET content = excluded.content",
134
+ ["/b.txt", new TextEncoder().encode("2")],
135
+ );
136
+ await tx.commit();
167
137
  } catch (error) {
168
- await tx.rollback();
169
- throw error;
138
+ await tx.rollback();
139
+ throw error;
170
140
  }
171
141
  ```
172
142
 
173
143
  ## Notes
174
144
 
175
- - `openLix()` opens a fresh in-memory Lix. Pass `new LocalFilesystem({ path, syncAllFiles: true })` for a filesystem workspace directory backed by `<path>/.lix/.internal/rocksdb`.
176
- - Pass `new LocalFilesystem({ path, lixDir, syncAllFiles: true })` for filesystem sync with repository metadata in an external `.lix` directory and no workspace `.lix` directory.
177
- - Pass `syncAllFiles: false` to start filesystem sync with no regular workspace files, then call `storage.importPaths(["notes/today.md"])` on the `LocalFilesystem` instance to sync selected files. Imported paths are exact workspace-relative file paths, not directories or globs.
178
- - In browsers, local mode and remote mode with client storage load the Rust
179
- engine as WebAssembly. Supplying a snapshot storage adapter persists that
180
- local Lix; in remote mode, the local engine contains only client state.
181
- - `LocalFilesystem` is Node.js-only. Constructing it is safe in
145
+ - `openLix()` opens a fresh in-memory Lix. Install `@lix-js/storage-filesystem` and pass `new FilesystemStorage({ path })` for a filesystem repository directory backed by `<path>/.lix/.internal/rocksdb`.
146
+ - In browsers, pass `new IndexedDbStorage({ name })` to persist a complete local Lix across reloads.
147
+ - Only one Lix handle may open an IndexedDB storage name at a time, including across browser tabs.
148
+ - Pass `syncAllFiles: false` to start filesystem sync with no regular repository files, then call `storage.importPaths(["notes/today.md"])` on the `FilesystemStorage` instance to sync selected files. Imported paths are exact repository-relative file paths, not directories or globs.
149
+ - In browsers, local mode and remote mode with IndexedDB storage load the Rust
150
+ engine as WebAssembly. In remote mode, the local engine contains only client
151
+ state.
152
+ - `FilesystemStorage` is Node.js-only. Constructing it is safe in
182
153
  shared code, but passing one to `openLix()` in a browser throws an error.
183
154
  - The package is ESM-only.
184
155
  - The package uses conditional ESM imports internally: Node.js resolves the
@@ -206,8 +177,9 @@ try {
206
177
  Hosts that apply one policy to every response can use
207
178
  `script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'` globally
208
179
  instead. Worker-scoped headers keep those permissions out of the page.
180
+
209
181
  - SQL parameters use normal JavaScript values: `string`, finite `number`, `boolean`, `Uint8Array`, `null`, JSON-compatible arrays, and JSON-compatible plain objects.
210
- - Use `Value.integer(...)`, `Value.real(...)`, `Value.text(...)`, `Value.json(...)`, or `Value.blob(...)` only when you need to pass an explicit native Lix value.
182
+ - Use `Value.integer(...)`, `Value.real(...)`, `Value.text(...)`, `Value.jsonb(...)`, `Value.timestamptz(...)`, or `Value.blob(...)` only when you need to pass an explicit native Lix value.
211
183
 
212
184
  ## Browser development
213
185
 
@@ -1,5 +1,6 @@
1
- import type { CreateBranchOptions, CreateBranchReceipt, CreateCheckpointReceipt, UndoReceipt, RedoReceipt, ExecuteOptions, LixBatchOptions, MergeBranchOptions, MergeBranchPreview, MergeBranchReceipt, SwitchBranchOptions, SwitchBranchReceipt, LixTelemetrySpan, JsonValue } from "./types.js";
1
+ import type { CreateBranchOptions, CreateBranchReceipt, CreateCheckpointReceipt, UndoReceipt, RedoReceipt, ExecuteOptions, LixBatchOptions, MergeBranchOptions, MergeBranchPreview, MergeBranchReceipt, SwitchBranchOptions, SwitchBranchReceipt, LixTelemetrySpan } from "./types.js";
2
2
  import type { NativeLixValue } from "./value.js";
3
+ import type { LixStorageAdapterConfig } from "./storage-adapter.js";
3
4
  export type BindingExecuteResult = {
4
5
  statementIndex?: number;
5
6
  label?: string;
@@ -30,13 +31,6 @@ export type LixBinding = {
30
31
  beginTransaction(): Promise<LixTransactionBinding>;
31
32
  activeBranchId(): Promise<string>;
32
33
  activeAccountId(): Promise<string>;
33
- clientStateEntries?(): Promise<Array<{
34
- key: string;
35
- value: JsonValue;
36
- }>>;
37
- clientStateGet?(key: string): Promise<JsonValue | undefined>;
38
- clientStateSet?(key: string, value: JsonValue): Promise<void>;
39
- clientStateDelete?(key: string): Promise<void>;
40
34
  createBranch(options: CreateBranchOptions): Promise<CreateBranchReceipt>;
41
35
  createCheckpoint(): Promise<CreateCheckpointReceipt>;
42
36
  undo(): Promise<UndoReceipt>;
@@ -46,7 +40,7 @@ export type LixBinding = {
46
40
  mergeBranchPreview(options: MergeBranchOptions): Promise<MergeBranchPreview>;
47
41
  mergeBranch(options: MergeBranchOptions): Promise<MergeBranchReceipt>;
48
42
  syncDiskToLix(): Promise<void>;
49
- /** Internal snapshot capability implemented by browser memory bindings. */
43
+ /** Explicit snapshot utility available on direct in-memory WASM bindings. */
50
44
  exportSnapshot?(): Promise<Uint8Array>;
51
45
  close(): Promise<void>;
52
46
  };
@@ -62,10 +56,7 @@ export type ObserveEventsBinding = {
62
56
  export type TelemetryDispatch = (span: LixTelemetrySpan) => void;
63
57
  export type LixStorageConfig = {
64
58
  kind: "memory";
65
- snapshot?: Uint8Array;
66
59
  } | {
67
- kind: "localFilesystem";
68
- path: string;
69
- lixDir?: string;
70
- syncAllFiles: boolean;
71
- };
60
+ kind: "indexedDb";
61
+ name: string;
62
+ } | LixStorageAdapterConfig;
@@ -1,14 +1,27 @@
1
+ import { IndexedDbBackend } from "./indexeddb-backend.js";
1
2
  // Generated before TypeScript compilation and emitted beside this module.
2
3
  // @ts-expect-error Generated by build:wasm.
3
- import initWasm, { openMemoryFromSnapshot } from "./wasm/lix_js_sdk.js";
4
+ import initWasm, { openIndexedDb, openMemory } from "./wasm/lix_js_sdk.js";
4
5
  let wasmInitialized;
5
6
  function initializeWasm() {
6
7
  return (wasmInitialized ??= initWasm());
7
8
  }
8
9
  export async function openLixBinding(storage, telemetry) {
9
- if (storage.kind !== "memory") {
10
- throw new Error("LocalFilesystem is only available in Node.js");
11
- }
12
10
  await initializeWasm();
13
- return openMemoryFromSnapshot(telemetry, storage.snapshot);
11
+ switch (storage.kind) {
12
+ case "memory":
13
+ return openMemory(telemetry);
14
+ case "indexedDb": {
15
+ const backend = await IndexedDbBackend.open(storage.name);
16
+ try {
17
+ return (await openIndexedDb(backend, telemetry));
18
+ }
19
+ catch (error) {
20
+ await backend.close().catch(() => undefined);
21
+ throw error;
22
+ }
23
+ }
24
+ case "filesystem":
25
+ throw new Error("FilesystemStorage is only available in Node.js");
26
+ }
14
27
  }
@@ -45,16 +45,15 @@ export function openLixBinding(storage, telemetry) {
45
45
  : undefined;
46
46
  switch (storage.kind) {
47
47
  case "memory":
48
- if (storage.snapshot !== undefined) {
49
- throw new Error("Memory snapshots are only available in the browser binding");
50
- }
51
48
  if (nativeTelemetry)
52
49
  return addon.Lix.openMemory(nativeTelemetry);
53
50
  return addon.Lix.openMemory();
54
- case "localFilesystem":
51
+ case "indexedDb":
52
+ throw new Error("IndexedDbStorage is only available in browsers");
53
+ case "filesystem":
55
54
  if (nativeTelemetry) {
56
- return addon.Lix.openLocalFilesystem(storage.path, storage.lixDir, storage.syncAllFiles, nativeTelemetry);
55
+ return addon.Lix.openFilesystemStorage(storage.path, storage.syncAllFiles, nativeTelemetry);
57
56
  }
58
- return addon.Lix.openLocalFilesystem(storage.path, storage.lixDir, storage.syncAllFiles);
57
+ return addon.Lix.openFilesystemStorage(storage.path, storage.syncAllFiles);
59
58
  }
60
59
  }
package/dist/errors.d.ts CHANGED
@@ -5,5 +5,3 @@ export type LixJsError = Error & {
5
5
  };
6
6
  export declare function invalidArgument(operation: string, argument: string, expected: string, actual: string, receiver?: string): LixJsError;
7
7
  export declare function invalidParam(index: number, message: string, actual: string): LixJsError;
8
- export declare function localFilesystemNotOpen(operation: string): LixJsError;
9
- export declare function localFilesystemAlreadyOpen(): LixJsError;
package/dist/errors.js CHANGED
@@ -17,16 +17,3 @@ export function invalidParam(index, message, actual) {
17
17
  };
18
18
  return error;
19
19
  }
20
- export function localFilesystemNotOpen(operation) {
21
- const error = new Error(`LocalFilesystem.${operation}() requires the storage to be opened with openLix() first`);
22
- error.name = "LixError";
23
- error.code = "LIX_LOCAL_FILESYSTEM_NOT_OPEN";
24
- error.details = { operation };
25
- return error;
26
- }
27
- export function localFilesystemAlreadyOpen() {
28
- const error = new Error("openLix() LocalFilesystem is already open; close the existing Lix or create a new LocalFilesystem");
29
- error.name = "LixError";
30
- error.code = "LIX_LOCAL_FILESYSTEM_IN_USE";
31
- return error;
32
- }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { LocalFilesystem, Lix, LixTransaction, ObserveEvents, openLix, } from "./open-lix.js";
1
+ export { IndexedDbStorage, Lix, LixTransaction, ObserveEvents, openLix, } from "./open-lix.js";
2
+ export type { LixStorage, LixStorageAdapterConfig, LixStorageConnection, } from "./storage-adapter.js";
2
3
  export { bundledPluginArchives, type BundledPluginArchive, } from "./bundled-plugins.js";
3
4
  export { Row } from "./result.js";
4
5
  export { Value } from "./value.js";
5
- export type { LixClientState } from "./client-state.js";
6
- export type { CreateBranchOptions, CreateBranchReceipt, CreateCheckpointReceipt, RedoReceipt, ExecuteOptions, ExecuteResult, LixBatchOptions, LixBatchStatement, LocalFilesystemOptions, JsonValue, LixValue, MergeBranchOptions, MergeBranchOutcome, MergeBranchPreview, MergeBranchReceipt, MergeChangeStats, MergeConflict, MergeConflictSide, ObserveEvent, OpenLixOptions, LixTelemetryOptions, LixTelemetrySpan, LixSnapshotStorage, RemoteLixFetch, RemoteLixServerOptions, UndoReceipt, SqlParam, SwitchBranchOptions, SwitchBranchReceipt, } from "./types.js";
6
+ export type { CreateBranchOptions, CreateBranchReceipt, CreateCheckpointReceipt, RedoReceipt, ExecuteOptions, ExecuteResult, LixBatchOptions, LixBatchStatement, IndexedDbStorageOptions, JsonValue, LixValue, MergeBranchOptions, MergeBranchOutcome, MergeBranchPreview, MergeBranchReceipt, MergeChangeStats, MergeConflict, MergeConflictSide, ObserveEvent, OpenLixOptions, LixTelemetryOptions, LixTelemetrySpan, RemoteLixFetch, RemoteLixServerOptions, UndoReceipt, SqlParam, SwitchBranchOptions, SwitchBranchReceipt, } from "./types.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { LocalFilesystem, Lix, LixTransaction, ObserveEvents, openLix, } from "./open-lix.js";
1
+ export { IndexedDbStorage, Lix, LixTransaction, ObserveEvents, openLix, } from "./open-lix.js";
2
2
  export { bundledPluginArchives, } from "./bundled-plugins.js";
3
3
  export { Row } from "./result.js";
4
4
  export { Value } from "./value.js";
@@ -0,0 +1,19 @@
1
+ type IndexedDbEntry = {
2
+ key: Uint8Array;
3
+ value: Uint8Array;
4
+ };
5
+ type IndexedDbChanges = {
6
+ deletes: Uint8Array[];
7
+ puts: IndexedDbEntry[];
8
+ strictDurability: boolean;
9
+ };
10
+ /** Internal worker-local bridge used by the WASM IndexedDB storage adapter. */
11
+ export declare class IndexedDbBackend {
12
+ #private;
13
+ private constructor();
14
+ static open(name: string): Promise<IndexedDbBackend>;
15
+ loadEntries(): Promise<IndexedDbEntry[]>;
16
+ applyChanges(changes: IndexedDbChanges): Promise<void>;
17
+ close(): Promise<void>;
18
+ }
19
+ export {};
@@ -0,0 +1,121 @@
1
+ const DATABASE_VERSION = 1;
2
+ const ENTRY_STORE = "entries";
3
+ /** Internal worker-local bridge used by the WASM IndexedDB storage adapter. */
4
+ export class IndexedDbBackend {
5
+ #database;
6
+ #releaseLock;
7
+ #closed = false;
8
+ constructor(database, releaseLock) {
9
+ this.#database = database;
10
+ this.#releaseLock = releaseLock;
11
+ }
12
+ static async open(name) {
13
+ const releaseLock = await acquireDatabaseLock(name);
14
+ try {
15
+ const request = indexedDB.open(name, DATABASE_VERSION);
16
+ request.onupgradeneeded = () => {
17
+ if (!request.result.objectStoreNames.contains(ENTRY_STORE)) {
18
+ request.result.createObjectStore(ENTRY_STORE);
19
+ }
20
+ };
21
+ return new IndexedDbBackend(await openDatabase(request), releaseLock);
22
+ }
23
+ catch (error) {
24
+ releaseLock();
25
+ throw error;
26
+ }
27
+ }
28
+ async loadEntries() {
29
+ const transaction = this.#database.transaction(ENTRY_STORE, "readonly");
30
+ const store = transaction.objectStore(ENTRY_STORE);
31
+ const entries = [];
32
+ await new Promise((resolve, reject) => {
33
+ const request = store.openCursor();
34
+ request.onerror = () => reject(request.error ?? transaction.error);
35
+ request.onsuccess = () => {
36
+ const cursor = request.result;
37
+ if (!cursor) {
38
+ resolve();
39
+ return;
40
+ }
41
+ try {
42
+ entries.push({
43
+ key: copyBytes(cursor.key, "IndexedDB entry key"),
44
+ value: copyBytes(cursor.value, "IndexedDB entry value"),
45
+ });
46
+ }
47
+ catch (error) {
48
+ transaction.abort();
49
+ reject(error);
50
+ return;
51
+ }
52
+ cursor.continue();
53
+ };
54
+ });
55
+ await transactionDone(transaction);
56
+ return entries;
57
+ }
58
+ async applyChanges(changes) {
59
+ const transaction = this.#database.transaction(ENTRY_STORE, "readwrite", {
60
+ durability: changes.strictDurability ? "strict" : "default",
61
+ });
62
+ const store = transaction.objectStore(ENTRY_STORE);
63
+ for (const key of changes.deletes)
64
+ store.delete(binaryKey(key));
65
+ for (const entry of changes.puts) {
66
+ store.put(entry.value, binaryKey(entry.key));
67
+ }
68
+ await transactionDone(transaction);
69
+ }
70
+ async close() {
71
+ if (this.#closed)
72
+ return;
73
+ this.#closed = true;
74
+ this.#database.close();
75
+ this.#releaseLock();
76
+ }
77
+ }
78
+ function acquireDatabaseLock(name) {
79
+ let release;
80
+ const released = new Promise((resolve) => {
81
+ release = resolve;
82
+ });
83
+ return new Promise((resolve, reject) => {
84
+ void navigator.locks
85
+ .request(`lix:indexeddb:${name}`, { mode: "exclusive", ifAvailable: true }, async (lock) => {
86
+ if (!lock) {
87
+ reject(new Error(`IndexedDB storage '${name}' is already open`));
88
+ return;
89
+ }
90
+ resolve(release);
91
+ await released;
92
+ })
93
+ .catch(reject);
94
+ });
95
+ }
96
+ function openDatabase(request) {
97
+ return new Promise((resolve, reject) => {
98
+ request.onsuccess = () => resolve(request.result);
99
+ request.onerror = () => reject(request.error);
100
+ request.onblocked = () => reject(new Error("IndexedDB database open was blocked"));
101
+ });
102
+ }
103
+ function binaryKey(value) {
104
+ return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
105
+ }
106
+ function transactionDone(transaction) {
107
+ return new Promise((resolve, reject) => {
108
+ transaction.oncomplete = () => resolve();
109
+ transaction.onabort = () => reject(transaction.error);
110
+ transaction.onerror = () => reject(transaction.error);
111
+ });
112
+ }
113
+ function copyBytes(value, label) {
114
+ if (value instanceof ArrayBuffer) {
115
+ return new Uint8Array(value.slice(0));
116
+ }
117
+ if (ArrayBuffer.isView(value)) {
118
+ return new Uint8Array(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
119
+ }
120
+ throw new Error(`${label} is not binary data`);
121
+ }
package/dist/lix.d.ts CHANGED
@@ -1,13 +1,10 @@
1
- import { type LixClientState, type ManagedClientState } from "./client-state.js";
2
1
  import type { LixBinding, LixTransactionBinding, ObserveEventsBinding } from "./binding-types.js";
3
2
  import type { CreateBranchOptions, CreateBranchReceipt, CreateCheckpointReceipt, RedoReceipt, ExecuteOptions, ExecuteResult, ExecuteBatchResult, LixBatchOptions, LixBatchStatement, MergeBranchOptions, MergeBranchPreview, MergeBranchReceipt, ObserveEvent, SqlParam, SwitchBranchOptions, SwitchBranchReceipt, UndoReceipt } from "./types.js";
4
3
  export declare class Lix {
5
4
  #private;
6
5
  private readonly binding;
7
- private readonly managedClientState?;
8
6
  private closePromise;
9
- readonly clientState: LixClientState;
10
- constructor(binding: LixBinding, managedClientState?: ManagedClientState | undefined);
7
+ constructor(binding: LixBinding);
11
8
  execute(sql: string, params?: SqlParam[], options?: ExecuteOptions): Promise<ExecuteResult>;
12
9
  executeBatch(statements: readonly LixBatchStatement[], options?: LixBatchOptions): Promise<readonly ExecuteBatchResult[]>;
13
10
  observe(sql: string, params?: SqlParam[]): ObserveEvents;
package/dist/lix.js CHANGED
@@ -1,7 +1,5 @@
1
1
  import { invalidArgument } from "./errors.js";
2
- import { ACTIVE_BRANCH_CLIENT_STATE_KEY, unavailableClientState, } from "./client-state.js";
3
2
  import { normalizeOptionals, wrapExecuteBatchResult, wrapExecuteResult, } from "./result.js";
4
- import { isSnapshotPersistenceAfterCommitError } from "./snapshot-persistence.js";
5
3
  import { normalizeParam, toNativeValue } from "./value.js";
6
4
  const transactionFinalizer = new FinalizationRegistry(({ transaction, onFinish }) => {
7
5
  void transaction
@@ -17,9 +15,7 @@ const observeFinalizer = new FinalizationRegistry(({ observe, onClose }) => {
17
15
  });
18
16
  export class Lix {
19
17
  binding;
20
- managedClientState;
21
18
  closePromise;
22
- clientState;
23
19
  #activeBranchListeners = new Set();
24
20
  #inFlightOperations = new Set();
25
21
  #observations = new Map();
@@ -27,20 +23,8 @@ export class Lix {
27
23
  #transactionsOpening = 0;
28
24
  #activeTransactions = 0;
29
25
  #acceptingOperations = true;
30
- constructor(binding, managedClientState) {
26
+ constructor(binding) {
31
27
  this.binding = binding;
32
- this.managedClientState = managedClientState;
33
- this.clientState = managedClientState
34
- ? {
35
- get: (key) => managedClientState.get(key),
36
- set: (key, value) => this.#runOperation(() => managedClientState.set(key, value)),
37
- delete: (key) => this.#runOperation(() => managedClientState.delete(key)),
38
- subscribe: (listener) => {
39
- this.#assertAcceptingOperations();
40
- return managedClientState.subscribe(listener);
41
- },
42
- }
43
- : unavailableClientState();
44
28
  }
45
29
  async execute(sql, params = [], options) {
46
30
  assertExecuteArgs("lix", sql, params, options);
@@ -110,16 +94,6 @@ export class Lix {
110
94
  async switchBranch(options) {
111
95
  return this.#runOperation(async () => {
112
96
  const receipt = await this.binding.switchBranch(options);
113
- try {
114
- if (this.managedClientState) {
115
- await this.managedClientState.set(ACTIVE_BRANCH_CLIENT_STATE_KEY, receipt.branchId);
116
- }
117
- }
118
- catch {
119
- // The remote branch switch already committed. Client persistence is a
120
- // best-effort reopen preference and cannot turn that success into a
121
- // rejected switch with ambiguous branch state.
122
- }
123
97
  for (const listener of [...this.#activeBranchListeners]) {
124
98
  try {
125
99
  listener();
@@ -155,9 +129,13 @@ export class Lix {
155
129
  this.#observations.clear();
156
130
  this.closePromise = (async () => {
157
131
  await Promise.allSettled([...this.#inFlightOperations]);
158
- await this.binding.close();
159
- await this.managedClientState?.close();
132
+ const results = await Promise.allSettled([
133
+ Promise.resolve().then(() => this.binding.close()),
134
+ ]);
160
135
  this.#activeBranchListeners.clear();
136
+ const failure = results.find((result) => result.status === "rejected");
137
+ if (failure)
138
+ throw failure.reason;
161
139
  })();
162
140
  }
163
141
  await this.closePromise;
@@ -256,31 +234,22 @@ export class LixTransaction {
256
234
  throw transactionClosedError();
257
235
  if (!this.finishPromise) {
258
236
  this.finishPromise = (async () => {
259
- if (kind === "transaction.commit")
260
- await this.binding.commit();
261
- else
262
- await this.binding.rollback();
263
- this.finished = true;
264
- transactionFinalizer.unregister(this);
265
- this.onFinish();
237
+ try {
238
+ if (kind === "transaction.commit")
239
+ await this.binding.commit();
240
+ else
241
+ await this.binding.rollback();
242
+ }
243
+ finally {
244
+ // A terminal binding call consumes the underlying transaction even
245
+ // when its durable commit or rollback reports an error.
246
+ this.finished = true;
247
+ transactionFinalizer.unregister(this);
248
+ this.onFinish();
249
+ }
266
250
  })();
267
251
  }
268
- try {
269
- await this.finishPromise;
270
- }
271
- catch (error) {
272
- if (isSnapshotPersistenceAfterCommitError(error)) {
273
- // The transaction finished in Rust; only durable snapshot saving
274
- // failed. Release the transaction lifecycle while reporting that
275
- // durability failure to the caller.
276
- this.finished = true;
277
- transactionFinalizer.unregister(this);
278
- this.onFinish();
279
- throw error;
280
- }
281
- this.finishPromise = undefined;
282
- throw error;
283
- }
252
+ await this.finishPromise;
284
253
  }
285
254
  }
286
255
  function transactionClosedError() {
@@ -1,13 +1,8 @@
1
1
  import { Lix } from "./lix.js";
2
- import type { LocalFilesystemOptions, OpenLixOptions } from "./types.js";
2
+ import type { IndexedDbStorageOptions, OpenLixOptions } from "./types.js";
3
3
  export { Lix, LixTransaction, ObserveEvents } from "./lix.js";
4
- export declare class LocalFilesystem {
5
- readonly path: string;
6
- readonly lixDir: string | undefined;
7
- readonly syncAllFiles: boolean;
8
- constructor(options: LocalFilesystemOptions);
9
- importPaths(paths: readonly string[]): Promise<void>;
10
- syncDiskToLix(): Promise<void>;
11
- private client;
4
+ export declare class IndexedDbStorage {
5
+ readonly name: string;
6
+ constructor(options: IndexedDbStorageOptions);
12
7
  }
13
8
  export declare function openLix(options?: OpenLixOptions): Promise<Lix>;