@lunora/client 1.0.0-alpha.25 → 1.0.0-alpha.250
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/README.md +16 -3
- package/dist/auth/index.d.mts +35 -4
- package/dist/auth/index.d.ts +35 -4
- package/dist/auth/index.mjs +1 -60
- package/dist/index.d.mts +721 -187
- package/dist/index.d.ts +721 -187
- package/dist/index.mjs +1 -21
- package/dist/packem_shared/CONFLICT_ERROR_CODE-B7qNZMMk.mjs +1 -0
- package/dist/packem_shared/DEFAULT_MAX_BUFFER-Bd-_iyiV.mjs +1 -0
- package/dist/packem_shared/LunoraClient-C-auNTDs.mjs +1 -0
- package/dist/packem_shared/OfflineQueue-Ba2qgZK1.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-BKgQtuh9.mjs +1 -0
- package/dist/packem_shared/TabCoordinator-DBuR7Dwv.mjs +1 -0
- package/dist/packem_shared/applyDelta-CWpwS3Bb.mjs +1 -0
- package/dist/packem_shared/client-tuning-C10B2KCO.mjs +1 -0
- package/dist/packem_shared/client-urls-BHLUjjcG.mjs +1 -0
- package/dist/packem_shared/connectLiveStream-BdjsWP1-.mjs +1 -0
- package/dist/packem_shared/createAsyncStoragePersistence-6FCvqc6C.mjs +1 -0
- package/dist/packem_shared/createAsyncStorageQueryCache-COqH5_SV.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-BfgXApi_.mjs +1 -0
- package/dist/packem_shared/createInMemoryQueryCache-B5wX-gTy.mjs +1 -0
- package/dist/packem_shared/createLocalStore-CsKA0HPB.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/createServerClient-kd9cUs8g.mjs +1 -0
- package/dist/packem_shared/createSnapshotPrecondition-BXP1NPBW.mjs +1 -0
- package/dist/packem_shared/delta-merge-Ls3DED4g.mjs +1 -0
- package/dist/packem_shared/deserializePreloaded-CyFK_cv6.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-B9iENFKq.mjs +7 -0
- package/dist/packem_shared/idb-utility-BS7xvou1.mjs +1 -0
- package/dist/packem_shared/live-stream-DBKgZlnb.mjs +10 -0
- package/dist/packem_shared/liveTopicSubscription-DQtKMFKw.mjs +1 -0
- package/dist/packem_shared/local-store-C3bw2MPY.mjs +1 -0
- package/dist/packem_shared/lunora-client.d-7WaMRZUK.d.ts +5305 -0
- package/dist/packem_shared/lunora-client.d-DtTKDhtw.d.mts +5305 -0
- package/dist/packem_shared/offline-queue-BEqCPDmw.mjs +1 -0
- package/dist/packem_shared/{preload.d-B6-lqUf2.d.ts → preload.d-B26QlpOB.d.ts} +2 -1
- package/dist/packem_shared/{preload.d-Dvk8zg6m.d.mts → preload.d-CT_9YaL9.d.mts} +2 -1
- package/dist/packem_shared/preloadQuery-uFy24PCR.mjs +1 -0
- package/dist/packem_shared/single-blob-store-DpqSxsgi.mjs +1 -0
- package/dist/packem_shared/snapshot-precondition-BEtcSbWr.mjs +1 -0
- package/dist/packem_shared/stream-idle-C7CsIT_m.mjs +1 -0
- package/dist/packem_shared/wire-errors-BgFi7ONA.mjs +1 -0
- package/dist/pagination/index.mjs +1 -61
- package/dist/query/index.d.mts +71 -3
- package/dist/query/index.d.ts +71 -3
- 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 +45 -15
- package/dist/ssr/index.d.ts +45 -15
- 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 +15 -2
- package/dist/packem_shared/CONFLICT_ERROR_CODE-B8gQ8tyU.mjs +0 -33
- package/dist/packem_shared/ClientServiceWorker-C3PAFwy0.mjs +0 -100
- package/dist/packem_shared/DEFAULT_MAX_BUFFER-7hFnzNk9.mjs +0 -110
- package/dist/packem_shared/LunoraClient-D3h4P7hg.mjs +0 -3613
- package/dist/packem_shared/OfflineQueue-BgarnAub.mjs +0 -1
- package/dist/packem_shared/SKIP-vItZChkw.mjs +0 -50
- package/dist/packem_shared/SubscriptionRegistry-CxS_Inha.mjs +0 -31
- package/dist/packem_shared/TabCoordinator-D_5oNTTt.mjs +0 -258
- package/dist/packem_shared/applyDelta-CRKZ1PBt.mjs +0 -81
- package/dist/packem_shared/createAsyncStoragePersistence-1Z5BZ8RC.mjs +0 -45
- package/dist/packem_shared/createClientQuery-dJZg1ohm.mjs +0 -80
- package/dist/packem_shared/createInMemoryBookmarkStorage-BoN7a7TH.mjs +0 -11
- package/dist/packem_shared/createInMemoryPersistence-DZ2VHWgm.mjs +0 -79
- package/dist/packem_shared/createInMemoryQueryCache-DiaGZkA2.mjs +0 -112
- package/dist/packem_shared/createLocalStore-BtqUmOQA.mjs +0 -2
- 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/createReply-lI4tVS2w.mjs +0 -36
- package/dist/packem_shared/createServerClient-DzeC2J3A.mjs +0 -11
- package/dist/packem_shared/createSnapshotPrecondition-CxQ1T4ZP.mjs +0 -18
- package/dist/packem_shared/deserializePreloaded-C0eJTY_W.mjs +0 -4
- package/dist/packem_shared/getServerSession-8jXewqxd.mjs +0 -13
- package/dist/packem_shared/httpStream-DIdL8NEw.mjs +0 -168
- package/dist/packem_shared/idb-utility-DrSVX43Q.mjs +0 -48
- package/dist/packem_shared/local-store-DIq-UWfD.mjs +0 -106
- package/dist/packem_shared/lunora-client.d-C4ud8bej.d.mts +0 -2834
- package/dist/packem_shared/lunora-client.d-C4ud8bej.d.ts +0 -2834
- package/dist/packem_shared/offline-queue-N-1JvYb4.mjs +0 -211
- package/dist/packem_shared/preloadQuery-lobFkD2Z.mjs +0 -13
- package/dist/packem_shared/wire-key-Djie6aaR.mjs +0 -266
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,193 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { type
|
|
3
|
-
|
|
4
|
-
export type {
|
|
1
|
+
import { R as ReconnectOptions, c as PersistenceAdapter, Q as QueryCacheAdapter, B as BookmarkStorage, C as ConnectionStatus, S as SubscriptionError, H as HttpStreamRef, d as HttpStreamArgsOf, e as HttpStreamChunkOf, f as StreamIterable, O as OfflineQueueOptions, L as LunoraClient } from "./packem_shared/lunora-client.d-DtTKDhtw.mjs";
|
|
2
|
+
export { type A as ActionCallOptions, type g as BatchSlot, type h as CachedQuery, type i as ClientDebugShard, type j as ClientDebugSnapshot, type k as ClientDebugSubscription, type l as ClientMessage, type m as ClientQueryRef, type n as ClientShapeSubscribeMessage, type o as ClientShapeUnsubscribeMessage, D as DEFAULT_MAX_BUFFER, type F as FunctionArgumentDescriptor, type p as FunctionDescriptor, type G as GlobalFacetResult, type q as GlobalFacetValue, type r as GlobalFilterClause, type s as GlobalTableInfo, type t as GlobalTablePage, type u as HttpStreamCallArgs, type v as LunoraClientError, type w as LunoraClientOptions, type M as MutationCallOptions, type x as MutationSettledEvent, type y as OnlineOnlyOption, type z as OptimisticLocalStore, type E as OptimisticUpdate, type I as OutboxMutation, type J as OutboxSink, type K as PersistedMutation, type P as Preloaded, type N as ReplayCredential, type T as ReplayIdentityVerdict, type V as RoomRef, type W as RowOp, type X as RpcEnvelope, type Y as RpcResponseBody, type Z as ScheduleRecord, type _ as ScheduleRetryPolicy, type $ as SchedulerPoolStatus, type a0 as SchedulerStatus, type a1 as ServerMessage, type a2 as ServerPokeEndMessage, type a3 as ServerPokePartMessage, type a4 as ServerPokeStartMessage, type a5 as ShardTrafficEntry, type a6 as ShardTrafficResult, type a7 as StorageListPage, type a8 as StorageObject, type a9 as StoredQuery, type aa as StreamHandle, type ab as SubscriptionCallback, type b as SubscriptionErrorCallback, ac as SubscriptionRegistry, type ad as SubscriptionState, type ae as SyncStatus, type af as SyncWatermark, type a as Unsubscribe, type U as User, type ag as WorkflowInstanceAction, type ah as WorkflowInstanceDetail, type ai as WorkflowInstancePage, type aj as WorkflowInstanceStatus, type ak as WorkflowInstanceSummary, type al as WorkflowStepDetail, type am as WsTokenProvider, an as createClientQuery, ao as createLocalStore, ap as createStream } from "./packem_shared/lunora-client.d-DtTKDhtw.mjs";
|
|
3
|
+
import { LunoraError, LunoraErrorCode } from '@lunora/errors';
|
|
4
|
+
export type { LunoraErrorCode } from '@lunora/errors';
|
|
5
|
+
export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-CT_9YaL9.mjs";
|
|
6
|
+
import { F as FunctionReference } from "./packem_shared/function-reference.d-Br_hsKje.mjs";
|
|
7
|
+
export type { A as ArgsOf, R as ReturnOf } from "./packem_shared/function-reference.d-Br_hsKje.mjs";
|
|
8
|
+
export type { AuthCapabilities, AuthConfigInfo, AuthImpersonation, AuthPage, AuthSession, AuthUser, AuthUserFieldSpec, CronJobInfo, KvKeyEntry, KvKeyListResult, KvNamespaceSummary, KvValueResult, PipelineLogColumnMap, PipelineLogCursor, PipelineLogField, PipelineLogPage, PipelineLogQuery, PipelineLogRow, VectorIndexSummary, VectorQueryMatch } from '@lunora/runtime';
|
|
9
|
+
/**
|
|
10
|
+
* A failure the SERVER reached no verdict on: the response carried no
|
|
11
|
+
* `{ error }` envelope at all — no `fetch` implementation, a non-JSON body from
|
|
12
|
+
* a proxy, a bare 5xx gateway page.
|
|
13
|
+
*
|
|
14
|
+
* The distinction matters because a durable write must be re-queued rather than
|
|
15
|
+
* dropped, and `code` cannot carry it: these arrive as `INTERNAL`, which is also
|
|
16
|
+
* what a server that DID reach a verdict sends. It used to be a `Symbol` stamped
|
|
17
|
+
* on with `Object.assign` and read back through a `Record<symbol, unknown>`
|
|
18
|
+
* cast, so the one classifier that knew the convention was the only reader that
|
|
19
|
+
* could ever see it. As a kind, every reader can: `error instanceof
|
|
20
|
+
* TransportError`.
|
|
21
|
+
*
|
|
22
|
+
* Still `INTERNAL` on the wire, so nothing that switches on `code` changes.
|
|
23
|
+
*
|
|
24
|
+
* `data` carries the structured payload such an error can still have — the
|
|
25
|
+
* `{ retryAfterMs }` a `Retry-After` on the unreadable response named, which
|
|
26
|
+
* {@link getRetryAfterMs} reads back and the outbox paces its retry on.
|
|
27
|
+
*/
|
|
28
|
+
declare class TransportError extends LunoraError {
|
|
29
|
+
constructor(message: string, data?: unknown);
|
|
30
|
+
}
|
|
31
|
+
/** Error code the server uses for optimistic-concurrency conflicts (HTTP 409). */
|
|
32
|
+
declare const CONFLICT_ERROR_CODE = "CONFLICT";
|
|
33
|
+
/**
|
|
34
|
+
* Whether an unknown rejection is an optimistic-concurrency conflict — the
|
|
35
|
+
* server lost a write race and the caller should refetch and retry (or surface
|
|
36
|
+
* the conflict). Structural check on the `code` property the client attaches
|
|
37
|
+
* when decoding the worker's `{ error: { code, message } }` envelope.
|
|
38
|
+
*/
|
|
39
|
+
declare const isConflictError: (error: unknown) => error is Error & {
|
|
40
|
+
code: "CONFLICT";
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Whether a rejection is an RLS/policy denial (`FORBIDDEN`, HTTP 403) — the
|
|
44
|
+
* caller is authenticated but not permitted to read/write the row. The most
|
|
45
|
+
* common per-call error a UI must handle in an RLS-first app.
|
|
46
|
+
*/
|
|
47
|
+
declare const isForbiddenError: (error: unknown) => error is Error & {
|
|
48
|
+
code: "FORBIDDEN";
|
|
49
|
+
};
|
|
50
|
+
/** Whether a rejection is an authentication failure (`UNAUTHORIZED`, HTTP 401) — no/invalid identity. */
|
|
51
|
+
declare const isUnauthorizedError: (error: unknown) => error is Error & {
|
|
52
|
+
code: "UNAUTHORIZED";
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Whether a rejection is a rate-limit denial (`TOO_MANY_REQUESTS`, HTTP 429).
|
|
56
|
+
* The retry hint (if the server sent one) is read with {@link getRetryAfterMs}.
|
|
57
|
+
*/
|
|
58
|
+
declare const isRateLimitedError: (error: unknown) => error is Error & {
|
|
59
|
+
code: "TOO_MANY_REQUESTS";
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Read the server's machine-readable `code` off a rejection, narrowed to the
|
|
63
|
+
* known {@link LunoraErrorCode} union. Returns `undefined` for a non-`Error`, a
|
|
64
|
+
* missing code, or a code string absent from the catalog (an app-minted code
|
|
65
|
+
* passed to `LunoraError` reads as `undefined` here rather than being falsely
|
|
66
|
+
* narrowed).
|
|
67
|
+
*/
|
|
68
|
+
declare const getErrorCode: (error: unknown) => LunoraErrorCode | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Read the rate-limit retry hint (`data.retryAfterMs`) off a
|
|
71
|
+
* `TOO_MANY_REQUESTS` rejection without hand-casting the `unknown` `data`
|
|
72
|
+
* payload. Returns the finite millisecond value the server sent, or `undefined`
|
|
73
|
+
* when absent/non-numeric. Pair with {@link isRateLimitedError}.
|
|
74
|
+
*/
|
|
75
|
+
declare const getRetryAfterMs: (error: unknown) => number | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Whether a durable replay's failure refused the credential rather than the
|
|
78
|
+
* write (see {@link AUTH_REPLAY_ERROR_CODES}) — a durable outbox holds such a
|
|
79
|
+
* write for the next attempt instead of dropping it.
|
|
80
|
+
*/
|
|
81
|
+
declare const isAuthReplayFailure: (error: unknown) => boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Exponential backoff calculator with optional jitter.
|
|
84
|
+
*
|
|
85
|
+
* `next()` doubles the delay each call up to `maxDelayMs`. When `jitter` is
|
|
86
|
+
* enabled the returned value is randomized in `[delay/2, delay]` so a fleet
|
|
87
|
+
* of clients reconnecting at the same time spread out their retries.
|
|
88
|
+
*/
|
|
89
|
+
interface ReconnectCalculator {
|
|
90
|
+
/** Returns the delay to wait before the next reconnect attempt. */
|
|
91
|
+
next: () => number;
|
|
92
|
+
/** Resets the backoff to the initial delay (call on successful reconnect). */
|
|
93
|
+
reset: () => void;
|
|
94
|
+
}
|
|
95
|
+
declare const createReconnect: (options?: ReconnectOptions, random?: () => number) => ReconnectCalculator;
|
|
96
|
+
/**
|
|
97
|
+
* Optimistic-reconcile logic shared by every `@lunora/*` framework adapter's
|
|
98
|
+
* agent-chat surface (`@lunora/react` `useAgentChat`, plus the `@lunora/vue`,
|
|
99
|
+
* `@lunora/solid`, `@lunora/svelte`, and `@lunora/angular` counterparts). The
|
|
100
|
+
* adapters keep only their framework-specific state plumbing
|
|
101
|
+
* (`useState` / `.value` / `.set()` / a store) and delegate the pure merge decision
|
|
102
|
+
* here, so the heuristic lives — and is tested — in exactly one place.
|
|
103
|
+
*/
|
|
104
|
+
/**
|
|
105
|
+
* The durable-message shape the reconcile reads: a structural subset of each
|
|
106
|
+
* adapter's `AgentChatMessage` (itself a client-safe mirror of `@lunora/agent`'s
|
|
107
|
+
* `AgentMessageRow`). Only `content`, `role`, and the per-thread-monotonic `seq` are
|
|
108
|
+
* consulted; adapters pass their fuller row type, which is assignable to this.
|
|
109
|
+
*/
|
|
110
|
+
interface ReconcileDurableMessage {
|
|
111
|
+
content: string;
|
|
112
|
+
role: "assistant" | "system" | "tool" | "user";
|
|
113
|
+
seq: number;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A local optimistic user turn awaiting server acknowledgement. `id` is a
|
|
117
|
+
* client-generated handle the adapter uses to roll the row back on a failed send;
|
|
118
|
+
* the reconcile itself reads only `content` and `maxDurableSeqAtSend`.
|
|
119
|
+
*/
|
|
120
|
+
interface OptimisticMessage {
|
|
121
|
+
content: string;
|
|
122
|
+
id: number;
|
|
123
|
+
/**
|
|
124
|
+
* The highest durable `seq` present when this row was sent. The reconcile
|
|
125
|
+
* retires the row when a durable `user` row with matching `content` lands at a
|
|
126
|
+
* STRICTLY GREATER `seq` (i.e. after the send) — window-independent because
|
|
127
|
+
* `seq` is monotonic, not a positional count. Also the age baseline for the
|
|
128
|
+
* {@link RETIRE_AFTER_DURABLE_SEQ_ADVANCE} fallback.
|
|
129
|
+
*
|
|
130
|
+
* `seq` is monotonic PER THREAD, not globally (`@lunora/agent` assigns it from
|
|
131
|
+
* the thread's own message count), so a baseline captured in one thread is
|
|
132
|
+
* meaningless in another: an optimistic row carried across a thread switch can
|
|
133
|
+
* never be retired by the new thread's history until its `seq` happens to pass
|
|
134
|
+
* the old thread's. Adapters therefore drop their optimistic rows whenever the
|
|
135
|
+
* resolved thread key changes.
|
|
136
|
+
*/
|
|
137
|
+
maxDurableSeqAtSend: number;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The highest `seq` across `messages`, or `-1` when empty. Used both to capture an
|
|
141
|
+
* optimistic row's `maxDurableSeqAtSend` at send time and to base the synthetic
|
|
142
|
+
* seqs of rendered optimistic rows above the highest real durable seq (not just
|
|
143
|
+
* `messages.length`, which under-counts when durable rows have gaps), so a
|
|
144
|
+
* placeholder seq never collides with a real one.
|
|
145
|
+
*/
|
|
146
|
+
declare const maxSeq: (messages: ReadonlyArray<{
|
|
147
|
+
seq: number;
|
|
148
|
+
}>) => number;
|
|
149
|
+
/**
|
|
150
|
+
* Secondary, windowed fallback: retire a pending optimistic row once the durable
|
|
151
|
+
* history's highest `seq` has advanced at least this far past the value seen at
|
|
152
|
+
* send, even though no matching-content user row is currently visible to claim it.
|
|
153
|
+
* This covers the pathological "identical content already present at send time"
|
|
154
|
+
* case, where no user row with a STRICTLY GREATER `seq` than the send-time max will
|
|
155
|
+
* ever appear for the primary content match to consume (e.g. the acknowledging row
|
|
156
|
+
* was evicted by a bounded `limit` before reconcile could see it). `seq` is monotonic
|
|
157
|
+
* within the thread, so it keeps climbing even when the visible user-row COUNT does not.
|
|
158
|
+
*
|
|
159
|
+
* The `2` is a heuristic threshold, NOT an invariant about turn shape. It is
|
|
160
|
+
* tempting to read it as "one turn == a user row and an assistant row (+2)", but
|
|
161
|
+
* `@lunora/agent`'s tool-loop turns can persist MANY rows, and an ERRORED turn
|
|
162
|
+
* persists only the user row (+1). Those non-(+2) turns are retired by the PRIMARY
|
|
163
|
+
* seq-based content match below (which sees the user row land at a greater `seq`),
|
|
164
|
+
* never by this count-based fallback. The fully robust fix is a server-echoed
|
|
165
|
+
* correlation id on each persisted user row (deferred; no plan filed).
|
|
166
|
+
*
|
|
167
|
+
* KNOWN LIMITATION (inherent to a client-only heuristic): on an ownerless /
|
|
168
|
+
* `instanceId`-less thread a FOREIGN writer that advances `seq` by >= 2 between this
|
|
169
|
+
* row's send and its own acknowledgement can trip this fallback and retire the row a
|
|
170
|
+
* beat early. The correlation id closes that gap; until then it is the accepted
|
|
171
|
+
* residual edge.
|
|
172
|
+
*/
|
|
173
|
+
declare const RETIRE_AFTER_DURABLE_SEQ_ADVANCE = 2;
|
|
174
|
+
/**
|
|
175
|
+
* Drop the optimistic user turns the durable history has now caught up on. Pure —
|
|
176
|
+
* it reads only the two arrays plus values captured on each pending row at send
|
|
177
|
+
* time (no clock, no state mutation), so it is safe to run in render.
|
|
178
|
+
*
|
|
179
|
+
* The primary retire condition: a durable `user` row with the same `content` exists
|
|
180
|
+
* whose `seq` is STRICTLY GREATER than the row's `maxDurableSeqAtSend` — i.e. a user
|
|
181
|
+
* row that landed AFTER the send. This is window-independent (it matches on monotonic
|
|
182
|
+
* `seq`, not a positional count), so it retires the normal turn AND an errored
|
|
183
|
+
* single-row (+1) turn even under a saturated, sliding `limit`. Each durable row is
|
|
184
|
+
* consumed at most once (the `consumed` set), so repeated identical prompts sent
|
|
185
|
+
* back-to-back each wait for their OWN durable row instead of collapsing onto one.
|
|
186
|
+
*
|
|
187
|
+
* Failing that, the {@link RETIRE_AFTER_DURABLE_SEQ_ADVANCE} windowed fallback fires
|
|
188
|
+
* (see its doc) for the "identical content already present" pathological case.
|
|
189
|
+
*/
|
|
190
|
+
declare const reconcileOptimistic: (optimistic: ReadonlyArray<OptimisticMessage>, durable: ReadonlyArray<ReconcileDurableMessage>) => OptimisticMessage[];
|
|
5
191
|
/**
|
|
6
192
|
* The slice of React Native's `AsyncStorage` (or any async key/value store —
|
|
7
193
|
* Expo `SecureStore`, a wrapped `localForage`, an in-memory map in tests) this
|
|
@@ -22,17 +208,90 @@ interface AsyncStoragePersistenceOptions {
|
|
|
22
208
|
/**
|
|
23
209
|
* Builds a {@link PersistenceAdapter} over an async key/value store — the React
|
|
24
210
|
* Native / Expo counterpart to the IndexedDB adapter (`createIndexedDbPersistence`).
|
|
25
|
-
* The whole FIFO mutation log is
|
|
26
|
-
*
|
|
27
|
-
*
|
|
211
|
+
* The whole FIFO mutation log is stored under a single key (`key`), so enqueue
|
|
212
|
+
* order is preserved and `load()` returns freshly-parsed records that callers
|
|
213
|
+
* can't alias.
|
|
214
|
+
*
|
|
215
|
+
* Records round-trip through {@link singleBlobStore}'s wire codec. Args it
|
|
216
|
+
* cannot carry (a `RegExp`, a class instance) reject at `append`/`replace` — the
|
|
217
|
+
* flush path rejects them terminally anyway. A blob that exists but can't be
|
|
218
|
+
* decoded loads as empty, and every write rejects rather than overwrite it and
|
|
219
|
+
* lose the queued writes it may still hold; `clear()` discards it.
|
|
28
220
|
*
|
|
29
|
-
* AsyncStorage has no transactions, so every read-modify-write
|
|
30
|
-
*
|
|
31
|
-
* a time and can't clobber each other's writes.
|
|
221
|
+
* AsyncStorage has no transactions, so every read-modify-write runs through
|
|
222
|
+
* {@link singleBlobStore}'s serialized chain — concurrent `append`/`remove`
|
|
223
|
+
* calls run one at a time and can't clobber each other's writes.
|
|
32
224
|
*/
|
|
33
225
|
declare const createAsyncStoragePersistence: (options: AsyncStoragePersistenceOptions) => PersistenceAdapter;
|
|
226
|
+
interface AsyncStorageQueryCacheOptions {
|
|
227
|
+
/** Storage key the cache is serialized under; defaults to `"lunora:query-cache"`. */
|
|
228
|
+
key?: string;
|
|
229
|
+
/** LRU row cap; defaults to 50 (see {@link DEFAULT_MAX_ENTRIES}). Must be a positive integer. The oldest rows by `ts` are pruned on `put` once exceeded. */
|
|
230
|
+
maxEntries?: number;
|
|
231
|
+
/** The async key/value store the cache is read from and written to (e.g. React Native `AsyncStorage`). */
|
|
232
|
+
storage: AsyncStorageLike;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Builds a {@link QueryCacheAdapter} over an async key/value store — the React
|
|
236
|
+
* Native / Expo counterpart to `createIndexedDbQueryCache`. The whole cache is
|
|
237
|
+
* serialized under a single key (`key`).
|
|
238
|
+
*
|
|
239
|
+
* Values round-trip through {@link singleBlobStore}'s wire codec (a `bigint`,
|
|
240
|
+
* `Date`, or bytes survives a reload); a corrupt blob reads as an empty cache.
|
|
241
|
+
*
|
|
242
|
+
* AsyncStorage has no transactions, so every read-modify-write runs through
|
|
243
|
+
* {@link singleBlobStore}'s serialized chain — concurrent `put`/`remove` calls
|
|
244
|
+
* run one at a time and can't clobber each other's writes.
|
|
245
|
+
*
|
|
246
|
+
* Single-blob storage rewrites the whole cache per `put` (bounded by
|
|
247
|
+
* `maxEntries` — see the cap's note on Android's row and shared-budget limits);
|
|
248
|
+
* if that ever shows up in profiles, per-key storage entries are the upgrade
|
|
249
|
+
* path.
|
|
250
|
+
*/
|
|
251
|
+
declare const createAsyncStorageQueryCache: (options: AsyncStorageQueryCacheOptions) => QueryCacheAdapter;
|
|
34
252
|
/** Default in-memory bookmark store. Survives the lifetime of the client. */
|
|
35
253
|
declare const createInMemoryBookmarkStorage: () => BookmarkStorage;
|
|
254
|
+
/**
|
|
255
|
+
* Reactive sinks an adapter binds to its own primitive's setters (a React
|
|
256
|
+
* `useState`, a Solid signal, a Vue ref, a Svelte store). The runner pushes into
|
|
257
|
+
* them; how they store the value is the adapter's concern (e.g. Solid and React
|
|
258
|
+
* wrap function-valued results in a thunk so the setter stores them rather than
|
|
259
|
+
* invoking them).
|
|
260
|
+
*/
|
|
261
|
+
interface CallRunnerSinks<R> {
|
|
262
|
+
/** Receives the normalized {@link Error} when the latest invocation rejects. */
|
|
263
|
+
setError: (error: Error) => void;
|
|
264
|
+
/** Receives `true` while at least one invocation is in flight (ref-counted across overlapping calls), else `false`. */
|
|
265
|
+
setPending: (pending: boolean) => void;
|
|
266
|
+
/** Receives the resolved value when the latest invocation succeeds. */
|
|
267
|
+
setResult: (result: R) => void;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Build the framework-neutral half of an adapter's write primitive — the `mutate`
|
|
271
|
+
* of `useMutation`/`createMutation`/`mutation`, and the `call` of
|
|
272
|
+
* `useAction`/`createAction`/`action`.
|
|
273
|
+
*
|
|
274
|
+
* It owns the orchestration every adapter otherwise copy-pastes: ref-counting
|
|
275
|
+
* overlapping invocations into `setPending` (so the flag clears only once the
|
|
276
|
+
* last settles), normalizing a thrown non-`Error`, and routing success/failure to
|
|
277
|
+
* `setResult`/`setError` before re-throwing the SAME instance (so a typed error's
|
|
278
|
+
* `.code`/`.data` survive for both the `catch` and the template).
|
|
279
|
+
*
|
|
280
|
+
* `invoke` is a pre-bound thunk — `(args, options) => client.mutation(fn, args, options)`
|
|
281
|
+
* or `(args, options) => client.action(fn, args, options)`. Binding at the call
|
|
282
|
+
* site is what keeps this one function: the runner never inspects `options`, it
|
|
283
|
+
* only forwards them, so the option type is inferred from the closure rather
|
|
284
|
+
* than hard-coded per procedure kind. What actually keeps `optimisticUpdate` off
|
|
285
|
+
* an action is the adapter's exported handle type, not this file.
|
|
286
|
+
*
|
|
287
|
+
* `data`/`error` track the LATEST invocation, not the last to settle: overlapping
|
|
288
|
+
* calls can resolve out of order, so an earlier call that finishes later must not
|
|
289
|
+
* clobber a newer call's outcome. Each invocation takes a monotonic token and
|
|
290
|
+
* writes the value sinks only while it is still the most recent one — otherwise a
|
|
291
|
+
* double-click could leave the UI showing the first click's result, or an error
|
|
292
|
+
* for a call that succeeded.
|
|
293
|
+
*/
|
|
294
|
+
declare const createCallRunner: <A, O, R>(invoke: (args: A, options?: O) => Promise<R>, sinks: CallRunnerSinks<R>) => ((args: A, options?: O) => Promise<R>);
|
|
36
295
|
interface TabCoordinatorOptions {
|
|
37
296
|
/**
|
|
38
297
|
* BroadcastChannel name. Defaults to `"lunora-bridge"`.
|
|
@@ -51,18 +310,71 @@ interface TabCoordinatorOptions {
|
|
|
51
310
|
* Called when this tab becomes the leader (should open WS connections).
|
|
52
311
|
*/
|
|
53
312
|
onBecomeLeader?: () => void;
|
|
313
|
+
/**
|
|
314
|
+
* Called when the leader broadcasts its aggregate `ConnectionStatus`
|
|
315
|
+
* (see `LunoraClient.emitConnectionStatus`) — a follower owns no socket
|
|
316
|
+
* of its own, so this is its only truthful signal for a status indicator
|
|
317
|
+
* or the offline-queue gate. Sent on every leader-side status change and
|
|
318
|
+
* once more right after a new leader takes over, so a follower already
|
|
319
|
+
* mid-mirroring isn't stuck on a stale value from the PREVIOUS leader.
|
|
320
|
+
*
|
|
321
|
+
* `identity` is the leader's own `identityFingerprint()` (`null` = signed
|
|
322
|
+
* out) — the channel-name scoping (see `LunoraClient.createTabCoordinator`)
|
|
323
|
+
* is the primary defence against a stale frame from a since-changed
|
|
324
|
+
* identity, but `setAuthToken` in one tab can move it to a new channel
|
|
325
|
+
* while this frame is already queued in another tab's message task queue.
|
|
326
|
+
* A follower drops the frame when `identity` is present and doesn't match
|
|
327
|
+
* its own; an **absent** `identity` (mixed-version leader) is accepted —
|
|
328
|
+
* today's behavior, and the channel split already separates version groups.
|
|
329
|
+
*/
|
|
330
|
+
onConnectionStatus?: (status: ConnectionStatus, identity?: string | null) => void;
|
|
331
|
+
/**
|
|
332
|
+
* Called when the leader broadcasts a subscription error.
|
|
333
|
+
*/
|
|
334
|
+
/** Fired on the LEADER when it answers another tab's leadership claim — the moment a new follower is known to be listening. */
|
|
335
|
+
onLeaderClaimAnswered?: () => void;
|
|
54
336
|
/**
|
|
55
337
|
* Called when this tab loses leadership (should close WS connections).
|
|
56
338
|
*/
|
|
57
339
|
onStopBeingLeader?: () => void;
|
|
58
340
|
/**
|
|
59
|
-
* Called when the leader broadcasts subscription data.
|
|
341
|
+
* Called when the leader broadcasts subscription data. `cursor`/`epoch`
|
|
342
|
+
* ride along when the leader is CLIENT-01-aware, so the follower can drop
|
|
343
|
+
* its own confirmed optimistic layers instead of just displaying the raw
|
|
344
|
+
* value; both are absent on a mixed-version leader that hasn't shipped the
|
|
345
|
+
* cursor yet (the follower falls back to its historical behavior — see
|
|
346
|
+
* the follower data handler in `tab-leadership.ts`).
|
|
347
|
+
*
|
|
348
|
+
* `identity` is the leader's `identityFingerprint()` (see
|
|
349
|
+
* `onConnectionStatus`'s docblock for the full rationale and the
|
|
350
|
+
* absent-field mixed-version rule — identical here).
|
|
60
351
|
*/
|
|
61
|
-
onSubscriptionData?: (key: string, data: unknown) => void;
|
|
352
|
+
onSubscriptionData?: (key: string, data: unknown, cursor?: number, epoch?: string, identity?: string | null) => void;
|
|
353
|
+
onSubscriptionError?: (key: string, error: SubscriptionError, identity?: string | null) => void;
|
|
62
354
|
/**
|
|
63
|
-
* Called when the leader broadcasts a
|
|
355
|
+
* Called when the leader broadcasts a `settled` frame's checkpoint advance
|
|
356
|
+
* (no value change, but the resume cursor moved). A follower needs this
|
|
357
|
+
* too — otherwise a `setQuery`/per-call optimistic overlay that a
|
|
358
|
+
* byte-identical write just confirmed stays masked on follower tabs until
|
|
359
|
+
* the next VISIBLE data frame arrives, even though the leader already
|
|
360
|
+
* dropped it.
|
|
361
|
+
*
|
|
362
|
+
* `lastMutationId` is the LEADER's own per-client `__client_watermark` —
|
|
363
|
+
* scoped server-side to the socket's announced `clientId` — so it only
|
|
364
|
+
* means anything to a follower whose `clientId` matches the leader's
|
|
365
|
+
* (`clientId` rides along for exactly that comparison; see
|
|
366
|
+
* `LunoraClient`'s wiring). An **absent** `clientId` (a mixed-version
|
|
367
|
+
* leader that hasn't shipped this field yet) also skips the
|
|
368
|
+
* `mutationId` half — safe, because the follower's own gates still
|
|
369
|
+
* resolve via its own RPC-ack watermark path and the `CheckpointRegistry`
|
|
370
|
+
* bounded fallback. The checkpoint (cursor) half of this callback fires
|
|
371
|
+
* unconditionally regardless of `clientId` — only the `mutationId` half
|
|
372
|
+
* is scoped.
|
|
373
|
+
*
|
|
374
|
+
* `identity` is the leader's `identityFingerprint()` — see
|
|
375
|
+
* `onConnectionStatus`'s docblock for the drop rule (identical here).
|
|
64
376
|
*/
|
|
65
|
-
|
|
377
|
+
onSubscriptionSettled?: (key: string, cursor?: number, epoch?: string, lastMutationId?: number, clientId?: string, identity?: string | null) => void;
|
|
66
378
|
}
|
|
67
379
|
declare class TabCoordinator {
|
|
68
380
|
private readonly bc;
|
|
@@ -75,15 +387,31 @@ declare class TabCoordinator {
|
|
|
75
387
|
private leader;
|
|
76
388
|
/** `true` once `start()` has been called. */
|
|
77
389
|
private running;
|
|
390
|
+
/**
|
|
391
|
+
* `true` while a `claimAndPromote()` claim window is open. Guards the yield
|
|
392
|
+
* handler and `checkLeaderHealth`'s belt-and-braces else-branch so the two
|
|
393
|
+
* promotion paths can't both arm a `becomeLeader` timeout for the same gap.
|
|
394
|
+
*/
|
|
395
|
+
private promotionPending;
|
|
78
396
|
/** Timestamp of the most recent leader heartbeat. */
|
|
79
397
|
private lastHeartbeat;
|
|
398
|
+
/**
|
|
399
|
+
* Snapshots waiting for the end of the current tick, latest per key. A
|
|
400
|
+
* `subscription-data` frame is a full snapshot, so a burst for one key within
|
|
401
|
+
* a tick only needs its last frame posted — each post structured-clones the
|
|
402
|
+
* whole value for every tab on the channel.
|
|
403
|
+
*/
|
|
404
|
+
private readonly pendingData;
|
|
80
405
|
private heartbeatTimer;
|
|
81
406
|
private leaderCheckTimer;
|
|
82
407
|
/** Callbacks set via constructor options. */
|
|
83
408
|
private readonly onBecomeLeader;
|
|
84
409
|
private readonly onStopBeingLeader;
|
|
410
|
+
private readonly onConnectionStatus;
|
|
85
411
|
private readonly onSubscriptionData;
|
|
412
|
+
private readonly onLeaderClaimAnswered;
|
|
86
413
|
private readonly onSubscriptionError;
|
|
414
|
+
private readonly onSubscriptionSettled;
|
|
87
415
|
constructor(options?: TabCoordinatorOptions);
|
|
88
416
|
/**
|
|
89
417
|
* Start the coordinator: attempt to claim leadership and begin the
|
|
@@ -97,6 +425,23 @@ declare class TabCoordinator {
|
|
|
97
425
|
stop(): void;
|
|
98
426
|
/** `true` when this tab is the current WebSocket leader. */
|
|
99
427
|
isLeader(): boolean;
|
|
428
|
+
/**
|
|
429
|
+
* Force this (freshly `start()`-ed) tab to become leader immediately,
|
|
430
|
+
* skipping the normal claim-then-`leaderTimeout` dance. Safe to call
|
|
431
|
+
* right after `start()` when the caller already knows this tab was the
|
|
432
|
+
* SOLE leader of the group it's replacing (e.g. `LunoraClient`'s
|
|
433
|
+
* identity-change coordinator restart — waiting out the full
|
|
434
|
+
* `leaderTimeout` there would freeze every live query for no reason,
|
|
435
|
+
* since this tab is overwhelmingly likely to be alone on the freshly
|
|
436
|
+
* derived channel). A sibling tab going through the normal `start()`
|
|
437
|
+
* dance for the SAME transition observes this tab's `becomeLeader`
|
|
438
|
+
* heartbeat and defers before its own claim-timeout fires; if another
|
|
439
|
+
* tab ALSO force-promotes at the same moment (e.g. two tabs both
|
|
440
|
+
* transitioning to the same new identity), the existing
|
|
441
|
+
* `resolveLeaderVsLeaderTieBreak` resolves the rare double-promotion
|
|
442
|
+
* the same way it resolves any other split-brain.
|
|
443
|
+
*/
|
|
444
|
+
promoteImmediately(): void;
|
|
100
445
|
/** The tab id of the current leader, or `undefined` if unknown / no leader. */
|
|
101
446
|
get leaderTabId(): string | undefined;
|
|
102
447
|
/** The id of this tab. */
|
|
@@ -105,15 +450,53 @@ declare class TabCoordinator {
|
|
|
105
450
|
get isRunning(): boolean;
|
|
106
451
|
/**
|
|
107
452
|
* Broadcast subscription data to all follower tabs. Only the leader should
|
|
108
|
-
* call this.
|
|
453
|
+
* call this. `cursor`/`epoch` are omitted from the wire frame when
|
|
454
|
+
* `undefined` (e.g. a CDC-off shard) — a follower on ANY version treats a
|
|
455
|
+
* missing `cursor` as "no confirmed-layer drop this frame", so omitting it
|
|
456
|
+
* here is equivalent to sending it as `undefined`. `identity` is this
|
|
457
|
+
* (leader) tab's own `identityFingerprint()` (`null` = signed out) —
|
|
458
|
+
* stamped so a follower can drop a frame from a since-changed identity
|
|
459
|
+
* (see the `onSubscriptionData` docblock); spread-included whenever
|
|
460
|
+
* supplied, since a fixed leader always knows its own identity (never
|
|
461
|
+
* omits it) and `null` must round-trip distinctly from "field absent".
|
|
109
462
|
*/
|
|
110
|
-
broadcastSubscriptionData(key: string, data: unknown): void;
|
|
463
|
+
broadcastSubscriptionData(key: string, data: unknown, cursor?: number, epoch?: string, identity?: string | null): void;
|
|
111
464
|
/**
|
|
112
465
|
* Broadcast a subscription error to all follower tabs. Only the leader
|
|
113
466
|
* should call this.
|
|
467
|
+
*
|
|
468
|
+
* `identity` is this tab's own `identityFingerprint()`, stamped so a
|
|
469
|
+
* follower can drop a frame minted under a different credential — the same
|
|
470
|
+
* guard `broadcastSubscriptionData` and `broadcastSubscriptionSettled`
|
|
471
|
+
* carry. This one lacked it, which was harmless only because nothing called
|
|
472
|
+
* it; once the leader started fanning errors, an unstamped frame could
|
|
473
|
+
* deliver one caller's rejection to a tab that had since signed in as
|
|
474
|
+
* someone else.
|
|
114
475
|
*/
|
|
115
|
-
broadcastSubscriptionError(key: string, error: SubscriptionError): void;
|
|
476
|
+
broadcastSubscriptionError(key: string, error: SubscriptionError, identity?: string | null): void;
|
|
477
|
+
/**
|
|
478
|
+
* Broadcast a `settled` frame's checkpoint advance to all follower tabs
|
|
479
|
+
* (no value change, but the resume cursor/epoch moved — see
|
|
480
|
+
* `LunoraClient.handleSettledMessage`). `clientId` is this (leader) tab's
|
|
481
|
+
* own client id, stamped so a follower can tell whether the echoed
|
|
482
|
+
* `lastMutationId` watermark is genuinely its own (see the
|
|
483
|
+
* `onSubscriptionSettled` docblock). `identity` is this tab's own
|
|
484
|
+
* `identityFingerprint()` — see `broadcastSubscriptionData`'s docblock for
|
|
485
|
+
* the same round-tripping rule. Only the leader should call this.
|
|
486
|
+
*/
|
|
487
|
+
broadcastSubscriptionSettled(key: string, cursor?: number, epoch?: string, lastMutationId?: number, clientId?: string, identity?: string | null): void;
|
|
488
|
+
/**
|
|
489
|
+
* Broadcast this tab's aggregate `ConnectionStatus` to follower tabs, so
|
|
490
|
+
* they can mirror a truthful status without a socket of their own (see
|
|
491
|
+
* `LunoraClient.computeStatus`/`emitConnectionStatus`). `identity` is this
|
|
492
|
+
* tab's own `identityFingerprint()` — see `broadcastSubscriptionData`'s
|
|
493
|
+
* docblock for the same round-tripping rule. Only the leader should call
|
|
494
|
+
* this.
|
|
495
|
+
*/
|
|
496
|
+
broadcastConnectionStatus(status: ConnectionStatus, identity?: string | null): void;
|
|
116
497
|
private broadcast;
|
|
498
|
+
/** Post the snapshots queued this tick — unless leadership was lost meanwhile, when the new leader's own snapshots supersede them. */
|
|
499
|
+
private flushPendingData;
|
|
117
500
|
private handleMessage;
|
|
118
501
|
private handleClaimLeadership;
|
|
119
502
|
private handleHeartbeat;
|
|
@@ -133,28 +516,15 @@ declare class TabCoordinator {
|
|
|
133
516
|
private becomeLeader;
|
|
134
517
|
private sendHeartbeat;
|
|
135
518
|
private checkLeaderHealth;
|
|
519
|
+
/**
|
|
520
|
+
* Broadcast a leadership claim and arm a `becomeLeader` fallback: if no
|
|
521
|
+
* other tab has asserted itself as leader by the time the window elapses,
|
|
522
|
+
* self-promote. Shared by `checkLeaderHealth`'s stale-leader path and the
|
|
523
|
+
* `yield-leadership` handler so both promotion triggers use one claim
|
|
524
|
+
* window instead of each arming its own timeout.
|
|
525
|
+
*/
|
|
526
|
+
private claimAndPromote;
|
|
136
527
|
}
|
|
137
|
-
/**
|
|
138
|
-
* Client-side incremental merging of structured mutation deltas.
|
|
139
|
-
*
|
|
140
|
-
* Lunora's live-query fan-out has two server paths:
|
|
141
|
-
*
|
|
142
|
-
* 1. Server re-execution (subscriptions carrying a `functionPath`) pushes a
|
|
143
|
-
* full `data` snapshot whenever a write touches a table the query reads. These
|
|
144
|
-
* already carry the authoritative result and are applied wholesale.
|
|
145
|
-
* 2. Legacy delta fan-out (`broadcastDelta`) pushes a structured `MutationDelta`
|
|
146
|
-
* as a `delta` frame to subscribers matched by table + args. The delta describes
|
|
147
|
-
* a single row change (`insert` / `update` / `delete`) keyed by row id, so the
|
|
148
|
-
* client can splice it into the cached list result without a full re-send.
|
|
149
|
-
*
|
|
150
|
-
* Historically the client treated the `delta` field as an opaque blob and
|
|
151
|
-
* replaced the whole cached value with it on every message — which only made
|
|
152
|
-
* sense for the rare delta payloads that already carried the full result. This
|
|
153
|
-
* module lets the client recognise a structured delta and merge it into the
|
|
154
|
-
* existing array (preserving order, no dup/loss), falling back to full
|
|
155
|
-
* replacement when the payload isn't a recognisable row delta or can't be
|
|
156
|
-
* applied cleanly against the current cached shape.
|
|
157
|
-
*/
|
|
158
528
|
/**
|
|
159
529
|
* One row change as emitted by `@lunora/do`'s `broadcastDelta`. Mirrors
|
|
160
530
|
* `MutationDelta` in `@lunora/do` structurally so the client carries no
|
|
@@ -177,28 +547,21 @@ interface MutationDelta {
|
|
|
177
547
|
*/
|
|
178
548
|
declare const isMutationDelta: (value: unknown) => value is MutationDelta;
|
|
179
549
|
/**
|
|
180
|
-
* Apply a structured `MutationDelta` to a cached
|
|
181
|
-
*
|
|
550
|
+
* Apply a structured `MutationDelta` to a cached list result, returning a new
|
|
551
|
+
* value (never mutating the input). Returns `undefined` when the delta can't be
|
|
182
552
|
* applied cleanly — the caller should then fall back to the existing
|
|
183
553
|
* full-replacement behaviour (or trust the next snapshot to reconcile).
|
|
184
554
|
*
|
|
185
|
-
* Mergeable
|
|
186
|
-
* `
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* against a delta replayed after a snapshot already included it).
|
|
192
|
-
* - `update`: replaces the matching row in place, preserving its position.
|
|
193
|
-
* - `delete`: removes the matching row.
|
|
194
|
-
*
|
|
195
|
-
* Returns `undefined` when `current` isn't an array of id-keyable objects, or
|
|
196
|
-
* when an `insert`/`update` delta carries no `row` to splice in.
|
|
555
|
+
* Mergeable shapes: an array of id-bearing row objects, or a `.paginate()`
|
|
556
|
+
* result wrapping one in `page` (see `rowListOf`). A paginated value keeps its
|
|
557
|
+
* other fields and comes back as a new object with a new `page` — the server
|
|
558
|
+
* only sends row deltas for that shape when everything outside the page is
|
|
559
|
+
* unchanged, so the merged value matches the snapshot it chose not to send.
|
|
560
|
+
* @returns the merged value, or `undefined` when the delta cannot be applied
|
|
197
561
|
*/
|
|
198
|
-
declare const applyDelta: (current: unknown, delta: MutationDelta) => undefined | unknown[];
|
|
562
|
+
declare const applyDelta: (current: unknown, delta: MutationDelta) => Record<string, unknown> | undefined | unknown[];
|
|
199
563
|
/**
|
|
200
564
|
* Options accepted by {@link httpStream}.
|
|
201
|
-
* @experimental Part of the HTTP-SSE stream surface.
|
|
202
565
|
*/
|
|
203
566
|
interface HttpStreamOptions {
|
|
204
567
|
/**
|
|
@@ -211,8 +574,43 @@ interface HttpStreamOptions {
|
|
|
211
574
|
fetch?: typeof fetch;
|
|
212
575
|
/** Extra request headers (e.g. `authorization`). `accept: text/event-stream` is always sent. */
|
|
213
576
|
headers?: Record<string, string>;
|
|
577
|
+
/**
|
|
578
|
+
* Treat a connection that delivers nothing — no chunk, no keepalive comment —
|
|
579
|
+
* for this many milliseconds as dead: it is cancelled, then reconnected with
|
|
580
|
+
* {@link HttpStreamOptions.reconnect} or failed with `STREAM_IDLE`. Off by
|
|
581
|
+
* default, because a route may be silent for as long as its handler works
|
|
582
|
+
* (a model thinking); set it to a few multiples of the route's keepalive
|
|
583
|
+
* interval when the route sends one.
|
|
584
|
+
*
|
|
585
|
+
* Measured per streamed read, so it needs a `fetch` that exposes
|
|
586
|
+
* `response.body`. A runtime whose `fetch` delivers no body stream (React
|
|
587
|
+
* Native) reads the whole payload with one `text()` call that resolves only
|
|
588
|
+
* at the end — no per-frame progress exists to measure silence against, so
|
|
589
|
+
* this bound does not apply there and a stalled read waits for
|
|
590
|
+
* `.cancel()` or {@link HttpStreamOptions.signal} instead.
|
|
591
|
+
*/
|
|
592
|
+
idleTimeoutMs?: number;
|
|
593
|
+
/**
|
|
594
|
+
* Seed the first request's `Last-Event-ID` header, resuming a stream from a
|
|
595
|
+
* position a previous consumer persisted. Once frames arrive this is
|
|
596
|
+
* superseded by their `id:`, and a reconnecting stream always sends the
|
|
597
|
+
* latest id it saw.
|
|
598
|
+
*/
|
|
599
|
+
lastEventId?: string;
|
|
214
600
|
/** Caps the in-flight chunk buffer (see `createStream`); exceeding it fails the stream. */
|
|
215
601
|
maxBuffer?: number;
|
|
602
|
+
/** Called with each frame's `id:` as it is seen (the value that will be replayed on reconnect). */
|
|
603
|
+
onEventId?: (id: string) => void;
|
|
604
|
+
/** Called when the server advertises a `retry:` reconnect delay, in milliseconds. */
|
|
605
|
+
onRetry?: (ms: number) => void;
|
|
606
|
+
/**
|
|
607
|
+
* Reopen the request after a transport interruption — a bare EOF or a
|
|
608
|
+
* read/transport error — replaying the last seen id as `Last-Event-ID`.
|
|
609
|
+
* Defaults to `false`, which surfaces the interruption as
|
|
610
|
+
* `HTTP_STREAM_INTERRUPTED` exactly as before. A terminal `complete`/`error`
|
|
611
|
+
* frame, a non-2xx status, and a consumer cancel never reconnect.
|
|
612
|
+
*/
|
|
613
|
+
reconnect?: boolean;
|
|
216
614
|
/** External abort signal — aborting it cancels the fetch (→ the server handler's `signal`). */
|
|
217
615
|
signal?: AbortSignal;
|
|
218
616
|
}
|
|
@@ -230,39 +628,132 @@ interface HttpStreamOptions {
|
|
|
230
628
|
* an `event: error` frame (or a transport failure) surfaces as a coded
|
|
231
629
|
* rejection on the next `next()`. `.cancel()` — or aborting `options.signal` —
|
|
232
630
|
* aborts the underlying fetch, which the server observes via `request.signal`.
|
|
233
|
-
* @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
|
|
234
631
|
*/
|
|
235
632
|
declare const httpStream: <Ref extends HttpStreamRef>(route: Ref, args?: HttpStreamArgsOf<Ref>, options?: HttpStreamOptions) => StreamIterable<HttpStreamChunkOf<Ref>>;
|
|
236
|
-
/**
|
|
237
|
-
interface
|
|
238
|
-
|
|
633
|
+
/** Options for {@link connectLiveStream}. */
|
|
634
|
+
interface ConnectLiveStreamOptions {
|
|
635
|
+
/** `fetch` override (a test seam, or a custom transport). */
|
|
636
|
+
fetch?: typeof fetch;
|
|
637
|
+
/** Extra headers sent on both the stream and the control requests. */
|
|
638
|
+
headers?: Record<string, string>;
|
|
639
|
+
/**
|
|
640
|
+
* Treat a connection that delivers nothing — not even a keepalive — for this many milliseconds as
|
|
641
|
+
* dead, and reconnect. Every target that serves live streams (Cloudflare Workers, the Node host
|
|
642
|
+
* and celld) runs the same `ShardDO` live handlers, and they write a `:ka` comment every 25s
|
|
643
|
+
* while a stream is open, so the default is `75000`. Set `0` to disable the bound when a proxy
|
|
644
|
+
* or a server of your own in front of the shard does not forward comment frames.
|
|
645
|
+
*/
|
|
646
|
+
idleTimeoutMs?: number;
|
|
647
|
+
/** Ceiling for the reconnect backoff, in ms. Defaults to `30000`. */
|
|
648
|
+
maxDelayMs?: number;
|
|
649
|
+
/** Base reconnect delay in ms. Defaults to `1000`. */
|
|
650
|
+
retryDelay?: number;
|
|
651
|
+
/** Send cookies / credentials on the stream and control requests. */
|
|
652
|
+
withCredentials?: boolean;
|
|
653
|
+
}
|
|
654
|
+
/** A delivered live event. */
|
|
655
|
+
interface LiveStreamEvent {
|
|
656
|
+
/** The decoded payload the publisher sent. */
|
|
657
|
+
payload: unknown;
|
|
658
|
+
/** The topic the event was published on. */
|
|
659
|
+
topic: string;
|
|
660
|
+
}
|
|
661
|
+
/** Options for {@link LiveStream.subscribe}. */
|
|
662
|
+
/** A subscribe the shard refused for good: a reconnect would only be refused again. */
|
|
663
|
+
interface LiveStreamSubscribeError {
|
|
664
|
+
/** The HTTP status of the refusal, e.g. 403, 409 or 429. */
|
|
665
|
+
status: number;
|
|
666
|
+
subscriptionId: string;
|
|
667
|
+
topic: string;
|
|
668
|
+
}
|
|
669
|
+
interface LiveStreamSubscribeOptions {
|
|
670
|
+
/** Stable caller id; a generated one is used when omitted. */
|
|
671
|
+
id?: string;
|
|
672
|
+
/**
|
|
673
|
+
* Called when the shard refuses this subscription (403, 409, 429 and similar). The subscription is dropped
|
|
674
|
+
* first, so a later subscribe with the same id is accepted. A 401 is not reported: the stream reconnects,
|
|
675
|
+
* which re-authenticates and registers the subscription again.
|
|
676
|
+
*/
|
|
677
|
+
onError?: (error: LiveStreamSubscribeError) => void;
|
|
678
|
+
/** Called for every event on `topic`. */
|
|
679
|
+
onEvent?: (payload: unknown) => void;
|
|
680
|
+
/** The topic to follow. */
|
|
681
|
+
topic: string;
|
|
682
|
+
}
|
|
683
|
+
/** A live stream connection. */
|
|
684
|
+
interface LiveStream {
|
|
685
|
+
/** Close the stream and stop reconnecting. */
|
|
686
|
+
close: () => void;
|
|
687
|
+
/** The server-minted connection id, or `undefined` before the first connect and while reconnecting. */
|
|
688
|
+
readonly connectionId: string | undefined;
|
|
689
|
+
/** Observe every event on the connection; returns an unsubscribe handle. */
|
|
690
|
+
onEvent: (handler: (event: LiveStreamEvent) => void) => () => void;
|
|
691
|
+
/** Resolves once the first connection is established (rejects only if `close`d first). */
|
|
692
|
+
readonly ready: Promise<void>;
|
|
693
|
+
/** Follow a topic; returns an unsubscribe handle. */
|
|
694
|
+
subscribe: (options: LiveStreamSubscribeOptions) => () => void;
|
|
239
695
|
}
|
|
240
696
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* store the value is the adapter's concern (e.g. Solid wraps function-valued
|
|
244
|
-
* results in a thunk).
|
|
697
|
+
* Connect to a live topic stream. See the module docblock.
|
|
698
|
+
* @param url the `connect` route URL (its sibling `control` route is derived)
|
|
245
699
|
*/
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
700
|
+
declare const connectLiveStream: (url: string, options?: ConnectLiveStreamOptions) => LiveStream;
|
|
701
|
+
/**
|
|
702
|
+
* A reference to one declared live topic, as `_generated/live.ts` hands it out. Plain data
|
|
703
|
+
* (no server code), so a client bundle can hold it; the phantom `Payload` is what lets
|
|
704
|
+
* `ctx.live.publish` and the framework hooks type the event.
|
|
705
|
+
*/
|
|
706
|
+
interface LiveTopicRef<Payload = unknown> {
|
|
707
|
+
/** Phantom: carries the declared payload type. Never present at runtime. */
|
|
708
|
+
readonly __payload?: Payload;
|
|
709
|
+
/** The `id` of the `defineLiveStream` that declares and authorizes this topic. */
|
|
710
|
+
readonly __lunoraLive: string;
|
|
711
|
+
/** The encoded wire topic (see {@link encodeLiveTopic}). */
|
|
712
|
+
readonly topic: string;
|
|
713
|
+
}
|
|
714
|
+
/** Options for {@link subscribeLiveTopic}. */
|
|
715
|
+
interface SubscribeLiveTopicOptions extends Omit<ConnectLiveStreamOptions, "fetch"> {
|
|
716
|
+
/** Called when the shard refuses this subscription (403, 409, 429 and similar). */
|
|
717
|
+
onError?: (error: LiveStreamSubscribeError) => void;
|
|
718
|
+
/** Called for every event on `topic`. */
|
|
719
|
+
onEvent?: (payload: unknown) => void;
|
|
720
|
+
/**
|
|
721
|
+
* Partitions the shared connection. A cookie-authenticated stream is bound to the session that opened it, so
|
|
722
|
+
* pass something that changes when the signed-in user does (a user id): a new value opens a new connection
|
|
723
|
+
* and the old one closes once its subscribers have moved. It is never sent to the server.
|
|
724
|
+
*/
|
|
725
|
+
sessionKey?: string;
|
|
726
|
+
/** The topic to follow. */
|
|
727
|
+
topic: string;
|
|
728
|
+
/** The `connect` route URL of a `defineLiveStream`. */
|
|
729
|
+
url: string;
|
|
253
730
|
}
|
|
254
731
|
/**
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
|
|
263
|
-
|
|
732
|
+
* Follow `topic` on the shared connection for `url`.
|
|
733
|
+
* @returns an unsubscribe handle; the last one out closes the connection
|
|
734
|
+
*/
|
|
735
|
+
declare const subscribeLiveTopic: (options: SubscribeLiveTopicOptions) => (() => void);
|
|
736
|
+
/**
|
|
737
|
+
* What a framework hook takes besides the topic reference: the transport tuning of
|
|
738
|
+
* {@link SubscribeLiveTopicOptions}, with `onEvent` typed by the topic's declared payload.
|
|
739
|
+
*/
|
|
740
|
+
interface LiveTopicOptions<Payload = unknown> extends Omit<SubscribeLiveTopicOptions, "onEvent" | "topic" | "url"> {
|
|
741
|
+
/** Called for every event on the topic. The payload is the type the topic declares. */
|
|
742
|
+
onEvent?: (payload: Payload) => void;
|
|
743
|
+
/**
|
|
744
|
+
* Follow the topic on this shard: the connect URL gets `?shard=<key>`, and the control calls carry it too. Omit it
|
|
745
|
+
* for the default shard. Publish to the same shard with `{ shardKey }`, since delivery never crosses shards.
|
|
746
|
+
*/
|
|
747
|
+
shardKey?: string;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Resolve a topic reference to the string-based options {@link subscribeLiveTopic} takes: the connect URL is the
|
|
751
|
+
* stream's route under the client's base URL, and the topic is the reference's encoded wire topic. Every framework
|
|
752
|
+
* hook goes through this, so they all build the same connection (and so share it).
|
|
264
753
|
*/
|
|
265
|
-
declare const
|
|
754
|
+
declare const liveTopicSubscription: <Payload>(client: {
|
|
755
|
+
readonly url: string;
|
|
756
|
+
}, ref: LiveTopicRef<Payload>, options?: LiveTopicOptions<Payload>) => SubscribeLiveTopicOptions;
|
|
266
757
|
/**
|
|
267
758
|
* The structural surface of a TanStack `Transaction` a bound custom mutator
|
|
268
759
|
* returns — its `isPersisted.promise` resolves once the write is persisted and
|
|
@@ -317,15 +808,48 @@ declare const createMutatorRunner: <TArgs>(handle: MutatorHandle<TArgs>, sinks:
|
|
|
317
808
|
reset: () => void;
|
|
318
809
|
};
|
|
319
810
|
interface QueuedMutation<T = unknown> {
|
|
811
|
+
/**
|
|
812
|
+
* Set on a write that was queued because its direct send failed in a way that
|
|
813
|
+
* says nothing about its fate (see `LunoraClient.mutation`). Its caller is still
|
|
814
|
+
* waiting and was never told it failed, so the replay counts its transient
|
|
815
|
+
* failures and rejects it after a few, rather than retrying for ever.
|
|
816
|
+
*/
|
|
817
|
+
readonly ambiguousSend?: boolean;
|
|
320
818
|
readonly args: Record<string, unknown>;
|
|
819
|
+
/**
|
|
820
|
+
* CDC cursor this write was composed against (see
|
|
821
|
+
* {@link PersistedMutation.baselineSeq}). Persisted and restored so a replay is
|
|
822
|
+
* judged against what its author could see, not against what the client has
|
|
823
|
+
* since caught up to.
|
|
824
|
+
*/
|
|
825
|
+
readonly baselineSeq?: number;
|
|
826
|
+
/**
|
|
827
|
+
* The client id that queued this write (see {@link PersistedMutation.clientId}).
|
|
828
|
+
* Persisted and restored, so a replay namespaces by the id that issued the
|
|
829
|
+
* write rather than whatever the current session minted.
|
|
830
|
+
*/
|
|
831
|
+
clientId?: string;
|
|
321
832
|
readonly functionPath: string;
|
|
322
833
|
/** Stable id used to remove the entry from durable storage once replayed; assigned by the queue when absent. */
|
|
323
834
|
id?: string;
|
|
324
835
|
/**
|
|
325
836
|
* Issuing identity fingerprint carried through to durable storage (`null` =
|
|
326
837
|
* signed out). Absent on hydrated legacy records, which replay ambiently.
|
|
838
|
+
*
|
|
839
|
+
* Mutable so {@link OfflineQueue.restampIdentity} can relabel a still-queued
|
|
840
|
+
* write when the identity's LABEL changes but the credential does not (the
|
|
841
|
+
* `setAuthToken(token, userId)` case where the subject resolves a tick after
|
|
842
|
+
* the token). The value is re-persisted alongside, so the new label survives
|
|
843
|
+
* a reload and a requeue.
|
|
844
|
+
*/
|
|
845
|
+
identity?: string | null;
|
|
846
|
+
/**
|
|
847
|
+
* Position in the order this session ISSUED its writes, taken before the first send. A write whose
|
|
848
|
+
* direct send failed ambiguously is queued only once that send gives up, behind writes issued (and
|
|
849
|
+
* queued) while it was in flight; this puts it back ahead of them. Absent on hydrated records,
|
|
850
|
+
* which are older than anything issued this session.
|
|
327
851
|
*/
|
|
328
|
-
readonly
|
|
852
|
+
readonly issueSeq?: number;
|
|
329
853
|
/**
|
|
330
854
|
* `true` when a live caller is still awaiting this write's `mutation()`
|
|
331
855
|
* Promise; `false`/absent for a write restored from durable storage after a
|
|
@@ -359,6 +883,14 @@ interface QueuedMutation<T = unknown> {
|
|
|
359
883
|
* eviction), so the client can surface the dropped write on its
|
|
360
884
|
* terminal-verdict observer even when the entry has no live awaiter (a hydrated
|
|
361
885
|
* record). The `error` carries the `OFFLINE_QUEUE_OVERFLOW` code.
|
|
886
|
+
*
|
|
887
|
+
* The `sdks/*` ports deliberately do NOT mirror this shape — they return the
|
|
888
|
+
* discarded entry from `enqueue`/`hydrate`/`drainConflict`/`clear` instead,
|
|
889
|
+
* because they call the queue with a real lock held and settling a write needs
|
|
890
|
+
* that same lock, which self-deadlocks a non-reentrant one. Keep the callback
|
|
891
|
+
* here: this client is single-threaded, so the hazard does not exist, and an
|
|
892
|
+
* observer the queue fires itself is what covers the case a return value's
|
|
893
|
+
* consumer can quietly miss — a hydrated entry whose `reject` is a no-op.
|
|
362
894
|
*/
|
|
363
895
|
type EvictHandler = (entry: QueuedMutation, error: Error & {
|
|
364
896
|
code?: string;
|
|
@@ -367,6 +899,8 @@ type EvictHandler = (entry: QueuedMutation, error: Error & {
|
|
|
367
899
|
interface OfflineQueueDeps {
|
|
368
900
|
/** Invoked when an entry is discarded on capacity overflow (carries `OFFLINE_QUEUE_OVERFLOW`). */
|
|
369
901
|
onEvict?: EvictHandler;
|
|
902
|
+
/** Invoked when a write that was out for replay settles, which `onSizeChange` does not cover: the queue was already shorter. */
|
|
903
|
+
onReplayingChange?: () => void;
|
|
370
904
|
/** Invoked with the new depth after any size change (drives the client's pending-sync count). */
|
|
371
905
|
onSizeChange?: (size: number) => void;
|
|
372
906
|
/** Durable store; when present, writes are mirrored and restored across reloads. */
|
|
@@ -394,12 +928,44 @@ declare class OfflineQueue {
|
|
|
394
928
|
private readonly persistence;
|
|
395
929
|
private readonly onEvict;
|
|
396
930
|
private readonly onSizeChange;
|
|
931
|
+
private readonly onReplayingChange;
|
|
397
932
|
/** App/schema version stamped on persisted writes; mismatched records are purged on hydrate. */
|
|
398
933
|
private readonly version;
|
|
399
934
|
private readonly items;
|
|
935
|
+
/** Ids of writes a flush has {@link OfflineQueue.drain}ed and neither requeued nor discarded yet, which are on the wire or about to be. */
|
|
936
|
+
private readonly replayingIds;
|
|
937
|
+
/** Appends still in flight, by write id, each resolving to whether the record landed. Anything that touches a record durably waits on its append first. */
|
|
938
|
+
private readonly appending;
|
|
939
|
+
/**
|
|
940
|
+
* Writes enqueued this session and not yet settled (see {@link OfflineQueue.discard}). Unlike `items`
|
|
941
|
+
* this survives a replay temporarily draining the queue, so a failed append is tracked for as long as
|
|
942
|
+
* the write is unsettled, and a retry derives its record from the entry as it is NOW.
|
|
943
|
+
*/
|
|
944
|
+
private readonly unsettled;
|
|
945
|
+
/** Ids whose append was rejected (a quota error, a closed connection), kept to be tried again — see {@link OfflineQueue.flushUnpersisted}. */
|
|
946
|
+
private readonly unpersisted;
|
|
400
947
|
constructor(options?: OfflineQueueOptions, deps?: OfflineQueueDeps);
|
|
401
948
|
get size(): number;
|
|
949
|
+
/** Writes drained for replay and not yet settled or requeued: out of {@link OfflineQueue.size}, but still unsettled. */
|
|
950
|
+
get replaying(): number;
|
|
402
951
|
enqueue<T>(entry: QueuedMutation<T>): void;
|
|
952
|
+
/**
|
|
953
|
+
* Remove a settled write's durable record — after its append, if that is
|
|
954
|
+
* still in flight. A write that replays quickly can settle before its append
|
|
955
|
+
* has landed; a `remove` that overtook it found nothing, and the record then
|
|
956
|
+
* landed behind it and replayed in the next session.
|
|
957
|
+
*/
|
|
958
|
+
discard(id: string): void;
|
|
959
|
+
/**
|
|
960
|
+
* Try again to persist every write whose append was rejected. Called when the
|
|
961
|
+
* page is being hidden: the last chance to get a still-queued write into
|
|
962
|
+
* durable storage before a reload or a discard of the tab loses it.
|
|
963
|
+
*
|
|
964
|
+
* A write whose append is still in flight is skipped: `visibilitychange` and `pagehide` fire
|
|
965
|
+
* back to back, and a second append for the same id would hit a unique-id constraint (or store
|
|
966
|
+
* the record twice) and leave the write wrongly tracked as unpersisted.
|
|
967
|
+
*/
|
|
968
|
+
flushUnpersisted(): void;
|
|
403
969
|
/**
|
|
404
970
|
* Restore mutations persisted in a prior session and re-queue them in FIFO
|
|
405
971
|
* order. Restored entries already live in durable storage, so they are not
|
|
@@ -419,6 +985,33 @@ declare class OfflineQueue {
|
|
|
419
985
|
* silently clobber the newer data with the stale one.
|
|
420
986
|
*/
|
|
421
987
|
hydrate(): Promise<(string | undefined)[]>;
|
|
988
|
+
/**
|
|
989
|
+
* Relabel every queued write stamped `from` to `to`, in memory AND in durable
|
|
990
|
+
* storage.
|
|
991
|
+
*
|
|
992
|
+
* Used when the auth identity's LABEL changes while the credential does not —
|
|
993
|
+
* `setAuthToken(token, userId)` where the user id resolves a tick after the
|
|
994
|
+
* token was set. `setAuthToken` documents that this re-stamps in-flight
|
|
995
|
+
* queued writes rather than dropping them, and that promise only held for the
|
|
996
|
+
* caller's live in-memory stamp map, which is consumed and deleted on the
|
|
997
|
+
* first flush attempt. Everything durable still carried the old token hash,
|
|
998
|
+
* so a reload — or a transient-failure requeue after the token had since been
|
|
999
|
+
* refreshed — fell back to it, failed the replay identity gate, and rejected
|
|
1000
|
+
* the SAME user's offline write with `OFFLINE_IDENTITY_CHANGED`.
|
|
1001
|
+
*
|
|
1002
|
+
* The durable rewrite goes through `PersistenceAdapter.replace`, the one
|
|
1003
|
+
* operation on the contract that is required to be atomic — see
|
|
1004
|
+
* {@link OfflineQueue.rewriteStamp}.
|
|
1005
|
+
*/
|
|
1006
|
+
restampIdentity(from: string | null, to: string | null): void;
|
|
1007
|
+
/**
|
|
1008
|
+
* Whether any queued mutation matches — the cheap "is there a write ahead of
|
|
1009
|
+
* me on this shard?" question, answered without draining. A write held at
|
|
1010
|
+
* flush time (an identity not yet re-confirmed) sits here with no barrier
|
|
1011
|
+
* published for a later `mutation()` to wait on, so this is what keeps a
|
|
1012
|
+
* live write from overtaking it.
|
|
1013
|
+
*/
|
|
1014
|
+
hasPending(predicate: (item: QueuedMutation) => boolean): boolean;
|
|
422
1015
|
/**
|
|
423
1016
|
* Remove and return queued mutations. With no `predicate`, drains the whole
|
|
424
1017
|
* queue. With one, drains only matching entries (preserving FIFO order) and
|
|
@@ -444,6 +1037,41 @@ declare class OfflineQueue {
|
|
|
444
1037
|
*/
|
|
445
1038
|
drainConflict(): QueuedMutation[];
|
|
446
1039
|
clear(): void;
|
|
1040
|
+
/** The durable form of a queued write. */
|
|
1041
|
+
private recordOf;
|
|
1042
|
+
/** Append `record`, remembering it as unpersisted if the append is rejected. */
|
|
1043
|
+
private persist;
|
|
1044
|
+
/**
|
|
1045
|
+
* The durable half of {@link OfflineQueue.restampIdentity}: rewrite the
|
|
1046
|
+
* persisted record under the new identity stamp.
|
|
1047
|
+
*
|
|
1048
|
+
* This used to be `remove` then `append` (because `append` is an insert, not
|
|
1049
|
+
* an upsert) with a compensating re-append on failure, and no arrangement of
|
|
1050
|
+
* those two calls is safe. A process stop between a committed `remove` and
|
|
1051
|
+
* the `append` leaves the mutation in NO durable store while the in-memory
|
|
1052
|
+
* entry has already advanced, so a reload loses the write outright — and
|
|
1053
|
+
* compensation cannot cover a crash, only a rejection. The re-append also
|
|
1054
|
+
* moved the record to the tail, replaying it out of issue order.
|
|
1055
|
+
*
|
|
1056
|
+
* `PersistenceAdapter.replace` is the single atomic operation that removes
|
|
1057
|
+
* both: the swap lands whole or not at all, and the record keeps its place
|
|
1058
|
+
* in FIFO order. A rejection means nothing changed durably — the record
|
|
1059
|
+
* stands under its OLD stamp, which is the documented outcome (a replay
|
|
1060
|
+
* under a stale stamp is refused with `OFFLINE_IDENTITY_CHANGED`, visible
|
|
1061
|
+
* and recoverable, unlike a silent loss) — so there is nothing to
|
|
1062
|
+
* compensate, only to report.
|
|
1063
|
+
*/
|
|
1064
|
+
private rewriteStamp;
|
|
1065
|
+
/**
|
|
1066
|
+
* Evict entries from the FRONT of `items` (the oldest — FIFO order) until
|
|
1067
|
+
* the queue is at or under `maxItems`, rejecting each with
|
|
1068
|
+
* `OFFLINE_QUEUE_OVERFLOW`, un-persisting it, and firing `onEvict`. Shared
|
|
1069
|
+
* by `enqueue` (a live write pushes past capacity) and `hydrate` (a durable
|
|
1070
|
+
* store restored more than `maxItems` records — CLIENT-03) so an overflow
|
|
1071
|
+
* always drops the same way regardless of which caller triggered it.
|
|
1072
|
+
*/
|
|
1073
|
+
private evictOverflow;
|
|
1074
|
+
private markReplaying;
|
|
447
1075
|
/** Notify the size observer (the client's pending-sync count) after any change. */
|
|
448
1076
|
private notifySize;
|
|
449
1077
|
}
|
|
@@ -494,7 +1122,7 @@ interface IndexedDbQueryCacheOptions {
|
|
|
494
1122
|
databaseName?: string;
|
|
495
1123
|
/** Injectable `IDBFactory` (e.g. `fake-indexeddb` in tests); defaults to the global `indexedDB`. */
|
|
496
1124
|
indexedDB?: IDBFactory;
|
|
497
|
-
/** LRU row cap; defaults to 500. The oldest rows by `ts` are pruned on `put` once exceeded. */
|
|
1125
|
+
/** LRU row cap; defaults to 500. Must be a positive integer. The oldest rows by `ts` are pruned on `put` once exceeded. */
|
|
498
1126
|
maxEntries?: number;
|
|
499
1127
|
/** Object-store name; defaults to `"query-cache"`. */
|
|
500
1128
|
storeName?: string;
|
|
@@ -512,28 +1140,22 @@ interface IndexedDbQueryCacheOptions {
|
|
|
512
1140
|
* environments should use {@link createInMemoryQueryCache}.
|
|
513
1141
|
*/
|
|
514
1142
|
declare const createIndexedDbQueryCache: (options?: IndexedDbQueryCacheOptions) => QueryCacheAdapter;
|
|
515
|
-
/**
|
|
516
|
-
* Exponential backoff calculator with optional jitter.
|
|
517
|
-
*
|
|
518
|
-
* `next()` doubles the delay each call up to `maxDelayMs`. When `jitter` is
|
|
519
|
-
* enabled the returned value is randomized in `[delay/2, delay]` so a fleet
|
|
520
|
-
* of clients reconnecting at the same time spread out their retries.
|
|
521
|
-
*/
|
|
522
|
-
interface ReconnectCalculator {
|
|
523
|
-
/** Returns the delay to wait before the next reconnect attempt. */
|
|
524
|
-
next: () => number;
|
|
525
|
-
/** Resets the backoff to the initial delay (call on successful reconnect). */
|
|
526
|
-
reset: () => void;
|
|
527
|
-
}
|
|
528
|
-
declare const createReconnect: (options?: ReconnectOptions, random?: () => number) => ReconnectCalculator;
|
|
529
1143
|
/**
|
|
530
1144
|
* Capture a snapshot of the current live query value at call time and produce a
|
|
531
1145
|
* `() => boolean` precondition that compares it against the value at replay time.
|
|
532
1146
|
*
|
|
533
1147
|
* When the precondition is checked (on queue drain / reconnect) it re-reads the
|
|
534
|
-
* query's current
|
|
535
|
-
*
|
|
536
|
-
*
|
|
1148
|
+
* query's current state via {@link LunoraClient.peekActiveQuerySnapshot}. The
|
|
1149
|
+
* comparison only runs when a live subscription backed **both** reads; if either
|
|
1150
|
+
* read found no active subscription (e.g. the originating component unmounted
|
|
1151
|
+
* before replay, or a reconnect blanked it and it has not been answered again),
|
|
1152
|
+
* the precondition returns `true` (not stale) because absence of a subscription
|
|
1153
|
+
* is not evidence the value changed — the read simply cannot see it. When both reads did have a live subscription and the value differs, the
|
|
1154
|
+
* precondition returns `false` and the offline mutation is dropped as stale.
|
|
1155
|
+
*
|
|
1156
|
+
* The offline queue does not evaluate it while the subscription is still waiting
|
|
1157
|
+
* for its answer on a reconnected socket: the value it holds then is the previous
|
|
1158
|
+
* socket's (or blank), so the write stays queued until the answer lands.
|
|
537
1159
|
* @example
|
|
538
1160
|
* ```ts
|
|
539
1161
|
* client.mutation(api.todos.update, { id, text }, {
|
|
@@ -542,92 +1164,4 @@ declare const createReconnect: (options?: ReconnectOptions, random?: () => numbe
|
|
|
542
1164
|
* ```
|
|
543
1165
|
*/
|
|
544
1166
|
declare const createSnapshotPrecondition: (client: LunoraClient, functionRef: FunctionReference, args: Record<string, unknown>, shardKey?: string) => (() => boolean);
|
|
545
|
-
|
|
546
|
-
* Client-side service-worker registration and lifecycle management.
|
|
547
|
-
*
|
|
548
|
-
* Usage:
|
|
549
|
-
* ```ts
|
|
550
|
-
* const sw = new ClientServiceWorker({ swUrl: "/sw.js" });
|
|
551
|
-
* await sw.register();
|
|
552
|
-
*
|
|
553
|
-
* if (sw.active) {
|
|
554
|
-
* sw.postMessage({ type: "sync" });
|
|
555
|
-
* }
|
|
556
|
-
* ```
|
|
557
|
-
*/
|
|
558
|
-
type ServiceWorkerStatus = "unsupported" | "unregistered" | "registering" | "active" | "error";
|
|
559
|
-
interface ClientSwOptions {
|
|
560
|
-
/** Called when the status changes. */
|
|
561
|
-
onStatusChange?: (status: ServiceWorkerStatus) => void;
|
|
562
|
-
/** Optional registration scope. Defaults to `/`. */
|
|
563
|
-
scope?: string;
|
|
564
|
-
/** URL of the service worker script (relative to origin). */
|
|
565
|
-
swUrl: string;
|
|
566
|
-
}
|
|
567
|
-
/**
|
|
568
|
-
* Manages service-worker registration and provides a simple API for
|
|
569
|
-
* sending messages and listening for responses.
|
|
570
|
-
*/
|
|
571
|
-
declare class ClientServiceWorker {
|
|
572
|
-
#private;
|
|
573
|
-
readonly swUrl: string;
|
|
574
|
-
readonly scope: string;
|
|
575
|
-
constructor(options: ClientSwOptions);
|
|
576
|
-
/** Current registration status. */
|
|
577
|
-
get status(): ServiceWorkerStatus;
|
|
578
|
-
/** The underlying `ServiceWorkerRegistration`, if registered. */
|
|
579
|
-
get registration(): ServiceWorkerRegistration | undefined;
|
|
580
|
-
/** Whether the SW is currently controlling this page. */
|
|
581
|
-
get active(): boolean;
|
|
582
|
-
/**
|
|
583
|
-
* Register the service worker.
|
|
584
|
-
*
|
|
585
|
-
* Returns `false` when the browser does not support service workers.
|
|
586
|
-
*/
|
|
587
|
-
register(): Promise<boolean>;
|
|
588
|
-
/**
|
|
589
|
-
* Remove the active service worker registration and clear listeners.
|
|
590
|
-
*
|
|
591
|
-
* Returns `false` when no registration is currently held.
|
|
592
|
-
*/
|
|
593
|
-
unregister(): Promise<boolean>;
|
|
594
|
-
/**
|
|
595
|
-
* Send a message to the active service worker.
|
|
596
|
-
*/
|
|
597
|
-
postMessage(message: unknown): void;
|
|
598
|
-
/**
|
|
599
|
-
* Register a handler for messages **from** the service worker.
|
|
600
|
-
* @returns Unsubscribe function.
|
|
601
|
-
*/
|
|
602
|
-
onMessage(handler: (event: MessageEvent) => void): () => void;
|
|
603
|
-
}
|
|
604
|
-
/**
|
|
605
|
-
* Outbound message from the client to the SW.
|
|
606
|
-
*/
|
|
607
|
-
interface ClientToSwMessage {
|
|
608
|
-
/** Opaque correlation ID for request/response patterns. */
|
|
609
|
-
correlationId?: string;
|
|
610
|
-
payload?: unknown;
|
|
611
|
-
type: string;
|
|
612
|
-
}
|
|
613
|
-
/**
|
|
614
|
-
* Inbound message from the SW to the client.
|
|
615
|
-
*/
|
|
616
|
-
interface SwToClientMessage {
|
|
617
|
-
/** Echoes the correlation ID from the client request, if any. */
|
|
618
|
-
correlationId?: string;
|
|
619
|
-
payload?: unknown;
|
|
620
|
-
type: string;
|
|
621
|
-
}
|
|
622
|
-
/**
|
|
623
|
-
* Send a typed message to the service worker and optionally await a
|
|
624
|
-
* matching response.
|
|
625
|
-
* @returns A promise that resolves when the SW sends a reply with the
|
|
626
|
-
* same `correlationId` (if `expectResponse` is true).
|
|
627
|
-
*/
|
|
628
|
-
declare const sendToSw: (sw: ServiceWorker | null, message: ClientToSwMessage, expectResponse?: boolean) => Promise<unknown>;
|
|
629
|
-
/**
|
|
630
|
-
* Create a reply for a client message (call from inside the SW).
|
|
631
|
-
*/
|
|
632
|
-
declare const createReply: (original: ClientToSwMessage, payload?: unknown) => SwToClientMessage;
|
|
633
|
-
export { type ArgsOf, type AsyncStorageLike, type AsyncStoragePersistenceOptions, type BookmarkStorage, ClientServiceWorker, type ClientSwOptions, type ClientToSwMessage, type FunctionReference, type HttpStreamArgsOf, type HttpStreamChunkOf, type HttpStreamOptions, type HttpStreamRef, type IndexedDbPersistenceOptions, type IndexedDbQueryCacheOptions, LunoraClient, type MutationCallOptions, type MutationDelta, type MutationRunnerSinks, type MutatorHandle, type MutatorRunnerSinks, type MutatorTransaction, OfflineQueue, type OfflineQueueOptions, type PersistenceAdapter, type QueryCacheAdapter, type QueuedMutation, type ReconnectCalculator, type ReconnectOptions, type ReturnOf, type ServiceWorkerStatus, type StreamIterable, type SubscriptionError, type SwToClientMessage, TabCoordinator, applyDelta, createAsyncStoragePersistence, createInMemoryBookmarkStorage, createInMemoryPersistence, createInMemoryQueryCache, createIndexedDbPersistence, createIndexedDbQueryCache, createMutationRunner, createMutatorRunner, createReconnect, createReply, createSnapshotPrecondition, httpStream, isMutationDelta, queryCacheKey, sendToSw };
|
|
1167
|
+
export { type AsyncStorageLike, type AsyncStoragePersistenceOptions, type AsyncStorageQueryCacheOptions, type BookmarkStorage, CONFLICT_ERROR_CODE, type ConnectLiveStreamOptions, type ConnectionStatus, type FunctionReference, type HttpStreamArgsOf, type HttpStreamChunkOf, type HttpStreamOptions, type HttpStreamRef, type IndexedDbPersistenceOptions, type IndexedDbQueryCacheOptions, type LiveStream, type LiveStreamEvent, type LiveStreamSubscribeError, type LiveStreamSubscribeOptions, type LiveTopicOptions, type LiveTopicRef, LunoraClient, type MutationDelta, type MutatorHandle, type MutatorRunnerSinks, type MutatorTransaction, OfflineQueue, type OfflineQueueOptions, type OptimisticMessage, type PersistenceAdapter, type QueryCacheAdapter, type QueuedMutation, RETIRE_AFTER_DURABLE_SEQ_ADVANCE, type ReconcileDurableMessage, type ReconnectCalculator, type ReconnectOptions, type StreamIterable, type SubscribeLiveTopicOptions, type SubscriptionError, TabCoordinator, TransportError, applyDelta, connectLiveStream, createAsyncStoragePersistence, createAsyncStorageQueryCache, createCallRunner, createInMemoryBookmarkStorage, createInMemoryPersistence, createInMemoryQueryCache, createIndexedDbPersistence, createIndexedDbQueryCache, createMutatorRunner, createReconnect, createSnapshotPrecondition, getErrorCode, getRetryAfterMs, httpStream, isAuthReplayFailure, isConflictError, isForbiddenError, isMutationDelta, isRateLimitedError, isUnauthorizedError, liveTopicSubscription, maxSeq, queryCacheKey, reconcileOptimistic, subscribeLiveTopic };
|