@lunora/client 1.0.0-alpha.7 → 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
  };
@@ -452,6 +455,13 @@ interface ServerDataMessage {
452
455
  /** The CDC epoch this frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
453
456
  epoch?: string;
454
457
  id: string;
458
+ /**
459
+ * The highest custom-mutator `mutationId` from this client the server has
460
+ * now applied (the per-client `__client_watermark`). Echoed so the client's
461
+ * outbox can drop confirmed pending mutations and let TanStack DB collapse
462
+ * the matching optimistic overlay. Absent on shards without custom mutators.
463
+ */
464
+ lastMutationId?: number;
455
465
  type: "data" | "delta";
456
466
  }
457
467
  /**
@@ -465,6 +475,8 @@ interface ServerResumeMessage {
465
475
  /** The CDC epoch this resume's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
466
476
  epoch?: string;
467
477
  id: string;
478
+ /** Per-client custom-mutator watermark (see {@link ServerDataMessage.lastMutationId}). */
479
+ lastMutationId?: number;
468
480
  type: "resume";
469
481
  }
470
482
  /**
@@ -811,6 +823,23 @@ interface SubscriptionError {
811
823
  message: string;
812
824
  }
813
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
+ }
814
843
  interface SubscriptionState {
815
844
  /** True once the server has acked the subscription on the current socket. */
816
845
  acked: boolean;
@@ -853,6 +882,21 @@ interface SubscriptionState {
853
882
  /** Last known value, used to short-circuit `useQuery`-style consumers. */
854
883
  lastValue: unknown;
855
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
+ /**
856
900
  * The `__cdc_log` high-watermark (`cursor`) the `lastValue` reflects,
857
901
  * captured from the last `data`/`delta`/`resume` frame. Persisted to the
858
902
  * durable read cache and replayed as `sinceSeq` on reconnect so the server
@@ -868,12 +912,6 @@ interface SubscriptionState {
868
912
  * until the first epoch-stamped frame arrives.
869
913
  */
870
914
  serverEpoch?: string;
871
- /**
872
- * Monotonic counter incremented on every server-pushed delta or data.
873
- * Used by optimistic-update rollback to detect whether the server has
874
- * already moved past the value we'd otherwise restore.
875
- */
876
- serverVersion: number;
877
915
  readonly shardKey?: string;
878
916
  }
879
917
  /**
@@ -900,10 +938,10 @@ declare class SubscriptionRegistry {
900
938
  * many subscribed queries at once (Convex's `OptimisticLocalStore` model).
901
939
  *
902
940
  * `getQuery` reads the current value (server value or any still-pending
903
- * optimistic override) of a subscribed query; `setQuery` writes an optimistic
904
- * override on top. Every write is collected as a rollback closure so the whole
905
- * batch unwinds atomically when the mutation settles or the server advances
906
- * 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
907
945
  * per-call `optimistic` transform uses, generalized to N queries.
908
946
  */
909
947
  interface OptimisticLocalStore {
@@ -933,13 +971,17 @@ interface OptimisticLocalStore {
933
971
  /** A mutation's multi-query optimistic update: read/write the cache via `localStore`. */
934
972
  type OptimisticUpdate<Args> = (localStore: OptimisticLocalStore, args: Args) => void;
935
973
  /**
936
- * Build an {@link OptimisticLocalStore} bound to a subscription registry, the
937
- * mutation's shard key, and the `writeOptimisticToState` primitive. Returns the
938
- * store plus the ordered rollback closures every `setQuery` produced, so the
939
- * caller can unwind the whole batch (LIFO) if the mutation later fails — and
940
- * 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.
941
982
  */
942
- 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)[];
943
985
  rollbacks: (() => void)[];
944
986
  store: OptimisticLocalStore;
945
987
  };
@@ -997,6 +1039,41 @@ declare const createStream: <T>(options: {
997
1039
  */
998
1040
  type ConnectionStatus = "connected" | "connecting" | "idle" | "offline";
999
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
+ /**
1000
1077
  * Per-call options for {@link LunoraClient.mutation} — the optimistic-update
1001
1078
  * machinery plus `shardKey`. Exported (at the end of this file) so the framework
1002
1079
  * adapters (`@lunora/react`, `/solid`, `/svelte`, `/vue`) can type their
@@ -1136,6 +1213,8 @@ declare class LunoraClient {
1136
1213
  private readonly statusListeners;
1137
1214
  /** Subscribers notified when the server drops a socket for an expired token (see `onTokenExpired`). */
1138
1215
  private readonly tokenExpiredListeners;
1216
+ /** Subscribers to offline-queued mutation verdicts (see `onMutationSettled`). */
1217
+ private readonly mutationSettledListeners;
1139
1218
  /**
1140
1219
  * Whisper-topic handlers, keyed by `connectionKey(shardKey)` → topic → set
1141
1220
  * of callbacks. Membership doubles as the resubscribe set replayed on every
@@ -1323,6 +1402,18 @@ declare class LunoraClient {
1323
1402
  * unsubscribe function.
1324
1403
  */
1325
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;
1326
1417
  query<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: {
1327
1418
  shardKey?: string;
1328
1419
  }): Promise<ReturnOf<F>>;
@@ -1832,6 +1923,13 @@ declare class LunoraClient {
1832
1923
  /** Recompute the aggregate status and notify listeners if it changed. */
1833
1924
  private emitConnectionStatus;
1834
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
+ /**
1835
1933
  * Apply an optimistic update to the subscription that matches the mutation's
1836
1934
  * `(functionRef, args, shardKey)` triple, returning the rollback callbacks to
1837
1935
  * invoke if the mutation later fails.
@@ -1848,12 +1946,14 @@ declare class LunoraClient {
1848
1946
  private applyOptimisticUpdates;
1849
1947
  /**
1850
1948
  * Run a Convex-parity `optimisticUpdate` callback against a localStore bound
1851
- * to the live subscription registry, appending each `setQuery` write's
1852
- * rollback to `optimisticRollbacks` (the same LIFO list the legacy path uses,
1853
- * unwound on settle/error). A throwing callback unwinds its own partial
1854
- * writes LIFO over just the rollbacks it producedand is swallowed, so a
1855
- * buggy optimistic update can never fail the mutation or leave a partial
1856
- * 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.
1857
1957
  */
1858
1958
  private applyOptimisticUpdate;
1859
1959
  private getConnection;
@@ -2003,4 +2103,4 @@ declare class LunoraClient {
2003
2103
  private clearQueryCacheForIdentityChange;
2004
2104
  private flushOfflineQueue;
2005
2105
  }
2006
- 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
  };
@@ -452,6 +455,13 @@ interface ServerDataMessage {
452
455
  /** The CDC epoch this frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
453
456
  epoch?: string;
454
457
  id: string;
458
+ /**
459
+ * The highest custom-mutator `mutationId` from this client the server has
460
+ * now applied (the per-client `__client_watermark`). Echoed so the client's
461
+ * outbox can drop confirmed pending mutations and let TanStack DB collapse
462
+ * the matching optimistic overlay. Absent on shards without custom mutators.
463
+ */
464
+ lastMutationId?: number;
455
465
  type: "data" | "delta";
456
466
  }
457
467
  /**
@@ -465,6 +475,8 @@ interface ServerResumeMessage {
465
475
  /** The CDC epoch this resume's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
466
476
  epoch?: string;
467
477
  id: string;
478
+ /** Per-client custom-mutator watermark (see {@link ServerDataMessage.lastMutationId}). */
479
+ lastMutationId?: number;
468
480
  type: "resume";
469
481
  }
470
482
  /**
@@ -811,6 +823,23 @@ interface SubscriptionError {
811
823
  message: string;
812
824
  }
813
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
+ }
814
843
  interface SubscriptionState {
815
844
  /** True once the server has acked the subscription on the current socket. */
816
845
  acked: boolean;
@@ -853,6 +882,21 @@ interface SubscriptionState {
853
882
  /** Last known value, used to short-circuit `useQuery`-style consumers. */
854
883
  lastValue: unknown;
855
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
+ /**
856
900
  * The `__cdc_log` high-watermark (`cursor`) the `lastValue` reflects,
857
901
  * captured from the last `data`/`delta`/`resume` frame. Persisted to the
858
902
  * durable read cache and replayed as `sinceSeq` on reconnect so the server
@@ -868,12 +912,6 @@ interface SubscriptionState {
868
912
  * until the first epoch-stamped frame arrives.
869
913
  */
870
914
  serverEpoch?: string;
871
- /**
872
- * Monotonic counter incremented on every server-pushed delta or data.
873
- * Used by optimistic-update rollback to detect whether the server has
874
- * already moved past the value we'd otherwise restore.
875
- */
876
- serverVersion: number;
877
915
  readonly shardKey?: string;
878
916
  }
879
917
  /**
@@ -900,10 +938,10 @@ declare class SubscriptionRegistry {
900
938
  * many subscribed queries at once (Convex's `OptimisticLocalStore` model).
901
939
  *
902
940
  * `getQuery` reads the current value (server value or any still-pending
903
- * optimistic override) of a subscribed query; `setQuery` writes an optimistic
904
- * override on top. Every write is collected as a rollback closure so the whole
905
- * batch unwinds atomically when the mutation settles or the server advances
906
- * 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
907
945
  * per-call `optimistic` transform uses, generalized to N queries.
908
946
  */
909
947
  interface OptimisticLocalStore {
@@ -933,13 +971,17 @@ interface OptimisticLocalStore {
933
971
  /** A mutation's multi-query optimistic update: read/write the cache via `localStore`. */
934
972
  type OptimisticUpdate<Args> = (localStore: OptimisticLocalStore, args: Args) => void;
935
973
  /**
936
- * Build an {@link OptimisticLocalStore} bound to a subscription registry, the
937
- * mutation's shard key, and the `writeOptimisticToState` primitive. Returns the
938
- * store plus the ordered rollback closures every `setQuery` produced, so the
939
- * caller can unwind the whole batch (LIFO) if the mutation later fails — and
940
- * 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.
941
982
  */
942
- 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)[];
943
985
  rollbacks: (() => void)[];
944
986
  store: OptimisticLocalStore;
945
987
  };
@@ -997,6 +1039,41 @@ declare const createStream: <T>(options: {
997
1039
  */
998
1040
  type ConnectionStatus = "connected" | "connecting" | "idle" | "offline";
999
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
+ /**
1000
1077
  * Per-call options for {@link LunoraClient.mutation} — the optimistic-update
1001
1078
  * machinery plus `shardKey`. Exported (at the end of this file) so the framework
1002
1079
  * adapters (`@lunora/react`, `/solid`, `/svelte`, `/vue`) can type their
@@ -1136,6 +1213,8 @@ declare class LunoraClient {
1136
1213
  private readonly statusListeners;
1137
1214
  /** Subscribers notified when the server drops a socket for an expired token (see `onTokenExpired`). */
1138
1215
  private readonly tokenExpiredListeners;
1216
+ /** Subscribers to offline-queued mutation verdicts (see `onMutationSettled`). */
1217
+ private readonly mutationSettledListeners;
1139
1218
  /**
1140
1219
  * Whisper-topic handlers, keyed by `connectionKey(shardKey)` → topic → set
1141
1220
  * of callbacks. Membership doubles as the resubscribe set replayed on every
@@ -1323,6 +1402,18 @@ declare class LunoraClient {
1323
1402
  * unsubscribe function.
1324
1403
  */
1325
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;
1326
1417
  query<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: {
1327
1418
  shardKey?: string;
1328
1419
  }): Promise<ReturnOf<F>>;
@@ -1832,6 +1923,13 @@ declare class LunoraClient {
1832
1923
  /** Recompute the aggregate status and notify listeners if it changed. */
1833
1924
  private emitConnectionStatus;
1834
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
+ /**
1835
1933
  * Apply an optimistic update to the subscription that matches the mutation's
1836
1934
  * `(functionRef, args, shardKey)` triple, returning the rollback callbacks to
1837
1935
  * invoke if the mutation later fails.
@@ -1848,12 +1946,14 @@ declare class LunoraClient {
1848
1946
  private applyOptimisticUpdates;
1849
1947
  /**
1850
1948
  * Run a Convex-parity `optimisticUpdate` callback against a localStore bound
1851
- * to the live subscription registry, appending each `setQuery` write's
1852
- * rollback to `optimisticRollbacks` (the same LIFO list the legacy path uses,
1853
- * unwound on settle/error). A throwing callback unwinds its own partial
1854
- * writes LIFO over just the rollbacks it producedand is swallowed, so a
1855
- * buggy optimistic update can never fail the mutation or leave a partial
1856
- * 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.
1857
1957
  */
1858
1958
  private applyOptimisticUpdate;
1859
1959
  private getConnection;
@@ -2003,4 +2103,4 @@ declare class LunoraClient {
2003
2103
  private clearQueryCacheForIdentityChange;
2004
2104
  private flushOfflineQueue;
2005
2105
  }
2006
- 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-DLbxPGH9.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