@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
|
@@ -0,0 +1,3491 @@
|
|
|
1
|
+
import { F as FunctionReference, A as ArgsOf, R as ReturnOf } from "./function-reference.d-Br_hsKje.mjs";
|
|
2
|
+
import { LunoraErrorCode } from '@lunora/errors';
|
|
3
|
+
import { CronJobInfo, VectorIndexSummary, VectorQueryMatch, PipelineLogQuery, PipelineLogPage, KvNamespaceSummary, KvKeyListResult, KvValueResult, AuthUser, AuthPage, AuthImpersonation, AuthCapabilities, AuthConfigInfo, AuthSession } from '@lunora/runtime';
|
|
4
|
+
/**
|
|
5
|
+
* Reactive key-value store for local-only client state.
|
|
6
|
+
*
|
|
7
|
+
* Unlike a server {@link SubscriptionState} (which tracks a live WS connection,
|
|
8
|
+
* an `acked` flag, `serverBase`, optimistic layers, and the full subscription
|
|
9
|
+
* machinery), a `ClientQueryRef` is purely local — no server round-trip, no
|
|
10
|
+
* WebSocket, no persistence. It exists so framework adapters can offer a
|
|
11
|
+
* `useClientQuery` hook whose values survive component remounts and are shared
|
|
12
|
+
* across every consumer of the same ref, with none of the ceremony or coupling
|
|
13
|
+
* of a dedicated context provider.
|
|
14
|
+
*
|
|
15
|
+
* The store lives inside `LunoraClient` (a private field) and is surfaced through
|
|
16
|
+
* `client.getClientQuery(ref)` / `setClientQuery(ref, value)` /
|
|
17
|
+
* `subscribeClientQuery(ref, callback)`.
|
|
18
|
+
*/
|
|
19
|
+
/** Opaque handle for a typed client-local query slot. */
|
|
20
|
+
interface ClientQueryRef<T = unknown> {
|
|
21
|
+
/** Default value when no value has been set explicitly. */
|
|
22
|
+
readonly defaultValue: T;
|
|
23
|
+
/** Stable identity for the slot. Must be unique within a client instance. */
|
|
24
|
+
readonly key: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create a typed {@link ClientQueryRef}. Call once per slot at module scope
|
|
28
|
+
* (or inside a component module) — the ref object is the stable identity.
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* // lunora/client-queries.ts
|
|
32
|
+
* import { createClientQuery } from "@lunora/client";
|
|
33
|
+
*
|
|
34
|
+
* export const sidebarOpen = createClientQuery("sidebarOpen", true);
|
|
35
|
+
* export const selectedMessageId = createClientQuery("selectedMessageId", undefined as string | undefined);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare const createClientQuery: <T>(key: string, defaultValue: T) => ClientQueryRef<T>;
|
|
39
|
+
/**
|
|
40
|
+
* Typed reference to an HTTP-SSE stream route (`httpRoute.<verb>(path).stream()`)
|
|
41
|
+
* emitted by `@lunora/codegen` as `httpStreams.<namespace>.<name>`.
|
|
42
|
+
*
|
|
43
|
+
* Distinct from {@link FunctionReference}: this is the **HTTP-SSE route stream**
|
|
44
|
+
* (opened with `fetch` + `ReadableStream` against the route's own URL), not the
|
|
45
|
+
* WS procedure stream (`kind: "stream"`). At runtime it carries the HTTP verb
|
|
46
|
+
* and the route path; the phantom marker carries the chunk / searchParams /
|
|
47
|
+
* params types so `httpStream` (and the framework hooks over it) infer the
|
|
48
|
+
* chunk type end-to-end.
|
|
49
|
+
* @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
|
|
50
|
+
*/
|
|
51
|
+
interface HttpStreamRef<Chunk = unknown, SearchParams = unknown, Params = unknown> {
|
|
52
|
+
/**
|
|
53
|
+
* Phantom marker carrying the `Chunk`/`SearchParams`/`Params` type
|
|
54
|
+
* parameters for inference. Never present at runtime; declared in a
|
|
55
|
+
* covariant (output) position so a concrete reference stays assignable to
|
|
56
|
+
* a widened one.
|
|
57
|
+
*/
|
|
58
|
+
readonly __lunoraHttpStream?: {
|
|
59
|
+
chunk: Chunk;
|
|
60
|
+
params: Params;
|
|
61
|
+
searchParams: SearchParams;
|
|
62
|
+
};
|
|
63
|
+
/** HTTP verb the route binds to (uppercased), e.g. `"GET"`. */
|
|
64
|
+
readonly method: string;
|
|
65
|
+
/** The route path as declared, e.g. `/api/tokens/:id` — `:name` segments are filled from `params`. */
|
|
66
|
+
readonly path: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The call-side args of an HTTP-SSE stream route: `:name` path params plus URL query params.
|
|
70
|
+
* @experimental Part of the HTTP-SSE stream surface.
|
|
71
|
+
*/
|
|
72
|
+
interface HttpStreamCallArgs<SearchParams = unknown, Params = unknown> {
|
|
73
|
+
/** Values for the route path's `:name` segments. */
|
|
74
|
+
params?: Params;
|
|
75
|
+
/** URL query params, appended to the request URL (undefined entries are skipped). */
|
|
76
|
+
searchParams?: SearchParams;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Extract the chunk type from a {@link HttpStreamRef}.
|
|
80
|
+
* @experimental Part of the HTTP-SSE stream surface.
|
|
81
|
+
*/
|
|
82
|
+
type HttpStreamChunkOf<R> = R extends HttpStreamRef<infer Chunk, infer _S, infer _P> ? Chunk : never;
|
|
83
|
+
/**
|
|
84
|
+
* Extract the call-side args type from a {@link HttpStreamRef}.
|
|
85
|
+
* @experimental Part of the HTTP-SSE stream surface.
|
|
86
|
+
*/
|
|
87
|
+
type HttpStreamArgsOf<R> = R extends HttpStreamRef<infer _C, infer S, infer P> ? HttpStreamCallArgs<S, P> : never;
|
|
88
|
+
type Unsubscribe = () => void;
|
|
89
|
+
/**
|
|
90
|
+
* Serializable result of `preloadQuery`. Produced on the server during SSR,
|
|
91
|
+
* embedded in the rendered HTML, then handed to `usePreloadedQuery` on the
|
|
92
|
+
* client so the first render shows the server value with no loading flash
|
|
93
|
+
* before a live subscription attaches. Every field survives `JSON.stringify`.
|
|
94
|
+
*/
|
|
95
|
+
interface Preloaded<T = unknown> {
|
|
96
|
+
readonly __lunoraPreloaded: true;
|
|
97
|
+
readonly args: Record<string, unknown>;
|
|
98
|
+
readonly functionPath: string;
|
|
99
|
+
readonly shardKey?: string;
|
|
100
|
+
readonly value: T;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Pluggable storage for the `x-d1-bookmark` value used to provide
|
|
104
|
+
* read-your-writes between a mutation and subsequent queries.
|
|
105
|
+
*/
|
|
106
|
+
interface BookmarkStorage {
|
|
107
|
+
get: () => string | null;
|
|
108
|
+
set: (value: string | null) => void;
|
|
109
|
+
}
|
|
110
|
+
interface ReconnectOptions {
|
|
111
|
+
initialDelayMs?: number;
|
|
112
|
+
jitter?: boolean;
|
|
113
|
+
maxDelayMs?: number;
|
|
114
|
+
}
|
|
115
|
+
/** Which durable-storage operation failed, passed to {@link OfflineQueueOptions.onPersistenceError}. */
|
|
116
|
+
type PersistenceOperation = "append" | "clear" | "load" | "remove";
|
|
117
|
+
/** Context handed to a persistence-error handler. */
|
|
118
|
+
interface PersistenceErrorContext {
|
|
119
|
+
readonly error: unknown;
|
|
120
|
+
/** The mutation id involved, when the failing op was scoped to one (`append`/`remove`). */
|
|
121
|
+
readonly mutationId?: string;
|
|
122
|
+
readonly operation: PersistenceOperation;
|
|
123
|
+
}
|
|
124
|
+
interface OfflineQueueOptions {
|
|
125
|
+
maxItems?: number;
|
|
126
|
+
/**
|
|
127
|
+
* Invoked when a {@link PersistenceAdapter} call rejects (e.g. IndexedDB quota
|
|
128
|
+
* exceeded). Without a handler, failures are logged via `console.warn` so they
|
|
129
|
+
* are never fully silent. Note: a failed `append` means the write is queued in
|
|
130
|
+
* memory but NOT durable — it will not survive a reload.
|
|
131
|
+
*/
|
|
132
|
+
onPersistenceError?: (context: PersistenceErrorContext) => void;
|
|
133
|
+
/**
|
|
134
|
+
* Queue mutations issued before a shard's first successful WebSocket
|
|
135
|
+
* connect (defaults to `false`). The standard behaviour (`LunoraClient`'s
|
|
136
|
+
* `mutation()`) queues only when the targeted shard has been connected at
|
|
137
|
+
* least once (`wasEverConnected`), so the registry / resubscribe handshake
|
|
138
|
+
* has run. Set this to `true` for offline-first apps that want to enqueue
|
|
139
|
+
* writes on the very first session before the WS is up.
|
|
140
|
+
*/
|
|
141
|
+
queueBeforeFirstConnect?: boolean;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Serializable shape of an offline mutation, durably stored by a
|
|
145
|
+
* {@link PersistenceAdapter} so queued writes survive a reload/crash. The live
|
|
146
|
+
* `resolve`/`reject` callbacks of an in-flight `QueuedMutation` are *not*
|
|
147
|
+
* persisted — a restored mutation is replayed with no original awaiter.
|
|
148
|
+
*/
|
|
149
|
+
interface PersistedMutation {
|
|
150
|
+
args: Record<string, unknown>;
|
|
151
|
+
/**
|
|
152
|
+
* The client id that queued this write, persisted so a replay after a reload
|
|
153
|
+
* lands in the SAME server-side dedup namespace it was issued under. The
|
|
154
|
+
* standalone client's own `clientId` is minted per session, so replaying under
|
|
155
|
+
* the live one would miss the `__idempotency` row for an anonymous caller and
|
|
156
|
+
* re-run a write the server already committed. Absent on records written by
|
|
157
|
+
* older client versions, which replay under the live id.
|
|
158
|
+
*/
|
|
159
|
+
clientId?: string;
|
|
160
|
+
functionPath: string;
|
|
161
|
+
id: string;
|
|
162
|
+
/**
|
|
163
|
+
* Issuing identity fingerprint, persisted so a hydrated write replays only
|
|
164
|
+
* under the identity that queued it (`null` = queued while signed out).
|
|
165
|
+
* Absent on records written by older client versions, which replay under
|
|
166
|
+
* the ambient identity for back-compat.
|
|
167
|
+
*/
|
|
168
|
+
identity?: string | null;
|
|
169
|
+
shardKey?: string;
|
|
170
|
+
/**
|
|
171
|
+
* App/schema version stamped at enqueue (from `LunoraClientOptions.persistenceVersion`).
|
|
172
|
+
* On hydrate, a record whose `version` doesn't match the current one is dropped
|
|
173
|
+
* and purged rather than replayed — so a write persisted by an older deploy
|
|
174
|
+
* (with a now-changed function signature) can't replay against the new schema.
|
|
175
|
+
* Absent when no `persistenceVersion` is configured (no version gating).
|
|
176
|
+
*/
|
|
177
|
+
version?: string;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Durable store for the offline mutation queue. The default client keeps the
|
|
181
|
+
* queue in memory; supplying an adapter (e.g. `createIndexedDbPersistence`)
|
|
182
|
+
* makes queued writes survive a page reload. Implementations must preserve FIFO
|
|
183
|
+
* (enqueue) order in `PersistenceAdapter.load`.
|
|
184
|
+
*
|
|
185
|
+
* Replay semantics are at-least-once: a mutation is removed only after the
|
|
186
|
+
* server confirms (or rejects) it, so a crash between commit and `remove` can
|
|
187
|
+
* replay it again on the next load.
|
|
188
|
+
*/
|
|
189
|
+
interface PersistenceAdapter {
|
|
190
|
+
/** Append a mutation to durable storage (called on enqueue). */
|
|
191
|
+
append: (mutation: PersistedMutation) => Promise<void>;
|
|
192
|
+
/** Drop every persisted mutation (e.g. on logout). */
|
|
193
|
+
clear: () => Promise<void>;
|
|
194
|
+
/** Load all persisted mutations in FIFO order — called once at startup. */
|
|
195
|
+
load: () => Promise<PersistedMutation[]>;
|
|
196
|
+
/** Remove a mutation by id once it has been replayed (resolved or rejected). */
|
|
197
|
+
remove: (id: string) => Promise<void>;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* One write handed to an {@link OutboxSink}. Mirrors {@link PersistedMutation}
|
|
201
|
+
* plus the custom-mutator identity (`clientId`/`mutationId`/`idempotencyKey`)
|
|
202
|
+
* the durable outbox needs to dedupe and watermark replays.
|
|
203
|
+
*/
|
|
204
|
+
interface OutboxMutation {
|
|
205
|
+
args: Record<string, unknown>;
|
|
206
|
+
/** Stable per-client id; pairs with {@link OutboxMutation.mutationId} as `idempotencyKey`. */
|
|
207
|
+
clientId: string;
|
|
208
|
+
functionPath: string;
|
|
209
|
+
/** `${clientId}:${mutationId}` — sent as `x-lunora-mutation-id` so a replay is server-idempotent. */
|
|
210
|
+
idempotencyKey: string;
|
|
211
|
+
/** Issuing identity fingerprint (`null` = signed out); drives the sink's identity guard. */
|
|
212
|
+
identity: string | null;
|
|
213
|
+
/** Monotonic per-client mutation id, backing the server `__client_watermark`. */
|
|
214
|
+
mutationId: number;
|
|
215
|
+
shardKey?: string;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Pluggable durable outbox seam. When set on {@link LunoraClientOptions.outbox},
|
|
219
|
+
* the client delegates offline write durability + at-least-once replay to this
|
|
220
|
+
* sink instead of its built-in {@link PersistenceAdapter}-backed `OfflineQueue`.
|
|
221
|
+
* `@lunora/db` supplies the blessed implementation (`createExecutorOutboxSink`,
|
|
222
|
+
* backed by the TanStack `OfflineExecutor`); the interface itself is
|
|
223
|
+
* dependency-free so `@lunora/client` stays TanStack-free.
|
|
224
|
+
*/
|
|
225
|
+
interface OutboxSink {
|
|
226
|
+
/**
|
|
227
|
+
* Persist and schedule a write for replay. Rejects with an
|
|
228
|
+
* `OFFLINE_QUEUE_OVERFLOW`-coded error when the sink's cap is exceeded, so
|
|
229
|
+
* the caller can surface back-pressure to the issuing mutation.
|
|
230
|
+
*/
|
|
231
|
+
enqueue: (mutation: OutboxMutation) => Promise<void>;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* One persisted query result in the durable read cache (Pillar 2). Keyed in the
|
|
235
|
+
* store by `shardKey + functionPath + argsKey`; the record carries everything
|
|
236
|
+
* needed to render offline on reload and to resume the live subscription.
|
|
237
|
+
*/
|
|
238
|
+
interface CachedQuery {
|
|
239
|
+
/**
|
|
240
|
+
* Issuing identity fingerprint (same shape the offline queue stamps). A
|
|
241
|
+
* cached value only hydrates when it matches the current identity, so a
|
|
242
|
+
* signed-out cache never leaks into a new session. `null` = cached while
|
|
243
|
+
* signed out.
|
|
244
|
+
*/
|
|
245
|
+
identity: string | null;
|
|
246
|
+
/**
|
|
247
|
+
* The `cursor` high-watermark this value reflects, replayed as `sinceSeq`
|
|
248
|
+
* on reconnect so the server can resume instead of re-snapshotting. Absent
|
|
249
|
+
* when the value predates CDC / no cursor was advertised.
|
|
250
|
+
*/
|
|
251
|
+
serverCursor?: number;
|
|
252
|
+
/**
|
|
253
|
+
* The CDC `epoch` the `serverCursor` belongs to, replayed as `sinceEpoch`
|
|
254
|
+
* on reconnect so the server only resumes when the client is still on the
|
|
255
|
+
* same changelog timeline. Absent when no epoch was advertised.
|
|
256
|
+
*/
|
|
257
|
+
serverEpoch?: string;
|
|
258
|
+
/** Wall-clock millis the value was written — drives LRU eviction. */
|
|
259
|
+
ts: number;
|
|
260
|
+
/** The full query result last seen from the server. */
|
|
261
|
+
value: unknown;
|
|
262
|
+
/**
|
|
263
|
+
* App/schema version stamped when persisted (from `LunoraClientOptions.persistenceVersion`).
|
|
264
|
+
* A cached value whose `version` doesn't match the current one is not hydrated —
|
|
265
|
+
* so a result of a now-changed shape from an older deploy can't render. Absent
|
|
266
|
+
* when no `persistenceVersion` is configured (no version gating).
|
|
267
|
+
*/
|
|
268
|
+
version?: string;
|
|
269
|
+
}
|
|
270
|
+
/** A stored read-cache row: the {@link CachedQuery} plus the key it is stored under. */
|
|
271
|
+
interface StoredQuery extends CachedQuery {
|
|
272
|
+
key: string;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Durable store for the client read cache (Pillar 2): query results survive a
|
|
276
|
+
* reload so reads hydrate from disk and render immediately while the socket
|
|
277
|
+
* reconnects. Opt-in via {@link LunoraClientOptions.queryCache}; omit to keep
|
|
278
|
+
* reads in memory only (today's behaviour). Mirrors {@link PersistenceAdapter}'s
|
|
279
|
+
* shape over the same IndexedDB plumbing.
|
|
280
|
+
*/
|
|
281
|
+
interface QueryCacheAdapter {
|
|
282
|
+
/** Drop every cached query (e.g. on logout / identity change). */
|
|
283
|
+
clear: () => Promise<void>;
|
|
284
|
+
/** Load every cached query — called once at startup to hydrate reads. */
|
|
285
|
+
load: () => Promise<StoredQuery[]>;
|
|
286
|
+
/** Upsert one cached query by key (called when a subscription value advances). */
|
|
287
|
+
put: (key: string, entry: CachedQuery) => Promise<void>;
|
|
288
|
+
/** Remove one cached query by key. */
|
|
289
|
+
remove: (key: string) => Promise<void>;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Resolves the WS `?token=` credential fresh at every (re)connect — the channel
|
|
293
|
+
* for short-lived tokens (e.g. the ephemeral admin sub-token the worker mints
|
|
294
|
+
* at `POST /_lunora/admin/ws-token`) instead of a static secret in the URL.
|
|
295
|
+
* May return the token synchronously or as a Promise; returning `undefined`
|
|
296
|
+
* connects without a token. A thrown error / rejected Promise fails that
|
|
297
|
+
* connect attempt, and the client retries with its normal reconnect backoff.
|
|
298
|
+
*/
|
|
299
|
+
type WsTokenProvider = () => Promise<string | undefined> | string | undefined;
|
|
300
|
+
interface LunoraClientOptions {
|
|
301
|
+
/**
|
|
302
|
+
* Base path the worker mounts better-auth at, used by the client's
|
|
303
|
+
* `getCurrentUser()` to reach the `get-session` route. Defaults to
|
|
304
|
+
* `/api/auth` (matching `@lunora/auth`'s `DEFAULT_AUTH_BASE_PATH`).
|
|
305
|
+
*/
|
|
306
|
+
authBasePath?: string;
|
|
307
|
+
bookmarkStorage?: BookmarkStorage;
|
|
308
|
+
/**
|
|
309
|
+
* Stable per-client id backing the custom-mutator watermark. Sent on the
|
|
310
|
+
* `connect` envelope (so the server can scope this client's
|
|
311
|
+
* `__client_watermark`) and stamped onto every {@link OutboxMutation} the
|
|
312
|
+
* {@link LunoraClientOptions.outbox} sink persists, where it pairs with the
|
|
313
|
+
* monotonic mutation id to form the idempotency key. The `@lunora/db` path
|
|
314
|
+
* persists a stable id alongside the outbox and passes it here; omit for the
|
|
315
|
+
* standalone client, which generates an ephemeral per-session id.
|
|
316
|
+
*/
|
|
317
|
+
clientId?: string;
|
|
318
|
+
/**
|
|
319
|
+
* Default app context sent in the `connect` envelope right after each socket
|
|
320
|
+
* opens, forwarded to the server's `onConnect`/`onDisconnect` lifecycle hooks
|
|
321
|
+
* as `event.context`. A per-shard context registered via
|
|
322
|
+
* `setConnectionContext` overrides this for that shard. Omit when no lifecycle
|
|
323
|
+
* hook needs connection context.
|
|
324
|
+
*/
|
|
325
|
+
connectionContext?: Record<string, unknown>;
|
|
326
|
+
/**
|
|
327
|
+
* Fail-fast timeout (ms) for opening a subscription WebSocket. If the
|
|
328
|
+
* handshake doesn't complete within this window — a hung dev proxy or a cold
|
|
329
|
+
* worker that never upgrades — the client force-closes the socket and routes
|
|
330
|
+
* through its normal reconnect/backoff (surfacing `offline` status) instead
|
|
331
|
+
* of leaving the live channel silently stuck on the browser's much longer
|
|
332
|
+
* default. Does not affect HTTP queries/mutations (those never ride the WS).
|
|
333
|
+
* Defaults to 10000 (10s); set to `0` (or negative) to disable.
|
|
334
|
+
*/
|
|
335
|
+
connectTimeoutMs?: number;
|
|
336
|
+
/**
|
|
337
|
+
* When `true`, tabs sharing the same origin (and the same signed-in identity)
|
|
338
|
+
* coordinate via BroadcastChannel so only one tab — the "leader" — opens
|
|
339
|
+
* WebSocket connections to the server. Reduces simultaneous WS connections,
|
|
340
|
+
* bandwidth, and cross-tab state drift. Requires `BroadcastChannel`
|
|
341
|
+
* (browser-only); silently ignored otherwise. Defaults to `false`.
|
|
342
|
+
*
|
|
343
|
+
* **The channel is one-directional: leader → follower.** The leader
|
|
344
|
+
* broadcasts the values, errors, checkpoints and connection status of the
|
|
345
|
+
* subscriptions *it* holds; there is no frame with which a follower can ask
|
|
346
|
+
* the leader for anything.
|
|
347
|
+
*
|
|
348
|
+
* `subscribe` works on a follower and is how the relay delivers: the
|
|
349
|
+
* registration is what the leader's broadcast key is matched against, so a
|
|
350
|
+
* follower sees a value while the leader independently holds the same
|
|
351
|
+
* `(fn, args, shardKey)`.
|
|
352
|
+
*
|
|
353
|
+
* Nothing else is served, because the leader broadcasts nothing for it and a
|
|
354
|
+
* follower cannot ask. `subscribeShape` and `acquireConnectionContext` are
|
|
355
|
+
* inert on a follower — framework code (`@lunora/db`'s shape sync, every
|
|
356
|
+
* `usePresence` adapter) calls them from an effect the app cannot opt out
|
|
357
|
+
* of, so throwing would unwind the tab rather than degrade one feature.
|
|
358
|
+
* `whisper`, `whisperSubscribe`, `setConnectionContext` and `stream` are
|
|
359
|
+
* only ever called by app code, which can handle a failure, so those throw
|
|
360
|
+
* `NOT_IMPLEMENTED` rather than returning a handle that never fires. (The
|
|
361
|
+
* brief window every tab spends claiming leadership at startup is not a
|
|
362
|
+
* follower state: a lone tab self-promotes and its registered subscriptions
|
|
363
|
+
* are sent then.) HTTP surfaces — `query`, `mutation`, `action`, the offline
|
|
364
|
+
* queue's replay — are unaffected on every tab.
|
|
365
|
+
*
|
|
366
|
+
* So: enable this when your tabs run the SAME app views over plain
|
|
367
|
+
* `subscribe`, and leave it off if tabs can sit on different routes or you
|
|
368
|
+
* use shapes, whispers, streams, or connection context.
|
|
369
|
+
*/
|
|
370
|
+
crossTabSync?: boolean;
|
|
371
|
+
fetch?: typeof fetch;
|
|
372
|
+
/**
|
|
373
|
+
* Interval (ms) between keepalive pings sent on each open subscription
|
|
374
|
+
* socket. The server answers them via the Durable Object's hibernation
|
|
375
|
+
* auto-response WITHOUT waking the DO, so an idle socket stays alive across
|
|
376
|
+
* hibernation without a billable wakeup. Defaults to 30000 (30s); set to
|
|
377
|
+
* `0` (or a negative value) to disable the heartbeat entirely.
|
|
378
|
+
*/
|
|
379
|
+
heartbeatIntervalMs?: number;
|
|
380
|
+
/**
|
|
381
|
+
* When `true` and a `queryCache` is active, framework hooks (React, Vue, …)
|
|
382
|
+
* wait for the durable cache to finish hydrating before their first render
|
|
383
|
+
* with an enabled subscription, so users see cached data instead of an
|
|
384
|
+
* undefined flash before the socket round-trip. Defaults to `false`.
|
|
385
|
+
*
|
|
386
|
+
* Requires `queryCache` to be set (not `false`); silently ignored otherwise.
|
|
387
|
+
*/
|
|
388
|
+
hydrateOnStart?: boolean;
|
|
389
|
+
offlineQueue?: OfflineQueueOptions;
|
|
390
|
+
/**
|
|
391
|
+
* Durable outbox seam for offline writes. When supplied (the `@lunora/db`
|
|
392
|
+
* path wires `createExecutorOutboxSink`), offline mutations are delegated to
|
|
393
|
+
* the sink and the built-in {@link PersistenceAdapter}-backed `OfflineQueue`
|
|
394
|
+
* is bypassed, so a db app has exactly one durable write path. Omit for the
|
|
395
|
+
* standalone client, which keeps using {@link LunoraClientOptions.persistence}.
|
|
396
|
+
*/
|
|
397
|
+
outbox?: OutboxSink;
|
|
398
|
+
/**
|
|
399
|
+
* Durable store for the offline mutation queue. Tri-state — an explicit
|
|
400
|
+
* {@link PersistenceAdapter} is used as-is; `false` opts out (the queue stays
|
|
401
|
+
* in memory, lost on reload); omitted (the default) auto-probes a durable
|
|
402
|
+
* IndexedDB store when the `indexedDB` global is present (browsers), otherwise
|
|
403
|
+
* in-memory, so SSR/Node/React-Native keep the in-memory behaviour and only
|
|
404
|
+
* environments that can persist do. Pass `createAsyncStoragePersistence()` on
|
|
405
|
+
* React Native.
|
|
406
|
+
*/
|
|
407
|
+
persistence?: false | PersistenceAdapter;
|
|
408
|
+
/**
|
|
409
|
+
* App/schema version stamped onto every persisted queued write and cached
|
|
410
|
+
* read. Bump it on a breaking change to a function signature or query shape:
|
|
411
|
+
* on the next boot, persisted writes / cached reads stamped with a different
|
|
412
|
+
* version are dropped (and purged) rather than replayed / hydrated against the
|
|
413
|
+
* new schema. Omit to disable version gating (records are never invalidated by
|
|
414
|
+
* version).
|
|
415
|
+
*
|
|
416
|
+
* **Adoption is itself an invalidation event:** records written before you set
|
|
417
|
+
* `persistenceVersion` carry no version, so the first boot after enabling it
|
|
418
|
+
* purges all currently-queued offline writes (and cached reads) as stale. Adopt
|
|
419
|
+
* it on a build where that clean slate is acceptable — typically the same
|
|
420
|
+
* breaking deploy you're protecting against — not purely speculatively.
|
|
421
|
+
*/
|
|
422
|
+
persistenceVersion?: string;
|
|
423
|
+
/**
|
|
424
|
+
* Durable store for the read cache (Pillar 2). When active, query results
|
|
425
|
+
* are persisted as their subscriptions advance and hydrated on construction
|
|
426
|
+
* so a reload renders cached data before the socket reconnects, then resumes
|
|
427
|
+
* the live subscription from the persisted cursor. Tri-state — an explicit
|
|
428
|
+
* {@link QueryCacheAdapter} is used as-is; `false` opts out (reads stay in
|
|
429
|
+
* memory only); omitted (the default) auto-probes IndexedDB exactly like
|
|
430
|
+
* {@link LunoraClientOptions.persistence}.
|
|
431
|
+
*/
|
|
432
|
+
queryCache?: QueryCacheAdapter | false;
|
|
433
|
+
reconnect?: ReconnectOptions;
|
|
434
|
+
url: string;
|
|
435
|
+
WebSocket?: typeof WebSocket;
|
|
436
|
+
/**
|
|
437
|
+
* Credential appended to the WebSocket URL as `?token=…`. The server matches
|
|
438
|
+
* it against `LUNORA_WS_BEARER` (to clear the upgrade gate) and/or
|
|
439
|
+
* `LUNORA_ADMIN_TOKEN` (to authorize `__lunora_admin__:*` subscriptions —
|
|
440
|
+
* what the studio supplies). Browsers can't set headers on the `WebSocket`
|
|
441
|
+
* constructor, so the query parameter is the only channel; it ends up in
|
|
442
|
+
* server logs and history, so prefer a short-lived rotating token in
|
|
443
|
+
* production over a static secret.
|
|
444
|
+
*
|
|
445
|
+
* Pass a {@link WsTokenProvider} function to resolve the token fresh at
|
|
446
|
+
* every (re)connect — the channel for short-lived credentials such as the
|
|
447
|
+
* ephemeral admin sub-token minted by `POST /_lunora/admin/ws-token`: the
|
|
448
|
+
* provider re-mints on each reconnect, including the one following a `4001`
|
|
449
|
+
* token-expired drop, so a static master token never has to ride the URL.
|
|
450
|
+
*/
|
|
451
|
+
wsToken?: string | WsTokenProvider;
|
|
452
|
+
wsUrl?: string;
|
|
453
|
+
}
|
|
454
|
+
/** Wire envelope sent on `POST /_lunora/rpc`. */
|
|
455
|
+
interface RpcEnvelope {
|
|
456
|
+
args?: Record<string, unknown>;
|
|
457
|
+
/**
|
|
458
|
+
* Stable per-client identifier (custom-mutator push path). Pairs with
|
|
459
|
+
* {@link RpcEnvelope.mutationId} to form `idempotencyKey` and scope the
|
|
460
|
+
* server `__client_watermark`. Absent on plain `client.mutation` calls.
|
|
461
|
+
*/
|
|
462
|
+
clientId?: string;
|
|
463
|
+
functionPath: string;
|
|
464
|
+
/**
|
|
465
|
+
* Idempotency key (`${clientId}:${mutationId}`) for the custom-mutator push
|
|
466
|
+
* path, mirrored into the `x-lunora-mutation-id` header. Absent on plain
|
|
467
|
+
* `client.mutation` calls.
|
|
468
|
+
*/
|
|
469
|
+
idempotencyKey?: string;
|
|
470
|
+
/**
|
|
471
|
+
* Monotonic per-client mutation id (custom-mutator push path), backing the
|
|
472
|
+
* server-side per-client watermark: `id <= watermark` is a replay (skipped),
|
|
473
|
+
* `id == watermark + 1` runs authoritatively, `id > watermark + 1` halts the
|
|
474
|
+
* batch so the client resends from `watermark + 1`. Absent on plain
|
|
475
|
+
* `client.mutation` calls.
|
|
476
|
+
*/
|
|
477
|
+
mutationId?: number;
|
|
478
|
+
shardKey?: string;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Wire response from the shard's `/rpc` endpoint (forwarded by the runtime). A
|
|
482
|
+
* watermarked custom-mutator push additionally carries `lastMutationId` — the
|
|
483
|
+
* highest per-client sequence the DO has applied — which the client uses to keep
|
|
484
|
+
* its `clientSeq` generator monotonic across reloads (see `LunoraClient.callMutator`).
|
|
485
|
+
* A plain mutation on a CDC shard carries `commitCursor` — the cursor the write
|
|
486
|
+
* committed at — which gates the drop of a per-call optimistic layer.
|
|
487
|
+
*/
|
|
488
|
+
type RpcResponseBody = {
|
|
489
|
+
error: {
|
|
490
|
+
code: string;
|
|
491
|
+
data?: unknown;
|
|
492
|
+
message: string;
|
|
493
|
+
};
|
|
494
|
+
} | {
|
|
495
|
+
commitCursor?: number;
|
|
496
|
+
lastMutationId?: number;
|
|
497
|
+
result: unknown;
|
|
498
|
+
};
|
|
499
|
+
/** Subscription protocol — client → server. */
|
|
500
|
+
interface ClientSubscribeMessage {
|
|
501
|
+
id: string;
|
|
502
|
+
/**
|
|
503
|
+
* `sinceSeq` is the persisted `cursor` high-watermark the client last saw
|
|
504
|
+
* for this shard (Pillar 1b resume). Present only when a durable
|
|
505
|
+
* {@link QueryCacheAdapter} restored a cached value with a cursor; the
|
|
506
|
+
* server replies with a lightweight `resume` frame instead of a full
|
|
507
|
+
* snapshot when nothing the query reads changed since it. Absent on a
|
|
508
|
+
* first-time subscribe.
|
|
509
|
+
*/
|
|
510
|
+
query: {
|
|
511
|
+
args?: Record<string, unknown>;
|
|
512
|
+
functionPath?: string;
|
|
513
|
+
sinceEpoch?: string;
|
|
514
|
+
sinceSeq?: number;
|
|
515
|
+
table?: string;
|
|
516
|
+
};
|
|
517
|
+
type: "subscribe";
|
|
518
|
+
}
|
|
519
|
+
interface ClientUnsubscribeMessage {
|
|
520
|
+
id: string;
|
|
521
|
+
type: "unsubscribe";
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* One-shot control frame sent right after the socket opens. Registers the
|
|
525
|
+
* connection's app `context` (e.g. `{ roomId, sessionId }`) with the server and
|
|
526
|
+
* fires the `onConnect` lifecycle hooks; the same context is replayed to
|
|
527
|
+
* `onDisconnect` when the socket drops.
|
|
528
|
+
*/
|
|
529
|
+
interface ClientConnectMessage {
|
|
530
|
+
/**
|
|
531
|
+
* Wire behaviours this client can handle that an older one cannot, so the
|
|
532
|
+
* server can use them without breaking clients that can't. Currently just
|
|
533
|
+
* `"pageDelta"`; see `shared/page-result.ts` for what it promises and why it
|
|
534
|
+
* must be announced rather than assumed. Omitting it is always safe.
|
|
535
|
+
*/
|
|
536
|
+
caps?: ReadonlyArray<string>;
|
|
537
|
+
/**
|
|
538
|
+
* Stable per-client id (persisted alongside the outbox). Lets the server
|
|
539
|
+
* scope this connection's `__client_watermark` so custom-mutator pokes can
|
|
540
|
+
* echo the right per-client `lastMutationId`. Omitted by clients that don't
|
|
541
|
+
* use custom mutators.
|
|
542
|
+
*/
|
|
543
|
+
clientId?: string;
|
|
544
|
+
context?: Record<string, unknown>;
|
|
545
|
+
id: string;
|
|
546
|
+
type: "connect";
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Subscribe to a declarative **shape** — server-side partial replication scoped
|
|
550
|
+
* by `shardBy` + the shape's predicate + RLS. The client sends the shape *name*
|
|
551
|
+
* + validated `args`; the server resolves the trusted `where` (identity/RLS
|
|
552
|
+
* `baseWhere` the client can't forge) and streams the matching rowset, then live
|
|
553
|
+
* {@link ServerPokePartMessage} diffs. `id` namespaces the subscription and is
|
|
554
|
+
* echoed as `shapeId` on every poke part.
|
|
555
|
+
*/
|
|
556
|
+
interface ClientShapeSubscribeMessage {
|
|
557
|
+
id: string;
|
|
558
|
+
shape: {
|
|
559
|
+
args?: Record<string, unknown>;
|
|
560
|
+
name: string;
|
|
561
|
+
};
|
|
562
|
+
/**
|
|
563
|
+
* Resume from this checkpoint (the `__cdc_log` cursor the client last
|
|
564
|
+
* applied for this shape). When absent or below the server's retained floor
|
|
565
|
+
* (`minCdcSeq`), the server re-seeds with a full insert-poke instead of a
|
|
566
|
+
* delta.
|
|
567
|
+
*/
|
|
568
|
+
sinceCheckpoint?: number;
|
|
569
|
+
/**
|
|
570
|
+
* The CDC epoch {@link ClientShapeSubscribeMessage.sinceCheckpoint} belongs
|
|
571
|
+
* to. A mismatch (forked changelog timeline) forces a full re-seed even when
|
|
572
|
+
* the cursor is numerically in range.
|
|
573
|
+
*/
|
|
574
|
+
sinceEpoch?: string;
|
|
575
|
+
type: "shape_subscribe";
|
|
576
|
+
}
|
|
577
|
+
/** Cancel a shape subscription started with the same `id`. */
|
|
578
|
+
interface ClientShapeUnsubscribeMessage {
|
|
579
|
+
id: string;
|
|
580
|
+
type: "shape_unsubscribe";
|
|
581
|
+
}
|
|
582
|
+
interface ClientAckMessage {
|
|
583
|
+
id: string;
|
|
584
|
+
type: "ack";
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Start a streaming query. The id namespaces a fresh stream and is echoed on
|
|
588
|
+
* every {@link ServerChunkMessage} the server pushes back. Cancel a running
|
|
589
|
+
* stream by sending a {@link ClientUnsubscribeMessage} with the same id —
|
|
590
|
+
* subscription and stream id-spaces share the cancel channel; the prefix
|
|
591
|
+
* (`sub_*` vs `stream_*`) keeps the local registries searchable.
|
|
592
|
+
*/
|
|
593
|
+
interface ClientStreamMessage {
|
|
594
|
+
/**
|
|
595
|
+
* Run generation the {@link ClientStreamMessage.sinceChunk} watermark
|
|
596
|
+
* belongs to: the `generation` stamp carried by the chunk frames this
|
|
597
|
+
* client already received, echoed back on a resume. The server refuses to
|
|
598
|
+
* splice a different run's tail onto the held prefix — a mismatch fails
|
|
599
|
+
* with `STREAM_INTERRUPTED` instead. Omitted on a first attach.
|
|
600
|
+
*/
|
|
601
|
+
generation?: number;
|
|
602
|
+
id: string;
|
|
603
|
+
query: {
|
|
604
|
+
args?: Record<string, unknown>;
|
|
605
|
+
functionPath: string;
|
|
606
|
+
shardKey?: string;
|
|
607
|
+
};
|
|
608
|
+
/**
|
|
609
|
+
* Resume watermark: the highest chunk `seq` this client already received.
|
|
610
|
+
* Only meaningful for a stream the server declared `durable` — the run
|
|
611
|
+
* replays everything after it and then continues live, which is what turns
|
|
612
|
+
* a reconnect into a resume instead of a lost generation. Omitted on a
|
|
613
|
+
* first attach.
|
|
614
|
+
*
|
|
615
|
+
* Named `sinceChunk`, not `sinceSeq`, because a subscribe envelope already
|
|
616
|
+
* carries a `query.sinceSeq` meaning the CDC cursor.
|
|
617
|
+
*/
|
|
618
|
+
sinceChunk?: number;
|
|
619
|
+
type: "stream";
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Join or leave a whisper `topic` — an app-chosen ephemeral channel scoped to a
|
|
623
|
+
* shard. While joined, the client receives every {@link ServerWhisperMessage}
|
|
624
|
+
* other members broadcast to the topic.
|
|
625
|
+
*/
|
|
626
|
+
interface ClientWhisperSubscribeMessage {
|
|
627
|
+
topic: string;
|
|
628
|
+
type: "whisper_subscribe" | "whisper_unsubscribe";
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Broadcast ephemeral `data` to the topic's other members on the shard. The
|
|
632
|
+
* payload is relayed verbatim with no server-side persistence (no SQLite/CDC
|
|
633
|
+
* write) — for typing indicators, live cursors, presence pings. The sender does
|
|
634
|
+
* not receive its own whisper.
|
|
635
|
+
*/
|
|
636
|
+
interface ClientWhisperMessage {
|
|
637
|
+
data?: unknown;
|
|
638
|
+
topic: string;
|
|
639
|
+
type: "whisper";
|
|
640
|
+
}
|
|
641
|
+
type ClientMessage = ClientAckMessage | ClientConnectMessage | ClientShapeSubscribeMessage | ClientShapeUnsubscribeMessage | ClientStreamMessage | ClientSubscribeMessage | ClientUnsubscribeMessage | ClientWhisperMessage | ClientWhisperSubscribeMessage;
|
|
642
|
+
/** Subscription protocol — server → client. */
|
|
643
|
+
interface ServerDataMessage {
|
|
644
|
+
/**
|
|
645
|
+
* The `__cdc_log` high-watermark covered by this frame (Pillar 1b). The
|
|
646
|
+
* client persists it as the query's `serverCursor` and replays it as
|
|
647
|
+
* `sinceSeq` on the next reconnect. Absent on shards that never enabled CDC.
|
|
648
|
+
*/
|
|
649
|
+
cursor?: number;
|
|
650
|
+
data?: unknown;
|
|
651
|
+
delta?: unknown;
|
|
652
|
+
/** The CDC epoch this frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
|
|
653
|
+
epoch?: string;
|
|
654
|
+
id: string;
|
|
655
|
+
/**
|
|
656
|
+
* The highest custom-mutator `mutationId` from this client the server has
|
|
657
|
+
* now applied (the per-client `__client_watermark`). Echoed so the client's
|
|
658
|
+
* outbox can drop confirmed pending mutations and let TanStack DB collapse
|
|
659
|
+
* the matching optimistic overlay. Absent on shards without custom mutators.
|
|
660
|
+
*/
|
|
661
|
+
lastMutationId?: number;
|
|
662
|
+
type: "data" | "delta";
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Lightweight resume acknowledgement (Pillar 1b): the server determined that
|
|
666
|
+
* nothing the subscription reads changed since the client's `sinceSeq`, so it
|
|
667
|
+
* skips re-sending the snapshot. The client keeps its cached value and only
|
|
668
|
+
* advances `serverCursor` to `cursor`.
|
|
669
|
+
*/
|
|
670
|
+
interface ServerResumeMessage {
|
|
671
|
+
cursor?: number;
|
|
672
|
+
/** The CDC epoch this resume's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
|
|
673
|
+
epoch?: string;
|
|
674
|
+
id: string;
|
|
675
|
+
/** Per-client custom-mutator watermark (see {@link ServerDataMessage.lastMutationId}). */
|
|
676
|
+
lastMutationId?: number;
|
|
677
|
+
type: "resume";
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Settled acknowledgement for a **list** subscription: a write touched one of
|
|
681
|
+
* the subscription's read tables but produced a byte-identical result, so the
|
|
682
|
+
* server suppressed the data frame. Sent ONLY to a `@lunora/db` custom-mutator
|
|
683
|
+
* client (one that announced a `clientId`, hence has a server-side
|
|
684
|
+
* `__client_watermark`) so its optimistic list overlay drops even when no data
|
|
685
|
+
* frame arrives. Plain `useQuery` subscribers never receive it, and an older
|
|
686
|
+
* client safely ignores the unknown frame.
|
|
687
|
+
*/
|
|
688
|
+
interface ServerSettledMessage {
|
|
689
|
+
cursor?: number;
|
|
690
|
+
/** The CDC epoch this settled frame's cursor belongs to (see {@link CachedQuery.serverEpoch}). */
|
|
691
|
+
epoch?: string;
|
|
692
|
+
id: string;
|
|
693
|
+
/**
|
|
694
|
+
* The highest custom-mutator `mutationId` from this client the server has
|
|
695
|
+
* now applied (the per-client `__client_watermark`). Forwarded to a
|
|
696
|
+
* collection's `onCheckpoint` so it can drop the overlay for the confirmed
|
|
697
|
+
* write whose result didn't change this list.
|
|
698
|
+
*/
|
|
699
|
+
lastMutationId?: number;
|
|
700
|
+
type: "settled";
|
|
701
|
+
}
|
|
702
|
+
interface ServerErrorMessage {
|
|
703
|
+
error?: unknown;
|
|
704
|
+
id?: string;
|
|
705
|
+
message?: string;
|
|
706
|
+
type: "error";
|
|
707
|
+
}
|
|
708
|
+
interface ServerAckMessage {
|
|
709
|
+
id: string;
|
|
710
|
+
type: "ack";
|
|
711
|
+
}
|
|
712
|
+
interface ServerCompleteMessage {
|
|
713
|
+
id: string;
|
|
714
|
+
type: "complete";
|
|
715
|
+
}
|
|
716
|
+
/** One frame of a streaming query — `data` carries the user-yielded chunk. */
|
|
717
|
+
interface ServerChunkMessage {
|
|
718
|
+
data: unknown;
|
|
719
|
+
/**
|
|
720
|
+
* Generation stamp of the **durable** run this chunk belongs to. The client
|
|
721
|
+
* stores it beside {@link ServerChunkMessage.seq} and echoes it as
|
|
722
|
+
* {@link ClientStreamMessage.generation} on a resume, so the server can
|
|
723
|
+
* tell a genuine resume from an attempt to splice onto a different run
|
|
724
|
+
* under the same key. Absent on an ephemeral stream.
|
|
725
|
+
*/
|
|
726
|
+
generation?: number;
|
|
727
|
+
id: string;
|
|
728
|
+
/**
|
|
729
|
+
* Monotonic position of this chunk within a **durable** run, starting at 1.
|
|
730
|
+
* The client stores the last one it saw and replays it as
|
|
731
|
+
* {@link ClientStreamMessage.sinceChunk} when the socket comes back. Absent
|
|
732
|
+
* on an ephemeral stream, which has nothing to resume from.
|
|
733
|
+
*/
|
|
734
|
+
seq?: number;
|
|
735
|
+
type: "chunk";
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* An ephemeral whisper relayed from another member of `topic` on the same shard
|
|
739
|
+
* (AnyCable-style whispering). `data` is the sender's payload verbatim; `from`
|
|
740
|
+
* is the sender's verified user id when known (absent for an anonymous sender).
|
|
741
|
+
* Never persisted server-side.
|
|
742
|
+
*/
|
|
743
|
+
interface ServerWhisperMessage {
|
|
744
|
+
data: unknown;
|
|
745
|
+
from?: string;
|
|
746
|
+
topic: string;
|
|
747
|
+
type: "whisper";
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* One row-level change in a shape's replication stream — the wire form of the
|
|
751
|
+
* DO's `__cdc_log` `CdcChange`. `insert`/`update` carry the post-image in
|
|
752
|
+
* `value` (projected to the shape's `columns`); `delete` omits it, identifying
|
|
753
|
+
* the removed row by `key` alone. The client applies these to its local
|
|
754
|
+
* collection; an unknown `key` on a `delete` is a safe no-op (a row the client
|
|
755
|
+
* never had in this shape).
|
|
756
|
+
*/
|
|
757
|
+
interface RowOp {
|
|
758
|
+
/** Row primary key (`_id`). */
|
|
759
|
+
key: string;
|
|
760
|
+
op: "delete" | "insert" | "update";
|
|
761
|
+
/** Logical table the row belongs to. */
|
|
762
|
+
table: string;
|
|
763
|
+
/** Post-image document for insert/update; absent on delete. */
|
|
764
|
+
value?: Record<string, unknown>;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Opens a **poke** — an atomically-applied batch of shape diffs (Zero's poke
|
|
768
|
+
* protocol). A `pokeStart` is followed by zero or more {@link ServerPokePartMessage}
|
|
769
|
+
* frames and closed by exactly one {@link ServerPokeEndMessage}; the client
|
|
770
|
+
* buffers every part and applies them in a single transaction at `pokeEnd`, so a
|
|
771
|
+
* socket that drops mid-poke simply re-seeds on reconnect (no torn view).
|
|
772
|
+
*/
|
|
773
|
+
interface ServerPokeStartMessage {
|
|
774
|
+
/**
|
|
775
|
+
* Poke-level fallback base, stamped by single-part senders. Per-shape
|
|
776
|
+
* {@link ServerPokePartMessage.baseCheckpoint} takes precedence; this is what
|
|
777
|
+
* a part without its own base falls back to.
|
|
778
|
+
*/
|
|
779
|
+
baseCheckpoint?: number;
|
|
780
|
+
/** CDC epoch this poke belongs to; a mismatch forces the client to re-seed rather than apply. */
|
|
781
|
+
epoch?: string;
|
|
782
|
+
/** Correlates this poke's `pokeStart`/`pokePart`/`pokeEnd` frames. */
|
|
783
|
+
pokeId: string;
|
|
784
|
+
type: "pokeStart";
|
|
785
|
+
}
|
|
786
|
+
/** One shape's slice of an in-flight poke: the row-ops to apply for `shapeId`. */
|
|
787
|
+
interface ServerPokePartMessage {
|
|
788
|
+
/**
|
|
789
|
+
* The checkpoint this shape's view must be at for `rowsPatch` to splice on
|
|
790
|
+
* cleanly. Per shape, because every shape on a socket has its own
|
|
791
|
+
* delivered-through cursor. Absent when the server cannot name a base — the
|
|
792
|
+
* gap check is then disarmed for this part, never guessed at.
|
|
793
|
+
*/
|
|
794
|
+
baseCheckpoint?: number;
|
|
795
|
+
/** Per-client custom-mutator watermark carried with this slice (see {@link ServerSettledMessage.lastMutationId}). */
|
|
796
|
+
lastMutationId?: number;
|
|
797
|
+
pokeId: string;
|
|
798
|
+
/**
|
|
799
|
+
* `true` when `rowsPatch` is the shape's COMPLETE membership, not a diff (a
|
|
800
|
+
* full seed or re-seed). The client MUST drop its current view for this shape
|
|
801
|
+
* before applying: a seed is inserts-only, so merging it leaves any row that
|
|
802
|
+
* left the shape while the client was disconnected on screen forever.
|
|
803
|
+
*
|
|
804
|
+
* Never inferred from an absent {@link ServerPokePartMessage.baseCheckpoint} —
|
|
805
|
+
* most live poke paths legitimately carry no base.
|
|
806
|
+
*/
|
|
807
|
+
reset?: boolean;
|
|
808
|
+
/** Ordered row-level changes for this shape, applied in sequence at `pokeEnd`. */
|
|
809
|
+
rowsPatch: RowOp[];
|
|
810
|
+
/** The {@link ClientShapeSubscribeMessage.id} these row-ops belong to. */
|
|
811
|
+
shapeId: string;
|
|
812
|
+
type: "pokePart";
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Closes a poke: the client commits the buffered parts atomically and advances
|
|
816
|
+
* its checkpoint to {@link ServerPokeEndMessage.checkpoint} (the `__cdc_log`
|
|
817
|
+
* cursor high-watermark the view now reflects), replayed as `sinceCheckpoint` on
|
|
818
|
+
* the next reconnect.
|
|
819
|
+
*/
|
|
820
|
+
interface ServerPokeEndMessage {
|
|
821
|
+
/** The `__cdc_log` cursor the view is at after applying this poke. */
|
|
822
|
+
checkpoint?: number;
|
|
823
|
+
/** CDC epoch the {@link ServerPokeEndMessage.checkpoint} belongs to. */
|
|
824
|
+
epoch?: string;
|
|
825
|
+
pokeId: string;
|
|
826
|
+
type: "pokeEnd";
|
|
827
|
+
}
|
|
828
|
+
type ServerMessage = ServerAckMessage | ServerChunkMessage | ServerCompleteMessage | ServerDataMessage | ServerErrorMessage | ServerPokeEndMessage | ServerPokePartMessage | ServerPokeStartMessage | ServerResumeMessage | ServerSettledMessage | ServerWhisperMessage;
|
|
829
|
+
/**
|
|
830
|
+
* The authenticated user as exposed client-side, mirroring better-auth's
|
|
831
|
+
* `user` row (the `user` field of the `get-session` response). Kept minimal
|
|
832
|
+
* and structural — only `id` is guaranteed; the rest are the common better-auth
|
|
833
|
+
* fields, and the index signature carries any plugin-contributed extras.
|
|
834
|
+
*/
|
|
835
|
+
interface User {
|
|
836
|
+
readonly createdAt?: NullableTimestamp;
|
|
837
|
+
readonly email?: null | string;
|
|
838
|
+
readonly emailVerified?: boolean | null;
|
|
839
|
+
readonly id: string;
|
|
840
|
+
readonly image?: null | string;
|
|
841
|
+
readonly name?: null | string;
|
|
842
|
+
readonly [key: string]: unknown;
|
|
843
|
+
readonly updatedAt?: NullableTimestamp;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Per-job retry policy carried on a {@link ScheduleRecord}. Mirrors
|
|
847
|
+
* `@lunora/scheduler`'s `RetryPolicy`; absent means the scheduler's defaults.
|
|
848
|
+
*/
|
|
849
|
+
interface ScheduleRetryPolicy {
|
|
850
|
+
/** Backoff growth across attempts. Default `"exponential"`. */
|
|
851
|
+
backoff?: "exponential" | "linear";
|
|
852
|
+
/** Base delay in milliseconds for the first retry. Default `30_000`. */
|
|
853
|
+
baseMs?: number;
|
|
854
|
+
/** Maximum number of dispatch attempts before dead-lettering. Default `5`. */
|
|
855
|
+
maxAttempts?: number;
|
|
856
|
+
/** Optional ceiling clamping the computed backoff delay. */
|
|
857
|
+
maxMs?: number;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* One pending scheduled function, as returned by the worker's
|
|
861
|
+
* `GET /_lunora/admin/scheduled` endpoint. The route is a byte-for-byte proxy of
|
|
862
|
+
* the SchedulerDO's own `/list`, so this mirrors `@lunora/scheduler`'s
|
|
863
|
+
* `ScheduleRecord` field-for-field — structurally, so the client carries no
|
|
864
|
+
* dependency on it. `packages/client/__tests__/structural-mirrors.test.ts` fails
|
|
865
|
+
* when the two drift.
|
|
866
|
+
*/
|
|
867
|
+
interface ScheduleRecord {
|
|
868
|
+
args: Record<string, unknown>;
|
|
869
|
+
/**
|
|
870
|
+
* Dispatch attempts already made. Absent (treated as 0) until the first
|
|
871
|
+
* failure; on a dead-letter record it is the exhausted count (> the retry
|
|
872
|
+
* budget). Surfaced so the studio can show how hard a job tried before it
|
|
873
|
+
* was parked.
|
|
874
|
+
*/
|
|
875
|
+
attempts?: number;
|
|
876
|
+
enqueuedAt: number;
|
|
877
|
+
/**
|
|
878
|
+
* The `ns:fn` path dispatched on fire. Absent when the job targets a durable
|
|
879
|
+
* workflow/agent instead — exactly one of `functionPath` /
|
|
880
|
+
* {@link ScheduleRecord.workflow} is set, so a view rendering a job's target
|
|
881
|
+
* must fall back to `workflow` rather than assuming a path.
|
|
882
|
+
*/
|
|
883
|
+
functionPath?: string;
|
|
884
|
+
id: string;
|
|
885
|
+
/** Scheduler/workpool instance the job was enqueued through. Absent for the default instance. */
|
|
886
|
+
instanceName?: string;
|
|
887
|
+
/** Logical workpool the job is routed to (concurrency-gated), when any. */
|
|
888
|
+
pool?: string;
|
|
889
|
+
/** Per-job retry policy; absent means the scheduler's built-in defaults. */
|
|
890
|
+
retry?: ScheduleRetryPolicy;
|
|
891
|
+
scheduledFor: number;
|
|
892
|
+
shardKey?: string;
|
|
893
|
+
/**
|
|
894
|
+
* The `WORKFLOW_*`/`AGENT_*` binding a fresh durable instance is started from
|
|
895
|
+
* on fire (the {@link ScheduleRecord.args} become its `params`). Set instead
|
|
896
|
+
* of {@link ScheduleRecord.functionPath}.
|
|
897
|
+
*/
|
|
898
|
+
workflow?: string;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* One workpool's live backlog, as returned by the worker's
|
|
902
|
+
* `GET /_lunora/admin/scheduled/status` endpoint. Mirrors `@lunora/scheduler`'s
|
|
903
|
+
* `SchedulerPoolStatus` structurally so the client carries no dependency on it.
|
|
904
|
+
*/
|
|
905
|
+
interface SchedulerPoolStatus {
|
|
906
|
+
/** Jobs currently dispatched-but-not-yet-completed (the held concurrency slots). */
|
|
907
|
+
inFlight: number;
|
|
908
|
+
/** The pool's concurrency cap. */
|
|
909
|
+
maxConcurrency: number;
|
|
910
|
+
/** The logical workpool name. */
|
|
911
|
+
name: string;
|
|
912
|
+
/** Pending jobs routed to this pool but not yet dispatched. */
|
|
913
|
+
queued: number;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* The app-level scheduler backlog, as returned by the worker's
|
|
917
|
+
* `GET /_lunora/admin/scheduled/status` endpoint. `pools` is the per-pool
|
|
918
|
+
* breakdown; `backlog` and `inFlight` are the app-wide sums of `queued` and
|
|
919
|
+
* `inFlight` across every pool — the headline numbers for the studio SLO
|
|
920
|
+
* view. Mirrors `@lunora/scheduler`'s `SchedulerStatus` structurally.
|
|
921
|
+
*/
|
|
922
|
+
interface SchedulerStatus {
|
|
923
|
+
/** Sum of every pool's `queued` count — the total pending backlog. */
|
|
924
|
+
backlog: number;
|
|
925
|
+
/** Sum of every pool's `inFlight` count — the total held concurrency slots. */
|
|
926
|
+
inFlight: number;
|
|
927
|
+
/** Per-pool backlog breakdown. */
|
|
928
|
+
pools: SchedulerPoolStatus[];
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* One shard's request volume, as returned by the worker's
|
|
932
|
+
* `POST /_lunora/admin/shard-traffic` endpoint. The cross-shard traffic feed
|
|
933
|
+
* the studio's `hot_shard` advisor lint consumes: `requests` is the shard's
|
|
934
|
+
* lifetime dispatch total, `shardKey` the DO id name (`""` for the root shard).
|
|
935
|
+
*/
|
|
936
|
+
interface ShardTrafficEntry {
|
|
937
|
+
requests: number;
|
|
938
|
+
shardKey: string;
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* The whole-shard-set traffic distribution returned by the worker's
|
|
942
|
+
* `POST /_lunora/admin/shard-traffic` endpoint. `shards` is one entry per live
|
|
943
|
+
* shard (a failed shard surfaces with `requests: 0`); `ok`/`failed` count the
|
|
944
|
+
* shards that returned vs. errored. Shaped to feed the advisor's `hot_shard`
|
|
945
|
+
* lint after the studio tags each entry with its sharded function `group`.
|
|
946
|
+
*/
|
|
947
|
+
interface ShardTrafficResult {
|
|
948
|
+
failed: number;
|
|
949
|
+
ok: number;
|
|
950
|
+
shards: ShardTrafficEntry[];
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* One object in the storage bucket, as returned by the worker's
|
|
954
|
+
* `GET /_lunora/admin/storage` endpoint. Mirrors `@lunora/storage`'s
|
|
955
|
+
* `R2ObjectLike` structurally.
|
|
956
|
+
*/
|
|
957
|
+
interface StorageObject {
|
|
958
|
+
customMetadata?: Record<string, string>;
|
|
959
|
+
etag: string;
|
|
960
|
+
httpMetadata?: {
|
|
961
|
+
contentType?: string;
|
|
962
|
+
};
|
|
963
|
+
key: string;
|
|
964
|
+
size: number;
|
|
965
|
+
/**
|
|
966
|
+
* When the object was stored. R2 emits a `Date`, which JSON-serializes to an
|
|
967
|
+
* ISO string over the wire; a mock may supply epoch ms — so consumers should
|
|
968
|
+
* normalise via `new Date(uploaded)`. Absent if the backend didn't report it.
|
|
969
|
+
*/
|
|
970
|
+
uploaded?: number | string;
|
|
971
|
+
}
|
|
972
|
+
/** One page of {@link StorageObject}s plus the cursor to fetch the next, if any. */
|
|
973
|
+
interface StorageListPage {
|
|
974
|
+
cursor?: string;
|
|
975
|
+
objects: StorageObject[];
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* One argument of a registered function, derived from its `v.*` validator by the
|
|
979
|
+
* worker. A compact signature shape — enough to render a function's API without
|
|
980
|
+
* the build-time codegen types.
|
|
981
|
+
*/
|
|
982
|
+
interface FunctionArgumentDescriptor {
|
|
983
|
+
/** Element validator kind for an `array` arg (one level), e.g. `string`. */
|
|
984
|
+
element?: string;
|
|
985
|
+
/** The (optional-unwrapped) validator kind, e.g. `string`, `id`, `object`. */
|
|
986
|
+
kind: string;
|
|
987
|
+
/** The argument name. */
|
|
988
|
+
name: string;
|
|
989
|
+
/** True when the arg is wrapped in `v.optional(...)`. */
|
|
990
|
+
optional: boolean;
|
|
991
|
+
/** Target table for an `id` arg (`v.id("table")`). */
|
|
992
|
+
table?: string;
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* One registered function, as returned by the worker's
|
|
996
|
+
* `GET /_lunora/admin/functions` endpoint: its `<file>:<function>` path, which
|
|
997
|
+
* client method (`query` / `mutation` / `action`) invokes it, and its argument
|
|
998
|
+
* signature. `args` is absent on responses from an older worker.
|
|
999
|
+
*/
|
|
1000
|
+
interface FunctionDescriptor {
|
|
1001
|
+
args?: FunctionArgumentDescriptor[];
|
|
1002
|
+
kind: "action" | "mutation" | "query";
|
|
1003
|
+
path: string;
|
|
1004
|
+
}
|
|
1005
|
+
/** A `.global()` (D1-backed) table plus its row count, from `/_lunora/admin/global/tables`. */
|
|
1006
|
+
interface GlobalTableInfo {
|
|
1007
|
+
name: string;
|
|
1008
|
+
rowCount: number;
|
|
1009
|
+
}
|
|
1010
|
+
/** A window of rows from one global table, from `/_lunora/admin/global/table`. */
|
|
1011
|
+
interface GlobalTablePage {
|
|
1012
|
+
columns: string[];
|
|
1013
|
+
/** FK columns (local column → referenced table) for external tables with real `REFERENCES` constraints, from `PRAGMA foreign_key_list`. */
|
|
1014
|
+
refs?: Record<string, string>;
|
|
1015
|
+
rows: Record<string, unknown>[];
|
|
1016
|
+
total: number;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* One equality constraint a facet-value click adds to the global browser's view
|
|
1020
|
+
* (`column = value`). `value` is the raw stored scalar the facet returned, sent
|
|
1021
|
+
* as-is and bound server-side, so it never injects SQL.
|
|
1022
|
+
*/
|
|
1023
|
+
interface GlobalFilterClause {
|
|
1024
|
+
column: string;
|
|
1025
|
+
value: unknown;
|
|
1026
|
+
}
|
|
1027
|
+
/** One distinct value of a faceted global column with its row count, from `/_lunora/admin/global/facet`. */
|
|
1028
|
+
interface GlobalFacetValue {
|
|
1029
|
+
count: number;
|
|
1030
|
+
value: unknown;
|
|
1031
|
+
}
|
|
1032
|
+
/** Per-column distinct-value summary for the global browser, from `/_lunora/admin/global/facet`. */
|
|
1033
|
+
interface GlobalFacetResult {
|
|
1034
|
+
truncated: boolean;
|
|
1035
|
+
values: GlobalFacetValue[];
|
|
1036
|
+
}
|
|
1037
|
+
/** A nullable timestamp field as better-auth serializes it: epoch-ms, ISO string, or null. */
|
|
1038
|
+
type NullableTimestamp = null | number | string;
|
|
1039
|
+
/** A workflow instance's lifecycle status. Mirrors Cloudflare's `InstanceStatus`. */
|
|
1040
|
+
type WorkflowInstanceStatus = "complete" | "errored" | "paused" | "queued" | "running" | "terminated" | "unknown" | "waiting" | "waitingForPause";
|
|
1041
|
+
/** The lifecycle mutations the status endpoint accepts. */
|
|
1042
|
+
type WorkflowInstanceAction = "pause" | "resume" | "terminate";
|
|
1043
|
+
/** One row of the workflow-instances list. */
|
|
1044
|
+
interface WorkflowInstanceSummary {
|
|
1045
|
+
createdOn?: string;
|
|
1046
|
+
endedOn?: string;
|
|
1047
|
+
id: string;
|
|
1048
|
+
startedOn?: string;
|
|
1049
|
+
status: WorkflowInstanceStatus;
|
|
1050
|
+
}
|
|
1051
|
+
/** One durable step of an instance's execution timeline. */
|
|
1052
|
+
interface WorkflowStepDetail {
|
|
1053
|
+
/** 1-based attempt count (`> 1` means the step retried). */
|
|
1054
|
+
attempts?: number;
|
|
1055
|
+
end?: string;
|
|
1056
|
+
error?: unknown;
|
|
1057
|
+
name: string;
|
|
1058
|
+
output?: unknown;
|
|
1059
|
+
start?: string;
|
|
1060
|
+
success?: boolean;
|
|
1061
|
+
/** `step` / `sleep` / `waitForEvent` / … (Cloudflare's step `type`). */
|
|
1062
|
+
type?: string;
|
|
1063
|
+
}
|
|
1064
|
+
/** A workflow instance's full detail: summary plus params/output/error and the step timeline. */
|
|
1065
|
+
interface WorkflowInstanceDetail extends WorkflowInstanceSummary {
|
|
1066
|
+
error?: unknown;
|
|
1067
|
+
output?: unknown;
|
|
1068
|
+
params?: unknown;
|
|
1069
|
+
steps: WorkflowStepDetail[];
|
|
1070
|
+
}
|
|
1071
|
+
/** A page of workflow instances. */
|
|
1072
|
+
interface WorkflowInstancePage {
|
|
1073
|
+
/**
|
|
1074
|
+
* Whether workflow inspection is configured on the worker (a Cloudflare
|
|
1075
|
+
* account id + API token). `false` when the admin proxy reports it can't
|
|
1076
|
+
* inspect instances; omitted (treated as configured) otherwise. Lets a
|
|
1077
|
+
* caller render a "set credentials" state without a failed request.
|
|
1078
|
+
*/
|
|
1079
|
+
configured?: boolean;
|
|
1080
|
+
instances: WorkflowInstanceSummary[];
|
|
1081
|
+
page: number;
|
|
1082
|
+
perPage: number;
|
|
1083
|
+
totalCount?: number;
|
|
1084
|
+
}
|
|
1085
|
+
type SubscriptionCallback = (data: unknown) => void;
|
|
1086
|
+
/** A subscription-scoped error the server pushed for this subscription id. */
|
|
1087
|
+
interface SubscriptionError {
|
|
1088
|
+
code?: string;
|
|
1089
|
+
message: string;
|
|
1090
|
+
}
|
|
1091
|
+
type SubscriptionErrorCallback = (error: SubscriptionError) => void;
|
|
1092
|
+
/**
|
|
1093
|
+
* One active per-call optimistic transform layered onto a subscription. The
|
|
1094
|
+
* displayed value is the authoritative {@link SubscriptionState.serverBase}
|
|
1095
|
+
* folded through every layer's `transform`, in order — so an incoming server
|
|
1096
|
+
* frame re-folds the still-pending layers onto the new base (rebasing) instead
|
|
1097
|
+
* of clobbering them. A layer is dropped — gaplessly — once a `data`/`delta`
|
|
1098
|
+
* frame whose `cursor >= commitCursor` arrives (its write is now reflected in
|
|
1099
|
+
* `serverBase`); `commitCursor` is the CDC cursor the server echoed on the
|
|
1100
|
+
* mutation's response, and stays `undefined` while the write is still queued/
|
|
1101
|
+
* in-flight (so the overlay survives unrelated deltas until confirmed).
|
|
1102
|
+
*/
|
|
1103
|
+
interface OptimisticLayer {
|
|
1104
|
+
/** The committed CDC cursor (from the mutation response); `undefined` until confirmed. */
|
|
1105
|
+
commitCursor?: number;
|
|
1106
|
+
readonly id: symbol;
|
|
1107
|
+
readonly transform: (current: unknown) => unknown;
|
|
1108
|
+
}
|
|
1109
|
+
interface SubscriptionState {
|
|
1110
|
+
/** True once the server has acked the subscription on the current socket. */
|
|
1111
|
+
acked: boolean;
|
|
1112
|
+
readonly args: Record<string, unknown>;
|
|
1113
|
+
/**
|
|
1114
|
+
* Stable wire-key of `args` (`stableWireKey`), computed once at subscribe
|
|
1115
|
+
* time. Cached so the optimistic-update fan-out can compare against a
|
|
1116
|
+
* mutation's args key without re-serializing every subscription's args on
|
|
1117
|
+
* every mutation.
|
|
1118
|
+
*/
|
|
1119
|
+
readonly argsKey: string;
|
|
1120
|
+
readonly callbacks: Set<SubscriptionCallback>;
|
|
1121
|
+
/**
|
|
1122
|
+
* Notified when a `settled` frame advances this subscription's watermark — a
|
|
1123
|
+
* write touched the subscription's tables but the result was byte-identical,
|
|
1124
|
+
* so the server suppressed the data frame. A `@lunora/db` list collection
|
|
1125
|
+
* uses this to drop the optimistic overlay for the confirmed write.
|
|
1126
|
+
*
|
|
1127
|
+
* A SET (not a single slot) because `SubscriptionState` is SHARED across
|
|
1128
|
+
* every subscriber to the same `(fn, args, shardKey)`: a `@lunora/db`
|
|
1129
|
+
* collection may subscribe to a query a plain `useQuery` already opened, so
|
|
1130
|
+
* each subscriber registers its own callback (mirroring `callbacks` /
|
|
1131
|
+
* `errorCallbacks`) and a `settled` frame fans out to all of them. Plain
|
|
1132
|
+
* `useQuery` consumers register nothing, leaving the set empty.
|
|
1133
|
+
*/
|
|
1134
|
+
readonly checkpointCallbacks: Set<(watermark: {
|
|
1135
|
+
checkpoint?: number;
|
|
1136
|
+
mutationId?: number;
|
|
1137
|
+
}) => void>;
|
|
1138
|
+
/** Notified when the server rejects this subscription (e.g. admin auth). */
|
|
1139
|
+
readonly errorCallbacks: Set<SubscriptionErrorCallback>;
|
|
1140
|
+
readonly fn: FunctionReference;
|
|
1141
|
+
readonly id: string;
|
|
1142
|
+
/**
|
|
1143
|
+
* The highest custom-mutator `mutationId` from this client the server has
|
|
1144
|
+
* applied, captured from the last `settled` frame (the suppressed-list-frame
|
|
1145
|
+
* watermark). Forwarded to {@link SubscriptionState.checkpointCallbacks}.
|
|
1146
|
+
* Absent until a `settled` frame arrives.
|
|
1147
|
+
*/
|
|
1148
|
+
lastMutationId?: number;
|
|
1149
|
+
/** Last known value, used to short-circuit `useQuery`-style consumers. */
|
|
1150
|
+
lastValue: unknown;
|
|
1151
|
+
/**
|
|
1152
|
+
* Active per-call optimistic layers, in application order (see
|
|
1153
|
+
* {@link OptimisticLayer}). Empty for subscriptions with no pending per-call
|
|
1154
|
+
* optimistic write — the common case, where `lastValue` tracks `serverBase`
|
|
1155
|
+
* exactly and behaviour is identical to a plain server-value assignment.
|
|
1156
|
+
*/
|
|
1157
|
+
optimisticLayers: OptimisticLayer[];
|
|
1158
|
+
/**
|
|
1159
|
+
* The authoritative server value the optimistic layers fold onto — the value
|
|
1160
|
+
* with NO optimistic overlay. Tracks `lastValue` exactly whenever no layers
|
|
1161
|
+
* are active; diverges only while a per-call optimistic write is pending. A
|
|
1162
|
+
* server frame updates this (and re-folds the layers); the durable read cache
|
|
1163
|
+
* persists this, never the optimistic overlay.
|
|
1164
|
+
*/
|
|
1165
|
+
serverBase: unknown;
|
|
1166
|
+
/**
|
|
1167
|
+
* The `__cdc_log` high-watermark (`cursor`) the `lastValue` reflects,
|
|
1168
|
+
* captured from the last `data`/`delta`/`resume` frame. Persisted to the
|
|
1169
|
+
* durable read cache and replayed as `sinceSeq` on reconnect so the server
|
|
1170
|
+
* can resume instead of re-snapshotting (Pillar 1b/2). Absent until the
|
|
1171
|
+
* first cursor-stamped frame arrives.
|
|
1172
|
+
*/
|
|
1173
|
+
serverCursor?: number;
|
|
1174
|
+
/**
|
|
1175
|
+
* The CDC `epoch` token the `serverCursor` belongs to, captured from the
|
|
1176
|
+
* same frame. Replayed as `sinceEpoch` on reconnect so the server resumes
|
|
1177
|
+
* only when the client is still on the same changelog timeline — a reset or
|
|
1178
|
+
* recycled shard advertises a new epoch, forcing a fresh snapshot. Absent
|
|
1179
|
+
* until the first epoch-stamped frame arrives.
|
|
1180
|
+
*/
|
|
1181
|
+
serverEpoch?: string;
|
|
1182
|
+
readonly shardKey?: string;
|
|
1183
|
+
/**
|
|
1184
|
+
* The wire-encoded form of `args`, computed once at `subscribe` time (so an
|
|
1185
|
+
* unsupported value fails loud at the call site, not inside a reconnect's
|
|
1186
|
+
* open handler). Sent on every `subscribe` frame — identical to `args` for
|
|
1187
|
+
* pure JSON, tagged tokens for `bigint`/`Date`/bytes/… (the shard
|
|
1188
|
+
* `decodeWire`s them at its subscribe entry point).
|
|
1189
|
+
*
|
|
1190
|
+
* A SNAPSHOT, not a view: `args` is the caller's own object, retained by
|
|
1191
|
+
* reference and never copied, so a caller that mutates it after subscribing
|
|
1192
|
+
* would otherwise poison every later resubscribe. `encodeWire` rebuilds
|
|
1193
|
+
* every container, so this copy is immune to that.
|
|
1194
|
+
*/
|
|
1195
|
+
readonly wireArgs: Record<string, unknown>;
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Active subscription registry. The client keys subscriptions by
|
|
1199
|
+
* `(functionPath, stableWireKey(args), shardKey)` so duplicate calls share a
|
|
1200
|
+
* single server-side registration. Args are stably encoded (keys sorted at every
|
|
1201
|
+
* depth) so two structurally-equal arg records constructed with a different key
|
|
1202
|
+
* order (`{ a, b }` vs `{ b, a }`) collapse to the same key instead of leaking a
|
|
1203
|
+
* duplicate subscription. Encoding the args' **wire form** keeps the key
|
|
1204
|
+
* byte-identical for pure-JSON args while giving wire-typed args (`bigint`,
|
|
1205
|
+
* `Date`, bytes, …) distinct stable tokens instead of a throw.
|
|
1206
|
+
*/
|
|
1207
|
+
declare class SubscriptionRegistry {
|
|
1208
|
+
static key(functionPath: string, args: Record<string, unknown>, shardKey?: string): string;
|
|
1209
|
+
/**
|
|
1210
|
+
* The registry key of an already-registered state, from its cached
|
|
1211
|
+
* {@link SubscriptionState.argsKey}. Re-deriving it from `state.args` would
|
|
1212
|
+
* re-read the caller's own (mutable) args object, so a caller that mutated
|
|
1213
|
+
* its args after subscribing would compute a DIFFERENT key on unsubscribe
|
|
1214
|
+
* and leak the registration forever.
|
|
1215
|
+
*/
|
|
1216
|
+
static keyOf(state: SubscriptionState): string;
|
|
1217
|
+
private readonly byKey;
|
|
1218
|
+
private readonly byId;
|
|
1219
|
+
get(key: string): SubscriptionState | undefined;
|
|
1220
|
+
getById(id: string): SubscriptionState | undefined;
|
|
1221
|
+
add(state: SubscriptionState): void;
|
|
1222
|
+
remove(state: SubscriptionState): void;
|
|
1223
|
+
all(): SubscriptionState[];
|
|
1224
|
+
/**
|
|
1225
|
+
* Drop every registration. Terminal — used by `LunoraClient.close()`, whose
|
|
1226
|
+
* whole point is to release the callback closures each {@link SubscriptionState}
|
|
1227
|
+
* holds (`callbacks`, `errorCallbacks`, `checkpointCallbacks` — React state
|
|
1228
|
+
* setters and `@lunora/db` collection closures), which otherwise outlive the
|
|
1229
|
+
* closed client for as long as the client object is reachable.
|
|
1230
|
+
*/
|
|
1231
|
+
clear(): void;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Read/write handle over the client's live query cache, handed to a mutation's
|
|
1235
|
+
* `withOptimisticUpdate` callback so a single mutation can optimistically patch
|
|
1236
|
+
* many subscribed queries at once (Convex's `OptimisticLocalStore` model).
|
|
1237
|
+
*
|
|
1238
|
+
* `getQuery` reads the current value (server value or any still-pending
|
|
1239
|
+
* optimistic override) of a subscribed query; `setQuery` registers a constant
|
|
1240
|
+
* optimistic layer on top. The whole batch rebases onto incoming deltas and
|
|
1241
|
+
* settles together — confirmed on the mutation's commit cursor, or rolled back
|
|
1242
|
+
* on failure — the same per-subscription layer machinery the single-query
|
|
1243
|
+
* per-call `optimistic` transform uses, generalized to N queries.
|
|
1244
|
+
*/
|
|
1245
|
+
interface OptimisticLocalStore {
|
|
1246
|
+
/**
|
|
1247
|
+
* Every loaded subscription on `function_`, regardless of args, paired with
|
|
1248
|
+
* the args it was subscribed under. Mirrors Convex's `getAllQueries` — handy
|
|
1249
|
+
* when a write must patch every variant of a list query (all channels,
|
|
1250
|
+
* all filters) without enumerating their args up front.
|
|
1251
|
+
*/
|
|
1252
|
+
getAllQueries: <F extends FunctionReference>(function_: F) => {
|
|
1253
|
+
args: ArgsOf<F>;
|
|
1254
|
+
value: ReturnOf<F> | undefined;
|
|
1255
|
+
}[];
|
|
1256
|
+
/**
|
|
1257
|
+
* Current cached value for the subscribed `(function_, args)` query, or
|
|
1258
|
+
* `undefined` when nothing is subscribed/loaded for it. Reflects any
|
|
1259
|
+
* optimistic override already written in this batch.
|
|
1260
|
+
*/
|
|
1261
|
+
getQuery: <F extends FunctionReference>(function_: F, args: ArgsOf<F>) => ReturnOf<F> | undefined;
|
|
1262
|
+
/**
|
|
1263
|
+
* Write an optimistic override for the subscribed `(function_, args)`
|
|
1264
|
+
* query. A no-op (returns without effect) when no subscription matches —
|
|
1265
|
+
* mirroring Convex, where you only patch queries the page is watching.
|
|
1266
|
+
*/
|
|
1267
|
+
setQuery: <F extends FunctionReference>(function_: F, args: ArgsOf<F>, value: ReturnOf<F> | undefined) => void;
|
|
1268
|
+
}
|
|
1269
|
+
/** A mutation's multi-query optimistic update: read/write the cache via `localStore`. */
|
|
1270
|
+
type OptimisticUpdate<Args> = (localStore: OptimisticLocalStore, args: Args) => void;
|
|
1271
|
+
/**
|
|
1272
|
+
* Build an {@link OptimisticLocalStore} bound to a subscription registry and the
|
|
1273
|
+
* mutation's shard key. Each `setQuery(value)` registers a constant-value layer
|
|
1274
|
+
* on its target subscription (via `applyOptimisticLayer`): the predicted value
|
|
1275
|
+
* survives incoming server deltas (re-clamped, masking concurrent changes to that
|
|
1276
|
+
* query — not merged) and drops gaplessly on the mutation's commit cursor, like
|
|
1277
|
+
* the single-query per-call `optimistic` path. Returns the store plus the ordered
|
|
1278
|
+
* `confirm` (success) and `rollback` (failure) closures every `setQuery` produced,
|
|
1279
|
+
* so the caller settles the whole batch when the mutation does.
|
|
1280
|
+
*/
|
|
1281
|
+
declare const createLocalStore: (subscriptions: SubscriptionRegistry, shardKey: string | undefined) => {
|
|
1282
|
+
confirms: ((commitCursor: number | undefined) => void)[];
|
|
1283
|
+
rollbacks: (() => void)[];
|
|
1284
|
+
store: OptimisticLocalStore;
|
|
1285
|
+
};
|
|
1286
|
+
declare const DEFAULT_MAX_BUFFER = 1024;
|
|
1287
|
+
interface StreamHandle<T = unknown> {
|
|
1288
|
+
/** Mark the stream complete (no more chunks); resolves any pending consumer to `done:true`. */
|
|
1289
|
+
readonly complete: () => void;
|
|
1290
|
+
/** Surface an error to any pending consumer; subsequent pushes are dropped. */
|
|
1291
|
+
readonly fail: (error: Error) => void;
|
|
1292
|
+
/**
|
|
1293
|
+
* Push one chunk. Silent no-op once the stream is `complete`, `fail`-ed,
|
|
1294
|
+
* or `cancel`-ed. When the buffer is already at `maxBuffer`, the stream
|
|
1295
|
+
* is failed with a `STREAM_BACKPRESSURE` error and the push is dropped —
|
|
1296
|
+
* the producer never sees a thrown exception.
|
|
1297
|
+
*/
|
|
1298
|
+
readonly push: (value: T) => void;
|
|
1299
|
+
}
|
|
1300
|
+
interface StreamIterable<T> extends AsyncIterable<T> {
|
|
1301
|
+
/** Cancel the stream from the consumer side: closes the iterator and notifies the registered canceller. */
|
|
1302
|
+
cancel: () => void;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Build a stream handle paired with an async-iterable. The handle is the
|
|
1306
|
+
* server-driven side (the WS dispatcher pushes chunks / completes / errors);
|
|
1307
|
+
* the iterable is what the user awaits. `onCancel` is invoked exactly once
|
|
1308
|
+
* when the consumer calls `.cancel()` (or `.return()`) so the client can
|
|
1309
|
+
* send a `{type:"unsubscribe"}` frame to the server.
|
|
1310
|
+
*/
|
|
1311
|
+
declare const createStream: <T>(options: {
|
|
1312
|
+
maxBuffer?: number;
|
|
1313
|
+
onCancel: () => void;
|
|
1314
|
+
}) => {
|
|
1315
|
+
handle: StreamHandle<T>;
|
|
1316
|
+
iterable: StreamIterable<T>;
|
|
1317
|
+
};
|
|
1318
|
+
type WSState = "idle" | "connecting" | "open" | "closed";
|
|
1319
|
+
/**
|
|
1320
|
+
* Aggregate live-socket health across every shard connection, for a UI status
|
|
1321
|
+
* indicator. `idle` = no socket opened yet; `connecting` = at least one socket
|
|
1322
|
+
* is (re)connecting and none is open; `connected` = at least one socket is open;
|
|
1323
|
+
* `offline` = sockets exist but all are down (between reconnect attempts).
|
|
1324
|
+
*/
|
|
1325
|
+
type ConnectionStatus = "connected" | "connecting" | "idle" | "offline";
|
|
1326
|
+
/** One shard's socket + watermark state in a {@link LunoraClient.debug} snapshot. */
|
|
1327
|
+
interface ClientDebugShard {
|
|
1328
|
+
/**
|
|
1329
|
+
* Highest custom-mutator watermark the server has echoed for this client on this
|
|
1330
|
+
* shard. A write whose `clientSeq` is above this has been sent but not confirmed
|
|
1331
|
+
* — the first thing to check when an optimistic overlay won't clear.
|
|
1332
|
+
*/
|
|
1333
|
+
confirmedMutationWatermark: number;
|
|
1334
|
+
/** Whether a `WebSocket` object currently exists (distinct from it being open). */
|
|
1335
|
+
hasSocket: boolean;
|
|
1336
|
+
/** `undefined` for the default (unsharded) connection. */
|
|
1337
|
+
shardKey: string | undefined;
|
|
1338
|
+
/** Whether this shard's socket has ever completed a handshake — gates offline queueing. */
|
|
1339
|
+
wasEverConnected: boolean;
|
|
1340
|
+
wsState: WSState;
|
|
1341
|
+
}
|
|
1342
|
+
/** One live query or shape subscription in a {@link LunoraClient.debug} snapshot. */
|
|
1343
|
+
interface ClientDebugSubscription {
|
|
1344
|
+
/** Whether the server has acknowledged the subscription on the current socket. */
|
|
1345
|
+
acked: boolean;
|
|
1346
|
+
/** `namespace:fn` for a query, `shape:<name>` for a replication shape. */
|
|
1347
|
+
functionPath: string;
|
|
1348
|
+
id: string;
|
|
1349
|
+
kind: "query" | "shape";
|
|
1350
|
+
/** Highest custom-mutator watermark echoed on THIS subscription (absent until a `settled`/poke frame arrives). */
|
|
1351
|
+
lastMutationId?: number;
|
|
1352
|
+
/** Per-call optimistic layers still folded onto this subscription's value — a non-zero count with no pending write is a leak. */
|
|
1353
|
+
pendingOptimisticLayers: number;
|
|
1354
|
+
/** Replicated rowset size (shapes only). */
|
|
1355
|
+
rowCount?: number;
|
|
1356
|
+
/** The `__cdc_log` cursor the current value reflects. */
|
|
1357
|
+
serverCursor?: number;
|
|
1358
|
+
shardKey: string | undefined;
|
|
1359
|
+
/** How many callers share this subscription (subscriptions are deduped by `(fn, args, shard)`). */
|
|
1360
|
+
subscriberCount: number;
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Everything the sync engine believes at one instant — see
|
|
1364
|
+
* {@link LunoraClient.debug} for why this exists.
|
|
1365
|
+
*/
|
|
1366
|
+
interface ClientDebugSnapshot {
|
|
1367
|
+
/** The watermark key the server's custom-mutator protocol advances per `clientSeq`. */
|
|
1368
|
+
clientId: string;
|
|
1369
|
+
closed: boolean;
|
|
1370
|
+
connectionStatus: ConnectionStatus;
|
|
1371
|
+
/** Writes waiting in the built-in offline queue (not the `@lunora/db` outbox). */
|
|
1372
|
+
pendingWrites: number;
|
|
1373
|
+
shards: ClientDebugShard[];
|
|
1374
|
+
subscriptions: ClientDebugSubscription[];
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Terminal verdict for a mutation that passed through the offline queue,
|
|
1378
|
+
* delivered to {@link LunoraClient.onMutationSettled}.
|
|
1379
|
+
*
|
|
1380
|
+
* Unlike the Promise returned by {@link LunoraClient.mutation} — which only the
|
|
1381
|
+
* original caller can await, and which no longer exists after a reload — this
|
|
1382
|
+
* fires for *every* queued write the server (or the queue) reaches a verdict on,
|
|
1383
|
+
* including writes restored from durable storage in a later session. It is the
|
|
1384
|
+
* channel a UI uses to tell the user "your queued change couldn't be saved"
|
|
1385
|
+
* instead of silently dropping a rolled-back optimistic row.
|
|
1386
|
+
*
|
|
1387
|
+
* `status: "rejected"` carries the failure `code` (e.g. `CONFLICT`,
|
|
1388
|
+
* `OFFLINE_QUEUE_OVERFLOW`, `OFFLINE_IDENTITY_CHANGED`) and the `error`.
|
|
1389
|
+
* `hadAwaiter` is `false` for a write whose original `mutation()` Promise is
|
|
1390
|
+
* gone (a hydrated/post-reload replay or an eviction), so a listener can tell
|
|
1391
|
+
* "the caller already saw this" apart from "nothing else will report this".
|
|
1392
|
+
*/
|
|
1393
|
+
interface MutationSettledEvent {
|
|
1394
|
+
/** The write's args, so a listener can describe or re-offer the change. */
|
|
1395
|
+
readonly args: Record<string, unknown>;
|
|
1396
|
+
/** Server/queue error code on `rejected` (e.g. `CONFLICT`), when present. */
|
|
1397
|
+
readonly code?: string;
|
|
1398
|
+
/** The rejection error on `status: "rejected"`. */
|
|
1399
|
+
readonly error?: unknown;
|
|
1400
|
+
/** The `<file>:<function>` reference of the mutation. */
|
|
1401
|
+
readonly functionPath: string;
|
|
1402
|
+
/** Whether a live caller was still awaiting this write's `mutation()` Promise. */
|
|
1403
|
+
readonly hadAwaiter: boolean;
|
|
1404
|
+
/** The write's stable id (idempotency key / queue id). */
|
|
1405
|
+
readonly id: string;
|
|
1406
|
+
/** Shard the write targeted, if any. */
|
|
1407
|
+
readonly shardKey?: string;
|
|
1408
|
+
/** Terminal outcome. */
|
|
1409
|
+
readonly status: "committed" | "rejected";
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Per-call options for {@link LunoraClient.action} — just `shardKey`, since an
|
|
1413
|
+
* action is not a write and carries none of the optimistic machinery. Exported
|
|
1414
|
+
* (at the end of this file) for the same reason {@link MutationCallOptions} is:
|
|
1415
|
+
* so the framework adapters (`@lunora/react`, `/solid`, `/svelte`, `/vue`,
|
|
1416
|
+
* `/angular`) type their `call(args, options?)` against one canonical
|
|
1417
|
+
* definition. Add an option here and every adapter forwards it; re-declare it
|
|
1418
|
+
* per adapter and they silently cannot.
|
|
1419
|
+
*/
|
|
1420
|
+
interface ActionCallOptions {
|
|
1421
|
+
/** Route the call to a specific shard. */
|
|
1422
|
+
shardKey?: string;
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Per-call options for {@link LunoraClient.mutation} — the optimistic-update
|
|
1426
|
+
* machinery plus `shardKey`. Exported (at the end of this file) so the framework
|
|
1427
|
+
* adapters (`@lunora/react`, `/solid`, `/svelte`, `/vue`) can type their
|
|
1428
|
+
* `mutate(args, options?)` against one canonical definition instead of
|
|
1429
|
+
* re-declaring it.
|
|
1430
|
+
*/
|
|
1431
|
+
interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unknown> {
|
|
1432
|
+
/**
|
|
1433
|
+
* Override the auto-generated idempotency key (`x-lunora-mutation-id`). Lets a
|
|
1434
|
+
* durable outbox replay a committed-but-unacked write under its *original* key
|
|
1435
|
+
* so the server dedups it instead of applying it twice. Omit for normal calls —
|
|
1436
|
+
* each then gets a fresh key.
|
|
1437
|
+
*/
|
|
1438
|
+
mutationId?: string;
|
|
1439
|
+
optimistic?: (current: TCurrent | undefined) => TValue;
|
|
1440
|
+
/**
|
|
1441
|
+
* Convex-parity multi-query optimistic update. Receives an
|
|
1442
|
+
* `OptimisticLocalStore` over the live subscription cache plus the
|
|
1443
|
+
* mutation's args, so one mutation can patch many subscribed queries at
|
|
1444
|
+
* once; every write is rolled back atomically if the mutation fails.
|
|
1445
|
+
*/
|
|
1446
|
+
optimisticUpdate?: OptimisticUpdate<TArgs>;
|
|
1447
|
+
/**
|
|
1448
|
+
* Sync predicate evaluated just before the offline queue replays this
|
|
1449
|
+
* write on reconnect. When it returns `false` the mutation is dropped
|
|
1450
|
+
* instead of replayed — use it to guard against replaying writes whose
|
|
1451
|
+
* assumptions are no longer valid (e.g. the document it referred to was
|
|
1452
|
+
* deleted by another client while this tab was offline).
|
|
1453
|
+
*/
|
|
1454
|
+
precondition?: () => boolean;
|
|
1455
|
+
shardKey?: string;
|
|
1456
|
+
}
|
|
1457
|
+
/** Callback a shape subscription invokes with its materialized rowset on every applied poke. */
|
|
1458
|
+
type ShapeCallback = (rows: Record<string, unknown>[]) => void;
|
|
1459
|
+
/**
|
|
1460
|
+
* The high-water marks a shape poke has now synced to the client: `checkpoint`
|
|
1461
|
+
* is the op-log cursor and `mutationId` the highest custom-mutator id the server
|
|
1462
|
+
* echoed for this client. A `@lunora/db` collection feeds these into its
|
|
1463
|
+
* checkpoint registry to drop optimistic overlays once the server's authoritative
|
|
1464
|
+
* rows have landed.
|
|
1465
|
+
*/
|
|
1466
|
+
interface SyncWatermark {
|
|
1467
|
+
checkpoint?: number;
|
|
1468
|
+
mutationId?: number;
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* An `Error` carrying the server's machine-readable `code` and (for a
|
|
1472
|
+
* `LunoraError`) structured `data`, plus an optional actionable `hint` (Markdown)
|
|
1473
|
+
* and `docsUrl` resolved from the central error catalog. The client's public
|
|
1474
|
+
* error contract for RPC/batch failures — a UI can render `hint`/`docsUrl` to
|
|
1475
|
+
* tell the user how to fix the error. The `(string & {})` arm keeps
|
|
1476
|
+
* forward-compat/unknown server codes assignable without losing autocomplete on
|
|
1477
|
+
* the known {@link LunoraErrorCode} union.
|
|
1478
|
+
*/
|
|
1479
|
+
type LunoraClientError = Error & {
|
|
1480
|
+
code?: LunoraErrorCode | (string & {});
|
|
1481
|
+
data?: unknown;
|
|
1482
|
+
docsUrl?: string;
|
|
1483
|
+
hint?: string | string[];
|
|
1484
|
+
};
|
|
1485
|
+
/** One demuxed result slot of a {@link LunoraClient.batch} call (plan 088). */
|
|
1486
|
+
type BatchSlot = {
|
|
1487
|
+
error: LunoraClientError;
|
|
1488
|
+
ok: false;
|
|
1489
|
+
} | {
|
|
1490
|
+
ok: true;
|
|
1491
|
+
value: unknown;
|
|
1492
|
+
};
|
|
1493
|
+
/**
|
|
1494
|
+
* Lunora browser/edge client. Talks RPC over HTTP and real-time deltas over
|
|
1495
|
+
* a single multiplexed WebSocket.
|
|
1496
|
+
*
|
|
1497
|
+
* Reconnect, offline queueing, and optimistic updates are all handled here;
|
|
1498
|
+
* see the package README for the wire protocol.
|
|
1499
|
+
*/
|
|
1500
|
+
declare class LunoraClient {
|
|
1501
|
+
/** Hard cap on concurrently-buffered pokes — a backstop that reclaims buffers abandoned by a mid-poke disconnect (no `pokeEnd`). Far above any real concurrent-in-flight count. */
|
|
1502
|
+
private static readonly MAX_POKE_BUFFERS;
|
|
1503
|
+
/**
|
|
1504
|
+
* Create a typed {@link ClientQueryRef}. Convenience wrapper around
|
|
1505
|
+
* {@link createClientQuery} so you don't need a separate import.
|
|
1506
|
+
* @example
|
|
1507
|
+
* ```ts
|
|
1508
|
+
* const sidebarOpen = LunoraClient.createClientQuery("sidebarOpen", true);
|
|
1509
|
+
* ```
|
|
1510
|
+
*/
|
|
1511
|
+
static createClientQuery<T>(key: string, defaultValue: T): ClientQueryRef<T>;
|
|
1512
|
+
readonly url: string;
|
|
1513
|
+
readonly wsUrl: string;
|
|
1514
|
+
/** Local reactive store for {@link ClientQueryRef} values — no server round-trip. Private; reach it via `getClientQuery` / `setClientQuery` / `subscribeClientQuery`. */
|
|
1515
|
+
private readonly clientQueryStore;
|
|
1516
|
+
private wsToken;
|
|
1517
|
+
/** Better-auth base path (trailing slash stripped) for the `get-session` lookup. */
|
|
1518
|
+
private readonly authBasePath;
|
|
1519
|
+
private readonly fetchImpl;
|
|
1520
|
+
private readonly WebSocketImpl;
|
|
1521
|
+
private readonly bookmark;
|
|
1522
|
+
private readonly reconnectOptions;
|
|
1523
|
+
/** WS connect timeout (ms); `0` disables it. See {@link LunoraClientOptions.connectTimeoutMs}. */
|
|
1524
|
+
private readonly connectTimeoutMs;
|
|
1525
|
+
/** Keepalive cadence (ms); `0` disables the heartbeat. See {@link LunoraClientOptions.heartbeatIntervalMs}. */
|
|
1526
|
+
private readonly heartbeatIntervalMs;
|
|
1527
|
+
private readonly offlineQueue;
|
|
1528
|
+
/**
|
|
1529
|
+
* Durable outbox seam (the `@lunora/db` `createExecutorOutboxSink`). When
|
|
1530
|
+
* set, offline writes are delegated here and the built-in {@link OfflineQueue}
|
|
1531
|
+
* is bypassed, so a db app has exactly one durable write path.
|
|
1532
|
+
*/
|
|
1533
|
+
private readonly outbox;
|
|
1534
|
+
/** Stable per-client id stamped onto every `OutboxMutation` (custom-mutator watermark). */
|
|
1535
|
+
private readonly clientId;
|
|
1536
|
+
/**
|
|
1537
|
+
* Highest CDC cursor this client has seen a write commit at, per shard key
|
|
1538
|
+
* (`""` for the default shard) — the read-your-writes bookmark sent as
|
|
1539
|
+
* `x-lunora-min-seq`.
|
|
1540
|
+
*
|
|
1541
|
+
* In memory only, and deliberately: it exists to keep THIS session's reads
|
|
1542
|
+
* behind THIS session's writes. Persisting it across reloads would pin a
|
|
1543
|
+
* fresh page to a cursor it has no reason to require and force needless
|
|
1544
|
+
* fallbacks to the owner.
|
|
1545
|
+
*/
|
|
1546
|
+
private readonly shardCursors;
|
|
1547
|
+
/**
|
|
1548
|
+
* The server's own name for the default shard (`defaultShardKey`), learned
|
|
1549
|
+
* from the first response to a call that named no shard. `undefined` until
|
|
1550
|
+
* then, which is why `cursorKeyFor` falls back to a placeholder entry that
|
|
1551
|
+
* `learnDefaultShardKey` folds in once the name arrives.
|
|
1552
|
+
*/
|
|
1553
|
+
private defaultShardKey;
|
|
1554
|
+
/**
|
|
1555
|
+
* `true` when the constructor's hydration microtask has finished loading the
|
|
1556
|
+
* durable read cache (Pillar 2) into `hydratedQueryCache`. Signals that
|
|
1557
|
+
* the cache is ready for synchronous `peekHydratedQuery` reads.
|
|
1558
|
+
*/
|
|
1559
|
+
private readyResolved;
|
|
1560
|
+
/** Resolvers for `whenReady()` — called once hydration completes. */
|
|
1561
|
+
private readyResolve;
|
|
1562
|
+
/**
|
|
1563
|
+
* Promise that resolves once the durable read cache has been loaded. When
|
|
1564
|
+
* `hydrateOnStart` is not set or no query cache is configured, resolves
|
|
1565
|
+
* immediately (the constructor creates an already-resolved promise).
|
|
1566
|
+
*/
|
|
1567
|
+
private readonly readyPromise;
|
|
1568
|
+
/**
|
|
1569
|
+
* Highest custom-mutator watermark the server has echoed for this client,
|
|
1570
|
+
* nested by identity fingerprint (`identityFingerprint() ?? ""`) then shard
|
|
1571
|
+
* bucket (`shardKey ?? ""`) — the DO tracks one `__client_watermark` per
|
|
1572
|
+
* `(identity, clientId)` pair, not per bucket alone, so a bucket-only key
|
|
1573
|
+
* would let a user switch claim the previous identity's sequence and wedge
|
|
1574
|
+
* every push on `OUT_OF_ORDER` until a reload (plan 316). A nested map
|
|
1575
|
+
* (rather than a single map composite-keyed by a joined string) is what a
|
|
1576
|
+
* previous fix here tried and got wrong: an identity fingerprint or a
|
|
1577
|
+
* `shardKey` is an arbitrary string, so any string delimiter — even one as
|
|
1578
|
+
* exotic as U+FFFD — can appear in one operand and collide with the other
|
|
1579
|
+
* (a fingerprint of `a<SEP>x` + bucket `y` joins to the same string as
|
|
1580
|
+
* a fingerprint of `a` + bucket `x<SEP>y`), mixing two identities' watermarks.
|
|
1581
|
+
* The nested map has no join step, so there is no delimiter to collide.
|
|
1582
|
+
* `callMutator` bumps it from every ack; the `@lunora/db` mutator runtime
|
|
1583
|
+
* seeds its `clientSeq` generator from it so a reload (which resets the
|
|
1584
|
+
* in-memory counter) never reissues a stale sequence the server would
|
|
1585
|
+
* silently swallow as a replay.
|
|
1586
|
+
*/
|
|
1587
|
+
private readonly clientWatermarks;
|
|
1588
|
+
/** Monotonic per-client mutation counter backing the server `__client_watermark`. */
|
|
1589
|
+
private outboxMutationCounter;
|
|
1590
|
+
private readonly onPersistenceError;
|
|
1591
|
+
private readonly persistence;
|
|
1592
|
+
/** App/schema version stamped on persisted writes + cached reads; mismatches are purged. */
|
|
1593
|
+
private readonly persistenceVersion;
|
|
1594
|
+
/** Releases the multi-tab outbox-leader Web Lock on close (see `hydrateAsOutboxLeader`). */
|
|
1595
|
+
private outboxLeaderRelease;
|
|
1596
|
+
/** Durable read cache (Pillar 2); `undefined` when `queryCache` is omitted or `false`. */
|
|
1597
|
+
private readonly queryCache;
|
|
1598
|
+
/**
|
|
1599
|
+
* Values restored from the `queryCache` at construction, keyed by the
|
|
1600
|
+
* read-cache key, awaiting the `subscribe()` that will consume them. A
|
|
1601
|
+
* key is consumed (deleted) the first time its subscription is created, so
|
|
1602
|
+
* the cache only ever seeds the initial value — live frames take over after.
|
|
1603
|
+
*/
|
|
1604
|
+
private readonly hydratedQueryCache;
|
|
1605
|
+
/**
|
|
1606
|
+
* Coalesced read-cache writes: the latest value per key, flushed to
|
|
1607
|
+
* the `queryCache` on a short debounce so a burst of deltas persists once.
|
|
1608
|
+
*/
|
|
1609
|
+
private readonly pendingCacheWrites;
|
|
1610
|
+
private cacheFlushTimer;
|
|
1611
|
+
private readonly subscriptions;
|
|
1612
|
+
/**
|
|
1613
|
+
* Cross-tab coordinator; created only when `crossTabSync: true`. When the
|
|
1614
|
+
* client is not the elected leader, all WebSocket operations are skipped.
|
|
1615
|
+
* Not `readonly` — `close()` clears it (mirrors `outboxLeaderRelease`).
|
|
1616
|
+
*/
|
|
1617
|
+
private tabCoordinator;
|
|
1618
|
+
/**
|
|
1619
|
+
* The leader's last-broadcast aggregate {@link ConnectionStatus}, mirrored
|
|
1620
|
+
* on a follower tab — which owns no `ShardConnection` of its own to
|
|
1621
|
+
* compute a status from (see `computeStatus`). `undefined` until the
|
|
1622
|
+
* leader's first broadcast (falls back to `"idle"`), and reset back to
|
|
1623
|
+
* `undefined` whenever this tab stops being a follower of the CURRENT
|
|
1624
|
+
* leader (becomes leader itself, or the leader changes), so a stale
|
|
1625
|
+
* mirror from a previous leader never survives a leadership change.
|
|
1626
|
+
*/
|
|
1627
|
+
private leaderStatus;
|
|
1628
|
+
/**
|
|
1629
|
+
* Sticky "has the mirrored leader status ever reported `connected`" flag —
|
|
1630
|
+
* the follower's counterpart to {@link ShardConnection.wasEverConnected},
|
|
1631
|
+
* since a follower has no `ShardConnection` of its own. Feeds the
|
|
1632
|
+
* offline-queue gate (see `mutation`) exactly like the real per-shard flag
|
|
1633
|
+
* does on the leader/single-tab path.
|
|
1634
|
+
*/
|
|
1635
|
+
private leaderWasEverConnected;
|
|
1636
|
+
/** One {@link ShardConnection} per shard key (keyed by `shardKey ?? ""`). */
|
|
1637
|
+
private readonly connections;
|
|
1638
|
+
/** Default `connect`-envelope context applied to a shard with no explicit override. */
|
|
1639
|
+
private readonly defaultConnectionContext;
|
|
1640
|
+
/**
|
|
1641
|
+
* Per-shard `connect`-envelope context registered via `setConnectionContext`
|
|
1642
|
+
* (keyed by `shardKey ?? ""`), overriding `defaultConnectionContext`. Sent
|
|
1643
|
+
* on every socket open so it replays across reconnects, and forwarded to the
|
|
1644
|
+
* server's `onConnect`/`onDisconnect` lifecycle hooks. This holds only the
|
|
1645
|
+
* imperative (last-writer-wins) override; refcounted holders registered via
|
|
1646
|
+
* `acquireConnectionContext` live in `connectionContextHolders` and take
|
|
1647
|
+
* precedence — see `effectiveConnectionContext`.
|
|
1648
|
+
*/
|
|
1649
|
+
private readonly connectionContexts;
|
|
1650
|
+
/**
|
|
1651
|
+
* Per-shard stack of refcounted connection-context holders (keyed by
|
|
1652
|
+
* `shardKey ?? ""`), registered via `acquireConnectionContext`. Each holder
|
|
1653
|
+
* is an opaque token carrying its `context`; the most-recently acquired
|
|
1654
|
+
* holder wins (last-writer-wins among live holders), and the context is only
|
|
1655
|
+
* cleared for a shard once its last holder releases — so two concurrently
|
|
1656
|
+
* mounted presence hooks on the same shard can't stomp each other's context
|
|
1657
|
+
* on cleanup. A holder is identified by reference identity so a release
|
|
1658
|
+
* removes exactly the right one regardless of stack position.
|
|
1659
|
+
*/
|
|
1660
|
+
private readonly connectionContextHolders;
|
|
1661
|
+
private authToken;
|
|
1662
|
+
/**
|
|
1663
|
+
* Optional STABLE identity subject (a user id), the basis of the offline-queue
|
|
1664
|
+
* identity stamp when supplied. Keeps a same-user token *refresh* from looking
|
|
1665
|
+
* like an identity change (which would discard queued writes). `undefined` =
|
|
1666
|
+
* not supplied, so identity falls back to a hash of the raw token. See
|
|
1667
|
+
* `setAuthToken` / `identityFingerprint`.
|
|
1668
|
+
*/
|
|
1669
|
+
private authSubject;
|
|
1670
|
+
/**
|
|
1671
|
+
* Identity stamp recorded against each queued offline mutation, keyed by
|
|
1672
|
+
* the queue-assigned mutation id. Captured at enqueue from the auth token
|
|
1673
|
+
* in effect at the time, and re-checked at flush so a queued write can
|
|
1674
|
+
* never replay under a different identity than the one that issued it.
|
|
1675
|
+
* See `identityFingerprint` for the fingerprint shape.
|
|
1676
|
+
*/
|
|
1677
|
+
private readonly queuedIdentities;
|
|
1678
|
+
/**
|
|
1679
|
+
* Distinct shard keys with a mutation currently sitting in `offlineQueue`
|
|
1680
|
+
* — fresh writes queued this session (`enqueueOfflineMutation`) or writes
|
|
1681
|
+
* restored from durable storage (`hydratePersistedQueue`). A follower tab
|
|
1682
|
+
* has no per-shard `ShardConnection` to iterate when its mirrored leader
|
|
1683
|
+
* status turns `"connected"` (see the `onConnectionStatus` coordinator
|
|
1684
|
+
* option), so this is what that flush walks instead. Entries are never
|
|
1685
|
+
* removed — flushing an already-empty shard is a cheap no-op, and the set
|
|
1686
|
+
* is bounded by the app's own distinct shard-key cardinality.
|
|
1687
|
+
*/
|
|
1688
|
+
private readonly queuedOfflineShardKeys;
|
|
1689
|
+
private closed;
|
|
1690
|
+
/** Subscribers to auth-token changes (see `onAuthTokenChange`). */
|
|
1691
|
+
private readonly authTokenListeners;
|
|
1692
|
+
/** Subscribers to aggregate connection-status changes (see `onConnectionStatus`). */
|
|
1693
|
+
private readonly statusListeners;
|
|
1694
|
+
/** Subscribers notified when the server drops a socket for an expired token (see `onTokenExpired`). */
|
|
1695
|
+
private readonly tokenExpiredListeners;
|
|
1696
|
+
/** Subscribers to offline-queued mutation verdicts (see `onMutationSettled`). */
|
|
1697
|
+
private readonly mutationSettledListeners;
|
|
1698
|
+
/** Subscribers to the offline-queue pending-count (see `onPendingChange`). */
|
|
1699
|
+
private readonly pendingChangeListeners;
|
|
1700
|
+
/**
|
|
1701
|
+
* Whisper-topic handlers, keyed by `connectionKey(shardKey)` → topic → set
|
|
1702
|
+
* of callbacks. Membership doubles as the resubscribe set replayed on every
|
|
1703
|
+
* (re)connect so a topic survives a socket bounce.
|
|
1704
|
+
*/
|
|
1705
|
+
private readonly whisperHandlers;
|
|
1706
|
+
/** Last status broadcast, so we only notify listeners on an actual change. */
|
|
1707
|
+
private lastStatus;
|
|
1708
|
+
private nextSubId;
|
|
1709
|
+
private nextStreamId;
|
|
1710
|
+
/**
|
|
1711
|
+
* In-flight client-side stream readers, keyed by the stream id sent on the
|
|
1712
|
+
* wire. The handle drives the underlying iterator queue and `shardKey`
|
|
1713
|
+
* tells us which socket to push the cancel frame onto when the consumer
|
|
1714
|
+
* calls `.cancel()` or the iterator is garbage-collected.
|
|
1715
|
+
*/
|
|
1716
|
+
private readonly streams;
|
|
1717
|
+
/** Live shape subscriptions (partial replication), keyed by their wire id. */
|
|
1718
|
+
/**
|
|
1719
|
+
* Teardown callbacks for the admin sockets {@link LunoraClient.subscribeScheduledJobs}
|
|
1720
|
+
* opens. Those run their own reconnect loop off a closure-local `closed`
|
|
1721
|
+
* flag rather than `this.closed` (they predate `ensureSocket`'s guard), so
|
|
1722
|
+
* without this registry a `close()` left every one of them reconnecting on
|
|
1723
|
+
* its backoff forever — re-minting an ephemeral admin sub-token on each
|
|
1724
|
+
* attempt when a `WsTokenProvider` is wired.
|
|
1725
|
+
*/
|
|
1726
|
+
private readonly adminSocketTeardowns;
|
|
1727
|
+
/**
|
|
1728
|
+
* The in-flight offline-queue replay per shard (`connectionKey`), while one
|
|
1729
|
+
* is running. Two jobs:
|
|
1730
|
+
*
|
|
1731
|
+
* 1. It serializes overlapping flushes for the same shard — two reconnect
|
|
1732
|
+
* events in quick succession used to drain and replay concurrently.
|
|
1733
|
+
* 2. It is the barrier {@link LunoraClient.mutation} waits on before sending a FRESH
|
|
1734
|
+
* write directly. The socket's `open` handler flips `wsState` to `"open"`
|
|
1735
|
+
* first and calls the flush last, so from that instant `mutation()`'s
|
|
1736
|
+
* offline gate is false and a new write raced straight to `/rpc` against
|
|
1737
|
+
* the replay of the older, queued write for the same document — the newer
|
|
1738
|
+
* one could land first and then be overwritten by the older. Ordering
|
|
1739
|
+
* inside the replay (`replaySequential`) never covered this, because the
|
|
1740
|
+
* racing write was never in the queue.
|
|
1741
|
+
*/
|
|
1742
|
+
private readonly offlineFlushes;
|
|
1743
|
+
private readonly shapeSubscriptions;
|
|
1744
|
+
/**
|
|
1745
|
+
* In-flight pokes being assembled between `pokeStart` and `pokeEnd`, keyed by
|
|
1746
|
+
* `<connectionKey>\u0000<pokeId>`.
|
|
1747
|
+
*
|
|
1748
|
+
* The connection has to be in the key: `pokeId` is a per-DO counter that also
|
|
1749
|
+
* resets on eviction, so a multi-shard client — one socket per shard, one map
|
|
1750
|
+
* here — sees two shards mint `poke-1` concurrently. Keyed by `pokeId` alone
|
|
1751
|
+
* their frames interleave into a single buffer: one shard applies the other's
|
|
1752
|
+
* epoch (spurious fork → view wiped) and the other finds no buffer for its
|
|
1753
|
+
* parts at all, silently dropping rows while the server's memo advances.
|
|
1754
|
+
*/
|
|
1755
|
+
private readonly pokeBuffers;
|
|
1756
|
+
private nextShapeId;
|
|
1757
|
+
constructor(options: LunoraClientOptions);
|
|
1758
|
+
/**
|
|
1759
|
+
* Set (or clear) the bearer token sent on every HTTP RPC. Notifies any
|
|
1760
|
+
* {@link onAuthTokenChange} listeners so React hooks like `useAuth` stay in
|
|
1761
|
+
* sync across all mounted instances.
|
|
1762
|
+
*
|
|
1763
|
+
* Pass a STABLE `subject` (the user id) to key the offline-queue identity on
|
|
1764
|
+
* it instead of the token bytes, so a token *refresh* (same user, new JWT)
|
|
1765
|
+
* doesn't read as an identity change and discard queued writes. The subject is
|
|
1766
|
+
* **sticky**: a later call that omits it (or passes `undefined`) keeps the
|
|
1767
|
+
* established subject — so `setAuthToken(refreshedToken)` after a prior
|
|
1768
|
+
* `setAuthToken(token, user.id)` retains the identity. Pass `null` to clear it
|
|
1769
|
+
* (an explicit sign-out). Establishing the subject for the first time on an
|
|
1770
|
+
* UNCHANGED token (e.g. the user id resolves a tick after the token was set)
|
|
1771
|
+
* re-stamps any in-flight queued writes rather than dropping them — same
|
|
1772
|
+
* credential, just a more stable label. A real user switch (the token AND
|
|
1773
|
+
* subject both change) still drops the previous user's writes.
|
|
1774
|
+
*
|
|
1775
|
+
* Does NOT update the WebSocket auth — the WS token is fixed at upgrade
|
|
1776
|
+
* time and lives in the URL. To refresh live WS auth, call
|
|
1777
|
+
* {@link setWsToken} explicitly, which closes existing shard sockets to
|
|
1778
|
+
* force a reconnect with the new credential.
|
|
1779
|
+
*/
|
|
1780
|
+
setAuthToken(token: string | null, subject?: string | null): void;
|
|
1781
|
+
getAuthToken(): string | null;
|
|
1782
|
+
/**
|
|
1783
|
+
* The current identity fingerprint (the same stamp queued offline writes
|
|
1784
|
+
* carry). Exposed so a durable {@link OutboxSink}'s replay handler — which
|
|
1785
|
+
* owns its own at-least-once replay outside the built-in `OfflineQueue` —
|
|
1786
|
+
* can drop a persisted write whose captured `identity` no longer matches the
|
|
1787
|
+
* signed-in user, the guard the queue path applies in `flushOfflineQueue`.
|
|
1788
|
+
*/
|
|
1789
|
+
currentIdentity(): string | null;
|
|
1790
|
+
/** This client's stable identifier — the watermark key the server's custom-mutator protocol advances per `clientSeq`. */
|
|
1791
|
+
clientIdentifier(): string;
|
|
1792
|
+
/**
|
|
1793
|
+
* The highest custom-mutator watermark the server has echoed for this client
|
|
1794
|
+
* on the given shard (0 if none yet). The `@lunora/db` mutator runtime seeds
|
|
1795
|
+
* its `clientSeq` generator from this so a reload never reissues a sequence
|
|
1796
|
+
* the server has already applied (which it would swallow as a replay, silently
|
|
1797
|
+
* dropping the write).
|
|
1798
|
+
*/
|
|
1799
|
+
confirmedMutationWatermark(shardKey?: string): number;
|
|
1800
|
+
/**
|
|
1801
|
+
* Push a custom mutator to its authoritative server impl over the watermark
|
|
1802
|
+
* protocol (Phase 4): the request carries `x-lunora-client-id` + a monotonic
|
|
1803
|
+
* `x-lunora-client-seq`, so the DO runs it exactly once and advances this
|
|
1804
|
+
* client's `__client_watermark`.
|
|
1805
|
+
*
|
|
1806
|
+
* Returns the server `result` plus `applied`: `true` when the DO ran this push
|
|
1807
|
+
* as the next-in-order mutation, `false` when it was a replay ack (`clientSeq`
|
|
1808
|
+
* was at or below the stored watermark — e.g. a stale sequence after a reload).
|
|
1809
|
+
* A `false` verdict tells the caller to reissue above the now-known watermark
|
|
1810
|
+
* (echoed into {@link confirmedMutationWatermark}) rather than treat the benign
|
|
1811
|
+
* ack as a confirmed write. Every ack — applied or not — bumps the watermark.
|
|
1812
|
+
*
|
|
1813
|
+
* This is the online transport for `@lunora/db`'s client-mutator runtime; the
|
|
1814
|
+
* optimistic overlay + durable-outbox concerns live in that runtime, not here.
|
|
1815
|
+
*/
|
|
1816
|
+
callMutator(functionPath: string, args: Record<string, unknown>, options?: {
|
|
1817
|
+
clientSeq?: number;
|
|
1818
|
+
shardKey?: string;
|
|
1819
|
+
}): Promise<{
|
|
1820
|
+
applied: boolean;
|
|
1821
|
+
result: unknown;
|
|
1822
|
+
}>;
|
|
1823
|
+
/**
|
|
1824
|
+
* Subscribe to auth-token changes. Returns an unsubscribe function. The
|
|
1825
|
+
* listener is NOT invoked on registration — use {@link getAuthToken} for
|
|
1826
|
+
* the current value.
|
|
1827
|
+
*/
|
|
1828
|
+
onAuthTokenChange(listener: (token: string | null) => void): Unsubscribe;
|
|
1829
|
+
/**
|
|
1830
|
+
* Fetch the currently authenticated user from better-auth's `get-session`
|
|
1831
|
+
* endpoint, returning the `user` record or `null` when signed out. Sends
|
|
1832
|
+
* the stored bearer token (if any) and `credentials: "include"` so a
|
|
1833
|
+
* cookie-session is also honoured. A network/parse failure or a non-OK
|
|
1834
|
+
* response resolves to `null` rather than throwing — callers treat "couldn't
|
|
1835
|
+
* resolve identity" as "signed out".
|
|
1836
|
+
*
|
|
1837
|
+
* Framework-agnostic: pair it with {@link onAuthTokenChange} to refetch when
|
|
1838
|
+
* the token changes (that's what `@lunora/react`'s `useAuth` does).
|
|
1839
|
+
*/
|
|
1840
|
+
getCurrentUser(): Promise<User | null>;
|
|
1841
|
+
/**
|
|
1842
|
+
* Replace the token appended to WS upgrade URLs as `?token=…` and close
|
|
1843
|
+
* every open shard socket so the reconnect picks up the new value. Call
|
|
1844
|
+
* this whenever the user's WS credential changes (rotating the admin token
|
|
1845
|
+
* in the studio, switching workspaces, etc.). Accepts a static string or a
|
|
1846
|
+
* {@link WsTokenProvider} resolved fresh at every (re)connect — the channel
|
|
1847
|
+
* for short-lived credentials like the minted ephemeral admin sub-token.
|
|
1848
|
+
* Bearer tokens for HTTP RPC are independent — see {@link setAuthToken}.
|
|
1849
|
+
*/
|
|
1850
|
+
setWsToken(token: string | undefined | WsTokenProvider): void;
|
|
1851
|
+
/**
|
|
1852
|
+
* Register (or clear, with `undefined`) the app context sent in the `connect`
|
|
1853
|
+
* envelope for a shard's socket, overriding the client-wide
|
|
1854
|
+
* {@link LunoraClientOptions.connectionContext}. The server forwards it to the
|
|
1855
|
+
* `onConnect`/`onDisconnect` lifecycle hooks as `event.context` — e.g.
|
|
1856
|
+
* `@lunora/react`'s `usePresence` registers `{ roomId, sessionId }` so the
|
|
1857
|
+
* presence row is removed the instant the socket drops, with no TTL lag.
|
|
1858
|
+
*
|
|
1859
|
+
* Stored per shard and replayed on every (re)connect. When a socket for the
|
|
1860
|
+
* shard is already open, a fresh `connect` envelope is sent immediately so the
|
|
1861
|
+
* server sees the new context without waiting for a reconnect.
|
|
1862
|
+
*
|
|
1863
|
+
* Not available on a `crossTabSync` FOLLOWER tab — the context rides the
|
|
1864
|
+
* `connect` envelope of a socket a follower does not own, so it would be
|
|
1865
|
+
* stored and never sent. Throws `NOT_IMPLEMENTED` there.
|
|
1866
|
+
*/
|
|
1867
|
+
setConnectionContext(context: Record<string, unknown> | undefined, options?: {
|
|
1868
|
+
shardKey?: string;
|
|
1869
|
+
}): void;
|
|
1870
|
+
/**
|
|
1871
|
+
* Refcounted variant of {@link setConnectionContext}: register a connection
|
|
1872
|
+
* `context` for a shard and get back a release function. Unlike the imperative
|
|
1873
|
+
* setter, the context is only cleared once the *last* acquired holder releases
|
|
1874
|
+
* it — so two components (e.g. two mounted `usePresence` hooks) on the same
|
|
1875
|
+
* shard no longer clobber each other's context when one of them unmounts. The
|
|
1876
|
+
* most-recently acquired live holder wins (last-writer-wins), and releasing
|
|
1877
|
+
* the top holder falls back to the previous one rather than clearing.
|
|
1878
|
+
*
|
|
1879
|
+
* With a single holder the behaviour is identical to a
|
|
1880
|
+
* `setConnectionContext(context)` / `setConnectionContext(undefined)` pair.
|
|
1881
|
+
* Releasing more than once is a no-op (the holder is matched by reference, so
|
|
1882
|
+
* a double release can't drop a different holder).
|
|
1883
|
+
*/
|
|
1884
|
+
acquireConnectionContext(context: Record<string, unknown>, options?: {
|
|
1885
|
+
shardKey?: string;
|
|
1886
|
+
}): Unsubscribe;
|
|
1887
|
+
/**
|
|
1888
|
+
* Join a whisper `topic` and receive every ephemeral message other members
|
|
1889
|
+
* broadcast to it on the same shard (typing indicators, live cursors,
|
|
1890
|
+
* presence pings). Whispers never touch the server's durable state — there's
|
|
1891
|
+
* no query, no row, no CDC entry. Returns an unsubscribe function; the topic
|
|
1892
|
+
* is left on the server once its last local handler unsubscribes.
|
|
1893
|
+
*
|
|
1894
|
+
* `handler` receives the raw `data` and the sender's verified `from` user id
|
|
1895
|
+
* (omitted for an anonymous sender). The topic is scoped to `options.shardKey`
|
|
1896
|
+
* (the default shard when omitted) — use the same shard you target with the
|
|
1897
|
+
* matching queries/mutations so members land on the same Durable Object.
|
|
1898
|
+
*
|
|
1899
|
+
* Security: whisper topics are NOT access-controlled beyond the shard
|
|
1900
|
+
* boundary — any client that can open a socket to the shard can join, read,
|
|
1901
|
+
* and inject on any topic name. `from` is server-stamped and unforgeable, but
|
|
1902
|
+
* do not put data on a whisper topic that some shard members shouldn't see,
|
|
1903
|
+
* and don't trust a whisper's `data` as authorization. Use a query/mutation
|
|
1904
|
+
* (with RLS) for anything privileged; whispers are for transient awareness.
|
|
1905
|
+
*
|
|
1906
|
+
* Not available on a `crossTabSync` FOLLOWER tab — whisper frames are not
|
|
1907
|
+
* relayed over the cross-tab channel, so this throws `NOT_IMPLEMENTED`
|
|
1908
|
+
* there rather than registering a handler nothing can ever reach. See
|
|
1909
|
+
* {@link LunoraClientOptions.crossTabSync}.
|
|
1910
|
+
*/
|
|
1911
|
+
whisperSubscribe(topic: string, handler: (data: unknown, from?: string) => void, options?: {
|
|
1912
|
+
shardKey?: string;
|
|
1913
|
+
}): Unsubscribe;
|
|
1914
|
+
/**
|
|
1915
|
+
* Broadcast an ephemeral `data` payload to the other members of a whisper
|
|
1916
|
+
* `topic` on `options.shardKey`'s shard. Fire-and-forget: the frame is
|
|
1917
|
+
* dropped when the shard socket isn't open (whispers are transient, never
|
|
1918
|
+
* queued), and the server silently drops it if the sender exceeds its
|
|
1919
|
+
* whisper rate budget. The sender never receives its own whisper. Omitting
|
|
1920
|
+
* `data` delivers JSON `null` to receivers (not `undefined`).
|
|
1921
|
+
*
|
|
1922
|
+
* That best-effort drop is for a socket that is momentarily down. A
|
|
1923
|
+
* `crossTabSync` FOLLOWER tab has no socket and never will (see
|
|
1924
|
+
* {@link LunoraClientOptions.crossTabSync}), so every whisper from it would
|
|
1925
|
+
* be dropped forever — it throws `NOT_IMPLEMENTED` instead.
|
|
1926
|
+
*/
|
|
1927
|
+
whisper(topic: string, data?: unknown, options?: {
|
|
1928
|
+
shardKey?: string;
|
|
1929
|
+
}): void;
|
|
1930
|
+
/**
|
|
1931
|
+
* Subscribe to token-expiry events: invoked whenever the server drops a
|
|
1932
|
+
* shard socket because the connection's credential lapsed (close code
|
|
1933
|
+
* `4001`). The client already reconnects automatically (re-resolving
|
|
1934
|
+
* identity from the cookie/token in effect); use this to refresh a
|
|
1935
|
+
* short-lived token first — e.g. call {@link setWsToken} / {@link setAuthToken}
|
|
1936
|
+
* with a freshly minted one. Returns an unsubscribe function.
|
|
1937
|
+
*/
|
|
1938
|
+
onTokenExpired(listener: () => void): Unsubscribe;
|
|
1939
|
+
/**
|
|
1940
|
+
* Current aggregate live-socket status across all shard connections. See
|
|
1941
|
+
* {@link ConnectionStatus}.
|
|
1942
|
+
*/
|
|
1943
|
+
connectionStatus(): ConnectionStatus;
|
|
1944
|
+
/**
|
|
1945
|
+
* Subscribe to aggregate connection-status changes. Invokes `listener`
|
|
1946
|
+
* immediately with the current status, then on every transition. Returns an
|
|
1947
|
+
* unsubscribe function.
|
|
1948
|
+
*/
|
|
1949
|
+
onConnectionStatus(listener: (status: ConnectionStatus) => void): Unsubscribe;
|
|
1950
|
+
/**
|
|
1951
|
+
* Number of offline writes waiting in the built-in queue to be sent — the
|
|
1952
|
+
* depth for a "N changes waiting to sync" indicator. Counts writes that are
|
|
1953
|
+
* queued (offline / mid-reconnect), not ones already in flight on the wire.
|
|
1954
|
+
* A `@lunora/db` app whose writes ride the unified outbox should read
|
|
1955
|
+
* `LunoraDb.pendingCount()` instead (this counts only the built-in queue).
|
|
1956
|
+
*/
|
|
1957
|
+
pendingCount(): number;
|
|
1958
|
+
/**
|
|
1959
|
+
* A point-in-time snapshot of everything the sync engine believes right now:
|
|
1960
|
+
* per-shard sockets and watermarks, every live query and shape subscription with
|
|
1961
|
+
* its cursor and ack state, and the offline-queue depth.
|
|
1962
|
+
*
|
|
1963
|
+
* This exists because the alternative is `console.log`. When an optimistic
|
|
1964
|
+
* overlay doesn't clear, the questions are always the same — *is the socket open?
|
|
1965
|
+
* what watermark has the server confirmed for this shard? has this shape been
|
|
1966
|
+
* poked since my write? is anything stuck in the queue?* — and none of them were
|
|
1967
|
+
* answerable from outside the client, so every adopter ends up instrumenting the
|
|
1968
|
+
* library by hand or building a bespoke policy layer around a symptom.
|
|
1969
|
+
*
|
|
1970
|
+
* Read it from a devtools console, log it next to a bug report, or render it in a
|
|
1971
|
+
* debug panel. Pull-only and allocation-cheap; nothing here is reactive, so poll
|
|
1972
|
+
* it or read it on demand.
|
|
1973
|
+
*
|
|
1974
|
+
* ```ts
|
|
1975
|
+
* const { shards, subscriptions } = client.debug();
|
|
1976
|
+
* // shards: [{ shardKey: "user-1", wsState: "open", confirmedMutationWatermark: 42, … }]
|
|
1977
|
+
* ```
|
|
1978
|
+
*/
|
|
1979
|
+
debug(): ClientDebugSnapshot;
|
|
1980
|
+
/**
|
|
1981
|
+
* Subscribe to changes in {@link pendingCount}. Invokes `listener` immediately
|
|
1982
|
+
* with the current count, then whenever the queue depth changes (a write is
|
|
1983
|
+
* enqueued, flushed, or discarded). Returns an unsubscribe function.
|
|
1984
|
+
*/
|
|
1985
|
+
onPendingChange(listener: (pending: number) => void): Unsubscribe;
|
|
1986
|
+
/**
|
|
1987
|
+
* Subscribe to terminal verdicts for offline-queued mutations. The listener
|
|
1988
|
+
* fires once per queued write that commits or is rejected — including a write
|
|
1989
|
+
* restored from durable storage after a reload, whose original `mutation()`
|
|
1990
|
+
* Promise no longer exists (`hadAwaiter: false`), and a write the queue
|
|
1991
|
+
* evicts on overflow or discards on an identity change. This is the durable
|
|
1992
|
+
* channel for surfacing a rolled-back optimistic write to the UI; an online
|
|
1993
|
+
* mutation that never queued still surfaces through the Promise `mutation()`
|
|
1994
|
+
* returns. The listener is NOT invoked on registration. Returns an
|
|
1995
|
+
* unsubscribe function. See {@link MutationSettledEvent}.
|
|
1996
|
+
*/
|
|
1997
|
+
onMutationSettled(listener: (event: MutationSettledEvent) => void): Unsubscribe;
|
|
1998
|
+
/**
|
|
1999
|
+
* The `WebSocket` implementation this client was constructed with (an
|
|
2000
|
+
* explicit `options.WebSocket`, or the ambient global on platforms that have
|
|
2001
|
+
* one) — `undefined` if neither is available. This is the seam a feature
|
|
2002
|
+
* that opens its OWN socket outside the client's multiplexed connection
|
|
2003
|
+
* (e.g. a voice-agent hook) should default to, instead of reaching for
|
|
2004
|
+
* `globalThis.WebSocket` directly: on React Native the client wraps this
|
|
2005
|
+
* constructor to inject the auth-headers factory's credential onto the
|
|
2006
|
+
* upgrade request (`createLunoraClient`'s `withAuthWebSocket`), which a raw
|
|
2007
|
+
* `new globalThis.WebSocket(url)` would silently bypass.
|
|
2008
|
+
*/
|
|
2009
|
+
getWebSocketImpl(): typeof WebSocket | undefined;
|
|
2010
|
+
/**
|
|
2011
|
+
* Read the current value for a {@link ClientQueryRef}. Returns
|
|
2012
|
+
* `ref.defaultValue` when no value has been explicitly set.
|
|
2013
|
+
*/
|
|
2014
|
+
getClientQuery<T>(ref: ClientQueryRef<T>): T;
|
|
2015
|
+
/**
|
|
2016
|
+
* Set a new value for `ref` and notify every subscriber. Pass `undefined`
|
|
2017
|
+
* to reset the slot to `ref.defaultValue`.
|
|
2018
|
+
*/
|
|
2019
|
+
setClientQuery<T>(ref: ClientQueryRef<T>, value: T): void;
|
|
2020
|
+
/**
|
|
2021
|
+
* Subscribe to changes for `ref`. The callback is NOT invoked on
|
|
2022
|
+
* registration — call {@link getClientQuery} for the current value.
|
|
2023
|
+
* Returns an unsubscribe function.
|
|
2024
|
+
*/
|
|
2025
|
+
subscribeClientQuery(ref: ClientQueryRef, callback: (value: unknown) => void): Unsubscribe;
|
|
2026
|
+
/**
|
|
2027
|
+
* Reset a {@link ClientQueryRef} to its default value, notifying every
|
|
2028
|
+
* subscriber. Equivalent to `setClientQuery(ref, ref.defaultValue)` but
|
|
2029
|
+
* removes the stored entry so a future {@link getClientQuery} returns
|
|
2030
|
+
* the default rather than an explicitly-set value.
|
|
2031
|
+
*/
|
|
2032
|
+
resetClientQuery(ref: ClientQueryRef): void;
|
|
2033
|
+
/**
|
|
2034
|
+
* Capture a snapshot of the current live query value at call time and
|
|
2035
|
+
* produce a `() => boolean` precondition that compares it against the
|
|
2036
|
+
* value at replay time (on queue drain / reconnect).
|
|
2037
|
+
*
|
|
2038
|
+
* Delegates to {@link createSnapshotPrecondition} with this client bound —
|
|
2039
|
+
* no need to pass `client` explicitly. The comparison semantics (including
|
|
2040
|
+
* how an absent subscription is treated) live there, in one place.
|
|
2041
|
+
* @example
|
|
2042
|
+
* ```ts
|
|
2043
|
+
* client.mutation(api.todos.update, { id, text }, {
|
|
2044
|
+
* precondition: client.snapshotPrecondition(api.todos.list, { userId }),
|
|
2045
|
+
* });
|
|
2046
|
+
* ```
|
|
2047
|
+
*/
|
|
2048
|
+
snapshotPrecondition(functionRef: FunctionReference, args: Record<string, unknown>, shardKey?: string): () => boolean;
|
|
2049
|
+
/**
|
|
2050
|
+
* Resolves once the durable read cache has been loaded into memory. When
|
|
2051
|
+
* `hydrateOnStart` is not configured or no query cache adapter is active,
|
|
2052
|
+
* returns an already-resolved promise so callers can always await it
|
|
2053
|
+
* unconditionally.
|
|
2054
|
+
*
|
|
2055
|
+
* Framework adapters (React, Vue, etc.) use this to gate the first
|
|
2056
|
+
* (enabled) render of a live query behind hydration, so the user sees
|
|
2057
|
+
* cached data instead of an undefined flash before the socket round-trip.
|
|
2058
|
+
*/
|
|
2059
|
+
whenReady(): Promise<void>;
|
|
2060
|
+
/**
|
|
2061
|
+
* Synchronously reports whether {@link whenReady} has already resolved (the
|
|
2062
|
+
* durable read cache is loaded, or none is configured). Framework adapters
|
|
2063
|
+
* read this to seed the hydration-gate state on the first render without
|
|
2064
|
+
* awaiting, then subscribe via {@link whenReady} for the pending case.
|
|
2065
|
+
*/
|
|
2066
|
+
get isReady(): boolean;
|
|
2067
|
+
/**
|
|
2068
|
+
* Synchronously peek at a value the durable read cache loaded for the given
|
|
2069
|
+
* function path + args + shard key. Returns `undefined` when:
|
|
2070
|
+
*
|
|
2071
|
+
* - No query cache adapter is configured.
|
|
2072
|
+
* - Hydration hasn't completed yet (race — await {@link whenReady} first).
|
|
2073
|
+
* - The cached value's identity fingerprint doesn't match the current auth.
|
|
2074
|
+
*
|
|
2075
|
+
* Unlike the internal {@link takeHydratedCache}, this is a READ-ONLY peek:
|
|
2076
|
+
* the cached entry stays in `hydratedQueryCache` so the subscription created
|
|
2077
|
+
* later by {@link subscribe} consumes it normally.
|
|
2078
|
+
*/
|
|
2079
|
+
peekHydratedQuery(functionPath: string, args: Record<string, unknown>, shardKey?: string): unknown;
|
|
2080
|
+
/**
|
|
2081
|
+
* Peek at the **current live value** of an active subscription, reporting
|
|
2082
|
+
* whether one exists at all rather than just its value. `present` is `false`
|
|
2083
|
+
* when no subscription is open for the given `(functionPath, args, shardKey)`;
|
|
2084
|
+
* `value` is the subscription's `lastValue`, which includes any optimistic
|
|
2085
|
+
* overlay.
|
|
2086
|
+
*
|
|
2087
|
+
* The two are separate because a caller like the snapshot precondition has to
|
|
2088
|
+
* tell "no subscription is active, so this read knows nothing" apart from
|
|
2089
|
+
* "the subscription is active and its value is `undefined`" — collapsing both
|
|
2090
|
+
* into a bare `undefined` return makes an unmounted component look like a
|
|
2091
|
+
* changed value, and drops the queued write.
|
|
2092
|
+
*
|
|
2093
|
+
* Unlike {@link peekHydratedQuery} (which reads from the durable read cache
|
|
2094
|
+
* and is independent of active subscriptions), this reflects the current
|
|
2095
|
+
* in-memory state of an already-opened subscription.
|
|
2096
|
+
*/
|
|
2097
|
+
peekActiveQuerySnapshot(functionPath: string, args: Record<string, unknown>, shardKey?: string): {
|
|
2098
|
+
present: boolean;
|
|
2099
|
+
value: unknown;
|
|
2100
|
+
};
|
|
2101
|
+
query<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: {
|
|
2102
|
+
shardKey?: string;
|
|
2103
|
+
}): Promise<ReturnOf<F>>;
|
|
2104
|
+
/**
|
|
2105
|
+
* Batch several independent calls into ONE round trip (plan 088). Each call is
|
|
2106
|
+
* dispatched server-side exactly as an individual RPC — per-shard
|
|
2107
|
+
* authorization, `(identity, mutationId)` idempotency, and custom-mutator
|
|
2108
|
+
* watermark ordering are all preserved — and the worker splits the batch by
|
|
2109
|
+
* shard so calls to different shards fan out to their own DOs. Results are
|
|
2110
|
+
* demuxed back in input order; a failing call does NOT fail the batch (its
|
|
2111
|
+
* slot carries `{ ok: false, error }`, with `.code`/`.data` reconstructed like
|
|
2112
|
+
* a single call). Args/results ride the value codec (bytes/bigint survive).
|
|
2113
|
+
*
|
|
2114
|
+
* No promise pipelining and no capability passing — a call's args cannot
|
|
2115
|
+
* reference another call's result (see plan 088 §fence; capabilities are
|
|
2116
|
+
* incompatible with DO hibernation).
|
|
2117
|
+
*/
|
|
2118
|
+
batch(calls: ReadonlyArray<{
|
|
2119
|
+
args?: Record<string, unknown>;
|
|
2120
|
+
fn: FunctionReference;
|
|
2121
|
+
shardKey?: string;
|
|
2122
|
+
}>): Promise<BatchSlot[]>;
|
|
2123
|
+
/**
|
|
2124
|
+
* Invoke a mutation. Errors propagate as rejections.
|
|
2125
|
+
*
|
|
2126
|
+
* Offline-queue semantics: a mutation is queued (and replayed on reconnect)
|
|
2127
|
+
* only when the targeted shard's socket was open at least once already
|
|
2128
|
+
* (`wasEverConnected`), so the registry / resubscribe handshake has run.
|
|
2129
|
+
* Mutations issued before the very first WS connect to a shard fail fast.
|
|
2130
|
+
* Opt into queueing-before-first-connect via
|
|
2131
|
+
* `OfflineQueueOptions.queueBeforeFirstConnect`.
|
|
2132
|
+
*
|
|
2133
|
+
* **Return-value caveat — the queued paths do not carry the server's result.**
|
|
2134
|
+
* The declared `Promise<ReturnOf<F>>` only holds when the write goes straight
|
|
2135
|
+
* to the server. Once a write is queued:
|
|
2136
|
+
*
|
|
2137
|
+
* - with a durable `outbox` configured, this resolves **immediately with
|
|
2138
|
+
* `undefined`** (typed as `ReturnOf<F>`) the moment the write is handed to
|
|
2139
|
+
* the outbox — the replay happens later, out of band, with no awaiter;
|
|
2140
|
+
* - with the built-in offline queue, it stays pending until the replay lands
|
|
2141
|
+
* and then resolves with the replayed call's value.
|
|
2142
|
+
*
|
|
2143
|
+
* So `const id = await client.mutation(api.todos.create, …)` is `undefined`
|
|
2144
|
+
* for every write issued while offline under an outbox. Generate ids
|
|
2145
|
+
* client-side (or read them back from a subscription) rather than depending
|
|
2146
|
+
* on a mutation's return value in an offline-capable app —
|
|
2147
|
+
* {@link LunoraClient.importRows} documents the same caveat for its counts.
|
|
2148
|
+
*/
|
|
2149
|
+
mutation<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: MutationCallOptions<unknown, unknown, ArgsOf<F>>): Promise<ReturnOf<F>>;
|
|
2150
|
+
action<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: ActionCallOptions): Promise<ReturnOf<F>>;
|
|
2151
|
+
/**
|
|
2152
|
+
* Bulk-import `rows` through a mutation that accepts a batch, chunked so a large
|
|
2153
|
+
* dataset lands in a bounded number of round-trips.
|
|
2154
|
+
*
|
|
2155
|
+
* **Offline caveat:** each chunk is sent with {@link LunoraClient.mutation}, which
|
|
2156
|
+
* resolves once the write is durably queued rather than once the server has applied
|
|
2157
|
+
* it. So an import run while offline resolves `{ chunks, imported }` with nothing
|
|
2158
|
+
* committed yet — the counts describe what was *handed over*, and the outbox
|
|
2159
|
+
* replays them on reconnect. Don't report "migration complete" on this alone.
|
|
2160
|
+
*
|
|
2161
|
+
* This is the one-shot migration / seed path: "I have 20k rows client-side and a
|
|
2162
|
+
* server mutation that inserts many at once". Doing it by hand goes wrong in two
|
|
2163
|
+
* predictable ways — a serial per-row loop pays one round-trip *and* one watermark
|
|
2164
|
+
* wait per row (a 200-row import becomes 200 sequential hops), while a single
|
|
2165
|
+
* giant call blows the DO's batch limit. So: chunk, send sequentially, and give
|
|
2166
|
+
* each chunk a stable idempotency key derived from `importId` + its index, so a
|
|
2167
|
+
* resumed or retried import doesn't double-insert the chunks that already landed.
|
|
2168
|
+
*
|
|
2169
|
+
* The server mutation is yours (Lunora can't guess the table or the row shape);
|
|
2170
|
+
* back it with `ctx.db.insertMany(...)`, or `insertManyUnsafe(...)` for data you
|
|
2171
|
+
* vouch for. `chunkSize` defaults to 500, matching the DO's default batch cap.
|
|
2172
|
+
*
|
|
2173
|
+
* ```ts
|
|
2174
|
+
* await client.importRows(api.migrate.importNodes, nodes, {
|
|
2175
|
+
* importId: `migrate-${userId}`,
|
|
2176
|
+
* onProgress: ({ done, total }) => setProgress(done / total),
|
|
2177
|
+
* shardKey: userId,
|
|
2178
|
+
* toArgs: (chunk) => ({ nodes: chunk }),
|
|
2179
|
+
* });
|
|
2180
|
+
* ```
|
|
2181
|
+
*/
|
|
2182
|
+
importRows(function_: FunctionReference, rows: ReadonlyArray<unknown>, options?: {
|
|
2183
|
+
/** Rows per call. Defaults to 500 — the DO's default batch cap. */
|
|
2184
|
+
chunkSize?: number;
|
|
2185
|
+
/**
|
|
2186
|
+
* Stable id for this import run. Each chunk is sent under
|
|
2187
|
+
* `${importId}:${chunkIndex}` as its mutation id, so re-running an import
|
|
2188
|
+
* that uses the SAME `chunkSize` re-sends each chunk under its prior key
|
|
2189
|
+
* and the server dedupes it instead of inserting twice.
|
|
2190
|
+
*
|
|
2191
|
+
* CAVEAT — the key is POSITIONAL, not content-based: it pins on the chunk
|
|
2192
|
+
* INDEX, not on the rows inside it. Resume or retry with a DIFFERENT
|
|
2193
|
+
* `chunkSize` (or a changed row ordering) and index N now covers different
|
|
2194
|
+
* rows than the first run's index N; the server sees a duplicate key and
|
|
2195
|
+
* SILENTLY DROPS those rows. Keep `chunkSize` (and the row order) identical
|
|
2196
|
+
* across resumes of the same `importId`. Omit `importId` only for a
|
|
2197
|
+
* throwaway import where double-insertion is acceptable.
|
|
2198
|
+
*/
|
|
2199
|
+
importId?: string;
|
|
2200
|
+
/**
|
|
2201
|
+
* Called after each chunk is accepted — for a progress bar.
|
|
2202
|
+
*
|
|
2203
|
+
* "Accepted" is not always "committed": while offline (or mid-reconnect) a
|
|
2204
|
+
* `mutation` resolves as soon as the write is durably **queued**, so a fully
|
|
2205
|
+
* offline import reports completion with nothing yet applied server-side.
|
|
2206
|
+
* Gate a migration's "done" state on connectivity, not just on this.
|
|
2207
|
+
*/
|
|
2208
|
+
onProgress?: (progress: {
|
|
2209
|
+
done: number;
|
|
2210
|
+
total: number;
|
|
2211
|
+
}) => void;
|
|
2212
|
+
/** Routes every chunk to one shard's DO. */
|
|
2213
|
+
shardKey?: string;
|
|
2214
|
+
/** Build the mutation args for one chunk. Defaults to `{ rows: chunk }`. */
|
|
2215
|
+
toArgs?: (chunk: ReadonlyArray<unknown>) => Record<string, unknown>;
|
|
2216
|
+
}): Promise<{
|
|
2217
|
+
chunks: number;
|
|
2218
|
+
imported: number;
|
|
2219
|
+
}>;
|
|
2220
|
+
/**
|
|
2221
|
+
* Read the cross-shard request distribution for a `.shardBy(...)` table —
|
|
2222
|
+
* the feed the studio's `hot_shard` advisor lint consumes. Hits the
|
|
2223
|
+
* admin-gated `POST /_lunora/admin/shard-traffic` endpoint, which fans the
|
|
2224
|
+
* cheap per-shard `getMetrics` read out across every live shard and returns
|
|
2225
|
+
* each shard's `{ shardKey, requests }` total (a failed shard surfaces with
|
|
2226
|
+
* `requests: 0`). Requires the worker to be built with a `queryCoordinator`
|
|
2227
|
+
* and `adminToken`, and this client's auth token to match; defaults any
|
|
2228
|
+
* absent field so an older worker yields an empty-but-valid shape.
|
|
2229
|
+
*/
|
|
2230
|
+
shardTraffic(table: string): Promise<ShardTrafficResult>;
|
|
2231
|
+
/**
|
|
2232
|
+
* List the functions queued via `runAfter` / `runAt`, soonest-due last
|
|
2233
|
+
* (the worker returns them in storage order). Hits the admin-gated
|
|
2234
|
+
* `/_lunora/admin/scheduled` endpoint, so the worker must be built with a
|
|
2235
|
+
* `schedulerDO` namespace and `adminToken`, and this client's auth token
|
|
2236
|
+
* must match. Powers `@lunora/studio`'s scheduled-jobs panel.
|
|
2237
|
+
*/
|
|
2238
|
+
listScheduledJobs(): Promise<ScheduleRecord[]>;
|
|
2239
|
+
/**
|
|
2240
|
+
* Read the app-level workpool backlog that powers `@lunora/studio`'s SLO
|
|
2241
|
+
* view: per-pool `{ name, queued, inFlight, maxConcurrency }` plus the
|
|
2242
|
+
* app-wide `backlog` (total queued) and `inFlight` (total held slots) sums.
|
|
2243
|
+
* Hits the admin-gated `GET /_lunora/admin/scheduled/status` endpoint, so the
|
|
2244
|
+
* same preconditions as {@link listScheduledJobs} apply (a `schedulerDO`
|
|
2245
|
+
* namespace + `adminToken` on the worker and a matching auth token here).
|
|
2246
|
+
* Defaults any absent field so an older worker still yields a valid shape.
|
|
2247
|
+
*/
|
|
2248
|
+
schedulerStatus(): Promise<SchedulerStatus>;
|
|
2249
|
+
/** Cancel a pending scheduled job by id. Returns whether a job was removed. */
|
|
2250
|
+
cancelScheduledJob(id: string): Promise<{
|
|
2251
|
+
cancelled: boolean;
|
|
2252
|
+
}>;
|
|
2253
|
+
/**
|
|
2254
|
+
* List the dead-letter jobs: schedules that exhausted their retry budget
|
|
2255
|
+
* and were parked instead of dropped. These never appear in
|
|
2256
|
+
* {@link listScheduledJobs} (their live header is gone), so this is the only
|
|
2257
|
+
* way the studio surfaces a permanently-failed job. Hits the admin-gated
|
|
2258
|
+
* `GET /_lunora/admin/scheduled/dead`; same preconditions as
|
|
2259
|
+
* {@link listScheduledJobs}. Powers `@lunora/studio`'s dead-letter panel.
|
|
2260
|
+
*/
|
|
2261
|
+
listDeadJobs(): Promise<ScheduleRecord[]>;
|
|
2262
|
+
/**
|
|
2263
|
+
* Resurrect a dead-letter job by id: it re-enters the schedule with a fresh
|
|
2264
|
+
* retry budget and fires on the next drain. Returns whether a parked record
|
|
2265
|
+
* matched. Hits the admin-gated `POST /_lunora/admin/scheduled/dead/retry`.
|
|
2266
|
+
*/
|
|
2267
|
+
retryDeadJob(id: string): Promise<{
|
|
2268
|
+
retried: boolean;
|
|
2269
|
+
}>;
|
|
2270
|
+
/**
|
|
2271
|
+
* Permanently drop a dead-letter job by id (the operator has decided not to
|
|
2272
|
+
* recover it). Returns whether a parked record was removed. Hits the
|
|
2273
|
+
* admin-gated `POST /_lunora/admin/scheduled/dead/cancel`.
|
|
2274
|
+
*/
|
|
2275
|
+
removeDeadJob(id: string): Promise<{
|
|
2276
|
+
removed: boolean;
|
|
2277
|
+
}>;
|
|
2278
|
+
/**
|
|
2279
|
+
* List a workflow's instances via the admin Workflows proxy
|
|
2280
|
+
* (`/_lunora/admin/workflows/instances`) — the Cloudflare control-plane data
|
|
2281
|
+
* the `Workflow` binding can't expose. Requires the worker to be built with a
|
|
2282
|
+
* `workflowsClient` (Cloudflare account id + API token). When one isn't
|
|
2283
|
+
* configured this does NOT reject: the proxy returns a `200 { configured:
|
|
2284
|
+
* false }` sentinel, so the result resolves with `configured === false` and an
|
|
2285
|
+
* empty `instances` list — callers should branch on that flag rather than
|
|
2286
|
+
* try/catch. (The instance-detail / status endpoints still reject with 501.)
|
|
2287
|
+
* `name` is the deployed workflow name.
|
|
2288
|
+
*/
|
|
2289
|
+
listWorkflowInstances(options: {
|
|
2290
|
+
name: string;
|
|
2291
|
+
page?: number;
|
|
2292
|
+
perPage?: number;
|
|
2293
|
+
status?: WorkflowInstanceStatus;
|
|
2294
|
+
}): Promise<WorkflowInstancePage>;
|
|
2295
|
+
/** Read one workflow instance with its step timeline (`/_lunora/admin/workflows/instance`). */
|
|
2296
|
+
getWorkflowInstance(options: {
|
|
2297
|
+
id: string;
|
|
2298
|
+
name: string;
|
|
2299
|
+
}): Promise<WorkflowInstanceDetail>;
|
|
2300
|
+
/** Pause / resume / terminate a workflow instance (`/_lunora/admin/workflows/status`). Needs an Edit-scoped Cloudflare token. */
|
|
2301
|
+
setWorkflowInstanceStatus(options: {
|
|
2302
|
+
action: WorkflowInstanceAction;
|
|
2303
|
+
id: string;
|
|
2304
|
+
name: string;
|
|
2305
|
+
}): Promise<{
|
|
2306
|
+
status: WorkflowInstanceStatus;
|
|
2307
|
+
}>;
|
|
2308
|
+
/**
|
|
2309
|
+
* Subscribe to the live scheduled-jobs list over the SchedulerDO's admin
|
|
2310
|
+
* WebSocket. `onJobs` fires with the full list on connect and on every
|
|
2311
|
+
* change (schedule / cancel / alarm-fire). Reconnects with the client's
|
|
2312
|
+
* configured backoff. Requires `wsToken` to be set to an admin credential
|
|
2313
|
+
* (the browser can't send an `Authorization` header on a WS) — the master
|
|
2314
|
+
* token, or preferably a {@link WsTokenProvider} minting the ephemeral
|
|
2315
|
+
* sub-token so the master credential stays out of the URL. Returns an
|
|
2316
|
+
* unsubscribe function that closes the socket and stops reconnecting.
|
|
2317
|
+
*/
|
|
2318
|
+
subscribeScheduledJobs(onJobs: (jobs: ScheduleRecord[]) => void): Unsubscribe;
|
|
2319
|
+
/**
|
|
2320
|
+
* List the registered public functions (queries / mutations / actions) with
|
|
2321
|
+
* their kinds. Hits the admin-gated `GET /_lunora/admin/functions` endpoint —
|
|
2322
|
+
* the worker must be built with a `functions` registry and `adminToken`, and
|
|
2323
|
+
* this client's auth token must match. Powers `@lunora/studio`'s function
|
|
2324
|
+
* runner auto-discovery.
|
|
2325
|
+
*/
|
|
2326
|
+
listFunctions(): Promise<FunctionDescriptor[]>;
|
|
2327
|
+
/**
|
|
2328
|
+
* List the code-defined cron triggers (the `cronJobs()` map injected on the
|
|
2329
|
+
* worker), each flattened to its firing `cron` expression. Hits the
|
|
2330
|
+
* admin-gated `GET /_lunora/admin/cron-jobs` endpoint — the worker must be
|
|
2331
|
+
* built with a `cronJobs` map and `adminToken`, and this client's auth token
|
|
2332
|
+
* must match. These are static (Cloudflare exposes no runtime cron
|
|
2333
|
+
* introspection), so the studio renders them read-only alongside the dynamic
|
|
2334
|
+
* scheduler jobs.
|
|
2335
|
+
*/
|
|
2336
|
+
getCronJobs(): Promise<CronJobInfo[]>;
|
|
2337
|
+
/**
|
|
2338
|
+
* Manually fire one code-defined cron job by name — the same dispatch the
|
|
2339
|
+
* scheduled trigger runs (dispatch the function, or start the durable
|
|
2340
|
+
* workflow), on demand. Hits the admin-gated `POST /_lunora/admin/cron-jobs/run`
|
|
2341
|
+
* endpoint; the worker must be built with a `cronJobs` map and `adminToken`,
|
|
2342
|
+
* and this client's auth token must match. Resolves when the job has run (a
|
|
2343
|
+
* function job's shard response is 2xx, or the workflow instance was created)
|
|
2344
|
+
* and rejects with the dispatch error otherwise.
|
|
2345
|
+
*/
|
|
2346
|
+
runCronJob(name: string): Promise<{
|
|
2347
|
+
name: string;
|
|
2348
|
+
ran: boolean;
|
|
2349
|
+
}>;
|
|
2350
|
+
/**
|
|
2351
|
+
* Fetch the generated OpenAPI 3.1 document. Hits the admin-gated
|
|
2352
|
+
* `GET /_lunora/admin/openapi` endpoint — the worker must be built with an
|
|
2353
|
+
* `openApiSpec` and `adminToken`, and this client's auth token must match.
|
|
2354
|
+
* Powers `@lunora/studio`'s API-reference (Scalar) view. When the worker has
|
|
2355
|
+
* no spec wired, the endpoint still resolves with an empty-but-valid OpenAPI
|
|
2356
|
+
* document (no `paths`), so callers can render a "not configured" state.
|
|
2357
|
+
*/
|
|
2358
|
+
fetchOpenApi(): Promise<Record<string, unknown>>;
|
|
2359
|
+
/**
|
|
2360
|
+
* Fetch the generated OpenRPC 1.x document. Hits the admin-gated
|
|
2361
|
+
* `GET /_lunora/admin/openrpc` endpoint — the worker must be built with an
|
|
2362
|
+
* `openRpcSpec` and `adminToken`, and this client's auth token must match.
|
|
2363
|
+
* OpenRPC is the RPC-native spec (a `methods` array over the JSON-RPC-shaped
|
|
2364
|
+
* `POST /_lunora/rpc` transport); it documents the RPC functions only.
|
|
2365
|
+
* Powers `@lunora/studio`'s OpenRPC API-reference view. When the worker has
|
|
2366
|
+
* no spec wired, the endpoint still resolves with an empty-but-valid OpenRPC
|
|
2367
|
+
* document (no `methods`), so callers can render a "not configured" state.
|
|
2368
|
+
*/
|
|
2369
|
+
fetchOpenRpc(): Promise<Record<string, unknown>>;
|
|
2370
|
+
/**
|
|
2371
|
+
* List objects in the storage bucket, optionally under a `prefix` and from a
|
|
2372
|
+
* pagination `cursor`. Hits the admin-gated `GET /_lunora/admin/storage`
|
|
2373
|
+
* endpoint — the worker must be built with a `storageList` function and
|
|
2374
|
+
* `adminToken`, and this client's auth token must match. Powers
|
|
2375
|
+
* `@lunora/studio`'s file browser.
|
|
2376
|
+
*/
|
|
2377
|
+
listStorageObjects(options?: {
|
|
2378
|
+
bucket?: string;
|
|
2379
|
+
cursor?: string;
|
|
2380
|
+
limit?: number;
|
|
2381
|
+
prefix?: string;
|
|
2382
|
+
}): Promise<StorageListPage>;
|
|
2383
|
+
/**
|
|
2384
|
+
* Delete one object from the storage bucket by key. Hits the admin-gated
|
|
2385
|
+
* `DELETE /_lunora/admin/storage?key=…` endpoint — the worker must be built
|
|
2386
|
+
* with a `storageDelete` function and `adminToken`. Powers the studio file
|
|
2387
|
+
* browser's per-row delete; resolves `{ deleted, key }`.
|
|
2388
|
+
*
|
|
2389
|
+
* An absent `deleted` field reads as `false`, matching every sibling admin
|
|
2390
|
+
* verb (`runCronJob`'s `ran`, …): the studio renders this value as the row's
|
|
2391
|
+
* outcome, so defaulting a missing field to success would report a delete
|
|
2392
|
+
* that a mismatched/older worker never performed.
|
|
2393
|
+
*/
|
|
2394
|
+
deleteStorageObject(key: string, options?: {
|
|
2395
|
+
bucket?: string;
|
|
2396
|
+
}): Promise<{
|
|
2397
|
+
deleted: boolean;
|
|
2398
|
+
key: string;
|
|
2399
|
+
}>;
|
|
2400
|
+
/**
|
|
2401
|
+
* List the storage bucket names the worker exposes, for the studio file
|
|
2402
|
+
* browser's bucket picker. Hits the admin-gated
|
|
2403
|
+
* `GET /_lunora/admin/storage/buckets` endpoint — always resolves (an empty
|
|
2404
|
+
* array when the worker configures no `storageBuckets`, i.e. single-bucket).
|
|
2405
|
+
*/
|
|
2406
|
+
listStorageBuckets(): Promise<string[]>;
|
|
2407
|
+
/**
|
|
2408
|
+
* Upload one object to the storage bucket. Hits the admin-gated
|
|
2409
|
+
* `PUT /_lunora/admin/storage?key=…` endpoint with the raw body and an
|
|
2410
|
+
* optional `contentType` header — the worker must be built with a
|
|
2411
|
+
* `storageUpload` function and `adminToken`. Powers the studio file
|
|
2412
|
+
* browser's upload control; resolves `{ etag?, key }`.
|
|
2413
|
+
*/
|
|
2414
|
+
uploadStorageObject(options: {
|
|
2415
|
+
body: ArrayBuffer | Blob;
|
|
2416
|
+
bucket?: string;
|
|
2417
|
+
contentType?: string;
|
|
2418
|
+
key: string;
|
|
2419
|
+
}): Promise<{
|
|
2420
|
+
etag?: string;
|
|
2421
|
+
key: string;
|
|
2422
|
+
}>;
|
|
2423
|
+
/**
|
|
2424
|
+
* Build a (signed or public) URL for one object. Hits the admin-gated
|
|
2425
|
+
* `GET /_lunora/admin/storage/url?key=…` endpoint — the worker must be built
|
|
2426
|
+
* with a `storageSignedUrl` function and `adminToken`. Powers the studio
|
|
2427
|
+
* file browser's copy-URL action; resolves the URL string.
|
|
2428
|
+
*
|
|
2429
|
+
* `options.expiresInSeconds` requests a share-link lifetime, which is
|
|
2430
|
+
* validated/clamped server-side. The options object mirrors the worker's
|
|
2431
|
+
* `StorageSignedUrlFunction` options (a `password` / download-limit are noted
|
|
2432
|
+
* as future fields there).
|
|
2433
|
+
*/
|
|
2434
|
+
signedStorageUrl(key: string, options?: {
|
|
2435
|
+
bucket?: string;
|
|
2436
|
+
expiresInSeconds?: number;
|
|
2437
|
+
}): Promise<string>;
|
|
2438
|
+
/**
|
|
2439
|
+
* List the `.global()` (D1-backed) tables with their row counts. Hits the
|
|
2440
|
+
* admin-gated `GET /_lunora/admin/global/tables` endpoint — the worker must
|
|
2441
|
+
* be built with a `globalIntrospector` and `adminToken`. Powers the data
|
|
2442
|
+
* browser's global mode.
|
|
2443
|
+
*/
|
|
2444
|
+
listGlobalTables(): Promise<GlobalTableInfo[]>;
|
|
2445
|
+
/**
|
|
2446
|
+
* Read a page of rows from one `.global()` table. `filters` AND-narrows the
|
|
2447
|
+
* page to rows matching each `column = value` eq constraint — the drill-down a
|
|
2448
|
+
* facet-value click applies; the array is wire-encoded, JSON-encoded into the
|
|
2449
|
+
* `filters` query param, and the values are bound server-side.
|
|
2450
|
+
*
|
|
2451
|
+
* The response is `decodeWire`d. The worker encodes it (`readGlobalTablePage`
|
|
2452
|
+
* in `@lunora/d1`) because JSON cannot carry a `v.bigint()` column at all and
|
|
2453
|
+
* silently flattens a `v.bytes()` one to `{}` — so without the decode here the
|
|
2454
|
+
* grid renders the raw 3-element tagged array instead of the value. The shard
|
|
2455
|
+
* browser's twin already pairs the same way through `rpc`; this is the global
|
|
2456
|
+
* half of that symmetry.
|
|
2457
|
+
*/
|
|
2458
|
+
readGlobalTablePage(options: {
|
|
2459
|
+
filters?: GlobalFilterClause[];
|
|
2460
|
+
limit?: number;
|
|
2461
|
+
offset?: number;
|
|
2462
|
+
table: string;
|
|
2463
|
+
}): Promise<GlobalTablePage>;
|
|
2464
|
+
/**
|
|
2465
|
+
* Summarise the distinct values of one column in a `.global()` table over the
|
|
2466
|
+
* active view (the same eq `filters` the browser is previewing) — the global
|
|
2467
|
+
* twin of the shard browser's facet. Hits the admin-gated
|
|
2468
|
+
* `GET /_lunora/admin/global/facet` endpoint; `column` is validated + bound
|
|
2469
|
+
* server-side. Powers the global data browser's facet sidebar.
|
|
2470
|
+
*
|
|
2471
|
+
* Wire-encoded/decoded on both legs for the same reason
|
|
2472
|
+
* {@link LunoraClient.readGlobalTablePage} is: a facet over a BLOB column
|
|
2473
|
+
* returns bytes, which `Response.json` flattens to `{}` — and since a facet
|
|
2474
|
+
* value is exactly what a click sends back as a `filters` clause, that is a
|
|
2475
|
+
* broken drill-down rather than a display glitch.
|
|
2476
|
+
*/
|
|
2477
|
+
facetGlobalColumn(options: {
|
|
2478
|
+
column: string;
|
|
2479
|
+
filters?: GlobalFilterClause[];
|
|
2480
|
+
limit?: number;
|
|
2481
|
+
table: string;
|
|
2482
|
+
}): Promise<GlobalFacetResult>;
|
|
2483
|
+
/**
|
|
2484
|
+
* List the schema's Vectorize indexes with their declared shape (table,
|
|
2485
|
+
* field, dimensions, metric, metadata) and live stats (vector count,
|
|
2486
|
+
* processing watermark) when the binding is reachable. Hits the admin-gated
|
|
2487
|
+
* `GET /_lunora/admin/vector/indexes` endpoint — the worker must be built
|
|
2488
|
+
* with a `vectorIntrospector` and `adminToken`. Powers the studio's vector
|
|
2489
|
+
* browser. Vectorize can't enumerate indexes at runtime, so this list comes
|
|
2490
|
+
* from the generated `LUNORA_VECTOR_INDEXES` registry.
|
|
2491
|
+
*/
|
|
2492
|
+
listVectorIndexes(): Promise<VectorIndexSummary[]>;
|
|
2493
|
+
/**
|
|
2494
|
+
* Run a nearest-neighbour similarity query against one vector index: the
|
|
2495
|
+
* worker embeds `text` via the index's embedder and returns the top matches.
|
|
2496
|
+
* Hits the admin-gated `POST /_lunora/admin/vector/query` endpoint. Throws
|
|
2497
|
+
* `VECTOR_QUERY_UNSUPPORTED` when the worker's introspector has no embedder
|
|
2498
|
+
* wired (the index lists read-only).
|
|
2499
|
+
*/
|
|
2500
|
+
queryVectorIndex(options: {
|
|
2501
|
+
name: string;
|
|
2502
|
+
text: string;
|
|
2503
|
+
topK?: number;
|
|
2504
|
+
}): Promise<VectorQueryMatch[]>;
|
|
2505
|
+
/**
|
|
2506
|
+
* Read one keyset-paginated page of the durable `ctx.log` archive that
|
|
2507
|
+
* `pipelineLogSink` writes to R2. Server-side only — the worker holds the R2
|
|
2508
|
+
* SQL credentials and runs the reader; the browser only sees the decoded
|
|
2509
|
+
* `{ rows, nextCursor }`. Pass the previous page's `nextCursor` as
|
|
2510
|
+
* `query.cursor` to page. Admin-gated. When the operator hasn't wired the
|
|
2511
|
+
* archive, `adminFetch` throws a `LunoraClientError` with `.code ===
|
|
2512
|
+
* "LOG_ARCHIVE_NOT_CONFIGURED"`, so a caller can render a "not configured"
|
|
2513
|
+
* state rather than an error.
|
|
2514
|
+
*/
|
|
2515
|
+
queryLogArchive(query?: PipelineLogQuery): Promise<PipelineLogPage>;
|
|
2516
|
+
/**
|
|
2517
|
+
* List the worker's registered Workers KV namespaces (binding names). Hits
|
|
2518
|
+
* the admin-gated `GET /_lunora/admin/kv/namespaces` endpoint — the worker
|
|
2519
|
+
* must be built with a `kvIntrospector` and `adminToken`. Powers the
|
|
2520
|
+
* studio's KV browser.
|
|
2521
|
+
*/
|
|
2522
|
+
listKvNamespaces(): Promise<KvNamespaceSummary[]>;
|
|
2523
|
+
/**
|
|
2524
|
+
* List keys in a KV namespace, optionally filtered by `prefix` and
|
|
2525
|
+
* paginated via `cursor`. Hits the admin-gated
|
|
2526
|
+
* `GET /_lunora/admin/kv/keys` endpoint.
|
|
2527
|
+
*/
|
|
2528
|
+
listKvKeys(options: {
|
|
2529
|
+
cursor?: string;
|
|
2530
|
+
limit?: number;
|
|
2531
|
+
namespace: string;
|
|
2532
|
+
prefix?: string;
|
|
2533
|
+
}): Promise<KvKeyListResult>;
|
|
2534
|
+
/**
|
|
2535
|
+
* Read a KV value (as text) and its metadata. Hits the admin-gated
|
|
2536
|
+
* `GET /_lunora/admin/kv/value` endpoint. Returns `{ value: null, metadata: null }`
|
|
2537
|
+
* when the key is absent.
|
|
2538
|
+
*/
|
|
2539
|
+
getKvValue(options: {
|
|
2540
|
+
key: string;
|
|
2541
|
+
namespace: string;
|
|
2542
|
+
}): Promise<KvValueResult>;
|
|
2543
|
+
/**
|
|
2544
|
+
* Write a string value to a KV namespace. Accepts an absolute `expiration`
|
|
2545
|
+
* (Unix seconds) or a relative `expirationTtl`, plus optional `metadata` —
|
|
2546
|
+
* re-send the loaded values on edit so a save preserves rather than clears
|
|
2547
|
+
* them. Hits the admin-gated `PUT /_lunora/admin/kv/value` endpoint.
|
|
2548
|
+
*/
|
|
2549
|
+
putKvValue(options: {
|
|
2550
|
+
expiration?: number;
|
|
2551
|
+
expirationTtl?: number;
|
|
2552
|
+
key: string;
|
|
2553
|
+
metadata?: unknown;
|
|
2554
|
+
namespace: string;
|
|
2555
|
+
value: string;
|
|
2556
|
+
}): Promise<void>;
|
|
2557
|
+
/**
|
|
2558
|
+
* Delete a key from a KV namespace. No-op when the key is absent. Hits the
|
|
2559
|
+
* admin-gated `DELETE /_lunora/admin/kv/value` endpoint.
|
|
2560
|
+
*/
|
|
2561
|
+
deleteKvKey(options: {
|
|
2562
|
+
key: string;
|
|
2563
|
+
namespace: string;
|
|
2564
|
+
}): Promise<void>;
|
|
2565
|
+
/**
|
|
2566
|
+
* List authenticated users, paged and optionally searched / filtered / sorted.
|
|
2567
|
+
* Hits the admin-gated `GET /_lunora/admin/auth/users` endpoint — the worker
|
|
2568
|
+
* must be built with an `authAdmin` and `adminToken`. Powers the studio's
|
|
2569
|
+
* users dashboard.
|
|
2570
|
+
*/
|
|
2571
|
+
listAuthUsers(options?: {
|
|
2572
|
+
filterField?: string;
|
|
2573
|
+
filterValue?: string;
|
|
2574
|
+
limit?: number;
|
|
2575
|
+
offset?: number;
|
|
2576
|
+
search?: string;
|
|
2577
|
+
searchField?: string;
|
|
2578
|
+
sortBy?: string;
|
|
2579
|
+
sortDirection?: "asc" | "desc";
|
|
2580
|
+
}): Promise<AuthPage<AuthUser>>;
|
|
2581
|
+
/**
|
|
2582
|
+
* Create a user. Hits the admin-gated `POST /_lunora/admin/auth/users/create`
|
|
2583
|
+
* endpoint (requires the worker's `authAdmin` to implement `createUser`).
|
|
2584
|
+
* `data` carries any app-defined `user.additionalFields`.
|
|
2585
|
+
*/
|
|
2586
|
+
createAuthUser(input: {
|
|
2587
|
+
data?: Record<string, unknown>;
|
|
2588
|
+
email: string;
|
|
2589
|
+
name: string;
|
|
2590
|
+
password?: string;
|
|
2591
|
+
role?: string | string[];
|
|
2592
|
+
}): Promise<AuthUser>;
|
|
2593
|
+
/** Set a user's role (string, or array joined comma-wise server-side). */
|
|
2594
|
+
setAuthUserRole(input: {
|
|
2595
|
+
role: string | string[];
|
|
2596
|
+
userId: string;
|
|
2597
|
+
}): Promise<AuthUser>;
|
|
2598
|
+
/** Ban a user. `expiresInSeconds` sets a temporary ban; omit it for a permanent one. Revokes the user's live sessions. */
|
|
2599
|
+
banAuthUser(input: {
|
|
2600
|
+
expiresInSeconds?: number;
|
|
2601
|
+
reason?: string;
|
|
2602
|
+
userId: string;
|
|
2603
|
+
}): Promise<AuthUser>;
|
|
2604
|
+
/** Lift a user's ban. */
|
|
2605
|
+
unbanAuthUser(input: {
|
|
2606
|
+
userId: string;
|
|
2607
|
+
}): Promise<AuthUser>;
|
|
2608
|
+
/** Set a user's password (admin override — no current-password challenge). */
|
|
2609
|
+
setAuthUserPassword(input: {
|
|
2610
|
+
newPassword: string;
|
|
2611
|
+
userId: string;
|
|
2612
|
+
}): Promise<void>;
|
|
2613
|
+
/** Permanently delete a user and revoke their sessions. */
|
|
2614
|
+
removeAuthUser(input: {
|
|
2615
|
+
userId: string;
|
|
2616
|
+
}): Promise<void>;
|
|
2617
|
+
/**
|
|
2618
|
+
* Mint an impersonation session for a user, returning its bearer `token`.
|
|
2619
|
+
* The caller is responsible for using the token (e.g. setting the session
|
|
2620
|
+
* cookie); the server performs no cookie round-trip.
|
|
2621
|
+
*/
|
|
2622
|
+
impersonateAuthUser(input: {
|
|
2623
|
+
userId: string;
|
|
2624
|
+
}): Promise<AuthImpersonation>;
|
|
2625
|
+
/** Revoke a single session by its id (force sign-out of one device). */
|
|
2626
|
+
revokeAuthSession(input: {
|
|
2627
|
+
sessionId: string;
|
|
2628
|
+
}): Promise<void>;
|
|
2629
|
+
/** Revoke every session for a user (force sign-out everywhere). */
|
|
2630
|
+
revokeAuthUserSessions(input: {
|
|
2631
|
+
userId: string;
|
|
2632
|
+
}): Promise<void>;
|
|
2633
|
+
/**
|
|
2634
|
+
* Report which auth dashboard surfaces are available — derived server-side
|
|
2635
|
+
* from the enabled better-auth plugins. The studio renders only the panels
|
|
2636
|
+
* whose capability is `true`.
|
|
2637
|
+
*/
|
|
2638
|
+
getAuthCapabilities(): Promise<AuthCapabilities>;
|
|
2639
|
+
/** Update a user's fields (name/email/app-defined `additionalFields`). */
|
|
2640
|
+
updateAuthUser(input: {
|
|
2641
|
+
data: Record<string, unknown>;
|
|
2642
|
+
userId: string;
|
|
2643
|
+
}): Promise<AuthUser>;
|
|
2644
|
+
/** List a user's linked accounts (credential / OAuth providers). Token material is stripped server-side. */
|
|
2645
|
+
listAuthAccounts(input: {
|
|
2646
|
+
userId: string;
|
|
2647
|
+
}): Promise<Record<string, unknown>[]>;
|
|
2648
|
+
/** Unlink a linked account from a user. */
|
|
2649
|
+
unlinkAuthAccount(input: {
|
|
2650
|
+
accountId: string;
|
|
2651
|
+
userId: string;
|
|
2652
|
+
}): Promise<void>;
|
|
2653
|
+
/** List a user's registered passkeys (requires the passkey plugin). */
|
|
2654
|
+
listAuthPasskeys(input: {
|
|
2655
|
+
userId: string;
|
|
2656
|
+
}): Promise<Record<string, unknown>[]>;
|
|
2657
|
+
/** Delete a passkey by id (requires the passkey plugin). */
|
|
2658
|
+
deleteAuthPasskey(input: {
|
|
2659
|
+
passkeyId: string;
|
|
2660
|
+
}): Promise<void>;
|
|
2661
|
+
/** Disable two-factor auth for a user (requires the two-factor plugin). */
|
|
2662
|
+
disableAuthTwoFactor(input: {
|
|
2663
|
+
userId: string;
|
|
2664
|
+
}): Promise<void>;
|
|
2665
|
+
/** List organizations, paged (requires the organization plugin). */
|
|
2666
|
+
listAuthOrganizations(options?: {
|
|
2667
|
+
limit?: number;
|
|
2668
|
+
offset?: number;
|
|
2669
|
+
}): Promise<AuthPage<Record<string, unknown>>>;
|
|
2670
|
+
/** List the members of an organization (requires the organization plugin). */
|
|
2671
|
+
listAuthOrgMembers(input: {
|
|
2672
|
+
limit?: number;
|
|
2673
|
+
offset?: number;
|
|
2674
|
+
organizationId: string;
|
|
2675
|
+
}): Promise<AuthPage<Record<string, unknown>>>;
|
|
2676
|
+
/** List an organization's pending invitations (requires the organization plugin). */
|
|
2677
|
+
listAuthOrgInvitations(input: {
|
|
2678
|
+
limit?: number;
|
|
2679
|
+
offset?: number;
|
|
2680
|
+
organizationId: string;
|
|
2681
|
+
}): Promise<AuthPage<Record<string, unknown>>>;
|
|
2682
|
+
/** Remove a member from an organization. */
|
|
2683
|
+
removeAuthOrgMember(input: {
|
|
2684
|
+
memberId: string;
|
|
2685
|
+
}): Promise<void>;
|
|
2686
|
+
/** Cancel a pending organization invitation. */
|
|
2687
|
+
cancelAuthOrgInvitation(input: {
|
|
2688
|
+
invitationId: string;
|
|
2689
|
+
}): Promise<void>;
|
|
2690
|
+
/**
|
|
2691
|
+
* Report the deployment's auth configuration — enabled plugins, sign-in
|
|
2692
|
+
* methods, user-settable create-user fields, organization sub-features
|
|
2693
|
+
* (teams / roles), and session / rate-limit policy. Drives the config panel
|
|
2694
|
+
* and the dynamic create-user form. Never carries a secret.
|
|
2695
|
+
*/
|
|
2696
|
+
getAuthConfig(): Promise<AuthConfigInfo>;
|
|
2697
|
+
/** Create an organization; optionally seed an `owner` member for `ownerId`. */
|
|
2698
|
+
createAuthOrganization(input: {
|
|
2699
|
+
logo?: string;
|
|
2700
|
+
metadata?: Record<string, unknown>;
|
|
2701
|
+
name: string;
|
|
2702
|
+
ownerId?: string;
|
|
2703
|
+
slug?: string;
|
|
2704
|
+
}): Promise<Record<string, unknown>>;
|
|
2705
|
+
/** Update an organization's name/slug/logo/metadata. */
|
|
2706
|
+
updateAuthOrganization(input: {
|
|
2707
|
+
logo?: string;
|
|
2708
|
+
metadata?: Record<string, unknown>;
|
|
2709
|
+
name?: string;
|
|
2710
|
+
organizationId: string;
|
|
2711
|
+
slug?: string;
|
|
2712
|
+
}): Promise<Record<string, unknown>>;
|
|
2713
|
+
/** Delete an organization and cascade its members, invitations, teams, and custom roles. */
|
|
2714
|
+
deleteAuthOrganization(input: {
|
|
2715
|
+
organizationId: string;
|
|
2716
|
+
}): Promise<void>;
|
|
2717
|
+
/** Directly add an existing user to an organization (no invitation/acceptance). */
|
|
2718
|
+
addAuthOrgMember(input: {
|
|
2719
|
+
organizationId: string;
|
|
2720
|
+
role?: string;
|
|
2721
|
+
userId: string;
|
|
2722
|
+
}): Promise<Record<string, unknown>>;
|
|
2723
|
+
/** Create a pending email invitation to an organization. */
|
|
2724
|
+
inviteAuthOrgMember(input: {
|
|
2725
|
+
email: string;
|
|
2726
|
+
inviterId?: string;
|
|
2727
|
+
organizationId: string;
|
|
2728
|
+
role?: string;
|
|
2729
|
+
}): Promise<Record<string, unknown>>;
|
|
2730
|
+
/** Change a member's role. */
|
|
2731
|
+
setAuthOrgMemberRole(input: {
|
|
2732
|
+
memberId: string;
|
|
2733
|
+
role: string | string[];
|
|
2734
|
+
}): Promise<Record<string, unknown>>;
|
|
2735
|
+
/** List an organization's teams (requires the organization plugin with teams enabled). */
|
|
2736
|
+
listAuthOrgTeams(input: {
|
|
2737
|
+
limit?: number;
|
|
2738
|
+
offset?: number;
|
|
2739
|
+
organizationId: string;
|
|
2740
|
+
}): Promise<AuthPage<Record<string, unknown>>>;
|
|
2741
|
+
/** Create a team under an organization. */
|
|
2742
|
+
createAuthOrgTeam(input: {
|
|
2743
|
+
name: string;
|
|
2744
|
+
organizationId: string;
|
|
2745
|
+
}): Promise<Record<string, unknown>>;
|
|
2746
|
+
/** Rename a team. */
|
|
2747
|
+
updateAuthOrgTeam(input: {
|
|
2748
|
+
name: string;
|
|
2749
|
+
teamId: string;
|
|
2750
|
+
}): Promise<Record<string, unknown>>;
|
|
2751
|
+
/** Delete a team and its memberships. */
|
|
2752
|
+
removeAuthOrgTeam(input: {
|
|
2753
|
+
teamId: string;
|
|
2754
|
+
}): Promise<void>;
|
|
2755
|
+
/** List a team's members. */
|
|
2756
|
+
listAuthOrgTeamMembers(input: {
|
|
2757
|
+
limit?: number;
|
|
2758
|
+
offset?: number;
|
|
2759
|
+
teamId: string;
|
|
2760
|
+
}): Promise<AuthPage<Record<string, unknown>>>;
|
|
2761
|
+
/** Add a user to a team. */
|
|
2762
|
+
addAuthOrgTeamMember(input: {
|
|
2763
|
+
teamId: string;
|
|
2764
|
+
userId: string;
|
|
2765
|
+
}): Promise<Record<string, unknown>>;
|
|
2766
|
+
/** Remove a member from a team. */
|
|
2767
|
+
removeAuthOrgTeamMember(input: {
|
|
2768
|
+
teamMemberId: string;
|
|
2769
|
+
}): Promise<void>;
|
|
2770
|
+
/** List an organization's custom roles (requires the organization plugin with dynamic access control). */
|
|
2771
|
+
listAuthOrgRoles(input: {
|
|
2772
|
+
limit?: number;
|
|
2773
|
+
offset?: number;
|
|
2774
|
+
organizationId: string;
|
|
2775
|
+
}): Promise<AuthPage<Record<string, unknown>>>;
|
|
2776
|
+
/** Create a custom org role with a permission grant (a `resource -> actions[]` map). */
|
|
2777
|
+
createAuthOrgRole(input: {
|
|
2778
|
+
organizationId: string;
|
|
2779
|
+
permission: Record<string, string[]>;
|
|
2780
|
+
role: string;
|
|
2781
|
+
}): Promise<Record<string, unknown>>;
|
|
2782
|
+
/** Replace a custom org role's permission grant. */
|
|
2783
|
+
updateAuthOrgRole(input: {
|
|
2784
|
+
permission: Record<string, string[]>;
|
|
2785
|
+
roleId: string;
|
|
2786
|
+
}): Promise<Record<string, unknown>>;
|
|
2787
|
+
/** Delete a custom org role. */
|
|
2788
|
+
deleteAuthOrgRole(input: {
|
|
2789
|
+
roleId: string;
|
|
2790
|
+
}): Promise<void>;
|
|
2791
|
+
/** List auth sessions, paged and optionally filtered to one user. */
|
|
2792
|
+
listAuthSessions(options?: {
|
|
2793
|
+
limit?: number;
|
|
2794
|
+
offset?: number;
|
|
2795
|
+
userId?: string;
|
|
2796
|
+
}): Promise<AuthPage<AuthSession>>;
|
|
2797
|
+
/**
|
|
2798
|
+
* Subscribe to a live query. The callback fires with the current value (from
|
|
2799
|
+
* the durable read cache, when one is hydrated) and again on every server
|
|
2800
|
+
* frame; the returned function unsubscribes.
|
|
2801
|
+
*
|
|
2802
|
+
* Subscriptions are deduped by `(functionPath, args, shardKey)` — a second
|
|
2803
|
+
* `subscribe` for the same triple joins the existing registration and shares
|
|
2804
|
+
* its value, cursor and optimistic layers.
|
|
2805
|
+
*
|
|
2806
|
+
* Not available on a `crossTabSync` FOLLOWER tab: the cross-tab channel only
|
|
2807
|
+
* carries the LEADER's own subscriptions outward, so a follower would receive
|
|
2808
|
+
* this query's frames only if the leader happened to hold it too. Throws
|
|
2809
|
+
* `NOT_IMPLEMENTED` there — see {@link LunoraClientOptions.crossTabSync}.
|
|
2810
|
+
*/
|
|
2811
|
+
subscribe<F extends FunctionReference>(function_: F, args: ArgsOf<F>, callback: (data: ReturnOf<F>) => void, options?: {
|
|
2812
|
+
onCheckpoint?: (watermark: SyncWatermark) => void;
|
|
2813
|
+
onError?: SubscriptionErrorCallback;
|
|
2814
|
+
shardKey?: string;
|
|
2815
|
+
}): Unsubscribe;
|
|
2816
|
+
/**
|
|
2817
|
+
* Subscribe to a declarative **shape** — server-side partial replication
|
|
2818
|
+
* scoped by `shardBy` + the shape's predicate + RLS. The parallel to
|
|
2819
|
+
* {@link subscribe} for the poke protocol: the client sends the shape *name* +
|
|
2820
|
+
* validated `args` (never a `where` the client could forge), the server seeds
|
|
2821
|
+
* the current membership as an insert-poke and streams live membership diffs.
|
|
2822
|
+
* Each applied poke materializes the shape's rowset and invokes `callback`.
|
|
2823
|
+
*
|
|
2824
|
+
* Unlike {@link subscribe}, shape subscriptions are NOT deduped by
|
|
2825
|
+
* (name, args): the server resolves them under the socket's verified identity,
|
|
2826
|
+
* so every call gets its own id + view. The returned function unsubscribes.
|
|
2827
|
+
*
|
|
2828
|
+
* Not available on a `crossTabSync` FOLLOWER tab: shape pokes are not part of
|
|
2829
|
+
* the leader→follower broadcast set, so a follower's shape could never
|
|
2830
|
+
* resolve. Throws `NOT_IMPLEMENTED` there — see
|
|
2831
|
+
* {@link LunoraClientOptions.crossTabSync}.
|
|
2832
|
+
*/
|
|
2833
|
+
subscribeShape(shape: {
|
|
2834
|
+
args?: Record<string, unknown>;
|
|
2835
|
+
name: string;
|
|
2836
|
+
}, callback: ShapeCallback, options?: {
|
|
2837
|
+
onCheckpoint?: (watermark: SyncWatermark) => void;
|
|
2838
|
+
onError?: SubscriptionErrorCallback;
|
|
2839
|
+
shardKey?: string;
|
|
2840
|
+
}): Unsubscribe;
|
|
2841
|
+
/**
|
|
2842
|
+
* Open a streaming query. The function reference must be a
|
|
2843
|
+
* `kind:"stream"` registration (built with `c.query.input(...).stream(...)`);
|
|
2844
|
+
* the type constraint catches accidental use of a query/mutation/action
|
|
2845
|
+
* reference at compile time. The returned iterable yields one element per
|
|
2846
|
+
* chunk frame the server pushes, terminating when the server sends
|
|
2847
|
+
* `complete` or the consumer calls `.cancel()`. Errors arrive as a
|
|
2848
|
+
* rejection on the next `next()`.
|
|
2849
|
+
*
|
|
2850
|
+
* Streams ride the same WS as subscriptions and share the unsubscribe
|
|
2851
|
+
* channel: cancelling sends `{type:"unsubscribe", id}` with the stream id,
|
|
2852
|
+
* which the DO recognises as an abort signal for the in-flight iterator.
|
|
2853
|
+
*
|
|
2854
|
+
* Stream-start frames buffered while the socket is (re)connecting are
|
|
2855
|
+
* capped at {@link MAX_PENDING_STREAMS} per connection — overflowing the
|
|
2856
|
+
* cap drops the oldest queued frame (and fails its consumer) so a stuck
|
|
2857
|
+
* reconnect can't OOM the page.
|
|
2858
|
+
*/
|
|
2859
|
+
stream<F extends FunctionReference<"stream">>(function_: F, args: ArgsOf<F>, options?: {
|
|
2860
|
+
durable?: boolean;
|
|
2861
|
+
maxBuffer?: number;
|
|
2862
|
+
shardKey?: string;
|
|
2863
|
+
}): StreamIterable<ReturnOf<F>>;
|
|
2864
|
+
/**
|
|
2865
|
+
* Open a typed **HTTP-SSE route stream** (`httpRoute.<verb>(path).stream()`).
|
|
2866
|
+
* Distinct from {@link LunoraClient.stream}, which consumes the WS procedure
|
|
2867
|
+
* stream (`kind: "stream"`): this one opens the route's own URL with `fetch`
|
|
2868
|
+
* and parses the Server-Sent Events framing the route pump writes (`data:`
|
|
2869
|
+
* chunks, a final `event: complete`, an `event: error` on throw).
|
|
2870
|
+
*
|
|
2871
|
+
* The reference comes from the generated `httpStreams.*` registry, so the
|
|
2872
|
+
* yielded chunk type is the route handler's yielded type. Cancelling the
|
|
2873
|
+
* returned iterable (or aborting `options.signal`) aborts the fetch, which
|
|
2874
|
+
* the server handler observes via its `signal`. The client's bearer token
|
|
2875
|
+
* (when set) rides as an `authorization` header.
|
|
2876
|
+
* @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
|
|
2877
|
+
*/
|
|
2878
|
+
httpStream<Ref extends HttpStreamRef>(route: Ref, args?: HttpStreamArgsOf<Ref>, options?: {
|
|
2879
|
+
headers?: Record<string, string>;
|
|
2880
|
+
maxBuffer?: number;
|
|
2881
|
+
signal?: AbortSignal;
|
|
2882
|
+
}): StreamIterable<HttpStreamChunkOf<Ref>>;
|
|
2883
|
+
close(): void;
|
|
2884
|
+
/** Guard shared by every public entry point: a closed client accepts no further calls. */
|
|
2885
|
+
private assertOpen;
|
|
2886
|
+
/**
|
|
2887
|
+
* `true` when this tab is a cross-tab FOLLOWER of a live leader — i.e. it
|
|
2888
|
+
* will not open a socket of its own and another tab is known to hold one.
|
|
2889
|
+
*
|
|
2890
|
+
* Deliberately NOT just `!isLeader()`. Every `crossTabSync` client is a
|
|
2891
|
+
* non-leader for the first `leaderTimeout` of its life, while its
|
|
2892
|
+
* claim-leadership probe is outstanding; a lone tab self-promotes at the end
|
|
2893
|
+
* of that window and `onBecomeLeader` opens the sockets and replays every
|
|
2894
|
+
* registered subscription. That window is a legitimate, self-healing defer,
|
|
2895
|
+
* not a failure. A KNOWN leader on another tab is the state that never heals.
|
|
2896
|
+
*/
|
|
2897
|
+
private followsAnotherTab;
|
|
2898
|
+
/**
|
|
2899
|
+
* Reject a call that needs a socket this tab will never have.
|
|
2900
|
+
*
|
|
2901
|
+
* The cross-tab protocol is one-directional: a leader broadcasts
|
|
2902
|
+
* `subscription-data` / `-error` / `-settled` / `connection-status` to
|
|
2903
|
+
* followers, and a follower has no frame with which to tell the leader what
|
|
2904
|
+
* it needs (see `cross-tab.ts`'s `WsFollowerMessage`, which is exactly
|
|
2905
|
+
* heartbeat / claim-leadership / yield-leadership). `subscribeShape` /
|
|
2906
|
+
* `whisper*` / `setConnectionContext` / `acquireConnectionContext` — none of
|
|
2907
|
+
* which the leader broadcasts at all — therefore never worked on a follower
|
|
2908
|
+
* under any circumstances: each returned a handle that looked live, fired no
|
|
2909
|
+
* callback, raised no error, and reported `connectionStatus() ===
|
|
2910
|
+
* "connected"` (mirrored from the leader).
|
|
2911
|
+
*
|
|
2912
|
+
* `subscribe` is deliberately NOT in that set. A follower's `subscribe`
|
|
2913
|
+
* registers the key the leader's broadcast is matched against, so it is the
|
|
2914
|
+
* mechanism the relay is built on rather than a surface that silently fails.
|
|
2915
|
+
* A follower sees a query only while the leader holds the same
|
|
2916
|
+
* `(fn, args, shardKey)` — that is the documented shape of the option, not a
|
|
2917
|
+
* defect.
|
|
2918
|
+
*
|
|
2919
|
+
* `stream()` reaches the same outcome by a different route: it fails the
|
|
2920
|
+
* handle it returns rather than throwing at the call. See
|
|
2921
|
+
* {@link LunoraClientOptions.crossTabSync} for what the option does and does
|
|
2922
|
+
* not cover.
|
|
2923
|
+
*/
|
|
2924
|
+
private assertLeaderOwnedSurface;
|
|
2925
|
+
/**
|
|
2926
|
+
* Clear every timer a {@link ShardConnection} can have armed.
|
|
2927
|
+
*
|
|
2928
|
+
* One function because both teardown paths must clear all three and a fourth
|
|
2929
|
+
* timer would otherwise have to be remembered in two places — which is how a
|
|
2930
|
+
* leak gets added rather than written.
|
|
2931
|
+
*/
|
|
2932
|
+
private clearConnectionTimers;
|
|
2933
|
+
/**
|
|
2934
|
+
* Tear down one {@link ShardConnection}'s live state: clear its reconnect/
|
|
2935
|
+
* connect timers, stop its heartbeat, and close its socket (if any).
|
|
2936
|
+
* Shared by `close()` (terminal) and the cross-tab `onStopBeingLeader`
|
|
2937
|
+
* handler (demoted, but still alive) so a demoted leader can't leak a
|
|
2938
|
+
* pending `reconnectTimer` or an open socket's `heartbeatTimer` the way
|
|
2939
|
+
* an inline `conn.socket?.close()` — which skips both — used to.
|
|
2940
|
+
*
|
|
2941
|
+
* Settles this shard's in-flight streams first. The teardown clears
|
|
2942
|
+
* `conn.socket` BEFORE the real `close` event fires, so that event trips
|
|
2943
|
+
* `openManagedSocket`'s identity guard (`conn.socket !== socket`) and
|
|
2944
|
+
* returns — meaning `handleDisconnect`, the only other place that settles a
|
|
2945
|
+
* shard's streams, never runs for this connection again. `close()` already
|
|
2946
|
+
* failed and cleared `this.streams` before it gets here, so this is a no-op
|
|
2947
|
+
* on that path; the cross-tab demotion path is the one where a consumer's
|
|
2948
|
+
* `for await` used to block forever with no error and no completion.
|
|
2949
|
+
*/
|
|
2950
|
+
private teardownConnection;
|
|
2951
|
+
/**
|
|
2952
|
+
* Build (but do not start) this client's `TabCoordinator`. Extracted out of
|
|
2953
|
+
* the constructor so `setAuthToken` can rebuild it on an identity change —
|
|
2954
|
+
* the default channel name embeds the identity fingerprint (see below), so
|
|
2955
|
+
* a new identity needs a new coordinator on a new channel. The callback
|
|
2956
|
+
* bodies are the drift-sensitive region (a hand-merged identity guard on
|
|
2957
|
+
* the shard message listener sits ahead of an extracted `lastFrameAt`
|
|
2958
|
+
* stamp elsewhere in this file) — moved verbatim, not reflowed.
|
|
2959
|
+
*/
|
|
2960
|
+
private createTabCoordinator;
|
|
2961
|
+
/**
|
|
2962
|
+
* Persist a mutation that can't go out on the wire right now (offline, or
|
|
2963
|
+
* mid-reconnect after a prior connect). The optimistic update has already
|
|
2964
|
+
* been applied by `mutation`; this only chooses the durable write path and
|
|
2965
|
+
* rolls the optimistic write back if persistence is rejected.
|
|
2966
|
+
*
|
|
2967
|
+
* Two paths: when an `outbox` sink is wired (the `@lunora/db` executor) it
|
|
2968
|
+
* owns persistence + at-least-once replay, so we delegate and return
|
|
2969
|
+
* optimistically (confirmation rides the synced view). Otherwise the
|
|
2970
|
+
* built-in `OfflineQueue` resolves/rejects the returned promise on replay.
|
|
2971
|
+
*/
|
|
2972
|
+
private enqueueOfflineMutation;
|
|
2973
|
+
/**
|
|
2974
|
+
* Restore offline mutations persisted in a prior session and open a socket
|
|
2975
|
+
* for each shard they target so they flush once the WS reconnects. Failures
|
|
2976
|
+
* are swallowed — a broken durable store must not stop the client booting.
|
|
2977
|
+
*/
|
|
2978
|
+
private hydratePersistedQueue;
|
|
2979
|
+
/**
|
|
2980
|
+
* Re-queue the durable offline writes — but only as the multi-tab LEADER. The
|
|
2981
|
+
* persisted queue is shared across a profile's tabs; without coordination
|
|
2982
|
+
* every tab would re-queue and replay the same writes (correct only because
|
|
2983
|
+
* the server dedups by idempotency key, but wasteful + racy). A Web Lock makes
|
|
2984
|
+
* exactly one tab hydrate; it holds the lock for its lifetime, so when it
|
|
2985
|
+
* closes another tab acquires the lock and takes over. Falls back to
|
|
2986
|
+
* unconditional hydration where Web Locks are unavailable (React Native, older
|
|
2987
|
+
* browsers, SSR) — single-context there, so no coordination is needed.
|
|
2988
|
+
*/
|
|
2989
|
+
private hydrateAsOutboxLeader;
|
|
2990
|
+
/**
|
|
2991
|
+
* Load every cached query into {@link hydratedQueryCache} so the next
|
|
2992
|
+
* `subscribe()` for each key seeds its initial value off disk. A
|
|
2993
|
+
* subscription created before this resolves simply misses the cache (it
|
|
2994
|
+
* gets a live snapshot as before); the gate at seed time also drops any
|
|
2995
|
+
* entry whose stamped identity no longer matches the current one.
|
|
2996
|
+
*/
|
|
2997
|
+
private hydrateQueryCache;
|
|
2998
|
+
/**
|
|
2999
|
+
* Consume the hydrated read-cache entry for a key (if any), gated on
|
|
3000
|
+
* identity. The entry is removed whether or not it matches — the cache only
|
|
3001
|
+
* ever seeds a subscription's first value. A mismatch (the cache was written
|
|
3002
|
+
* under a different identity) yields `undefined` so a signed-out cache never
|
|
3003
|
+
* leaks into a new session.
|
|
3004
|
+
*/
|
|
3005
|
+
private takeHydratedCache;
|
|
3006
|
+
/**
|
|
3007
|
+
* Queue a coalesced read-cache write for a subscription's current value.
|
|
3008
|
+
* Latest-wins per key; flushed on a short debounce so a delta burst writes
|
|
3009
|
+
* once. No-op when the read cache is disabled or the value is undefined
|
|
3010
|
+
* (nothing to render offline).
|
|
3011
|
+
*/
|
|
3012
|
+
private persistQueryValue;
|
|
3013
|
+
/** Drain {@link pendingCacheWrites} to the durable store. */
|
|
3014
|
+
private flushQueryCacheWrites;
|
|
3015
|
+
/** Derive the aggregate status from the per-shard socket states. */
|
|
3016
|
+
private computeStatus;
|
|
3017
|
+
/** Recompute the aggregate status and notify listeners if it changed. */
|
|
3018
|
+
private emitConnectionStatus;
|
|
3019
|
+
/**
|
|
3020
|
+
* Build a {@link MutationSettledEvent} from a queued entry and emit it on the
|
|
3021
|
+
* {@link onMutationSettled} channel. `item.id` is always assigned by the time
|
|
3022
|
+
* a write settles (`enqueue`/`hydrate` guarantee it), so the `?? ""` fallback
|
|
3023
|
+
* is unreachable — present only to satisfy the optional queue-id type.
|
|
3024
|
+
*/
|
|
3025
|
+
private emitItemSettled;
|
|
3026
|
+
/**
|
|
3027
|
+
* Apply an optimistic update to the subscription that matches the mutation's
|
|
3028
|
+
* `(functionRef, args, shardKey)` triple, returning the rollback callbacks to
|
|
3029
|
+
* invoke if the mutation later fails.
|
|
3030
|
+
*
|
|
3031
|
+
* The registry is already indexed by exactly this triple via
|
|
3032
|
+
* `SubscriptionRegistry.key`, so at most one subscription can match. A direct
|
|
3033
|
+
* O(1) keyed lookup replaces the former O(N) linear scan over all subscriptions.
|
|
3034
|
+
*
|
|
3035
|
+
* `shardKey` normalization: both `undefined` and `""` map to the empty string
|
|
3036
|
+
* inside `SubscriptionRegistry.key` (via `?? ""`), so a mutation fired without
|
|
3037
|
+
* a shardKey correctly matches a subscription registered without one regardless
|
|
3038
|
+
* of whether the caller passed `undefined` or omitted the field.
|
|
3039
|
+
*/
|
|
3040
|
+
private applyOptimisticUpdates;
|
|
3041
|
+
/**
|
|
3042
|
+
* Run a Convex-parity `optimisticUpdate` callback against a localStore bound
|
|
3043
|
+
* to the live subscription registry. Each `setQuery` registers a constant
|
|
3044
|
+
* optimistic LAYER on its target subscription (via the same engine the
|
|
3045
|
+
* per-call `optimistic` path uses), so the multi-query patch rebases onto
|
|
3046
|
+
* incoming deltas and drops gaplessly on its commit cursor — its `confirm` /
|
|
3047
|
+
* `rollback` closures are appended to the mutation's settle lists. A throwing
|
|
3048
|
+
* callback unwinds its own partial writes — LIFO over just the rollbacks it
|
|
3049
|
+
* produced — and is swallowed, so a buggy optimistic update can never fail the
|
|
3050
|
+
* mutation or leave a partial patch live.
|
|
3051
|
+
*/
|
|
3052
|
+
private applyOptimisticUpdate;
|
|
3053
|
+
private getConnection;
|
|
3054
|
+
/**
|
|
3055
|
+
* Send an unsubscribe frame (tagged with its wire type) on the shard's
|
|
3056
|
+
* socket, or queue it for the next reconnect when the send can't go out.
|
|
3057
|
+
*/
|
|
3058
|
+
private sendOrQueueUnsubscribe;
|
|
3059
|
+
/**
|
|
3060
|
+
* The `(wsState, hasSocket, wasEverConnected)` triple `mutation()`'s
|
|
3061
|
+
* offline-queue gate reads. On the leader/single-tab path this is exactly
|
|
3062
|
+
* the real `ShardConnection`'s state (byte-identical to the pre-cross-tab
|
|
3063
|
+
* behavior). A follower has no `ShardConnection` of its own (see
|
|
3064
|
+
* `ensureSocket`), so it derives the same triple from the mirrored
|
|
3065
|
+
* `leaderStatus`/`leaderWasEverConnected` instead: `"connected"` maps to
|
|
3066
|
+
* `"open"` (queue-eligible once `wasEverConnected`), `"connecting"` stays
|
|
3067
|
+
* `"connecting"` (the mid-reconnect queue branch), anything else is
|
|
3068
|
+
* `"idle"`. `hasSocket` is always `false` for a follower — it never has
|
|
3069
|
+
* one.
|
|
3070
|
+
*/
|
|
3071
|
+
private connectionGateState;
|
|
3072
|
+
private getOrCreateConnection;
|
|
3073
|
+
private wsUrlFor;
|
|
3074
|
+
/**
|
|
3075
|
+
* Build the outbound RPC headers: JSON content type, optional bearer auth,
|
|
3076
|
+
* the optional mutation-replay idempotency key, and the D1 read-your-writes
|
|
3077
|
+
* bookmark when the caller opted into `attachBookmark`. The mutation id
|
|
3078
|
+
* rides both the direct send and any offline-queue replay of the same write,
|
|
3079
|
+
* so a mutation the server already committed returns its cached result
|
|
3080
|
+
* instead of running twice.
|
|
3081
|
+
*/
|
|
3082
|
+
private rpcRequestHeaders;
|
|
3083
|
+
private rpc;
|
|
3084
|
+
/**
|
|
3085
|
+
* Authenticated request to a non-RPC admin endpoint (the scheduler list /
|
|
3086
|
+
* cancel routes). Attaches the bearer token, parses JSON, and surfaces the
|
|
3087
|
+
* worker's `{ error: { code, message } }` envelope as a coded `Error` —
|
|
3088
|
+
* mirroring {@link rpc} so callers see the same failure shape.
|
|
3089
|
+
*/
|
|
3090
|
+
private adminFetch;
|
|
3091
|
+
/**
|
|
3092
|
+
* Resolve the effective connection context for a shard: the most-recently
|
|
3093
|
+
* acquired refcounted holder ({@link acquireConnectionContext}) wins, falling
|
|
3094
|
+
* back to the imperative {@link setConnectionContext} override, then the
|
|
3095
|
+
* client-wide default. Returns `undefined` when none apply.
|
|
3096
|
+
*/
|
|
3097
|
+
private effectiveConnectionContext;
|
|
3098
|
+
/** Re-send the `connect` envelope for a shard whose effective context just changed (if its socket is open). */
|
|
3099
|
+
private refreshConnectionContext;
|
|
3100
|
+
/**
|
|
3101
|
+
* Send the one-shot `connect` envelope on an open shard socket. Always sent
|
|
3102
|
+
* once per socket open, so the server's `onConnect` hooks fire symmetrically
|
|
3103
|
+
* with `onDisconnect` (which the DO dispatches unconditionally at close for
|
|
3104
|
+
* every lifecycle-aware socket). The DO no-ops cheaply when no `onConnect`
|
|
3105
|
+
* hooks are registered, so the single frame costs nothing in the common case.
|
|
3106
|
+
*
|
|
3107
|
+
* The shard's registered context (or the client-wide default) rides along
|
|
3108
|
+
* when one is set — the DO records it on the attachment for replay to
|
|
3109
|
+
* `onDisconnect`. A socket with no registered context still announces itself;
|
|
3110
|
+
* the envelope simply omits `context`, which is optional on the wire.
|
|
3111
|
+
* Register a context — e.g. `setConnectionContext({})` — to attach app state
|
|
3112
|
+
* to the lifecycle dispatch.
|
|
3113
|
+
*/
|
|
3114
|
+
private sendConnectEnvelope;
|
|
3115
|
+
/**
|
|
3116
|
+
* Re-send every shape subscription bound to `shardKey` over its (now open)
|
|
3117
|
+
* socket. Each frame carries the shape's last applied checkpoint, so the
|
|
3118
|
+
* server resumes from it — or re-seeds when the cursor fell below CDC
|
|
3119
|
+
* retention or the epoch forked.
|
|
3120
|
+
*/
|
|
3121
|
+
private resendShapeSubscriptions;
|
|
3122
|
+
private ensureSocket;
|
|
3123
|
+
/**
|
|
3124
|
+
* Resolve the {@link WsTokenProvider} and open the shard socket with the
|
|
3125
|
+
* minted token. The connection is already in the `connecting` state, so the
|
|
3126
|
+
* async gap is race-guarded: a client `close()`, a `setWsToken` bounce, or a
|
|
3127
|
+
* competing connect that landed first all abandon this attempt. A provider
|
|
3128
|
+
* failure fails the attempt through {@link handleDisconnect}, which arms the
|
|
3129
|
+
* normal reconnect backoff — a broken mint endpoint degrades to retries, not
|
|
3130
|
+
* a silent tokenless socket the admin gate would reject.
|
|
3131
|
+
*/
|
|
3132
|
+
private openSocketWithProvidedToken;
|
|
3133
|
+
/**
|
|
3134
|
+
* Construct one WebSocket connection attempt and wire the shared
|
|
3135
|
+
* lifecycle guarantees around it — the fail-fast connect-timeout, the
|
|
3136
|
+
* identity guard that stops a superseded attempt's late `open`/`message`/
|
|
3137
|
+
* `close`/`error` from touching a connection a newer attempt already
|
|
3138
|
+
* owns, and (once open) the keepalive heartbeat with its half-open
|
|
3139
|
+
* watchdog (plan 217). One call opens ONE attempt; the caller owns
|
|
3140
|
+
* reconnect scheduling from `onClose` — mirrors the shard's existing
|
|
3141
|
+
* `ensureSocket` / `handleDisconnect` split, now shared with
|
|
3142
|
+
* `subscribeScheduledJobs` so it stops re-living the bug that split
|
|
3143
|
+
* already fixed once (CLIENT-05).
|
|
3144
|
+
*
|
|
3145
|
+
* The identity guard is `conn.socket !== socket`, re-checked before every
|
|
3146
|
+
* action below. `conn.socket` is reassigned to a new attempt's socket
|
|
3147
|
+
* synchronously — right here, before `open` ever fires — so an older
|
|
3148
|
+
* attempt's guard trips the instant it's superseded, even if its
|
|
3149
|
+
* underlying socket only fires its real `close`/`error` much later. This
|
|
3150
|
+
* ordering is load-bearing: preserve it exactly.
|
|
3151
|
+
*/
|
|
3152
|
+
private openManagedSocket;
|
|
3153
|
+
/** Construct the shard socket and wire its lifecycle handlers. The connection must already be in the `connecting` state. */
|
|
3154
|
+
private openSocket;
|
|
3155
|
+
/**
|
|
3156
|
+
* Send the stream-start frames queued while the socket was (re)connecting,
|
|
3157
|
+
* marking each one that lands as started on the server.
|
|
3158
|
+
*/
|
|
3159
|
+
private flushPendingStreams;
|
|
3160
|
+
/**
|
|
3161
|
+
* Tear down a stream the consumer cancelled, telling the server when the
|
|
3162
|
+
* server is the one still holding it.
|
|
3163
|
+
*/
|
|
3164
|
+
private cancelStream;
|
|
3165
|
+
private handleDisconnect;
|
|
3166
|
+
/**
|
|
3167
|
+
* Begin the keepalive heartbeat on an open connection attempt — the only
|
|
3168
|
+
* caller is {@link openManagedSocket}'s own `open` handler, so both the
|
|
3169
|
+
* shard socket and `subscribeScheduledJobs` share this one implementation
|
|
3170
|
+
* instead of each hand-rolling their own (plan 217, generalized).
|
|
3171
|
+
*
|
|
3172
|
+
* Each tick first checks the half-open watchdog (see
|
|
3173
|
+
* {@link ManagedSocketState.lastFrameAt}): if no frame at all has arrived
|
|
3174
|
+
* within `heartbeatIntervalMs * 2.5`, the far end has gone quiet without
|
|
3175
|
+
* the socket ever firing `close` — force it closed and report it through
|
|
3176
|
+
* `onWatchdogTrip` (the caller's `onClose`) so the normal reconnect/backoff
|
|
3177
|
+
* takes over instead of every live query on it silently staling forever.
|
|
3178
|
+
* Otherwise it sends a {@link WS_KEEPALIVE_PING} text frame the server
|
|
3179
|
+
* answers from its hibernation auto-response without waking the DO. A
|
|
3180
|
+
* no-op when the heartbeat is disabled (an interval of zero or less);
|
|
3181
|
+
* idempotent — any existing timer is cleared first so a reconnect can't
|
|
3182
|
+
* leak intervals.
|
|
3183
|
+
*/
|
|
3184
|
+
private startHeartbeat;
|
|
3185
|
+
/** Clear a connection's keepalive timer, if any. Safe to call repeatedly. */
|
|
3186
|
+
private stopHeartbeat;
|
|
3187
|
+
/** Mark every subscription bound to `shardKey` as needing a fresh ack. */
|
|
3188
|
+
private markShardPendingAck;
|
|
3189
|
+
private sendSubscribeIfOpen;
|
|
3190
|
+
private sendShapeSubscribeIfOpen;
|
|
3191
|
+
private handleServerMessage;
|
|
3192
|
+
private handleErrorMessage;
|
|
3193
|
+
/** Buffer key for an in-flight poke: `pokeId` is only unique per shard socket, so it is scoped by connection. */
|
|
3194
|
+
private pokeBufferKey;
|
|
3195
|
+
private handlePokeStart;
|
|
3196
|
+
private handlePokePart;
|
|
3197
|
+
private handlePokeEnd;
|
|
3198
|
+
/**
|
|
3199
|
+
* Commit one shape's slice of a poke, or refuse it and re-seed.
|
|
3200
|
+
*
|
|
3201
|
+
* Split out of {@link handlePokeEnd} because every decision here is PER SHAPE
|
|
3202
|
+
* — the reset flag, the base checkpoint, the watermark — while the poke
|
|
3203
|
+
* envelope around it is not.
|
|
3204
|
+
*/
|
|
3205
|
+
private applyPokePart;
|
|
3206
|
+
/**
|
|
3207
|
+
* Force the server to re-send a full snapshot for `state`, leaving the
|
|
3208
|
+
* currently displayed value alone until it lands. Used when a delta frame
|
|
3209
|
+
* cannot be applied: dropping the resume cursor is what makes the resubscribe
|
|
3210
|
+
* a snapshot rather than a `resume`, and un-acking is what lets
|
|
3211
|
+
* `sendSubscribeIfOpen` put the frame on the wire at all. Mirrors the shape
|
|
3212
|
+
* path's re-seed on a diverged base.
|
|
3213
|
+
*/
|
|
3214
|
+
private resnapshotSubscription;
|
|
3215
|
+
/** Materialize a shape's keyed view to an array and invoke its callbacks. */
|
|
3216
|
+
private emitShapeRows;
|
|
3217
|
+
private handleDataMessage;
|
|
3218
|
+
/**
|
|
3219
|
+
* Handle a `resume` frame (Pillar 1b): the server proved nothing the
|
|
3220
|
+
* subscription reads changed since our `sinceSeq`, so the cached value is
|
|
3221
|
+
* still current. We keep `lastValue` as-is, mark the sub acked, and advance
|
|
3222
|
+
* the cursor (re-persisting so the next reconnect resumes from the newer
|
|
3223
|
+
* watermark). No callback fires — the value didn't change, and `subscribe()`
|
|
3224
|
+
* already replayed the cached value to every consumer synchronously.
|
|
3225
|
+
*/
|
|
3226
|
+
private handleResumeMessage;
|
|
3227
|
+
/**
|
|
3228
|
+
* Handle a `settled` frame: a write touched one of this subscription's read
|
|
3229
|
+
* tables but produced a byte-identical result, so the server suppressed the
|
|
3230
|
+
* data frame. Like {@link handleResumeMessage} the value didn't change — we
|
|
3231
|
+
* advance the resume position and re-persist — but we ALSO surface the echoed
|
|
3232
|
+
* custom-mutator watermark via `onCheckpoint` so a `@lunora/db` list
|
|
3233
|
+
* collection drops the optimistic overlay for the confirmed write (otherwise
|
|
3234
|
+
* its checkpoint gate, fed only by data frames, would hang forever). Sent
|
|
3235
|
+
* only to custom-mutator clients; plain `useQuery` subscribers leave
|
|
3236
|
+
* `onCheckpoint` unset and this is a near no-op.
|
|
3237
|
+
*/
|
|
3238
|
+
private handleSettledMessage;
|
|
3239
|
+
/**
|
|
3240
|
+
* Mark `state` acked and, when the frame carries a newer cursor/epoch than
|
|
3241
|
+
* the cached position, advance the resume watermark and re-persist. Shared by
|
|
3242
|
+
* the `resume` and `settled` frame handlers — both acknowledge "nothing the
|
|
3243
|
+
* client must re-render changed, but the resume position may have moved".
|
|
3244
|
+
*/
|
|
3245
|
+
private ackAndAdvanceCursor;
|
|
3246
|
+
/**
|
|
3247
|
+
* Resolve the value to publish for a `data`/`delta` frame.
|
|
3248
|
+
*
|
|
3249
|
+
* A `data` frame is an authoritative snapshot (the server re-execution path)
|
|
3250
|
+
* and always replaces the cached value wholesale. A `delta` frame carrying a
|
|
3251
|
+
* structured `MutationDelta` (the `broadcastDelta` row-change path) is
|
|
3252
|
+
* merged incrementally into the cached list — preserving order, no dup/loss —
|
|
3253
|
+
* so each subscription (including every paginated page) updates by delta
|
|
3254
|
+
* rather than a full re-send. We fall back to full replacement when the
|
|
3255
|
+
* delta isn't a recognisable row change, when there's no cached value yet,
|
|
3256
|
+
* or when it can't be applied cleanly against the current cached shape.
|
|
3257
|
+
*/
|
|
3258
|
+
private resolveDataPayload;
|
|
3259
|
+
/** Route an inbound whisper to the topic's handlers on the originating shard. */
|
|
3260
|
+
private dispatchWhisper;
|
|
3261
|
+
/** Notify every {@link onTokenExpired} listener (best-effort, listener throws swallowed). */
|
|
3262
|
+
private notifyTokenExpired;
|
|
3263
|
+
/**
|
|
3264
|
+
* CLIENT-04: `type: "complete"` today is sent ONLY by `@lunora/do`'s
|
|
3265
|
+
* `handleStream` (see `shard-do.ts`), gated to the `stream` envelope type
|
|
3266
|
+
* and minting only `stream_*` ids — the `subscribe` path never sends it, so
|
|
3267
|
+
* a live SUBSCRIPTION provably never receives `complete` from the current
|
|
3268
|
+
* server. But `ServerCompleteMessage` is a generic `id`-keyed frame and
|
|
3269
|
+
* `ShardDO` is user-subclassable, so this stays defensive rather than
|
|
3270
|
+
* assuming a `sub_*` id can never reach here: unlike the historical
|
|
3271
|
+
* `subscriptions.remove(state)`, which dropped the state out of
|
|
3272
|
+
* `subscriptions.all()` — the set the reconnect resubscribe loop walks
|
|
3273
|
+
* (`ensureSocket`'s `open` handler) — and so froze the query forever across
|
|
3274
|
+
* every future reconnect, this fans a cancellation error to any listener
|
|
3275
|
+
* and marks the registration un-acked instead. Non-destructive: the state
|
|
3276
|
+
* stays in the registry, so the very next reconnect resubscribes it. The
|
|
3277
|
+
* two id-spaces don't overlap (`sub_*` vs `stream_*`), so the stream and
|
|
3278
|
+
* subscription lookups below are mutually exclusive.
|
|
3279
|
+
*/
|
|
3280
|
+
private handleCompleteMessage;
|
|
3281
|
+
private unpersist;
|
|
3282
|
+
/**
|
|
3283
|
+
* Stable, non-reversible fingerprint of the current auth identity used to
|
|
3284
|
+
* stamp queued offline writes. `null` (signed out) is its own identity and
|
|
3285
|
+
* never matches a bearer-token fingerprint. The raw token is never stored;
|
|
3286
|
+
* a length-prefixed FNV-1a hash is enough to detect an identity *change*
|
|
3287
|
+
* without keeping the credential around in the queue map.
|
|
3288
|
+
*/
|
|
3289
|
+
private identityFingerprint;
|
|
3290
|
+
/**
|
|
3291
|
+
* Stable token-hash fingerprint of a bearer token (the `<len>:<fnv>:<djb2>`
|
|
3292
|
+
* format a token-stamped queued write carries). Extracted so the replay gate
|
|
3293
|
+
* can recompute the hash of the current credential and recognise a write
|
|
3294
|
+
* stamped under it — even after the fingerprint was relabelled to a subject.
|
|
3295
|
+
*
|
|
3296
|
+
* Two independent 32-bit passes (FNV-1a + djb2) give a ~64-bit digest, so
|
|
3297
|
+
* two distinct equal-length tokens are astronomically unlikely to share a
|
|
3298
|
+
* fingerprint. A single 32-bit hash collides ~1-in-4e9 per equal-length
|
|
3299
|
+
* pair — enough that, on a shared device, user B could hydrate A's cached
|
|
3300
|
+
* reads. Different algorithms (not the same FNV with a different seed, which
|
|
3301
|
+
* would be affine-related) keep the two passes genuinely independent.
|
|
3302
|
+
* Still synchronous (no crypto) and stable across surrogate pairs.
|
|
3303
|
+
*/
|
|
3304
|
+
private hashToken;
|
|
3305
|
+
/**
|
|
3306
|
+
* True when `stamped` is a token-hash of the SAME credential still held now,
|
|
3307
|
+
* even though the live identity has since been relabelled to a subject. Covers
|
|
3308
|
+
* `setAuthToken(token, userId)` where the subject resolved a tick after the
|
|
3309
|
+
* token was set: a write persisted (or requeued) under the token hash must
|
|
3310
|
+
* still replay — the credential never changed, only its label — instead of
|
|
3311
|
+
* being dropped as an identity mismatch. This is the durable counterpart to
|
|
3312
|
+
* {@link restampQueuedIdentity}, which only relabels the in-memory live stamp
|
|
3313
|
+
* (consumed on the first flush) and never touches `item.identity` or the
|
|
3314
|
+
* persisted record, so a reload or a transient-failure requeue would otherwise
|
|
3315
|
+
* fall back to the stale token-hash and wrongly reject the same user's write.
|
|
3316
|
+
*/
|
|
3317
|
+
private isSameCredentialUnderTokenHash;
|
|
3318
|
+
/**
|
|
3319
|
+
* Drain every in-memory offline write and reject it because the auth
|
|
3320
|
+
* identity changed. Durable entries are also dropped from persistence so a
|
|
3321
|
+
* later `hydrate` can't resurrect another user's writes. Stamps are cleared
|
|
3322
|
+
* alongside. Persisted entries restored without a live awaiter still get
|
|
3323
|
+
* unpersisted here.
|
|
3324
|
+
*/
|
|
3325
|
+
private rejectQueuedForIdentityChange;
|
|
3326
|
+
/**
|
|
3327
|
+
* Migrate every identity stamp from `from` to `to` — used when the auth
|
|
3328
|
+
* identity label changes but the underlying credential (token) does NOT, e.g.
|
|
3329
|
+
* the user id resolves a tick after the token was set. The in-memory
|
|
3330
|
+
* `queuedIdentities` map is the flush-time source of truth, so re-stamping it
|
|
3331
|
+
* keeps the in-flight writes replayable under the new (more stable) identity
|
|
3332
|
+
* instead of the flush guard discarding them as a mismatch.
|
|
3333
|
+
*
|
|
3334
|
+
* That map alone was not enough: it is consumed and DELETED on the first
|
|
3335
|
+
* flush attempt (`passesReplayIdentityGate`), while the queue entry and its
|
|
3336
|
+
* persisted record keep the original stamp. So a reload, or a requeue after a
|
|
3337
|
+
* transient failure, fell back to the old token hash — and once the token had
|
|
3338
|
+
* been refreshed, `isSameCredentialUnderTokenHash` no longer recognised it
|
|
3339
|
+
* and the write was rejected `OFFLINE_IDENTITY_CHANGED` for the very user
|
|
3340
|
+
* `setAuthToken`'s sticky-`subject` contract promises to protect. The queue's
|
|
3341
|
+
* own re-stamp covers both the entry and its durable record.
|
|
3342
|
+
*/
|
|
3343
|
+
private restampQueuedIdentity;
|
|
3344
|
+
/**
|
|
3345
|
+
* Migrate the {@link clientWatermarks} bucket map nested under identity
|
|
3346
|
+
* `from` to identity `to` — the sibling of {@link restampQueuedIdentity},
|
|
3347
|
+
* for the same same-credential-subject-resolves case. Without this, a
|
|
3348
|
+
* bucket's watermark cached under the token-hash fingerprint would look
|
|
3349
|
+
* unset once the fingerprint relabels to `subj:…`, so the next push
|
|
3350
|
+
* re-derives `1` against a server watermark the DO already advanced — the
|
|
3351
|
+
* OUT_OF_ORDER wedge this cache-keying scheme exists to fix, reintroduced
|
|
3352
|
+
* by the fix itself. `to` may already hold a bucket map (switching back to
|
|
3353
|
+
* an identity that has its own cached watermarks); merge into it rather
|
|
3354
|
+
* than clobbering it, with `from`'s entries winning on a colliding bucket —
|
|
3355
|
+
* the same overwrite a plain `Map.set` would have done before this was a
|
|
3356
|
+
* nested map.
|
|
3357
|
+
*/
|
|
3358
|
+
private restampWatermarks;
|
|
3359
|
+
/**
|
|
3360
|
+
* Drop the durable read cache on an identity change so a cached value stamped
|
|
3361
|
+
* under the previous identity can never hydrate into a new session. Clears
|
|
3362
|
+
* the in-flight write batch and the not-yet-consumed hydrated entries too;
|
|
3363
|
+
* the durable `clear()` is best-effort.
|
|
3364
|
+
*/
|
|
3365
|
+
private clearQueryCacheForIdentityChange;
|
|
3366
|
+
/**
|
|
3367
|
+
* Flush every shard with a mutation currently queued in `offlineQueue`
|
|
3368
|
+
* (see `queuedOfflineShardKeys`). Used on a FOLLOWER tab when the
|
|
3369
|
+
* mirrored leader status transitions to `"connected"` — a follower has no
|
|
3370
|
+
* per-shard `ShardConnection` reconnect event to hang the usual
|
|
3371
|
+
* single-shard `flushOfflineQueue(shardKey)` call off of (see the
|
|
3372
|
+
* `handleConnect` call site), so this walks every shard that might have
|
|
3373
|
+
* something queued instead. Flushing an already-empty shard is a cheap
|
|
3374
|
+
* no-op (`flushOfflineQueue` returns immediately once `drain` yields
|
|
3375
|
+
* nothing), so over-inclusion here is harmless.
|
|
3376
|
+
*/
|
|
3377
|
+
private flushAllOfflineQueues;
|
|
3378
|
+
/**
|
|
3379
|
+
* Replay a shard's queued writes, serialized per shard and published as
|
|
3380
|
+
* {@link offlineFlushes} so a concurrent `mutation()` can wait behind it.
|
|
3381
|
+
* Never rejects: every entry's outcome is settled individually inside
|
|
3382
|
+
* {@link drainOfflineQueue}, and a poisoned chain would strand every later
|
|
3383
|
+
* flush AND every write waiting on the barrier.
|
|
3384
|
+
*/
|
|
3385
|
+
private flushOfflineQueue;
|
|
3386
|
+
private drainOfflineQueue;
|
|
3387
|
+
/**
|
|
3388
|
+
* Partition already-gated writes into the encodable ones (returned) and reject
|
|
3389
|
+
* the rest terminally. A write whose args can't be wire-encoded (e.g. a RegExp
|
|
3390
|
+
* or class instance in a `v.any()` field) can NEVER replay — the codec failure
|
|
3391
|
+
* is deterministic, not transient. Rejecting here is essential: otherwise
|
|
3392
|
+
* `encodeWire` throws mid-flush, is classified as transient (a codec error has
|
|
3393
|
+
* no `.code`), and re-queues forever — a silent hang where the caller's Promise
|
|
3394
|
+
* never settles and the optimistic write never rolls back. Encoding is cheap;
|
|
3395
|
+
* the flush is the slow reconnect path.
|
|
3396
|
+
*/
|
|
3397
|
+
private encodableOrSettleTerminal;
|
|
3398
|
+
/**
|
|
3399
|
+
* Identity guard for one queued write about to replay: a write stamped under
|
|
3400
|
+
* one identity must never replay under another. The live `queuedIdentities`
|
|
3401
|
+
* map is the source of truth for the current session; a hydrated write whose
|
|
3402
|
+
* id isn't in the map falls back to the stamp persisted with the record
|
|
3403
|
+
* (`item.identity`), so a reload can't replay another user's queued writes.
|
|
3404
|
+
* Only legacy records (persisted before stamps were durable —
|
|
3405
|
+
* `item.identity === undefined`) replay under whatever identity is current.
|
|
3406
|
+
*
|
|
3407
|
+
* `Map.get` returns `undefined` for unstamped/hydrated ids and `item.identity`
|
|
3408
|
+
* is `undefined` for legacy records; a persisted `null` (queued while signed
|
|
3409
|
+
* out) is a real value that must not collapse into `undefined` — hence the
|
|
3410
|
+
* explicit `=== undefined` check rather than `??`. Returns `true` when the
|
|
3411
|
+
* write may replay; otherwise settles it `OFFLINE_IDENTITY_CHANGED` and returns
|
|
3412
|
+
* `false`. Either way the live stamp is consumed.
|
|
3413
|
+
*/
|
|
3414
|
+
private passesReplayIdentityGate;
|
|
3415
|
+
/** Settle a write that replayed successfully: confirm its optimistic layer against the echoed commit cursor BEFORE resolving, so the gapless drop is in place when the awaiter (and any confirming frame) observes the settle. */
|
|
3416
|
+
private settleReplaySuccess;
|
|
3417
|
+
/**
|
|
3418
|
+
* The entry a shard's cursor lives under.
|
|
3419
|
+
*
|
|
3420
|
+
* Only the server knows that an omitted `shardKey` and an explicit one
|
|
3421
|
+
* spelling out its configured default name are the same shard — the default
|
|
3422
|
+
* is server-side configuration the client never sees. Keying on what was
|
|
3423
|
+
* SENT would split one shard's cursor across two entries, so a write under
|
|
3424
|
+
* one spelling would stop constraining a read under the other. Resolving
|
|
3425
|
+
* `undefined` through the learned name is what keeps both spellings on one
|
|
3426
|
+
* entry.
|
|
3427
|
+
*/
|
|
3428
|
+
private cursorKeyFor;
|
|
3429
|
+
/**
|
|
3430
|
+
* Learn the server's own name for the default shard from a response to a
|
|
3431
|
+
* call that named no shard.
|
|
3432
|
+
*
|
|
3433
|
+
* Any cursor recorded before the name was known sits under the placeholder
|
|
3434
|
+
* entry, so it is folded in rather than stranded — otherwise the requirement
|
|
3435
|
+
* from a client's first write would be lost exactly once, which is the kind
|
|
3436
|
+
* of gap that only shows up as a stale read under load.
|
|
3437
|
+
*/
|
|
3438
|
+
private learnDefaultShardKey;
|
|
3439
|
+
/**
|
|
3440
|
+
* Record the cursor a write committed at as this shard's read-your-writes
|
|
3441
|
+
* requirement.
|
|
3442
|
+
*
|
|
3443
|
+
* Monotonic: responses can land out of order, and moving the requirement
|
|
3444
|
+
* BACKWARDS would let a later read be answered from a replica copy that
|
|
3445
|
+
* predates a write this client already saw.
|
|
3446
|
+
*/
|
|
3447
|
+
private recordShardCursor;
|
|
3448
|
+
/** Settle a write the server reached a coded verdict on: replaying would re-trigger the same failure (a poison-message loop), so drop it. */
|
|
3449
|
+
private settleReplayTerminal;
|
|
3450
|
+
/**
|
|
3451
|
+
* Replay already-identity-gated writes one at a time on the single-call `/rpc`
|
|
3452
|
+
* path, preserving FIFO order (parallel `.then()` chains would race the
|
|
3453
|
+
* ordering callers depend on). Each replays under its stable `mutationId` so
|
|
3454
|
+
* the server dedups a write it already committed (exactly-once). A coded error
|
|
3455
|
+
* is a server verdict (drop it); a codeless (transport/transient) failure stops
|
|
3456
|
+
* the flush and re-queues this write and every unreplayed one for the next
|
|
3457
|
+
* reconnect — their callers stay pending, and the identity guard re-applies on
|
|
3458
|
+
* retry via each record's persisted stamp.
|
|
3459
|
+
*/
|
|
3460
|
+
private replaySequential;
|
|
3461
|
+
/**
|
|
3462
|
+
* Coalesce already-identity-gated writes for a single shard into ONE
|
|
3463
|
+
* `/_lunora/rpc-batch` round trip (plan 088 follow-on). The worker forwards
|
|
3464
|
+
* them to the shard DO, which replays each through its single-call dispatch, so
|
|
3465
|
+
* per-entry `mutationId` idempotency and in-order application are inherited from
|
|
3466
|
+
* the proven path. Per-slot demux mirrors {@link replaySequential}'s
|
|
3467
|
+
* classification: success confirms the optimistic layer against the echoed
|
|
3468
|
+
* `commitCursor`; a coded application verdict is terminal; a transient shard
|
|
3469
|
+
* failure (`SHARD_UNAVAILABLE`/`SHARD_ERROR`), a missing slot, or a whole-batch
|
|
3470
|
+
* transport failure re-queues for the next reconnect (never dropping a durable
|
|
3471
|
+
* write). A whole-batch coded rejection (bad request / authorization denial the
|
|
3472
|
+
* server reached a verdict on) is terminal for every entry.
|
|
3473
|
+
*
|
|
3474
|
+
* Returns the writes that must be re-queued and `stop` — `true` when the whole
|
|
3475
|
+
* chunk failed at the transport level, so the caller leaves later chunks queued
|
|
3476
|
+
* rather than sending on. The caller re-queues once, in order, so requeuing is
|
|
3477
|
+
* NOT done here.
|
|
3478
|
+
*/
|
|
3479
|
+
private replayBatched;
|
|
3480
|
+
/**
|
|
3481
|
+
* Demux a `/_lunora/rpc-batch` reply back onto the queued writes it replayed,
|
|
3482
|
+
* in input order. Each slot's envelope classifies its write the same way
|
|
3483
|
+
* {@link replaySequential} does: a success confirms the optimistic layer
|
|
3484
|
+
* against the echoed `commitCursor`; a coded application verdict is terminal;
|
|
3485
|
+
* a transient shard failure ({@link TRANSIENT_BATCH_ERROR_CODES}) or a slot the
|
|
3486
|
+
* server never returned is returned for the caller to re-queue.
|
|
3487
|
+
* @returns the writes that must be re-queued (transient slots), in input order
|
|
3488
|
+
*/
|
|
3489
|
+
private settleReplayBatchSlots;
|
|
3490
|
+
}
|
|
3491
|
+
export { ServerPokePartMessage as $, ActionCallOptions as A, BookmarkStorage as B, ConnectionStatus as C, DEFAULT_MAX_BUFFER as D, OutboxMutation as E, FunctionArgumentDescriptor as F, GlobalFacetResult as G, HttpStreamRef as H, OutboxSink as I, PersistedMutation as J, RowOp as K, LunoraClient as L, MutationCallOptions as M, RpcEnvelope as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReconnectOptions as R, SubscriptionError as S, RpcResponseBody as T, User as U, ScheduleRecord as V, ScheduleRetryPolicy as W, SchedulerPoolStatus as X, SchedulerStatus as Y, ServerMessage as Z, ServerPokeEndMessage as _, Unsubscribe as a, ServerPokeStartMessage as a0, ShardTrafficEntry as a1, ShardTrafficResult as a2, StorageListPage as a3, StorageObject as a4, StoredQuery as a5, StreamHandle as a6, SubscriptionCallback as a7, SubscriptionRegistry as a8, SubscriptionState as a9, SyncWatermark as aa, WorkflowInstanceAction as ab, WorkflowInstanceDetail as ac, WorkflowInstancePage as ad, WorkflowInstanceStatus as ae, WorkflowInstanceSummary as af, WorkflowStepDetail as ag, WsTokenProvider as ah, createClientQuery as ai, createLocalStore as aj, createStream as ak, SubscriptionErrorCallback as b, PersistenceAdapter as c, HttpStreamArgsOf as d, HttpStreamChunkOf as e, StreamIterable as f, BatchSlot as g, CachedQuery as h, ClientDebugShard as i, ClientDebugSnapshot as j, ClientDebugSubscription as k, ClientMessage as l, ClientQueryRef as m, ClientShapeSubscribeMessage as n, ClientShapeUnsubscribeMessage as o, FunctionDescriptor as p, GlobalFacetValue as q, GlobalFilterClause as r, GlobalTableInfo as s, GlobalTablePage as t, HttpStreamCallArgs as u, LunoraClientError as v, LunoraClientOptions as w, MutationSettledEvent as x, OptimisticLocalStore as y, OptimisticUpdate as z };
|