@lunora/client 1.0.0-alpha.23 → 1.0.0-alpha.231

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.
Files changed (97) hide show
  1. package/README.md +16 -3
  2. package/dist/auth/index.d.mts +43 -12
  3. package/dist/auth/index.d.ts +43 -12
  4. package/dist/auth/index.mjs +1 -60
  5. package/dist/index.d.mts +869 -438
  6. package/dist/index.d.ts +869 -438
  7. package/dist/index.mjs +1 -21
  8. package/dist/packem_shared/CONFLICT_ERROR_CODE-B7qNZMMk.mjs +1 -0
  9. package/dist/packem_shared/DEFAULT_MAX_BUFFER-Bd-_iyiV.mjs +1 -0
  10. package/dist/packem_shared/LunoraClient-D6MyuSWy.mjs +1 -0
  11. package/dist/packem_shared/OfflineQueue-BWamzR6p.mjs +1 -0
  12. package/dist/packem_shared/RETIRE_AFTER_DURABLE_SEQ_ADVANCE-DoEHtuR8.mjs +1 -0
  13. package/dist/packem_shared/SKIP-d7LeP-sY.mjs +1 -0
  14. package/dist/packem_shared/SubscriptionRegistry-DKIXE7M7.mjs +1 -0
  15. package/dist/packem_shared/TabCoordinator-CXD7vulJ.mjs +1 -0
  16. package/dist/packem_shared/applyDelta-CWpwS3Bb.mjs +1 -0
  17. package/dist/packem_shared/client-urls-BHLUjjcG.mjs +1 -0
  18. package/dist/packem_shared/connectLiveStream-DCgklEjI.mjs +1 -0
  19. package/dist/packem_shared/createAsyncStoragePersistence-6FCvqc6C.mjs +1 -0
  20. package/dist/packem_shared/createAsyncStorageQueryCache-f5023fdB.mjs +1 -0
  21. package/dist/packem_shared/createCallRunner-Bwcrfh76.mjs +1 -0
  22. package/dist/packem_shared/createClientQuery-B8Nfj-7o.mjs +1 -0
  23. package/dist/packem_shared/createInMemoryBookmarkStorage-BooZhW0n.mjs +1 -0
  24. package/dist/packem_shared/createInMemoryPersistence-Cz2W6XVj.mjs +1 -0
  25. package/dist/packem_shared/createInMemoryQueryCache-D2uNHLfM.mjs +1 -0
  26. package/dist/packem_shared/createLocalStore-BT0QPKdf.mjs +1 -0
  27. package/dist/packem_shared/createMutatorRunner-CuMRJpUx.mjs +1 -0
  28. package/dist/packem_shared/createReconnect-CjTmjJDH.mjs +1 -0
  29. package/dist/packem_shared/createServerClient-BqCdfoE1.mjs +1 -0
  30. package/dist/packem_shared/createSnapshotPrecondition-CLkxVeN1.mjs +1 -0
  31. package/dist/packem_shared/delta-merge-Ls3DED4g.mjs +1 -0
  32. package/dist/packem_shared/deserializePreloaded-CyFK_cv6.mjs +1 -0
  33. package/dist/packem_shared/function-reference.d-Br_hsKje.d.mts +45 -0
  34. package/dist/packem_shared/function-reference.d-Br_hsKje.d.ts +45 -0
  35. package/dist/packem_shared/getServerSession-BaOoo1p6.mjs +1 -0
  36. package/dist/packem_shared/httpStream-DaqP70Ne.mjs +9 -0
  37. package/dist/packem_shared/idb-utility-C8WS390w.mjs +1 -0
  38. package/dist/packem_shared/live-stream-ct8j7T75.mjs +10 -0
  39. package/dist/packem_shared/liveTopicSubscription-Bs-ULKfs.mjs +1 -0
  40. package/dist/packem_shared/local-store-fqWV2n_Z.mjs +1 -0
  41. package/dist/packem_shared/lunora-client.d-DpmxnYmL.d.ts +4916 -0
  42. package/dist/packem_shared/lunora-client.d-dOEEoF4x.d.mts +4916 -0
  43. package/dist/packem_shared/offline-queue-D_fr-uQ3.mjs +1 -0
  44. package/dist/packem_shared/preload.d-BkXsKMmU.d.ts +21 -0
  45. package/dist/packem_shared/preload.d-XVLTUaah.d.mts +21 -0
  46. package/dist/packem_shared/preloadQuery-uFy24PCR.mjs +1 -0
  47. package/dist/packem_shared/single-blob-store-DpqSxsgi.mjs +1 -0
  48. package/dist/packem_shared/wire-errors-Bw04a4ZQ.mjs +1 -0
  49. package/dist/pagination/index.d.mts +42 -42
  50. package/dist/pagination/index.d.ts +42 -42
  51. package/dist/pagination/index.mjs +1 -61
  52. package/dist/query/index.d.mts +111 -43
  53. package/dist/query/index.d.ts +111 -43
  54. package/dist/query/index.mjs +1 -1
  55. package/dist/service.d.mts +49 -0
  56. package/dist/service.d.ts +49 -0
  57. package/dist/service.mjs +1 -0
  58. package/dist/ssr/index.d.mts +109 -79
  59. package/dist/ssr/index.d.ts +109 -79
  60. package/dist/ssr/index.mjs +1 -4
  61. package/dist/upload.d.mts +35 -0
  62. package/dist/upload.d.ts +35 -0
  63. package/dist/upload.mjs +1 -0
  64. package/package.json +15 -2
  65. package/dist/packem_shared/CONFLICT_ERROR_CODE-B8gQ8tyU.mjs +0 -33
  66. package/dist/packem_shared/ClientServiceWorker-C3PAFwy0.mjs +0 -100
  67. package/dist/packem_shared/DEFAULT_MAX_BUFFER-7hFnzNk9.mjs +0 -110
  68. package/dist/packem_shared/LunoraClient-BBCQjjbl.mjs +0 -3599
  69. package/dist/packem_shared/OfflineQueue-B4HUF7rt.mjs +0 -1
  70. package/dist/packem_shared/SKIP-vItZChkw.mjs +0 -50
  71. package/dist/packem_shared/SubscriptionRegistry-CxS_Inha.mjs +0 -31
  72. package/dist/packem_shared/TabCoordinator-BwRR8H06.mjs +0 -222
  73. package/dist/packem_shared/applyDelta-CRKZ1PBt.mjs +0 -81
  74. package/dist/packem_shared/createAsyncStoragePersistence-1Z5BZ8RC.mjs +0 -45
  75. package/dist/packem_shared/createClientQuery-CQ51bWAE.mjs +0 -71
  76. package/dist/packem_shared/createInMemoryBookmarkStorage-BoN7a7TH.mjs +0 -11
  77. package/dist/packem_shared/createInMemoryPersistence-DZ2VHWgm.mjs +0 -79
  78. package/dist/packem_shared/createInMemoryQueryCache-DiaGZkA2.mjs +0 -112
  79. package/dist/packem_shared/createLocalStore-BtqUmOQA.mjs +0 -2
  80. package/dist/packem_shared/createMutationRunner-BqsavzvG.mjs +0 -21
  81. package/dist/packem_shared/createMutatorRunner-BETvCd0p.mjs +0 -31
  82. package/dist/packem_shared/createReconnect-Di_-oHH7.mjs +0 -22
  83. package/dist/packem_shared/createReply-lI4tVS2w.mjs +0 -36
  84. package/dist/packem_shared/createServerClient-CTTAmvMx.mjs +0 -11
  85. package/dist/packem_shared/createSnapshotPrecondition-CxQ1T4ZP.mjs +0 -18
  86. package/dist/packem_shared/deserializePreloaded-C0eJTY_W.mjs +0 -4
  87. package/dist/packem_shared/getServerSession-8jXewqxd.mjs +0 -13
  88. package/dist/packem_shared/httpStream-BJU-aflc.mjs +0 -159
  89. package/dist/packem_shared/idb-utility-DrSVX43Q.mjs +0 -48
  90. package/dist/packem_shared/local-store-DIq-UWfD.mjs +0 -106
  91. package/dist/packem_shared/lunora-client.d-JvtVpf8A.d.mts +0 -2824
  92. package/dist/packem_shared/lunora-client.d-JvtVpf8A.d.ts +0 -2824
  93. package/dist/packem_shared/offline-queue-CF4_Co5k.mjs +0 -199
  94. package/dist/packem_shared/preload.d-C4_d_l5v.d.ts +0 -20
  95. package/dist/packem_shared/preload.d-DKbjGN5O.d.mts +0 -20
  96. package/dist/packem_shared/preloadQuery-lobFkD2Z.mjs +0 -13
  97. package/dist/packem_shared/wire-key-Djie6aaR.mjs +0 -266
package/dist/index.d.mts CHANGED
@@ -1,13 +1,199 @@
1
- import { c as PersistenceAdapter, B as BookmarkStorage, S as SubscriptionError, H as HttpStreamRef, d as HttpStreamArgsOf, e as HttpStreamChunkOf, f as StreamIterable, F as FunctionReference, A as ArgsOf, M as MutationCallOptions, R as ReturnOf, O as OfflineQueueOptions, Q as QueryCacheAdapter, g as ReconnectOptions, L as LunoraClient } from "./packem_shared/lunora-client.d-JvtVpf8A.mjs";
2
- export { type h as BatchSlot, C as CONFLICT_ERROR_CODE, type i as CachedQuery, type j as ClientMessage, type k as ClientQueryRef, type l as ClientShapeSubscribeMessage, type m as ClientShapeUnsubscribeMessage, type n as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type o as FunctionArgumentDescriptor, type p as FunctionDescriptor, type G as GlobalFacetResult, type q as GlobalFacetValue, type r as GlobalFilterClause, type s as GlobalTableInfo, type t as GlobalTablePage, type u as HttpStreamCallArgs, type v as LunoraClientError, type w as LunoraClientOptions, type x as LunoraErrorCode, type y as MutationSettledEvent, type z as OptimisticLocalStore, type E as OptimisticUpdate, type I as OutboxMutation, type J as OutboxSink, type K as PersistedMutation, type P as Preloaded, type N as RowOp, type T as RpcEnvelope, type V as RpcResponseBody, type W as ScheduleRecord, type X as SchedulerPoolStatus, type Y as SchedulerStatus, type Z as ServerMessage, type _ as ServerPokeEndMessage, type $ as ServerPokePartMessage, type a0 as ServerPokeStartMessage, type a1 as ShardTrafficEntry, type a2 as ShardTrafficResult, type a3 as StorageListPage, type a4 as StorageObject, type a5 as StreamHandle, type a6 as SubscriptionCallback, type b as SubscriptionErrorCallback, a7 as SubscriptionRegistry, type a8 as SubscriptionState, type a9 as SyncWatermark, type a as Unsubscribe, type U as User, type aa as WorkflowInstanceAction, type ab as WorkflowInstanceDetail, type ac as WorkflowInstancePage, type ad as WorkflowInstanceStatus, type ae as WorkflowInstanceSummary, type af as WorkflowStepDetail, type ag as WsTokenProvider, ah as createClientQuery, ai as createLocalStore, aj as createStream, ak as getErrorCode, al as getRetryAfterMs, am as isConflictError, an as isForbiddenError, ao as isRateLimitedError, ap as isUnauthorizedError } from "./packem_shared/lunora-client.d-JvtVpf8A.mjs";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-DKbjGN5O.mjs";
4
- export type { AuthCapabilities, AuthConfigInfo, AuthImpersonation, AuthPage, AuthSession, AuthUser, AuthUserFieldSpec, CronJobInfo, KvKeyEntry, KvKeyListResult, KvNamespaceSummary, KvValueResult, VectorIndexSummary, VectorQueryMatch } from '@lunora/runtime';
5
- /**
6
- * The slice of React Native's `AsyncStorage` (or any async key/value store —
7
- * Expo `SecureStore`, a wrapped `localForage`, an in-memory map in tests) this
8
- * adapter needs. Matches `@react-native-async-storage/async-storage`'s core
9
- * surface, so you can pass the module straight in.
10
- */
1
+ import { R as ReconnectOptions, c as PersistenceAdapter, Q as QueryCacheAdapter, B as BookmarkStorage, C as ConnectionStatus, S as SubscriptionError, H as HttpStreamRef, d as HttpStreamArgsOf, e as HttpStreamChunkOf, f as StreamIterable, O as OfflineQueueOptions, L as LunoraClient } from "./packem_shared/lunora-client.d-dOEEoF4x.mjs";
2
+ export { type A as ActionCallOptions, type g as BatchSlot, type h as CachedQuery, type i as ClientDebugShard, type j as ClientDebugSnapshot, type k as ClientDebugSubscription, type l as ClientMessage, type m as ClientQueryRef, type n as ClientShapeSubscribeMessage, type o as ClientShapeUnsubscribeMessage, D as DEFAULT_MAX_BUFFER, type F as FunctionArgumentDescriptor, type p as FunctionDescriptor, type G as GlobalFacetResult, type q as GlobalFacetValue, type r as GlobalFilterClause, type s as GlobalTableInfo, type t as GlobalTablePage, type u as HttpStreamCallArgs, type v as LunoraClientError, type w as LunoraClientOptions, type M as MutationCallOptions, type x as MutationSettledEvent, type y as OptimisticLocalStore, type z as OptimisticUpdate, type E as OutboxMutation, type I as OutboxSink, type J as PersistedMutation, type P as Preloaded, type K as ReplayCredential, type N as ReplayIdentityVerdict, type T as RoomRef, type V as RowOp, type W as RpcEnvelope, type X as RpcResponseBody, type Y as ScheduleRecord, type Z as ScheduleRetryPolicy, type _ as SchedulerPoolStatus, type $ as SchedulerStatus, type a0 as ServerMessage, type a1 as ServerPokeEndMessage, type a2 as ServerPokePartMessage, type a3 as ServerPokeStartMessage, type a4 as ShardTrafficEntry, type a5 as ShardTrafficResult, type a6 as StorageListPage, type a7 as StorageObject, type a8 as StoredQuery, type a9 as StreamHandle, type aa as SubscriptionCallback, type b as SubscriptionErrorCallback, ab as SubscriptionRegistry, type ac as SubscriptionState, type ad as SyncWatermark, type a as Unsubscribe, type U as User, type ae as WorkflowInstanceAction, type af as WorkflowInstanceDetail, type ag as WorkflowInstancePage, type ah as WorkflowInstanceStatus, type ai as WorkflowInstanceSummary, type aj as WorkflowStepDetail, type ak as WsTokenProvider, al as createClientQuery, am as createLocalStore, an as createStream } from "./packem_shared/lunora-client.d-dOEEoF4x.mjs";
3
+ import { LunoraError, LunoraErrorCode } from '@lunora/errors';
4
+ export type { LunoraErrorCode } from '@lunora/errors';
5
+ export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-XVLTUaah.mjs";
6
+ import { F as FunctionReference } from "./packem_shared/function-reference.d-Br_hsKje.mjs";
7
+ export type { A as ArgsOf, R as ReturnOf } from "./packem_shared/function-reference.d-Br_hsKje.mjs";
8
+ export type { AuthCapabilities, AuthConfigInfo, AuthImpersonation, AuthPage, AuthSession, AuthUser, AuthUserFieldSpec, CronJobInfo, KvKeyEntry, KvKeyListResult, KvNamespaceSummary, KvValueResult, PipelineLogColumnMap, PipelineLogCursor, PipelineLogField, PipelineLogPage, PipelineLogQuery, PipelineLogRow, VectorIndexSummary, VectorQueryMatch } from '@lunora/runtime';
9
+ /**
10
+ * A failure the SERVER reached no verdict on: the response carried no
11
+ * `{ error }` envelope at all — no `fetch` implementation, a non-JSON body from
12
+ * a proxy, a bare 5xx gateway page.
13
+ *
14
+ * The distinction matters because a durable write must be re-queued rather than
15
+ * dropped, and `code` cannot carry it: these arrive as `INTERNAL`, which is also
16
+ * what a server that DID reach a verdict sends. It used to be a `Symbol` stamped
17
+ * on with `Object.assign` and read back through a `Record<symbol, unknown>`
18
+ * cast, so the one classifier that knew the convention was the only reader that
19
+ * could ever see it. As a kind, every reader can: `error instanceof
20
+ * TransportError`.
21
+ *
22
+ * Still `INTERNAL` on the wire, so nothing that switches on `code` changes.
23
+ *
24
+ * `data` carries the structured payload such an error can still have — the
25
+ * `{ retryAfterMs }` a `Retry-After` on the unreadable response named, which
26
+ * {@link getRetryAfterMs} reads back and the outbox paces its retry on.
27
+ */
28
+ declare class TransportError extends LunoraError {
29
+ constructor(message: string, data?: unknown);
30
+ }
31
+ /** Error code the server uses for optimistic-concurrency conflicts (HTTP 409). */
32
+ declare const CONFLICT_ERROR_CODE = "CONFLICT";
33
+ /**
34
+ * Whether an unknown rejection is an optimistic-concurrency conflict — the
35
+ * server lost a write race and the caller should refetch and retry (or surface
36
+ * the conflict). Structural check on the `code` property the client attaches
37
+ * when decoding the worker's `{ error: { code, message } }` envelope.
38
+ */
39
+ declare const isConflictError: (error: unknown) => error is Error & {
40
+ code: "CONFLICT";
41
+ };
42
+ /**
43
+ * Whether a rejection is an RLS/policy denial (`FORBIDDEN`, HTTP 403) — the
44
+ * caller is authenticated but not permitted to read/write the row. The most
45
+ * common per-call error a UI must handle in an RLS-first app.
46
+ */
47
+ declare const isForbiddenError: (error: unknown) => error is Error & {
48
+ code: "FORBIDDEN";
49
+ };
50
+ /** Whether a rejection is an authentication failure (`UNAUTHORIZED`, HTTP 401) — no/invalid identity. */
51
+ declare const isUnauthorizedError: (error: unknown) => error is Error & {
52
+ code: "UNAUTHORIZED";
53
+ };
54
+ /**
55
+ * Whether a rejection is a rate-limit denial (`TOO_MANY_REQUESTS`, HTTP 429).
56
+ * The retry hint (if the server sent one) is read with {@link getRetryAfterMs}.
57
+ */
58
+ declare const isRateLimitedError: (error: unknown) => error is Error & {
59
+ code: "TOO_MANY_REQUESTS";
60
+ };
61
+ /**
62
+ * Read the server's machine-readable `code` off a rejection, narrowed to the
63
+ * known {@link LunoraErrorCode} union. Returns `undefined` for a non-`Error`, a
64
+ * missing code, or a code string absent from the catalog (an app-minted code
65
+ * passed to `LunoraError` reads as `undefined` here rather than being falsely
66
+ * narrowed).
67
+ */
68
+ declare const getErrorCode: (error: unknown) => LunoraErrorCode | undefined;
69
+ /**
70
+ * Read the rate-limit retry hint (`data.retryAfterMs`) off a
71
+ * `TOO_MANY_REQUESTS` rejection without hand-casting the `unknown` `data`
72
+ * payload. Returns the finite millisecond value the server sent, or `undefined`
73
+ * when absent/non-numeric. Pair with {@link isRateLimitedError}.
74
+ */
75
+ declare const getRetryAfterMs: (error: unknown) => number | undefined;
76
+ /**
77
+ * Whether a durable replay's failure refused the credential rather than the
78
+ * write (see {@link AUTH_REPLAY_ERROR_CODES}) — a durable outbox holds such a
79
+ * write for the next attempt instead of dropping it.
80
+ */
81
+ declare const isAuthReplayFailure: (error: unknown) => boolean;
82
+ /**
83
+ * Exponential backoff calculator with optional jitter.
84
+ *
85
+ * `next()` doubles the delay each call up to `maxDelayMs`. When `jitter` is
86
+ * enabled the returned value is randomized in `[delay/2, delay]` so a fleet
87
+ * of clients reconnecting at the same time spread out their retries.
88
+ */
89
+ interface ReconnectCalculator {
90
+ /** Returns the delay to wait before the next reconnect attempt. */
91
+ next: () => number;
92
+ /** Resets the backoff to the initial delay (call on successful reconnect). */
93
+ reset: () => void;
94
+ }
95
+ declare const createReconnect: (options?: ReconnectOptions, random?: () => number) => ReconnectCalculator;
96
+ /**
97
+ * Optimistic-reconcile logic shared by every `@lunora/*` framework adapter's
98
+ * agent-chat surface (`@lunora/react` `useAgentChat`, plus the `@lunora/vue`,
99
+ * `@lunora/solid`, `@lunora/svelte`, and `@lunora/angular` counterparts). The
100
+ * adapters keep only their framework-specific state plumbing
101
+ * (`useState` / `.value` / `.set()` / a store) and delegate the pure merge decision
102
+ * here, so the heuristic lives — and is tested — in exactly one place.
103
+ */
104
+ /**
105
+ * The durable-message shape the reconcile reads: a structural subset of each
106
+ * adapter's `AgentChatMessage` (itself a client-safe mirror of `@lunora/agent`'s
107
+ * `AgentMessageRow`). Only `content`, `role`, and the per-thread-monotonic `seq` are
108
+ * consulted; adapters pass their fuller row type, which is assignable to this.
109
+ */
110
+ interface ReconcileDurableMessage {
111
+ content: string;
112
+ role: "assistant" | "system" | "tool" | "user";
113
+ seq: number;
114
+ }
115
+ /**
116
+ * A local optimistic user turn awaiting server acknowledgement. `id` is a
117
+ * client-generated handle the adapter uses to roll the row back on a failed send;
118
+ * the reconcile itself reads only `content` and `maxDurableSeqAtSend`.
119
+ */
120
+ interface OptimisticMessage {
121
+ content: string;
122
+ id: number;
123
+ /**
124
+ * The highest durable `seq` present when this row was sent. The reconcile
125
+ * retires the row when a durable `user` row with matching `content` lands at a
126
+ * STRICTLY GREATER `seq` (i.e. after the send) — window-independent because
127
+ * `seq` is monotonic, not a positional count. Also the age baseline for the
128
+ * {@link RETIRE_AFTER_DURABLE_SEQ_ADVANCE} fallback.
129
+ *
130
+ * `seq` is monotonic PER THREAD, not globally (`@lunora/agent` assigns it from
131
+ * the thread's own message count), so a baseline captured in one thread is
132
+ * meaningless in another: an optimistic row carried across a thread switch can
133
+ * never be retired by the new thread's history until its `seq` happens to pass
134
+ * the old thread's. Adapters therefore drop their optimistic rows whenever the
135
+ * resolved thread key changes.
136
+ */
137
+ maxDurableSeqAtSend: number;
138
+ }
139
+ /**
140
+ * The highest `seq` across `messages`, or `-1` when empty. Used both to capture an
141
+ * optimistic row's `maxDurableSeqAtSend` at send time and to base the synthetic
142
+ * seqs of rendered optimistic rows above the highest real durable seq (not just
143
+ * `messages.length`, which under-counts when durable rows have gaps), so a
144
+ * placeholder seq never collides with a real one.
145
+ */
146
+ declare const maxSeq: (messages: ReadonlyArray<{
147
+ seq: number;
148
+ }>) => number;
149
+ /**
150
+ * Secondary, windowed fallback: retire a pending optimistic row once the durable
151
+ * history's highest `seq` has advanced at least this far past the value seen at
152
+ * send, even though no matching-content user row is currently visible to claim it.
153
+ * This covers the pathological "identical content already present at send time"
154
+ * case, where no user row with a STRICTLY GREATER `seq` than the send-time max will
155
+ * ever appear for the primary content match to consume (e.g. the acknowledging row
156
+ * was evicted by a bounded `limit` before reconcile could see it). `seq` is monotonic
157
+ * within the thread, so it keeps climbing even when the visible user-row COUNT does not.
158
+ *
159
+ * The `2` is a heuristic threshold, NOT an invariant about turn shape. It is
160
+ * tempting to read it as "one turn == a user row and an assistant row (+2)", but
161
+ * `@lunora/agent`'s tool-loop turns can persist MANY rows, and an ERRORED turn
162
+ * persists only the user row (+1). Those non-(+2) turns are retired by the PRIMARY
163
+ * seq-based content match below (which sees the user row land at a greater `seq`),
164
+ * never by this count-based fallback. The fully robust fix is a server-echoed
165
+ * correlation id on each persisted user row (deferred; no plan filed).
166
+ *
167
+ * KNOWN LIMITATION (inherent to a client-only heuristic): on an ownerless /
168
+ * `instanceId`-less thread a FOREIGN writer that advances `seq` by >= 2 between this
169
+ * row's send and its own acknowledgement can trip this fallback and retire the row a
170
+ * beat early. The correlation id closes that gap; until then it is the accepted
171
+ * residual edge.
172
+ */
173
+ declare const RETIRE_AFTER_DURABLE_SEQ_ADVANCE = 2;
174
+ /**
175
+ * Drop the optimistic user turns the durable history has now caught up on. Pure —
176
+ * it reads only the two arrays plus values captured on each pending row at send
177
+ * time (no clock, no state mutation), so it is safe to run in render.
178
+ *
179
+ * The primary retire condition: a durable `user` row with the same `content` exists
180
+ * whose `seq` is STRICTLY GREATER than the row's `maxDurableSeqAtSend` — i.e. a user
181
+ * row that landed AFTER the send. This is window-independent (it matches on monotonic
182
+ * `seq`, not a positional count), so it retires the normal turn AND an errored
183
+ * single-row (+1) turn even under a saturated, sliding `limit`. Each durable row is
184
+ * consumed at most once (the `consumed` set), so repeated identical prompts sent
185
+ * back-to-back each wait for their OWN durable row instead of collapsing onto one.
186
+ *
187
+ * Failing that, the {@link RETIRE_AFTER_DURABLE_SEQ_ADVANCE} windowed fallback fires
188
+ * (see its doc) for the "identical content already present" pathological case.
189
+ */
190
+ declare const reconcileOptimistic: (optimistic: ReadonlyArray<OptimisticMessage>, durable: ReadonlyArray<ReconcileDurableMessage>) => OptimisticMessage[];
191
+ /**
192
+ * The slice of React Native's `AsyncStorage` (or any async key/value store —
193
+ * Expo `SecureStore`, a wrapped `localForage`, an in-memory map in tests) this
194
+ * adapter needs. Matches `@react-native-async-storage/async-storage`'s core
195
+ * surface, so you can pass the module straight in.
196
+ */
11
197
  interface AsyncStorageLike {
12
198
  getItem: (key: string) => Promise<string | null>;
13
199
  removeItem: (key: string) => Promise<void>;
@@ -20,49 +206,175 @@ interface AsyncStoragePersistenceOptions {
20
206
  storage: AsyncStorageLike;
21
207
  }
22
208
  /**
23
- * Builds a {@link PersistenceAdapter} over an async key/value store — the React
24
- * Native / Expo counterpart to the IndexedDB adapter (`createIndexedDbPersistence`).
25
- * The whole FIFO mutation log is serialized to JSON under a single key (`key`),
26
- * so enqueue order is preserved and `load()` returns freshly-parsed records that
27
- * callers can't alias.
28
- *
29
- * AsyncStorage has no transactions, so every read-modify-write is funnelled
30
- * through a single promise chain — concurrent `append`/`remove` calls run one at
31
- * a time and can't clobber each other's writes.
32
- */
209
+ * Builds a {@link PersistenceAdapter} over an async key/value store — the React
210
+ * Native / Expo counterpart to the IndexedDB adapter (`createIndexedDbPersistence`).
211
+ * The whole FIFO mutation log is stored under a single key (`key`), so enqueue
212
+ * order is preserved and `load()` returns freshly-parsed records that callers
213
+ * can't alias.
214
+ *
215
+ * Records round-trip through {@link singleBlobStore}'s wire codec. Args it
216
+ * cannot carry (a `RegExp`, a class instance) reject at `append`/`replace` — the
217
+ * flush path rejects them terminally anyway. A blob that exists but can't be
218
+ * decoded loads as empty, and every write rejects rather than overwrite it and
219
+ * lose the queued writes it may still hold; `clear()` discards it.
220
+ *
221
+ * AsyncStorage has no transactions, so every read-modify-write runs through
222
+ * {@link singleBlobStore}'s serialized chain — concurrent `append`/`remove`
223
+ * calls run one at a time and can't clobber each other's writes.
224
+ */
33
225
  declare const createAsyncStoragePersistence: (options: AsyncStoragePersistenceOptions) => PersistenceAdapter;
226
+ interface AsyncStorageQueryCacheOptions {
227
+ /** Storage key the cache is serialized under; defaults to `"lunora:query-cache"`. */
228
+ key?: string;
229
+ /** LRU row cap; defaults to 50 (see {@link DEFAULT_MAX_ENTRIES}). Must be a positive integer. The oldest rows by `ts` are pruned on `put` once exceeded. */
230
+ maxEntries?: number;
231
+ /** The async key/value store the cache is read from and written to (e.g. React Native `AsyncStorage`). */
232
+ storage: AsyncStorageLike;
233
+ }
234
+ /**
235
+ * Builds a {@link QueryCacheAdapter} over an async key/value store — the React
236
+ * Native / Expo counterpart to `createIndexedDbQueryCache`. The whole cache is
237
+ * serialized under a single key (`key`).
238
+ *
239
+ * Values round-trip through {@link singleBlobStore}'s wire codec (a `bigint`,
240
+ * `Date`, or bytes survives a reload); a corrupt blob reads as an empty cache.
241
+ *
242
+ * AsyncStorage has no transactions, so every read-modify-write runs through
243
+ * {@link singleBlobStore}'s serialized chain — concurrent `put`/`remove` calls
244
+ * run one at a time and can't clobber each other's writes.
245
+ *
246
+ * Single-blob storage rewrites the whole cache per `put` (bounded by
247
+ * `maxEntries` — see the cap's note on Android's row and shared-budget limits);
248
+ * if that ever shows up in profiles, per-key storage entries are the upgrade
249
+ * path.
250
+ */
251
+ declare const createAsyncStorageQueryCache: (options: AsyncStorageQueryCacheOptions) => QueryCacheAdapter;
34
252
  /** Default in-memory bookmark store. Survives the lifetime of the client. */
35
253
  declare const createInMemoryBookmarkStorage: () => BookmarkStorage;
254
+ /**
255
+ * Reactive sinks an adapter binds to its own primitive's setters (a React
256
+ * `useState`, a Solid signal, a Vue ref, a Svelte store). The runner pushes into
257
+ * them; how they store the value is the adapter's concern (e.g. Solid and React
258
+ * wrap function-valued results in a thunk so the setter stores them rather than
259
+ * invoking them).
260
+ */
261
+ interface CallRunnerSinks<R> {
262
+ /** Receives the normalized {@link Error} when the latest invocation rejects. */
263
+ setError: (error: Error) => void;
264
+ /** Receives `true` while at least one invocation is in flight (ref-counted across overlapping calls), else `false`. */
265
+ setPending: (pending: boolean) => void;
266
+ /** Receives the resolved value when the latest invocation succeeds. */
267
+ setResult: (result: R) => void;
268
+ }
269
+ /**
270
+ * Build the framework-neutral half of an adapter's write primitive — the `mutate`
271
+ * of `useMutation`/`createMutation`/`mutation`, and the `call` of
272
+ * `useAction`/`createAction`/`action`.
273
+ *
274
+ * It owns the orchestration every adapter otherwise copy-pastes: ref-counting
275
+ * overlapping invocations into `setPending` (so the flag clears only once the
276
+ * last settles), normalizing a thrown non-`Error`, and routing success/failure to
277
+ * `setResult`/`setError` before re-throwing the SAME instance (so a typed error's
278
+ * `.code`/`.data` survive for both the `catch` and the template).
279
+ *
280
+ * `invoke` is a pre-bound thunk — `(args, options) => client.mutation(fn, args, options)`
281
+ * or `(args, options) => client.action(fn, args, options)`. Binding at the call
282
+ * site is what keeps this one function: the runner never inspects `options`, it
283
+ * only forwards them, so the option type is inferred from the closure rather
284
+ * than hard-coded per procedure kind. What actually keeps `optimisticUpdate` off
285
+ * an action is the adapter's exported handle type, not this file.
286
+ *
287
+ * `data`/`error` track the LATEST invocation, not the last to settle: overlapping
288
+ * calls can resolve out of order, so an earlier call that finishes later must not
289
+ * clobber a newer call's outcome. Each invocation takes a monotonic token and
290
+ * writes the value sinks only while it is still the most recent one — otherwise a
291
+ * double-click could leave the UI showing the first click's result, or an error
292
+ * for a call that succeeded.
293
+ */
294
+ declare const createCallRunner: <A, O, R>(invoke: (args: A, options?: O) => Promise<R>, sinks: CallRunnerSinks<R>) => ((args: A, options?: O) => Promise<R>);
36
295
  interface TabCoordinatorOptions {
37
296
  /**
38
- * BroadcastChannel name. Defaults to `"lunora-bridge"`.
39
- */
297
+ * BroadcastChannel name. Defaults to `"lunora-bridge"`.
298
+ */
40
299
  channelName?: string;
41
300
  /**
42
- * Interval (ms) between leader heartbeats. Defaults to 1000.
43
- */
301
+ * Interval (ms) between leader heartbeats. Defaults to 1000.
302
+ */
44
303
  heartbeatInterval?: number;
45
304
  /**
46
- * Milliseconds without a heartbeat to consider the leader dead. Must be
47
- * larger than `heartbeatInterval`. Defaults to 3000.
48
- */
305
+ * Milliseconds without a heartbeat to consider the leader dead. Must be
306
+ * larger than `heartbeatInterval`. Defaults to 3000.
307
+ */
49
308
  leaderTimeout?: number;
50
309
  /**
51
- * Called when this tab becomes the leader (should open WS connections).
52
- */
310
+ * Called when this tab becomes the leader (should open WS connections).
311
+ */
53
312
  onBecomeLeader?: () => void;
54
313
  /**
55
- * Called when this tab loses leadership (should close WS connections).
56
- */
314
+ * Called when the leader broadcasts its aggregate `ConnectionStatus`
315
+ * (see `LunoraClient.emitConnectionStatus`) — a follower owns no socket
316
+ * of its own, so this is its only truthful signal for a status indicator
317
+ * or the offline-queue gate. Sent on every leader-side status change and
318
+ * once more right after a new leader takes over, so a follower already
319
+ * mid-mirroring isn't stuck on a stale value from the PREVIOUS leader.
320
+ *
321
+ * `identity` is the leader's own `identityFingerprint()` (`null` = signed
322
+ * out) — the channel-name scoping (see `LunoraClient.createTabCoordinator`)
323
+ * is the primary defence against a stale frame from a since-changed
324
+ * identity, but `setAuthToken` in one tab can move it to a new channel
325
+ * while this frame is already queued in another tab's message task queue.
326
+ * A follower drops the frame when `identity` is present and doesn't match
327
+ * its own; an **absent** `identity` (mixed-version leader) is accepted —
328
+ * today's behavior, and the channel split already separates version groups.
329
+ */
330
+ onConnectionStatus?: (status: ConnectionStatus, identity?: string | null) => void;
331
+ /**
332
+ * Called when the leader broadcasts a subscription error.
333
+ */
334
+ /** Fired on the LEADER when it answers another tab's leadership claim — the moment a new follower is known to be listening. */
335
+ onLeaderClaimAnswered?: () => void;
336
+ /**
337
+ * Called when this tab loses leadership (should close WS connections).
338
+ */
57
339
  onStopBeingLeader?: () => void;
58
340
  /**
59
- * Called when the leader broadcasts subscription data.
60
- */
61
- onSubscriptionData?: (key: string, data: unknown) => void;
341
+ * Called when the leader broadcasts subscription data. `cursor`/`epoch`
342
+ * ride along when the leader is CLIENT-01-aware, so the follower can drop
343
+ * its own confirmed optimistic layers instead of just displaying the raw
344
+ * value; both are absent on a mixed-version leader that hasn't shipped the
345
+ * cursor yet (the follower falls back to its historical behavior — see
346
+ * the follower data handler in `tab-leadership.ts`).
347
+ *
348
+ * `identity` is the leader's `identityFingerprint()` (see
349
+ * `onConnectionStatus`'s docblock for the full rationale and the
350
+ * absent-field mixed-version rule — identical here).
351
+ */
352
+ onSubscriptionData?: (key: string, data: unknown, cursor?: number, epoch?: string, identity?: string | null) => void;
353
+ onSubscriptionError?: (key: string, error: SubscriptionError, identity?: string | null) => void;
62
354
  /**
63
- * Called when the leader broadcasts a subscription error.
64
- */
65
- onSubscriptionError?: (key: string, error: SubscriptionError) => void;
355
+ * Called when the leader broadcasts a `settled` frame's checkpoint advance
356
+ * (no value change, but the resume cursor moved). A follower needs this
357
+ * too — otherwise a `setQuery`/per-call optimistic overlay that a
358
+ * byte-identical write just confirmed stays masked on follower tabs until
359
+ * the next VISIBLE data frame arrives, even though the leader already
360
+ * dropped it.
361
+ *
362
+ * `lastMutationId` is the LEADER's own per-client `__client_watermark` —
363
+ * scoped server-side to the socket's announced `clientId` — so it only
364
+ * means anything to a follower whose `clientId` matches the leader's
365
+ * (`clientId` rides along for exactly that comparison; see
366
+ * `LunoraClient`'s wiring). An **absent** `clientId` (a mixed-version
367
+ * leader that hasn't shipped this field yet) also skips the
368
+ * `mutationId` half — safe, because the follower's own gates still
369
+ * resolve via its own RPC-ack watermark path and the `CheckpointRegistry`
370
+ * bounded fallback. The checkpoint (cursor) half of this callback fires
371
+ * unconditionally regardless of `clientId` — only the `mutationId` half
372
+ * is scoped.
373
+ *
374
+ * `identity` is the leader's `identityFingerprint()` — see
375
+ * `onConnectionStatus`'s docblock for the drop rule (identical here).
376
+ */
377
+ onSubscriptionSettled?: (key: string, cursor?: number, epoch?: string, lastMutationId?: number, clientId?: string, identity?: string | null) => void;
66
378
  }
67
379
  declare class TabCoordinator {
68
380
  private readonly bc;
@@ -75,6 +387,12 @@ declare class TabCoordinator {
75
387
  private leader;
76
388
  /** `true` once `start()` has been called. */
77
389
  private running;
390
+ /**
391
+ * `true` while a `claimAndPromote()` claim window is open. Guards the yield
392
+ * handler and `checkLeaderHealth`'s belt-and-braces else-branch so the two
393
+ * promotion paths can't both arm a `becomeLeader` timeout for the same gap.
394
+ */
395
+ private promotionPending;
78
396
  /** Timestamp of the most recent leader heartbeat. */
79
397
  private lastHeartbeat;
80
398
  private heartbeatTimer;
@@ -82,21 +400,41 @@ declare class TabCoordinator {
82
400
  /** Callbacks set via constructor options. */
83
401
  private readonly onBecomeLeader;
84
402
  private readonly onStopBeingLeader;
403
+ private readonly onConnectionStatus;
85
404
  private readonly onSubscriptionData;
405
+ private readonly onLeaderClaimAnswered;
86
406
  private readonly onSubscriptionError;
407
+ private readonly onSubscriptionSettled;
87
408
  constructor(options?: TabCoordinatorOptions);
88
409
  /**
89
- * Start the coordinator: attempt to claim leadership and begin the
90
- * heartbeat/leader-check cycle. Safe to call multiple times.
91
- */
410
+ * Start the coordinator: attempt to claim leadership and begin the
411
+ * heartbeat/leader-check cycle. Safe to call multiple times.
412
+ */
92
413
  start(): void;
93
414
  /**
94
- * Stop the coordinator: yield leadership (if held), close the channel, and
95
- * clear all timers. Safe to call multiple times.
96
- */
415
+ * Stop the coordinator: yield leadership (if held), close the channel, and
416
+ * clear all timers. Safe to call multiple times.
417
+ */
97
418
  stop(): void;
98
419
  /** `true` when this tab is the current WebSocket leader. */
99
420
  isLeader(): boolean;
421
+ /**
422
+ * Force this (freshly `start()`-ed) tab to become leader immediately,
423
+ * skipping the normal claim-then-`leaderTimeout` dance. Safe to call
424
+ * right after `start()` when the caller already knows this tab was the
425
+ * SOLE leader of the group it's replacing (e.g. `LunoraClient`'s
426
+ * identity-change coordinator restart — waiting out the full
427
+ * `leaderTimeout` there would freeze every live query for no reason,
428
+ * since this tab is overwhelmingly likely to be alone on the freshly
429
+ * derived channel). A sibling tab going through the normal `start()`
430
+ * dance for the SAME transition observes this tab's `becomeLeader`
431
+ * heartbeat and defers before its own claim-timeout fires; if another
432
+ * tab ALSO force-promotes at the same moment (e.g. two tabs both
433
+ * transitioning to the same new identity), the existing
434
+ * `resolveLeaderVsLeaderTieBreak` resolves the rare double-promotion
435
+ * the same way it resolves any other split-brain.
436
+ */
437
+ promoteImmediately(): void;
100
438
  /** The tab id of the current leader, or `undefined` if unknown / no leader. */
101
439
  get leaderTabId(): string | undefined;
102
440
  /** The id of this tab. */
@@ -104,48 +442,86 @@ declare class TabCoordinator {
104
442
  /** `true` when the coordinator has been started and is not yet stopped. */
105
443
  get isRunning(): boolean;
106
444
  /**
107
- * Broadcast subscription data to all follower tabs. Only the leader should
108
- * call this.
109
- */
110
- broadcastSubscriptionData(key: string, data: unknown): void;
445
+ * Broadcast subscription data to all follower tabs. Only the leader should
446
+ * call this. `cursor`/`epoch` are omitted from the wire frame when
447
+ * `undefined` (e.g. a CDC-off shard) — a follower on ANY version treats a
448
+ * missing `cursor` as "no confirmed-layer drop this frame", so omitting it
449
+ * here is equivalent to sending it as `undefined`. `identity` is this
450
+ * (leader) tab's own `identityFingerprint()` (`null` = signed out) —
451
+ * stamped so a follower can drop a frame from a since-changed identity
452
+ * (see the `onSubscriptionData` docblock); spread-included whenever
453
+ * supplied, since a fixed leader always knows its own identity (never
454
+ * omits it) and `null` must round-trip distinctly from "field absent".
455
+ */
456
+ broadcastSubscriptionData(key: string, data: unknown, cursor?: number, epoch?: string, identity?: string | null): void;
111
457
  /**
112
- * Broadcast a subscription error to all follower tabs. Only the leader
113
- * should call this.
114
- */
115
- broadcastSubscriptionError(key: string, error: SubscriptionError): void;
458
+ * Broadcast a subscription error to all follower tabs. Only the leader
459
+ * should call this.
460
+ *
461
+ * `identity` is this tab's own `identityFingerprint()`, stamped so a
462
+ * follower can drop a frame minted under a different credential — the same
463
+ * guard `broadcastSubscriptionData` and `broadcastSubscriptionSettled`
464
+ * carry. This one lacked it, which was harmless only because nothing called
465
+ * it; once the leader started fanning errors, an unstamped frame could
466
+ * deliver one caller's rejection to a tab that had since signed in as
467
+ * someone else.
468
+ */
469
+ broadcastSubscriptionError(key: string, error: SubscriptionError, identity?: string | null): void;
470
+ /**
471
+ * Broadcast a `settled` frame's checkpoint advance to all follower tabs
472
+ * (no value change, but the resume cursor/epoch moved — see
473
+ * `LunoraClient.handleSettledMessage`). `clientId` is this (leader) tab's
474
+ * own client id, stamped so a follower can tell whether the echoed
475
+ * `lastMutationId` watermark is genuinely its own (see the
476
+ * `onSubscriptionSettled` docblock). `identity` is this tab's own
477
+ * `identityFingerprint()` — see `broadcastSubscriptionData`'s docblock for
478
+ * the same round-tripping rule. Only the leader should call this.
479
+ */
480
+ broadcastSubscriptionSettled(key: string, cursor?: number, epoch?: string, lastMutationId?: number, clientId?: string, identity?: string | null): void;
481
+ /**
482
+ * Broadcast this tab's aggregate `ConnectionStatus` to follower tabs, so
483
+ * they can mirror a truthful status without a socket of their own (see
484
+ * `LunoraClient.computeStatus`/`emitConnectionStatus`). `identity` is this
485
+ * tab's own `identityFingerprint()` — see `broadcastSubscriptionData`'s
486
+ * docblock for the same round-tripping rule. Only the leader should call
487
+ * this.
488
+ */
489
+ broadcastConnectionStatus(status: ConnectionStatus, identity?: string | null): void;
116
490
  private broadcast;
117
491
  private handleMessage;
492
+ private handleClaimLeadership;
493
+ private handleHeartbeat;
494
+ /**
495
+ * Resolve two leaders existing at once — e.g. this tab was backgrounded
496
+ * and its heartbeat/health timers were throttled while a foreground
497
+ * follower's were not, so the follower's `checkLeaderHealth` timed out
498
+ * the (still-alive) leader and self-promoted. `BroadcastChannel` message
499
+ * delivery isn't subject to the same timer-throttling clamp, so even a
500
+ * backgrounded leader eventually observes the pretender's heartbeat here
501
+ * — resolve the split-brain deterministically with the same
502
+ * lexicographically-smaller-tabId rule used at claim-adoption. If the
503
+ * other tab wins, step down; if we win, reassert immediately so the
504
+ * pretender demotes itself the moment it processes our heartbeat.
505
+ */
506
+ private resolveLeaderVsLeaderTieBreak;
118
507
  private becomeLeader;
119
508
  private sendHeartbeat;
120
509
  private checkLeaderHealth;
510
+ /**
511
+ * Broadcast a leadership claim and arm a `becomeLeader` fallback: if no
512
+ * other tab has asserted itself as leader by the time the window elapses,
513
+ * self-promote. Shared by `checkLeaderHealth`'s stale-leader path and the
514
+ * `yield-leadership` handler so both promotion triggers use one claim
515
+ * window instead of each arming its own timeout.
516
+ */
517
+ private claimAndPromote;
121
518
  }
122
519
  /**
123
- * Client-side incremental merging of structured mutation deltas.
124
- *
125
- * Lunora's live-query fan-out has two server paths:
126
- *
127
- * 1. Server re-execution (subscriptions carrying a `functionPath`) pushes a
128
- * full `data` snapshot whenever a write touches a table the query reads. These
129
- * already carry the authoritative result and are applied wholesale.
130
- * 2. Legacy delta fan-out (`broadcastDelta`) pushes a structured `MutationDelta`
131
- * as a `delta` frame to subscribers matched by table + args. The delta describes
132
- * a single row change (`insert` / `update` / `delete`) keyed by row id, so the
133
- * client can splice it into the cached list result without a full re-send.
134
- *
135
- * Historically the client treated the `delta` field as an opaque blob and
136
- * replaced the whole cached value with it on every message — which only made
137
- * sense for the rare delta payloads that already carried the full result. This
138
- * module lets the client recognise a structured delta and merge it into the
139
- * existing array (preserving order, no dup/loss), falling back to full
140
- * replacement when the payload isn't a recognisable row delta or can't be
141
- * applied cleanly against the current cached shape.
142
- */
143
- /**
144
- * One row change as emitted by `@lunora/do`'s `broadcastDelta`. Mirrors
145
- * `MutationDelta` in `@lunora/do` structurally so the client carries no
146
- * dependency on it. `row` is absent on `delete` events (and may be absent on
147
- * older servers for any op).
148
- */
520
+ * One row change as emitted by `@lunora/do`'s `broadcastDelta`. Mirrors
521
+ * `MutationDelta` in `@lunora/do` structurally so the client carries no
522
+ * dependency on it. `row` is absent on `delete` events (and may be absent on
523
+ * older servers for any op).
524
+ */
149
525
  interface MutationDelta {
150
526
  /** Row id (`_id`) the change applies to. */
151
527
  key: string;
@@ -154,124 +530,221 @@ interface MutationDelta {
154
530
  table: string;
155
531
  }
156
532
  /**
157
- * Structural guard: is `value` a `MutationDelta` the client knows how to merge?
158
- * We require `op`, `table`, and a string `key` so opaque payloads that merely
159
- * happen to be objects (e.g. an aggregate `{ count: 1 }` a query returns
160
- * verbatim) are never mistaken for a row delta and keep replacing the cached
161
- * value wholesale.
162
- */
533
+ * Structural guard: is `value` a `MutationDelta` the client knows how to merge?
534
+ * We require `op`, `table`, and a string `key` so opaque payloads that merely
535
+ * happen to be objects (e.g. an aggregate `{ count: 1 }` a query returns
536
+ * verbatim) are never mistaken for a row delta and keep replacing the cached
537
+ * value wholesale.
538
+ */
163
539
  declare const isMutationDelta: (value: unknown) => value is MutationDelta;
164
540
  /**
165
- * Apply a structured `MutationDelta` to a cached array result, returning a new
166
- * array (never mutating the input). Returns `undefined` when the delta can't be
167
- * applied cleanly — the caller should then fall back to the existing
168
- * full-replacement behaviour (or trust the next snapshot to reconcile).
169
- *
170
- * Mergeable shape: a plain array of id-bearing row objects, e.g. the result of
171
- * `db.query().collect()`.
172
- *
173
- * Insert / update / delete are matched by row `_id`:
174
- * - `insert`: appended (or placed by `_creationTime` order) if absent; treated
175
- * as an update if a row with the same id already exists (idempotent — guards
176
- * against a delta replayed after a snapshot already included it).
177
- * - `update`: replaces the matching row in place, preserving its position.
178
- * - `delete`: removes the matching row.
179
- *
180
- * Returns `undefined` when `current` isn't an array of id-keyable objects, or
181
- * when an `insert`/`update` delta carries no `row` to splice in.
182
- */
183
- declare const applyDelta: (current: unknown, delta: MutationDelta) => undefined | unknown[];
184
- /**
185
- * Options accepted by {@link httpStream}.
186
- * @experimental Part of the HTTP-SSE stream surface.
187
- */
541
+ * Apply a structured `MutationDelta` to a cached list result, returning a new
542
+ * value (never mutating the input). Returns `undefined` when the delta can't be
543
+ * applied cleanly — the caller should then fall back to the existing
544
+ * full-replacement behaviour (or trust the next snapshot to reconcile).
545
+ *
546
+ * Mergeable shapes: an array of id-bearing row objects, or a `.paginate()`
547
+ * result wrapping one in `page` (see `rowListOf`). A paginated value keeps its
548
+ * other fields and comes back as a new object with a new `page` — the server
549
+ * only sends row deltas for that shape when everything outside the page is
550
+ * unchanged, so the merged value matches the snapshot it chose not to send.
551
+ * @returns the merged value, or `undefined` when the delta cannot be applied
552
+ */
553
+ declare const applyDelta: (current: unknown, delta: MutationDelta) => Record<string, unknown> | undefined | unknown[];
554
+ /**
555
+ * Options accepted by {@link httpStream}.
556
+ */
188
557
  interface HttpStreamOptions {
189
558
  /**
190
- * Origin (or origin + prefix) the route path is appended to, e.g.
191
- * `https://my-app.example.com`. Defaults to `""` — a relative URL, which
192
- * resolves against the page origin in a browser.
193
- */
559
+ * Origin (or origin + prefix) the route path is appended to, e.g.
560
+ * `https://my-app.example.com`. Defaults to `""` — a relative URL, which
561
+ * resolves against the page origin in a browser.
562
+ */
194
563
  baseUrl?: string;
195
564
  /** `fetch` implementation override; defaults to the global `fetch`. */
196
565
  fetch?: typeof fetch;
197
566
  /** Extra request headers (e.g. `authorization`). `accept: text/event-stream` is always sent. */
198
567
  headers?: Record<string, string>;
568
+ /**
569
+ * Seed the first request's `Last-Event-ID` header, resuming a stream from a
570
+ * position a previous consumer persisted. Once frames arrive this is
571
+ * superseded by their `id:`, and a reconnecting stream always sends the
572
+ * latest id it saw.
573
+ */
574
+ lastEventId?: string;
199
575
  /** Caps the in-flight chunk buffer (see `createStream`); exceeding it fails the stream. */
200
576
  maxBuffer?: number;
577
+ /** Called with each frame's `id:` as it is seen (the value that will be replayed on reconnect). */
578
+ onEventId?: (id: string) => void;
579
+ /** Called when the server advertises a `retry:` reconnect delay, in milliseconds. */
580
+ onRetry?: (ms: number) => void;
581
+ /**
582
+ * Reopen the request after a transport interruption — a bare EOF or a
583
+ * read/transport error — replaying the last seen id as `Last-Event-ID`.
584
+ * Defaults to `false`, which surfaces the interruption as
585
+ * `HTTP_STREAM_INTERRUPTED` exactly as before. A terminal `complete`/`error`
586
+ * frame, a non-2xx status, and a consumer cancel never reconnect.
587
+ */
588
+ reconnect?: boolean;
201
589
  /** External abort signal — aborting it cancels the fetch (→ the server handler's `signal`). */
202
590
  signal?: AbortSignal;
203
591
  }
204
592
  /**
205
- * Open a typed HTTP-SSE stream route and iterate its chunks:
206
- *
207
- * ```ts
208
- * const stream = httpStream(httpStreams.http.tokens, { searchParams: { prompt } }, { baseUrl });
209
- * for await (const token of stream) {
210
- * render(token); // typed as the route handler's yielded chunk
211
- * }
212
- * ```
213
- *
214
- * The returned iterable terminates when the server writes `event: complete`;
215
- * an `event: error` frame (or a transport failure) surfaces as a coded
216
- * rejection on the next `next()`. `.cancel()` — or aborting `options.signal` —
217
- * aborts the underlying fetch, which the server observes via `request.signal`.
218
- * @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
219
- */
593
+ * Open a typed HTTP-SSE stream route and iterate its chunks:
594
+ *
595
+ * ```ts
596
+ * const stream = httpStream(httpStreams.http.tokens, { searchParams: { prompt } }, { baseUrl });
597
+ * for await (const token of stream) {
598
+ * render(token); // typed as the route handler's yielded chunk
599
+ * }
600
+ * ```
601
+ *
602
+ * The returned iterable terminates when the server writes `event: complete`;
603
+ * an `event: error` frame (or a transport failure) surfaces as a coded
604
+ * rejection on the next `next()`. `.cancel()` — or aborting `options.signal` —
605
+ * aborts the underlying fetch, which the server observes via `request.signal`.
606
+ */
220
607
  declare const httpStream: <Ref extends HttpStreamRef>(route: Ref, args?: HttpStreamArgsOf<Ref>, options?: HttpStreamOptions) => StreamIterable<HttpStreamChunkOf<Ref>>;
221
- /** The single transport method a mutation runner needs — narrowed so adapters can test against a stub. */
222
- interface MutationCapableClient<F extends FunctionReference> {
223
- mutation: (function_: F, args: ArgsOf<F>, options?: MutationCallOptions<unknown, unknown, ArgsOf<F>>) => Promise<ReturnOf<F>>;
608
+ /** Options for {@link connectLiveStream}. */
609
+ interface ConnectLiveStreamOptions {
610
+ /** `fetch` override (a test seam, or a custom transport). */
611
+ fetch?: typeof fetch;
612
+ /** Extra headers sent on both the stream and the control requests. */
613
+ headers?: Record<string, string>;
614
+ /** Ceiling for the reconnect backoff, in ms. Defaults to `30000`. */
615
+ maxDelayMs?: number;
616
+ /** Base reconnect delay in ms. Defaults to `1000`. */
617
+ retryDelay?: number;
618
+ /** Send cookies / credentials on the stream and control requests. */
619
+ withCredentials?: boolean;
620
+ }
621
+ /** A delivered live event. */
622
+ interface LiveStreamEvent {
623
+ /** The decoded payload the publisher sent. */
624
+ payload: unknown;
625
+ /** The topic the event was published on. */
626
+ topic: string;
627
+ }
628
+ /** Options for {@link LiveStream.subscribe}. */
629
+ /** A subscribe the shard refused for good: a reconnect would only be refused again. */
630
+ interface LiveStreamSubscribeError {
631
+ /** The HTTP status of the refusal, e.g. 403, 409 or 429. */
632
+ status: number;
633
+ subscriptionId: string;
634
+ topic: string;
635
+ }
636
+ interface LiveStreamSubscribeOptions {
637
+ /** Stable caller id; a generated one is used when omitted. */
638
+ id?: string;
639
+ /**
640
+ * Called when the shard refuses this subscription (403, 409, 429 and similar). The subscription is dropped
641
+ * first, so a later subscribe with the same id is accepted. A 401 is not reported: the stream reconnects,
642
+ * which re-authenticates and registers the subscription again.
643
+ */
644
+ onError?: (error: LiveStreamSubscribeError) => void;
645
+ /** Called for every event on `topic`. */
646
+ onEvent?: (payload: unknown) => void;
647
+ /** The topic to follow. */
648
+ topic: string;
649
+ }
650
+ /** A live stream connection. */
651
+ interface LiveStream {
652
+ /** Close the stream and stop reconnecting. */
653
+ close: () => void;
654
+ /** The server-minted connection id, or `undefined` before the first connect and while reconnecting. */
655
+ readonly connectionId: string | undefined;
656
+ /** Observe every event on the connection; returns an unsubscribe handle. */
657
+ onEvent: (handler: (event: LiveStreamEvent) => void) => () => void;
658
+ /** Resolves once the first connection is established (rejects only if `close`d first). */
659
+ readonly ready: Promise<void>;
660
+ /** Follow a topic; returns an unsubscribe handle. */
661
+ subscribe: (options: LiveStreamSubscribeOptions) => () => void;
224
662
  }
225
663
  /**
226
- * Reactive sinks an adapter binds to its own primitive's setters (a Solid
227
- * signal, a Vue ref, a Svelte store). The runner pushes into them; how they
228
- * store the value is the adapter's concern (e.g. Solid wraps function-valued
229
- * results in a thunk).
230
- */
231
- interface MutationRunnerSinks<R> {
232
- /** Receives the normalized {@link Error} when an invocation rejects. */
233
- setError: (error: Error) => void;
234
- /** Receives `true` while at least one invocation is in flight (ref-counted across overlapping calls), else `false`. */
235
- setPending: (pending: boolean) => void;
236
- /** Receives the resolved value when an invocation succeeds. */
237
- setResult: (result: R) => void;
664
+ * Connect to a live topic stream. See the module docblock.
665
+ * @param url the `connect` route URL (its sibling `control` route is derived)
666
+ */
667
+ declare const connectLiveStream: (url: string, options?: ConnectLiveStreamOptions) => LiveStream;
668
+ /**
669
+ * A reference to one declared live topic, as `_generated/live.ts` hands it out. Plain data
670
+ * (no server code), so a client bundle can hold it; the phantom `Payload` is what lets
671
+ * `ctx.live.publish` and the framework hooks type the event.
672
+ */
673
+ interface LiveTopicRef<Payload = unknown> {
674
+ /** Phantom: carries the declared payload type. Never present at runtime. */
675
+ readonly __payload?: Payload;
676
+ /** The `id` of the `defineLiveStream` that declares and authorizes this topic. */
677
+ readonly __lunoraLive: string;
678
+ /** The encoded wire topic (see {@link encodeLiveTopic}). */
679
+ readonly topic: string;
680
+ }
681
+ /** Options for {@link subscribeLiveTopic}. */
682
+ interface SubscribeLiveTopicOptions extends Omit<ConnectLiveStreamOptions, "fetch"> {
683
+ /** Called when the shard refuses this subscription (403, 409, 429 and similar). */
684
+ onError?: (error: LiveStreamSubscribeError) => void;
685
+ /** Called for every event on `topic`. */
686
+ onEvent?: (payload: unknown) => void;
687
+ /**
688
+ * Partitions the shared connection. A cookie-authenticated stream is bound to the session that opened it, so
689
+ * pass something that changes when the signed-in user does (a user id): a new value opens a new connection
690
+ * and the old one closes once its subscribers have moved. It is never sent to the server.
691
+ */
692
+ sessionKey?: string;
693
+ /** The topic to follow. */
694
+ topic: string;
695
+ /** The `connect` route URL of a `defineLiveStream`. */
696
+ url: string;
697
+ }
698
+ /**
699
+ * Follow `topic` on the shared connection for `url`.
700
+ * @returns an unsubscribe handle; the last one out closes the connection
701
+ */
702
+ declare const subscribeLiveTopic: (options: SubscribeLiveTopicOptions) => (() => void);
703
+ /**
704
+ * What a framework hook takes besides the topic reference: the transport tuning of
705
+ * {@link SubscribeLiveTopicOptions}, with `onEvent` typed by the topic's declared payload.
706
+ */
707
+ interface LiveTopicOptions<Payload = unknown> extends Omit<SubscribeLiveTopicOptions, "onEvent" | "topic" | "url"> {
708
+ /** Called for every event on the topic. The payload is the type the topic declares. */
709
+ onEvent?: (payload: Payload) => void;
710
+ /**
711
+ * Follow the topic on this shard: the connect URL gets `?shard=<key>`, and the control calls carry it too. Omit it
712
+ * for the default shard. Publish to the same shard with `{ shardKey }`, since delivery never crosses shards.
713
+ */
714
+ shardKey?: string;
238
715
  }
239
716
  /**
240
- * Build the framework-neutral `mutate` half of an adapter's mutation hook.
241
- *
242
- * Owns the orchestration every adapter otherwise copy-pastes: ref-counts
243
- * overlapping invocations into `setPending` (so it only clears once the last
244
- * settles), normalizes a thrown non-`Error`, and routes success/failure to
245
- * `setResult`/`setError` before re-throwing. Each adapter (`@lunora/react`,
246
- * `/solid`, `/svelte`, `/vue`) binds the three sinks to its own reactive
247
- * setters, so this logic lives in exactly one place. Optimistic-update options
248
- * pass straight through to `client.mutation`.
249
- */
250
- declare const createMutationRunner: <F extends FunctionReference>(client: MutationCapableClient<F>, function_: F, sinks: MutationRunnerSinks<ReturnOf<F>>) => ((args: ArgsOf<F>, options?: MutationCallOptions<unknown, unknown, ArgsOf<F>>) => Promise<ReturnOf<F>>);
251
- /**
252
- * The structural surface of a TanStack `Transaction` a bound custom mutator
253
- * returns — its `isPersisted.promise` resolves once the write is persisted and
254
- * rejects on failure. Typed structurally so the framework adapters need not
255
- * depend on `@tanstack/db` or `@lunora/db` (the handle is created app-side by
256
- * `bindMutators`).
257
- */
717
+ * Resolve a topic reference to the string-based options {@link subscribeLiveTopic} takes: the connect URL is the
718
+ * stream's route under the client's base URL, and the topic is the reference's encoded wire topic. Every framework
719
+ * hook goes through this, so they all build the same connection (and so share it).
720
+ */
721
+ declare const liveTopicSubscription: <Payload>(client: {
722
+ readonly url: string;
723
+ }, ref: LiveTopicRef<Payload>, options?: LiveTopicOptions<Payload>) => SubscribeLiveTopicOptions;
724
+ /**
725
+ * The structural surface of a TanStack `Transaction` a bound custom mutator
726
+ * returns — its `isPersisted.promise` resolves once the write is persisted and
727
+ * rejects on failure. Typed structurally so the framework adapters need not
728
+ * depend on `@tanstack/db` or `@lunora/db` (the handle is created app-side by
729
+ * `bindMutators`).
730
+ */
258
731
  interface MutatorTransaction {
259
732
  isPersisted: {
260
733
  promise: Promise<unknown>;
261
734
  };
262
735
  }
263
736
  /**
264
- * A bound custom-mutator handle produced by `bindMutators(client, ctx, mutators)`
265
- * in `@lunora/db`. Calling it applies the optimistic overlay to the local
266
- * collections and pushes the authoritative server write; it returns the TanStack
267
- * transaction whose `isPersisted` promise tracks completion.
268
- */
737
+ * A bound custom-mutator handle produced by `bindMutators(client, ctx, mutators)`
738
+ * in `@lunora/db`. Calling it applies the optimistic overlay to the local
739
+ * collections and pushes the authoritative server write; it returns the TanStack
740
+ * transaction whose `isPersisted` promise tracks completion.
741
+ */
269
742
  type MutatorHandle<TArgs> = (args: TArgs) => MutatorTransaction;
270
743
  /**
271
- * Reactive sinks an adapter binds to its own primitive's setters (a React
272
- * `useState`, a Solid signal, a Vue ref, a Svelte store). The runner pushes into
273
- * them; how they store the value is the adapter's concern.
274
- */
744
+ * Reactive sinks an adapter binds to its own primitive's setters (a React
745
+ * `useState`, a Solid signal, a Vue ref, a Svelte store). The runner pushes into
746
+ * them; how they store the value is the adapter's concern.
747
+ */
275
748
  interface MutatorRunnerSinks {
276
749
  /** Receives the normalized {@link Error} when an invocation rejects, or `undefined` on success / reset. */
277
750
  setError: (error: Error | undefined) => void;
@@ -279,59 +752,78 @@ interface MutatorRunnerSinks {
279
752
  setPending: (pending: boolean) => void;
280
753
  }
281
754
  /**
282
- * Build the framework-neutral `mutate` / `reset` pair of an adapter's
283
- * custom-mutator hook (`useMutator` / `createMutator` / `mutator`).
284
- *
285
- * Owns the orchestration every adapter otherwise copy-pastes: ref-counts
286
- * overlapping invocations into `setPending` (so it only clears once the last
287
- * settles), awaits the bound handle's `isPersisted` promise, normalizes a thrown
288
- * non-`Error`, and routes failure to `setError` (clearing it on success) before
289
- * re-throwing. Each adapter (`@lunora/react`, `/solid`, `/svelte`, `/vue`) binds
290
- * the two sinks to its own reactive setters, so this logic lives in exactly one
291
- * place. The optimistic overlay + server push are owned by the bound handle.
292
- *
293
- * `error` tracks the LATEST invocation, not the last to settle: overlapping
294
- * calls can resolve out of order, so an earlier call that finishes later must
295
- * not clobber a newer call's outcome. Each invocation takes a monotonic token
296
- * and only writes `setError` while it is still the most recent one — otherwise
297
- * `error`/`isError` could surface a stale success or failure (the documented
298
- * "latest invocation's error" contract every adapter advertises).
299
- */
755
+ * Build the framework-neutral `mutate` / `reset` pair of an adapter's
756
+ * custom-mutator hook (`useMutator` / `createMutator` / `mutator`).
757
+ *
758
+ * Owns the orchestration every adapter otherwise copy-pastes: ref-counts
759
+ * overlapping invocations into `setPending` (so it only clears once the last
760
+ * settles), awaits the bound handle's `isPersisted` promise, normalizes a thrown
761
+ * non-`Error`, and routes failure to `setError` (clearing it on success) before
762
+ * re-throwing. Each adapter (`@lunora/react`, `/solid`, `/svelte`, `/vue`) binds
763
+ * the two sinks to its own reactive setters, so this logic lives in exactly one
764
+ * place. The optimistic overlay + server push are owned by the bound handle.
765
+ *
766
+ * `error` tracks the LATEST invocation, not the last to settle: overlapping
767
+ * calls can resolve out of order, so an earlier call that finishes later must
768
+ * not clobber a newer call's outcome. Each invocation takes a monotonic token
769
+ * and only writes `setError` while it is still the most recent one — otherwise
770
+ * `error`/`isError` could surface a stale success or failure (the documented
771
+ * "latest invocation's error" contract every adapter advertises).
772
+ */
300
773
  declare const createMutatorRunner: <TArgs>(handle: MutatorHandle<TArgs>, sinks: MutatorRunnerSinks) => {
301
774
  mutate: (args: TArgs) => Promise<void>;
302
775
  reset: () => void;
303
776
  };
304
777
  interface QueuedMutation<T = unknown> {
305
778
  readonly args: Record<string, unknown>;
779
+ /**
780
+ * CDC cursor this write was composed against (see
781
+ * {@link PersistedMutation.baselineSeq}). Persisted and restored so a replay is
782
+ * judged against what its author could see, not against what the client has
783
+ * since caught up to.
784
+ */
785
+ readonly baselineSeq?: number;
786
+ /**
787
+ * The client id that queued this write (see {@link PersistedMutation.clientId}).
788
+ * Persisted and restored, so a replay namespaces by the id that issued the
789
+ * write rather than whatever the current session minted.
790
+ */
791
+ clientId?: string;
306
792
  readonly functionPath: string;
307
793
  /** Stable id used to remove the entry from durable storage once replayed; assigned by the queue when absent. */
308
794
  id?: string;
309
795
  /**
310
- * Issuing identity fingerprint carried through to durable storage (`null` =
311
- * signed out). Absent on hydrated legacy records, which replay ambiently.
312
- */
313
- readonly identity?: string | null;
314
- /**
315
- * `true` when a live caller is still awaiting this write's `mutation()`
316
- * Promise; `false`/absent for a write restored from durable storage after a
317
- * reload (its original awaiter is gone). Carried so terminal-verdict
318
- * observers can distinguish "the caller already saw this" from "nothing else
319
- * will report this". Maps to the public `MutationSettledEvent.hadAwaiter`.
320
- */
796
+ * Issuing identity fingerprint carried through to durable storage (`null` =
797
+ * signed out). Absent on hydrated legacy records, which replay ambiently.
798
+ *
799
+ * Mutable so {@link OfflineQueue.restampIdentity} can relabel a still-queued
800
+ * write when the identity's LABEL changes but the credential does not (the
801
+ * `setAuthToken(token, userId)` case where the subject resolves a tick after
802
+ * the token). The value is re-persisted alongside, so the new label survives
803
+ * a reload and a requeue.
804
+ */
805
+ identity?: string | null;
806
+ /**
807
+ * `true` when a live caller is still awaiting this write's `mutation()`
808
+ * Promise; `false`/absent for a write restored from durable storage after a
809
+ * reload (its original awaiter is gone). Carried so terminal-verdict
810
+ * observers can distinguish "the caller already saw this" from "nothing else
811
+ * will report this". Maps to the public `MutationSettledEvent.hadAwaiter`.
812
+ */
321
813
  liveAwaiter?: boolean;
322
814
  /**
323
- * Invoked on a successful replay with the server's echoed commit CDC cursor,
324
- * so a live per-call optimistic layer drops gaplessly once a frame reaches it.
325
- * Absent on hydrated records (the optimistic write lived in a prior session).
326
- */
815
+ * Invoked on a successful replay with the server's echoed commit CDC cursor,
816
+ * so a live per-call optimistic layer drops gaplessly once a frame reaches it.
817
+ * Absent on hydrated records (the optimistic write lived in a prior session).
818
+ */
327
819
  readonly onCommit?: (commitCursor: number | undefined) => void;
328
820
  /**
329
- * Optional sync predicate evaluated just before replay. When it returns
330
- * `false` the write is dropped instead of replaying, handling the case
331
- * where the mutation's preconditions are no longer valid (e.g. the
332
- * document it referred to was deleted while offline). Absent or `true`
333
- * means "ok to replay".
334
- */
821
+ * Optional sync predicate evaluated just before replay. When it returns
822
+ * `false` the write is dropped instead of replaying, handling the case
823
+ * where the mutation's preconditions are no longer valid (e.g. the
824
+ * document it referred to was deleted while offline). Absent or `true`
825
+ * means "ok to replay".
826
+ */
335
827
  readonly precondition?: () => boolean;
336
828
  /** Rejects if the mutation can no longer be replayed. */
337
829
  readonly reject: (error: unknown) => void;
@@ -340,11 +832,19 @@ interface QueuedMutation<T = unknown> {
340
832
  readonly shardKey?: string;
341
833
  }
342
834
  /**
343
- * Invoked when the queue itself discards an entry on overflow (capacity
344
- * eviction), so the client can surface the dropped write on its
345
- * terminal-verdict observer even when the entry has no live awaiter (a hydrated
346
- * record). The `error` carries the `OFFLINE_QUEUE_OVERFLOW` code.
347
- */
835
+ * Invoked when the queue itself discards an entry on overflow (capacity
836
+ * eviction), so the client can surface the dropped write on its
837
+ * terminal-verdict observer even when the entry has no live awaiter (a hydrated
838
+ * record). The `error` carries the `OFFLINE_QUEUE_OVERFLOW` code.
839
+ *
840
+ * The `sdks/*` ports deliberately do NOT mirror this shape — they return the
841
+ * discarded entry from `enqueue`/`hydrate`/`drainConflict`/`clear` instead,
842
+ * because they call the queue with a real lock held and settling a write needs
843
+ * that same lock, which self-deadlocks a non-reentrant one. Keep the callback
844
+ * here: this client is single-threaded, so the hazard does not exist, and an
845
+ * observer the queue fires itself is what covers the case a return value's
846
+ * consumer can quietly miss — a hydrated entry whose `reject` is a no-op.
847
+ */
348
848
  type EvictHandler = (entry: QueuedMutation, error: Error & {
349
849
  code?: string;
350
850
  }) => void;
@@ -360,28 +860,17 @@ interface OfflineQueueDeps {
360
860
  version?: string;
361
861
  }
362
862
  /**
363
- * A process-unique id, used both per-mutation and as the fallback `clientId`. It
364
- * MUST be globally unique: the server scopes a custom mutator's replay watermark
365
- * by `(verifiedIdentity, clientId)`, and an anonymous push has no verified
366
- * identity — so two anonymous clients that collide on `clientId` would share one
367
- * watermark namespace, letting one stall/suppress the other's ordered mutations.
368
- * `crypto.randomUUID` covers every modern runtime; the fallback still mixes
369
- * crypto-quality (or `Math.random`) entropy with the timestamp + counter so it
370
- * can't collide across two clients started in the same millisecond.
371
- */
372
-
373
- /**
374
- * Bounded FIFO queue. Mutations issued while the client is offline are
375
- * enqueued and replayed in the order they were submitted once the WS
376
- * reconnects and identifies. If the queue exceeds `maxItems` the oldest
377
- * entry is rejected with `OFFLINE_QUEUE_OVERFLOW`.
378
- *
379
- * When a {@link PersistenceAdapter} is supplied, enqueued mutations are mirrored
380
- * to durable storage so they survive a reload — {@link OfflineQueue.hydrate} restores them on
381
- * the next startup and the client replays them on reconnect. Durable removal is
382
- * the caller's responsibility *after* a successful replay (see `LunoraClient`);
383
- * the queue only persists on enqueue and un-persists on overflow.
384
- */
863
+ * Bounded FIFO queue. Mutations issued while the client is offline are
864
+ * enqueued and replayed in the order they were submitted once the WS
865
+ * reconnects and identifies. If the queue exceeds `maxItems` the oldest
866
+ * entry is rejected with `OFFLINE_QUEUE_OVERFLOW`.
867
+ *
868
+ * When a {@link PersistenceAdapter} is supplied, enqueued mutations are mirrored
869
+ * to durable storage so they survive a reload — {@link OfflineQueue.hydrate} restores them on
870
+ * the next startup and the client replays them on reconnect. Durable removal is
871
+ * the caller's responsibility *after* a successful replay (see `LunoraClient`);
872
+ * the queue only persists on enqueue and un-persists on overflow.
873
+ */
385
874
  declare class OfflineQueue {
386
875
  /** Opt-in to queueing mutations before the targeted shard's first connect. */
387
876
  readonly queueBeforeFirstConnect: boolean;
@@ -397,48 +886,115 @@ declare class OfflineQueue {
397
886
  get size(): number;
398
887
  enqueue<T>(entry: QueuedMutation<T>): void;
399
888
  /**
400
- * Restore mutations persisted in a prior session and re-queue them in FIFO
401
- * order. Restored entries already live in durable storage, so they are not
402
- * re-appended; they carry no-op `resolve`/`reject` (the original awaiter is
403
- * gone after a reload). No-op when no persistence adapter is configured.
404
- * Returns the distinct shard keys of the restored writes so the caller can
405
- * open their sockets to trigger a flush.
406
- */
889
+ * Restore mutations persisted in a prior session and re-queue them in FIFO
890
+ * order. Restored entries already live in durable storage, so they are not
891
+ * re-appended; they carry no-op `resolve`/`reject` (the original awaiter is
892
+ * gone after a reload). No-op when no persistence adapter is configured.
893
+ * Returns the distinct shard keys of the restored writes so the caller can
894
+ * open their sockets to trigger a flush.
895
+ *
896
+ * `hydrate()` runs post-construction (the caller awaits an async durable-store
897
+ * load), so a mutation issued while offline during that boot window is
898
+ * enqueued into `items` *before* this method's `await` resolves. Restored
899
+ * records are therefore `unshift`-ed ahead of whatever is already queued
900
+ * rather than `push`-ed to the end: the durable store's persist order is
901
+ * authoritative (a prior-session write is always older than anything from
902
+ * this session), so replaying a same-session boot-time write before an
903
+ * older restored write on the same document would let last-writer-wins
904
+ * silently clobber the newer data with the stale one.
905
+ */
407
906
  hydrate(): Promise<(string | undefined)[]>;
408
907
  /**
409
- * Remove and return queued mutations. With no `predicate`, drains the whole
410
- * queue. With one, drains only matching entries (preserving FIFO order) and
411
- * leaves the rest queued — used to flush a single shard's writes when its
412
- * socket reconnects while other shards are still down.
413
- */
908
+ * Relabel every queued write stamped `from` to `to`, in memory AND in durable
909
+ * storage.
910
+ *
911
+ * Used when the auth identity's LABEL changes while the credential does not —
912
+ * `setAuthToken(token, userId)` where the user id resolves a tick after the
913
+ * token was set. `setAuthToken` documents that this re-stamps in-flight
914
+ * queued writes rather than dropping them, and that promise only held for the
915
+ * caller's live in-memory stamp map, which is consumed and deleted on the
916
+ * first flush attempt. Everything durable still carried the old token hash,
917
+ * so a reload — or a transient-failure requeue after the token had since been
918
+ * refreshed — fell back to it, failed the replay identity gate, and rejected
919
+ * the SAME user's offline write with `OFFLINE_IDENTITY_CHANGED`.
920
+ *
921
+ * The durable rewrite goes through `PersistenceAdapter.replace`, the one
922
+ * operation on the contract that is required to be atomic — see
923
+ * {@link OfflineQueue.rewriteStamp}.
924
+ */
925
+ restampIdentity(from: string | null, to: string | null): void;
926
+ /**
927
+ * Whether any queued mutation matches — the cheap "is there a write ahead of
928
+ * me on this shard?" question, answered without draining. A write held at
929
+ * flush time (an identity not yet re-confirmed) sits here with no barrier
930
+ * published for a later `mutation()` to wait on, so this is what keeps a
931
+ * live write from overtaking it.
932
+ */
933
+ hasPending(predicate: (item: QueuedMutation) => boolean): boolean;
934
+ /**
935
+ * Remove and return queued mutations. With no `predicate`, drains the whole
936
+ * queue. With one, drains only matching entries (preserving FIFO order) and
937
+ * leaves the rest queued — used to flush a single shard's writes when its
938
+ * socket reconnects while other shards are still down.
939
+ */
414
940
  drain(predicate?: (item: QueuedMutation) => boolean): QueuedMutation[];
415
941
  /**
416
- * Return previously-drained mutations to the front of the queue, preserving
417
- * their FIFO order, without re-persisting them — they were never unpersisted,
418
- * so durable storage still holds them. Used when a flush aborts on a transient
419
- * transport failure: the unreplayed writes stay queued for the next reconnect.
420
- */
942
+ * Return previously-drained mutations to the front of the queue, preserving
943
+ * their FIFO order, without re-persisting them — they were never unpersisted,
944
+ * so durable storage still holds them. Used when a flush aborts on a transient
945
+ * transport failure: the unreplayed writes stay queued for the next reconnect.
946
+ */
421
947
  requeue(items: QueuedMutation[]): void;
422
948
  /**
423
- * Remove mutations whose precondition evaluates to `false` (stale/dirty
424
- * writes that should not replay) and reject each with an
425
- * `OFFLINE_PRECONDITION_FAILED` error. The valid (admitted) mutations stay
426
- * queued in FIFO order. Returns the drained stale entries.
427
- *
428
- * Called during reconnect before the flush cycle to weed out writes whose
429
- * assumptions no longer hold (e.g. a document was deleted by another client).
430
- */
949
+ * Remove mutations whose precondition evaluates to `false` (stale/dirty
950
+ * writes that should not replay) and reject each with an
951
+ * `OFFLINE_PRECONDITION_FAILED` error. The valid (admitted) mutations stay
952
+ * queued in FIFO order. Returns the drained stale entries.
953
+ *
954
+ * Called during reconnect before the flush cycle to weed out writes whose
955
+ * assumptions no longer hold (e.g. a document was deleted by another client).
956
+ */
431
957
  drainConflict(): QueuedMutation[];
432
958
  clear(): void;
959
+ /**
960
+ * The durable half of {@link OfflineQueue.restampIdentity}: rewrite the
961
+ * persisted record under the new identity stamp.
962
+ *
963
+ * This used to be `remove` then `append` (because `append` is an insert, not
964
+ * an upsert) with a compensating re-append on failure, and no arrangement of
965
+ * those two calls is safe. A process stop between a committed `remove` and
966
+ * the `append` leaves the mutation in NO durable store while the in-memory
967
+ * entry has already advanced, so a reload loses the write outright — and
968
+ * compensation cannot cover a crash, only a rejection. The re-append also
969
+ * moved the record to the tail, replaying it out of issue order.
970
+ *
971
+ * `PersistenceAdapter.replace` is the single atomic operation that removes
972
+ * both: the swap lands whole or not at all, and the record keeps its place
973
+ * in FIFO order. A rejection means nothing changed durably — the record
974
+ * stands under its OLD stamp, which is the documented outcome (a replay
975
+ * under a stale stamp is refused with `OFFLINE_IDENTITY_CHANGED`, visible
976
+ * and recoverable, unlike a silent loss) — so there is nothing to
977
+ * compensate, only to report.
978
+ */
979
+ private rewriteStamp;
980
+ /**
981
+ * Evict entries from the FRONT of `items` (the oldest — FIFO order) until
982
+ * the queue is at or under `maxItems`, rejecting each with
983
+ * `OFFLINE_QUEUE_OVERFLOW`, un-persisting it, and firing `onEvict`. Shared
984
+ * by `enqueue` (a live write pushes past capacity) and `hydrate` (a durable
985
+ * store restored more than `maxItems` records — CLIENT-03) so an overflow
986
+ * always drops the same way regardless of which caller triggered it.
987
+ */
988
+ private evictOverflow;
433
989
  /** Notify the size observer (the client's pending-sync count) after any change. */
434
990
  private notifySize;
435
991
  }
436
992
  /**
437
- * In-memory {@link PersistenceAdapter}. Doesn't survive a reload — it exists so
438
- * the persistence wiring can be exercised without IndexedDB (tests, SSR, or as
439
- * a deliberate "no durable store" choice that still satisfies the interface).
440
- * Preserves enqueue order; `clone` keeps callers from mutating stored args.
441
- */
993
+ * In-memory {@link PersistenceAdapter}. Doesn't survive a reload — it exists so
994
+ * the persistence wiring can be exercised without IndexedDB (tests, SSR, or as
995
+ * a deliberate "no durable store" choice that still satisfies the interface).
996
+ * Preserves enqueue order; `clone` keeps callers from mutating stored args.
997
+ */
442
998
  declare const createInMemoryPersistence: () => PersistenceAdapter;
443
999
  interface IndexedDbPersistenceOptions {
444
1000
  /** Database name; defaults to `"lunora-outbox"` (its own DB, separate from the read cache). */
@@ -449,47 +1005,29 @@ interface IndexedDbPersistenceOptions {
449
1005
  storeName?: string;
450
1006
  }
451
1007
  /**
452
- * IndexedDB-backed {@link PersistenceAdapter}. Each mutation is stored under an
453
- * autoincrementing key (so `load()` returns them in enqueue order regardless of
454
- * the string ids) with a unique secondary index on `id` for `remove()`.
455
- *
456
- * The store handle is opened lazily and the open promise is cached, so repeated
457
- * ops reuse one connection. Throws eagerly if no `IDBFactory` is available —
458
- * callers in non-browser environments should use {@link createInMemoryPersistence}.
459
- */
1008
+ * IndexedDB-backed {@link PersistenceAdapter}. Each mutation is stored under an
1009
+ * autoincrementing key (so `load()` returns them in enqueue order regardless of
1010
+ * the string ids) with a unique secondary index on `id` for `remove()`.
1011
+ *
1012
+ * The store handle is opened lazily and the open promise is cached, so repeated
1013
+ * ops reuse one connection. Throws eagerly if no `IDBFactory` is available —
1014
+ * callers in non-browser environments should use {@link createInMemoryPersistence}.
1015
+ */
460
1016
  declare const createIndexedDbPersistence: (options?: IndexedDbPersistenceOptions) => PersistenceAdapter;
461
1017
  /**
462
- * Resolve the effective offline-queue persistence from the user option,
463
- * defaulting to a durable IndexedDB store when the environment supports one.
464
- *
465
- * An explicit adapter is used as-is; `false` opts out (the caller keeps an
466
- * in-memory queue, lost on reload); `undefined` (the default) auto-probes
467
- * IndexedDB when the global is present (browsers) and `autoProbe` is set,
468
- * otherwise `undefined` — so SSR/Node/React-Native keep today's in-memory
469
- * behaviour and only environments that can persist do.
470
- *
471
- * `autoProbe` is `false` when the `@lunora/db` outbox is wired: that sink is the
472
- * single durable write path, so the built-in queue must stay in memory rather
473
- * than persist a second, never-flushed copy. An explicit adapter is still
474
- * honoured (the caller asked for it); only the implicit default is suppressed.
475
- *
476
- * The IndexedDB adapter opens its connection lazily, so constructing it here is
477
- * cheap and never throws (the `indexedDB` global is verified present first).
478
- */
479
- /**
480
- * Compose the read-cache key for a subscription. Mirrors how
481
- * `SubscriptionRegistry` keys live subscriptions so a hydrated value lines up
482
- * with the subscription that will consume it. `shardKey` defaults to `""` (the
483
- * root shard) exactly as the registry does.
484
- */
1018
+ * Compose the read-cache key for a subscription. Mirrors how
1019
+ * `SubscriptionRegistry` keys live subscriptions so a hydrated value lines up
1020
+ * with the subscription that will consume it. `shardKey` defaults to `""` (the
1021
+ * root shard) exactly as the registry does.
1022
+ */
485
1023
  declare const queryCacheKey: (functionPath: string, argsKey: string, shardKey?: string) => string;
486
1024
  /**
487
- * In-memory {@link QueryCacheAdapter}. Doesn't survive a reload — it exists so
488
- * the read-cache wiring can be exercised without IndexedDB (tests, SSR, or as a
489
- * deliberate "no durable store" choice that still satisfies the interface).
490
- * Enforces the same LRU row cap as the IndexedDB adapter; `clone` keeps callers
491
- * from mutating stored values.
492
- */
1025
+ * In-memory {@link QueryCacheAdapter}. Doesn't survive a reload — it exists so
1026
+ * the read-cache wiring can be exercised without IndexedDB (tests, SSR, or as a
1027
+ * deliberate "no durable store" choice that still satisfies the interface).
1028
+ * Enforces the same LRU row cap as the IndexedDB adapter; `clone` keeps callers
1029
+ * from mutating stored values.
1030
+ */
493
1031
  declare const createInMemoryQueryCache: (options?: {
494
1032
  maxEntries?: number;
495
1033
  }) => QueryCacheAdapter;
@@ -498,149 +1036,42 @@ interface IndexedDbQueryCacheOptions {
498
1036
  databaseName?: string;
499
1037
  /** Injectable `IDBFactory` (e.g. `fake-indexeddb` in tests); defaults to the global `indexedDB`. */
500
1038
  indexedDB?: IDBFactory;
501
- /** LRU row cap; defaults to 500. The oldest rows by `ts` are pruned on `put` once exceeded. */
1039
+ /** LRU row cap; defaults to 500. Must be a positive integer. The oldest rows by `ts` are pruned on `put` once exceeded. */
502
1040
  maxEntries?: number;
503
1041
  /** Object-store name; defaults to `"query-cache"`. */
504
1042
  storeName?: string;
505
1043
  }
506
1044
  /**
507
- * IndexedDB-backed {@link QueryCacheAdapter}. Each query is stored under its
508
- * composite key (`functionPath::argsKey::shardKey`) with a `ts` index driving
509
- * LRU eviction. The store handle is opened lazily and cached, so repeated ops
510
- * reuse one connection.
511
- *
512
- * The store lives in its own `lunora-query-cache` database — deliberately
513
- * separate from the offline-mutation outbox's `lunora-outbox` database so the two
514
- * independently-toggleable adapters never share (and drift on) a schema version.
515
- * Throws eagerly if no `IDBFactory` is available — callers in non-browser
516
- * environments should use {@link createInMemoryQueryCache}.
517
- */
1045
+ * IndexedDB-backed {@link QueryCacheAdapter}. Each query is stored under its
1046
+ * composite key (`functionPath::argsKey::shardKey`) with a `ts` index driving
1047
+ * LRU eviction. The store handle is opened lazily and cached, so repeated ops
1048
+ * reuse one connection.
1049
+ *
1050
+ * The store lives in its own `lunora-query-cache` database — deliberately
1051
+ * separate from the offline-mutation outbox's `lunora-outbox` database so the two
1052
+ * independently-toggleable adapters never share (and drift on) a schema version.
1053
+ * Throws eagerly if no `IDBFactory` is available — callers in non-browser
1054
+ * environments should use {@link createInMemoryQueryCache}.
1055
+ */
518
1056
  declare const createIndexedDbQueryCache: (options?: IndexedDbQueryCacheOptions) => QueryCacheAdapter;
519
1057
  /**
520
- * Resolve the effective read-cache from the user option, defaulting to a durable
521
- * IndexedDB store when the environment supports one. Same tri-state semantics as
522
- * `resolvePersistenceAdapter` in `./persistence`:
523
- *
524
- * - an explicit adapter is used as-is;
525
- * - `false` opts out — reads stay in memory only;
526
- * - `undefined` (the default) auto-probes IndexedDB (browsers), else `undefined`.
527
- */
528
- /**
529
- * Exponential backoff calculator with optional jitter.
530
- *
531
- * `next()` doubles the delay each call up to `maxDelayMs`. When `jitter` is
532
- * enabled the returned value is randomized in `[delay/2, delay]` so a fleet
533
- * of clients reconnecting at the same time spread out their retries.
534
- */
535
- interface ReconnectCalculator {
536
- /** Returns the delay to wait before the next reconnect attempt. */
537
- next: () => number;
538
- /** Resets the backoff to the initial delay (call on successful reconnect). */
539
- reset: () => void;
540
- }
541
- declare const createReconnect: (options?: ReconnectOptions, random?: () => number) => ReconnectCalculator;
542
- /**
543
- * Capture a snapshot of the current live query value at call time and produce a
544
- * `() => boolean` precondition that compares it against the value at replay time.
545
- *
546
- * When the precondition is checked (on queue drain / reconnect) it re-reads the
547
- * query's current value via {@link LunoraClient.peekActiveQueryValue}. If the
548
- * value differs from what was captured at call time the precondition returns
549
- * `false` and the offline mutation is dropped as stale.
550
- * @example
551
- * ```ts
552
- * client.mutation(api.todos.update, { id, text }, {
553
- * precondition: createSnapshotPrecondition(client, api.todos.list, { userId }),
554
- * });
555
- * ```
556
- */
1058
+ * Capture a snapshot of the current live query value at call time and produce a
1059
+ * `() => boolean` precondition that compares it against the value at replay time.
1060
+ *
1061
+ * When the precondition is checked (on queue drain / reconnect) it re-reads the
1062
+ * query's current state via {@link LunoraClient.peekActiveQuerySnapshot}. The
1063
+ * comparison only runs when a live subscription backed **both** reads; if either
1064
+ * read found no active subscription (e.g. the originating component unmounted
1065
+ * before replay), the precondition returns `true` (not stale) because absence of
1066
+ * a subscription is not evidence the value changed — the read simply cannot see
1067
+ * it. When both reads did have a live subscription and the value differs, the
1068
+ * precondition returns `false` and the offline mutation is dropped as stale.
1069
+ * @example
1070
+ * ```ts
1071
+ * client.mutation(api.todos.update, { id, text }, {
1072
+ * precondition: createSnapshotPrecondition(client, api.todos.list, { userId }),
1073
+ * });
1074
+ * ```
1075
+ */
557
1076
  declare const createSnapshotPrecondition: (client: LunoraClient, functionRef: FunctionReference, args: Record<string, unknown>, shardKey?: string) => (() => boolean);
558
- /**
559
- * Client-side service-worker registration and lifecycle management.
560
- *
561
- * Usage:
562
- * ```ts
563
- * const sw = new ClientServiceWorker({ swUrl: "/sw.js" });
564
- * await sw.register();
565
- *
566
- * if (sw.active) {
567
- * sw.postMessage({ type: "sync" });
568
- * }
569
- * ```
570
- */
571
- type ServiceWorkerStatus = "unsupported" | "unregistered" | "registering" | "active" | "error";
572
- interface ClientSwOptions {
573
- /** Called when the status changes. */
574
- onStatusChange?: (status: ServiceWorkerStatus) => void;
575
- /** Optional registration scope. Defaults to `/`. */
576
- scope?: string;
577
- /** URL of the service worker script (relative to origin). */
578
- swUrl: string;
579
- }
580
- /**
581
- * Manages service-worker registration and provides a simple API for
582
- * sending messages and listening for responses.
583
- */
584
- declare class ClientServiceWorker {
585
- #private;
586
- readonly swUrl: string;
587
- readonly scope: string;
588
- constructor(options: ClientSwOptions);
589
- /** Current registration status. */
590
- get status(): ServiceWorkerStatus;
591
- /** The underlying `ServiceWorkerRegistration`, if registered. */
592
- get registration(): ServiceWorkerRegistration | undefined;
593
- /** Whether the SW is currently controlling this page. */
594
- get active(): boolean;
595
- /**
596
- * Register the service worker.
597
- *
598
- * Returns `false` when the browser does not support service workers.
599
- */
600
- register(): Promise<boolean>;
601
- /**
602
- * Remove the active service worker registration and clear listeners.
603
- *
604
- * Returns `false` when no registration is currently held.
605
- */
606
- unregister(): Promise<boolean>;
607
- /**
608
- * Send a message to the active service worker.
609
- */
610
- postMessage(message: unknown): void;
611
- /**
612
- * Register a handler for messages **from** the service worker.
613
- * @returns Unsubscribe function.
614
- */
615
- onMessage(handler: (event: MessageEvent) => void): () => void;
616
- }
617
- /**
618
- * Outbound message from the client to the SW.
619
- */
620
- interface ClientToSwMessage {
621
- /** Opaque correlation ID for request/response patterns. */
622
- correlationId?: string;
623
- payload?: unknown;
624
- type: string;
625
- }
626
- /**
627
- * Inbound message from the SW to the client.
628
- */
629
- interface SwToClientMessage {
630
- /** Echoes the correlation ID from the client request, if any. */
631
- correlationId?: string;
632
- payload?: unknown;
633
- type: string;
634
- }
635
- /**
636
- * Send a typed message to the service worker and optionally await a
637
- * matching response.
638
- * @returns A promise that resolves when the SW sends a reply with the
639
- * same `correlationId` (if `expectResponse` is true).
640
- */
641
- declare const sendToSw: (sw: ServiceWorker | null, message: ClientToSwMessage, expectResponse?: boolean) => Promise<unknown>;
642
- /**
643
- * Create a reply for a client message (call from inside the SW).
644
- */
645
- declare const createReply: (original: ClientToSwMessage, payload?: unknown) => SwToClientMessage;
646
- export { type ArgsOf, type AsyncStorageLike, type AsyncStoragePersistenceOptions, type BookmarkStorage, ClientServiceWorker, type ClientSwOptions, type ClientToSwMessage, type FunctionReference, type HttpStreamArgsOf, type HttpStreamChunkOf, type HttpStreamOptions, type HttpStreamRef, type IndexedDbPersistenceOptions, type IndexedDbQueryCacheOptions, LunoraClient, type MutationCallOptions, type MutationDelta, type MutationRunnerSinks, type MutatorHandle, type MutatorRunnerSinks, type MutatorTransaction, OfflineQueue, type OfflineQueueOptions, type PersistenceAdapter, type QueryCacheAdapter, type QueuedMutation, type ReconnectCalculator, type ReconnectOptions, type ReturnOf, type ServiceWorkerStatus, type StreamIterable, type SubscriptionError, type SwToClientMessage, TabCoordinator, applyDelta, createAsyncStoragePersistence, createInMemoryBookmarkStorage, createInMemoryPersistence, createInMemoryQueryCache, createIndexedDbPersistence, createIndexedDbQueryCache, createMutationRunner, createMutatorRunner, createReconnect, createReply, createSnapshotPrecondition, httpStream, isMutationDelta, queryCacheKey, sendToSw };
1077
+ export { type AsyncStorageLike, type AsyncStoragePersistenceOptions, type AsyncStorageQueryCacheOptions, type BookmarkStorage, CONFLICT_ERROR_CODE, type ConnectLiveStreamOptions, type ConnectionStatus, type FunctionReference, type HttpStreamArgsOf, type HttpStreamChunkOf, type HttpStreamOptions, type HttpStreamRef, type IndexedDbPersistenceOptions, type IndexedDbQueryCacheOptions, type LiveStream, type LiveStreamEvent, type LiveStreamSubscribeError, type LiveStreamSubscribeOptions, type LiveTopicOptions, type LiveTopicRef, LunoraClient, type MutationDelta, type MutatorHandle, type MutatorRunnerSinks, type MutatorTransaction, OfflineQueue, type OfflineQueueOptions, type OptimisticMessage, type PersistenceAdapter, type QueryCacheAdapter, type QueuedMutation, RETIRE_AFTER_DURABLE_SEQ_ADVANCE, type ReconcileDurableMessage, type ReconnectCalculator, type ReconnectOptions, type StreamIterable, type SubscribeLiveTopicOptions, type SubscriptionError, TabCoordinator, TransportError, applyDelta, connectLiveStream, createAsyncStoragePersistence, createAsyncStorageQueryCache, createCallRunner, createInMemoryBookmarkStorage, createInMemoryPersistence, createInMemoryQueryCache, createIndexedDbPersistence, createIndexedDbQueryCache, createMutatorRunner, createReconnect, createSnapshotPrecondition, getErrorCode, getRetryAfterMs, httpStream, isAuthReplayFailure, isConflictError, isForbiddenError, isMutationDelta, isRateLimitedError, isUnauthorizedError, liveTopicSubscription, maxSeq, queryCacheKey, reconcileOptimistic, subscribeLiveTopic };