@uniflowed/query 0.0.0-alpha.10

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/cache.js ADDED
@@ -0,0 +1,136 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/query/cache`: every entry, and how to say which ones you mean.
4
+ //
5
+ // The cache is a `Map` from a key's hash to its [`Query`], plus the vocabulary
6
+ // for describing a *set* of entries. Those are two different jobs and this
7
+ // module owns the second: a query knows what happened to one key, and the
8
+ // cache knows how "everything under `["users"]` that somebody is looking at"
9
+ // turns into a list of them.
10
+ //
11
+ // # Why filters are a shape rather than a list of methods
12
+ //
13
+ // Invalidation, cancellation, refetching and removal all need the same
14
+ // question answered — which entries does this describe? — and they need it
15
+ // answered identically, or `invalidateQueries` and `cancelQueries` given the
16
+ // same argument would act on different entries. So there is one
17
+ // [`QueryFilters`] shape and one [`findAll`], and every operation on the
18
+ // client is that plus a verb.
19
+ //
20
+ // `type: "active"` is the one that carries weight. Invalidation has to refetch
21
+ // what is on screen and merely mark the rest, because refetching an entry
22
+ // nobody is watching is a request whose answer will be garbage-collected
23
+ // before it is read. Without the distinction, invalidating `["users"]` in an
24
+ // application with a hundred cached users makes a hundred requests.
25
+ //
26
+ // # Why the cache is explicit rather than a module-level default
27
+ //
28
+ // A singleton is shared with every other test in the process, and one test's
29
+ // cached answer then decides another test's result — a class of flake that
30
+ // costs hours to attribute because the failing test is not the one at fault.
31
+ // It is also wrong in production: a server rendering two requests must not let
32
+ // one reader's data reach the other. One cache per [`QueryClient`], one client
33
+ // per tree, one tree per request.
34
+
35
+ import { hashKey, matchesKey } from "./key.js";
36
+ import type { QueryKey } from "./key.js";
37
+ import { DEFAULT_GC_TIME, Query } from "./query.js";
38
+
39
+ /**
40
+ * Which entries an operation applies to.
41
+ *
42
+ * Everything is optional and everything narrows: no filter at all means every
43
+ * entry, which is what `invalidateQueries()` with no argument means and why
44
+ * that call is worth writing.
45
+ */
46
+ export type QueryFilters = {|
47
+ /** A key prefix, or an exact key with `exact`. */
48
+ readonly queryKey?: QueryKey,
49
+ readonly exact?: boolean,
50
+ /** `active` is "somebody is watching"; see the module docs. */
51
+ readonly type?: "all" | "active" | "inactive",
52
+ readonly predicate?: (query: Query<mixed>) => boolean,
53
+ |};
54
+
55
+ export class QueryCache {
56
+ readonly queries: Map<string, Query<mixed>> = new Map();
57
+
58
+ /** The entry for `hash`, if there is one. Never creates. */
59
+ get(hash: string): Query<mixed> | void {
60
+ return this.queries.get(hash);
61
+ }
62
+
63
+ /**
64
+ * The entry for `key`, creating it if it is not there.
65
+ *
66
+ * Creating is a mutation, which is why nothing on the render path calls
67
+ * this: a component reads with [`get`] and returns "nothing yet" when the
68
+ * answer is nothing yet. The entry is built in the effect that subscribes,
69
+ * where a mutation is allowed to happen and where React can undo it.
70
+ */
71
+ build(key: QueryKey, gcTime: number = DEFAULT_GC_TIME): Query<mixed> {
72
+ const hash = hashKey(key);
73
+ const existing = this.queries.get(hash);
74
+ if (existing != null) {
75
+ return existing;
76
+ }
77
+ const query = new Query<mixed>(this, key, gcTime);
78
+ this.queries.set(hash, query);
79
+ return query;
80
+ }
81
+
82
+ /**
83
+ * Drop `query`, if it is still the entry under its key.
84
+ *
85
+ * The identity check matters: garbage collection is scheduled on a timer,
86
+ * and by the time it fires the entry may already have been removed and
87
+ * rebuilt by a remount. Deleting by hash alone would then collect the *new*
88
+ * entry — with its observers, its data and its request — because an old
89
+ * timer said so.
90
+ */
91
+ remove(query: Query<mixed>): void {
92
+ if (this.queries.get(query.hash) === query) {
93
+ this.queries.delete(query.hash);
94
+ }
95
+ query.destroy();
96
+ }
97
+
98
+ /** Every entry the filter describes, in insertion order. */
99
+ findAll(filters?: QueryFilters): Array<Query<mixed>> {
100
+ const all = Array.from(this.queries.values());
101
+ if (filters == null) {
102
+ return all;
103
+ }
104
+ return all.filter((query) => matches(query, filters));
105
+ }
106
+
107
+ /** The first entry the filter describes. */
108
+ find(filters: QueryFilters): Query<mixed> | void {
109
+ return this.findAll(filters)[0];
110
+ }
111
+
112
+ /** Forget everything, cancelling anything in flight. */
113
+ clear(): void {
114
+ for (const query of Array.from(this.queries.values())) {
115
+ this.remove(query);
116
+ }
117
+ this.queries.clear();
118
+ }
119
+ }
120
+
121
+ function matches(query: Query<mixed>, filters: QueryFilters): boolean {
122
+ if (
123
+ filters.queryKey != null &&
124
+ !matchesKey(query.key, filters.queryKey, filters.exact === true)
125
+ ) {
126
+ return false;
127
+ }
128
+ const type = filters.type ?? "all";
129
+ if (type === "active" && !query.isActive()) {
130
+ return false;
131
+ }
132
+ if (type === "inactive" && query.isActive()) {
133
+ return false;
134
+ }
135
+ return filters.predicate == null || filters.predicate(query);
136
+ }
package/client.js ADDED
@@ -0,0 +1,289 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/query/client`: the cache as something you can talk to.
4
+ //
5
+ // A [`QueryClient`] is the cache plus the two things a cache is useless
6
+ // without: the application's defaults, and a vocabulary for acting on entries
7
+ // from outside React. Everything here is callable from an event handler, a
8
+ // route loader, a test, or a server render — nothing on this object needs a
9
+ // component to exist.
10
+ //
11
+ // # Why defaults live here rather than at the call site
12
+ //
13
+ // `staleTime` is an application-wide decision — how long is an answer good for
14
+ // around here — and repeating it at three hundred call sites means the three
15
+ // hundred and first is different and nobody knows why. So the client holds
16
+ // them and every option falls back to them, one level, with no merging of
17
+ // nested objects to reason about.
18
+ //
19
+ // # Why `setQueryData` takes an updater
20
+ //
21
+ // The value being replaced is the one in the cache *now*, which is not
22
+ // necessarily the one the component rendered — a background refresh may have
23
+ // landed in between. Reading it out, changing it, and writing it back would
24
+ // then silently discard that refresh. The updater form reads and writes in the
25
+ // same step, and returning `undefined` from it means "on reflection, do
26
+ // nothing", which is how an optimistic update declines to guess about an entry
27
+ // that has since been dropped.
28
+ //
29
+ // # Why invalidation refetches only what is on screen
30
+ //
31
+ // `invalidateQueries(["users"])` in an application holding a hundred cached
32
+ // users must not make a hundred requests. Every match is *marked* stale, so
33
+ // whichever of them is looked at next refetches on sight; only the ones with a
34
+ // live observer are refetched now. The distinction is what makes invalidation
35
+ // by prefix safe to use freely, which in turn is what makes it the right way
36
+ // to express "something about users changed".
37
+
38
+ import { QueryCache } from "./cache.js";
39
+ import type { QueryFilters } from "./cache.js";
40
+ import { hashKey } from "./key.js";
41
+ import type { QueryKey } from "./key.js";
42
+ import type { QueryOptions, ResolvedQueryOptions } from "./observer.js";
43
+ import { Presence } from "./presence.js";
44
+ import { DEFAULT_GC_TIME } from "./query.js";
45
+ import type { FetchContext, QueryState } from "./query.js";
46
+ import type { MutationOptions, ResolvedMutationOptions } from "./mutation.js";
47
+ import { backoffDelay } from "./retry.js";
48
+ import type { RetryDelay, RetryPolicy } from "./retry.js";
49
+
50
+ /** What a whole application decides once. */
51
+ export type QueryDefaults = {|
52
+ readonly staleTime?: number,
53
+ readonly gcTime?: number,
54
+ readonly retry?: RetryPolicy,
55
+ readonly retryDelay?: RetryDelay,
56
+ readonly refetchInterval?: number | null,
57
+ readonly refetchOnWindowFocus?: boolean,
58
+ readonly refetchOnReconnect?: boolean,
59
+ |};
60
+
61
+ export type MutationDefaults = {|
62
+ readonly retry?: RetryPolicy,
63
+ readonly retryDelay?: RetryDelay,
64
+ |};
65
+
66
+ export type QueryClientOptions = {|
67
+ readonly queries?: QueryDefaults,
68
+ readonly mutations?: MutationDefaults,
69
+ /** Replaceable so React Native and tests can drive focus themselves. */
70
+ readonly presence?: Presence,
71
+ |};
72
+
73
+ /** What `fetchQuery` needs, which is a query without anything React-shaped. */
74
+ export type FetchQueryOptions<TData> = {|
75
+ readonly queryKey: QueryKey,
76
+ readonly queryFn: (context: FetchContext<TData>) => Promise<TData>,
77
+ readonly staleTime?: number,
78
+ readonly gcTime?: number,
79
+ readonly retry?: RetryPolicy,
80
+ readonly retryDelay?: RetryDelay,
81
+ |};
82
+
83
+ /**
84
+ * Three retries with doubling backoff, five minutes of grace, fresh for no
85
+ * time at all.
86
+ *
87
+ * `staleTime: 0` is the conservative default and the surprising one: a mount
88
+ * refetches, showing the cached answer immediately and replacing it when the
89
+ * new one lands. Applications that know better should say so — `staleTime` is
90
+ * the single most valuable option on this object.
91
+ */
92
+ const QUERY_DEFAULTS = {
93
+ staleTime: 0,
94
+ gcTime: DEFAULT_GC_TIME,
95
+ retry: (3: RetryPolicy),
96
+ retryDelay: (backoffDelay: RetryDelay),
97
+ refetchInterval: null,
98
+ refetchOnWindowFocus: true,
99
+ refetchOnReconnect: true,
100
+ };
101
+
102
+ /**
103
+ * Writes are not retried by default.
104
+ *
105
+ * A failed read can be repeated because reading twice is free. A failed write
106
+ * may well have succeeded on the server and lost its answer on the way back,
107
+ * and repeating it creates the second invoice. Retrying a mutation is a
108
+ * decision about idempotency that only the caller can make.
109
+ */
110
+ const MUTATION_DEFAULTS = {
111
+ retry: (false: RetryPolicy),
112
+ retryDelay: (backoffDelay: RetryDelay),
113
+ };
114
+
115
+ export class QueryClient {
116
+ readonly cache: QueryCache = new QueryCache();
117
+ readonly presence: Presence;
118
+ readonly queryDefaults: typeof QUERY_DEFAULTS;
119
+ readonly mutationDefaults: typeof MUTATION_DEFAULTS;
120
+
121
+ constructor(options?: QueryClientOptions) {
122
+ this.presence = options?.presence ?? new Presence();
123
+ this.queryDefaults = { ...QUERY_DEFAULTS, ...stripUndefined(options?.queries) };
124
+ this.mutationDefaults = { ...MUTATION_DEFAULTS, ...stripUndefined(options?.mutations) };
125
+ }
126
+
127
+ /** The options with this client's defaults filled in. A pure function. */
128
+ resolveQuery<TData, TSelected>(
129
+ options: QueryOptions<TData, TSelected>,
130
+ ): ResolvedQueryOptions<TData, TSelected> {
131
+ const defaults = this.queryDefaults;
132
+ return {
133
+ queryKey: options.queryKey,
134
+ queryFn: options.queryFn,
135
+ enabled: options.enabled ?? true,
136
+ staleTime: options.staleTime ?? defaults.staleTime,
137
+ gcTime: options.gcTime ?? defaults.gcTime,
138
+ retry: options.retry ?? defaults.retry,
139
+ retryDelay: options.retryDelay ?? defaults.retryDelay,
140
+ select: options.select,
141
+ placeholderData: options.placeholderData,
142
+ refetchInterval: options.refetchInterval ?? defaults.refetchInterval,
143
+ refetchOnWindowFocus: options.refetchOnWindowFocus ?? defaults.refetchOnWindowFocus,
144
+ refetchOnReconnect: options.refetchOnReconnect ?? defaults.refetchOnReconnect,
145
+ };
146
+ }
147
+
148
+ resolveMutation<TVariables, TData, TContext>(
149
+ options: MutationOptions<TVariables, TData, TContext>,
150
+ ): ResolvedMutationOptions<TVariables, TData, TContext> {
151
+ return {
152
+ ...options,
153
+ retry: options.retry ?? this.mutationDefaults.retry,
154
+ retryDelay: options.retryDelay ?? this.mutationDefaults.retryDelay,
155
+ };
156
+ }
157
+
158
+ /**
159
+ * What is cached for `key`, without asking for it.
160
+ *
161
+ * `mixed`, not a generic the caller instantiates. A key is an array of
162
+ * strings and numbers; it carries no type, and a signature that pretended
163
+ * otherwise would be an unchecked cast wearing a type parameter. Narrow it
164
+ * where you read it, with the same schema that validated the response.
165
+ */
166
+ getQueryData(key: QueryKey): mixed {
167
+ return this.cache.get(hashKey(key))?.state.data;
168
+ }
169
+
170
+ /** Everything known about `key`, including the timestamps a result omits. */
171
+ getQueryState(key: QueryKey): QueryState<mixed> | void {
172
+ return this.cache.get(hashKey(key))?.state;
173
+ }
174
+
175
+ /**
176
+ * Put a value in, creating the entry if there is none.
177
+ *
178
+ * The write goes through structural sharing, so setting data that is deeply
179
+ * equal to what is there changes no identity and re-renders nothing.
180
+ */
181
+ setQueryData(key: QueryKey, updater: mixed | ((previous: mixed) => mixed)): mixed {
182
+ const query = this.cache.build(key, this.queryDefaults.gcTime);
183
+ const next =
184
+ typeof updater === "function" ? (updater as $FlowFixMe)(query.state.data) : updater;
185
+ if (next === undefined) {
186
+ return query.state.data;
187
+ }
188
+ return query.setData(next);
189
+ }
190
+
191
+ /**
192
+ * Fetch `key` now unless it is fresh, and hand back the answer.
193
+ *
194
+ * This is the imperative half of `useQuery`: the same cache, the same
195
+ * de-duplication, no component. A route loader that calls this before
196
+ * navigating hands the component a cache that is already warm, and the
197
+ * component's own mount finds nothing to do.
198
+ */
199
+ fetchQuery<TData>(options: FetchQueryOptions<TData>): Promise<mixed> {
200
+ const resolved = this.resolveQuery(options as $FlowFixMe);
201
+ const query = this.cache.build(resolved.queryKey, resolved.gcTime);
202
+ if (!query.isStale(resolved.staleTime)) {
203
+ return Promise.resolve(query.state.data);
204
+ }
205
+ return query.fetch((context) => resolved.queryFn(context as $FlowFixMe), {
206
+ retry: resolved.retry,
207
+ retryDelay: resolved.retryDelay,
208
+ cancelRefetch: false,
209
+ });
210
+ }
211
+
212
+ /**
213
+ * The same thing, for when the answer is not wanted here.
214
+ *
215
+ * Never rejects: a prefetch is an optimisation, and an optimisation that can
216
+ * take down the page that started it is not one. The failure is recorded on
217
+ * the entry, where the component that eventually reads it will find it.
218
+ */
219
+ prefetchQuery<TData>(options: FetchQueryOptions<TData>): Promise<void> {
220
+ return this.fetchQuery(options).then(ignore, ignore);
221
+ }
222
+
223
+ /**
224
+ * Mark matching entries stale, and refetch the ones being watched.
225
+ *
226
+ * The default filter is "everything", which is the right thing after signing
227
+ * in or out.
228
+ */
229
+ invalidateQueries(filters?: QueryFilters): Promise<void> {
230
+ for (const query of this.cache.findAll(filters)) {
231
+ query.invalidate();
232
+ }
233
+ return this.refetchQueries({ ...(filters ?? {}), type: "active" });
234
+ }
235
+
236
+ /** Refetch matching entries now. Never rejects. */
237
+ refetchQueries(filters?: QueryFilters): Promise<void> {
238
+ const matched = this.cache.findAll(filters);
239
+ return Promise.all(matched.map((query) => query.refetch())).then(ignore);
240
+ }
241
+
242
+ /**
243
+ * Abort matching requests and put their entries back as they were.
244
+ *
245
+ * The call an optimistic update makes first: a refetch already in flight
246
+ * would otherwise land after the optimistic write and replace it with the
247
+ * server's previous answer, which looks exactly like the mutation being
248
+ * undone at random.
249
+ */
250
+ cancelQueries(filters?: QueryFilters): Promise<void> {
251
+ for (const query of this.cache.findAll(filters)) {
252
+ query.cancel({ revert: true });
253
+ }
254
+ return Promise.resolve();
255
+ }
256
+
257
+ /** Drop matching entries entirely, cancelling anything in flight. */
258
+ removeQueries(filters?: QueryFilters): void {
259
+ for (const query of this.cache.findAll(filters)) {
260
+ this.cache.remove(query);
261
+ }
262
+ }
263
+
264
+ /** How many matching requests are in flight, for a global progress bar. */
265
+ isFetching(filters?: QueryFilters): number {
266
+ return this.cache.findAll(filters).filter((query) => query.state.fetchStatus === "fetching")
267
+ .length;
268
+ }
269
+
270
+ /** Forget everything. A test's `afterEach`, or a sign-out. */
271
+ clear(): void {
272
+ this.cache.clear();
273
+ }
274
+ }
275
+
276
+ function stripUndefined<T: { +[string]: mixed }>(source: T | void): { [string]: mixed } {
277
+ const out: { [string]: mixed } = {};
278
+ if (source == null) {
279
+ return out;
280
+ }
281
+ for (const name of Object.keys(source)) {
282
+ if (source[name] !== undefined) {
283
+ out[name] = source[name];
284
+ }
285
+ }
286
+ return out;
287
+ }
288
+
289
+ function ignore(): void {}
package/index.js ADDED
@@ -0,0 +1,158 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/query`: asking for the same thing twice should cost once.
4
+ //
5
+ // Server state is not application state. It is owned somewhere else, it goes
6
+ // out of date without anybody touching it, several components want the same
7
+ // piece of it at the same moment, and every read can fail. A `useState` beside
8
+ // a `useEffect` models none of that, which is why every application that
9
+ // starts with one ends up rebuilding this package badly.
10
+ //
11
+ // ```js
12
+ // const client = new QueryClient({ queries: { staleTime: 30_000 } });
13
+ //
14
+ // component Profile(id: string) {
15
+ // const { data, isPending, error } = useQuery({
16
+ // queryKey: ["user", id],
17
+ // queryFn: ({ signal }) => fetch(`/users/${id}`, { signal }).then((r) => r.json()),
18
+ // });
19
+ // if (isPending) return <Spinner />;
20
+ // if (error != null) return <Failure error={error} />;
21
+ // return <Card user={data} />;
22
+ // }
23
+ // ```
24
+ //
25
+ // A header and a sidebar rendering that make one request. Navigating away and
26
+ // back shows the profile immediately and refreshes it behind. A response that
27
+ // is byte-identical to the cached one re-renders nothing at all.
28
+ //
29
+ // # The five decisions everything else follows from
30
+ //
31
+ // **A key is a value, not a reference.** `["user", id]` written in two files
32
+ // is one entry, because keys are compared by their contents. `key.js`.
33
+ //
34
+ // **An unchanged answer is the same object.** A response is merged into the
35
+ // cached one so that every deeply-equal subtree keeps its identity. That is
36
+ // what makes "did anything change" an `Object.is`, which is what makes a poll
37
+ // over unchanging data free. `structural.js`.
38
+ //
39
+ // **One entry, one request in flight.** Two components mounting in the same
40
+ // tick join one request — not only for the saving, but because two requests
41
+ // can answer in either order and the two components would then disagree.
42
+ // `query.js`.
43
+ //
44
+ // **Stale is not the same as absent.** A cached answer is shown while it is
45
+ // refreshed, so navigating back does not flash a spinner over data that is
46
+ // already on screen. `staleTime` is the whole of the policy. `query.js`.
47
+ //
48
+ // **The snapshot is stable when nothing changed.** A component reads through
49
+ // `useSyncExternalStore`, which re-renders only when the snapshot's identity
50
+ // changes; the observer returns the *same* result object whenever every field
51
+ // in it is unchanged. `observer.js`.
52
+ //
53
+ // # How the package is laid out
54
+ //
55
+ // Eleven modules beside this one, each named after the thing it decides. Nothing
56
+ // is under an `internal/`: every one of them is a reasonable thing to import
57
+ // on purpose, and hiding them would have cost a reader a directory hop to
58
+ // reach the first line of code without making anything safer.
59
+ //
60
+ // The value-level leaves, which have no state and no React in them:
61
+ //
62
+ // - `key.js` — when two requests are the same request, and what a prefix
63
+ // filter matches.
64
+ // - `structural.js` — when two answers are the same answer, and how to keep
65
+ // the old references for the parts that are.
66
+ // - `retry.js` — whether to try again, how long to wait, and how to stop.
67
+ //
68
+ // The cache, which is a plain object graph that works with nothing rendering:
69
+ //
70
+ // - `query.js` — one key: its state machine, its one in-flight request, its
71
+ // abort signal and its collection timer.
72
+ // - `cache.js` — every entry, and the filter vocabulary that says which ones
73
+ // an operation means.
74
+ // - `mutation.js` — the write side: optimistic context, rollback, and why a
75
+ // write is never cancelled.
76
+ // - `presence.js` — the tab came back; the network came back.
77
+ // - `client.js` — the cache with the application's defaults on it, and the
78
+ // imperative surface: `setQueryData`, `invalidateQueries`, `prefetchQuery`,
79
+ // `cancelQueries`.
80
+ //
81
+ // The React edge:
82
+ //
83
+ // - `observer.js` — one component's options over one entry: the narrowing, the
84
+ // placeholder, and the reference-stable snapshot that decides whether a
85
+ // render happens at all.
86
+ // - `infinite.js` — what a paged query changes about an ordinary one, which is
87
+ // only how the entry is filled.
88
+ // - `react.js` — the provider and the four hooks, each a
89
+ // `useSyncExternalStore` and almost nothing else.
90
+ //
91
+ // `observer.js` is the one to read first if something re-renders when it
92
+ // should not, and `query.js` if something fetches when it should not.
93
+ //
94
+ // # What is not here, and where it went instead
95
+ //
96
+ // `initialData` is absent: `client.setQueryData` before rendering is the same
97
+ // thing with fewer rules, and the difference between initial data and
98
+ // placeholder data is a distinction this package would rather not make people
99
+ // learn. `placeholderData` is here, because "show the previous page while the
100
+ // next one loads" has no other spelling.
101
+ //
102
+ // Suspense, offline request queues, and `client.fetchInfiniteQuery` are named
103
+ // as out of scope in the modules that would own them, with the reason in each
104
+ // case. A single request in flight for `useAsync` — one call, no cache — is
105
+ // `@uniflowed/hooks`.
106
+
107
+ export type { QueryFilters } from "./cache.js";
108
+ export type {
109
+ FetchQueryOptions,
110
+ MutationDefaults,
111
+ QueryClientOptions,
112
+ QueryDefaults,
113
+ } from "./client.js";
114
+ export type {
115
+ InfiniteData,
116
+ InfinitePageContext,
117
+ InfiniteQueryOptions,
118
+ InfiniteQueryResult,
119
+ PageParamFn,
120
+ } from "./infinite.js";
121
+ export type { QueryKey } from "./key.js";
122
+ export type {
123
+ MutationCallbacks,
124
+ MutationOptions,
125
+ MutationResult,
126
+ MutationState,
127
+ MutationStatus,
128
+ } from "./mutation.js";
129
+ export type { QueryOptions, QueryResult, ResolvedQueryOptions } from "./observer.js";
130
+ export type { PresenceEvent } from "./presence.js";
131
+ export type {
132
+ FetchContext,
133
+ FetchDirection,
134
+ FetchStatus,
135
+ Fetcher,
136
+ QueryState,
137
+ QueryStatus,
138
+ QueryWatcher,
139
+ } from "./query.js";
140
+ export type { RetryDelay, RetryPolicy } from "./retry.js";
141
+
142
+ export { QueryCache } from "./cache.js";
143
+ export { QueryClient } from "./client.js";
144
+ export { InfiniteQueryObserver, infinitePages } from "./infinite.js";
145
+ export { hashKey, matchesKey } from "./key.js";
146
+ export { Mutation } from "./mutation.js";
147
+ export { QueryObserver } from "./observer.js";
148
+ export { Presence } from "./presence.js";
149
+ export { DEFAULT_GC_TIME, Query } from "./query.js";
150
+ export {
151
+ QueryClientProvider,
152
+ useInfiniteQuery,
153
+ useMutation,
154
+ useQuery,
155
+ useQueryClient,
156
+ } from "./react.js";
157
+ export { CancelledError, backoffDelay } from "./retry.js";
158
+ export { structuralShare } from "./structural.js";