@lix-js/sdk 0.9.0 → 0.10.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 CHANGED
@@ -41,7 +41,7 @@ const files = lix.observe("SELECT path FROM lix_file ORDER BY path");
41
41
  const initial = await files.next();
42
42
 
43
43
  await lix.execute(
44
- "INSERT INTO lix_file (path, data) VALUES ($1, $2)",
44
+ "INSERT INTO lix_file (path, content) VALUES ($1, $2)",
45
45
  ["/hello.txt", new TextEncoder().encode("hello")],
46
46
  );
47
47
  const update = await files.next();
@@ -101,20 +101,37 @@ const lix = await openLix({
101
101
  });
102
102
 
103
103
  await lix.execute(
104
- "INSERT INTO lix_file (path, data) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET data = excluded.data",
104
+ "INSERT INTO lix_file (path, content) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET content = excluded.content",
105
105
  ["/hello.txt", new TextEncoder().encode("world")],
106
106
  );
107
107
 
108
- const result = await lix.execute("SELECT data FROM lix_file WHERE path = $1", [
108
+ const result = await lix.execute("SELECT content FROM lix_file WHERE path = $1", [
109
109
  "/hello.txt",
110
110
  ]);
111
- const bytes = result.rows[0]?.value("data").asBytes();
111
+ const bytes = result.rows[0]?.value("content").asBytes();
112
112
 
113
113
  console.log(bytes && new TextDecoder().decode(bytes));
114
114
 
115
115
  await lix.close();
116
116
  ```
117
117
 
118
+ ## Discover the SQL contract
119
+
120
+ Lix extends the standard `information_schema.columns` relation with
121
+ `lix_value_kind` and `lix_insert_policy`. Inspect it before generating writes:
122
+
123
+ ```sql
124
+ SELECT table_name, column_name, data_type, is_nullable, column_default,
125
+ lix_value_kind, lix_insert_policy
126
+ FROM information_schema.columns
127
+ WHERE table_name = 'lix_file'
128
+ ORDER BY ordinal_position;
129
+ ```
130
+
131
+ `lix_insert_policy` distinguishes `REQUIRED`, `DEFAULT`, `CONDITIONAL`, and
132
+ `READ_ONLY` columns. For the complete table and history-function map, see
133
+ [SQL Surfaces](https://lix.dev/docs/surfaces).
134
+
118
135
  ## Branches
119
136
 
120
137
  ```ts
@@ -123,7 +140,7 @@ const draft = await lix.createBranch({ name: "Draft" });
123
140
 
124
141
  await lix.switchBranch({ branchId: draft.id });
125
142
  await lix.execute(
126
- "INSERT INTO lix_file (path, data) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET data = excluded.data",
143
+ "INSERT INTO lix_file (path, content) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET content = excluded.content",
127
144
  ["/status.txt", new TextEncoder().encode("draft")],
128
145
  );
129
146
 
@@ -139,11 +156,11 @@ const tx = await lix.beginTransaction();
139
156
 
140
157
  try {
141
158
  await tx.execute(
142
- "INSERT INTO lix_file (path, data) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET data = excluded.data",
159
+ "INSERT INTO lix_file (path, content) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET content = excluded.content",
143
160
  ["/a.txt", new TextEncoder().encode("1")],
144
161
  );
145
162
  await tx.execute(
146
- "INSERT INTO lix_file (path, data) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET data = excluded.data",
163
+ "INSERT INTO lix_file (path, content) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET content = excluded.content",
147
164
  ["/b.txt", new TextEncoder().encode("2")],
148
165
  );
149
166
  await tx.commit();
@@ -170,10 +187,10 @@ try {
170
187
  WebAssembly binding. Vite follows this split without consumer configuration.
171
188
  - Every browser `openLix()` owns one dedicated worker, so database work does
172
189
  not block the page's main thread. Node.js uses the native binding's actor.
173
- - Node.js executes installed Component API v2 plugins with the Rust SDK's
190
+ - Node.js executes installed Component API v1 plugins with the Rust SDK's
174
191
  Wasmtime runtime. The browser and Workerd bindings currently open without a
175
192
  component runtime: they can use ordinary Lix storage and SQL, but do not
176
- execute installed plugins. A browser V2 host is a separate follow-up.
193
+ execute installed plugins. A browser Component host is a separate follow-up.
177
194
  - A page Content Security Policy only needs to permit the package's same-origin
178
195
  worker. WebAssembly compilation happens inside that worker, so the required
179
196
  permission can be scoped to the worker script's HTTP response instead of
@@ -1,4 +1,4 @@
1
- import type { CreateBranchOptions, CreateBranchReceipt, CreateCheckpointReceipt, 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, JsonValue } from "./types.js";
2
2
  import type { NativeLixValue } from "./value.js";
3
3
  export type BindingExecuteResult = {
4
4
  columns: string[];
@@ -26,6 +26,7 @@ export type LixBinding = {
26
26
  observe(sql: string, params: BindingParam[]): Promise<ObserveEventsBinding>;
27
27
  beginTransaction(): Promise<LixTransactionBinding>;
28
28
  activeBranchId(): Promise<string>;
29
+ activeAccountId(): Promise<string>;
29
30
  clientStateEntries?(): Promise<Array<{
30
31
  key: string;
31
32
  value: JsonValue;
@@ -35,6 +36,8 @@ export type LixBinding = {
35
36
  clientStateDelete?(key: string): Promise<void>;
36
37
  createBranch(options: CreateBranchOptions): Promise<CreateBranchReceipt>;
37
38
  createCheckpoint(): Promise<CreateCheckpointReceipt>;
39
+ undo(): Promise<UndoReceipt>;
40
+ redo(): Promise<RedoReceipt>;
38
41
  switchBranch(options: SwitchBranchOptions): Promise<SwitchBranchReceipt>;
39
42
  importFilesystemPaths(paths: string[]): Promise<void>;
40
43
  mergeBranchPreview(options: MergeBranchOptions): Promise<MergeBranchPreview>;
@@ -48,10 +48,18 @@ export function openLixBinding(storage, telemetry) {
48
48
  if (storage.snapshot !== undefined) {
49
49
  throw new Error("Memory snapshots are only available in the browser binding");
50
50
  }
51
- return addon.Lix.openMemory(nativeTelemetry);
51
+ if (nativeTelemetry)
52
+ return addon.Lix.openMemory(nativeTelemetry);
53
+ return addon.Lix.openMemory();
52
54
  case "sqlite":
53
- return addon.Lix.openSQLite(storage.path, nativeTelemetry);
55
+ if (nativeTelemetry) {
56
+ return addon.Lix.openSQLite(storage.path, nativeTelemetry);
57
+ }
58
+ return addon.Lix.openSQLite(storage.path);
54
59
  case "localFilesystem":
55
- return addon.Lix.openLocalFilesystem(storage.path, storage.lixDir, storage.syncAllFiles, nativeTelemetry);
60
+ if (nativeTelemetry) {
61
+ return addon.Lix.openLocalFilesystem(storage.path, storage.lixDir, storage.syncAllFiles, nativeTelemetry);
62
+ }
63
+ return addon.Lix.openLocalFilesystem(storage.path, storage.lixDir, storage.syncAllFiles);
56
64
  }
57
65
  }
@@ -1,11 +1,11 @@
1
1
  const BUNDLED_PLUGIN_MANIFEST = [
2
2
  {
3
- key: "plugin_markdown_incremental_v2",
4
- fileName: "plugin_markdown_incremental_v2.lixplugin",
3
+ key: "plugin_markdown",
4
+ fileName: "plugin_markdown.lixplugin",
5
5
  },
6
6
  {
7
- key: "plugin_csv_v2",
8
- fileName: "plugin_csv_v2.lixplugin",
7
+ key: "plugin_csv",
8
+ fileName: "plugin_csv.lixplugin",
9
9
  },
10
10
  ];
11
11
  export async function bundledPluginArchives() {
@@ -1,16 +1,20 @@
1
1
  import type { LixBinding } from "./binding-types.js";
2
- import type { JsonValue } from "./types.js";
2
+ import type { JsonValue, LixSnapshotStorage } from "./types.js";
3
3
  export declare const ACTIVE_BRANCH_CLIENT_STATE_KEY = "lix_active_branch_id";
4
+ export declare const ACTIVE_ACCOUNT_CLIENT_STATE_KEY = "lix_active_account_id";
4
5
  export type LixClientState = {
5
6
  /** Returns the hydrated client-local value without a network round trip. */
6
7
  get<T extends JsonValue = JsonValue>(key: string): T | undefined;
7
- /** Commits the value through the local Rust Lix and then persists its snapshot. */
8
+ /** Persists a client-local value in the configured client storage. */
8
9
  set(key: string, value: JsonValue): Promise<void>;
9
- /** Deletes the value through the local Rust Lix and then persists its snapshot. */
10
+ /** Deletes a client-local value from the configured client storage. */
10
11
  delete(key: string): Promise<void>;
11
12
  /** Subscribes to successful mutations made through this client-state handle. */
12
13
  subscribe(listener: () => void): () => void;
13
14
  };
15
+ export type ManagedClientState = LixClientState & {
16
+ close(): Promise<void>;
17
+ };
14
18
  export declare function unavailableClientState(): LixClientState;
15
19
  type ClientStateBinding = LixBinding & {
16
20
  exportSnapshot?: () => Promise<Uint8Array>;
@@ -37,4 +41,13 @@ export declare class ManagedLixClientState implements LixClientState {
37
41
  subscribe(listener: () => void): () => void;
38
42
  close(): Promise<void>;
39
43
  }
44
+ /**
45
+ * Opens client state directly over snapshot storage without starting a local
46
+ * Lix runtime. This is used by remote Lix connections, where the storage
47
+ * option persists client-local state rather than the remote workspace.
48
+ */
49
+ export declare function openStoredClientState(options: {
50
+ readonly storage: LixSnapshotStorage;
51
+ readonly namespace: string;
52
+ }): Promise<ManagedClientState>;
40
53
  export {};
@@ -1,6 +1,8 @@
1
1
  import { isSnapshotPersistenceAfterCommitError } from "./snapshot-persistence.js";
2
2
  import { Value } from "./value.js";
3
3
  export const ACTIVE_BRANCH_CLIENT_STATE_KEY = "lix_active_branch_id";
4
+ export const ACTIVE_ACCOUNT_CLIENT_STATE_KEY = "lix_active_account_id";
5
+ const STORED_CLIENT_STATE_HEADER = "lix-client-state-v1\n";
4
6
  export function unavailableClientState() {
5
7
  const unavailable = () => {
6
8
  const error = new Error("Lix client state requires client storage; pass storage to openLix()");
@@ -158,6 +160,144 @@ export class ManagedLixClientState {
158
160
  }
159
161
  }
160
162
  }
163
+ /**
164
+ * Opens client state directly over snapshot storage without starting a local
165
+ * Lix runtime. This is used by remote Lix connections, where the storage
166
+ * option persists client-local state rather than the remote workspace.
167
+ */
168
+ export async function openStoredClientState(options) {
169
+ const snapshot = await options.storage.load(options.namespace);
170
+ if (snapshot !== undefined && !(snapshot instanceof Uint8Array)) {
171
+ throw new TypeError("Client-state storage load() must return a Uint8Array");
172
+ }
173
+ return new StoredClientState(options.storage, options.namespace, decodeStoredClientState(snapshot));
174
+ }
175
+ class StoredClientState {
176
+ #storage;
177
+ #namespace;
178
+ #values;
179
+ #listeners = new Set();
180
+ #operationQueue = Promise.resolve();
181
+ #closePromise;
182
+ #acceptingOperations = true;
183
+ #dirty = false;
184
+ constructor(storage, namespace, values) {
185
+ this.#storage = storage;
186
+ this.#namespace = namespace;
187
+ this.#values = values;
188
+ }
189
+ get(key) {
190
+ assertClientStateKey(key);
191
+ const value = this.#values.get(key);
192
+ return value === undefined ? undefined : cloneJsonValue(value);
193
+ }
194
+ set(key, value) {
195
+ assertClientStateKey(key);
196
+ assertJsonValue(value);
197
+ this.#assertOpen();
198
+ const nextValue = cloneJsonValue(value);
199
+ return this.#enqueue(async () => {
200
+ this.#values.set(key, nextValue);
201
+ this.#dirty = true;
202
+ this.#publish();
203
+ await this.#persist();
204
+ });
205
+ }
206
+ delete(key) {
207
+ assertClientStateKey(key);
208
+ this.#assertOpen();
209
+ return this.#enqueue(async () => {
210
+ if (this.#values.delete(key))
211
+ this.#publish();
212
+ this.#dirty = true;
213
+ await this.#persist();
214
+ });
215
+ }
216
+ subscribe(listener) {
217
+ if (typeof listener !== "function") {
218
+ throw new TypeError("clientState.subscribe() requires a function");
219
+ }
220
+ this.#assertOpen();
221
+ this.#listeners.add(listener);
222
+ return () => this.#listeners.delete(listener);
223
+ }
224
+ async close() {
225
+ if (this.#closePromise)
226
+ return this.#closePromise;
227
+ this.#acceptingOperations = false;
228
+ this.#closePromise = (async () => {
229
+ await this.#operationQueue;
230
+ if (this.#dirty)
231
+ await this.#persist();
232
+ this.#listeners.clear();
233
+ })();
234
+ return this.#closePromise;
235
+ }
236
+ #enqueue(operation) {
237
+ const result = this.#operationQueue.then(operation, operation);
238
+ this.#operationQueue = result.then(() => undefined, () => undefined);
239
+ return result;
240
+ }
241
+ async #persist() {
242
+ await this.#storage.save(this.#namespace, encodeStoredClientState(this.#values));
243
+ this.#dirty = false;
244
+ }
245
+ #publish() {
246
+ for (const listener of [...this.#listeners]) {
247
+ try {
248
+ listener();
249
+ }
250
+ catch {
251
+ // Subscribers do not participate in the completed mutation.
252
+ }
253
+ }
254
+ }
255
+ #assertOpen() {
256
+ if (!this.#acceptingOperations) {
257
+ throw new Error("Lix client state is closed");
258
+ }
259
+ }
260
+ }
261
+ function encodeStoredClientState(values) {
262
+ const entries = [...values].map(([key, value]) => [
263
+ key,
264
+ cloneJsonValue(value),
265
+ ]);
266
+ return new TextEncoder().encode(`${STORED_CLIENT_STATE_HEADER}${JSON.stringify(entries)}`);
267
+ }
268
+ function decodeStoredClientState(snapshot) {
269
+ if (snapshot === undefined)
270
+ return new Map();
271
+ const header = new TextEncoder().encode(STORED_CLIENT_STATE_HEADER);
272
+ if (snapshot.length < header.length ||
273
+ header.some((byte, index) => snapshot[index] !== byte)) {
274
+ // Remote storage previously contained a full Lix snapshot. Backward
275
+ // compatibility is intentionally not provided: start with empty state and
276
+ // replace it on the next write.
277
+ return new Map();
278
+ }
279
+ let parsed;
280
+ try {
281
+ parsed = JSON.parse(new TextDecoder().decode(snapshot.subarray(header.length)));
282
+ }
283
+ catch (error) {
284
+ throw new Error("Stored Lix client state is invalid", { cause: error });
285
+ }
286
+ if (!Array.isArray(parsed)) {
287
+ throw new Error("Stored Lix client state entries must be an array");
288
+ }
289
+ const values = new Map();
290
+ for (const entry of parsed) {
291
+ if (!Array.isArray(entry) || entry.length !== 2) {
292
+ throw new Error("Stored Lix client state entry is invalid");
293
+ }
294
+ const [key, value] = entry;
295
+ assertClientStateKey(key);
296
+ assertJsonValue(value);
297
+ values.set(key, cloneJsonValue(value));
298
+ }
299
+ return values;
300
+ }
161
301
  function assertClientStateKey(key) {
162
302
  if (typeof key !== "string" || key.length === 0) {
163
303
  throw new TypeError("clientState key must be a non-empty string");
package/dist/index.d.ts CHANGED
@@ -3,4 +3,4 @@ export { bundledPluginArchives, type BundledPluginArchive, } from "./bundled-plu
3
3
  export { Row } from "./result.js";
4
4
  export { Value } from "./value.js";
5
5
  export type { LixClientState } from "./client-state.js";
6
- export type { CreateBranchOptions, CreateBranchReceipt, CreateCheckpointReceipt, ExecuteOptions, ExecuteResult, LixBatchOptions, LixBatchStatement, LocalFilesystemOptions, JsonValue, LixValue, MergeBranchOptions, MergeBranchOutcome, MergeBranchPreview, MergeBranchReceipt, MergeChangeStats, MergeConflict, MergeConflictSide, ObserveEvent, OpenLixOptions, LixTelemetryOptions, LixTelemetrySpan, LixSnapshotStorage, RemoteLixFetch, RemoteLixServerOptions, SqlParam, SQLiteOptions, SwitchBranchOptions, SwitchBranchReceipt, } from "./types.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, SQLiteOptions, SwitchBranchOptions, SwitchBranchReceipt, } from "./types.js";
package/dist/lix.d.ts CHANGED
@@ -1,22 +1,25 @@
1
- import { type LixClientState, type ManagedLixClientState } from "./client-state.js";
1
+ import { type LixClientState, type ManagedClientState } from "./client-state.js";
2
2
  import type { LixBinding, LixTransactionBinding, ObserveEventsBinding } from "./binding-types.js";
3
- import type { CreateBranchOptions, CreateBranchReceipt, CreateCheckpointReceipt, ExecuteOptions, ExecuteResult, LixBatchOptions, LixBatchStatement, MergeBranchOptions, MergeBranchPreview, MergeBranchReceipt, ObserveEvent, SqlParam, SwitchBranchOptions, SwitchBranchReceipt } from "./types.js";
3
+ import type { CreateBranchOptions, CreateBranchReceipt, CreateCheckpointReceipt, RedoReceipt, ExecuteOptions, ExecuteResult, LixBatchOptions, LixBatchStatement, MergeBranchOptions, MergeBranchPreview, MergeBranchReceipt, ObserveEvent, SqlParam, SwitchBranchOptions, SwitchBranchReceipt, UndoReceipt } from "./types.js";
4
4
  export declare class Lix {
5
5
  #private;
6
6
  private readonly binding;
7
7
  private readonly managedClientState?;
8
8
  private closePromise;
9
9
  readonly clientState: LixClientState;
10
- constructor(binding: LixBinding, managedClientState?: ManagedLixClientState | undefined);
10
+ constructor(binding: LixBinding, managedClientState?: ManagedClientState | undefined);
11
11
  execute(sql: string, params?: SqlParam[], options?: ExecuteOptions): Promise<ExecuteResult>;
12
12
  executeBatch(statements: readonly LixBatchStatement[], options?: LixBatchOptions): Promise<readonly ExecuteResult[]>;
13
13
  observe(sql: string, params?: SqlParam[]): ObserveEvents;
14
14
  beginTransaction(): Promise<LixTransaction>;
15
15
  activeBranchId(): Promise<string>;
16
+ activeAccountId(): Promise<string>;
16
17
  /** Subscribes to successful branch switches made through this Lix handle. */
17
18
  subscribeActiveBranch(listener: () => void): () => void;
18
19
  createBranch(options: CreateBranchOptions): Promise<CreateBranchReceipt>;
19
20
  createCheckpoint(): Promise<CreateCheckpointReceipt>;
21
+ undo(): Promise<UndoReceipt>;
22
+ redo(): Promise<RedoReceipt>;
20
23
  switchBranch(options: SwitchBranchOptions): Promise<SwitchBranchReceipt>;
21
24
  mergeBranchPreview(options: MergeBranchOptions): Promise<MergeBranchPreview>;
22
25
  mergeBranch(options: MergeBranchOptions): Promise<MergeBranchReceipt>;
package/dist/lix.js CHANGED
@@ -83,6 +83,9 @@ export class Lix {
83
83
  async activeBranchId() {
84
84
  return this.#runOperation(() => this.binding.activeBranchId());
85
85
  }
86
+ async activeAccountId() {
87
+ return this.#runOperation(() => this.binding.activeAccountId());
88
+ }
86
89
  /** Subscribes to successful branch switches made through this Lix handle. */
87
90
  subscribeActiveBranch(listener) {
88
91
  if (typeof listener !== "function") {
@@ -98,6 +101,12 @@ export class Lix {
98
101
  async createCheckpoint() {
99
102
  return this.#runOperation(() => this.binding.createCheckpoint());
100
103
  }
104
+ async undo() {
105
+ return this.#runOperation(() => this.binding.undo());
106
+ }
107
+ async redo() {
108
+ return this.#runOperation(() => this.binding.redo());
109
+ }
101
110
  async switchBranch(options) {
102
111
  return this.#runOperation(async () => {
103
112
  const receipt = await this.binding.switchBranch(options);
package/dist/open-lix.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { localFilesystemAlreadyOpen, localFilesystemNotOpen, } from "./errors.js";
2
- import { ACTIVE_BRANCH_CLIENT_STATE_KEY, openClientState, } from "./client-state.js";
2
+ import { ACTIVE_ACCOUNT_CLIENT_STATE_KEY, ACTIVE_BRANCH_CLIENT_STATE_KEY, openClientState, openStoredClientState, } from "./client-state.js";
3
3
  import { Lix } from "./lix.js";
4
4
  export { Lix, LixTransaction, ObserveEvents } from "./lix.js";
5
5
  export class SQLite {
@@ -78,39 +78,35 @@ export async function openLix(options = {}) {
78
78
  return new Lix(await openRemoteLixBinding(options.server));
79
79
  }
80
80
  assertSnapshotStorage(options.storage);
81
- const { openPersistentLixWorkerBinding } = await import("./worker/client.js");
82
- const clientBinding = await openPersistentLixWorkerBinding({
81
+ const clientState = await openStoredClientState({
83
82
  storage: options.storage,
84
83
  namespace: remoteClientStateNamespace(options.server.url),
85
84
  });
86
- let clientState;
87
- try {
88
- clientState = await openClientState({
89
- binding: clientBinding,
90
- closeBinding: true,
91
- });
92
- }
93
- catch (error) {
94
- await clientBinding.close().catch(() => undefined);
95
- throw error;
96
- }
97
85
  const restoredBranchId = clientState.get(ACTIVE_BRANCH_CLIENT_STATE_KEY);
86
+ const restoredAccountId = clientState.get(ACTIVE_ACCOUNT_CLIENT_STATE_KEY);
98
87
  let remoteBinding;
99
88
  try {
100
89
  try {
101
90
  remoteBinding = await openRemoteLixBinding(options.server, {
102
91
  initialActiveBranchId: restoredBranchId,
92
+ initialActiveAccountId: restoredAccountId,
103
93
  });
104
94
  }
105
95
  catch (error) {
106
96
  if (!restoredBranchId || !isBranchNotFoundError(error))
107
97
  throw error;
108
- remoteBinding = await openRemoteLixBinding(options.server);
98
+ remoteBinding = await openRemoteLixBinding(options.server, {
99
+ initialActiveAccountId: restoredAccountId,
100
+ });
109
101
  }
110
102
  const activeBranchId = await remoteBinding.activeBranchId();
103
+ const activeAccountId = await remoteBinding.activeAccountId();
111
104
  if (activeBranchId !== restoredBranchId) {
112
105
  await clientState.set(ACTIVE_BRANCH_CLIENT_STATE_KEY, activeBranchId);
113
106
  }
107
+ if (activeAccountId !== restoredAccountId) {
108
+ await clientState.set(ACTIVE_ACCOUNT_CLIENT_STATE_KEY, activeAccountId);
109
+ }
114
110
  return new Lix(remoteBinding, clientState);
115
111
  }
116
112
  catch (error) {
@@ -3,6 +3,7 @@ import type { RemoteLixServerOptions } from "../types.js";
3
3
  import { type RemoteHandshakeRequest } from "./protocol.js";
4
4
  type RemoteLixClientOptions = {
5
5
  initialActiveBranchId?: RemoteHandshakeRequest["activeBranchId"];
6
+ initialActiveAccountId?: RemoteHandshakeRequest["activeAccountId"];
6
7
  };
7
8
  export declare function openRemoteLixBinding(options: RemoteLixServerOptions, clientOptions?: RemoteLixClientOptions): Promise<LixBinding>;
8
9
  export {};
@@ -3,6 +3,7 @@ import { readSseEvents } from "./sse.js";
3
3
  const OBSERVE_RETRY_BASE_MS = 100;
4
4
  const OBSERVE_RETRY_MAX_MS = 5_000;
5
5
  const REMOTE_SESSION_HEADER = "Lix-Session-Id";
6
+ const REMOTE_TRANSACTION_HEADER = "Lix-Transaction-Id";
6
7
  const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
7
8
  const REQUEST_BLOB_DELTA_MIN_BYTES = 32 * 1024;
8
9
  const REQUEST_BLOB_DELTA_MIN_WIRE_RATIO = 0.9;
@@ -31,10 +32,12 @@ class RemoteLixBinding {
31
32
  #fetch;
32
33
  #headers;
33
34
  #initialActiveBranchId;
35
+ #initialActiveAccountId;
34
36
  #observationHub;
35
37
  #requestBlobBases = new Map();
36
38
  #sessionId;
37
39
  #activeBranchId;
40
+ #activeAccountId;
38
41
  #requestBlobBaseBytes = 0;
39
42
  #acceptingOperations = true;
40
43
  #operationQueue = Promise.resolve();
@@ -63,17 +66,28 @@ class RemoteLixBinding {
63
66
  throw new TypeError("initialActiveBranchId must be a non-empty string");
64
67
  }
65
68
  this.#initialActiveBranchId = clientOptions.initialActiveBranchId;
69
+ if (clientOptions.initialActiveAccountId !== undefined &&
70
+ clientOptions.initialActiveAccountId.length === 0) {
71
+ throw new TypeError("initialActiveAccountId must be a non-empty string");
72
+ }
73
+ this.#initialActiveAccountId = clientOptions.initialActiveAccountId;
66
74
  this.#observationHub = new RemoteObservationHub({
67
75
  openStream: (subscriptions, signal) => this.#requestObserveStream(subscriptions, signal),
68
76
  });
69
77
  }
70
78
  async open() {
71
- const path = this.#initialActiveBranchId === undefined
72
- ? ""
73
- : `?activeBranchId=${encodeURIComponent(this.#initialActiveBranchId)}`;
79
+ const query = new URLSearchParams();
80
+ if (this.#initialActiveBranchId !== undefined) {
81
+ query.set("activeBranchId", this.#initialActiveBranchId);
82
+ }
83
+ if (this.#initialActiveAccountId !== undefined) {
84
+ query.set("activeAccountId", this.#initialActiveAccountId);
85
+ }
86
+ const path = query.size === 0 ? "" : `?${query}`;
74
87
  const handshake = decodeHandshake(await this.#requestJson(path, { method: "GET" }));
75
88
  this.#sessionId = handshake.sessionId;
76
89
  this.#activeBranchId = handshake.activeBranchId;
90
+ this.#activeAccountId = handshake.activeAccountId;
77
91
  }
78
92
  async execute(sql, params, options) {
79
93
  this.#assertOpen();
@@ -142,7 +156,62 @@ class RemoteLixBinding {
142
156
  }
143
157
  async beginTransaction() {
144
158
  this.#assertOpen();
145
- throw unsupportedRemoteOperation("beginTransaction");
159
+ return this.#enqueue(async () => {
160
+ const begun = record(await this.#requestJson("transaction/begin", { method: "POST" }), "begin transaction response");
161
+ if (typeof begun.transactionId !== "string") {
162
+ throw protocolError("begin transaction response.transactionId must be a string");
163
+ }
164
+ const transactionId = begun.transactionId;
165
+ let active = true;
166
+ const assertActive = () => {
167
+ if (!active) {
168
+ throw remoteError("LIX_INVALID_TRANSACTION_STATE", "Lix transaction is closed");
169
+ }
170
+ };
171
+ return {
172
+ execute: async (sql, params, options) => {
173
+ assertActive();
174
+ const snapshot = snapshotParams(params);
175
+ const requestOptions = remoteExecuteOptions(options);
176
+ return this.#enqueue(async () => {
177
+ const value = await this.#requestJson("transaction/execute", {
178
+ method: "POST",
179
+ headers: { [REMOTE_TRANSACTION_HEADER]: transactionId },
180
+ body: JSON.stringify({
181
+ sql,
182
+ params: snapshot.map(encodeWireValue),
183
+ ...(requestOptions === undefined
184
+ ? {}
185
+ : { options: requestOptions }),
186
+ }),
187
+ });
188
+ return decodeExecuteResult(value);
189
+ });
190
+ },
191
+ commit: async () => {
192
+ assertActive();
193
+ return this.#enqueue(async () => {
194
+ assertActive();
195
+ await this.#requestJson("transaction/commit", {
196
+ method: "POST",
197
+ headers: { [REMOTE_TRANSACTION_HEADER]: transactionId },
198
+ }, "empty");
199
+ active = false;
200
+ });
201
+ },
202
+ rollback: async () => {
203
+ assertActive();
204
+ return this.#enqueue(async () => {
205
+ assertActive();
206
+ await this.#requestJson("transaction/rollback", {
207
+ method: "POST",
208
+ headers: { [REMOTE_TRANSACTION_HEADER]: transactionId },
209
+ }, "empty");
210
+ active = false;
211
+ });
212
+ },
213
+ };
214
+ });
146
215
  }
147
216
  async activeBranchId() {
148
217
  this.#assertOpen();
@@ -157,6 +226,19 @@ class RemoteLixBinding {
157
226
  return this.#activeBranchId;
158
227
  });
159
228
  }
229
+ async activeAccountId() {
230
+ this.#assertOpen();
231
+ return this.#enqueue(async () => {
232
+ if (this.#activeAccountId === undefined) {
233
+ const handshake = decodeHandshake(await this.#requestJson("", { method: "GET" }));
234
+ if (handshake.sessionId !== this.#sessionId) {
235
+ throw protocolError("remote handshake changed sessionId");
236
+ }
237
+ this.#activeAccountId = handshake.activeAccountId;
238
+ }
239
+ return this.#activeAccountId;
240
+ });
241
+ }
160
242
  async createBranch(options) {
161
243
  this.#assertOpen();
162
244
  return this.#enqueue(async () => {
@@ -189,6 +271,38 @@ class RemoteLixBinding {
189
271
  return { commitId: value.commitId };
190
272
  });
191
273
  }
274
+ async undo() {
275
+ this.#assertOpen();
276
+ return this.#enqueue(async () => {
277
+ const value = record(await this.#requestJson("undo", { method: "POST" }), "undo response");
278
+ if (typeof value.branchId !== "string" ||
279
+ typeof value.targetCommitId !== "string" ||
280
+ typeof value.inverseCommitId !== "string") {
281
+ throw protocolError("undo response is invalid");
282
+ }
283
+ return {
284
+ branchId: value.branchId,
285
+ targetCommitId: value.targetCommitId,
286
+ inverseCommitId: value.inverseCommitId,
287
+ };
288
+ });
289
+ }
290
+ async redo() {
291
+ this.#assertOpen();
292
+ return this.#enqueue(async () => {
293
+ const value = record(await this.#requestJson("redo", { method: "POST" }), "redo response");
294
+ if (typeof value.branchId !== "string" ||
295
+ typeof value.targetCommitId !== "string" ||
296
+ typeof value.replayCommitId !== "string") {
297
+ throw protocolError("redo response is invalid");
298
+ }
299
+ return {
300
+ branchId: value.branchId,
301
+ targetCommitId: value.targetCommitId,
302
+ replayCommitId: value.replayCommitId,
303
+ };
304
+ });
305
+ }
192
306
  async switchBranch(options) {
193
307
  this.#assertOpen();
194
308
  return this.#enqueue(async () => {
@@ -1,6 +1,6 @@
1
1
  import type { BindingExecuteResult, BindingObserveEvent } from "../binding-types.js";
2
2
  import type { NativeLixValue } from "../value.js";
3
- export declare const REMOTE_PROTOCOL_VERSION = 1;
3
+ export declare const REMOTE_PROTOCOL_VERSION = 2;
4
4
  export declare const REMOTE_PROTOCOL_PATH = "/lix/v1/";
5
5
  export type WireValue = {
6
6
  kind: "null";
@@ -36,10 +36,12 @@ export type WireRequestValue = WireValue | WireRequestBlobSplice;
36
36
  export type RemoteHandshake = {
37
37
  protocolVersion: number;
38
38
  activeBranchId: string;
39
+ activeAccountId: string;
39
40
  sessionId: string;
40
41
  };
41
42
  export type RemoteHandshakeRequest = {
42
43
  activeBranchId?: string;
44
+ activeAccountId?: string;
43
45
  };
44
46
  export type RemoteExecuteRequest = {
45
47
  sql: string;
@@ -122,6 +124,16 @@ export type RemoteCreateBranchResponse = {
122
124
  export type RemoteCreateCheckpointResponse = {
123
125
  commitId: string;
124
126
  };
127
+ export type RemoteUndoResponse = {
128
+ branchId: string;
129
+ targetCommitId: string;
130
+ inverseCommitId: string;
131
+ };
132
+ export type RemoteRedoResponse = {
133
+ branchId: string;
134
+ targetCommitId: string;
135
+ replayCommitId: string;
136
+ };
125
137
  export type RemoteSwitchBranchRequest = {
126
138
  branchId: string;
127
139
  };
@@ -1,4 +1,4 @@
1
- export const REMOTE_PROTOCOL_VERSION = 1;
1
+ export const REMOTE_PROTOCOL_VERSION = 2;
2
2
  export const REMOTE_PROTOCOL_PATH = "/lix/v1/";
3
3
  export function encodeWireValue(value) {
4
4
  switch (value.kind) {
@@ -66,6 +66,10 @@ export function decodeHandshake(value) {
66
66
  handshake.activeBranchId.length === 0) {
67
67
  throw protocolError("remote handshake requires activeBranchId");
68
68
  }
69
+ if (typeof handshake.activeAccountId !== "string" ||
70
+ handshake.activeAccountId.length === 0) {
71
+ throw protocolError("remote handshake requires activeAccountId");
72
+ }
69
73
  if (typeof handshake.sessionId !== "string" ||
70
74
  !/^[\x21-\x7e]{1,256}$/.test(handshake.sessionId)) {
71
75
  throw protocolError("remote handshake requires a valid sessionId");
@@ -73,6 +77,7 @@ export function decodeHandshake(value) {
73
77
  return {
74
78
  protocolVersion: REMOTE_PROTOCOL_VERSION,
75
79
  activeBranchId: handshake.activeBranchId,
80
+ activeAccountId: handshake.activeAccountId,
76
81
  sessionId: handshake.sessionId,
77
82
  };
78
83
  }
@@ -127,7 +132,7 @@ function applyObserveBlobDelta(delta, sequence, base) {
127
132
  }
128
133
  const baseValue = base.rows.rows[0]?.[0];
129
134
  if (base.rows.columns.length !== 1 ||
130
- base.rows.columns[0] !== "data" ||
135
+ base.rows.columns[0] !== "content" ||
131
136
  base.rows.rows.length !== 1 ||
132
137
  base.rows.rows[0]?.length !== 1 ||
133
138
  base.rows.rowsAffected !== 0 ||
@@ -154,7 +159,7 @@ function applyObserveBlobDelta(delta, sequence, base) {
154
159
  blob.set(insert, prefixBytes);
155
160
  blob.set(baseValue.blob.subarray(baseValue.blob.byteLength - suffixBytes), prefixBytes + insert.byteLength);
156
161
  return {
157
- columns: ["data"],
162
+ columns: ["content"],
158
163
  rows: [[{ kind: "blob", value: null, blob }]],
159
164
  rowsAffected: 0,
160
165
  notices: [],
package/dist/types.d.ts CHANGED
@@ -132,6 +132,16 @@ export type CreateBranchReceipt = {
132
132
  export type CreateCheckpointReceipt = {
133
133
  commitId: string;
134
134
  };
135
+ export type UndoReceipt = {
136
+ branchId: string;
137
+ targetCommitId: string;
138
+ inverseCommitId: string;
139
+ };
140
+ export type RedoReceipt = {
141
+ branchId: string;
142
+ targetCommitId: string;
143
+ replayCommitId: string;
144
+ };
135
145
  export type SwitchBranchOptions = {
136
146
  branchId: string;
137
147
  };
@@ -5,6 +5,7 @@ export class WasmLix {
5
5
  private constructor();
6
6
  free(): void;
7
7
  [Symbol.dispose](): void;
8
+ activeAccountId(): Promise<string>;
8
9
  activeBranchId(): Promise<string>;
9
10
  beginTransaction(): Promise<WasmLixTransaction>;
10
11
  clientStateDelete(key: string): Promise<void>;
@@ -20,7 +21,9 @@ export class WasmLix {
20
21
  mergeBranch(options: any): Promise<any>;
21
22
  mergeBranchPreview(options: any): Promise<any>;
22
23
  observe(sql: string, params: any): Promise<WasmObserveEvents>;
24
+ redo(): Promise<any>;
23
25
  switchBranch(options: any): Promise<any>;
26
+ undo(): Promise<any>;
24
27
  }
25
28
 
26
29
  export class WasmLixTransaction {
@@ -56,6 +59,7 @@ export interface InitOutput {
56
59
  readonly openMemory: (a: number) => number;
57
60
  readonly openMemoryFromSnapshot: (a: number, b: number, c: number) => number;
58
61
  readonly parseSqlScript: (a: number, b: number, c: number, d: number) => void;
62
+ readonly wasmlix_activeAccountId: (a: number) => number;
59
63
  readonly wasmlix_activeBranchId: (a: number) => number;
60
64
  readonly wasmlix_beginTransaction: (a: number) => number;
61
65
  readonly wasmlix_clientStateDelete: (a: number, b: number, c: number) => number;
@@ -71,14 +75,16 @@ export interface InitOutput {
71
75
  readonly wasmlix_mergeBranch: (a: number, b: number) => number;
72
76
  readonly wasmlix_mergeBranchPreview: (a: number, b: number) => number;
73
77
  readonly wasmlix_observe: (a: number, b: number, c: number, d: number) => number;
78
+ readonly wasmlix_redo: (a: number) => number;
74
79
  readonly wasmlix_switchBranch: (a: number, b: number) => number;
80
+ readonly wasmlix_undo: (a: number) => number;
75
81
  readonly wasmlixtransaction_commit: (a: number) => number;
76
82
  readonly wasmlixtransaction_execute: (a: number, b: number, c: number, d: number, e: number) => number;
77
83
  readonly wasmlixtransaction_rollback: (a: number) => number;
78
84
  readonly wasmobserveevents_close: (a: number) => void;
79
85
  readonly wasmobserveevents_next: (a: number) => number;
80
- readonly __wasm_bindgen_func_elem_111270: (a: number, b: number, c: number, d: number) => void;
81
- readonly __wasm_bindgen_func_elem_111272: (a: number, b: number, c: number, d: number) => void;
86
+ readonly __wasm_bindgen_func_elem_120695: (a: number, b: number, c: number, d: number) => void;
87
+ readonly __wasm_bindgen_func_elem_120697: (a: number, b: number, c: number, d: number) => void;
82
88
  readonly __wbindgen_export: (a: number, b: number) => number;
83
89
  readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
84
90
  readonly __wbindgen_export3: (a: number) => void;
@@ -17,6 +17,13 @@ export class WasmLix {
17
17
  const ptr = this.__destroy_into_raw();
18
18
  wasm.__wbg_wasmlix_free(ptr, 0);
19
19
  }
20
+ /**
21
+ * @returns {Promise<string>}
22
+ */
23
+ activeAccountId() {
24
+ const ret = wasm.wasmlix_activeAccountId(this.__wbg_ptr);
25
+ return takeObject(ret);
26
+ }
20
27
  /**
21
28
  * @returns {Promise<string>}
22
29
  */
@@ -146,6 +153,13 @@ export class WasmLix {
146
153
  const ret = wasm.wasmlix_observe(this.__wbg_ptr, ptr0, len0, addHeapObject(params));
147
154
  return takeObject(ret);
148
155
  }
156
+ /**
157
+ * @returns {Promise<any>}
158
+ */
159
+ redo() {
160
+ const ret = wasm.wasmlix_redo(this.__wbg_ptr);
161
+ return takeObject(ret);
162
+ }
149
163
  /**
150
164
  * @param {any} options
151
165
  * @returns {Promise<any>}
@@ -154,6 +168,13 @@ export class WasmLix {
154
168
  const ret = wasm.wasmlix_switchBranch(this.__wbg_ptr, addHeapObject(options));
155
169
  return takeObject(ret);
156
170
  }
171
+ /**
172
+ * @returns {Promise<any>}
173
+ */
174
+ undo() {
175
+ const ret = wasm.wasmlix_undo(this.__wbg_ptr);
176
+ return takeObject(ret);
177
+ }
157
178
  }
158
179
  if (Symbol.dispose) WasmLix.prototype[Symbol.dispose] = WasmLix.prototype.free;
159
180
 
@@ -515,7 +536,7 @@ function __wbg_get_imports() {
515
536
  const a = state0.a;
516
537
  state0.a = 0;
517
538
  try {
518
- return __wasm_bindgen_func_elem_111272(a, state0.b, arg0, arg1);
539
+ return __wasm_bindgen_func_elem_120697(a, state0.b, arg0, arg1);
519
540
  } finally {
520
541
  state0.a = a;
521
542
  }
@@ -625,8 +646,8 @@ function __wbg_get_imports() {
625
646
  return addHeapObject(ret);
626
647
  },
627
648
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
628
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 29773, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
629
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_111270);
649
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 30501, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
650
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_120695);
630
651
  return addHeapObject(ret);
631
652
  },
632
653
  __wbindgen_cast_0000000000000002: function(arg0) {
@@ -675,10 +696,10 @@ function __wbg_get_imports() {
675
696
  };
676
697
  }
677
698
 
678
- function __wasm_bindgen_func_elem_111270(arg0, arg1, arg2) {
699
+ function __wasm_bindgen_func_elem_120695(arg0, arg1, arg2) {
679
700
  try {
680
701
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
681
- wasm.__wasm_bindgen_func_elem_111270(retptr, arg0, arg1, addHeapObject(arg2));
702
+ wasm.__wasm_bindgen_func_elem_120695(retptr, arg0, arg1, addHeapObject(arg2));
682
703
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
683
704
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
684
705
  if (r1) {
@@ -689,8 +710,8 @@ function __wasm_bindgen_func_elem_111270(arg0, arg1, arg2) {
689
710
  }
690
711
  }
691
712
 
692
- function __wasm_bindgen_func_elem_111272(arg0, arg1, arg2, arg3) {
693
- wasm.__wasm_bindgen_func_elem_111272(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
713
+ function __wasm_bindgen_func_elem_120697(arg0, arg1, arg2, arg3) {
714
+ wasm.__wasm_bindgen_func_elem_120697(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
694
715
  }
695
716
 
696
717
  const WasmLixFinalization = (typeof FinalizationRegistry === 'undefined')
Binary file
@@ -7,6 +7,7 @@ export const __wbg_wasmobserveevents_free: (a: number, b: number) => void;
7
7
  export const openMemory: (a: number) => number;
8
8
  export const openMemoryFromSnapshot: (a: number, b: number, c: number) => number;
9
9
  export const parseSqlScript: (a: number, b: number, c: number, d: number) => void;
10
+ export const wasmlix_activeAccountId: (a: number) => number;
10
11
  export const wasmlix_activeBranchId: (a: number) => number;
11
12
  export const wasmlix_beginTransaction: (a: number) => number;
12
13
  export const wasmlix_clientStateDelete: (a: number, b: number, c: number) => number;
@@ -22,14 +23,16 @@ export const wasmlix_exportSnapshot: (a: number) => number;
22
23
  export const wasmlix_mergeBranch: (a: number, b: number) => number;
23
24
  export const wasmlix_mergeBranchPreview: (a: number, b: number) => number;
24
25
  export const wasmlix_observe: (a: number, b: number, c: number, d: number) => number;
26
+ export const wasmlix_redo: (a: number) => number;
25
27
  export const wasmlix_switchBranch: (a: number, b: number) => number;
28
+ export const wasmlix_undo: (a: number) => number;
26
29
  export const wasmlixtransaction_commit: (a: number) => number;
27
30
  export const wasmlixtransaction_execute: (a: number, b: number, c: number, d: number, e: number) => number;
28
31
  export const wasmlixtransaction_rollback: (a: number) => number;
29
32
  export const wasmobserveevents_close: (a: number) => void;
30
33
  export const wasmobserveevents_next: (a: number) => number;
31
- export const __wasm_bindgen_func_elem_111270: (a: number, b: number, c: number, d: number) => void;
32
- export const __wasm_bindgen_func_elem_111272: (a: number, b: number, c: number, d: number) => void;
34
+ export const __wasm_bindgen_func_elem_120695: (a: number, b: number, c: number, d: number) => void;
35
+ export const __wasm_bindgen_func_elem_120697: (a: number, b: number, c: number, d: number) => void;
33
36
  export const __wbindgen_export: (a: number, b: number) => number;
34
37
  export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
35
38
  export const __wbindgen_export3: (a: number) => void;
@@ -1,4 +1,4 @@
1
- import { createWorkerConnection } from "#worker-factory";
1
+ import { createWorkerConnection, openDirectLixBinding } from "#worker-factory";
2
2
  import { snapshotPersistenceAfterCommitError } from "../snapshot-persistence.js";
3
3
  import { deserializeWorkerError, } from "./protocol.js";
4
4
  const MAX_IDLE_WORKERS = 1;
@@ -26,6 +26,41 @@ export async function openLixWorker(storage, onDisposed, telemetry) {
26
26
  }
27
27
  /** Opens the local worker transport behind the semantic Lix binding. */
28
28
  export async function openLixWorkerBinding(storage, onDisposed, telemetry) {
29
+ if (openDirectLixBinding) {
30
+ const telemetryDispatch = telemetry
31
+ ? (span) => {
32
+ try {
33
+ telemetry.onSpan(span);
34
+ }
35
+ catch {
36
+ // Telemetry is observational and must not fail engine commands.
37
+ }
38
+ }
39
+ : undefined;
40
+ const binding = await openDirectLixBinding(storage, telemetryDispatch);
41
+ if (!onDisposed)
42
+ return binding;
43
+ let disposed = false;
44
+ return new Proxy(binding, {
45
+ get(target, property, receiver) {
46
+ if (property === "close") {
47
+ return async () => {
48
+ try {
49
+ await target.close();
50
+ }
51
+ finally {
52
+ if (!disposed) {
53
+ disposed = true;
54
+ onDisposed();
55
+ }
56
+ }
57
+ };
58
+ }
59
+ const value = Reflect.get(target, property, receiver);
60
+ return typeof value === "function" ? value.bind(target) : value;
61
+ },
62
+ });
63
+ }
29
64
  const client = await openLixWorker(storage, onDisposed, telemetry);
30
65
  return workerBinding(client);
31
66
  }
@@ -95,12 +130,15 @@ function workerBinding(client) {
95
130
  return workerTransactionBinding(request, transactionId);
96
131
  },
97
132
  activeBranchId: () => request({ kind: "activeBranchId" }),
133
+ activeAccountId: () => request({ kind: "activeAccountId" }),
98
134
  clientStateEntries: () => request({ kind: "clientState.entries" }),
99
135
  clientStateGet: (key) => request({ kind: "clientState.get", key }),
100
136
  clientStateSet: (key, value) => request({ kind: "clientState.set", key, value }),
101
137
  clientStateDelete: (key) => request({ kind: "clientState.delete", key }),
102
138
  createBranch: (options) => request({ kind: "createBranch", options }),
103
139
  createCheckpoint: () => request({ kind: "createCheckpoint" }),
140
+ undo: () => request({ kind: "undo" }),
141
+ redo: () => request({ kind: "redo" }),
104
142
  switchBranch: (options) => request({ kind: "switchBranch", options }),
105
143
  importFilesystemPaths: (paths) => request({ kind: "importFilesystemPaths", paths }),
106
144
  mergeBranchPreview: (options) => request({ kind: "mergeBranchPreview", options }),
@@ -158,6 +196,7 @@ function persistentSnapshotBinding(binding, storage, namespace) {
158
196
  };
159
197
  },
160
198
  activeBranchId: () => binding.activeBranchId(),
199
+ activeAccountId: () => binding.activeAccountId(),
161
200
  clientStateEntries: () => {
162
201
  const method = binding.clientStateEntries;
163
202
  if (!method)
@@ -184,6 +223,8 @@ function persistentSnapshotBinding(binding, storage, namespace) {
184
223
  },
185
224
  createBranch: (branchOptions) => afterMutation(binding.createBranch(branchOptions)),
186
225
  createCheckpoint: () => afterMutation(binding.createCheckpoint()),
226
+ undo: () => afterMutation(binding.undo()),
227
+ redo: () => afterMutation(binding.redo()),
187
228
  switchBranch: (branchOptions) => afterMutation(binding.switchBranch(branchOptions)),
188
229
  importFilesystemPaths: (paths) => afterMutation(binding.importFilesystemPaths(paths)),
189
230
  mergeBranchPreview: (branchOptions) => binding.mergeBranchPreview(branchOptions),
@@ -1,2 +1,4 @@
1
+ import type { LixBinding, LixStorageConfig, TelemetryDispatch } from "../binding-types.js";
1
2
  import type { WorkerConnection } from "./protocol.js";
3
+ export declare const openDirectLixBinding: undefined | ((storage: LixStorageConfig, telemetry?: TelemetryDispatch) => Promise<LixBinding>);
2
4
  export declare function createWorkerConnection(): WorkerConnection;
@@ -1,4 +1,6 @@
1
1
  /// <reference lib="webworker" />
2
+ // Browser/Wasm execution stays off the main thread.
3
+ export const openDirectLixBinding = undefined;
2
4
  export function createWorkerConnection() {
3
5
  const worker = new Worker(new URL("./entry.browser.js", import.meta.url), {
4
6
  type: "module",
@@ -1,2 +1,4 @@
1
+ import type { LixBinding, LixStorageConfig, TelemetryDispatch } from "../binding-types.js";
1
2
  import type { WorkerConnection } from "./protocol.js";
2
3
  export declare function createWorkerConnection(): WorkerConnection;
4
+ export declare const openDirectLixBinding: (storage: LixStorageConfig, telemetry?: TelemetryDispatch) => Promise<LixBinding>;
@@ -1,4 +1,5 @@
1
1
  import { Worker } from "node:worker_threads";
2
+ import { openLixBinding } from "../binding.node.js";
2
3
  export function createWorkerConnection() {
3
4
  const worker = new Worker(new URL("./entry.node.js", import.meta.url), {
4
5
  name: "lix",
@@ -33,3 +34,7 @@ export function createWorkerConnection() {
33
34
  },
34
35
  };
35
36
  }
37
+ /// Native Lix already owns a dedicated serialized engine actor. Routing it
38
+ /// through a second JavaScript worker adds two message-port hops per query
39
+ /// without adding isolation or concurrency.
40
+ export const openDirectLixBinding = (storage, telemetry) => openLixBinding(storage, telemetry);
@@ -87,6 +87,8 @@ export function startWorkerHost(endpoint) {
87
87
  }
88
88
  case "activeBranchId":
89
89
  return requiredLix().activeBranchId();
90
+ case "activeAccountId":
91
+ return requiredLix().activeAccountId();
90
92
  case "clientState.entries":
91
93
  return requiredClientStateMethod("clientStateEntries")();
92
94
  case "clientState.get":
@@ -99,6 +101,10 @@ export function startWorkerHost(endpoint) {
99
101
  return requiredLix().createBranch(operation.options);
100
102
  case "createCheckpoint":
101
103
  return requiredLix().createCheckpoint();
104
+ case "undo":
105
+ return requiredLix().undo();
106
+ case "redo":
107
+ return requiredLix().redo();
102
108
  case "switchBranch":
103
109
  return requiredLix().switchBranch(operation.options);
104
110
  case "mergeBranchPreview":
@@ -33,6 +33,8 @@ export type WorkerOperation = {
33
33
  transactionId: number;
34
34
  } | {
35
35
  kind: "activeBranchId";
36
+ } | {
37
+ kind: "activeAccountId";
36
38
  } | {
37
39
  kind: "clientState.entries";
38
40
  } | {
@@ -50,6 +52,10 @@ export type WorkerOperation = {
50
52
  options: CreateBranchOptions;
51
53
  } | {
52
54
  kind: "createCheckpoint";
55
+ } | {
56
+ kind: "undo";
57
+ } | {
58
+ kind: "redo";
53
59
  } | {
54
60
  kind: "switchBranch";
55
61
  options: SwitchBranchOptions;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lix-js/sdk",
3
3
  "type": "module",
4
- "version": "0.9.0",
4
+ "version": "0.10.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -57,10 +57,10 @@
57
57
  "typecheck": "tsc -p tsconfig.test.json --noEmit"
58
58
  },
59
59
  "optionalDependencies": {
60
- "@lix-js/sdk-darwin-arm64": "0.9.0",
61
- "@lix-js/sdk-linux-arm64": "0.9.0",
62
- "@lix-js/sdk-linux-x64": "0.9.0",
63
- "@lix-js/sdk-win32-x64": "0.9.0"
60
+ "@lix-js/sdk-darwin-arm64": "0.10.0",
61
+ "@lix-js/sdk-linux-arm64": "0.10.0",
62
+ "@lix-js/sdk-linux-x64": "0.10.0",
63
+ "@lix-js/sdk-win32-x64": "0.10.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@vitest/browser-playwright": "4.1.10",