@lix-js/sdk 0.15.1 → 0.16.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 CHANGED
@@ -22,10 +22,51 @@ console.log(result.rows[0]?.message);
22
22
  await lix.close();
23
23
  ```
24
24
 
25
+ ## Hosted lifecycle
26
+
27
+ `openLix()` selects execution from the supplied locations:
28
+
29
+ | Options | Behavior |
30
+ | --- | --- |
31
+ | Neither | Fresh in-memory repository |
32
+ | `storage` | Local repository, initialized if empty |
33
+ | `server` | Execute against an existing hosted repository |
34
+ | `storage` and `server` | Local reads and writes with background synchronization |
35
+
36
+ Creation and deletion are explicit server operations:
37
+
38
+ ```ts
39
+ import { createLix, deleteLix, openLix } from "@lix-js/sdk";
40
+
41
+ const repository = await createLix({
42
+ server: { url: "https://example.com", headers: getAuthHeaders },
43
+ from: localLix, // Omit to create an empty repository.
44
+ });
45
+
46
+ const remote = await openLix({
47
+ server: { url: repository.url, headers: getAuthHeaders },
48
+ });
49
+ await remote.close();
50
+ await deleteLix({ server: { url: repository.url, headers: getAuthHeaders } });
51
+ ```
52
+
53
+ `from` captures a consistent repository snapshot, including history and untracked
54
+ rows. The source remains open. Creation returns `{ id, url }`, not a session.
55
+ Use the same optional `idempotencyKey` when retrying creation after an uncertain
56
+ response. Deletion removes the hosted repository and leaves local copies intact. Opening
57
+ or synchronizing never implicitly creates a missing hosted repository.
58
+
59
+ Browser creation from a local repository requires Fetch request streaming.
60
+ Browsers without that support return `LIX_UNSUPPORTED_OPERATION`; Lix does not
61
+ buffer the complete repository as a fallback. Browser Fetch may also require an
62
+ HTTP/2 or HTTP/3 connection for streaming uploads. Empty creation and remote
63
+ execution do not require streaming uploads.
64
+
25
65
  ## Synchronized local repositories
26
66
 
27
- Use sync mode when reads and writes should execute locally while Lix exchanges
28
- commits with a hosted repository in the background:
67
+ Combine storage with a server to keep a synchronized local replica. Reads and
68
+ writes execute locally; background synchronization exchanges changes with the
69
+ server:
29
70
 
30
71
  ```ts
31
72
  import { openLix } from "@lix-js/sdk";
@@ -34,7 +75,6 @@ import { OpfsStorage } from "@lix-js/storage-opfs";
34
75
  const lix = await openLix({
35
76
  storage: new OpfsStorage({ name: "acme" }),
36
77
  server: {
37
- mode: "sync",
38
78
  url: "https://example.com/lix/01936f4e-7b6c-7c3d-8f9a-123456789abc",
39
79
  headers: async () => ({
40
80
  Authorization: `Bearer ${await accessToken()}`,
@@ -43,11 +83,59 @@ const lix = await openLix({
43
83
  });
44
84
  ```
45
85
 
46
- `await lix.execute(...)` means that the local transaction committed. It does
47
- not mean that the server has received the commit. Current data and new commits
48
- synchronize automatically; older history and binary content load when needed.
86
+ A successful mutation confirms a local commit; it does not confirm server
87
+ acceptance. Pending commits upload in the background. Cached reads and local
88
+ writes work offline; older history and binary content load when needed.
49
89
  See [Collaboration and Sync](https://lix.dev/docs/collaboration-and-sync).
50
90
 
91
+ ### Upgrading a local replica
92
+
93
+ Keep the same storage name across SDK upgrades. For a supported older synchronized
94
+ replica, Lix preserves its existing storage generation and bootstraps a separate
95
+ generation from the same authoritative repository and account. The replacement
96
+ becomes active only after bootstrap and validation succeed. Pending work and
97
+ local-only rows remain in the preserved generation; they do not block opening the
98
+ current server state. An upgrade requires a server connection and enough local
99
+ storage for both generations.
100
+
101
+ Recovery is explicit and separate from opening:
102
+
103
+ ```ts
104
+ const sources = await lix.replicaRecoverySources();
105
+ for (const source of sources.filter((source) => source.recoveryRequired)) {
106
+ const exported = await lix.exportReplicaRecovery(source.id);
107
+ // Save exported as JSON when the user requests a portable recovery copy.
108
+ console.log(exported.unresolved);
109
+
110
+ const receipt = await lix.recoverReplica(source.id);
111
+ // Present these separate branches for review; the active branch is unchanged.
112
+ console.log(receipt.branchIds, receipt.restoredRows, receipt.unresolved);
113
+ }
114
+ ```
115
+
116
+ `exportReplicaRecovery()` captures available logical rows, blob contents, and
117
+ original branch/checkpoint coordinates. `recoverReplica()` restores supported
118
+ tracked rows into separate recovery branches. It does not automatically publish
119
+ local-only rows or merge recovered work into the active branch. Inspect
120
+ `unresolved`: original history and any unavailable content remain in the retained
121
+ source. A recovery receipt describes local restoration, not a server durability
122
+ acknowledgement. Neither operation deletes the retained generation, and retries
123
+ reuse recovery branch receipts rather than overwriting previously recovered work.
124
+
125
+ Recovery export currently allows up to 100,000 logical rows across all branches,
126
+ 64 MiB per blob, and 128 MiB of blob content in total. Unfinished upload parts
127
+ have a separate 128 MiB content budget. A row-limit or upload-limit error leaves
128
+ the source intact; omitted blob content is identified in `unresolved`. Exported
129
+ JSON can be larger than these content budgets because binary data uses base64.
130
+ This recovery file is not a complete repository backup.
131
+
132
+ These methods require a local storage-backed handle; remote-only handles reject
133
+ with `LIX_ERROR_LOCAL_STORAGE_REQUIRED`. Do not clear browser storage to resolve
134
+ upgrade or recovery errors. Browser storage eviction or an unsupported old format
135
+ can still require external recovery; retaining bytes alone is not proof that all
136
+ work has been recovered. Standalone and authoritative repositories continue to
137
+ use history-preserving format migrations.
138
+
51
139
  ## Remote repositories
52
140
 
53
141
  Use the same Lix client as a thin client against a hosted repository:
@@ -55,7 +143,6 @@ Use the same Lix client as a thin client against a hosted repository:
55
143
  ```ts
56
144
  const lix = await openLix({
57
145
  server: {
58
- mode: "remote",
59
146
  url: "https://example.com/lix/01936f4e-7b6c-7c3d-8f9a-123456789abc",
60
147
  headers: async () => ({
61
148
  Authorization: `Bearer ${await accessToken()}`,
@@ -161,6 +248,11 @@ const merge = await lix.mergeBranch({ sourceBranchId: draft.id });
161
248
 
162
249
  ## Transactions
163
250
 
251
+ `beginTransaction()` captures the current branch and account in an independent
252
+ transaction context. Use `tx.execute()` for transaction work; its reads see staged
253
+ writes. Ordinary reads and observers on `lix` continue to see committed data while
254
+ the transaction is open. Commit or roll back before closing `lix`.
255
+
164
256
  ```ts
165
257
  const tx = await lix.beginTransaction();
166
258
 
@@ -1,4 +1,4 @@
1
- import type { CreateBranchOptions, CreateBranchReceipt, UndoReceipt, RedoReceipt, ExecuteOptions, LixBatchOptions, MergeBranchOptions, MergeBranchPreview, MergeBranchReceipt, SwitchBranchOptions, SwitchBranchReceipt, LixTelemetrySpan, LixTelemetryParentContext, LixOpenProgress, LixOpenReport, OpenAnotherSessionOptions, ResultColumn } from "./types.js";
1
+ import type { CommitSpan, CreateBranchOptions, CreateBranchReceipt, UndoReceipt, RedoReceipt, ExecuteOptions, LixBatchOptions, MergeBranchOptions, MergeBranchPreview, MergeBranchReceipt, SwitchBranchOptions, SwitchBranchReceipt, LixTelemetrySpan, LixTelemetryParentContext, LixOpenProgress, LixOpenReport, ReplicaRecoverySource, ReplicaRecoveryExport, ReplicaRecoveryReceipt, OpenAnotherSessionOptions, ResultColumn } from "./types.js";
2
2
  import type { NativeLixValue } from "./value.js";
3
3
  import type { LixStorageProvider } from "./storage-adapter.js";
4
4
  export type SyncServerBindingOptions = {
@@ -18,6 +18,7 @@ export type BindingExecuteResult = {
18
18
  message: string;
19
19
  hint?: string;
20
20
  }>;
21
+ commit?: CommitSpan;
21
22
  };
22
23
  export type BindingObserveEvent = {
23
24
  sequence: number;
@@ -41,6 +42,7 @@ export type BindingBatchStatement = {
41
42
  label?: string;
42
43
  };
43
44
  export type LixBinding = {
45
+ createHosted?(server: HostedServerBindingOptions): Promise<import("./types.js").HostedLix>;
44
46
  openReport?(): LixOpenReport | undefined;
45
47
  setTelemetryParent(parent?: TelemetryParentContext): void;
46
48
  openAnotherSession(options: OpenAnotherSessionOptions): Promise<LixBinding>;
@@ -48,6 +50,9 @@ export type LixBinding = {
48
50
  executeBatch(statements: BindingBatchStatement[], options?: LixBatchOptions): Promise<BindingExecuteResult[]>;
49
51
  observe(sql: string, params: BindingParam[]): Promise<ObserveEventsBinding>;
50
52
  beginTransaction(): Promise<LixTransactionBinding>;
53
+ replicaRecoverySources(): Promise<ReplicaRecoverySource[]>;
54
+ exportReplicaRecovery(id: string): Promise<ReplicaRecoveryExport>;
55
+ recoverReplica(id: string): Promise<ReplicaRecoveryReceipt>;
51
56
  activeBranchId(): Promise<string>;
52
57
  activeAccountId(): Promise<string>;
53
58
  createBranch(options: CreateBranchOptions): Promise<CreateBranchReceipt>;
@@ -88,3 +93,8 @@ export type LixStorageConfig = {
88
93
  path: string;
89
94
  syncAllFiles: boolean;
90
95
  };
96
+ export type HostedServerBindingOptions = {
97
+ idempotencyKey?: string;
98
+ url: string;
99
+ headers: [string, string][];
100
+ };
@@ -1,2 +1,4 @@
1
1
  import type { LixStorageConfig, LixBinding, SyncServerBindingOptions, TelemetryDispatch, TelemetryParentContext, OpenProgressDispatch } from "./binding-types.js";
2
2
  export declare function openLixBinding(storage: LixStorageConfig, telemetry?: TelemetryDispatch, telemetryParent?: TelemetryParentContext, server?: SyncServerBindingOptions, openProgress?: OpenProgressDispatch, snapshot?: ReadableStream<Uint8Array>): Promise<LixBinding>;
3
+ export declare function createHostedBinding(server: import("./binding-types.js").HostedServerBindingOptions): Promise<import("./types.js").HostedLix>;
4
+ export declare function deleteHostedBinding(server: import("./binding-types.js").HostedServerBindingOptions): Promise<void>;
@@ -2,7 +2,9 @@ import { initializeWasm } from "./wasm-init.js";
2
2
  import { restoreSnapshot } from "./snapshot-restore.js";
3
3
  // Generated before TypeScript compilation and emitted beside this module.
4
4
  // @ts-ignore Generated by build:wasm and absent in source-only checks.
5
- import { openJsStorage, openJsStorageFromSnapshot, openMemory, openMemoryFromSnapshot, } from "./wasm/lix_js_sdk.js";
5
+ import { createHosted, deleteHosted, openJsStorage, openJsStorageFromSnapshot, openMemory, openMemoryFromSnapshot,
6
+ // @ts-ignore Generated by build:wasm and absent in source-only checks.
7
+ } from "./wasm/lix_js_sdk.js";
6
8
  export async function openLixBinding(storage, telemetry, telemetryParent, server, openProgress, snapshot) {
7
9
  await initializeWasm();
8
10
  switch (storage.kind) {
@@ -32,3 +34,11 @@ export async function openLixBinding(storage, telemetry, telemetryParent, server
32
34
  throw new Error("FilesystemStorage is only available in Node.js");
33
35
  }
34
36
  }
37
+ export async function createHostedBinding(server) {
38
+ await initializeWasm();
39
+ return createHosted(server);
40
+ }
41
+ export async function deleteHostedBinding(server) {
42
+ await initializeWasm();
43
+ await deleteHosted(server);
44
+ }
@@ -1,3 +1,5 @@
1
1
  import type { LixStorageConfig, LixBinding, SyncServerBindingOptions, TelemetryDispatch, TelemetryParentContext, OpenProgressDispatch } from "./binding-types.js";
2
2
  export declare function openLixBinding(storage: LixStorageConfig, telemetry?: TelemetryDispatch, telemetryParent?: TelemetryParentContext, server?: SyncServerBindingOptions, openProgress?: OpenProgressDispatch, snapshot?: ReadableStream<Uint8Array>): Promise<LixBinding>;
3
3
  export declare function openNativeLixBinding(storage: LixStorageConfig, telemetry?: TelemetryDispatch, telemetryParent?: TelemetryParentContext, server?: SyncServerBindingOptions, openProgress?: OpenProgressDispatch, snapshot?: ReadableStream<Uint8Array>): Promise<LixBinding>;
4
+ export declare function createHostedBinding(server: import("./binding-types.js").HostedServerBindingOptions): Promise<import("./types.js").HostedLix>;
5
+ export declare function deleteHostedBinding(server: import("./binding-types.js").HostedServerBindingOptions): Promise<void>;
@@ -62,7 +62,7 @@ class NativeAddonUnavailableError extends Error {
62
62
  this.name = "NativeAddonUnavailableError";
63
63
  }
64
64
  }
65
- function loadNativeAddon() {
65
+ function loadAddon() {
66
66
  if (addon)
67
67
  return addon;
68
68
  if (addonLoadError)
@@ -114,7 +114,7 @@ export async function openNativeLixBinding(storage, telemetry, telemetryParent,
114
114
  : undefined;
115
115
  switch (storage.kind) {
116
116
  case "memory": {
117
- const nativeAddon = loadNativeAddon();
117
+ const nativeAddon = loadAddon();
118
118
  const nativeTelemetry = telemetry
119
119
  ? (spanJson) => telemetry(JSON.parse(spanJson))
120
120
  : undefined;
@@ -130,7 +130,7 @@ export async function openNativeLixBinding(storage, telemetry, telemetryParent,
130
130
  case "jsStorage":
131
131
  throw new Error("JavaScript storage providers are only available in browsers");
132
132
  case "filesystem": {
133
- const nativeAddon = loadNativeAddon();
133
+ const nativeAddon = loadAddon();
134
134
  const nativeTelemetry = telemetry
135
135
  ? (spanJson) => telemetry(JSON.parse(spanJson))
136
136
  : undefined;
@@ -145,3 +145,9 @@ export async function openNativeLixBinding(storage, telemetry, telemetryParent,
145
145
  }
146
146
  }
147
147
  }
148
+ export async function createHostedBinding(server) {
149
+ return loadAddon().createHosted(server.url, server.headers, server.idempotencyKey);
150
+ }
151
+ export async function deleteHostedBinding(server) {
152
+ await loadAddon().deleteHosted(server.url, server.headers);
153
+ }
@@ -0,0 +1,5 @@
1
+ import type { CreateLixOptions, DeleteLixOptions, HostedLix } from "./types.js";
2
+ /** Creates a hosted repository, optionally from a consistent snapshot of a local Lix. */
3
+ export declare function createLix(options: CreateLixOptions): Promise<HostedLix>;
4
+ /** Deletes the hosted repository. Local replicas are not deleted. */
5
+ export declare function deleteLix(options: DeleteLixOptions): Promise<void>;
@@ -0,0 +1,36 @@
1
+ import { createHostedFromLix } from "./lix.js";
2
+ import { hostedLixWorkerOperation } from "./worker/client.js";
3
+ async function resolveServer(server) {
4
+ if (!server || typeof server !== "object")
5
+ throw new TypeError("A server is required");
6
+ if ("fetch" in server && server.fetch !== undefined)
7
+ throw new TypeError("hosted lifecycle does not accept a custom fetch");
8
+ if ("mode" in server)
9
+ throw new TypeError("server.mode was removed");
10
+ const url = new URL(server.url).toString();
11
+ const headers = new Headers(typeof server.headers === "function"
12
+ ? await server.headers()
13
+ : server.headers);
14
+ const entries = [];
15
+ headers.forEach((value, key) => entries.push([key, value]));
16
+ return { url, headers: entries };
17
+ }
18
+ /** Creates a hosted repository, optionally from a consistent snapshot of a local Lix. */
19
+ export async function createLix(options) {
20
+ if (options.from !== undefined) {
21
+ return createHostedFromLix(options.from, async () => ({
22
+ ...(await resolveServer(options.server)),
23
+ idempotencyKey: options.idempotencyKey,
24
+ }));
25
+ }
26
+ const server = await resolveServer(options.server);
27
+ return hostedLixWorkerOperation({
28
+ kind: "hosted.create",
29
+ server: { ...server, idempotencyKey: options.idempotencyKey },
30
+ });
31
+ }
32
+ /** Deletes the hosted repository. Local replicas are not deleted. */
33
+ export async function deleteLix(options) {
34
+ const server = await resolveServer(options.server);
35
+ await hostedLixWorkerOperation({ kind: "hosted.delete", server });
36
+ }
package/dist/index.d.ts CHANGED
@@ -3,4 +3,5 @@ export type { LixStorage, LixStorageBound, LixStorageChangeWatch, LixStorageComm
3
3
  export { LixStorageError } from "./storage-adapter.js";
4
4
  export { bundledPluginArchives, type BundledPluginArchive, } from "./bundled-plugins.js";
5
5
  export { Value } from "./value.js";
6
- export type { CreateBranchOptions, CreateBranchReceipt, RedoReceipt, ExecuteOptions, ExecuteResult, ExecuteBatchResult, LixBatchOptions, LixBatchStatement, JsonValue, LixValue, ResultArrayRow, ResultColumn, ResultColumnType, ResultObjectRow, ResultRow, MergeBranchOptions, MergeBranchOutcome, MergeBranchPreview, MergeBranchReceipt, MergeChangeStats, MergeConflict, MergeConflictSide, ObserveEvent, OpenLixOptions, OpenAnotherSessionOptions, LixTelemetryOptions, LixTelemetryParentContext, LixTelemetrySpan, LixTelemetrySpanLink, LixOpenMigrationReport, LixOpenPhase, LixOpenProgress, LixOpenProgressOptions, LixOpenReport, RemoteLixFetch, RemoteLixServerOptions, SyncLixServerOptions, UndoReceipt, SqlParam, SwitchBranchOptions, SwitchBranchReceipt, } from "./types.js";
6
+ export type { CreateBranchOptions, CreateBranchReceipt, RedoReceipt, CommitSpan, ExecuteOptions, ExecuteResult, ExecuteBatchResult, LixBatchOptions, LixBatchStatement, JsonValue, LixValue, ResultArrayRow, ResultColumn, ResultColumnType, ResultObjectRow, ResultRow, MergeBranchOptions, MergeBranchOutcome, MergeBranchPreview, MergeBranchReceipt, MergeChangeStats, MergeConflict, MergeConflictSide, ObserveEvent, OpenLixOptions, OpenAnotherSessionOptions, LixTelemetryOptions, LixTelemetryParentContext, LixTelemetrySpan, LixTelemetrySpanLink, LixOpenMigrationReport, LixOpenPhase, LixOpenProgress, LixOpenProgressOptions, LixOpenReport, ReplicaRecoverySource, ReplicaRecoveryRow, ReplicaRecoveryBranch, ReplicaRecoveryBlob, ReplicaRecoveryFile, ReplicaRecoveryExport, ReplicaRecoveryReceipt, RemoteLixFetch, LixServerOptions, HostedLix, CreateLixOptions, DeleteLixOptions, UndoReceipt, SqlParam, SwitchBranchOptions, SwitchBranchReceipt, } from "./types.js";
7
+ export { createLix, deleteLix } from "./hosted-lix.js";
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export { Lix, LixTransaction, ObserveEvents, openLix } from "./open-lix.js";
2
2
  export { LixStorageError } from "./storage-adapter.js";
3
3
  export { bundledPluginArchives, } from "./bundled-plugins.js";
4
4
  export { Value } from "./value.js";
5
+ export { createLix, deleteLix } from "./hosted-lix.js";
package/dist/lix.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { LixBinding, LixTransactionBinding, ObserveEventsBinding } from "./binding-types.js";
2
- import type { CreateBranchOptions, CreateBranchReceipt, RedoReceipt, ExecuteOptions, ExecuteResult, ExecuteBatchResult, LixBatchOptions, LixBatchStatement, MergeBranchOptions, MergeBranchPreview, MergeBranchReceipt, ObserveEvent, OpenAnotherSessionOptions, LixOpenReport, SqlParam, ResultArrayRow, ResultObjectRow, ResultRow, SwitchBranchOptions, SwitchBranchReceipt, UndoReceipt } from "./types.js";
2
+ import type { CreateBranchOptions, CreateBranchReceipt, RedoReceipt, ExecuteOptions, ExecuteResult, ExecuteBatchResult, LixBatchOptions, LixBatchStatement, MergeBranchOptions, MergeBranchPreview, MergeBranchReceipt, ObserveEvent, OpenAnotherSessionOptions, LixOpenReport, ReplicaRecoverySource, ReplicaRecoveryExport, ReplicaRecoveryReceipt, SqlParam, ResultArrayRow, ResultObjectRow, ResultRow, SwitchBranchOptions, SwitchBranchReceipt, UndoReceipt } from "./types.js";
3
+ /** @internal Used by createLix without adding another method to Lix. */
4
+ export declare function createHostedFromLix(lix: Lix, server: () => Promise<import("./binding-types.js").HostedServerBindingOptions>): Promise<import("./types.js").HostedLix>;
3
5
  export declare class Lix {
4
6
  #private;
5
7
  private readonly binding;
@@ -24,6 +26,12 @@ export declare class Lix {
24
26
  executeBatch(statements: readonly LixBatchStatement[], options?: LixBatchOptions): Promise<readonly ExecuteBatchResult<ResultRow>[]>;
25
27
  observe(sql: string, params?: SqlParam[]): ObserveEvents;
26
28
  beginTransaction(): Promise<LixTransaction>;
29
+ /** Lists preserved generations belonging to this local repository. */
30
+ replicaRecoverySources(): Promise<ReplicaRecoverySource[]>;
31
+ /** Exports retained work without deleting or changing its source. */
32
+ exportReplicaRecovery(id: string): Promise<ReplicaRecoveryExport>;
33
+ /** Restores supported rows onto separate recovery branches; preserves the source. */
34
+ recoverReplica(id: string): Promise<ReplicaRecoveryReceipt>;
27
35
  activeBranchId(): Promise<string>;
28
36
  activeAccountId(): Promise<string>;
29
37
  /** Subscribes to successful branch switches made through this Lix handle. */
@@ -50,7 +58,6 @@ export declare class ObserveEvents {
50
58
  export declare class LixTransaction {
51
59
  private readonly binding;
52
60
  private readonly onFinish;
53
- private finishPromise;
54
61
  private finished;
55
62
  constructor(binding: LixTransactionBinding, onFinish?: () => void);
56
63
  execute(sql: string, params: SqlParam[] | undefined, options: ExecuteOptions & {
package/dist/lix.js CHANGED
@@ -13,6 +13,14 @@ const observeFinalizer = new FinalizationRegistry(({ observe, onClose }) => {
13
13
  events?.close();
14
14
  });
15
15
  });
16
+ const hostedCreators = new WeakMap();
17
+ /** @internal Used by createLix without adding another method to Lix. */
18
+ export function createHostedFromLix(lix, server) {
19
+ const create = hostedCreators.get(lix);
20
+ if (!create)
21
+ throw new TypeError("createLix() from must be an open local Lix");
22
+ return create(server);
23
+ }
16
24
  export class Lix {
17
25
  binding;
18
26
  openReport;
@@ -27,6 +35,11 @@ export class Lix {
27
35
  #acceptingOperations = true;
28
36
  constructor(binding) {
29
37
  this.binding = binding;
38
+ hostedCreators.set(this, (server) => this.#runOperation(async () => {
39
+ if (!binding.createHosted)
40
+ throw new TypeError("createLix() from requires a local Lix");
41
+ return binding.createHosted(await server());
42
+ }));
30
43
  const report = binding.openReport?.();
31
44
  this.openReport = report
32
45
  ? Object.freeze({
@@ -82,6 +95,18 @@ export class Lix {
82
95
  }
83
96
  });
84
97
  }
98
+ /** Lists preserved generations belonging to this local repository. */
99
+ async replicaRecoverySources() {
100
+ return this.#runOperation(() => this.binding.replicaRecoverySources());
101
+ }
102
+ /** Exports retained work without deleting or changing its source. */
103
+ async exportReplicaRecovery(id) {
104
+ return this.#runOperation(() => this.binding.exportReplicaRecovery(id));
105
+ }
106
+ /** Restores supported rows onto separate recovery branches; preserves the source. */
107
+ async recoverReplica(id) {
108
+ return this.#runOperation(() => this.binding.recoverReplica(id));
109
+ }
85
110
  async activeBranchId() {
86
111
  return this.#runOperation(() => this.binding.activeBranchId());
87
112
  }
@@ -317,7 +342,6 @@ export class ObserveEvents {
317
342
  export class LixTransaction {
318
343
  binding;
319
344
  onFinish;
320
- finishPromise;
321
345
  finished = false;
322
346
  constructor(binding, onFinish = () => undefined) {
323
347
  this.binding = binding;
@@ -325,6 +349,8 @@ export class LixTransaction {
325
349
  transactionFinalizer.register(this, { transaction: binding, onFinish: this.onFinish }, this);
326
350
  }
327
351
  async execute(sql, params = [], options) {
352
+ if (this.finished)
353
+ throw transactionClosedError();
328
354
  assertExecuteArgs("lixTransaction", sql, params, options);
329
355
  const { rowMode = "object", ...bindingOptions } = options ?? {};
330
356
  return wrapExecuteResult(await this.binding.execute(sql, params.map((param, index) => toNativeValue(normalizeParam(param, index))), bindingOptions), rowMode);
@@ -338,24 +364,21 @@ export class LixTransaction {
338
364
  async finish(kind) {
339
365
  if (this.finished)
340
366
  throw transactionClosedError();
341
- if (!this.finishPromise) {
342
- this.finishPromise = (async () => {
343
- try {
344
- if (kind === "transaction.commit")
345
- await this.binding.commit();
346
- else
347
- await this.binding.rollback();
348
- }
349
- finally {
350
- // A terminal binding call consumes the underlying transaction even
351
- // when its durable commit or rollback reports an error.
352
- this.finished = true;
353
- transactionFinalizer.unregister(this);
354
- this.onFinish();
355
- }
356
- })();
367
+ // The first terminal call owns the handle immediately. In particular,
368
+ // a concurrent rollback must never report a pending commit's success.
369
+ this.finished = true;
370
+ try {
371
+ if (kind === "transaction.commit")
372
+ await this.binding.commit();
373
+ else
374
+ await this.binding.rollback();
375
+ }
376
+ finally {
377
+ // Keep the parent transaction lease until the binding settles. A
378
+ // terminal call consumes the handle even when it reports an error.
379
+ transactionFinalizer.unregister(this);
380
+ this.onFinish();
357
381
  }
358
- await this.finishPromise;
359
382
  }
360
383
  }
361
384
  function transactionClosedError() {
package/dist/open-lix.js CHANGED
@@ -26,37 +26,34 @@ async function openLixInternal(options, snapshot) {
26
26
  throw new TypeError("openLix() onProgress must be a function");
27
27
  }
28
28
  if (options.server !== undefined) {
29
+ if ("mode" in options.server)
30
+ throw new TypeError("server.mode was removed; provide storage for synchronization or omit it for remote execution");
29
31
  if (snapshot) {
30
32
  throw new TypeError("openLix.fromSnapshot() does not accept server mode");
31
33
  }
32
- if (options.server.mode === "remote") {
34
+ if (options.storage === undefined) {
35
+ if (options.telemetry !== undefined || options.onProgress !== undefined)
36
+ throw new TypeError("remote execution does not accept local telemetry or onProgress");
33
37
  const { openRemoteLixBinding } = await import("./remote/client.js");
34
- if ("storage" in options && options.storage !== undefined) {
35
- throw new TypeError("openLix() remote mode does not accept storage");
36
- }
37
38
  return new Lix(await openRemoteLixBinding(options.server));
38
39
  }
39
40
  }
40
- const syncServer = options.server?.mode === "sync"
41
+ const syncServer = options.server !== undefined && options.storage !== undefined
41
42
  ? {
42
43
  url: new URL(options.server.url).toString(),
43
44
  headers: options.server.headers,
44
45
  fetch: options.server.fetch,
45
46
  }
46
47
  : undefined;
47
- if (syncServer?.fetch !== undefined && typeof syncServer.fetch !== "function") {
48
+ if (syncServer?.fetch !== undefined &&
49
+ typeof syncServer.fetch !== "function") {
48
50
  throw new TypeError("openLix() sync server fetch must be a function");
49
51
  }
50
- if (syncServer?.headers !== undefined && typeof syncServer.headers !== "function") {
52
+ if (syncServer?.headers !== undefined &&
53
+ typeof syncServer.headers !== "function") {
51
54
  // Validate static headers before opening a worker/native runtime.
52
55
  new Headers(syncServer.headers);
53
56
  }
54
- if (options.server !== undefined && syncServer === undefined) {
55
- throw new TypeError("openLix() server mode must be 'remote' or 'sync'");
56
- }
57
- if (syncServer !== undefined && options.storage === undefined) {
58
- throw new TypeError("openLix() sync mode requires a durability-capable storage adapter");
59
- }
60
57
  const { openLixWorkerBinding } = await import("./worker/client.js");
61
58
  if (options.storage === undefined) {
62
59
  const binding = await openLixWorkerBinding({ kind: "memory" }, undefined, options.telemetry, syncServer, options.onProgress, snapshot);
@@ -1,7 +1,7 @@
1
1
  import type { LixBinding } from "../binding-types.js";
2
- import type { RemoteLixServerOptions } from "../types.js";
2
+ import type { LixServerOptions } from "../types.js";
3
3
  type RemoteLixClientOptions = {
4
4
  initialActiveBranchId?: string;
5
5
  };
6
- export declare function openRemoteLixBinding(options: RemoteLixServerOptions, clientOptions?: RemoteLixClientOptions): Promise<LixBinding>;
6
+ export declare function openRemoteLixBinding(options: LixServerOptions, clientOptions?: RemoteLixClientOptions): Promise<LixBinding>;
7
7
  export {};
@@ -6,9 +6,6 @@ export async function openRemoteLixBinding(options, clientOptions = {}) {
6
6
  if (!options || typeof options !== "object") {
7
7
  throw new TypeError("openLix() remote server must be an object");
8
8
  }
9
- if (options.mode !== "remote") {
10
- throw new TypeError("openLix() remote server mode must be 'remote'");
11
- }
12
9
  const remoteFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
13
10
  if (typeof remoteFetch !== "function") {
14
11
  throw new TypeError("openLix() remote mode requires fetch");
@@ -98,10 +98,6 @@ export type LixStoragePrecondition = {
98
98
  kind: "rangeEmpty";
99
99
  space: LixStorageSpace;
100
100
  range: LixStorageKeyRange;
101
- } | {
102
- kind: "branchEquals";
103
- refKey: Uint8Array;
104
- expected: Uint8Array;
105
101
  };
106
102
  export type LixStorageGetManyRequest = {
107
103
  space: LixStorageSpace;