@lunora/client 1.0.0-alpha.8 → 1.0.0-alpha.9

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.
@@ -23,12 +23,14 @@ class OfflineQueue {
23
23
  maxItems;
24
24
  onPersistenceError;
25
25
  persistence;
26
+ onEvict;
26
27
  items = [];
27
- constructor(options = {}, persistence) {
28
+ constructor(options = {}, persistence, onEvict) {
28
29
  this.maxItems = options.maxItems ?? 1e3;
29
30
  this.queueBeforeFirstConnect = options.queueBeforeFirstConnect ?? false;
30
31
  this.onPersistenceError = options.onPersistenceError;
31
32
  this.persistence = persistence;
33
+ this.onEvict = onEvict;
32
34
  }
33
35
  get size() {
34
36
  return this.items.length;
@@ -51,6 +53,7 @@ class OfflineQueue {
51
53
  const error = new Error("offline queue overflow");
52
54
  error.code = "OFFLINE_QUEUE_OVERFLOW";
53
55
  dropped.reject(error);
56
+ this.onEvict?.(dropped, error);
54
57
  }
55
58
  }
56
59
  }
@@ -0,0 +1 @@
1
+ export { c as createLocalStore } from './local-store-BNgN3Dw3.mjs';
@@ -1,4 +1,4 @@
1
- import { LunoraClient } from './LunoraClient-Bsv8immC.mjs';
1
+ import { LunoraClient } from './LunoraClient-C4eFp92b.mjs';
2
2
 
3
3
  const createServerClient = (options) => {
4
4
  const client = new LunoraClient({ fetch: options.fetch, url: options.url });
@@ -0,0 +1,111 @@
1
+ const foldOptimistic = (base, layers) => {
2
+ let value = base;
3
+ for (const layer of layers) {
4
+ try {
5
+ value = layer.transform(value);
6
+ } catch {
7
+ }
8
+ }
9
+ return value;
10
+ };
11
+ const notifySubscription = (state, value) => {
12
+ if (value === state.lastValue) {
13
+ return;
14
+ }
15
+ state.lastValue = value;
16
+ for (const callback of state.callbacks) {
17
+ try {
18
+ callback(value);
19
+ } catch {
20
+ }
21
+ }
22
+ };
23
+ const applyOptimisticLayer = (state, optimistic) => {
24
+ let next;
25
+ try {
26
+ next = optimistic(state.lastValue);
27
+ } catch {
28
+ return void 0;
29
+ }
30
+ const layer = { id: /* @__PURE__ */ Symbol("optimistic"), transform: optimistic };
31
+ state.optimisticLayers.push(layer);
32
+ notifySubscription(state, next);
33
+ const remove = () => {
34
+ const index = state.optimisticLayers.findIndex((entry) => entry.id === layer.id);
35
+ if (index === -1) {
36
+ return false;
37
+ }
38
+ state.optimisticLayers.splice(index, 1);
39
+ return true;
40
+ };
41
+ const refold = () => {
42
+ notifySubscription(state, foldOptimistic(state.serverBase, state.optimisticLayers));
43
+ };
44
+ return {
45
+ confirm: (commitCursor) => {
46
+ if (commitCursor === void 0) {
47
+ remove();
48
+ return;
49
+ }
50
+ layer.commitCursor = commitCursor;
51
+ if (state.serverCursor !== void 0 && state.serverCursor >= commitCursor && remove()) {
52
+ refold();
53
+ }
54
+ },
55
+ rollback: () => {
56
+ if (remove()) {
57
+ refold();
58
+ }
59
+ }
60
+ };
61
+ };
62
+ const dropConfirmedLayers = (state, cursor) => {
63
+ if (cursor === void 0 || state.optimisticLayers.length === 0) {
64
+ return false;
65
+ }
66
+ const before = state.optimisticLayers.length;
67
+ state.optimisticLayers = state.optimisticLayers.filter((layer) => layer.commitCursor === void 0 || layer.commitCursor > cursor);
68
+ return state.optimisticLayers.length !== before;
69
+ };
70
+
71
+ const createLocalStore = (subscriptions, shardKey, stableStringify) => {
72
+ const confirms = [];
73
+ const rollbacks = [];
74
+ const findState = (functionRef, argsKey) => {
75
+ for (const state of subscriptions.all()) {
76
+ if (state.fn.__lunoraRef === functionRef && state.shardKey === shardKey && state.argsKey === argsKey) {
77
+ return state;
78
+ }
79
+ }
80
+ return void 0;
81
+ };
82
+ const store = {
83
+ getAllQueries: (function_) => {
84
+ const matches = [];
85
+ for (const state of subscriptions.all()) {
86
+ if (state.fn.__lunoraRef === function_.__lunoraRef && state.shardKey === shardKey) {
87
+ matches.push({ args: state.args, value: state.lastValue });
88
+ }
89
+ }
90
+ return matches;
91
+ },
92
+ getQuery: (function_, args) => {
93
+ const state = findState(function_.__lunoraRef, stableStringify(args ?? {}));
94
+ return state?.lastValue;
95
+ },
96
+ setQuery: (function_, args, value) => {
97
+ const state = findState(function_.__lunoraRef, stableStringify(args ?? {}));
98
+ if (!state) {
99
+ return;
100
+ }
101
+ const handle = applyOptimisticLayer(state, () => value);
102
+ if (handle) {
103
+ confirms.push(handle.confirm);
104
+ rollbacks.push(handle.rollback);
105
+ }
106
+ }
107
+ };
108
+ return { confirms, rollbacks, store };
109
+ };
110
+
111
+ export { applyOptimisticLayer as a, createLocalStore as c, dropConfirmedLayers as d, foldOptimistic as f, notifySubscription as n };
@@ -312,6 +312,8 @@ interface RpcEnvelope {
312
312
  * watermarked custom-mutator push additionally carries `lastMutationId` — the
313
313
  * highest per-client sequence the DO has applied — which the client uses to keep
314
314
  * its `clientSeq` generator monotonic across reloads (see `LunoraClient.callMutator`).
315
+ * A plain mutation on a CDC shard carries `commitCursor` — the cursor the write
316
+ * committed at — which gates the drop of a per-call optimistic layer.
315
317
  */
316
318
  type RpcResponseBody = {
317
319
  error: {
@@ -319,6 +321,7 @@ type RpcResponseBody = {
319
321
  message: string;
320
322
  };
321
323
  } | {
324
+ commitCursor?: number;
322
325
  lastMutationId?: number;
323
326
  result: unknown;
324
327
  };
@@ -820,6 +823,23 @@ interface SubscriptionError {
820
823
  message: string;
821
824
  }
822
825
  type SubscriptionErrorCallback = (error: SubscriptionError) => void;
826
+ /**
827
+ * One active per-call optimistic transform layered onto a subscription. The
828
+ * displayed value is the authoritative {@link SubscriptionState.serverBase}
829
+ * folded through every layer's `transform`, in order — so an incoming server
830
+ * frame re-folds the still-pending layers onto the new base (rebasing) instead
831
+ * of clobbering them. A layer is dropped — gaplessly — once a `data`/`delta`
832
+ * frame whose `cursor >= commitCursor` arrives (its write is now reflected in
833
+ * `serverBase`); `commitCursor` is the CDC cursor the server echoed on the
834
+ * mutation's response, and stays `undefined` while the write is still queued/
835
+ * in-flight (so the overlay survives unrelated deltas until confirmed).
836
+ */
837
+ interface OptimisticLayer {
838
+ /** The committed CDC cursor (from the mutation response); `undefined` until confirmed. */
839
+ commitCursor?: number;
840
+ readonly id: symbol;
841
+ readonly transform: (current: unknown) => unknown;
842
+ }
823
843
  interface SubscriptionState {
824
844
  /** True once the server has acked the subscription on the current socket. */
825
845
  acked: boolean;
@@ -862,6 +882,21 @@ interface SubscriptionState {
862
882
  /** Last known value, used to short-circuit `useQuery`-style consumers. */
863
883
  lastValue: unknown;
864
884
  /**
885
+ * Active per-call optimistic layers, in application order (see
886
+ * {@link OptimisticLayer}). Empty for subscriptions with no pending per-call
887
+ * optimistic write — the common case, where `lastValue` tracks `serverBase`
888
+ * exactly and behaviour is identical to a plain server-value assignment.
889
+ */
890
+ optimisticLayers: OptimisticLayer[];
891
+ /**
892
+ * The authoritative server value the optimistic layers fold onto — the value
893
+ * with NO optimistic overlay. Tracks `lastValue` exactly whenever no layers
894
+ * are active; diverges only while a per-call optimistic write is pending. A
895
+ * server frame updates this (and re-folds the layers); the durable read cache
896
+ * persists this, never the optimistic overlay.
897
+ */
898
+ serverBase: unknown;
899
+ /**
865
900
  * The `__cdc_log` high-watermark (`cursor`) the `lastValue` reflects,
866
901
  * captured from the last `data`/`delta`/`resume` frame. Persisted to the
867
902
  * durable read cache and replayed as `sinceSeq` on reconnect so the server
@@ -877,12 +912,6 @@ interface SubscriptionState {
877
912
  * until the first epoch-stamped frame arrives.
878
913
  */
879
914
  serverEpoch?: string;
880
- /**
881
- * Monotonic counter incremented on every server-pushed delta or data.
882
- * Used by optimistic-update rollback to detect whether the server has
883
- * already moved past the value we'd otherwise restore.
884
- */
885
- serverVersion: number;
886
915
  readonly shardKey?: string;
887
916
  }
888
917
  /**
@@ -909,10 +938,10 @@ declare class SubscriptionRegistry {
909
938
  * many subscribed queries at once (Convex's `OptimisticLocalStore` model).
910
939
  *
911
940
  * `getQuery` reads the current value (server value or any still-pending
912
- * optimistic override) of a subscribed query; `setQuery` writes an optimistic
913
- * override on top. Every write is collected as a rollback closure so the whole
914
- * batch unwinds atomically when the mutation settles or the server advances
915
- * past it — the same per-subscription rollback machinery the legacy
941
+ * optimistic override) of a subscribed query; `setQuery` registers a constant
942
+ * optimistic layer on top. The whole batch rebases onto incoming deltas and
943
+ * settles together confirmed on the mutation's commit cursor, or rolled back
944
+ * on failure — the same per-subscription layer machinery the single-query
916
945
  * per-call `optimistic` transform uses, generalized to N queries.
917
946
  */
918
947
  interface OptimisticLocalStore {
@@ -942,13 +971,17 @@ interface OptimisticLocalStore {
942
971
  /** A mutation's multi-query optimistic update: read/write the cache via `localStore`. */
943
972
  type OptimisticUpdate<Args> = (localStore: OptimisticLocalStore, args: Args) => void;
944
973
  /**
945
- * Build an {@link OptimisticLocalStore} bound to a subscription registry, the
946
- * mutation's shard key, and the `writeOptimisticToState` primitive. Returns the
947
- * store plus the ordered rollback closures every `setQuery` produced, so the
948
- * caller can unwind the whole batch (LIFO) if the mutation later fails — and
949
- * leave them in place to be GC'd alongside the subscription on success.
974
+ * Build an {@link OptimisticLocalStore} bound to a subscription registry and the
975
+ * mutation's shard key. Each `setQuery(value)` registers a constant-value layer
976
+ * on its target subscription (via `applyOptimisticLayer`): the predicted value
977
+ * survives incoming server deltas (re-clamped, masking concurrent changes to that
978
+ * query not merged) and drops gaplessly on the mutation's commit cursor, like
979
+ * the single-query per-call `optimistic` path. Returns the store plus the ordered
980
+ * `confirm` (success) and `rollback` (failure) closures every `setQuery` produced,
981
+ * so the caller settles the whole batch when the mutation does.
950
982
  */
951
- declare const createLocalStore: (subscriptions: SubscriptionRegistry, shardKey: string | undefined, write: (state: SubscriptionState, next: unknown) => () => void, stableStringify: (value: unknown) => string) => {
983
+ declare const createLocalStore: (subscriptions: SubscriptionRegistry, shardKey: string | undefined, stableStringify: (value: unknown) => string) => {
984
+ confirms: ((commitCursor: number | undefined) => void)[];
952
985
  rollbacks: (() => void)[];
953
986
  store: OptimisticLocalStore;
954
987
  };
@@ -1006,6 +1039,41 @@ declare const createStream: <T>(options: {
1006
1039
  */
1007
1040
  type ConnectionStatus = "connected" | "connecting" | "idle" | "offline";
1008
1041
  /**
1042
+ * Terminal verdict for a mutation that passed through the offline queue,
1043
+ * delivered to {@link LunoraClient.onMutationSettled}.
1044
+ *
1045
+ * Unlike the Promise returned by {@link LunoraClient.mutation} — which only the
1046
+ * original caller can await, and which no longer exists after a reload — this
1047
+ * fires for *every* queued write the server (or the queue) reaches a verdict on,
1048
+ * including writes restored from durable storage in a later session. It is the
1049
+ * channel a UI uses to tell the user "your queued change couldn't be saved"
1050
+ * instead of silently dropping a rolled-back optimistic row.
1051
+ *
1052
+ * `status: "rejected"` carries the failure `code` (e.g. `CONFLICT`,
1053
+ * `OFFLINE_QUEUE_OVERFLOW`, `OFFLINE_IDENTITY_CHANGED`) and the `error`.
1054
+ * `hadAwaiter` is `false` for a write whose original `mutation()` Promise is
1055
+ * gone (a hydrated/post-reload replay or an eviction), so a listener can tell
1056
+ * "the caller already saw this" apart from "nothing else will report this".
1057
+ */
1058
+ interface MutationSettledEvent {
1059
+ /** The write's args, so a listener can describe or re-offer the change. */
1060
+ readonly args: Record<string, unknown>;
1061
+ /** Server/queue error code on `rejected` (e.g. `CONFLICT`), when present. */
1062
+ readonly code?: string;
1063
+ /** The rejection error on `status: "rejected"`. */
1064
+ readonly error?: unknown;
1065
+ /** The `&lt;file>:&lt;function>` reference of the mutation. */
1066
+ readonly functionPath: string;
1067
+ /** Whether a live caller was still awaiting this write's `mutation()` Promise. */
1068
+ readonly hadAwaiter: boolean;
1069
+ /** The write's stable id (idempotency key / queue id). */
1070
+ readonly id: string;
1071
+ /** Shard the write targeted, if any. */
1072
+ readonly shardKey?: string;
1073
+ /** Terminal outcome. */
1074
+ readonly status: "committed" | "rejected";
1075
+ }
1076
+ /**
1009
1077
  * Per-call options for {@link LunoraClient.mutation} — the optimistic-update
1010
1078
  * machinery plus `shardKey`. Exported (at the end of this file) so the framework
1011
1079
  * adapters (`@lunora/react`, `/solid`, `/svelte`, `/vue`) can type their
@@ -1145,6 +1213,8 @@ declare class LunoraClient {
1145
1213
  private readonly statusListeners;
1146
1214
  /** Subscribers notified when the server drops a socket for an expired token (see `onTokenExpired`). */
1147
1215
  private readonly tokenExpiredListeners;
1216
+ /** Subscribers to offline-queued mutation verdicts (see `onMutationSettled`). */
1217
+ private readonly mutationSettledListeners;
1148
1218
  /**
1149
1219
  * Whisper-topic handlers, keyed by `connectionKey(shardKey)` → topic → set
1150
1220
  * of callbacks. Membership doubles as the resubscribe set replayed on every
@@ -1332,6 +1402,18 @@ declare class LunoraClient {
1332
1402
  * unsubscribe function.
1333
1403
  */
1334
1404
  onConnectionStatus(listener: (status: ConnectionStatus) => void): Unsubscribe;
1405
+ /**
1406
+ * Subscribe to terminal verdicts for offline-queued mutations. The listener
1407
+ * fires once per queued write that commits or is rejected — including a write
1408
+ * restored from durable storage after a reload, whose original `mutation()`
1409
+ * Promise no longer exists (`hadAwaiter: false`), and a write the queue
1410
+ * evicts on overflow or discards on an identity change. This is the durable
1411
+ * channel for surfacing a rolled-back optimistic write to the UI; an online
1412
+ * mutation that never queued still surfaces through the Promise `mutation()`
1413
+ * returns. The listener is NOT invoked on registration. Returns an
1414
+ * unsubscribe function. See {@link MutationSettledEvent}.
1415
+ */
1416
+ onMutationSettled(listener: (event: MutationSettledEvent) => void): Unsubscribe;
1335
1417
  query<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: {
1336
1418
  shardKey?: string;
1337
1419
  }): Promise<ReturnOf<F>>;
@@ -1841,6 +1923,13 @@ declare class LunoraClient {
1841
1923
  /** Recompute the aggregate status and notify listeners if it changed. */
1842
1924
  private emitConnectionStatus;
1843
1925
  /**
1926
+ * Build a {@link MutationSettledEvent} from a queued entry and emit it on the
1927
+ * {@link onMutationSettled} channel. `item.id` is always assigned by the time
1928
+ * a write settles (`enqueue`/`hydrate` guarantee it), so the `?? ""` fallback
1929
+ * is unreachable — present only to satisfy the optional queue-id type.
1930
+ */
1931
+ private emitItemSettled;
1932
+ /**
1844
1933
  * Apply an optimistic update to the subscription that matches the mutation's
1845
1934
  * `(functionRef, args, shardKey)` triple, returning the rollback callbacks to
1846
1935
  * invoke if the mutation later fails.
@@ -1857,12 +1946,14 @@ declare class LunoraClient {
1857
1946
  private applyOptimisticUpdates;
1858
1947
  /**
1859
1948
  * Run a Convex-parity `optimisticUpdate` callback against a localStore bound
1860
- * to the live subscription registry, appending each `setQuery` write's
1861
- * rollback to `optimisticRollbacks` (the same LIFO list the legacy path uses,
1862
- * unwound on settle/error). A throwing callback unwinds its own partial
1863
- * writes LIFO over just the rollbacks it producedand is swallowed, so a
1864
- * buggy optimistic update can never fail the mutation or leave a partial
1865
- * patch live, mirroring the legacy transform's throw handling.
1949
+ * to the live subscription registry. Each `setQuery` registers a constant
1950
+ * optimistic LAYER on its target subscription (via the same engine the
1951
+ * per-call `optimistic` path uses), so the multi-query patch rebases onto
1952
+ * incoming deltas and drops gaplessly on its commit cursorits `confirm` /
1953
+ * `rollback` closures are appended to the mutation's settle lists. A throwing
1954
+ * callback unwinds its own partial writes LIFO over just the rollbacks it
1955
+ * produced — and is swallowed, so a buggy optimistic update can never fail the
1956
+ * mutation or leave a partial patch live.
1866
1957
  */
1867
1958
  private applyOptimisticUpdate;
1868
1959
  private getConnection;
@@ -2012,4 +2103,4 @@ declare class LunoraClient {
2012
2103
  private clearQueryCacheForIdentityChange;
2013
2104
  private flushOfflineQueue;
2014
2105
  }
2015
- export { SyncWatermark as $, ArgsOf as A, BookmarkStorage as B, CachedQuery as C, DEFAULT_MAX_BUFFER as D, ServerMessage as E, FunctionReference as F, GlobalFacetResult as G, ServerPokeEndMessage as H, ServerPokePartMessage as I, ServerPokeStartMessage as J, ShardTrafficEntry as K, LunoraClient as L, MutationCallOptions as M, ShardTrafficResult as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, StorageListPage as T, User as U, StorageObject as V, StreamHandle as W, StreamIterable as X, SubscriptionCallback as Y, SubscriptionRegistry as Z, SubscriptionState as _, Unsubscribe as a, WorkflowInstanceAction as a0, WorkflowInstanceDetail as a1, WorkflowInstancePage as a2, WorkflowInstanceStatus as a3, WorkflowInstanceSummary as a4, WorkflowStepDetail as a5, createLocalStore as a6, createStream as a7, SubscriptionErrorCallback as b, PersistenceAdapter as c, ReconnectOptions as d, ClientMessage as e, ClientShapeSubscribeMessage as f, ClientShapeUnsubscribeMessage as g, ConnectionStatus as h, FunctionArgumentDescriptor as i, FunctionDescriptor as j, GlobalFacetValue as k, GlobalFilterClause as l, GlobalTableInfo as m, GlobalTablePage as n, LunoraClientOptions as o, OptimisticLocalStore as p, OptimisticUpdate as q, OutboxMutation as r, OutboxSink as s, PersistedMutation as t, RowOp as u, RpcEnvelope as v, RpcResponseBody as w, ScheduleRecord as x, SchedulerPoolStatus as y, SchedulerStatus as z };
2106
+ export { SubscriptionState as $, ArgsOf as A, BookmarkStorage as B, CachedQuery as C, DEFAULT_MAX_BUFFER as D, SchedulerStatus as E, FunctionReference as F, GlobalFacetResult as G, ServerMessage as H, ServerPokeEndMessage as I, ServerPokePartMessage as J, ServerPokeStartMessage as K, LunoraClient as L, MutationCallOptions as M, ShardTrafficEntry as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, ShardTrafficResult as T, User as U, StorageListPage as V, StorageObject as W, StreamHandle as X, StreamIterable as Y, SubscriptionCallback as Z, SubscriptionRegistry as _, Unsubscribe as a, SyncWatermark as a0, WorkflowInstanceAction as a1, WorkflowInstanceDetail as a2, WorkflowInstancePage as a3, WorkflowInstanceStatus as a4, WorkflowInstanceSummary as a5, WorkflowStepDetail as a6, createLocalStore as a7, createStream as a8, SubscriptionErrorCallback as b, PersistenceAdapter as c, ReconnectOptions as d, ClientMessage as e, ClientShapeSubscribeMessage as f, ClientShapeUnsubscribeMessage as g, ConnectionStatus as h, FunctionArgumentDescriptor as i, FunctionDescriptor as j, GlobalFacetValue as k, GlobalFilterClause as l, GlobalTableInfo as m, GlobalTablePage as n, LunoraClientOptions as o, MutationSettledEvent as p, OptimisticLocalStore as q, OptimisticUpdate as r, OutboxMutation as s, OutboxSink as t, PersistedMutation as u, RowOp as v, RpcEnvelope as w, RpcResponseBody as x, ScheduleRecord as y, SchedulerPoolStatus as z };
@@ -312,6 +312,8 @@ interface RpcEnvelope {
312
312
  * watermarked custom-mutator push additionally carries `lastMutationId` — the
313
313
  * highest per-client sequence the DO has applied — which the client uses to keep
314
314
  * its `clientSeq` generator monotonic across reloads (see `LunoraClient.callMutator`).
315
+ * A plain mutation on a CDC shard carries `commitCursor` — the cursor the write
316
+ * committed at — which gates the drop of a per-call optimistic layer.
315
317
  */
316
318
  type RpcResponseBody = {
317
319
  error: {
@@ -319,6 +321,7 @@ type RpcResponseBody = {
319
321
  message: string;
320
322
  };
321
323
  } | {
324
+ commitCursor?: number;
322
325
  lastMutationId?: number;
323
326
  result: unknown;
324
327
  };
@@ -820,6 +823,23 @@ interface SubscriptionError {
820
823
  message: string;
821
824
  }
822
825
  type SubscriptionErrorCallback = (error: SubscriptionError) => void;
826
+ /**
827
+ * One active per-call optimistic transform layered onto a subscription. The
828
+ * displayed value is the authoritative {@link SubscriptionState.serverBase}
829
+ * folded through every layer's `transform`, in order — so an incoming server
830
+ * frame re-folds the still-pending layers onto the new base (rebasing) instead
831
+ * of clobbering them. A layer is dropped — gaplessly — once a `data`/`delta`
832
+ * frame whose `cursor >= commitCursor` arrives (its write is now reflected in
833
+ * `serverBase`); `commitCursor` is the CDC cursor the server echoed on the
834
+ * mutation's response, and stays `undefined` while the write is still queued/
835
+ * in-flight (so the overlay survives unrelated deltas until confirmed).
836
+ */
837
+ interface OptimisticLayer {
838
+ /** The committed CDC cursor (from the mutation response); `undefined` until confirmed. */
839
+ commitCursor?: number;
840
+ readonly id: symbol;
841
+ readonly transform: (current: unknown) => unknown;
842
+ }
823
843
  interface SubscriptionState {
824
844
  /** True once the server has acked the subscription on the current socket. */
825
845
  acked: boolean;
@@ -862,6 +882,21 @@ interface SubscriptionState {
862
882
  /** Last known value, used to short-circuit `useQuery`-style consumers. */
863
883
  lastValue: unknown;
864
884
  /**
885
+ * Active per-call optimistic layers, in application order (see
886
+ * {@link OptimisticLayer}). Empty for subscriptions with no pending per-call
887
+ * optimistic write — the common case, where `lastValue` tracks `serverBase`
888
+ * exactly and behaviour is identical to a plain server-value assignment.
889
+ */
890
+ optimisticLayers: OptimisticLayer[];
891
+ /**
892
+ * The authoritative server value the optimistic layers fold onto — the value
893
+ * with NO optimistic overlay. Tracks `lastValue` exactly whenever no layers
894
+ * are active; diverges only while a per-call optimistic write is pending. A
895
+ * server frame updates this (and re-folds the layers); the durable read cache
896
+ * persists this, never the optimistic overlay.
897
+ */
898
+ serverBase: unknown;
899
+ /**
865
900
  * The `__cdc_log` high-watermark (`cursor`) the `lastValue` reflects,
866
901
  * captured from the last `data`/`delta`/`resume` frame. Persisted to the
867
902
  * durable read cache and replayed as `sinceSeq` on reconnect so the server
@@ -877,12 +912,6 @@ interface SubscriptionState {
877
912
  * until the first epoch-stamped frame arrives.
878
913
  */
879
914
  serverEpoch?: string;
880
- /**
881
- * Monotonic counter incremented on every server-pushed delta or data.
882
- * Used by optimistic-update rollback to detect whether the server has
883
- * already moved past the value we'd otherwise restore.
884
- */
885
- serverVersion: number;
886
915
  readonly shardKey?: string;
887
916
  }
888
917
  /**
@@ -909,10 +938,10 @@ declare class SubscriptionRegistry {
909
938
  * many subscribed queries at once (Convex's `OptimisticLocalStore` model).
910
939
  *
911
940
  * `getQuery` reads the current value (server value or any still-pending
912
- * optimistic override) of a subscribed query; `setQuery` writes an optimistic
913
- * override on top. Every write is collected as a rollback closure so the whole
914
- * batch unwinds atomically when the mutation settles or the server advances
915
- * past it — the same per-subscription rollback machinery the legacy
941
+ * optimistic override) of a subscribed query; `setQuery` registers a constant
942
+ * optimistic layer on top. The whole batch rebases onto incoming deltas and
943
+ * settles together confirmed on the mutation's commit cursor, or rolled back
944
+ * on failure — the same per-subscription layer machinery the single-query
916
945
  * per-call `optimistic` transform uses, generalized to N queries.
917
946
  */
918
947
  interface OptimisticLocalStore {
@@ -942,13 +971,17 @@ interface OptimisticLocalStore {
942
971
  /** A mutation's multi-query optimistic update: read/write the cache via `localStore`. */
943
972
  type OptimisticUpdate<Args> = (localStore: OptimisticLocalStore, args: Args) => void;
944
973
  /**
945
- * Build an {@link OptimisticLocalStore} bound to a subscription registry, the
946
- * mutation's shard key, and the `writeOptimisticToState` primitive. Returns the
947
- * store plus the ordered rollback closures every `setQuery` produced, so the
948
- * caller can unwind the whole batch (LIFO) if the mutation later fails — and
949
- * leave them in place to be GC'd alongside the subscription on success.
974
+ * Build an {@link OptimisticLocalStore} bound to a subscription registry and the
975
+ * mutation's shard key. Each `setQuery(value)` registers a constant-value layer
976
+ * on its target subscription (via `applyOptimisticLayer`): the predicted value
977
+ * survives incoming server deltas (re-clamped, masking concurrent changes to that
978
+ * query not merged) and drops gaplessly on the mutation's commit cursor, like
979
+ * the single-query per-call `optimistic` path. Returns the store plus the ordered
980
+ * `confirm` (success) and `rollback` (failure) closures every `setQuery` produced,
981
+ * so the caller settles the whole batch when the mutation does.
950
982
  */
951
- declare const createLocalStore: (subscriptions: SubscriptionRegistry, shardKey: string | undefined, write: (state: SubscriptionState, next: unknown) => () => void, stableStringify: (value: unknown) => string) => {
983
+ declare const createLocalStore: (subscriptions: SubscriptionRegistry, shardKey: string | undefined, stableStringify: (value: unknown) => string) => {
984
+ confirms: ((commitCursor: number | undefined) => void)[];
952
985
  rollbacks: (() => void)[];
953
986
  store: OptimisticLocalStore;
954
987
  };
@@ -1006,6 +1039,41 @@ declare const createStream: <T>(options: {
1006
1039
  */
1007
1040
  type ConnectionStatus = "connected" | "connecting" | "idle" | "offline";
1008
1041
  /**
1042
+ * Terminal verdict for a mutation that passed through the offline queue,
1043
+ * delivered to {@link LunoraClient.onMutationSettled}.
1044
+ *
1045
+ * Unlike the Promise returned by {@link LunoraClient.mutation} — which only the
1046
+ * original caller can await, and which no longer exists after a reload — this
1047
+ * fires for *every* queued write the server (or the queue) reaches a verdict on,
1048
+ * including writes restored from durable storage in a later session. It is the
1049
+ * channel a UI uses to tell the user "your queued change couldn't be saved"
1050
+ * instead of silently dropping a rolled-back optimistic row.
1051
+ *
1052
+ * `status: "rejected"` carries the failure `code` (e.g. `CONFLICT`,
1053
+ * `OFFLINE_QUEUE_OVERFLOW`, `OFFLINE_IDENTITY_CHANGED`) and the `error`.
1054
+ * `hadAwaiter` is `false` for a write whose original `mutation()` Promise is
1055
+ * gone (a hydrated/post-reload replay or an eviction), so a listener can tell
1056
+ * "the caller already saw this" apart from "nothing else will report this".
1057
+ */
1058
+ interface MutationSettledEvent {
1059
+ /** The write's args, so a listener can describe or re-offer the change. */
1060
+ readonly args: Record<string, unknown>;
1061
+ /** Server/queue error code on `rejected` (e.g. `CONFLICT`), when present. */
1062
+ readonly code?: string;
1063
+ /** The rejection error on `status: "rejected"`. */
1064
+ readonly error?: unknown;
1065
+ /** The `&lt;file>:&lt;function>` reference of the mutation. */
1066
+ readonly functionPath: string;
1067
+ /** Whether a live caller was still awaiting this write's `mutation()` Promise. */
1068
+ readonly hadAwaiter: boolean;
1069
+ /** The write's stable id (idempotency key / queue id). */
1070
+ readonly id: string;
1071
+ /** Shard the write targeted, if any. */
1072
+ readonly shardKey?: string;
1073
+ /** Terminal outcome. */
1074
+ readonly status: "committed" | "rejected";
1075
+ }
1076
+ /**
1009
1077
  * Per-call options for {@link LunoraClient.mutation} — the optimistic-update
1010
1078
  * machinery plus `shardKey`. Exported (at the end of this file) so the framework
1011
1079
  * adapters (`@lunora/react`, `/solid`, `/svelte`, `/vue`) can type their
@@ -1145,6 +1213,8 @@ declare class LunoraClient {
1145
1213
  private readonly statusListeners;
1146
1214
  /** Subscribers notified when the server drops a socket for an expired token (see `onTokenExpired`). */
1147
1215
  private readonly tokenExpiredListeners;
1216
+ /** Subscribers to offline-queued mutation verdicts (see `onMutationSettled`). */
1217
+ private readonly mutationSettledListeners;
1148
1218
  /**
1149
1219
  * Whisper-topic handlers, keyed by `connectionKey(shardKey)` → topic → set
1150
1220
  * of callbacks. Membership doubles as the resubscribe set replayed on every
@@ -1332,6 +1402,18 @@ declare class LunoraClient {
1332
1402
  * unsubscribe function.
1333
1403
  */
1334
1404
  onConnectionStatus(listener: (status: ConnectionStatus) => void): Unsubscribe;
1405
+ /**
1406
+ * Subscribe to terminal verdicts for offline-queued mutations. The listener
1407
+ * fires once per queued write that commits or is rejected — including a write
1408
+ * restored from durable storage after a reload, whose original `mutation()`
1409
+ * Promise no longer exists (`hadAwaiter: false`), and a write the queue
1410
+ * evicts on overflow or discards on an identity change. This is the durable
1411
+ * channel for surfacing a rolled-back optimistic write to the UI; an online
1412
+ * mutation that never queued still surfaces through the Promise `mutation()`
1413
+ * returns. The listener is NOT invoked on registration. Returns an
1414
+ * unsubscribe function. See {@link MutationSettledEvent}.
1415
+ */
1416
+ onMutationSettled(listener: (event: MutationSettledEvent) => void): Unsubscribe;
1335
1417
  query<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: {
1336
1418
  shardKey?: string;
1337
1419
  }): Promise<ReturnOf<F>>;
@@ -1841,6 +1923,13 @@ declare class LunoraClient {
1841
1923
  /** Recompute the aggregate status and notify listeners if it changed. */
1842
1924
  private emitConnectionStatus;
1843
1925
  /**
1926
+ * Build a {@link MutationSettledEvent} from a queued entry and emit it on the
1927
+ * {@link onMutationSettled} channel. `item.id` is always assigned by the time
1928
+ * a write settles (`enqueue`/`hydrate` guarantee it), so the `?? ""` fallback
1929
+ * is unreachable — present only to satisfy the optional queue-id type.
1930
+ */
1931
+ private emitItemSettled;
1932
+ /**
1844
1933
  * Apply an optimistic update to the subscription that matches the mutation's
1845
1934
  * `(functionRef, args, shardKey)` triple, returning the rollback callbacks to
1846
1935
  * invoke if the mutation later fails.
@@ -1857,12 +1946,14 @@ declare class LunoraClient {
1857
1946
  private applyOptimisticUpdates;
1858
1947
  /**
1859
1948
  * Run a Convex-parity `optimisticUpdate` callback against a localStore bound
1860
- * to the live subscription registry, appending each `setQuery` write's
1861
- * rollback to `optimisticRollbacks` (the same LIFO list the legacy path uses,
1862
- * unwound on settle/error). A throwing callback unwinds its own partial
1863
- * writes LIFO over just the rollbacks it producedand is swallowed, so a
1864
- * buggy optimistic update can never fail the mutation or leave a partial
1865
- * patch live, mirroring the legacy transform's throw handling.
1949
+ * to the live subscription registry. Each `setQuery` registers a constant
1950
+ * optimistic LAYER on its target subscription (via the same engine the
1951
+ * per-call `optimistic` path uses), so the multi-query patch rebases onto
1952
+ * incoming deltas and drops gaplessly on its commit cursorits `confirm` /
1953
+ * `rollback` closures are appended to the mutation's settle lists. A throwing
1954
+ * callback unwinds its own partial writes LIFO over just the rollbacks it
1955
+ * produced — and is swallowed, so a buggy optimistic update can never fail the
1956
+ * mutation or leave a partial patch live.
1866
1957
  */
1867
1958
  private applyOptimisticUpdate;
1868
1959
  private getConnection;
@@ -2012,4 +2103,4 @@ declare class LunoraClient {
2012
2103
  private clearQueryCacheForIdentityChange;
2013
2104
  private flushOfflineQueue;
2014
2105
  }
2015
- export { SyncWatermark as $, ArgsOf as A, BookmarkStorage as B, CachedQuery as C, DEFAULT_MAX_BUFFER as D, ServerMessage as E, FunctionReference as F, GlobalFacetResult as G, ServerPokeEndMessage as H, ServerPokePartMessage as I, ServerPokeStartMessage as J, ShardTrafficEntry as K, LunoraClient as L, MutationCallOptions as M, ShardTrafficResult as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, StorageListPage as T, User as U, StorageObject as V, StreamHandle as W, StreamIterable as X, SubscriptionCallback as Y, SubscriptionRegistry as Z, SubscriptionState as _, Unsubscribe as a, WorkflowInstanceAction as a0, WorkflowInstanceDetail as a1, WorkflowInstancePage as a2, WorkflowInstanceStatus as a3, WorkflowInstanceSummary as a4, WorkflowStepDetail as a5, createLocalStore as a6, createStream as a7, SubscriptionErrorCallback as b, PersistenceAdapter as c, ReconnectOptions as d, ClientMessage as e, ClientShapeSubscribeMessage as f, ClientShapeUnsubscribeMessage as g, ConnectionStatus as h, FunctionArgumentDescriptor as i, FunctionDescriptor as j, GlobalFacetValue as k, GlobalFilterClause as l, GlobalTableInfo as m, GlobalTablePage as n, LunoraClientOptions as o, OptimisticLocalStore as p, OptimisticUpdate as q, OutboxMutation as r, OutboxSink as s, PersistedMutation as t, RowOp as u, RpcEnvelope as v, RpcResponseBody as w, ScheduleRecord as x, SchedulerPoolStatus as y, SchedulerStatus as z };
2106
+ export { SubscriptionState as $, ArgsOf as A, BookmarkStorage as B, CachedQuery as C, DEFAULT_MAX_BUFFER as D, SchedulerStatus as E, FunctionReference as F, GlobalFacetResult as G, ServerMessage as H, ServerPokeEndMessage as I, ServerPokePartMessage as J, ServerPokeStartMessage as K, LunoraClient as L, MutationCallOptions as M, ShardTrafficEntry as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, ShardTrafficResult as T, User as U, StorageListPage as V, StorageObject as W, StreamHandle as X, StreamIterable as Y, SubscriptionCallback as Z, SubscriptionRegistry as _, Unsubscribe as a, SyncWatermark as a0, WorkflowInstanceAction as a1, WorkflowInstanceDetail as a2, WorkflowInstancePage as a3, WorkflowInstanceStatus as a4, WorkflowInstanceSummary as a5, WorkflowStepDetail as a6, createLocalStore as a7, createStream as a8, SubscriptionErrorCallback as b, PersistenceAdapter as c, ReconnectOptions as d, ClientMessage as e, ClientShapeSubscribeMessage as f, ClientShapeUnsubscribeMessage as g, ConnectionStatus as h, FunctionArgumentDescriptor as i, FunctionDescriptor as j, GlobalFacetValue as k, GlobalFilterClause as l, GlobalTableInfo as m, GlobalTablePage as n, LunoraClientOptions as o, MutationSettledEvent as p, OptimisticLocalStore as q, OptimisticUpdate as r, OutboxMutation as s, OutboxSink as t, PersistedMutation as u, RowOp as v, RpcEnvelope as w, RpcResponseBody as x, ScheduleRecord as y, SchedulerPoolStatus as z };
@@ -1,4 +1,4 @@
1
- import { F as FunctionReference, L as LunoraClient, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./lunora-client.d-CeUZ8lE1.mjs";
1
+ import { F as FunctionReference, L as LunoraClient, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./lunora-client.d-CfwxuJo8.mjs";
2
2
  /**
3
3
  * Run a query once on the server (during SSR) and capture its result in a
4
4
  * serializable {@link Preloaded} token. Embed the token in the rendered HTML and
@@ -1,4 +1,4 @@
1
- import { F as FunctionReference, L as LunoraClient, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./lunora-client.d-CeUZ8lE1.js";
1
+ import { F as FunctionReference, L as LunoraClient, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./lunora-client.d-CfwxuJo8.js";
2
2
  /**
3
3
  * Run a query once on the server (during SSR) and capture its result in a
4
4
  * serializable {@link Preloaded} token. Embed the token in the rendered HTML and
@@ -1,5 +1,5 @@
1
- import { S as SubscriptionError, F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-CeUZ8lE1.mjs";
2
- export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-CeUZ8lE1.mjs";
1
+ import { S as SubscriptionError, F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-CfwxuJo8.mjs";
2
+ export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-CfwxuJo8.mjs";
3
3
  import '@lunora/runtime';
4
4
  /**
5
5
  * The sentinel a framework adapter resolves its reactive args to when it wants
@@ -1,5 +1,5 @@
1
- import { S as SubscriptionError, F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-CeUZ8lE1.js";
2
- export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-CeUZ8lE1.js";
1
+ import { S as SubscriptionError, F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-CfwxuJo8.js";
2
+ export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-CfwxuJo8.js";
3
3
  import '@lunora/runtime';
4
4
  /**
5
5
  * The sentinel a framework adapter resolves its reactive args to when it wants
@@ -1,6 +1,6 @@
1
- import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-CeUZ8lE1.mjs";
2
- export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/lunora-client.d-CeUZ8lE1.mjs";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "../packem_shared/preload.d-DipqU0PI.mjs";
1
+ import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-CfwxuJo8.mjs";
2
+ export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/lunora-client.d-CfwxuJo8.mjs";
3
+ export { p as preloadQuery, a as preloadedQueryResult } from "../packem_shared/preload.d-Bbn8plix.mjs";
4
4
  import '@lunora/runtime';
5
5
  /**
6
6
  * Structural shape of a better-auth `getSession` call's resolved value.