@lunora/client 1.0.0-alpha.17 → 1.0.0-alpha.171

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