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