@taladb/react 0.10.2 → 0.11.0
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-MIT +21 -0
- package/README.md +187 -0
- package/dist/chunk-SEH233OC.mjs +129 -0
- package/dist/index.d.mts +40 -309
- package/dist/index.d.ts +40 -309
- package/dist/index.js +23 -575
- package/dist/index.mjs +32 -707
- package/dist/query/index.d.mts +1664 -0
- package/dist/query/index.d.ts +1664 -0
- package/dist/query/index.js +2226 -0
- package/dist/query/index.mjs +2091 -0
- package/package.json +14 -5
- /package/{LICENSE → LICENSE-APACHE} +0 -0
|
@@ -0,0 +1,1664 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { Filter, Document, Value, Collection, TalaDB } from 'taladb';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Request parameters, declared once and used three times.
|
|
7
|
+
*
|
|
8
|
+
* An application already has `?category=book&page=1&sort=-createdAt`. Restating
|
|
9
|
+
* `category` a second time as an engine predicate is duplication that drifts, so
|
|
10
|
+
* one declaration feeds the query key, the request, and the local filter.
|
|
11
|
+
*
|
|
12
|
+
* **Nothing is inferred.** The obvious convention — "a param whose name matches
|
|
13
|
+
* a document field is a predicate, anything else is pagination" — has to be
|
|
14
|
+
* rejected, because there is no reliable way to know a collection's field names:
|
|
15
|
+
* schemas are Standard Schema values with no portable key enumeration, and
|
|
16
|
+
* sampling documents fails exactly when it matters most, on a cold empty
|
|
17
|
+
* collection. Guessing wrong there is silent: `page: 1` treated as a predicate
|
|
18
|
+
* matches nothing and renders an empty screen with `status: 'success'`.
|
|
19
|
+
*
|
|
20
|
+
* So an undeclared param contributes no local predicate. It still reaches the
|
|
21
|
+
* key and the request; it simply does not narrow the local read. The local
|
|
22
|
+
* filter is then *broader* than the server's, never narrower — and being
|
|
23
|
+
* broader costs nothing, because the query's stored id list still bounds the
|
|
24
|
+
* result.
|
|
25
|
+
*/
|
|
26
|
+
type ParamOp = '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$in' | '$contains';
|
|
27
|
+
type ParamPrimitive = string | number | boolean;
|
|
28
|
+
type ParamValue = ParamPrimitive | ParamPrimitive[] | null | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* What one request parameter means.
|
|
31
|
+
*
|
|
32
|
+
* `TValue` is carried only for inference, so `defineParams` can type its own
|
|
33
|
+
* call signature from the declaration.
|
|
34
|
+
*/
|
|
35
|
+
interface ParamSpec<TValue = ParamValue> {
|
|
36
|
+
readonly kind: 'predicate' | 'shape';
|
|
37
|
+
readonly field?: string;
|
|
38
|
+
readonly op?: ParamOp;
|
|
39
|
+
/** Phantom. Never read at runtime. */
|
|
40
|
+
readonly __value?: TValue;
|
|
41
|
+
}
|
|
42
|
+
/** `?status=open` → `{ status: 'open' }` */
|
|
43
|
+
declare const eq: (field: string) => ParamSpec<ParamPrimitive>;
|
|
44
|
+
/** `?exclude=archived` → `{ status: { $ne: 'archived' } }` */
|
|
45
|
+
declare const ne: (field: string) => ParamSpec<ParamPrimitive>;
|
|
46
|
+
/** `?minPrice=10` → `{ price: { $gte: 10 } }` */
|
|
47
|
+
declare const gte: (field: string) => ParamSpec<number | string>;
|
|
48
|
+
/** `?maxPrice=99` → `{ price: { $lte: 99 } }` */
|
|
49
|
+
declare const lte: (field: string) => ParamSpec<number | string>;
|
|
50
|
+
/** `?after=2026-01-01` → `{ createdAt: { $gt: … } }` */
|
|
51
|
+
declare const gt: (field: string) => ParamSpec<number | string>;
|
|
52
|
+
/** `?before=2026-01-01` → `{ createdAt: { $lt: … } }` */
|
|
53
|
+
declare const lt: (field: string) => ParamSpec<number | string>;
|
|
54
|
+
/** `?search=dune` → `{ title: { $contains: 'dune' } }` */
|
|
55
|
+
declare const contains: (field: string) => ParamSpec<string>;
|
|
56
|
+
/** `?tags=a,b` → `{ tags: { $in: ['a', 'b'] } }` */
|
|
57
|
+
declare const oneOf: (field: string) => ParamSpec<ParamPrimitive[]>;
|
|
58
|
+
/**
|
|
59
|
+
* A parameter that shapes the *response* rather than selecting documents —
|
|
60
|
+
* `page`, `perPage`, `sort`, `cursor`.
|
|
61
|
+
*
|
|
62
|
+
* These have no local meaning at all: the stored id list already *is* the slice,
|
|
63
|
+
* in the server's order. Declaring them explicitly is what turns a typo'd param
|
|
64
|
+
* name into something visible rather than a param that quietly stops filtering.
|
|
65
|
+
*/
|
|
66
|
+
declare const shape: () => ParamSpec<ParamPrimitive>;
|
|
67
|
+
/** A parameter set with its values bound, ready for a request and a query. */
|
|
68
|
+
interface BoundParams<TValues = Record<string, ParamValue>> {
|
|
69
|
+
/** The values as given. */
|
|
70
|
+
readonly values: TValues;
|
|
71
|
+
/**
|
|
72
|
+
* The request half. Empty and `null` values are dropped; arrays are
|
|
73
|
+
* comma-joined; booleans become `true`/`false`.
|
|
74
|
+
*/
|
|
75
|
+
toSearchParams(): URLSearchParams;
|
|
76
|
+
/**
|
|
77
|
+
* The collection half — `undefined` when no declared parameter is set, which
|
|
78
|
+
* is the case for a plain object.
|
|
79
|
+
*/
|
|
80
|
+
toFilter(): Filter<Document> | undefined;
|
|
81
|
+
/** Declared parameters of kind `shape` that currently have a value. */
|
|
82
|
+
shapeParams(): string[];
|
|
83
|
+
}
|
|
84
|
+
/** The value type a declaration accepts. Every parameter is optional. */
|
|
85
|
+
type ValuesOf<S> = {
|
|
86
|
+
[K in keyof S]?: S[K] extends ParamSpec<infer V> ? V : never;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Declare a parameter set.
|
|
90
|
+
*
|
|
91
|
+
* Returns a function that binds values to it. The value type is inferred from
|
|
92
|
+
* the declaration, so a typo is a compile error rather than a parameter that
|
|
93
|
+
* silently stops filtering — protection a stringly-typed `?category=` never
|
|
94
|
+
* gives you.
|
|
95
|
+
*/
|
|
96
|
+
declare function defineParams<S extends Record<string, ParamSpec<never>>>(spec: S): (values: ValuesOf<S>) => BoundParams<ValuesOf<S>>;
|
|
97
|
+
/** True for anything already bound — a declaration's output, not a plain object. */
|
|
98
|
+
declare function isBoundParams(value: unknown): value is BoundParams;
|
|
99
|
+
/**
|
|
100
|
+
* Accept either form.
|
|
101
|
+
*
|
|
102
|
+
* A plain object gets the request half and no local predicates — which is the
|
|
103
|
+
* safe default, and the reason upgrading to `defineParams` is additive rather
|
|
104
|
+
* than a rewrite.
|
|
105
|
+
*/
|
|
106
|
+
declare function normalizeParams(params: BoundParams | Record<string, ParamValue> | undefined): BoundParams | undefined;
|
|
107
|
+
|
|
108
|
+
/** Where a document sits relative to the server. Indexed — this *is* the queue. */
|
|
109
|
+
type SyncState = 'synced' | 'pending' | 'failed';
|
|
110
|
+
/** What a pending document still owes the server. */
|
|
111
|
+
type SyncOp = 'insert' | 'update' | 'delete';
|
|
112
|
+
/**
|
|
113
|
+
* Fields this layer adds to every document it manages, alongside the engine's
|
|
114
|
+
* own `_id`, `_changed_at` and `_v`.
|
|
115
|
+
*
|
|
116
|
+
* They live *on the document* rather than in a separate outbox collection
|
|
117
|
+
* because TalaDB exposes no multi-collection transaction: a document write and
|
|
118
|
+
* an outbox row describing it would be two commits, and a crash between them
|
|
119
|
+
* either orphans a queued mutation or strands a document that never syncs.
|
|
120
|
+
* Keeping the state here makes every queued write a single atomic commit.
|
|
121
|
+
*
|
|
122
|
+
* All `_`-prefixed names are reserved. A user schema that declares one will be
|
|
123
|
+
* rejected rather than silently overwritten.
|
|
124
|
+
*/
|
|
125
|
+
interface SyncEnvelope {
|
|
126
|
+
/** Drain state. Indexed. */
|
|
127
|
+
_sync: SyncState;
|
|
128
|
+
/** Meaningful only while `_sync !== 'synced'`. */
|
|
129
|
+
_op?: SyncOp;
|
|
130
|
+
/** Retry count, drives backoff. */
|
|
131
|
+
_attempt: number;
|
|
132
|
+
/** Last terminal error, surfaced by `useSyncStatus`. */
|
|
133
|
+
_error: string | null;
|
|
134
|
+
/**
|
|
135
|
+
* Epoch ms of the last server hydration, or `0` for a document that only
|
|
136
|
+
* exists locally so far.
|
|
137
|
+
*
|
|
138
|
+
* There is deliberately no `_expires_at` companion. Freshness belongs to a
|
|
139
|
+
* *query*, not a document — see {@link QueryRecord} — and nothing here
|
|
140
|
+
* expires by deletion: these documents live in the application's own
|
|
141
|
+
* collections, which it also reads directly, so a TTL sweep would delete real
|
|
142
|
+
* user data rather than reclaim a cache.
|
|
143
|
+
*/
|
|
144
|
+
_fetched_at: number;
|
|
145
|
+
/**
|
|
146
|
+
* Epoch ms before which the drain will not retry this document. `0` means
|
|
147
|
+
* eligible now; the drain pushes it forward as it backs off.
|
|
148
|
+
*/
|
|
149
|
+
_retry_at: number;
|
|
150
|
+
/**
|
|
151
|
+
* Where this write is going, as a URL template — `/api/v2/todos/:id`.
|
|
152
|
+
*
|
|
153
|
+
* Stamped when the write is queued, because a queued write outlives the
|
|
154
|
+
* component that made it: the drain may send it after a route change, a
|
|
155
|
+
* reload, or a week later, when no closure survives to ask. A template is
|
|
156
|
+
* data and can be stored; a function cannot.
|
|
157
|
+
*
|
|
158
|
+
* Absent falls back to the provider's backend.
|
|
159
|
+
*/
|
|
160
|
+
_endpoint?: string;
|
|
161
|
+
/** HTTP verb for the current `_op`, recomputed whenever `_op` changes. */
|
|
162
|
+
_method?: string;
|
|
163
|
+
/** Opaque local revision used to detect edits while a request is in flight. */
|
|
164
|
+
_revision: string | null;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* One record per query shape, in the `taladb_queries` collection.
|
|
168
|
+
*
|
|
169
|
+
* Separate from the documents on purpose. A collection name says *where
|
|
170
|
+
* documents live*; a query key says *what went stale*. Collapse the two and TTL
|
|
171
|
+
* becomes meaningless — a document belongs to many queries with different
|
|
172
|
+
* freshness — and result-set membership becomes unrecoverable.
|
|
173
|
+
*/
|
|
174
|
+
interface QueryRecord extends Document {
|
|
175
|
+
/** `deriveDocId(QUERY_COLLECTION, collection + key)` — deterministic. */
|
|
176
|
+
_id: string;
|
|
177
|
+
/** The collection whose documents this query returned. */
|
|
178
|
+
collection: string;
|
|
179
|
+
/** The serialised query key, for debugging and inspection. */
|
|
180
|
+
key: string;
|
|
181
|
+
/** Result-set membership, in server order. */
|
|
182
|
+
ids: string[];
|
|
183
|
+
fetchedAt: number;
|
|
184
|
+
/** Freshness window in ms. `0` means "always revalidate". */
|
|
185
|
+
ttl: number;
|
|
186
|
+
/**
|
|
187
|
+
* What `queryFn` returned, so a warm mount can rebuild the same shape without
|
|
188
|
+
* fetching. A detail view whose response was one document must not come back
|
|
189
|
+
* as a one-element array after a reload.
|
|
190
|
+
*/
|
|
191
|
+
shape?: ResultShape;
|
|
192
|
+
/**
|
|
193
|
+
* The raw response, for envelope queries only.
|
|
194
|
+
*
|
|
195
|
+
* Stored so `assemble` has its non-document half — `total`, `nextCursor` —
|
|
196
|
+
* available on a warm mount. Written only when `assemble` is supplied, since
|
|
197
|
+
* nothing else ever reads it and the response duplicates every document it
|
|
198
|
+
* carries.
|
|
199
|
+
*/
|
|
200
|
+
payload?: Value;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* How a request came out, from the drain loop's point of view.
|
|
204
|
+
*
|
|
205
|
+
* `applied` is the one people forget. A write that timed out and was retried
|
|
206
|
+
* will often come back 409 — already applied. Without a way to say so, every
|
|
207
|
+
* retried write poisons the queue permanently.
|
|
208
|
+
*/
|
|
209
|
+
type WriteOutcome = 'ok' | 'applied' | 'retry' | 'terminal';
|
|
210
|
+
/** One queued write, as handed to the backend adapter. */
|
|
211
|
+
interface PendingWrite {
|
|
212
|
+
collection: string;
|
|
213
|
+
/** The client-generated ULID. Authoritative — the server must not reassign it. */
|
|
214
|
+
id: string;
|
|
215
|
+
type: SyncOp;
|
|
216
|
+
/** The stored document after the local commit; `null` for a delete. */
|
|
217
|
+
document: Record<string, unknown> | null;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The application's backend, described well enough for the drain loop to talk
|
|
221
|
+
* to it. This is the only extension point: the library never guesses at URL
|
|
222
|
+
* shapes or status codes.
|
|
223
|
+
*/
|
|
224
|
+
interface BackendDefinition {
|
|
225
|
+
/** Where this write goes. */
|
|
226
|
+
url: (op: PendingWrite) => string;
|
|
227
|
+
/** Defaults to POST / PUT / DELETE for insert / update / delete. */
|
|
228
|
+
method?: Partial<Record<SyncOp, string>>;
|
|
229
|
+
/** Resolved per request, so a write queued offline sends a current token. */
|
|
230
|
+
headers?: () => HeadersInit | Promise<HeadersInit>;
|
|
231
|
+
/** Map a response onto the four outcomes. */
|
|
232
|
+
classify: (response: Response, op: PendingWrite) => WriteOutcome;
|
|
233
|
+
/** Override the fetch implementation (tests, instrumentation). */
|
|
234
|
+
fetch?: typeof globalThis.fetch;
|
|
235
|
+
}
|
|
236
|
+
/** A `BackendDefinition` with defaults filled in. */
|
|
237
|
+
interface ResolvedBackend extends BackendDefinition {
|
|
238
|
+
method: Record<SyncOp, string>;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* When the queue drains. Separate from the mutation call site on purpose: the
|
|
242
|
+
* call site expresses *intent*, the provider expresses *timing*. "Write now,
|
|
243
|
+
* sync later on a schedule" is this knob, not a third mutation mode.
|
|
244
|
+
*/
|
|
245
|
+
type DrainPolicy = 'auto' | 'interval' | 'online-only' | 'manual';
|
|
246
|
+
interface DrainOptions {
|
|
247
|
+
/**
|
|
248
|
+
* `auto` (default) sends an `optimistic` write as soon as it commits.
|
|
249
|
+
* `interval` waits for its timer. `online-only` sends while online and resumes
|
|
250
|
+
* on the browser's `online` event. `manual` never drains on its own.
|
|
251
|
+
*
|
|
252
|
+
* A `queued` mutation ignores `auto` and waits for a trigger either way.
|
|
253
|
+
*/
|
|
254
|
+
policy?: DrainPolicy;
|
|
255
|
+
/** For `'interval'`. */
|
|
256
|
+
intervalMs?: number;
|
|
257
|
+
/** Documents per drain cycle. */
|
|
258
|
+
batch?: number;
|
|
259
|
+
/**
|
|
260
|
+
* Flush once this many writes have queued since the last cycle, whatever the
|
|
261
|
+
* policy says. Default `100`; `false` disables it.
|
|
262
|
+
*
|
|
263
|
+
* The timer is the fallback, not the mechanism. Under a long `interval` a
|
|
264
|
+
* burst would otherwise sit unsent for the whole window, and the queue would
|
|
265
|
+
* grow without bound between ticks.
|
|
266
|
+
*/
|
|
267
|
+
flushAt?: number | false;
|
|
268
|
+
/**
|
|
269
|
+
* Flush when the tab is hidden or closing. Default `true`.
|
|
270
|
+
*
|
|
271
|
+
* This is the moment a queue most needs draining and the one the timer is
|
|
272
|
+
* least likely to be about to cover. `visibilitychange` does the real work —
|
|
273
|
+
* the page is still alive, so requests complete normally. `pagehide` is a
|
|
274
|
+
* last-ditch attempt sent with `keepalive`, which browsers cap at 64 KB
|
|
275
|
+
* across all in-flight requests, so it is best-effort by construction and the
|
|
276
|
+
* queue stays correct without it.
|
|
277
|
+
*/
|
|
278
|
+
flushOnHide?: boolean;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Identifies a query *shape*, not a storage location.
|
|
282
|
+
*
|
|
283
|
+
* A collection name says where documents live; a key says what went stale.
|
|
284
|
+
* Collapsing the two makes TTL meaningless and server-side removals invisible,
|
|
285
|
+
* so they stay separate: documents land in `collection`, freshness is tracked
|
|
286
|
+
* per `key`.
|
|
287
|
+
*/
|
|
288
|
+
type QueryKey = readonly unknown[];
|
|
289
|
+
/**
|
|
290
|
+
* The constraint on a user's model type.
|
|
291
|
+
*
|
|
292
|
+
* Deliberately **not** `Document`. `Document` carries an index signature, and
|
|
293
|
+
* TypeScript does not give interfaces implicit index signatures, so an ordinary
|
|
294
|
+
* application model —
|
|
295
|
+
*
|
|
296
|
+
* ```ts
|
|
297
|
+
* interface Todo { _id: string; title: string }
|
|
298
|
+
* ```
|
|
299
|
+
*
|
|
300
|
+
* — fails `T extends Document` outright. Requiring every model in a migrating
|
|
301
|
+
* codebase to be restated as a type alias or to `extends Document` is a far
|
|
302
|
+
* larger diff than the import line this surface exists to keep small. The
|
|
303
|
+
* narrowing to `Document` happens internally, at the collection boundary, where
|
|
304
|
+
* the registered schema validates at runtime anyway.
|
|
305
|
+
*/
|
|
306
|
+
type Doc = {
|
|
307
|
+
_id?: string;
|
|
308
|
+
};
|
|
309
|
+
/** `false` never, `true` when stale, `'always'` regardless of freshness. */
|
|
310
|
+
type RefetchTrigger = boolean | 'always';
|
|
311
|
+
/** `false` never, `true` forever, a number of attempts, or a per-failure decision. */
|
|
312
|
+
type RetryOption = boolean | number | ((failureCount: number, error: Error) => boolean);
|
|
313
|
+
/** A fixed delay, or one computed from the zero-based attempt index. */
|
|
314
|
+
type RetryDelayOption = number | ((attemptIndex: number, error: Error) => number);
|
|
315
|
+
/** How connectivity gates fetching. */
|
|
316
|
+
type NetworkMode = 'online' | 'always' | 'offlineFirst';
|
|
317
|
+
/** What `queryFn` is handed. Mirrors TanStack's `QueryFunctionContext`. */
|
|
318
|
+
interface QueryFunctionContext {
|
|
319
|
+
queryKey: QueryKey;
|
|
320
|
+
signal: AbortSignal;
|
|
321
|
+
meta?: Record<string, unknown>;
|
|
322
|
+
/**
|
|
323
|
+
* The bound request parameters, if `params` was given.
|
|
324
|
+
*
|
|
325
|
+
* Always in bound form, even when a plain object was passed, so
|
|
326
|
+
* `params.toSearchParams()` works either way. The library never builds a URL
|
|
327
|
+
* itself — it only reads the half of the declaration that concerns the
|
|
328
|
+
* collection.
|
|
329
|
+
*/
|
|
330
|
+
params?: BoundParams;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Options shared by every `queryFn` shape.
|
|
334
|
+
*
|
|
335
|
+
* `TFetched` is what `queryFn` returns; `TData` is what `data` ends up as once
|
|
336
|
+
* `assemble` and `select` have run. They differ whenever a transform is in play.
|
|
337
|
+
*/
|
|
338
|
+
interface BaseQueryOptions<TFetched = unknown, TData = unknown> {
|
|
339
|
+
/**
|
|
340
|
+
* Where fetched documents are stored.
|
|
341
|
+
*
|
|
342
|
+
* Optional: defaults to `queryKey[0]` when that is a string, which is what
|
|
343
|
+
* makes a migrated `queryKey: ['todos', …]` need no edit. Inference is
|
|
344
|
+
* validated against the provider's collection registry rather than trusted —
|
|
345
|
+
* this option decides the *physical destination* of every fetched document,
|
|
346
|
+
* so a wrong value pours server data into a collection the app reads for
|
|
347
|
+
* something else.
|
|
348
|
+
*/
|
|
349
|
+
collection?: string;
|
|
350
|
+
queryKey: QueryKey;
|
|
351
|
+
/**
|
|
352
|
+
* Freshness window in ms. Defaults to **one hour**, not TanStack's `0`.
|
|
353
|
+
*
|
|
354
|
+
* A memory cache that dies on reload is usually seconds old and revalidating
|
|
355
|
+
* it is nearly free; a cache that survives reload and is expected to work
|
|
356
|
+
* offline is neither. Pass `0` for TanStack's behaviour, or set it once on
|
|
357
|
+
* the provider.
|
|
358
|
+
*/
|
|
359
|
+
staleTime?: number;
|
|
360
|
+
/**
|
|
361
|
+
* Default `true`. A function is re-evaluated on every render, which is how a
|
|
362
|
+
* dependent query waits for the value it needs.
|
|
363
|
+
*
|
|
364
|
+
* Note it takes no arguments, where TanStack passes the query — exposing our
|
|
365
|
+
* internals to make one signature match would be a poor trade.
|
|
366
|
+
*/
|
|
367
|
+
enabled?: boolean | (() => boolean);
|
|
368
|
+
/** Opaque; passed through to `queryFn`. */
|
|
369
|
+
meta?: Record<string, unknown>;
|
|
370
|
+
/**
|
|
371
|
+
* Data to start from on a cold key, instead of fetching into a spinner.
|
|
372
|
+
*
|
|
373
|
+
* Unlike {@link BaseQueryOptions.placeholderData} this is **persisted**: the
|
|
374
|
+
* documents are hydrated into the collection and a query record is written,
|
|
375
|
+
* exactly as a fetch would. It is server data the app happened to already
|
|
376
|
+
* have — from SSR, a route loader, a bundled seed — not a stand-in for it.
|
|
377
|
+
*/
|
|
378
|
+
initialData?: NoInfer<TFetched> | (() => NoInfer<TFetched>);
|
|
379
|
+
/**
|
|
380
|
+
* When {@link BaseQueryOptions.initialData} was last true, as epoch ms.
|
|
381
|
+
* Defaults to now.
|
|
382
|
+
*
|
|
383
|
+
* Feeds staleness, so seeded data can be born stale and revalidate straight
|
|
384
|
+
* away — which is usually what an SSR payload wants.
|
|
385
|
+
*/
|
|
386
|
+
initialDataUpdatedAt?: number;
|
|
387
|
+
/**
|
|
388
|
+
* Data to show *instead of* a pending state, never written anywhere.
|
|
389
|
+
*
|
|
390
|
+
* `placeholderData: keepPreviousData` is the common case: hold the last page
|
|
391
|
+
* on screen while the next one loads rather than blanking the list. The
|
|
392
|
+
* result reports `isPlaceholderData: true` so the UI can dim it.
|
|
393
|
+
*
|
|
394
|
+
* This is in the *final* `data` space — after `assemble` and `select` — which
|
|
395
|
+
* is what lets `keepPreviousData` hand back the previous `data` untouched.
|
|
396
|
+
* TanStack places it before `select`; the difference shows only for a
|
|
397
|
+
* placeholder that is not the previous value.
|
|
398
|
+
*/
|
|
399
|
+
placeholderData?: NoInfer<TData> | ((previous: NoInfer<TData> | undefined) => NoInfer<TData> | undefined);
|
|
400
|
+
/**
|
|
401
|
+
* How many times to retry a failed fetch. Default `3`.
|
|
402
|
+
*
|
|
403
|
+
* `false` disables retrying, `true` retries forever, a number caps the
|
|
404
|
+
* attempts, and a predicate decides per failure — which is how a 404 is kept
|
|
405
|
+
* from being retried four times.
|
|
406
|
+
*/
|
|
407
|
+
retry?: RetryOption;
|
|
408
|
+
/** Milliseconds between attempts. Default is exponential from 1s, capped at 30s. */
|
|
409
|
+
retryDelay?: RetryDelayOption;
|
|
410
|
+
/**
|
|
411
|
+
* Revalidate on mount when the data is stale. Default `true`.
|
|
412
|
+
*
|
|
413
|
+
* `'always'` revalidates whatever `staleTime` says; `false` leaves a mounted
|
|
414
|
+
* query showing whatever the device has until something else asks.
|
|
415
|
+
*/
|
|
416
|
+
refetchOnMount?: RefetchTrigger;
|
|
417
|
+
/** Revalidate when the window regains focus, if stale. Default `true`. */
|
|
418
|
+
refetchOnWindowFocus?: RefetchTrigger;
|
|
419
|
+
/** Revalidate when the network comes back, if stale. Default `true`. */
|
|
420
|
+
refetchOnReconnect?: RefetchTrigger;
|
|
421
|
+
/** Poll every N ms. Default `false`. A function is re-evaluated per render. */
|
|
422
|
+
refetchInterval?: number | false | (() => number | false);
|
|
423
|
+
/** Keep polling while the tab is hidden. Default `false`. */
|
|
424
|
+
refetchIntervalInBackground?: boolean;
|
|
425
|
+
/**
|
|
426
|
+
* What to do about fetching while offline. Default `'online'`.
|
|
427
|
+
*
|
|
428
|
+
* `'online'` reports `fetchStatus: 'paused'` instead of failing, and resumes
|
|
429
|
+
* on reconnect. `'always'` ignores connectivity — the right setting when
|
|
430
|
+
* `queryFn` talks to something local. `'offlineFirst'` attempts once, which
|
|
431
|
+
* suits a request that may be answered by a service worker.
|
|
432
|
+
*/
|
|
433
|
+
networkMode?: NetworkMode;
|
|
434
|
+
/**
|
|
435
|
+
* How long to remember this query's result set after nothing is using it.
|
|
436
|
+
* Default `Infinity` — remember indefinitely.
|
|
437
|
+
*
|
|
438
|
+
* **Not garbage collection, and deliberately not called that.** Documents are
|
|
439
|
+
* never deleted: they live in your own collections, which the application
|
|
440
|
+
* also reads directly, so sweeping them would destroy real user data rather
|
|
441
|
+
* than reclaim a cache. What expires is the *record* of which documents this
|
|
442
|
+
* query returned — a few hundred bytes — after which the next mount is cold
|
|
443
|
+
* and refetches to re-establish membership.
|
|
444
|
+
*
|
|
445
|
+
* The default is `Infinity` because a query record is what makes a warm start
|
|
446
|
+
* warm, and a cache that survives reload has no memory pressure to relieve.
|
|
447
|
+
* Set it when a result set is genuinely disposable — a search whose terms
|
|
448
|
+
* will never be typed again.
|
|
449
|
+
*/
|
|
450
|
+
forgetAfter?: number;
|
|
451
|
+
/**
|
|
452
|
+
* Rethrow a fetch error during render, for an error boundary to catch.
|
|
453
|
+
* Default `false`.
|
|
454
|
+
*/
|
|
455
|
+
throwOnError?: boolean | ((error: Error) => boolean);
|
|
456
|
+
/**
|
|
457
|
+
* Serialise a query key into the string that identifies its record.
|
|
458
|
+
*
|
|
459
|
+
* The default sorts plain-object properties and preserves `undefined`, so
|
|
460
|
+
* `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` are one query rather than two.
|
|
461
|
+
* Replace it only if you need keys this cannot express — the result becomes
|
|
462
|
+
* part of the record's identity on disk.
|
|
463
|
+
*/
|
|
464
|
+
queryKeyHashFn?: (queryKey: QueryKey) => string;
|
|
465
|
+
/**
|
|
466
|
+
* Request parameters — `?category=book&page=1`.
|
|
467
|
+
*
|
|
468
|
+
* Appended to `queryKey`, handed to `queryFn`, and (for parameters declared
|
|
469
|
+
* with `defineParams`) translated into a local filter. A plain object gets
|
|
470
|
+
* the first two; declaring the parameters adds the third.
|
|
471
|
+
*/
|
|
472
|
+
params?: BoundParams | Record<string, ParamValue>;
|
|
473
|
+
/**
|
|
474
|
+
* What to answer with when the key is cold and the fetch fails.
|
|
475
|
+
*
|
|
476
|
+
* `'strict'` (default) keeps the guarantee that `data` is exactly what the
|
|
477
|
+
* server returned: no record, no answer. `'collection'` falls back to
|
|
478
|
+
* querying everything cached through the `params` filter and reports
|
|
479
|
+
* `fromCollection: true`, so the UI can say so rather than presenting a
|
|
480
|
+
* partial set as authoritative.
|
|
481
|
+
*
|
|
482
|
+
* Only meaningful with parameters declared via `defineParams` — there is
|
|
483
|
+
* otherwise no predicate to query the collection with.
|
|
484
|
+
*/
|
|
485
|
+
offline?: 'strict' | 'collection';
|
|
486
|
+
/**
|
|
487
|
+
* Accepted and ignored — every result field is computed either way. Present
|
|
488
|
+
* so migrated code compiles; noted once in development.
|
|
489
|
+
*/
|
|
490
|
+
notifyOnChangeProps?: readonly string[] | 'all' | (() => readonly string[] | 'all' | undefined);
|
|
491
|
+
/**
|
|
492
|
+
* Accepted and ignored — structural sharing is always on here. Present so
|
|
493
|
+
* migrated code compiles; noted once in development.
|
|
494
|
+
*/
|
|
495
|
+
structuralSharing?: boolean | ((previous: unknown, next: unknown) => unknown);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* `queryFn` returns a list of documents. The common case.
|
|
499
|
+
*
|
|
500
|
+
* `TData` is inferred from `select` when it is present, and is `T[]` otherwise.
|
|
501
|
+
*/
|
|
502
|
+
interface UseQueryOptions<T extends Doc, TData = T[]> extends BaseQueryOptions<T[], TData> {
|
|
503
|
+
/** Fetch the current server state for this key. Honour `signal`. */
|
|
504
|
+
queryFn: (context: QueryFunctionContext) => Promise<T[]>;
|
|
505
|
+
/**
|
|
506
|
+
* Transform what this component sees, without changing what is stored.
|
|
507
|
+
*
|
|
508
|
+
* Runs over the live documents on every render in which they change, and its
|
|
509
|
+
* output gets the same structural sharing they do — so an inline arrow here
|
|
510
|
+
* is safe, and `data` keeps a stable identity between equal results.
|
|
511
|
+
*/
|
|
512
|
+
select?: (data: T[]) => TData;
|
|
513
|
+
/**
|
|
514
|
+
* Narrow this query's result set locally, in the engine.
|
|
515
|
+
*
|
|
516
|
+
* Composed with the query's stored id list as `$and`, so it filters *within*
|
|
517
|
+
* what the server returned rather than replacing it. Indexed, and live: a
|
|
518
|
+
* document edited out of the predicate leaves the view immediately, without
|
|
519
|
+
* waiting for a refetch.
|
|
520
|
+
*
|
|
521
|
+
* Deliberately **not** part of the query key. That is the whole point — one
|
|
522
|
+
* fetch, many narrowings, so All / Active / Done can be three components over
|
|
523
|
+
* a single request. Folding it into the key would give each its own record
|
|
524
|
+
* and its own fetch, which is worse than not having the option.
|
|
525
|
+
*
|
|
526
|
+
* It also never touches the stored record: the id list stays exactly what the
|
|
527
|
+
* server returned. Otherwise a refetch would fight the filter, and a
|
|
528
|
+
* locally-excluded document would be indistinguishable from one the server
|
|
529
|
+
* deleted.
|
|
530
|
+
*/
|
|
531
|
+
where?: Filter<T & Document>;
|
|
532
|
+
documents?: never;
|
|
533
|
+
assemble?: never;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* `queryFn` returns one document — a detail view.
|
|
537
|
+
*
|
|
538
|
+
* `data` is that document, not a one-element array, and becomes `undefined` if
|
|
539
|
+
* the document is deleted locally while the query is on screen. `select` is
|
|
540
|
+
* only called when there is a document, so it never has to handle the gap.
|
|
541
|
+
*/
|
|
542
|
+
interface UseDocumentQueryOptions<T extends Doc, TData = T> extends BaseQueryOptions<T, TData | undefined> {
|
|
543
|
+
queryFn: (context: QueryFunctionContext) => Promise<T>;
|
|
544
|
+
select?: (data: T) => TData;
|
|
545
|
+
/**
|
|
546
|
+
* Narrow this query's result set locally, in the engine.
|
|
547
|
+
*
|
|
548
|
+
* Composed with the query's stored id list as `$and`, so it filters *within*
|
|
549
|
+
* what the server returned rather than replacing it. Indexed, and live: a
|
|
550
|
+
* document edited out of the predicate leaves the view immediately, without
|
|
551
|
+
* waiting for a refetch.
|
|
552
|
+
*
|
|
553
|
+
* Deliberately **not** part of the query key. That is the whole point — one
|
|
554
|
+
* fetch, many narrowings, so All / Active / Done can be three components over
|
|
555
|
+
* a single request. Folding it into the key would give each its own record
|
|
556
|
+
* and its own fetch, which is worse than not having the option.
|
|
557
|
+
*
|
|
558
|
+
* It also never touches the stored record: the id list stays exactly what the
|
|
559
|
+
* server returned. Otherwise a refetch would fight the filter, and a
|
|
560
|
+
* locally-excluded document would be indistinguishable from one the server
|
|
561
|
+
* deleted.
|
|
562
|
+
*/
|
|
563
|
+
where?: Filter<T & Document>;
|
|
564
|
+
documents?: never;
|
|
565
|
+
assemble?: never;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* `queryFn` returns an envelope — `{ items, total, nextCursor }` and the like.
|
|
569
|
+
*
|
|
570
|
+
* This is the one shape no rename can absorb: `data` is rebuilt from the local
|
|
571
|
+
* collection, so the response has to say which part of itself is documents.
|
|
572
|
+
* `documents` extracts them; `assemble` puts the result back together, and is
|
|
573
|
+
* what keeps liveness — `data.items` re-renders on a local write while
|
|
574
|
+
* `data.total` stays whatever the server said.
|
|
575
|
+
*/
|
|
576
|
+
interface UseEnvelopeQueryOptions<TRaw, TDoc extends Doc, TAssembled = TDoc[], TData = TAssembled> extends BaseQueryOptions<TRaw, TData> {
|
|
577
|
+
queryFn: (context: QueryFunctionContext) => Promise<TRaw>;
|
|
578
|
+
documents: (raw: TRaw) => TDoc[];
|
|
579
|
+
/** Defaults to returning the documents alone. */
|
|
580
|
+
assemble?: (documents: TDoc[], raw: TRaw) => TAssembled;
|
|
581
|
+
/**
|
|
582
|
+
* Narrow this query's result set locally, in the engine.
|
|
583
|
+
*
|
|
584
|
+
* Composed with the query's stored id list as `$and`, so it filters *within*
|
|
585
|
+
* what the server returned rather than replacing it. Indexed, and live: a
|
|
586
|
+
* document edited out of the predicate leaves the view immediately, without
|
|
587
|
+
* waiting for a refetch.
|
|
588
|
+
*
|
|
589
|
+
* Deliberately **not** part of the query key. That is the whole point — one
|
|
590
|
+
* fetch, many narrowings, so All / Active / Done can be three components over
|
|
591
|
+
* a single request. Folding it into the key would give each its own record
|
|
592
|
+
* and its own fetch, which is worse than not having the option.
|
|
593
|
+
*
|
|
594
|
+
* It also never touches the stored record: the id list stays exactly what the
|
|
595
|
+
* server returned. Otherwise a refetch would fight the filter, and a
|
|
596
|
+
* locally-excluded document would be indistinguishable from one the server
|
|
597
|
+
* deleted.
|
|
598
|
+
*/
|
|
599
|
+
where?: Filter<TDoc & Document>;
|
|
600
|
+
select?: (data: TAssembled) => TData;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Provider-wide defaults, in TanStack's `new QueryClient({ defaultOptions })`
|
|
604
|
+
* shape so that config migrates as a copy-paste.
|
|
605
|
+
*
|
|
606
|
+
* This is also where `staleTime: 0` goes to restore TanStack's revalidate-on-
|
|
607
|
+
* every-mount behaviour in one line, and where a test harness sets
|
|
608
|
+
* `retry: false` so failing requests fail immediately.
|
|
609
|
+
*/
|
|
610
|
+
interface QueryDefaults {
|
|
611
|
+
queries?: Pick<BaseQueryOptions, 'staleTime' | 'retry' | 'retryDelay' | 'refetchOnMount' | 'refetchOnWindowFocus' | 'refetchOnReconnect' | 'refetchInterval' | 'refetchIntervalInBackground' | 'networkMode' | 'forgetAfter' | 'throwOnError'>;
|
|
612
|
+
}
|
|
613
|
+
/** Any of the three forms, as the implementation sees them. */
|
|
614
|
+
type AnyQueryOptions = BaseQueryOptions<unknown, unknown> & {
|
|
615
|
+
queryFn: (context: QueryFunctionContext) => Promise<unknown>;
|
|
616
|
+
documents?: (raw: never) => Doc[];
|
|
617
|
+
assemble?: (documents: never, raw: never) => unknown;
|
|
618
|
+
select?: (data: never) => unknown;
|
|
619
|
+
where?: Filter<Document>;
|
|
620
|
+
};
|
|
621
|
+
/** How a `queryFn`'s return value maps onto documents. Stored on the record. */
|
|
622
|
+
type ResultShape = 'array' | 'document' | 'envelope';
|
|
623
|
+
/** Fields present on a query result whatever its status. */
|
|
624
|
+
interface QueryResultCommon<TData> {
|
|
625
|
+
/** `'fetching'` while a request is in flight, `'paused'` when offline. */
|
|
626
|
+
fetchStatus: 'fetching' | 'paused' | 'idle';
|
|
627
|
+
/** `isPending && isFetching` — a first load with nothing to show yet. */
|
|
628
|
+
isLoading: boolean;
|
|
629
|
+
isFetching: boolean;
|
|
630
|
+
isPaused: boolean;
|
|
631
|
+
/** Fetching over data that is already on screen. */
|
|
632
|
+
isRefetching: boolean;
|
|
633
|
+
/**
|
|
634
|
+
* Past `staleTime`.
|
|
635
|
+
*
|
|
636
|
+
* Note this is *not* "currently revalidating" — that is `isFetching`. The two
|
|
637
|
+
* were one flag before parity work and the meanings are opposite.
|
|
638
|
+
*/
|
|
639
|
+
isStale: boolean;
|
|
640
|
+
isFetched: boolean;
|
|
641
|
+
isFetchedAfterMount: boolean;
|
|
642
|
+
isPlaceholderData: boolean;
|
|
643
|
+
/**
|
|
644
|
+
* `data` was answered from the whole collection rather than from this
|
|
645
|
+
* query's stored result set — see `offline: 'collection'`. Always `false`
|
|
646
|
+
* under the default `'strict'`.
|
|
647
|
+
*/
|
|
648
|
+
fromCollection: boolean;
|
|
649
|
+
dataUpdatedAt: number;
|
|
650
|
+
errorUpdatedAt: number;
|
|
651
|
+
failureCount: number;
|
|
652
|
+
failureReason: Error | null;
|
|
653
|
+
errorUpdateCount: number;
|
|
654
|
+
refetch: (options?: {
|
|
655
|
+
/** Reject rather than recording the failure on the result. */
|
|
656
|
+
throwOnError?: boolean;
|
|
657
|
+
/**
|
|
658
|
+
* Default `true`: abandon any request already on its way and start again.
|
|
659
|
+
* `false` joins the in-flight request instead.
|
|
660
|
+
*/
|
|
661
|
+
cancelRefetch?: boolean;
|
|
662
|
+
}) => Promise<QueryResult<TData>>;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* The result of `useQuery`.
|
|
666
|
+
*
|
|
667
|
+
* A discriminated union rather than one object with `data: T[] | undefined`,
|
|
668
|
+
* because the canonical React Query component shape depends on narrowing:
|
|
669
|
+
*
|
|
670
|
+
* ```tsx
|
|
671
|
+
* if (isPending) return <Spinner/>
|
|
672
|
+
* if (isError) return <p>{error.message}</p>
|
|
673
|
+
* return <ul>{data.map(…)}</ul> // `data` is T[] here, with no `!` or `?.`
|
|
674
|
+
* ```
|
|
675
|
+
*
|
|
676
|
+
* `data` is resolved through the local database rather than from the fetch
|
|
677
|
+
* response, so a local write to any document in the set re-renders this query
|
|
678
|
+
* immediately — there is no cache to invalidate, because the collection *is*
|
|
679
|
+
* the state.
|
|
680
|
+
*/
|
|
681
|
+
type QueryResult<TData> = (QueryResultCommon<TData> & {
|
|
682
|
+
status: 'pending';
|
|
683
|
+
/** Undefined, never `[]` — an empty array would render as an empty list. */
|
|
684
|
+
data: undefined;
|
|
685
|
+
error: null;
|
|
686
|
+
isPending: true;
|
|
687
|
+
isSuccess: false;
|
|
688
|
+
isError: false;
|
|
689
|
+
isLoadingError: false;
|
|
690
|
+
isRefetchError: false;
|
|
691
|
+
}) | (QueryResultCommon<TData> & {
|
|
692
|
+
status: 'success';
|
|
693
|
+
data: TData;
|
|
694
|
+
error: null;
|
|
695
|
+
isPending: false;
|
|
696
|
+
isSuccess: true;
|
|
697
|
+
isError: false;
|
|
698
|
+
isLoadingError: false;
|
|
699
|
+
isRefetchError: false;
|
|
700
|
+
}) | (QueryResultCommon<TData> & {
|
|
701
|
+
status: 'error';
|
|
702
|
+
/** Nothing was cached, so the failure left nothing to render. */
|
|
703
|
+
data: undefined;
|
|
704
|
+
error: Error;
|
|
705
|
+
isPending: false;
|
|
706
|
+
isSuccess: false;
|
|
707
|
+
isError: true;
|
|
708
|
+
isLoadingError: true;
|
|
709
|
+
isRefetchError: false;
|
|
710
|
+
}) | (QueryResultCommon<TData> & {
|
|
711
|
+
status: 'error';
|
|
712
|
+
/**
|
|
713
|
+
* A *refetch* failed over data the device already holds. Clearing the
|
|
714
|
+
* screen here would undo the point of a persistent cache.
|
|
715
|
+
*/
|
|
716
|
+
data: TData;
|
|
717
|
+
error: Error;
|
|
718
|
+
isPending: false;
|
|
719
|
+
isSuccess: false;
|
|
720
|
+
isError: true;
|
|
721
|
+
isLoadingError: false;
|
|
722
|
+
isRefetchError: true;
|
|
723
|
+
});
|
|
724
|
+
/**
|
|
725
|
+
* When the network happens, relative to the local write.
|
|
726
|
+
*
|
|
727
|
+
* - `optimistic` (default) — commit locally, return, and send in the
|
|
728
|
+
* background as soon as possible. `pending` stays false: the UI never waits.
|
|
729
|
+
* - `immediate` — send first and write locally only once the server agrees.
|
|
730
|
+
* `pending` reflects the request. For writes the user would be misled by if
|
|
731
|
+
* they silently failed: checkout, payment, anything irreversible.
|
|
732
|
+
* - `queued` — commit locally and wait for the provider's schedule rather than
|
|
733
|
+
* sending straight away. For high-volume writes worth batching.
|
|
734
|
+
*/
|
|
735
|
+
type MutationMode = 'optimistic' | 'immediate' | 'queued';
|
|
736
|
+
/** Which write a mutation performs. Declared once, not passed per call. */
|
|
737
|
+
type MutationOperation = 'insert' | 'update' | 'delete';
|
|
738
|
+
/**
|
|
739
|
+
* What `mutate` takes, per operation.
|
|
740
|
+
*
|
|
741
|
+
* These differ, and defaulting all three to `T` would be wrong in two of them:
|
|
742
|
+
* an insert has no `_id` yet (it is minted client-side), and an update carries
|
|
743
|
+
* only the fields being changed. Getting this wrong shows up as a compile error
|
|
744
|
+
* on every call site, which is how it was found.
|
|
745
|
+
*/
|
|
746
|
+
type InsertVariables<T extends Doc> = Omit<T, '_id'> & {
|
|
747
|
+
_id?: string;
|
|
748
|
+
};
|
|
749
|
+
type UpdateVariables<T extends Doc> = {
|
|
750
|
+
_id: string;
|
|
751
|
+
} & Partial<Omit<T, '_id'>>;
|
|
752
|
+
type DeleteVariables = {
|
|
753
|
+
_id: string;
|
|
754
|
+
};
|
|
755
|
+
/** Passed between a mutation's callbacks, as TanStack's `context` is. */
|
|
756
|
+
type MutationContext = unknown;
|
|
757
|
+
interface MutationCallbacks<T extends Doc, TVariables, TContext = MutationContext> {
|
|
758
|
+
/**
|
|
759
|
+
* Runs before the write. Its return value becomes `context` for the
|
|
760
|
+
* callbacks below.
|
|
761
|
+
*
|
|
762
|
+
* In TanStack this is where an optimistic update is applied by hand and a
|
|
763
|
+
* rollback snapshot returned. Neither is needed here — the optimism *is* the
|
|
764
|
+
* database, and every query over these documents re-renders on the local
|
|
765
|
+
* commit — so this is a plain hook point for logging and deriving context.
|
|
766
|
+
*/
|
|
767
|
+
onMutate?: (variables: TVariables) => TContext | Promise<TContext>;
|
|
768
|
+
/**
|
|
769
|
+
* The write succeeded.
|
|
770
|
+
*
|
|
771
|
+
* Under `optimistic` and `queued` that means the **local commit**: the write
|
|
772
|
+
* is durable and the UI has already updated, but nothing has reached the
|
|
773
|
+
* server. Under `immediate` it means the server agreed.
|
|
774
|
+
*
|
|
775
|
+
* Waiting for the server in every mode would be more faithful to TanStack and
|
|
776
|
+
* much worse in practice — the drain often lands after this component is
|
|
777
|
+
* gone, so the callback would silently never fire. Use `onSynced` for server
|
|
778
|
+
* confirmation, and `useSyncStatus` for anything that must survive
|
|
779
|
+
* navigation.
|
|
780
|
+
*/
|
|
781
|
+
onSuccess?: (data: T, variables: TVariables, context: TContext) => unknown;
|
|
782
|
+
onError?: (error: Error, variables: TVariables, context: TContext | undefined) => unknown;
|
|
783
|
+
onSettled?: (data: T | undefined, error: Error | null, variables: TVariables, context: TContext | undefined) => unknown;
|
|
784
|
+
/**
|
|
785
|
+
* The **server** has confirmed this write.
|
|
786
|
+
*
|
|
787
|
+
* Best-effort by nature: under `optimistic` and `queued` the drain may land
|
|
788
|
+
* after this component has gone, in another tab, or on a later run of the
|
|
789
|
+
* app, and then nothing here fires. Anything that must not be missed — a
|
|
790
|
+
* failed write needing a human decision — belongs in `useSyncStatus`, which
|
|
791
|
+
* survives navigation.
|
|
792
|
+
*
|
|
793
|
+
* Under `immediate` it fires immediately after `onSuccess`, because the two
|
|
794
|
+
* mean the same thing in that mode.
|
|
795
|
+
*/
|
|
796
|
+
onSynced?: (data: T, variables: TVariables) => unknown;
|
|
797
|
+
}
|
|
798
|
+
interface UseMutationOptions<T extends Doc = Doc, TVariables = InsertVariables<T>, TContext = MutationContext> extends MutationCallbacks<T, TVariables, TContext> {
|
|
799
|
+
collection: string;
|
|
800
|
+
/**
|
|
801
|
+
* Which write this hook performs. Default `'insert'`.
|
|
802
|
+
*
|
|
803
|
+
* Deliberately **not** inferred from the presence of `_id`: a caller who
|
|
804
|
+
* passes an id on an insert would silently get an update instead.
|
|
805
|
+
*/
|
|
806
|
+
operation?: MutationOperation;
|
|
807
|
+
/**
|
|
808
|
+
* URL template for this collection's writes — `/api/v2/todos/:id`.
|
|
809
|
+
*
|
|
810
|
+
* `:id` and `:collection` interpolate; an insert drops a trailing `/:id`.
|
|
811
|
+
* A template rather than a function because a queued write is sent long
|
|
812
|
+
* after its component is gone, so the route has to be storable.
|
|
813
|
+
*
|
|
814
|
+
* Omitted falls back to the provider's backend.
|
|
815
|
+
*/
|
|
816
|
+
url?: string;
|
|
817
|
+
/** Verb overrides. Defaults to POST / PUT / DELETE. */
|
|
818
|
+
method?: Partial<Record<SyncOp, string>>;
|
|
819
|
+
/** Default `'optimistic'`. */
|
|
820
|
+
mode?: MutationMode;
|
|
821
|
+
/**
|
|
822
|
+
* Perform the request yourself. **`mode: 'immediate'` only.**
|
|
823
|
+
*
|
|
824
|
+
* Under `optimistic` and `queued` the request is sent by the drain loop long
|
|
825
|
+
* after this component is gone — after a route change, a reload, a week. A
|
|
826
|
+
* closure cannot be stored, so there would be nothing left to call; accepting
|
|
827
|
+
* one there would mean silently dropping the write on reload, which is worse
|
|
828
|
+
* than refusing it. Those modes route by `url` instead, which is data.
|
|
829
|
+
*
|
|
830
|
+
* Given one, `url`, `method` and the provider's `classify` do not apply: the
|
|
831
|
+
* function owns the request. Its resolved value is stored as the document.
|
|
832
|
+
*/
|
|
833
|
+
mutationFn?: (variables: TVariables) => Promise<T>;
|
|
834
|
+
/** Opaque; carried for instrumentation. */
|
|
835
|
+
meta?: Record<string, unknown>;
|
|
836
|
+
/**
|
|
837
|
+
* How many times to retry a failed write. Default `0`.
|
|
838
|
+
*
|
|
839
|
+
* **Not** the queries' default of 3, and deliberately so: a read is
|
|
840
|
+
* idempotent and a write may not be. Retrying a `POST` that timed out after
|
|
841
|
+
* the server accepted it is how duplicate rows appear. Turn it on only where
|
|
842
|
+
* the endpoint is genuinely idempotent — which contract rule 2 asks for, but
|
|
843
|
+
* the default should not assume.
|
|
844
|
+
*
|
|
845
|
+
* Applies to `immediate` only. Queued writes have their own durable backoff
|
|
846
|
+
* in the drain, which survives reload; this one does not.
|
|
847
|
+
*/
|
|
848
|
+
retry?: RetryOption;
|
|
849
|
+
/** Milliseconds between attempts. Default is exponential from 1s, capped at 30s. */
|
|
850
|
+
retryDelay?: RetryDelayOption;
|
|
851
|
+
/**
|
|
852
|
+
* What to do about writing while offline. Default `'online'`.
|
|
853
|
+
*
|
|
854
|
+
* Under `immediate`, `'online'` **pauses** rather than failing — the mutation
|
|
855
|
+
* stays pending, reports `isPaused`, and goes as soon as the connection
|
|
856
|
+
* returns. `'always'` attempts regardless.
|
|
857
|
+
*
|
|
858
|
+
* `optimistic` and `queued` ignore this: they commit locally and the drain
|
|
859
|
+
* owns connectivity from there.
|
|
860
|
+
*/
|
|
861
|
+
networkMode?: NetworkMode;
|
|
862
|
+
/** Rethrow a write error during render, for an error boundary to catch. */
|
|
863
|
+
throwOnError?: boolean | ((error: Error) => boolean);
|
|
864
|
+
/**
|
|
865
|
+
* Run mutations sharing an id one at a time, in call order.
|
|
866
|
+
*
|
|
867
|
+
* Without it, two rapid submits race and the later response may land first.
|
|
868
|
+
* Note this is a *scheduling* guarantee, distinct from the layer's rule that
|
|
869
|
+
* a document has at most one pending operation — that one coalesces edits,
|
|
870
|
+
* this one orders requests.
|
|
871
|
+
*/
|
|
872
|
+
scope?: {
|
|
873
|
+
id: string;
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
/** Fields present on a mutation result whatever its status. */
|
|
877
|
+
interface MutationResultCommon<T extends Doc, TVariables> {
|
|
878
|
+
/** Fire-and-forget. Errors surface on `error`, never thrown to render. */
|
|
879
|
+
mutate: (variables: TVariables, options?: MutationCallbacks<T, TVariables>) => void;
|
|
880
|
+
/** Awaitable. Under `optimistic`, resolves on the *local* commit. */
|
|
881
|
+
mutateAsync: (variables: TVariables, options?: MutationCallbacks<T, TVariables>) => Promise<T>;
|
|
882
|
+
/** Back to `idle`, forgetting the last result. */
|
|
883
|
+
reset: () => void;
|
|
884
|
+
/** Offline, so an `immediate` write has not been attempted. */
|
|
885
|
+
isPaused: boolean;
|
|
886
|
+
failureCount: number;
|
|
887
|
+
failureReason: Error | null;
|
|
888
|
+
/** Epoch ms of the last `mutate` call, or `0`. */
|
|
889
|
+
submittedAt: number;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* The result of `useMutation`.
|
|
893
|
+
*
|
|
894
|
+
* A discriminated union on `status`, so `if (isSuccess)` narrows `data` and
|
|
895
|
+
* `if (isError)` narrows `error` — the same treatment `QueryResult` gets, for
|
|
896
|
+
* the same reason.
|
|
897
|
+
*
|
|
898
|
+
* Note `idle`, which has no `useQuery` equivalent: a mutation that has never
|
|
899
|
+
* run has not failed and is not loading, and migrated code branches on it to
|
|
900
|
+
* render a clean form.
|
|
901
|
+
*/
|
|
902
|
+
type MutationResult<T extends Doc = Doc, TVariables = InsertVariables<T>> = (MutationResultCommon<T, TVariables> & {
|
|
903
|
+
status: 'idle';
|
|
904
|
+
data: undefined;
|
|
905
|
+
error: null;
|
|
906
|
+
variables: undefined;
|
|
907
|
+
isIdle: true;
|
|
908
|
+
isPending: false;
|
|
909
|
+
isSuccess: false;
|
|
910
|
+
isError: false;
|
|
911
|
+
}) | (MutationResultCommon<T, TVariables> & {
|
|
912
|
+
status: 'pending';
|
|
913
|
+
data: undefined;
|
|
914
|
+
error: null;
|
|
915
|
+
variables: TVariables;
|
|
916
|
+
isIdle: false;
|
|
917
|
+
isPending: true;
|
|
918
|
+
isSuccess: false;
|
|
919
|
+
isError: false;
|
|
920
|
+
}) | (MutationResultCommon<T, TVariables> & {
|
|
921
|
+
status: 'success';
|
|
922
|
+
/** The stored document. Locally committed, or the server's body under `immediate`. */
|
|
923
|
+
data: T;
|
|
924
|
+
error: null;
|
|
925
|
+
variables: TVariables;
|
|
926
|
+
isIdle: false;
|
|
927
|
+
isPending: false;
|
|
928
|
+
isSuccess: true;
|
|
929
|
+
isError: false;
|
|
930
|
+
}) | (MutationResultCommon<T, TVariables> & {
|
|
931
|
+
status: 'error';
|
|
932
|
+
data: undefined;
|
|
933
|
+
error: Error;
|
|
934
|
+
variables: TVariables;
|
|
935
|
+
isIdle: false;
|
|
936
|
+
isPending: false;
|
|
937
|
+
isSuccess: false;
|
|
938
|
+
isError: true;
|
|
939
|
+
});
|
|
940
|
+
/**
|
|
941
|
+
* A write intent, as the storage layer sees it.
|
|
942
|
+
*
|
|
943
|
+
* No longer the public `mutate` signature — that takes plain variables, as
|
|
944
|
+
* TanStack's does — but still the internal representation, because a queued
|
|
945
|
+
* write has to be *data* to survive the component that made it.
|
|
946
|
+
*
|
|
947
|
+
* `where` is `{ _id }` rather than an arbitrary filter: a filter-based write
|
|
948
|
+
* issued from a secondary tab is forwarded to the primary and re-evaluated
|
|
949
|
+
* there against authoritative data, so it can affect a different set of
|
|
950
|
+
* documents than the optimistic UI just showed.
|
|
951
|
+
*/
|
|
952
|
+
type MutationOp<T extends Doc> = {
|
|
953
|
+
type: 'insert';
|
|
954
|
+
doc: Omit<T, '_id'> & {
|
|
955
|
+
_id?: string;
|
|
956
|
+
};
|
|
957
|
+
} | {
|
|
958
|
+
type: 'update';
|
|
959
|
+
where: {
|
|
960
|
+
_id: string;
|
|
961
|
+
};
|
|
962
|
+
set: Partial<Omit<T, '_id'>>;
|
|
963
|
+
} | {
|
|
964
|
+
type: 'delete';
|
|
965
|
+
where: {
|
|
966
|
+
_id: string;
|
|
967
|
+
};
|
|
968
|
+
};
|
|
969
|
+
/** A write that failed terminally and will not retry on its own. */
|
|
970
|
+
interface FailedWrite {
|
|
971
|
+
collection: string;
|
|
972
|
+
id: string;
|
|
973
|
+
op: SyncOp;
|
|
974
|
+
error: string;
|
|
975
|
+
attempts: number;
|
|
976
|
+
}
|
|
977
|
+
interface SyncStatus {
|
|
978
|
+
/** Queued, will send. */
|
|
979
|
+
pending: number;
|
|
980
|
+
/** Terminal. Needs a human decision. */
|
|
981
|
+
failed: FailedWrite[];
|
|
982
|
+
/** A drain cycle is running. */
|
|
983
|
+
draining: boolean;
|
|
984
|
+
online: boolean;
|
|
985
|
+
/** Requeue a failed write. */
|
|
986
|
+
retry: (id: string) => void;
|
|
987
|
+
/** Drop the local change and stop trying. */
|
|
988
|
+
discard: (id: string) => void;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
interface QueryContextValue {
|
|
992
|
+
/** The `taladb_queries` collection, or `null` until it has opened. */
|
|
993
|
+
queries: Collection<QueryRecord> | null;
|
|
994
|
+
backend: ResolvedBackend;
|
|
995
|
+
drain: Required<Pick<DrainOptions, 'policy'>> & DrainOptions;
|
|
996
|
+
/** Maps a query key to a collection name. See `QueryProviderProps`. */
|
|
997
|
+
resolveCollection?: (queryKey: QueryKey) => string | undefined;
|
|
998
|
+
/** Provider-wide query defaults. Each hook option overrides its entry. */
|
|
999
|
+
defaults: QueryDefaults['queries'];
|
|
1000
|
+
/** Declare a collection as managed, so the drain looks at it. */
|
|
1001
|
+
register: (collection: string) => void;
|
|
1002
|
+
/** Collections currently managed. Stable identity until one is added. */
|
|
1003
|
+
collections: string[];
|
|
1004
|
+
/** A drain cycle is in flight. */
|
|
1005
|
+
draining: boolean;
|
|
1006
|
+
/** Ask for a drain cycle soon. `force` is reserved for an explicit retry. */
|
|
1007
|
+
requestDrain: (force?: boolean) => void;
|
|
1008
|
+
/**
|
|
1009
|
+
* Record a write that is deliberately *not* asking to be sent yet — a
|
|
1010
|
+
* `queued` mutation.
|
|
1011
|
+
*
|
|
1012
|
+
* Separate from `requestDrain` because the difference between `queued` and
|
|
1013
|
+
* `optimistic` is exactly whether the write asks for a cycle. It still has to
|
|
1014
|
+
* be counted, or a backlog building under a long interval would never trip
|
|
1015
|
+
* the threshold.
|
|
1016
|
+
*/
|
|
1017
|
+
noteWrite: () => void;
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Read the query layer's context.
|
|
1021
|
+
*
|
|
1022
|
+
* Throws rather than returning a default, because every plausible default is
|
|
1023
|
+
* wrong: silently doing nothing would make a missing provider look like an
|
|
1024
|
+
* empty result set.
|
|
1025
|
+
*/
|
|
1026
|
+
declare function useQueryContext(): QueryContextValue;
|
|
1027
|
+
interface QueryProviderProps {
|
|
1028
|
+
children: ReactNode;
|
|
1029
|
+
/**
|
|
1030
|
+
* Headers for every outbound write, resolved **at send time**.
|
|
1031
|
+
*
|
|
1032
|
+
* That timing is the reason this lives here rather than on the hook: a write
|
|
1033
|
+
* queued while offline and drained an hour later must carry a current token,
|
|
1034
|
+
* not the one it was made with.
|
|
1035
|
+
*/
|
|
1036
|
+
headers?: () => HeadersInit | Promise<HeadersInit>;
|
|
1037
|
+
/**
|
|
1038
|
+
* Map a response onto what the queue should do. Optional — by default `2xx`
|
|
1039
|
+
* succeeds, `409` is already-applied, `5xx`/`408`/`429` retry, and any other
|
|
1040
|
+
* `4xx` is terminal.
|
|
1041
|
+
*/
|
|
1042
|
+
classify?: (response: Response, op: PendingWrite) => WriteOutcome;
|
|
1043
|
+
/** Override the fetch implementation (tests, instrumentation). */
|
|
1044
|
+
fetch?: typeof globalThis.fetch;
|
|
1045
|
+
drain?: DrainOptions;
|
|
1046
|
+
/**
|
|
1047
|
+
* Work out which collection a query key's documents belong in.
|
|
1048
|
+
*
|
|
1049
|
+
* `useQuery` infers the collection from `queryKey[0]`, which covers the usual
|
|
1050
|
+
* `['todos', …]` convention. Codebases whose keys do not lead with the
|
|
1051
|
+
* collection — `['api', 'v2', 'todos']`, or keys built by a helper — set this
|
|
1052
|
+
* once here rather than passing `collection` at every call site.
|
|
1053
|
+
*
|
|
1054
|
+
* Returning `undefined` falls back to the default inference. The result is
|
|
1055
|
+
* still checked against the registered collections.
|
|
1056
|
+
*/
|
|
1057
|
+
resolveCollection?: (queryKey: QueryKey) => string | undefined;
|
|
1058
|
+
/**
|
|
1059
|
+
* Defaults for every `useQuery` below this provider, in TanStack's
|
|
1060
|
+
* `new QueryClient({ defaultOptions })` shape so existing config moves across
|
|
1061
|
+
* unchanged.
|
|
1062
|
+
*
|
|
1063
|
+
* ```tsx
|
|
1064
|
+
* <QueryProvider defaultOptions={{ queries: { staleTime: 0, retry: false } }}>
|
|
1065
|
+
* ```
|
|
1066
|
+
*
|
|
1067
|
+
* `staleTime: 0` here is the one-line way back to TanStack's
|
|
1068
|
+
* revalidate-on-every-mount behaviour.
|
|
1069
|
+
*/
|
|
1070
|
+
defaultOptions?: QueryDefaults;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Provides the query layer's shared state beneath a `<TalaDBProvider>`.
|
|
1074
|
+
*
|
|
1075
|
+
* Nested rather than merged into `TalaDBProvider` so an application using only
|
|
1076
|
+
* the local hooks never opens the query-record collection at all.
|
|
1077
|
+
*/
|
|
1078
|
+
declare function QueryProvider({ children, headers, classify, fetch, drain, resolveCollection, defaultOptions, }: QueryProviderProps): react_jsx_runtime.JSX.Element;
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* One hour.
|
|
1082
|
+
*
|
|
1083
|
+
* TanStack defaults `staleTime` to `0`, which revalidates on every mount. That
|
|
1084
|
+
* is right for a cache that dies on reload — the data is usually seconds old
|
|
1085
|
+
* and the request is usually free — and wrong for one that survives reload and
|
|
1086
|
+
* is expected to work offline, where it turns every mount into a network hit
|
|
1087
|
+
* against data the device already holds.
|
|
1088
|
+
*/
|
|
1089
|
+
declare const DEFAULT_STALE_TIME: number;
|
|
1090
|
+
/**
|
|
1091
|
+
* Pass this as `placeholderData` to hold the previous key's data on screen
|
|
1092
|
+
* while the next one loads.
|
|
1093
|
+
*
|
|
1094
|
+
* v5 replaced the old `keepPreviousData` *option* with exactly this identity
|
|
1095
|
+
* function, so it is exported under the same name a migrating codebase already
|
|
1096
|
+
* imports.
|
|
1097
|
+
*/
|
|
1098
|
+
declare function keepPreviousData<T>(previous: T | undefined): T | undefined;
|
|
1099
|
+
/**
|
|
1100
|
+
* Read a slice of a collection, hydrated from a server and revalidated in the
|
|
1101
|
+
* background.
|
|
1102
|
+
*
|
|
1103
|
+
* `data` is resolved through the local database rather than from the fetch
|
|
1104
|
+
* response, so a local write to any document in the set re-renders this query
|
|
1105
|
+
* immediately — there is no cache to invalidate, because the collection *is*
|
|
1106
|
+
* the state. Documents arrive in the order the server returned them.
|
|
1107
|
+
*
|
|
1108
|
+
* On a cold key the hook fetches and reports `pending`. On a warm key it
|
|
1109
|
+
* renders what it has straight away and revalidates behind that only once
|
|
1110
|
+
* `staleTime` has elapsed — a reload never shows a spinner over data the device
|
|
1111
|
+
* already holds.
|
|
1112
|
+
*/
|
|
1113
|
+
declare function useQuery<T extends Doc, TData = T[]>(options: UseQueryOptions<T, TData>): QueryResult<TData>;
|
|
1114
|
+
declare function useQuery<T extends Doc, TData = T>(options: UseDocumentQueryOptions<T, TData>): QueryResult<TData | undefined>;
|
|
1115
|
+
declare function useQuery<TRaw, TDoc extends Doc, TAssembled = TDoc[], TData = TAssembled>(options: UseEnvelopeQueryOptions<TRaw, TDoc, TAssembled, TData>): QueryResult<TData>;
|
|
1116
|
+
/** Test seam — pending timers are module state that would leak between cases. */
|
|
1117
|
+
declare function resetForgetTimers(): void;
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Structural sharing: keep the previous reference when the new value is equal.
|
|
1121
|
+
*
|
|
1122
|
+
* Without this, `data` is a fresh array on every database snapshot — the live
|
|
1123
|
+
* query re-runs, rebuilds its documents, and hands back objects that are deeply
|
|
1124
|
+
* equal but referentially new. Migrated code is full of `useEffect(…, [data])`
|
|
1125
|
+
* and `useMemo(…, [data])`, and every one of them would fire on each snapshot,
|
|
1126
|
+
* including snapshots that changed nothing relevant to this query. TanStack does
|
|
1127
|
+
* the same thing for the same reason.
|
|
1128
|
+
*
|
|
1129
|
+
* The result is that a component re-renders only when something it can actually
|
|
1130
|
+
* observe has changed, and that `data === data` across renders holds as long as
|
|
1131
|
+
* the contents hold.
|
|
1132
|
+
*/
|
|
1133
|
+
declare function replaceEqualDeep<T>(previous: unknown, next: T): T;
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* One request per key, however many components ask for it.
|
|
1137
|
+
*
|
|
1138
|
+
* Without this, a screen holding a list, a count badge and a header that all use
|
|
1139
|
+
* `queryKey: ['todos']` fires three identical requests on mount. Each hook here
|
|
1140
|
+
* owns its own state — there is no shared observer the way TanStack has one — so
|
|
1141
|
+
* nothing else would collapse them. It is invisible in tests that mount
|
|
1142
|
+
* sequentially, because the first writes a record the rest read warm, which is
|
|
1143
|
+
* exactly why it needs its own guard rather than an assertion somewhere.
|
|
1144
|
+
*
|
|
1145
|
+
* The shared work is the *whole* fetch — request, hydration, and the query
|
|
1146
|
+
* record — not just the network call. Sharing only the response would still have
|
|
1147
|
+
* every waiter hydrate the same documents, and write amplification is a real
|
|
1148
|
+
* hazard here: on a secondary tab each of those writes is forwarded against a
|
|
1149
|
+
* bounded buffer.
|
|
1150
|
+
*/
|
|
1151
|
+
/** Identifies a request. Same collection and key hash means the same work. */
|
|
1152
|
+
declare function inflightKey(collection: string, keyHash: string): string;
|
|
1153
|
+
/**
|
|
1154
|
+
* Run `work` once for this key, or join the run already happening.
|
|
1155
|
+
*
|
|
1156
|
+
* `callerSignal` expresses only this caller's interest. Abandoning it does not
|
|
1157
|
+
* abort the shared request unless every other caller has abandoned it too —
|
|
1158
|
+
* otherwise one component unmounting would cancel a fetch three others are
|
|
1159
|
+
* still waiting on.
|
|
1160
|
+
*/
|
|
1161
|
+
declare function runShared<T>(key: string, work: (signal: AbortSignal) => Promise<T>, callerSignal: AbortSignal): Promise<T>;
|
|
1162
|
+
/**
|
|
1163
|
+
* Abandon the in-flight request for a key, if there is one.
|
|
1164
|
+
*
|
|
1165
|
+
* This is what `refetch({ cancelRefetch: true })` — the default — means: start
|
|
1166
|
+
* again rather than return whatever is already on its way.
|
|
1167
|
+
*/
|
|
1168
|
+
declare function cancelShared(key: string): void;
|
|
1169
|
+
/** Whether a request for this key is already on its way. */
|
|
1170
|
+
declare function isShared(key: string): boolean;
|
|
1171
|
+
/** Test seam — in-flight state is module state that would leak between cases. */
|
|
1172
|
+
declare function resetInflight(): void;
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Read-path retry policy.
|
|
1176
|
+
*
|
|
1177
|
+
* Deliberately separate from the write queue's backoff in `drain.ts`. They look
|
|
1178
|
+
* alike and mean different things: a queued write must survive a reload and a
|
|
1179
|
+
* week offline, so its attempt count lives on the document. A read is
|
|
1180
|
+
* in-flight state belonging to one mounted component, and is abandoned the
|
|
1181
|
+
* moment that component unmounts.
|
|
1182
|
+
*/
|
|
1183
|
+
/** TanStack's default: three retries, so four attempts in all. */
|
|
1184
|
+
declare const DEFAULT_RETRY = 3;
|
|
1185
|
+
/**
|
|
1186
|
+
* Whether to try again after `failureCount` failures.
|
|
1187
|
+
*
|
|
1188
|
+
* `failureCount` is the number of attempts that have already failed, so a
|
|
1189
|
+
* numeric `retry` of 3 keeps going while it is 1, 2 or 3 and gives up at 4.
|
|
1190
|
+
*/
|
|
1191
|
+
declare function shouldRetry(retry: RetryOption | undefined, failureCount: number, error: Error): boolean;
|
|
1192
|
+
/**
|
|
1193
|
+
* How long to wait before attempt `attemptIndex + 1`.
|
|
1194
|
+
*
|
|
1195
|
+
* Exponential from one second, capped at thirty — the same curve TanStack uses,
|
|
1196
|
+
* so a migrated app's retry timing does not quietly change.
|
|
1197
|
+
*/
|
|
1198
|
+
declare function retryDelayMs(retryDelay: RetryDelayOption | undefined, attemptIndex: number, error: Error): number;
|
|
1199
|
+
/** Whether the browser believes it is offline. Unknown counts as online. */
|
|
1200
|
+
declare function isOffline(): boolean;
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Development-time notices.
|
|
1204
|
+
*
|
|
1205
|
+
* Some TanStack options are accepted here and do nothing — a drop-in that
|
|
1206
|
+
* *throws* on an unknown option is not a drop-in. But silence is a trap, so
|
|
1207
|
+
* every such option says so once, loudly enough to find and quietly enough to
|
|
1208
|
+
* live with.
|
|
1209
|
+
*/
|
|
1210
|
+
declare function isDev(): boolean;
|
|
1211
|
+
/**
|
|
1212
|
+
* Warn once per key for the life of the session.
|
|
1213
|
+
*
|
|
1214
|
+
* A hook warning on every render is noise that gets filtered out, which is the
|
|
1215
|
+
* same as not warning at all.
|
|
1216
|
+
*/
|
|
1217
|
+
declare function warnOnce(key: string, message: string): void;
|
|
1218
|
+
/** Test seam — the warn-once set is module state that would leak between cases. */
|
|
1219
|
+
declare function resetWarnings(): void;
|
|
1220
|
+
/** Announce any accept-and-ignore option present on this call. */
|
|
1221
|
+
declare function noteIgnoredOptions(options: Record<string, unknown>): void;
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* Work out which collection a query's documents belong in.
|
|
1225
|
+
*
|
|
1226
|
+
* This is the one option TanStack Query has no analogue for, and the one most
|
|
1227
|
+
* worth getting right: it is not a cache label, it is the physical destination.
|
|
1228
|
+
* `hydrate` writes every fetched document into `db.collection(name)`, so a wrong
|
|
1229
|
+
* value does not degrade a cache — it pours server documents into a collection
|
|
1230
|
+
* the application reads for something else, or conjures one nobody declared.
|
|
1231
|
+
* There is no cache-miss failure mode here to soften the landing.
|
|
1232
|
+
*
|
|
1233
|
+
* So inference is allowed, but never trusted: the provider's `collections`
|
|
1234
|
+
* registry is the list of legal answers, and a guess that is not on it is an
|
|
1235
|
+
* error rather than a new collection.
|
|
1236
|
+
*/
|
|
1237
|
+
interface ResolveCollectionInput {
|
|
1238
|
+
/** Explicit option from the call site. Always wins. */
|
|
1239
|
+
collection?: string;
|
|
1240
|
+
queryKey: QueryKey;
|
|
1241
|
+
/** Provider-level override, for keys that do not lead with the collection. */
|
|
1242
|
+
resolve?: (queryKey: QueryKey) => string | undefined;
|
|
1243
|
+
/** Registered collection names, from the `TalaDBProvider` registry. */
|
|
1244
|
+
registered: string[];
|
|
1245
|
+
}
|
|
1246
|
+
declare function resolveCollectionName({ collection, queryKey, resolve, registered, }: ResolveCollectionInput): string;
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Work out which documents a `queryFn` response contains.
|
|
1250
|
+
*
|
|
1251
|
+
* This is the deepest mismatch with TanStack Query and the one no rename can
|
|
1252
|
+
* absorb: their `data` *is* whatever `queryFn` returned, ours is rebuilt from
|
|
1253
|
+
* the local collection. So a response has to reduce to documents, and the three
|
|
1254
|
+
* shapes that reduce cleanly are recognised without configuration:
|
|
1255
|
+
*
|
|
1256
|
+
* - `T[]` — a list. The common case.
|
|
1257
|
+
* - `T` — one document, for a detail view.
|
|
1258
|
+
* - anything else — an envelope, which must say which part of itself is
|
|
1259
|
+
* documents via `documents`, and how to put itself back together via
|
|
1260
|
+
* `assemble`.
|
|
1261
|
+
*
|
|
1262
|
+
* A response that is none of those fails loudly. It is the one case a codemod
|
|
1263
|
+
* cannot fix, so the runtime has to explain it.
|
|
1264
|
+
*/
|
|
1265
|
+
interface ExtractInput {
|
|
1266
|
+
raw: unknown;
|
|
1267
|
+
collection: string;
|
|
1268
|
+
queryKey: QueryKey;
|
|
1269
|
+
documents?: (raw: unknown) => Doc[];
|
|
1270
|
+
}
|
|
1271
|
+
interface Extracted {
|
|
1272
|
+
documents: Doc[];
|
|
1273
|
+
shape: ResultShape;
|
|
1274
|
+
}
|
|
1275
|
+
declare function extractDocuments({ raw, collection, queryKey, documents, }: ExtractInput): Extracted;
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Write to a collection.
|
|
1279
|
+
*
|
|
1280
|
+
* `mutate(variables)` takes the document — TanStack's signature — and the
|
|
1281
|
+
* operation is declared once on the hook:
|
|
1282
|
+
*
|
|
1283
|
+
* ```ts
|
|
1284
|
+
* const createTodo = useMutation<Todo>({ collection: 'todos', url: '/api/todos/:id' })
|
|
1285
|
+
* createTodo.mutate({ title: 'Buy milk' })
|
|
1286
|
+
*
|
|
1287
|
+
* const updateTodo = useMutation<Todo>({ collection: 'todos', url: '/api/todos/:id', operation: 'update' })
|
|
1288
|
+
* updateTodo.mutate({ _id, done: true })
|
|
1289
|
+
* ```
|
|
1290
|
+
*
|
|
1291
|
+
* `url` is a template rather than a function for one reason: under `optimistic`
|
|
1292
|
+
* and `queued` the request is sent by the drain loop, long after this component
|
|
1293
|
+
* is gone — after a route change, a reload, a week. A template is data and can
|
|
1294
|
+
* be stored on the queued document; a closure cannot. That is also why
|
|
1295
|
+
* `mutationFn` is only possible under `immediate`.
|
|
1296
|
+
*
|
|
1297
|
+
* Auth and error policy stay on `<QueryProvider>`. `headers` in particular has
|
|
1298
|
+
* to be resolved at send time, so a write queued offline carries a current
|
|
1299
|
+
* token instead of the expired one it was made with.
|
|
1300
|
+
*/
|
|
1301
|
+
declare function useMutation<T extends Doc, TVariables = T>(options: UseMutationOptions<T, TVariables> & {
|
|
1302
|
+
mode: 'immediate';
|
|
1303
|
+
mutationFn: (variables: TVariables) => Promise<T>;
|
|
1304
|
+
}): MutationResult<T, TVariables>;
|
|
1305
|
+
declare function useMutation<T extends Doc = Doc>(options: UseMutationOptions<T, InsertVariables<T>> & {
|
|
1306
|
+
operation?: 'insert';
|
|
1307
|
+
}): MutationResult<T, InsertVariables<T>>;
|
|
1308
|
+
declare function useMutation<T extends Doc = Doc>(options: UseMutationOptions<T, UpdateVariables<T>> & {
|
|
1309
|
+
operation: 'update';
|
|
1310
|
+
}): MutationResult<T, UpdateVariables<T>>;
|
|
1311
|
+
declare function useMutation<T extends Doc = Doc>(options: UseMutationOptions<T, DeleteVariables> & {
|
|
1312
|
+
operation: 'delete';
|
|
1313
|
+
}): MutationResult<T, DeleteVariables>;
|
|
1314
|
+
/** Test seam — scope chains are module state that would leak between cases. */
|
|
1315
|
+
declare function resetMutationScopes(): void;
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* What the write queue is doing, live.
|
|
1319
|
+
*
|
|
1320
|
+
* Optimistic writes are the point of this layer and its biggest hazard: a
|
|
1321
|
+
* request that fails after the user has navigated away has no UI left to fail
|
|
1322
|
+
* into. Without something rendering this, a write can be lost with no symptom
|
|
1323
|
+
* at all — which is how local-first libraries lose people's trust. Show
|
|
1324
|
+
* `pending` somewhere quiet and `failed` somewhere loud.
|
|
1325
|
+
*
|
|
1326
|
+
* A terminal failure is never retried on its own. It needs a decision:
|
|
1327
|
+
* `retry` when the cause has been fixed server-side, `discard` to stop trying.
|
|
1328
|
+
*/
|
|
1329
|
+
declare function useSyncStatus(): SyncStatus;
|
|
1330
|
+
|
|
1331
|
+
/** Where a queued write should be sent, stamped onto the document. */
|
|
1332
|
+
interface Route {
|
|
1333
|
+
url?: string;
|
|
1334
|
+
method?: Partial<Record<SyncOp, string>>;
|
|
1335
|
+
}
|
|
1336
|
+
/** What a local write did, for tests and for `useSyncStatus` to count. */
|
|
1337
|
+
interface LocalWriteResult {
|
|
1338
|
+
id: string;
|
|
1339
|
+
/** What the document now owes the server, or `null` if it owes nothing. */
|
|
1340
|
+
op: SyncOp | null;
|
|
1341
|
+
/** The write cancelled an unsent insert, so the document was removed outright. */
|
|
1342
|
+
cancelled: boolean;
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Apply a write locally and record what the server still owes.
|
|
1346
|
+
*
|
|
1347
|
+
* Deletes become **tombstones**, not deletions. A queued delete lives on the
|
|
1348
|
+
* document it deletes; calling `deleteOne` here would erase the only record of
|
|
1349
|
+
* the operation, and the server would never hear about it. The document is
|
|
1350
|
+
* removed for real once the drain gets an acknowledgement.
|
|
1351
|
+
*
|
|
1352
|
+
* At most one operation is pending per document — a second edit folds into the
|
|
1353
|
+
* first rather than queueing behind it. That is what removes the dependency
|
|
1354
|
+
* graph, the id-remap table, and the replay log in one stroke; the cost is that
|
|
1355
|
+
* intermediate states are lost, so `$inc`-style writes cannot be expressed
|
|
1356
|
+
* offline.
|
|
1357
|
+
*/
|
|
1358
|
+
declare function writeLocal<T extends Document>(collection: Collection<T>, op: MutationOp<T>, now: number, route?: Route): Promise<LocalWriteResult>;
|
|
1359
|
+
/**
|
|
1360
|
+
* Apply a confirmed server document locally, after a `remote-first` write.
|
|
1361
|
+
*
|
|
1362
|
+
* The response body is canonical, so server-set fields — timestamps, defaults,
|
|
1363
|
+
* computed columns — land without a second fetch.
|
|
1364
|
+
*/
|
|
1365
|
+
declare function writeConfirmed<T extends Document>(collection: Collection<T>, doc: T, now: number): Promise<void>;
|
|
1366
|
+
|
|
1367
|
+
interface DrainStats {
|
|
1368
|
+
sent: number;
|
|
1369
|
+
/** Confirmed by the server (`ok` or `applied`). */
|
|
1370
|
+
synced: number;
|
|
1371
|
+
/** Will be retried after a back-off. */
|
|
1372
|
+
deferred: number;
|
|
1373
|
+
/** Terminal — parked for a human decision. */
|
|
1374
|
+
failed: number;
|
|
1375
|
+
/** Superseded by a local edit that landed while the request was in flight. */
|
|
1376
|
+
superseded: number;
|
|
1377
|
+
/** True when the cycle did not run because this tab is not the primary. */
|
|
1378
|
+
skippedNotPrimary: boolean;
|
|
1379
|
+
}
|
|
1380
|
+
interface DrainDeps {
|
|
1381
|
+
db: TalaDB;
|
|
1382
|
+
/** Fallback for documents queued without their own route. */
|
|
1383
|
+
backend: ResolvedBackend | null;
|
|
1384
|
+
/** Collections this layer manages, resolved fresh each cycle. */
|
|
1385
|
+
collections: () => string[];
|
|
1386
|
+
batch?: number;
|
|
1387
|
+
now?: () => number;
|
|
1388
|
+
random?: () => number;
|
|
1389
|
+
/** Send with `keepalive`, for a flush on `pagehide`. */
|
|
1390
|
+
keepalive?: boolean;
|
|
1391
|
+
}
|
|
1392
|
+
/** 1s, doubling, ±25% jitter, capped at five minutes. */
|
|
1393
|
+
declare function backoffMs(attempt: number, random?: () => number): number;
|
|
1394
|
+
/**
|
|
1395
|
+
* Send every eligible queued write once.
|
|
1396
|
+
*
|
|
1397
|
+
* **Runs only on the primary tab.** This is a correctness requirement, not an
|
|
1398
|
+
* optimisation. A secondary tab reads a pending set that lags other tabs by up
|
|
1399
|
+
* to half a second, and its own bookkeeping — marking a document synced after a
|
|
1400
|
+
* successful request — is itself a forwarded write applied at the primary
|
|
1401
|
+
* later. Until that lands the document still reads as pending locally, so the
|
|
1402
|
+
* next cycle sends it again. Contract rules 2 and 3 keep that survivable rather
|
|
1403
|
+
* than corrupting, which is exactly why they are rules.
|
|
1404
|
+
*
|
|
1405
|
+
* Primary status changes mid-session when the owning tab closes, so it is
|
|
1406
|
+
* re-checked every cycle rather than cached.
|
|
1407
|
+
*/
|
|
1408
|
+
declare function drainOnce(deps: DrainDeps): Promise<DrainStats>;
|
|
1409
|
+
|
|
1410
|
+
interface SendResult {
|
|
1411
|
+
outcome: WriteOutcome;
|
|
1412
|
+
/** The canonical document, when the response carried one. */
|
|
1413
|
+
document: Record<string, unknown> | null;
|
|
1414
|
+
status: number;
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Send one queued write to the application's backend and classify the result.
|
|
1418
|
+
*
|
|
1419
|
+
* Shared by `remote-first` mutations and the drain loop, so both agree on what
|
|
1420
|
+
* a 409 means and neither invents its own retry policy.
|
|
1421
|
+
*/
|
|
1422
|
+
declare function sendWrite(backend: ResolvedBackend, op: PendingWrite, signal?: AbortSignal, attempt?: number,
|
|
1423
|
+
/**
|
|
1424
|
+
* Ask the browser to let this request outlive the page.
|
|
1425
|
+
*
|
|
1426
|
+
* Set only for a flush on `pagehide`, where an ordinary `fetch` is cancelled
|
|
1427
|
+
* the moment the document goes away. The cost is a hard 64 KB cap on the
|
|
1428
|
+
* combined body of all in-flight keepalive requests, which is why this is
|
|
1429
|
+
* never the default — a normal drain has a live page and does not need it.
|
|
1430
|
+
*/
|
|
1431
|
+
keepalive?: boolean): Promise<SendResult>;
|
|
1432
|
+
/** Describe a stored document as the write the backend should receive. */
|
|
1433
|
+
declare function toPendingWrite(collection: string, id: string, type: PendingWrite['type'], document: Record<string, unknown> | null): PendingWrite;
|
|
1434
|
+
|
|
1435
|
+
interface HydrateResult {
|
|
1436
|
+
/** Documents the device had never seen. */
|
|
1437
|
+
inserted: number;
|
|
1438
|
+
/** Documents refreshed from the response. */
|
|
1439
|
+
updated: number;
|
|
1440
|
+
/** Documents left alone because they carry unsynced local work. */
|
|
1441
|
+
skipped: number;
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Write a server response into the local collection.
|
|
1445
|
+
*
|
|
1446
|
+
* The rule that matters, and the one a naive bulk upsert gets wrong: **a
|
|
1447
|
+
* response must never overwrite a document with unsynced local work.** The
|
|
1448
|
+
* moment connectivity returns, a refetch and a queued edit race, and the
|
|
1449
|
+
* obvious implementation silently discards whatever the user did offline.
|
|
1450
|
+
* Anything not `synced` is therefore left exactly as it is — the drain loop
|
|
1451
|
+
* owns those documents until the server accepts them.
|
|
1452
|
+
*
|
|
1453
|
+
* Inserts go through one `insertMany`; updates cannot be batched, because each
|
|
1454
|
+
* document sets different values and the engine exposes no upsert. That matters
|
|
1455
|
+
* on a secondary browser tab, where every write is forwarded to the primary and
|
|
1456
|
+
* the forward buffer is bounded — see PLAN-query.md §5.
|
|
1457
|
+
*/
|
|
1458
|
+
declare function hydrate<T extends Document>(collection: Collection<T>, docs: T[], now: number): Promise<HydrateResult>;
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* Rules 1 and 2 — client ids are authoritative, and writes are idempotent.
|
|
1462
|
+
*
|
|
1463
|
+
* Same detection, different consequence: on a first attempt a reassigned id
|
|
1464
|
+
* means every write will duplicate from now on; on a retry it means this write
|
|
1465
|
+
* *just* duplicated, because the server treated the retry as a new record.
|
|
1466
|
+
*/
|
|
1467
|
+
declare function checkResponseId(op: PendingWrite, body: Record<string, unknown> | null, attempt: number): void;
|
|
1468
|
+
/** Rule 4 — the response body is the canonical document. */
|
|
1469
|
+
declare function checkResponseBody(op: PendingWrite, body: Record<string, unknown> | null): void;
|
|
1470
|
+
/** Rule 3 — error classes are explicit, and a 4xx is not retryable. */
|
|
1471
|
+
declare function checkClassification(op: PendingWrite, status: number, outcome: WriteOutcome): void;
|
|
1472
|
+
/** Rule 6 — mutation filters must be `_id`-based. */
|
|
1473
|
+
declare function checkMutationFilter(where: unknown): void;
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* Fields this layer owns on every document it manages.
|
|
1477
|
+
*
|
|
1478
|
+
* They live on the document rather than in a separate outbox collection because
|
|
1479
|
+
* TalaDB exposes no multi-collection transaction: a document write and an
|
|
1480
|
+
* outbox row describing it would be two commits, and a crash between them
|
|
1481
|
+
* either orphans a queued mutation or strands a document that never syncs.
|
|
1482
|
+
* Keeping the state here makes every queued write a single atomic commit — and
|
|
1483
|
+
* turns the queue into an indexed query.
|
|
1484
|
+
*/
|
|
1485
|
+
declare const ENVELOPE_FIELDS: readonly ["_sync", "_op", "_attempt", "_error", "_fetched_at", "_retry_at", "_endpoint", "_method", "_revision"];
|
|
1486
|
+
/**
|
|
1487
|
+
* Indexes this layer needs on every collection it manages.
|
|
1488
|
+
*
|
|
1489
|
+
* `_sync` is the queue the drain selects on. `_changed_at` is its order: the
|
|
1490
|
+
* drain sorts by it to send per-document writes in the order they happened, and
|
|
1491
|
+
* since 0.11.0 that field is no longer indexed automatically — without this the
|
|
1492
|
+
* sort falls back to reading the whole collection. `createIndex` is idempotent,
|
|
1493
|
+
* so both are safe to run on every registration.
|
|
1494
|
+
*/
|
|
1495
|
+
declare const ENVELOPE_INDEXES: readonly ["_sync", "_changed_at"];
|
|
1496
|
+
/**
|
|
1497
|
+
* Reserved field names present in a document, if any.
|
|
1498
|
+
*
|
|
1499
|
+
* Checked against real documents rather than against the registered schema:
|
|
1500
|
+
* schemas are Standard Schema values (Zod, Valibot, …) with no portable way to
|
|
1501
|
+
* enumerate their keys, and the failure this guards against — a server response
|
|
1502
|
+
* carrying a `_sync` field that overwrites the envelope and drops the document
|
|
1503
|
+
* out of the queue — shows up in the data regardless of what the schema says.
|
|
1504
|
+
*/
|
|
1505
|
+
declare function reservedFieldsIn(doc: Record<string, unknown>): string[];
|
|
1506
|
+
/** A document as stored: the caller's fields plus the envelope. */
|
|
1507
|
+
type Enveloped<T extends Document> = T & SyncEnvelope;
|
|
1508
|
+
/**
|
|
1509
|
+
* Stamp a document that came from the server and matches it.
|
|
1510
|
+
*
|
|
1511
|
+
* Only ever applied to documents that are not currently pending — a response
|
|
1512
|
+
* must never overwrite an unsynced local edit, which is the mistake a bulk
|
|
1513
|
+
* upsert makes by default.
|
|
1514
|
+
*/
|
|
1515
|
+
declare function stampSynced<T extends Document>(doc: T, now: number): Enveloped<T>;
|
|
1516
|
+
/**
|
|
1517
|
+
* Stamp a locally-written document that the server has not seen yet.
|
|
1518
|
+
*
|
|
1519
|
+
* `_fetched_at` is deliberately preserved from any previous hydration: it
|
|
1520
|
+
* records when the server last spoke about this document, and a local edit does
|
|
1521
|
+
* not change that.
|
|
1522
|
+
*/
|
|
1523
|
+
declare function stampPending<T extends Document>(doc: T, op: SyncOp, previous?: Partial<SyncEnvelope>): Enveloped<T>;
|
|
1524
|
+
/**
|
|
1525
|
+
* Fold a new write into whatever this document already owed the server.
|
|
1526
|
+
*
|
|
1527
|
+
* At most one operation is pending per document, so a second edit overwrites
|
|
1528
|
+
* the first rather than queueing behind it. This is what removes the dependency
|
|
1529
|
+
* graph, the id-remap table, and the replay log in one go — the cost being that
|
|
1530
|
+
* intermediate states are lost, so `$inc`-style operations cannot be expressed
|
|
1531
|
+
* offline.
|
|
1532
|
+
*
|
|
1533
|
+
* Returns `null` when the write cancels the pending operation outright: an
|
|
1534
|
+
* insert the server never saw, then deleted, is simply gone.
|
|
1535
|
+
*/
|
|
1536
|
+
declare function coalesce(pending: SyncOp | undefined, next: SyncOp): SyncOp | null;
|
|
1537
|
+
|
|
1538
|
+
/**
|
|
1539
|
+
* The collection holding one record per query shape.
|
|
1540
|
+
*
|
|
1541
|
+
* Not `_queries`: the engine reserves collection names beginning with `_` and
|
|
1542
|
+
* rejects them outright with `InvalidName`.
|
|
1543
|
+
*/
|
|
1544
|
+
declare const QUERY_COLLECTION = "taladb_queries";
|
|
1545
|
+
/**
|
|
1546
|
+
* Serialise a query key to a string that is identical for equal keys and
|
|
1547
|
+
* different for unequal ones.
|
|
1548
|
+
*
|
|
1549
|
+
* Plain-object properties are emitted in sorted order, so `{ a: 1, b: 2 }` and
|
|
1550
|
+
* `{ b: 2, a: 1 }` — which a component may produce on alternating renders from
|
|
1551
|
+
* the same props — hash to one record rather than two. Arrays keep their order,
|
|
1552
|
+
* because `['a', 'b']` and `['b', 'a']` are genuinely different keys.
|
|
1553
|
+
*
|
|
1554
|
+
* `undefined` is preserved as an explicit marker rather than dropped, so
|
|
1555
|
+
* `{ status: undefined }` and `{}` stay distinct: the first says "this filter
|
|
1556
|
+
* exists and is unset", the second says "no such filter".
|
|
1557
|
+
*
|
|
1558
|
+
* Keys must be JSON-shaped — primitives, arrays, plain objects. A function,
|
|
1559
|
+
* symbol, or class instance throws rather than silently collapsing two
|
|
1560
|
+
* different queries onto one record.
|
|
1561
|
+
*/
|
|
1562
|
+
declare function hashQueryKey(key: QueryKey): string;
|
|
1563
|
+
/**
|
|
1564
|
+
* The `_id` of the `taladb_queries` record for one query shape.
|
|
1565
|
+
*
|
|
1566
|
+
* Document ids are 16 raw bytes on disk and must be ULIDs, so the natural key —
|
|
1567
|
+
* here `(collection, key)` — is folded into one by the same `deriveDocId` used
|
|
1568
|
+
* for hydrating rows that already have an identity upstream. The mapping is
|
|
1569
|
+
* deterministic, which is what lets a refetch overwrite its own record instead
|
|
1570
|
+
* of accumulating a second one.
|
|
1571
|
+
*/
|
|
1572
|
+
declare function queryRecordId(collection: string, key: QueryKey,
|
|
1573
|
+
/** Override the key serialiser — see the `queryKeyHashFn` option. */
|
|
1574
|
+
hash?: (key: QueryKey) => string): string;
|
|
1575
|
+
/**
|
|
1576
|
+
* Mint a document id on the client.
|
|
1577
|
+
*
|
|
1578
|
+
* Client ids are authoritative — contract rule 1 — so a write must know its id
|
|
1579
|
+
* before it reaches the network, not after. That matters most for a
|
|
1580
|
+
* `remote-first` insert, which has no local document to take an
|
|
1581
|
+
* engine-generated id from, and it is what lets a retried insert be recognised
|
|
1582
|
+
* as a duplicate rather than creating a second row.
|
|
1583
|
+
*
|
|
1584
|
+
* Routed through `deriveDocId` rather than a second ULID encoder, so there is
|
|
1585
|
+
* exactly one implementation of "produce an id this engine will accept".
|
|
1586
|
+
*/
|
|
1587
|
+
declare function newDocId(collection: string): string;
|
|
1588
|
+
|
|
1589
|
+
/** The verb each operation uses unless the caller overrides it. */
|
|
1590
|
+
declare const DEFAULT_METHODS: Record<SyncOp, string>;
|
|
1591
|
+
/**
|
|
1592
|
+
* Resolve a URL template for one write.
|
|
1593
|
+
*
|
|
1594
|
+
* A template rather than a function because a queued write is sent long after
|
|
1595
|
+
* the component that made it is gone — possibly after a reload — so the route
|
|
1596
|
+
* has to be storable alongside the document. `:id` and `:collection`
|
|
1597
|
+
* interpolate.
|
|
1598
|
+
*
|
|
1599
|
+
* An insert has no id in the path: `/api/todos/:id` becomes `/api/todos`. That
|
|
1600
|
+
* matches how REST endpoints are actually shaped, so one template covers all
|
|
1601
|
+
* three verbs instead of forcing a branch at every call site.
|
|
1602
|
+
*/
|
|
1603
|
+
declare function resolveUrl(template: string, collection: string, id: string, type: SyncOp): string;
|
|
1604
|
+
/**
|
|
1605
|
+
* How a response is read when the application has not supplied a `classify`.
|
|
1606
|
+
*
|
|
1607
|
+
* Deliberately conservative: only a 409 is assumed to mean "already applied",
|
|
1608
|
+
* because that is the one convention universal enough to guess at. Everything
|
|
1609
|
+
* else follows the HTTP status class.
|
|
1610
|
+
*/
|
|
1611
|
+
declare function defaultClassify(response: Response): WriteOutcome;
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* Prepare a collection to be managed by this layer.
|
|
1615
|
+
*
|
|
1616
|
+
* Idempotent — `createIndex` is a no-op when the index already exists, so this
|
|
1617
|
+
* runs on every registration rather than needing to track what it has seen.
|
|
1618
|
+
*/
|
|
1619
|
+
declare function ensureEnvelopeIndexes(collection: Collection<Document>): Promise<void>;
|
|
1620
|
+
/** The `taladb_queries` collection, prepared for use. */
|
|
1621
|
+
declare function openQueryCollection(db: TalaDB): Promise<Collection<QueryRecord>>;
|
|
1622
|
+
/**
|
|
1623
|
+
* Read the record for one query shape, or `null` if this query has never been
|
|
1624
|
+
* fetched on this device.
|
|
1625
|
+
*/
|
|
1626
|
+
declare function readQueryRecord(queries: Collection<QueryRecord>, collection: string, key: QueryKey, hash?: (key: QueryKey) => string): Promise<QueryRecord | null>;
|
|
1627
|
+
/**
|
|
1628
|
+
* Forget which documents a query returned.
|
|
1629
|
+
*
|
|
1630
|
+
* Deliberately *only* the record. The documents stay: they live in the
|
|
1631
|
+
* application's own collections, which it also reads directly, so deleting them
|
|
1632
|
+
* would destroy real user data rather than reclaim a cache. Dropping the record
|
|
1633
|
+
* makes the next mount cold, and it refetches to re-establish membership.
|
|
1634
|
+
*/
|
|
1635
|
+
declare function deleteQueryRecord(queries: Collection<QueryRecord>, collection: string, key: QueryKey, hash?: (key: QueryKey) => string): Promise<void>;
|
|
1636
|
+
/**
|
|
1637
|
+
* Record what a fetch returned: which documents were in the result set, in
|
|
1638
|
+
* server order, and when.
|
|
1639
|
+
*
|
|
1640
|
+
* Membership is stored rather than derived because the two facts a client
|
|
1641
|
+
* cannot otherwise distinguish — "this document no longer matches the filter"
|
|
1642
|
+
* and "this document was deleted upstream" — look identical from here. Keeping
|
|
1643
|
+
* the id list means a refetch can shrink the result set without having to
|
|
1644
|
+
* delete anything.
|
|
1645
|
+
*/
|
|
1646
|
+
declare function writeQueryRecord(queries: Collection<QueryRecord>, collection: string, key: QueryKey, ids: string[], now: number, ttl: number,
|
|
1647
|
+
/** How `queryFn`'s return value mapped onto documents. */
|
|
1648
|
+
shape?: ResultShape,
|
|
1649
|
+
/** The raw response, for envelope queries. `undefined` for every other shape. */
|
|
1650
|
+
payload?: Value,
|
|
1651
|
+
/** Override the key serialiser — see `queryKeyHashFn`. */
|
|
1652
|
+
hash?: (key: QueryKey) => string): Promise<QueryRecord>;
|
|
1653
|
+
/**
|
|
1654
|
+
* Whether a record is past its freshness window.
|
|
1655
|
+
*
|
|
1656
|
+
* Staleness drives a background refetch — never a deletion. The documents this
|
|
1657
|
+
* layer hydrates land in the application's own collections, which the app also
|
|
1658
|
+
* reads directly; a TTL sweep over them would delete real user data, not
|
|
1659
|
+
* reclaim cache. A record with `ttl: 0` is always stale, which is how
|
|
1660
|
+
* "revalidate on every mount" is expressed.
|
|
1661
|
+
*/
|
|
1662
|
+
declare function isStale(record: QueryRecord, now: number): boolean;
|
|
1663
|
+
|
|
1664
|
+
export { type AnyQueryOptions, type BackendDefinition, type BaseQueryOptions, type BoundParams, DEFAULT_METHODS, DEFAULT_RETRY, DEFAULT_STALE_TIME, type DeleteVariables, type Doc, type DrainDeps, type DrainOptions, type DrainPolicy, type DrainStats, ENVELOPE_FIELDS, ENVELOPE_INDEXES, type Enveloped, type ExtractInput, type Extracted, type FailedWrite, type HydrateResult, type InsertVariables, type LocalWriteResult, type MutationCallbacks, type MutationContext, type MutationMode, type MutationOp, type MutationOperation, type MutationResult, type MutationResultCommon, type NetworkMode, type ParamOp, type ParamPrimitive, type ParamSpec, type ParamValue, type PendingWrite, QUERY_COLLECTION, type QueryContextValue, type QueryDefaults, type QueryFunctionContext, type QueryKey, QueryProvider, type QueryProviderProps, type QueryRecord, type QueryResult, type QueryResultCommon, type RefetchTrigger, type ResolveCollectionInput, type ResolvedBackend, type ResultShape, type RetryDelayOption, type RetryOption, type Route, type SendResult, type SyncEnvelope, type SyncOp, type SyncState, type SyncStatus, type UpdateVariables, type UseDocumentQueryOptions, type UseEnvelopeQueryOptions, type UseMutationOptions, type UseQueryOptions, type ValuesOf, type WriteOutcome, backoffMs, cancelShared, checkClassification, checkMutationFilter, checkResponseBody, checkResponseId, coalesce, contains, defaultClassify, defineParams, deleteQueryRecord, drainOnce, ensureEnvelopeIndexes, eq, extractDocuments, gt, gte, hashQueryKey, hydrate, inflightKey, isBoundParams, isDev, isOffline, isShared, isStale, keepPreviousData, lt, lte, ne, newDocId, normalizeParams, noteIgnoredOptions, oneOf, openQueryCollection, queryRecordId, readQueryRecord, replaceEqualDeep, reservedFieldsIn, resetWarnings as resetCollectionWarnings, resetForgetTimers, resetInflight, resetMutationScopes, resetWarnings, resolveCollectionName, resolveUrl, retryDelayMs, runShared, sendWrite, shape, shouldRetry, stampPending, stampSynced, toPendingWrite, useMutation, useQuery, useQueryContext, useSyncStatus, warnOnce, writeConfirmed, writeLocal, writeQueryRecord };
|