@taladb/react 0.10.1 → 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.
@@ -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 };