@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.
- 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-CeUZ8lE1.d.mts → lunora-client.d-CfwxuJo8.d.mts} +114 -23
- package/dist/packem_shared/{lunora-client.d-CeUZ8lE1.d.ts → lunora-client.d-CfwxuJo8.d.ts} +114 -23
- package/dist/packem_shared/{preload.d-DipqU0PI.d.mts → preload.d-Bbn8plix.d.mts} +1 -1
- package/dist/packem_shared/{preload.d-CzHLgVfw.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-CeUZ8lE1.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
|
};
|
|
@@ -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`
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
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
|
|
946
|
-
* mutation's shard key
|
|
947
|
-
*
|
|
948
|
-
*
|
|
949
|
-
*
|
|
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,
|
|
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 `<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
|
+
/**
|
|
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
|
|
1861
|
-
*
|
|
1862
|
-
*
|
|
1863
|
-
*
|
|
1864
|
-
*
|
|
1865
|
-
*
|
|
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.
|
|
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 {
|
|
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`
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
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
|
|
946
|
-
* mutation's shard key
|
|
947
|
-
*
|
|
948
|
-
*
|
|
949
|
-
*
|
|
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,
|
|
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 `<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
|
+
/**
|
|
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
|
|
1861
|
-
*
|
|
1862
|
-
*
|
|
1863
|
-
*
|
|
1864
|
-
*
|
|
1865
|
-
*
|
|
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.
|
|
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 {
|
|
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
|
|
@@ -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.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
|
package/dist/query/index.d.mts
CHANGED
|
@@ -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-
|
|
2
|
-
export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-
|
|
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
|
package/dist/query/index.d.ts
CHANGED
|
@@ -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-
|
|
2
|
-
export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-
|
|
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
|
package/dist/ssr/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-
|
|
2
|
-
export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/lunora-client.d-
|
|
3
|
-
export { p as preloadQuery, a as preloadedQueryResult } from "../packem_shared/preload.d-
|
|
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.
|