@lix-js/sdk 0.8.4 → 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.
Files changed (49) hide show
  1. package/README.md +105 -22
  2. package/dist/binding-types.d.ts +21 -2
  3. package/dist/binding.browser.d.ts +2 -2
  4. package/dist/binding.browser.js +3 -3
  5. package/dist/binding.node.d.ts +2 -2
  6. package/dist/binding.node.js +18 -4
  7. package/dist/bundled-plugins/plugin_csv.lixplugin +0 -0
  8. package/dist/bundled-plugins/plugin_markdown.lixplugin +0 -0
  9. package/dist/bundled-plugins.js +2 -2
  10. package/dist/client-state.d.ts +53 -0
  11. package/dist/client-state.js +318 -0
  12. package/dist/index.d.ts +2 -1
  13. package/dist/lix.d.ts +47 -0
  14. package/dist/lix.js +378 -0
  15. package/dist/local-storage-adapter.d.ts +26 -0
  16. package/dist/local-storage-adapter.js +117 -0
  17. package/dist/open-lix.d.ts +3 -34
  18. package/dist/open-lix.js +99 -170
  19. package/dist/remote/client.d.ts +9 -0
  20. package/dist/remote/client.js +1150 -0
  21. package/dist/remote/protocol.d.ts +178 -0
  22. package/dist/remote/protocol.js +367 -0
  23. package/dist/remote/sse.d.ts +12 -0
  24. package/dist/remote/sse.js +87 -0
  25. package/dist/snapshot-persistence.d.ts +7 -0
  26. package/dist/snapshot-persistence.js +26 -0
  27. package/dist/types.d.ts +68 -1
  28. package/dist/wasm/lix_js_sdk.d.ts +22 -4
  29. package/dist/wasm/lix_js_sdk.js +96 -17
  30. package/dist/wasm/lix_js_sdk_bg.wasm +0 -0
  31. package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +11 -2
  32. package/dist/worker/client.d.ts +23 -4
  33. package/dist/worker/client.js +329 -11
  34. package/dist/worker/factory.browser.d.ts +2 -0
  35. package/dist/worker/factory.browser.js +2 -0
  36. package/dist/worker/factory.node.d.ts +2 -0
  37. package/dist/worker/factory.node.js +5 -0
  38. package/dist/worker/host.js +36 -2
  39. package/dist/worker/protocol.d.ts +32 -2
  40. package/dist/workerd.js +1 -3
  41. package/package.json +19 -16
  42. package/dist/bundled-plugins/plugin_md_v2.lixplugin +0 -0
  43. package/dist/jco/js-component-bindgen-component.core.wasm +0 -0
  44. package/dist/jco/js-component-bindgen-component.core2.wasm +0 -0
  45. package/dist/jco/js-component-bindgen-component.js +0 -13662
  46. package/dist/jco-transpile.browser.d.ts +0 -14
  47. package/dist/jco-transpile.browser.js +0 -22
  48. package/dist/plugin-runtime.d.ts +0 -45
  49. package/dist/plugin-runtime.js +0 -124
package/README.md CHANGED
@@ -22,6 +22,72 @@ console.log(result.rows[0]?.get("message"));
22
22
  await lix.close();
23
23
  ```
24
24
 
25
+ ## Remote workspaces
26
+
27
+ Use the same Lix client as a thin client against a hosted workspace:
28
+
29
+ ```ts
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
+ },
38
+ });
39
+
40
+ const files = lix.observe("SELECT path FROM lix_file ORDER BY path");
41
+ const initial = await files.next();
42
+
43
+ await lix.execute(
44
+ "INSERT INTO lix_file (path, content) VALUES ($1, $2)",
45
+ ["/hello.txt", new TextEncoder().encode("hello")],
46
+ );
47
+ const update = await files.next();
48
+
49
+ files.close();
50
+ await lix.close();
51
+ ```
52
+
53
+ Without `storage`, remote mode uses the server for all persistence and does not
54
+ open a local engine. Dynamic headers are resolved for every request and
55
+ observation reconnect. An injected `fetch` can route requests through a service
56
+ binding or another authorized server-side transport.
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.
90
+
25
91
  Filesystem sync and SQLite persistence use native Node.js dependencies:
26
92
 
27
93
  ```ts
@@ -35,20 +101,37 @@ const lix = await openLix({
35
101
  });
36
102
 
37
103
  await lix.execute(
38
- "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",
39
105
  ["/hello.txt", new TextEncoder().encode("world")],
40
106
  );
41
107
 
42
- 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", [
43
109
  "/hello.txt",
44
110
  ]);
45
- const bytes = result.rows[0]?.value("data").asBytes();
111
+ const bytes = result.rows[0]?.value("content").asBytes();
46
112
 
47
113
  console.log(bytes && new TextDecoder().decode(bytes));
48
114
 
49
115
  await lix.close();
50
116
  ```
51
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
+
52
135
  ## Branches
53
136
 
54
137
  ```ts
@@ -57,7 +140,7 @@ const draft = await lix.createBranch({ name: "Draft" });
57
140
 
58
141
  await lix.switchBranch({ branchId: draft.id });
59
142
  await lix.execute(
60
- "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",
61
144
  ["/status.txt", new TextEncoder().encode("draft")],
62
145
  );
63
146
 
@@ -73,11 +156,11 @@ const tx = await lix.beginTransaction();
73
156
 
74
157
  try {
75
158
  await tx.execute(
76
- "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",
77
160
  ["/a.txt", new TextEncoder().encode("1")],
78
161
  );
79
162
  await tx.execute(
80
- "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",
81
164
  ["/b.txt", new TextEncoder().encode("2")],
82
165
  );
83
166
  await tx.commit();
@@ -93,36 +176,36 @@ try {
93
176
  - Pass `new LocalFilesystem({ path, lixDir, syncAllFiles: true })` for filesystem sync with repository metadata in an external `.lix` directory and no workspace `.lix` directory.
94
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.
95
178
  - Use `new SQLite({ path })` when a single SQLite-backed `.lix` file is the application document itself, for example when defining a new file format and using Lix as the application's file format.
96
- - In browsers, `openLix()` loads the Rust engine as WebAssembly and uses the
97
- in-memory storage.
179
+ - In browsers, local mode and remote mode with client storage load the Rust
180
+ engine as WebAssembly. Supplying a snapshot storage adapter persists that
181
+ local Lix; in remote mode, the local engine contains only client state.
98
182
  - `LocalFilesystem` and `SQLite` are Node.js-only. Constructing them is safe in
99
183
  shared code, but passing one to `openLix()` in a browser throws an error.
100
184
  - The package is ESM-only.
101
185
  - The package uses conditional ESM imports internally: Node.js resolves the
102
186
  native N-API binding, while browsers and other runtimes resolve the portable
103
187
  WebAssembly binding. Vite follows this split without consumer configuration.
104
- - Every `openLix()` owns one dedicated worker. The engine, storage, and
105
- installed WASM plugin components all run in that worker in both Node.js and
106
- browsers, so database and plugin work does not block the page's main thread.
107
- - Installed WASM plugin components are transpiled with JCO and executed by the
108
- worker's WebAssembly runtime in both environments. Plugin execution does not
109
- yet enforce the declared fuel, timeout, or memory limits, so only install
110
- trusted plugins.
188
+ - Every browser `openLix()` owns one dedicated worker, so database work does
189
+ not block the page's main thread. Node.js uses the native binding's actor.
190
+ - Node.js executes installed Component API v1 plugins with the Rust SDK's
191
+ Wasmtime runtime. The browser and Workerd bindings currently open without a
192
+ component runtime: they can use ordinary Lix storage and SQL, but do not
193
+ execute installed plugins. A browser Component host is a separate follow-up.
111
194
  - A page Content Security Policy only needs to permit the package's same-origin
112
- worker. WebAssembly compilation and JCO's generated `data:` module imports
113
- happen inside that worker, so they can be scoped to the worker script's HTTP
114
- response instead of being allowed by the document:
195
+ worker. WebAssembly compilation happens inside that worker, so the required
196
+ permission can be scoped to the worker script's HTTP response instead of
197
+ being allowed by the document:
115
198
 
116
199
  ```http
117
200
  # HTML document response
118
201
  Content-Security-Policy: default-src 'self'; script-src 'self'; worker-src 'self'
119
202
 
120
203
  # Lix worker response (Vite emits assets/entry.browser-<hash>.js)
121
- Content-Security-Policy: default-src 'none'; script-src 'self' data: 'wasm-unsafe-eval'; connect-src 'self'
204
+ Content-Security-Policy: default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self'
122
205
  ```
123
206
 
124
207
  Hosts that apply one policy to every response can use
125
- `script-src 'self' data: 'wasm-unsafe-eval'; worker-src 'self'` globally
208
+ `script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'` globally
126
209
  instead. Worker-scoped headers keep those permissions out of the page.
127
210
  - SQL parameters use normal JavaScript values: `string`, finite `number`, `boolean`, `Uint8Array`, `null`, JSON-compatible arrays, and JSON-compatible plain objects.
128
211
  - Use `Value.integer(...)`, `Value.real(...)`, `Value.text(...)`, `Value.json(...)`, or `Value.blob(...)` only when you need to pass an explicit native Lix value.
@@ -141,8 +224,8 @@ npm run test:browser
141
224
 
142
225
  `npm run test:browser:production` additionally packs the SDK, installs the
143
226
  tarball into a minimal Vite app, makes a production build, and exercises SQL
144
- plus both bundled plugins in Chromium. It runs with both worker-scoped and
145
- global strict CSP headers.
227
+ plus bundled-plugin archive loading in Chromium. It runs with both
228
+ worker-scoped and global strict CSP headers.
146
229
 
147
230
  Use `npm run build:wasm:dev` while iterating on the Rust bridge when release
148
231
  optimization is unnecessary.
@@ -1,4 +1,4 @@
1
- import type { CreateBranchOptions, CreateBranchReceipt, ExecuteOptions, MergeBranchOptions, MergeBranchPreview, MergeBranchReceipt, SwitchBranchOptions, SwitchBranchReceipt } 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[];
@@ -16,17 +16,35 @@ export type BindingObserveEvent = {
16
16
  rows: BindingExecuteResult;
17
17
  };
18
18
  export type BindingParam = NativeLixValue;
19
+ export type BindingBatchStatement = {
20
+ sql: string;
21
+ params: BindingParam[];
22
+ };
19
23
  export type LixBinding = {
20
24
  execute(sql: string, params: BindingParam[], options?: ExecuteOptions): Promise<BindingExecuteResult>;
25
+ executeBatch(statements: BindingBatchStatement[], options?: LixBatchOptions): Promise<BindingExecuteResult[]>;
21
26
  observe(sql: string, params: BindingParam[]): Promise<ObserveEventsBinding>;
22
27
  beginTransaction(): Promise<LixTransactionBinding>;
23
28
  activeBranchId(): Promise<string>;
29
+ activeAccountId(): Promise<string>;
30
+ clientStateEntries?(): Promise<Array<{
31
+ key: string;
32
+ value: JsonValue;
33
+ }>>;
34
+ clientStateGet?(key: string): Promise<JsonValue | undefined>;
35
+ clientStateSet?(key: string, value: JsonValue): Promise<void>;
36
+ clientStateDelete?(key: string): Promise<void>;
24
37
  createBranch(options: CreateBranchOptions): Promise<CreateBranchReceipt>;
38
+ createCheckpoint(): Promise<CreateCheckpointReceipt>;
39
+ undo(): Promise<UndoReceipt>;
40
+ redo(): Promise<RedoReceipt>;
25
41
  switchBranch(options: SwitchBranchOptions): Promise<SwitchBranchReceipt>;
26
42
  importFilesystemPaths(paths: string[]): Promise<void>;
27
43
  mergeBranchPreview(options: MergeBranchOptions): Promise<MergeBranchPreview>;
28
44
  mergeBranch(options: MergeBranchOptions): Promise<MergeBranchReceipt>;
29
45
  syncDiskToLix(): Promise<void>;
46
+ /** Internal snapshot capability implemented by browser memory bindings. */
47
+ exportSnapshot?(): Promise<Uint8Array>;
30
48
  close(): Promise<void>;
31
49
  };
32
50
  export type LixTransactionBinding = {
@@ -38,9 +56,10 @@ export type ObserveEventsBinding = {
38
56
  next(): Promise<BindingObserveEvent | null | undefined>;
39
57
  close(): void;
40
58
  };
41
- export type PluginRuntimeDispatch = (request: unknown) => Promise<unknown>;
59
+ export type TelemetryDispatch = (span: LixTelemetrySpan) => void;
42
60
  export type LixStorageConfig = {
43
61
  kind: "memory";
62
+ snapshot?: Uint8Array;
44
63
  } | {
45
64
  kind: "sqlite";
46
65
  path: string;
@@ -1,2 +1,2 @@
1
- import type { LixStorageConfig, LixBinding, PluginRuntimeDispatch } from "./binding-types.js";
2
- export declare function openLixBinding(storage: LixStorageConfig, dispatch: PluginRuntimeDispatch): Promise<LixBinding>;
1
+ import type { LixStorageConfig, LixBinding, TelemetryDispatch } from "./binding-types.js";
2
+ export declare function openLixBinding(storage: LixStorageConfig, telemetry?: TelemetryDispatch): Promise<LixBinding>;
@@ -1,14 +1,14 @@
1
1
  // Generated before TypeScript compilation and emitted beside this module.
2
2
  // @ts-expect-error Generated by build:wasm.
3
- import initWasm, { openMemory } from "./wasm/lix_js_sdk.js";
3
+ import initWasm, { openMemoryFromSnapshot } from "./wasm/lix_js_sdk.js";
4
4
  let wasmInitialized;
5
5
  function initializeWasm() {
6
6
  return (wasmInitialized ??= initWasm());
7
7
  }
8
- export async function openLixBinding(storage, dispatch) {
8
+ export async function openLixBinding(storage, telemetry) {
9
9
  if (storage.kind !== "memory") {
10
10
  throw new Error(`${storage.kind === "localFilesystem" ? "LocalFilesystem" : "SQLite"} is only available in Node.js`);
11
11
  }
12
12
  await initializeWasm();
13
- return openMemory(dispatch);
13
+ return openMemoryFromSnapshot(telemetry, storage.snapshot);
14
14
  }
@@ -1,2 +1,2 @@
1
- import type { LixStorageConfig, LixBinding, PluginRuntimeDispatch } from "./binding-types.js";
2
- export declare function openLixBinding(storage: LixStorageConfig, dispatch: PluginRuntimeDispatch): Promise<LixBinding>;
1
+ import type { LixStorageConfig, LixBinding, TelemetryDispatch } from "./binding-types.js";
2
+ export declare function openLixBinding(storage: LixStorageConfig, telemetry?: TelemetryDispatch): Promise<LixBinding>;
@@ -39,13 +39,27 @@ catch (cause) {
39
39
  error.cause = cause;
40
40
  throw error;
41
41
  }
42
- export function openLixBinding(storage, dispatch) {
42
+ export function openLixBinding(storage, telemetry) {
43
+ const nativeTelemetry = telemetry
44
+ ? (spanJson) => telemetry(JSON.parse(spanJson))
45
+ : undefined;
43
46
  switch (storage.kind) {
44
47
  case "memory":
45
- return addon.Lix.openMemory(dispatch);
48
+ if (storage.snapshot !== undefined) {
49
+ throw new Error("Memory snapshots are only available in the browser binding");
50
+ }
51
+ if (nativeTelemetry)
52
+ return addon.Lix.openMemory(nativeTelemetry);
53
+ return addon.Lix.openMemory();
46
54
  case "sqlite":
47
- return addon.Lix.openSQLite(storage.path, dispatch);
55
+ if (nativeTelemetry) {
56
+ return addon.Lix.openSQLite(storage.path, nativeTelemetry);
57
+ }
58
+ return addon.Lix.openSQLite(storage.path);
48
59
  case "localFilesystem":
49
- return addon.Lix.openLocalFilesystem(storage.path, storage.lixDir, storage.syncAllFiles, dispatch);
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);
50
64
  }
51
65
  }
@@ -1,7 +1,7 @@
1
1
  const BUNDLED_PLUGIN_MANIFEST = [
2
2
  {
3
- key: "plugin_md_v2",
4
- fileName: "plugin_md_v2.lixplugin",
3
+ key: "plugin_markdown",
4
+ fileName: "plugin_markdown.lixplugin",
5
5
  },
6
6
  {
7
7
  key: "plugin_csv",
@@ -0,0 +1,53 @@
1
+ import type { LixBinding } from "./binding-types.js";
2
+ import type { JsonValue, LixSnapshotStorage } from "./types.js";
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";
5
+ export type LixClientState = {
6
+ /** Returns the hydrated client-local value without a network round trip. */
7
+ get<T extends JsonValue = JsonValue>(key: string): T | undefined;
8
+ /** Persists a client-local value in the configured client storage. */
9
+ set(key: string, value: JsonValue): Promise<void>;
10
+ /** Deletes a client-local value from the configured client storage. */
11
+ delete(key: string): Promise<void>;
12
+ /** Subscribes to successful mutations made through this client-state handle. */
13
+ subscribe(listener: () => void): () => void;
14
+ };
15
+ export type ManagedClientState = LixClientState & {
16
+ close(): Promise<void>;
17
+ };
18
+ export declare function unavailableClientState(): LixClientState;
19
+ type ClientStateBinding = LixBinding & {
20
+ exportSnapshot?: () => Promise<Uint8Array>;
21
+ };
22
+ export type OpenClientStateOptions = {
23
+ readonly binding: ClientStateBinding;
24
+ readonly saveSnapshot?: (snapshot: Uint8Array) => Promise<void>;
25
+ readonly closeBinding?: boolean;
26
+ };
27
+ /**
28
+ * Opens the typed client-state facade over a private local Rust Lix.
29
+ *
30
+ * Values are ordinary global, untracked `lix_key_value` rows. The physical
31
+ * prefix is intentionally private so built-in Lix key/value rows never leak
32
+ * through this small API.
33
+ */
34
+ export declare function openClientState(options: OpenClientStateOptions): Promise<ManagedLixClientState>;
35
+ export declare class ManagedLixClientState implements LixClientState {
36
+ #private;
37
+ constructor(options: OpenClientStateOptions, initial: Map<string, JsonValue>);
38
+ get<T extends JsonValue = JsonValue>(key: string): T | undefined;
39
+ set(key: string, value: JsonValue): Promise<void>;
40
+ delete(key: string): Promise<void>;
41
+ subscribe(listener: () => void): () => void;
42
+ close(): Promise<void>;
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>;
53
+ export {};