@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.
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/index.d.mts +29 -4
- package/dist/index.d.ts +29 -4
- package/dist/index.mjs +3 -3
- package/dist/packem_shared/{LunoraClient-Bsv8immC.mjs → LunoraClient-C4eFp92b.mjs} +157 -105
- package/dist/packem_shared/{OfflineQueue-D-ASeqL7.mjs → OfflineQueue-eJBiWPfg.mjs} +4 -1
- package/dist/packem_shared/createLocalStore-IOur0jHF.mjs +1 -0
- package/dist/packem_shared/{createServerClient-DucOyOz3.mjs → createServerClient-PHoMdNGJ.mjs} +1 -1
- package/dist/packem_shared/local-store-BNgN3Dw3.mjs +111 -0
- package/dist/packem_shared/{lunora-client.d-DLbxPGH9.d.mts → lunora-client.d-CfwxuJo8.d.mts} +123 -23
- package/dist/packem_shared/{lunora-client.d-DLbxPGH9.d.ts → lunora-client.d-CfwxuJo8.d.ts} +123 -23
- package/dist/packem_shared/{preload.d-BAT6GeSD.d.mts → preload.d-Bbn8plix.d.mts} +1 -1
- package/dist/packem_shared/{preload.d-CSkjbfEq.d.ts → preload.d-DuMetDdU.d.ts} +1 -1
- package/dist/query/index.d.mts +2 -2
- package/dist/query/index.d.ts +2 -2
- package/dist/ssr/index.d.mts +3 -3
- package/dist/ssr/index.d.ts +3 -3
- package/dist/ssr/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/packem_shared/createLocalStore-DSUfoLqY.mjs +0 -36
|
@@ -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';
|
|
@@ -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 };
|
package/dist/packem_shared/{lunora-client.d-DLbxPGH9.d.mts → lunora-client.d-CfwxuJo8.d.mts}
RENAMED
|
@@ -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`
|
|
904
|
-
*
|
|
905
|
-
*
|
|
906
|
-
*
|
|
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
|
|
937
|
-
* mutation's shard key
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
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,
|
|
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 `<file>:<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
|
|
1852
|
-
*
|
|
1853
|
-
*
|
|
1854
|
-
*
|
|
1855
|
-
*
|
|
1856
|
-
*
|
|
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 cursor — its `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 {
|
|
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`
|
|
904
|
-
*
|
|
905
|
-
*
|
|
906
|
-
*
|
|
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
|
|
937
|
-
* mutation's shard key
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
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,
|
|
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 `<file>:<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
|
|
1852
|
-
*
|
|
1853
|
-
*
|
|
1854
|
-
*
|
|
1855
|
-
*
|
|
1856
|
-
*
|
|
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 cursor — its `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 {
|
|
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-
|
|
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
|