@lix-js/sdk 0.7.0 → 0.8.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 (43) hide show
  1. package/README.md +90 -22
  2. package/dist/binding-types.d.ts +52 -0
  3. package/dist/binding-types.js +1 -0
  4. package/dist/binding.browser.d.ts +2 -0
  5. package/dist/binding.browser.js +14 -0
  6. package/dist/binding.node.d.ts +2 -0
  7. package/dist/{native.js → binding.node.js} +15 -11
  8. package/dist/bundled-plugins/plugin_csv.lixplugin +0 -0
  9. package/dist/bundled-plugins/plugin_md_v2.lixplugin +0 -0
  10. package/dist/bundled-plugins.js +10 -2
  11. package/dist/errors.d.ts +2 -0
  12. package/dist/errors.js +13 -0
  13. package/dist/index.d.ts +1 -1
  14. package/dist/jco/js-component-bindgen-component.core.wasm +0 -0
  15. package/dist/jco/js-component-bindgen-component.core2.wasm +0 -0
  16. package/dist/jco/js-component-bindgen-component.js +13662 -0
  17. package/dist/jco-transpile.browser.d.ts +14 -0
  18. package/dist/jco-transpile.browser.js +22 -0
  19. package/dist/open-lix.d.ts +21 -39
  20. package/dist/open-lix.js +172 -34
  21. package/dist/plugin-runtime.d.ts +45 -0
  22. package/dist/plugin-runtime.js +124 -0
  23. package/dist/types.d.ts +5 -0
  24. package/dist/wasm/lix_js_sdk.d.ts +90 -0
  25. package/dist/wasm/lix_js_sdk.js +918 -0
  26. package/dist/wasm/lix_js_sdk_bg.wasm +0 -0
  27. package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +29 -0
  28. package/dist/worker/client.d.ts +17 -0
  29. package/dist/worker/client.js +104 -0
  30. package/dist/worker/entry.browser.d.ts +1 -0
  31. package/dist/worker/entry.browser.js +11 -0
  32. package/dist/worker/entry.node.d.ts +1 -0
  33. package/dist/worker/entry.node.js +13 -0
  34. package/dist/worker/factory.browser.d.ts +2 -0
  35. package/dist/worker/factory.browser.js +23 -0
  36. package/dist/worker/factory.node.d.ts +2 -0
  37. package/dist/worker/factory.node.js +35 -0
  38. package/dist/worker/host.d.ts +2 -0
  39. package/dist/worker/host.js +145 -0
  40. package/dist/worker/protocol.d.ts +96 -0
  41. package/dist/worker/protocol.js +23 -0
  42. package/package.json +33 -8
  43. package/dist/native.d.ts +0 -1
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @lix-js/sdk
2
2
 
3
- JavaScript SDK for Lix, backed by the native Rust SDK.
3
+ JavaScript SDK for Lix. It uses the native Rust addon in Node.js and the same
4
+ Rust SDK compiled to WebAssembly in browsers.
4
5
 
5
6
  ## Install
6
7
 
@@ -10,20 +11,36 @@ npm install @lix-js/sdk
10
11
 
11
12
  ## Usage
12
13
 
14
+ The default in-memory backend works in browsers and Node.js:
15
+
16
+ ```ts
17
+ import { openLix } from "@lix-js/sdk";
18
+
19
+ const lix = await openLix();
20
+ const result = await lix.execute("SELECT $1 AS message", ["hello"]);
21
+ console.log(result.rows[0]?.get("message"));
22
+ await lix.close();
23
+ ```
24
+
25
+ Filesystem and SQLite backends use native Node.js dependencies:
26
+
13
27
  ```ts
14
- import { openLix, SqliteBackend } from "@lix-js/sdk";
28
+ import { FsBackend, openLix } from "@lix-js/sdk";
15
29
 
16
30
  const lix = await openLix({
17
- backend: new SqliteBackend({ path: "app.lix" }),
31
+ backend: new FsBackend({
32
+ path: "./workspace",
33
+ syncAllFiles: true,
34
+ }),
18
35
  });
19
36
 
20
37
  await lix.execute(
21
- "INSERT INTO lix_file (path, data) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET data = excluded.data",
22
- ["/hello.txt", new TextEncoder().encode("world")],
38
+ "INSERT INTO lix_file (path, data) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET data = excluded.data",
39
+ ["/hello.txt", new TextEncoder().encode("world")],
23
40
  );
24
41
 
25
42
  const result = await lix.execute("SELECT data FROM lix_file WHERE path = $1", [
26
- "/hello.txt",
43
+ "/hello.txt",
27
44
  ]);
28
45
  const bytes = result.rows[0]?.value("data").asBytes();
29
46
 
@@ -40,8 +57,8 @@ const draft = await lix.createBranch({ name: "Draft" });
40
57
 
41
58
  await lix.switchBranch({ branchId: draft.id });
42
59
  await lix.execute(
43
- "INSERT INTO lix_file (path, data) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET data = excluded.data",
44
- ["/status.txt", new TextEncoder().encode("draft")],
60
+ "INSERT INTO lix_file (path, data) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET data = excluded.data",
61
+ ["/status.txt", new TextEncoder().encode("draft")],
45
62
  );
46
63
 
47
64
  await lix.switchBranch({ branchId: main });
@@ -55,26 +72,77 @@ const merge = await lix.mergeBranch({ sourceBranchId: draft.id });
55
72
  const tx = await lix.beginTransaction();
56
73
 
57
74
  try {
58
- await tx.execute(
59
- "INSERT INTO lix_file (path, data) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET data = excluded.data",
60
- ["/a.txt", new TextEncoder().encode("1")],
61
- );
62
- await tx.execute(
63
- "INSERT INTO lix_file (path, data) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET data = excluded.data",
64
- ["/b.txt", new TextEncoder().encode("2")],
65
- );
66
- await tx.commit();
75
+ await tx.execute(
76
+ "INSERT INTO lix_file (path, data) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET data = excluded.data",
77
+ ["/a.txt", new TextEncoder().encode("1")],
78
+ );
79
+ await tx.execute(
80
+ "INSERT INTO lix_file (path, data) VALUES ($1, $2) ON CONFLICT (path) DO UPDATE SET data = excluded.data",
81
+ ["/b.txt", new TextEncoder().encode("2")],
82
+ );
83
+ await tx.commit();
67
84
  } catch (error) {
68
- await tx.rollback();
69
- throw error;
85
+ await tx.rollback();
86
+ throw error;
70
87
  }
71
88
  ```
72
89
 
73
90
  ## Notes
74
91
 
75
- - `openLix()` opens a fresh in-memory Lix. Pass `new SqliteBackend({ path })` for a raw SQLite `.lix` file, or `new FsBackend({ path })` for a filesystem workspace directory backed by `<path>/.lix/.internal/db.sqlite`.
76
- - The SDK is Node/native only right now; it is not browser-compatible.
92
+ - `openLix()` opens a fresh in-memory Lix. Pass `new FsBackend({ path, syncAllFiles: true })` for a filesystem workspace directory backed by `<path>/.lix/.internal/rocksdb`.
93
+ - Pass `new FsBackend({ path, lixDir, syncAllFiles: true })` for filesystem sync with repository metadata in an external `.lix` directory and no workspace `.lix` directory.
94
+ - Pass `syncAllFiles: false` to start filesystem sync with no regular workspace files, then call `backend.importPaths(["notes/today.md"])` on the `FsBackend` instance to sync selected files. Imported paths are exact workspace-relative file paths, not directories or globs.
95
+ - Use `new SqliteBackend({ 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 backend.
98
+ - `FsBackend` and `SqliteBackend` are Node.js-only. Constructing them is safe in
99
+ shared code, but passing one to `openLix()` in a browser throws an error.
77
100
  - The package is ESM-only.
78
- - The native addon is built from Rust and loaded by the TypeScript wrapper.
101
+ - The package uses conditional ESM imports internally: Node.js resolves the
102
+ native N-API binding, while browsers and other runtimes resolve the portable
103
+ WebAssembly binding. Vite follows this split without consumer configuration.
104
+ - Every `openLix()` owns one dedicated worker. The engine, storage backend, 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.
111
+ - 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:
115
+
116
+ ```http
117
+ # HTML document response
118
+ Content-Security-Policy: default-src 'self'; script-src 'self'; worker-src 'self'
119
+
120
+ # 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'
122
+ ```
123
+
124
+ Hosts that apply one policy to every response can use
125
+ `script-src 'self' data: 'wasm-unsafe-eval'; worker-src 'self'` globally
126
+ instead. Worker-scoped headers keep those permissions out of the page.
79
127
  - SQL parameters use normal JavaScript values: `string`, finite `number`, `boolean`, `Uint8Array`, `null`, JSON-compatible arrays, and JSON-compatible plain objects.
80
128
  - Use `Value.integer(...)`, `Value.real(...)`, `Value.text(...)`, `Value.json(...)`, or `Value.blob(...)` only when you need to pass an explicit native Lix value.
129
+
130
+ ## Browser development
131
+
132
+ The browser suite runs the published package shape in a real headless Chromium
133
+ page through Vite/Vitest Browser Mode:
134
+
135
+ ```bash
136
+ rustup target add wasm32-unknown-unknown
137
+ cargo install wasm-bindgen-cli --version 0.2.122 --locked
138
+ npx playwright install chromium
139
+ npm run test:browser
140
+ ```
141
+
142
+ `npm run test:browser:production` additionally packs the SDK, installs the
143
+ 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.
146
+
147
+ Use `npm run build:wasm:dev` while iterating on the Rust bridge when release
148
+ optimization is unnecessary.
@@ -0,0 +1,52 @@
1
+ import type { CreateBranchOptions, CreateBranchReceipt, ExecuteOptions, MergeBranchOptions, MergeBranchPreview, MergeBranchReceipt, SwitchBranchOptions, SwitchBranchReceipt } from "./types.js";
2
+ import type { NativeLixValue } from "./value.js";
3
+ export type BindingExecuteResult = {
4
+ columns: string[];
5
+ rows: NativeLixValue[][];
6
+ rowsAffected: number;
7
+ notices: Array<{
8
+ code: string;
9
+ message: string;
10
+ hint?: string;
11
+ }>;
12
+ };
13
+ export type BindingObserveEvent = {
14
+ sequence: number;
15
+ mutationSequence: number;
16
+ rows: BindingExecuteResult;
17
+ };
18
+ export type BindingParam = NativeLixValue;
19
+ export type LixBinding = {
20
+ execute(sql: string, params: BindingParam[], options?: ExecuteOptions): Promise<BindingExecuteResult>;
21
+ observe(sql: string, params: BindingParam[]): Promise<ObserveEventsBinding>;
22
+ beginTransaction(): Promise<LixTransactionBinding>;
23
+ activeBranchId(): Promise<string>;
24
+ createBranch(options: CreateBranchOptions): Promise<CreateBranchReceipt>;
25
+ switchBranch(options: SwitchBranchOptions): Promise<SwitchBranchReceipt>;
26
+ importFilesystemPaths(paths: string[]): Promise<void>;
27
+ mergeBranchPreview(options: MergeBranchOptions): Promise<MergeBranchPreview>;
28
+ mergeBranch(options: MergeBranchOptions): Promise<MergeBranchReceipt>;
29
+ syncDiskToLix(): Promise<void>;
30
+ close(): Promise<void>;
31
+ };
32
+ export type LixTransactionBinding = {
33
+ execute(sql: string, params: BindingParam[], options?: ExecuteOptions): Promise<BindingExecuteResult>;
34
+ commit(): Promise<void>;
35
+ rollback(): Promise<void>;
36
+ };
37
+ export type ObserveEventsBinding = {
38
+ next(): Promise<BindingObserveEvent | null | undefined>;
39
+ close(): void;
40
+ };
41
+ export type PluginRuntimeDispatch = (request: unknown) => Promise<unknown>;
42
+ export type LixBackendConfig = {
43
+ kind: "memory";
44
+ } | {
45
+ kind: "sqlite";
46
+ path: string;
47
+ } | {
48
+ kind: "fs";
49
+ path: string;
50
+ lixDir?: string;
51
+ syncAllFiles: boolean;
52
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { LixBackendConfig, LixBinding, PluginRuntimeDispatch } from "./binding-types.js";
2
+ export declare function openLixBinding(backend: LixBackendConfig, dispatch: PluginRuntimeDispatch): Promise<LixBinding>;
@@ -0,0 +1,14 @@
1
+ // Generated before TypeScript compilation and emitted beside this module.
2
+ // @ts-expect-error Generated by build:wasm.
3
+ import initWasm, { openMemory } from "./wasm/lix_js_sdk.js";
4
+ let wasmInitialized;
5
+ function initializeWasm() {
6
+ return (wasmInitialized ??= initWasm());
7
+ }
8
+ export async function openLixBinding(backend, dispatch) {
9
+ if (backend.kind !== "memory") {
10
+ throw new Error(`${backend.kind === "fs" ? "FsBackend" : "SqliteBackend"} is only available in Node.js`);
11
+ }
12
+ await initializeWasm();
13
+ return openMemory(dispatch);
14
+ }
@@ -0,0 +1,2 @@
1
+ import type { LixBackendConfig, LixBinding, PluginRuntimeDispatch } from "./binding-types.js";
2
+ export declare function openLixBinding(backend: LixBackendConfig, dispatch: PluginRuntimeDispatch): Promise<LixBinding>;
@@ -9,15 +9,11 @@ const nativePackages = {
9
9
  "darwin-arm64": "@lix-js/sdk-darwin-arm64",
10
10
  "win32-x64": "@lix-js/sdk-win32-x64",
11
11
  };
12
- function nativePackageName() {
13
- const key = `${process.platform}-${process.arch}`;
14
- return nativePackages[key];
15
- }
16
12
  function resolveNativePath() {
17
- if (existsSync(localNativePath)) {
13
+ if (existsSync(localNativePath))
18
14
  return localNativePath;
19
- }
20
- const packageName = nativePackageName();
15
+ const key = `${process.platform}-${process.arch}`;
16
+ const packageName = nativePackages[key];
21
17
  let packageResolutionError;
22
18
  if (packageName) {
23
19
  try {
@@ -32,10 +28,9 @@ function resolveNativePath() {
32
28
  }
33
29
  throw packageResolutionError;
34
30
  }
35
- const native = { exports: {} };
31
+ let addon;
36
32
  try {
37
- const nativePath = resolveNativePath();
38
- process.dlopen(native, nativePath);
33
+ addon = require(resolveNativePath());
39
34
  }
40
35
  catch (cause) {
41
36
  const error = new Error(`Failed to load @lix-js/sdk native addon for ${process.platform}-${process.arch}. ` +
@@ -44,4 +39,13 @@ catch (cause) {
44
39
  error.cause = cause;
45
40
  throw error;
46
41
  }
47
- export const addon = native.exports;
42
+ export function openLixBinding(backend, dispatch) {
43
+ switch (backend.kind) {
44
+ case "memory":
45
+ return addon.Lix.openMemory(dispatch);
46
+ case "sqlite":
47
+ return addon.Lix.openSqlite(backend.path, dispatch);
48
+ case "fs":
49
+ return addon.Lix.openFs(backend.path, backend.lixDir, backend.syncAllFiles, dispatch);
50
+ }
51
+ }
@@ -1,4 +1,3 @@
1
- import { readFile } from "node:fs/promises";
2
1
  const BUNDLED_PLUGIN_MANIFEST = [
3
2
  {
4
3
  key: "plugin_md_v2",
@@ -23,11 +22,20 @@ async function readBundledArchive(fileName) {
23
22
  ];
24
23
  for (const url of urls) {
25
24
  try {
25
+ if (url.protocol !== "file:") {
26
+ const response = await fetch(url);
27
+ if (response.ok) {
28
+ return new Uint8Array(await response.arrayBuffer());
29
+ }
30
+ continue;
31
+ }
32
+ const moduleName = "node:fs/promises";
33
+ const { readFile } = await import(/* @vite-ignore */ moduleName);
26
34
  return new Uint8Array(await readFile(url));
27
35
  }
28
36
  catch {
29
37
  // Try the next build/source layout.
30
38
  }
31
39
  }
32
- return new Uint8Array(await readFile(urls[0]));
40
+ throw new Error(`Could not load bundled plugin archive ${fileName}`);
33
41
  }
package/dist/errors.d.ts CHANGED
@@ -5,3 +5,5 @@ 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 fsBackendNotOpen(operation: string): LixJsError;
9
+ export declare function fsBackendAlreadyOpen(): LixJsError;
package/dist/errors.js CHANGED
@@ -17,3 +17,16 @@ export function invalidParam(index, message, actual) {
17
17
  };
18
18
  return error;
19
19
  }
20
+ export function fsBackendNotOpen(operation) {
21
+ const error = new Error(`FsBackend.${operation}() requires the backend to be opened with openLix() first`);
22
+ error.name = "LixError";
23
+ error.code = "LIX_FS_BACKEND_NOT_OPEN";
24
+ error.details = { operation };
25
+ return error;
26
+ }
27
+ export function fsBackendAlreadyOpen() {
28
+ const error = new Error("openLix() FsBackend is already open; close the existing Lix or create a new FsBackend");
29
+ error.name = "LixError";
30
+ error.code = "LIX_FS_BACKEND_IN_USE";
31
+ return error;
32
+ }
package/dist/index.d.ts CHANGED
@@ -2,4 +2,4 @@ export { FsBackend, Lix, LixTransaction, ObserveEvents, openLix, SqliteBackend,
2
2
  export { bundledPluginArchives, type BundledPluginArchive, } from "./bundled-plugins.js";
3
3
  export { Row } from "./result.js";
4
4
  export { Value } from "./value.js";
5
- export type { CreateBranchOptions, CreateBranchReceipt, ExecuteResult, FsBackendOptions, JsonValue, LixValue, MergeBranchOptions, MergeBranchOutcome, MergeBranchPreview, MergeBranchReceipt, MergeChangeStats, MergeConflict, MergeConflictSide, ObserveEvent, OpenLixOptions, SqlParam, SqliteBackendOptions, SwitchBranchOptions, SwitchBranchReceipt, } from "./types.js";
5
+ export type { CreateBranchOptions, CreateBranchReceipt, ExecuteOptions, ExecuteResult, FsBackendOptions, JsonValue, LixValue, MergeBranchOptions, MergeBranchOutcome, MergeBranchPreview, MergeBranchReceipt, MergeChangeStats, MergeConflict, MergeConflictSide, ObserveEvent, OpenLixOptions, SqlParam, SqliteBackendOptions, SwitchBranchOptions, SwitchBranchReceipt, } from "./types.js";