@lix-js/sdk 0.11.0 → 0.12.1
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 +48 -76
- package/dist/binding-types.d.ts +6 -15
- package/dist/binding.browser.js +18 -5
- package/dist/binding.node.js +5 -6
- package/dist/bundled-plugins/plugin_csv.lixplugin +0 -0
- package/dist/bundled-plugins/plugin_markdown.lixplugin +0 -0
- package/dist/errors.d.ts +0 -2
- package/dist/errors.js +0 -13
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/indexeddb-backend.d.ts +19 -0
- package/dist/indexeddb-backend.js +121 -0
- package/dist/lix.d.ts +1 -4
- package/dist/lix.js +21 -52
- package/dist/open-lix.d.ts +4 -9
- package/dist/open-lix.js +45 -135
- package/dist/remote/client.d.ts +2 -3
- package/dist/remote/client.js +13 -21
- package/dist/remote/{protocol.d.ts → server-protocol.d.ts} +35 -33
- package/dist/remote/{protocol.js → server-protocol.js} +24 -17
- package/dist/storage-adapter.d.ts +30 -0
- package/dist/storage-adapter.js +12 -0
- package/dist/types.d.ts +14 -24
- package/dist/value.d.ts +2 -1
- package/dist/value.js +23 -10
- package/dist/wasm/lix_js_sdk.d.ts +5 -10
- package/dist/wasm/lix_js_sdk.js +33 -45
- package/dist/wasm/lix_js_sdk_bg.wasm +0 -0
- package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +3 -6
- package/dist/worker/client.d.ts +1 -12
- package/dist/worker/client.js +0 -161
- package/dist/worker/host.js +0 -23
- package/dist/worker/protocol.d.ts +1 -15
- package/package.json +8 -12
- package/dist/client-state.d.ts +0 -53
- package/dist/client-state.js +0 -315
- package/dist/local-storage-adapter.d.ts +0 -26
- package/dist/local-storage-adapter.js +0 -117
- package/dist/snapshot-persistence.d.ts +0 -7
- 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
|
|
25
|
+
## Remote repositories
|
|
26
26
|
|
|
27
|
-
Use the same Lix client as a thin client against a hosted
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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 {
|
|
65
|
+
import { openLix } from "@lix-js/sdk";
|
|
66
|
+
import { FilesystemStorage } from "@lix-js/storage-filesystem";
|
|
95
67
|
|
|
96
68
|
const lix = await openLix({
|
|
97
|
-
|
|
98
|
-
path: "./workspace",
|
|
99
|
-
syncAllFiles: true,
|
|
100
|
-
}),
|
|
69
|
+
storage: new FilesystemStorage({ path: "./repository" }),
|
|
101
70
|
});
|
|
102
71
|
|
|
103
72
|
await lix.execute(
|
|
104
|
-
|
|
105
|
-
|
|
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(
|
|
109
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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.
|
|
176
|
-
-
|
|
177
|
-
-
|
|
178
|
-
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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.
|
|
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
|
|
package/dist/binding-types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { CreateBranchOptions, CreateBranchReceipt, CreateCheckpointReceipt, UndoReceipt, RedoReceipt, ExecuteOptions, LixBatchOptions, MergeBranchOptions, MergeBranchPreview, MergeBranchReceipt, SwitchBranchOptions, SwitchBranchReceipt, LixTelemetrySpan
|
|
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
|
-
/**
|
|
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: "
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
syncAllFiles: boolean;
|
|
71
|
-
};
|
|
60
|
+
kind: "indexedDb";
|
|
61
|
+
name: string;
|
|
62
|
+
} | LixStorageAdapterConfig;
|
package/dist/binding.browser.js
CHANGED
|
@@ -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, {
|
|
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
|
-
|
|
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
|
}
|
package/dist/binding.node.js
CHANGED
|
@@ -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 "
|
|
51
|
+
case "indexedDb":
|
|
52
|
+
throw new Error("IndexedDbStorage is only available in browsers");
|
|
53
|
+
case "filesystem":
|
|
55
54
|
if (nativeTelemetry) {
|
|
56
|
-
return addon.Lix.
|
|
55
|
+
return addon.Lix.openFilesystemStorage(storage.path, storage.syncAllFiles, nativeTelemetry);
|
|
57
56
|
}
|
|
58
|
-
return addon.Lix.
|
|
57
|
+
return addon.Lix.openFilesystemStorage(storage.path, storage.syncAllFiles);
|
|
59
58
|
}
|
|
60
59
|
}
|
|
Binary file
|
|
Binary file
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
159
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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() {
|
package/dist/open-lix.d.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { Lix } from "./lix.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { IndexedDbStorageOptions, OpenLixOptions } from "./types.js";
|
|
3
3
|
export { Lix, LixTransaction, ObserveEvents } from "./lix.js";
|
|
4
|
-
export declare class
|
|
5
|
-
readonly
|
|
6
|
-
|
|
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>;
|