@raf-query/core 0.0.12 → 0.0.14
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/CHANGELOG.md +13 -1
- package/README.md +37 -23
- package/dist/client.d.mts +5 -4
- package/dist/client.mjs +9 -2
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +2 -1
- package/dist/observer.d.mts +82 -0
- package/dist/observer.mjs +167 -0
- package/dist/options.d.mts +17 -5
- package/dist/query.d.mts +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.0.14](https://github.com/KATT/raf-query/compare/0.0.13...0.0.14) (2026-10-12)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* tracked query results via a core QueryObserver, like TanStack Query ([#13](https://github.com/KATT/raf-query/issues/13)) ([7b99b47](https://github.com/KATT/raf-query/commit/7b99b4704be110c999b41277afd5dac4c515f45f))
|
|
8
|
+
|
|
9
|
+
## [0.0.13](https://github.com/KATT/raf-query/compare/0.0.12...0.0.13) (2026-10-12)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- useQuery returns data for handles from ensure ([#16](https://github.com/KATT/raf-query/issues/16)) ([a50b8fd](https://github.com/KATT/raf-query/commit/a50b8fdc51b952341377d4b55f25ebb6b21d39e1))
|
|
14
|
+
|
|
3
15
|
## [0.0.12](https://github.com/KATT/raf-query/compare/0.0.11...0.0.12) (2026-10-12)
|
|
4
16
|
|
|
5
|
-
|
|
17
|
+
- **examples:** move the shared db and ui packages into examples/.shared ([#14](https://github.com/KATT/raf-query/issues/14)) ([fccdc7b](https://github.com/KATT/raf-query/commit/fccdc7bb8075ce2fb0cdbe07b26b36f2ace2bd9c))
|
|
6
18
|
|
|
7
19
|
## [0.0.11](https://github.com/KATT/raf-query/compare/0.0.10...0.0.11) (2026-10-12)
|
|
8
20
|
|
package/README.md
CHANGED
|
@@ -72,11 +72,11 @@ The factory is the only place a query is declared paged. Every other API — `pr
|
|
|
72
72
|
|
|
73
73
|
All three verbs live on the query client and accept a factory plus its input.
|
|
74
74
|
|
|
75
|
-
| Call | Returns
|
|
76
|
-
| --------------------------------- |
|
|
77
|
-
| `qc.prefetch(factory, input)` | `QueryHandle`
|
|
78
|
-
| `await qc.ensure(factory, input)` | `Promise<
|
|
79
|
-
| `await qc.fetch(factory, input)` | `Promise<Data>`
|
|
75
|
+
| Call | Returns | Blocks? | Use for |
|
|
76
|
+
| --------------------------------- | ----------------------------- | ------- | ---------------------------------------------------------------------- |
|
|
77
|
+
| `qc.prefetch(factory, input)` | `QueryHandle` | No | Data the route can render without; it streams in |
|
|
78
|
+
| `await qc.ensure(factory, input)` | `Promise<EnsuredQueryHandle>` | Yes | Data the route must not render without; `useQuery` returns it directly |
|
|
79
|
+
| `await qc.fetch(factory, input)` | `Promise<Data>` | Yes | Rendering in a server component, or building another input |
|
|
80
80
|
|
|
81
81
|
```tsx
|
|
82
82
|
// TanStack Start
|
|
@@ -125,9 +125,16 @@ interface QueryHandle<TFactory> {
|
|
|
125
125
|
readonly key: QueryKey; // ["post", "hello"]
|
|
126
126
|
readonly input: InputOf<TFactory>; // { slug: "hello" }
|
|
127
127
|
}
|
|
128
|
+
|
|
129
|
+
// From `ensure`: the route waited for the data.
|
|
130
|
+
interface EnsuredQueryHandle<TFactory> {
|
|
131
|
+
readonly key: QueryKey;
|
|
132
|
+
readonly input: InputOf<TFactory>;
|
|
133
|
+
readonly ensured: true;
|
|
134
|
+
}
|
|
128
135
|
```
|
|
129
136
|
|
|
130
|
-
Handles hold no data and no functions. Data reaches the client cache through the [hydration stream](#ssr-and-hydration), so loader data stays tiny and each query is serialized once. The loader's return value lists exactly which queries a route started, and
|
|
137
|
+
Handles hold no data and no functions. Data reaches the client cache through the [hydration stream](#ssr-and-hydration), so loader data stays tiny and each query is serialized once. The loader's return value lists exactly which queries a route started, and its types show which ones the route waited for: `useQuery` returns the data for an `EnsuredQueryHandle` and a promise for a `QueryHandle`. The two aren't assignable to each other, so type a prop with the one its loader returns.
|
|
131
138
|
|
|
132
139
|
## Reading queries
|
|
133
140
|
|
|
@@ -135,17 +142,20 @@ Handles hold no data and no functions. Data reaches the client cache through the
|
|
|
135
142
|
|
|
136
143
|
```ts
|
|
137
144
|
const [promise, controls] = useQuery(factory, handle, options?);
|
|
145
|
+
const [data, controls] = useQuery(factory, ensuredHandle, options?);
|
|
138
146
|
```
|
|
139
147
|
|
|
140
148
|
`options` takes [`whilePending`](#when-the-url-changes) and per-read refetch settings (`refetchInterval`, `refetchOnWindowFocus`, `refetchOnReconnect`). There is no option to change the input; the handle decides what is read.
|
|
141
149
|
|
|
142
150
|
`promise` is a stable `Promise<Data>` owned by the cache. Unwrap it with `use()`. When the cache already has data, the promise is pre-resolved and `use()` returns synchronously without suspending.
|
|
143
151
|
|
|
152
|
+
A handle from `ensure` had its data loaded before the route rendered, so `useQuery` returns the data itself. If the entry has lost that data since (it was garbage-collected while the router kept the loader data, or `reset`), the hook suspends until it is refetched.
|
|
153
|
+
|
|
144
154
|
```tsx
|
|
145
155
|
function PostPage() {
|
|
146
156
|
const loaderData = Route.useLoaderData();
|
|
147
|
-
|
|
148
|
-
const post =
|
|
157
|
+
// `post` came from `qc.ensure`, `comments` from `qc.prefetch`.
|
|
158
|
+
const [post, postState] = useQuery(postQuery, loaderData.post);
|
|
149
159
|
|
|
150
160
|
return (
|
|
151
161
|
<article>
|
|
@@ -179,7 +189,7 @@ function PostPage() {
|
|
|
179
189
|
| `hash` | Stable string form of the handle's key, for `key` props on Suspense boundaries |
|
|
180
190
|
| `refetch()` | Refetch now. Returns a promise, so it can be awaited inside a transition or action |
|
|
181
191
|
|
|
182
|
-
When the cache updates — refetch, invalidation, `setData` — the hook returns a new, pre-resolved promise and the component re-renders without suspending. New data is structurally shared with the old: objects and arrays that are deeply equal keep their identity, and a refetch that returns equal data keeps the same promise, so nothing re-renders. When the **first** load fails, the promise rejects and `use()` throws to the nearest error boundary. Resetting that boundary refetches the query.
|
|
192
|
+
When the cache updates — refetch, invalidation, `setData` — the hook returns a new, pre-resolved promise and the component re-renders without suspending. New data is structurally shared with the old: objects and arrays that are deeply equal keep their identity, and a refetch that returns equal data keeps the same promise, so nothing re-renders. Controls are tracked: reading `controls.isFetching` subscribes the component to `isFetching`, and a change to a control it never read doesn't re-render it. When the **first** load fails, the promise rejects and `use()` throws to the nearest error boundary. Resetting that boundary refetches the query.
|
|
183
193
|
|
|
184
194
|
Queries do not refetch on mount. They refetch when a loader starts them again, when invalidated, on window focus or reconnect while stale, on `refetch()`, or on `refetchInterval`.
|
|
185
195
|
|
|
@@ -267,6 +277,7 @@ Either way, an input that's already cached renders immediately. A component that
|
|
|
267
277
|
To show a skeleton instead, for instance when moving between unrelated posts, give the Suspense boundary the query's key. React treats it as new content and shows its fallback, even inside a transition:
|
|
268
278
|
|
|
269
279
|
```tsx
|
|
280
|
+
// This loader prefetches the post instead of ensuring it.
|
|
270
281
|
const [postPromise, postState] = useQuery(postQuery, loaderData.post);
|
|
271
282
|
|
|
272
283
|
return (
|
|
@@ -282,10 +293,10 @@ In Next.js the same rules apply: read `searchParams` in the page, `prefetch` wit
|
|
|
282
293
|
|
|
283
294
|
### Passing promises down
|
|
284
295
|
|
|
285
|
-
A component can call the hook and hand the promise to a child, so only the child suspends:
|
|
296
|
+
A component can call the hook with a prefetched handle and hand the promise to a child, so only the child suspends:
|
|
286
297
|
|
|
287
298
|
```tsx
|
|
288
|
-
const [postPromise] = useQuery(postQuery, loaderData.post);
|
|
299
|
+
const [postPromise] = useQuery(postQuery, loaderData.post); // from `qc.prefetch`
|
|
289
300
|
return (
|
|
290
301
|
<Suspense fallback={<Spinner />}>
|
|
291
302
|
<PostBody post={postPromise} />
|
|
@@ -293,7 +304,7 @@ return (
|
|
|
293
304
|
);
|
|
294
305
|
```
|
|
295
306
|
|
|
296
|
-
|
|
307
|
+
A component re-renders when the promise changes, and when a control it reads changes. Controls it never reads, like `isFetching` here, don't re-render it, so a parent that only passes the promise down stays put during background refetches.
|
|
297
308
|
|
|
298
309
|
## Render-time inputs: `useWaterfall`
|
|
299
310
|
|
|
@@ -435,6 +446,7 @@ export default async function Page(props: {
|
|
|
435
446
|
- `HydrationBoundary` sends the request client's entries to the browser cache; pending ones stream as promises. Render one in each server component that starts queries for client components. Entries already sent in this request are skipped.
|
|
436
447
|
- `QueryProvider` creates the browser client, and streams queries that client components start during SSR (`useWaterfall`) into the HTML. Pass `config` for the client's options; it must be serializable.
|
|
437
448
|
- Server components that only render data can `await qc.fetch(...)` anywhere; the per-request client dedupes them.
|
|
449
|
+
- `fetch` returns data, not a handle. When a server component renders the data and also hands the query to a client component, pass `await qc.ensure(...)`: after the `fetch` it resolves from the cache, and the client component gets the data from `useQuery` without a promise.
|
|
438
450
|
- Factories can call server actions: server components call them directly, and the browser calls them on refetch. Validate their arguments, since anyone can call them.
|
|
439
451
|
|
|
440
452
|
#### Server-only queries
|
|
@@ -545,17 +557,19 @@ Query options, accepted by `queryOptions`, `infiniteQueryOptions` and `config.de
|
|
|
545
557
|
|
|
546
558
|
`@raf-query/core`:
|
|
547
559
|
|
|
548
|
-
| Export | Use
|
|
549
|
-
| ---------------------------------------------- |
|
|
550
|
-
| `QueryClient` | The cache
|
|
551
|
-
| `queryOptions(options)` | Typed options for a query factory
|
|
552
|
-
| `infiniteQueryOptions(options)` | Typed options for a paged query factory
|
|
553
|
-
| `allKeyed(record)` | Await a record of promises, keeping keys
|
|
554
|
-
| `dehydrate(client)` / `hydrate(client, state)` | Low-level cache transfer used by the integrations
|
|
555
|
-
| `hashKey(key)`, `matchesKey(key, prefix)` | Key helpers
|
|
556
|
-
| `
|
|
557
|
-
| `
|
|
558
|
-
| `
|
|
560
|
+
| Export | Use |
|
|
561
|
+
| ---------------------------------------------- | ---------------------------------------------------------------- |
|
|
562
|
+
| `QueryClient` | The cache |
|
|
563
|
+
| `queryOptions(options)` | Typed options for a query factory |
|
|
564
|
+
| `infiniteQueryOptions(options)` | Typed options for a paged query factory |
|
|
565
|
+
| `allKeyed(record)` | Await a record of promises, keeping keys |
|
|
566
|
+
| `dehydrate(client)` / `hydrate(client, state)` | Low-level cache transfer used by the integrations |
|
|
567
|
+
| `hashKey(key)`, `matchesKey(key, prefix)` | Key helpers |
|
|
568
|
+
| `QueryObserver` | One reader's view of a query, with tracked results; for bindings |
|
|
569
|
+
| `CancelledError` | Rejection of a request cancelled by `clear()` or a restart |
|
|
570
|
+
| `QueryHandle<TFactory>` | Handle type for props |
|
|
571
|
+
| `EnsuredQueryHandle<TFactory>` | Handle type for props fed by `ensure` |
|
|
572
|
+
| `DataOf<TFactory>`, `InputOf<TFactory>` | A factory's data and input types |
|
|
559
573
|
|
|
560
574
|
`@raf-query/react` (plus all of the above):
|
|
561
575
|
|
package/dist/client.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { QueryKey } from "./key.mjs";
|
|
2
|
-
import { DataOf, InputArgs, InputOf, LoadOptions, QueryBehavior, QueryFactory, QueryHandle } from "./options.mjs";
|
|
2
|
+
import { DataOf, EnsuredQueryHandle, InputArgs, InputOf, LoadOptions, QueryBehavior, QueryFactory, QueryHandle } from "./options.mjs";
|
|
3
3
|
import { Query, QueryState } from "./query.mjs";
|
|
4
4
|
import { Effect, Stream } from "effect";
|
|
5
5
|
//#region src/client.d.ts
|
|
@@ -36,7 +36,7 @@ export type LoadArgs<TFactory extends QueryFactory> = InputArgs<TFactory> | [inp
|
|
|
36
36
|
export type Updater<TData> = TData | ((previous: TData | undefined) => TData);
|
|
37
37
|
/** Effect-returning twins of the client's async methods. */
|
|
38
38
|
export interface QueryClientEffect {
|
|
39
|
-
ensure<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Effect.Effect<
|
|
39
|
+
ensure<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Effect.Effect<EnsuredQueryHandle<TFactory>, unknown>;
|
|
40
40
|
fetch<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Effect.Effect<DataOf<TFactory>, unknown>;
|
|
41
41
|
invalidate<TFactory extends QueryFactory>(factory: TFactory, ...args: InputArgs<TFactory>): Effect.Effect<void>;
|
|
42
42
|
invalidate(filter: QueryFilter): Effect.Effect<void>;
|
|
@@ -69,9 +69,10 @@ export declare class QueryClient {
|
|
|
69
69
|
prefetch<TFactory extends QueryFactory>(factory: TFactory, ...args: InputArgs<TFactory>): QueryHandle<TFactory>;
|
|
70
70
|
/**
|
|
71
71
|
* Start a query and wait for it on a cache miss. Rejects if that request
|
|
72
|
-
* fails. Stale cached data resolves immediately and revalidates.
|
|
72
|
+
* fails. Stale cached data resolves immediately and revalidates. The
|
|
73
|
+
* handle is marked `ensured`, so `useQuery` returns its data directly.
|
|
73
74
|
*/
|
|
74
|
-
ensure<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Promise<
|
|
75
|
+
ensure<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Promise<EnsuredQueryHandle<TFactory>>;
|
|
75
76
|
/** Like `ensure`, but resolves to the data. */
|
|
76
77
|
fetch<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Promise<DataOf<TFactory>>;
|
|
77
78
|
/**
|
package/dist/client.mjs
CHANGED
|
@@ -21,6 +21,12 @@ function typed(entry) {
|
|
|
21
21
|
function handleOf(entry) {
|
|
22
22
|
return entry.handle;
|
|
23
23
|
}
|
|
24
|
+
function ensuredHandleOf(entry) {
|
|
25
|
+
return {
|
|
26
|
+
...handleOf(entry),
|
|
27
|
+
ensured: true
|
|
28
|
+
};
|
|
29
|
+
}
|
|
24
30
|
/**
|
|
25
31
|
* The query cache. Create one per request on the server and one per browser
|
|
26
32
|
* tab.
|
|
@@ -95,12 +101,13 @@ var QueryClient = class {
|
|
|
95
101
|
}
|
|
96
102
|
/**
|
|
97
103
|
* Start a query and wait for it on a cache miss. Rejects if that request
|
|
98
|
-
* fails. Stale cached data resolves immediately and revalidates.
|
|
104
|
+
* fails. Stale cached data resolves immediately and revalidates. The
|
|
105
|
+
* handle is marked `ensured`, so `useQuery` returns its data directly.
|
|
99
106
|
*/
|
|
100
107
|
async ensure(factory, ...args) {
|
|
101
108
|
const entry = this.#build(factory, args[0]);
|
|
102
109
|
await this.#load(entry, args[1]?.pages);
|
|
103
|
-
return
|
|
110
|
+
return ensuredHandleOf(entry);
|
|
104
111
|
}
|
|
105
112
|
/** Like `ensure`, but resolves to the data. */
|
|
106
113
|
async fetch(factory, ...args) {
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Settled, allKeyed } from "./all-keyed.mjs";
|
|
2
2
|
import { QueryKey, hashKey, matchesKey } from "./key.mjs";
|
|
3
|
-
import { AnyQueryOptions, DataOf, InfiniteData, InfiniteQueryOptions, InputArgs, InputOf, IsPaged, LoadOptions, PageFunctionContext, PageOf, QueryBehavior, QueryFactory, QueryFnResult, QueryFunctionContext, QueryHandle, QueryOptions, RetryPolicy, infiniteQueryOptions, queryOptions } from "./options.mjs";
|
|
4
|
-
import { CancelledError, FetchRequest, Query,
|
|
3
|
+
import { AnyQueryOptions, DataOf, EnsuredQueryHandle, InfiniteData, InfiniteQueryOptions, InputArgs, InputOf, IsPaged, LoadOptions, PageFunctionContext, PageOf, QueryBehavior, QueryFactory, QueryFnResult, QueryFunctionContext, QueryHandle, QueryOptions, RetryPolicy, infiniteQueryOptions, queryOptions } from "./options.mjs";
|
|
4
|
+
import { CancelledError, FetchRequest, Query, QueryState, SubscribeOptions } from "./query.mjs";
|
|
5
5
|
import { CacheEvent, LoadArgs, QueryClient, QueryClientConfig, QueryClientEffect, QueryFilter, Updater } from "./client.mjs";
|
|
6
6
|
import { DehydrateOptions, DehydratedQuery, DehydratedState, HydrateOptions, canDehydrate, dehydrate, dehydrateQuery, hydrate } from "./hydration.mjs";
|
|
7
|
-
|
|
7
|
+
import { QueryObserver, QueryObserverOptions, QueryObserverResult } from "./observer.mjs";
|
|
8
|
+
export { type AnyQueryOptions, type CacheEvent, CancelledError, type DataOf, type DehydrateOptions, type DehydratedQuery, type DehydratedState, type EnsuredQueryHandle, type FetchRequest, type HydrateOptions, type InfiniteData, type InfiniteQueryOptions, type InputArgs, type InputOf, type IsPaged, type LoadArgs, type LoadOptions, type PageFunctionContext, type PageOf, type Query, type QueryBehavior, QueryClient, type QueryClientConfig, type QueryClientEffect, type QueryFactory, type QueryFilter, type QueryFnResult, type QueryFunctionContext, type QueryHandle, type QueryKey, QueryObserver, type QueryObserverOptions, type QueryObserverResult, type QueryOptions, type QueryState, type RetryPolicy, type Settled, type SubscribeOptions, type Updater, allKeyed, canDehydrate, dehydrate, dehydrateQuery, hashKey, hydrate, infiniteQueryOptions, matchesKey, queryOptions };
|
package/dist/index.mjs
CHANGED
|
@@ -4,4 +4,5 @@ import { infiniteQueryOptions, queryOptions } from "./options.mjs";
|
|
|
4
4
|
import { CancelledError } from "./query.mjs";
|
|
5
5
|
import { QueryClient } from "./client.mjs";
|
|
6
6
|
import { canDehydrate, dehydrate, dehydrateQuery, hydrate } from "./hydration.mjs";
|
|
7
|
-
|
|
7
|
+
import { QueryObserver } from "./observer.mjs";
|
|
8
|
+
export { CancelledError, QueryClient, QueryObserver, allKeyed, canDehydrate, dehydrate, dehydrateQuery, hashKey, hydrate, infiniteQueryOptions, matchesKey, queryOptions };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { QueryKey } from "./key.mjs";
|
|
2
|
+
import { QueryFactory } from "./options.mjs";
|
|
3
|
+
import { Query } from "./query.mjs";
|
|
4
|
+
//#region src/observer.d.ts
|
|
5
|
+
/** How one reader observes its query. */
|
|
6
|
+
export interface QueryObserverOptions {
|
|
7
|
+
/** Refetch on this interval (ms) while subscribed and the page is visible. */
|
|
8
|
+
readonly refetchInterval?: number;
|
|
9
|
+
/** Refetch on reconnect while stale. Default: the query's option, else `true`. */
|
|
10
|
+
readonly refetchOnReconnect?: boolean;
|
|
11
|
+
/** Refetch on window focus while stale. Default: the query's option, else `true`. */
|
|
12
|
+
readonly refetchOnWindowFocus?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* What to show while the query has no data yet:
|
|
15
|
+
* - `"suspend"` (default): the result's promise is the query's pending one.
|
|
16
|
+
* - `"previous"`: keep the last observed query's data, with `isPrevious`.
|
|
17
|
+
*/
|
|
18
|
+
readonly whilePending?: "previous" | "suspend";
|
|
19
|
+
}
|
|
20
|
+
/** What a reader sees of its query. Replaced, never mutated, on every change. */
|
|
21
|
+
export interface QueryObserverResult<TData = unknown> {
|
|
22
|
+
/** Error of the last failed request. Existing data stays readable. */
|
|
23
|
+
readonly error: unknown;
|
|
24
|
+
fetchNextPage(): Promise<void>;
|
|
25
|
+
fetchPreviousPage(): Promise<void>;
|
|
26
|
+
/** Stable string form of the observed query's key. */
|
|
27
|
+
readonly hash: string;
|
|
28
|
+
readonly hasNextPage: boolean;
|
|
29
|
+
readonly hasPreviousPage: boolean;
|
|
30
|
+
/** Input of the data in `promise`; differs from the query's while `isPrevious`. */
|
|
31
|
+
readonly input: unknown;
|
|
32
|
+
/** A request is in flight: first load, new input, or background refetch. */
|
|
33
|
+
readonly isFetching: boolean;
|
|
34
|
+
readonly isFetchingNextPage: boolean;
|
|
35
|
+
readonly isFetchingPreviousPage: boolean;
|
|
36
|
+
/** No data yet for the observed query. */
|
|
37
|
+
readonly isPending: boolean;
|
|
38
|
+
/** `promise` holds a previous query's data (`whilePending: "previous"`). */
|
|
39
|
+
readonly isPrevious: boolean;
|
|
40
|
+
/** The data is older than `staleTime`, or was invalidated. */
|
|
41
|
+
readonly isStale: boolean;
|
|
42
|
+
/** Key of the data in `promise`; differs from the query's while `isPrevious`. */
|
|
43
|
+
readonly key: QueryKey;
|
|
44
|
+
/** Settles with the data to show; see `QueryState.promise`. */
|
|
45
|
+
readonly promise: Promise<TData>;
|
|
46
|
+
refetch(): Promise<void>;
|
|
47
|
+
/** When the data in `promise` was fetched (ms since epoch); `0` before that. */
|
|
48
|
+
readonly updatedAt: number;
|
|
49
|
+
}
|
|
50
|
+
type ResultKey = keyof QueryObserverResult;
|
|
51
|
+
/**
|
|
52
|
+
* One reader of one query at a time, like TanStack Query's `QueryObserver`:
|
|
53
|
+
* it derives the reader's result from the query's state, keeps a previous
|
|
54
|
+
* query's data for `whilePending: "previous"`, runs `refetchInterval`, and
|
|
55
|
+
* notifies its listeners only when a result property they read changed
|
|
56
|
+
* (see {@link QueryObserver.trackResult}). Bindings create one per hook.
|
|
57
|
+
*/
|
|
58
|
+
export declare class QueryObserver<TData = unknown, TFactory extends QueryFactory = QueryFactory> {
|
|
59
|
+
#private;
|
|
60
|
+
constructor(query: Query<TData, TFactory>, options?: QueryObserverOptions);
|
|
61
|
+
/**
|
|
62
|
+
* The result for `query` with `options` right now, without switching to
|
|
63
|
+
* it. Bindings call this during render, before `setOptions` commits.
|
|
64
|
+
*/
|
|
65
|
+
getOptimisticResult(query: Query<TData, TFactory>, options: QueryObserverOptions): QueryObserverResult<TData>;
|
|
66
|
+
getCurrentResult: () => QueryObserverResult<TData>;
|
|
67
|
+
/** Observe `query` with `options` from now on. */
|
|
68
|
+
setOptions(query: Query<TData, TFactory>, options: QueryObserverOptions): void;
|
|
69
|
+
/** Listen for result changes. The first listener subscribes to the query. */
|
|
70
|
+
subscribe: (listener: () => void) => (() => void);
|
|
71
|
+
/**
|
|
72
|
+
* Wrap `result` so that reading a property marks it tracked. Once any
|
|
73
|
+
* property is tracked, listeners are only notified when a tracked one
|
|
74
|
+
* changes.
|
|
75
|
+
*/
|
|
76
|
+
trackResult<TResult extends QueryObserverResult<TData>>(result: TResult): TResult;
|
|
77
|
+
/** Mark `name` as read, for bindings that track access themselves. */
|
|
78
|
+
trackProp(name: ResultKey): void;
|
|
79
|
+
/** Recompute the result, and notify listeners if a tracked property changed. */
|
|
80
|
+
updateResult(): void;
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
//#region src/observer.ts
|
|
2
|
+
const resultKeys = {
|
|
3
|
+
error: true,
|
|
4
|
+
fetchNextPage: true,
|
|
5
|
+
fetchPreviousPage: true,
|
|
6
|
+
hash: true,
|
|
7
|
+
hasNextPage: true,
|
|
8
|
+
hasPreviousPage: true,
|
|
9
|
+
input: true,
|
|
10
|
+
isFetching: true,
|
|
11
|
+
isFetchingNextPage: true,
|
|
12
|
+
isFetchingPreviousPage: true,
|
|
13
|
+
isPending: true,
|
|
14
|
+
isPrevious: true,
|
|
15
|
+
isStale: true,
|
|
16
|
+
key: true,
|
|
17
|
+
promise: true,
|
|
18
|
+
refetch: true,
|
|
19
|
+
updatedAt: true
|
|
20
|
+
};
|
|
21
|
+
function isResultKey(name) {
|
|
22
|
+
return typeof name === "string" && Object.hasOwn(resultKeys, name);
|
|
23
|
+
}
|
|
24
|
+
function shallowEqual(a, b) {
|
|
25
|
+
for (const name of Object.keys(resultKeys)) if (isResultKey(name) && a[name] !== b[name]) return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One reader of one query at a time, like TanStack Query's `QueryObserver`:
|
|
30
|
+
* it derives the reader's result from the query's state, keeps a previous
|
|
31
|
+
* query's data for `whilePending: "previous"`, runs `refetchInterval`, and
|
|
32
|
+
* notifies its listeners only when a result property they read changed
|
|
33
|
+
* (see {@link QueryObserver.trackResult}). Bindings create one per hook.
|
|
34
|
+
*/
|
|
35
|
+
var QueryObserver = class {
|
|
36
|
+
#current;
|
|
37
|
+
#interval;
|
|
38
|
+
#lastWithData;
|
|
39
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
40
|
+
#options;
|
|
41
|
+
#query;
|
|
42
|
+
#trackedProps = /* @__PURE__ */ new Set();
|
|
43
|
+
#unsubscribe;
|
|
44
|
+
constructor(query, options = {}) {
|
|
45
|
+
this.#query = query;
|
|
46
|
+
this.#options = options;
|
|
47
|
+
this.#current = this.#createResult(query, options);
|
|
48
|
+
this.updateResult();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The result for `query` with `options` right now, without switching to
|
|
52
|
+
* it. Bindings call this during render, before `setOptions` commits.
|
|
53
|
+
*/
|
|
54
|
+
getOptimisticResult(query, options) {
|
|
55
|
+
const result = this.#createResult(query, options);
|
|
56
|
+
if (!shallowEqual(this.#current, result)) this.#current = result;
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
getCurrentResult = () => this.#current;
|
|
60
|
+
/** Observe `query` with `options` from now on. */
|
|
61
|
+
setOptions(query, options) {
|
|
62
|
+
const previous = this.#query;
|
|
63
|
+
const previousInterval = this.#options.refetchInterval;
|
|
64
|
+
this.#query = query;
|
|
65
|
+
this.#options = options;
|
|
66
|
+
if (this.#listeners.size > 0) {
|
|
67
|
+
if (query !== previous) {
|
|
68
|
+
this.#unsubscribe?.();
|
|
69
|
+
this.#unsubscribe = this.#subscribeToQuery();
|
|
70
|
+
}
|
|
71
|
+
if (query !== previous || options.refetchInterval !== previousInterval) this.#startInterval();
|
|
72
|
+
}
|
|
73
|
+
this.updateResult();
|
|
74
|
+
}
|
|
75
|
+
/** Listen for result changes. The first listener subscribes to the query. */
|
|
76
|
+
subscribe = (listener) => {
|
|
77
|
+
this.#listeners.add(listener);
|
|
78
|
+
if (this.#listeners.size === 1) {
|
|
79
|
+
this.#unsubscribe = this.#subscribeToQuery();
|
|
80
|
+
this.#startInterval();
|
|
81
|
+
this.updateResult();
|
|
82
|
+
}
|
|
83
|
+
return () => {
|
|
84
|
+
if (!this.#listeners.delete(listener) || this.#listeners.size > 0) return;
|
|
85
|
+
this.#unsubscribe?.();
|
|
86
|
+
this.#unsubscribe = void 0;
|
|
87
|
+
clearInterval(this.#interval);
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Wrap `result` so that reading a property marks it tracked. Once any
|
|
92
|
+
* property is tracked, listeners are only notified when a tracked one
|
|
93
|
+
* changes.
|
|
94
|
+
*/
|
|
95
|
+
trackResult(result) {
|
|
96
|
+
return new Proxy(result, { get: (target, name, receiver) => {
|
|
97
|
+
if (isResultKey(name)) this.trackProp(name);
|
|
98
|
+
return Reflect.get(target, name, receiver);
|
|
99
|
+
} });
|
|
100
|
+
}
|
|
101
|
+
/** Mark `name` as read, for bindings that track access themselves. */
|
|
102
|
+
trackProp(name) {
|
|
103
|
+
this.#trackedProps.add(name);
|
|
104
|
+
}
|
|
105
|
+
/** Recompute the result, and notify listeners if a tracked property changed. */
|
|
106
|
+
updateResult() {
|
|
107
|
+
const previous = this.#current;
|
|
108
|
+
const next = this.#createResult(this.#query, this.#options);
|
|
109
|
+
if (this.#query.getState().hasData) this.#lastWithData = this.#query;
|
|
110
|
+
if (shallowEqual(previous, next)) return;
|
|
111
|
+
this.#current = next;
|
|
112
|
+
if (this.#shouldNotify(previous, next)) for (const listener of this.#listeners) listener();
|
|
113
|
+
}
|
|
114
|
+
#shouldNotify(previous, next) {
|
|
115
|
+
if (this.#trackedProps.size === 0) return true;
|
|
116
|
+
for (const name of this.#trackedProps) if (previous[name] !== next[name]) return true;
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
#subscribeToQuery() {
|
|
120
|
+
const options = () => this.#options;
|
|
121
|
+
return this.#query.subscribe(() => this.updateResult(), {
|
|
122
|
+
get refetchOnReconnect() {
|
|
123
|
+
return options().refetchOnReconnect;
|
|
124
|
+
},
|
|
125
|
+
get refetchOnWindowFocus() {
|
|
126
|
+
return options().refetchOnWindowFocus;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
#startInterval() {
|
|
131
|
+
clearInterval(this.#interval);
|
|
132
|
+
this.#interval = void 0;
|
|
133
|
+
const interval = this.#options.refetchInterval;
|
|
134
|
+
if (interval === void 0 || !Number.isFinite(interval) || interval <= 0) return;
|
|
135
|
+
this.#interval = setInterval(() => {
|
|
136
|
+
if (!("document" in globalThis) || document.visibilityState !== "hidden") this.#query.refetch();
|
|
137
|
+
}, interval);
|
|
138
|
+
}
|
|
139
|
+
#createResult(query, options) {
|
|
140
|
+
const state = query.getState();
|
|
141
|
+
const last = this.#lastWithData;
|
|
142
|
+
const previous = options.whilePending === "previous" && !state.hasData && state.status !== "error" && last !== void 0 && last.hash !== query.hash ? last : void 0;
|
|
143
|
+
const shown = previous ?? query;
|
|
144
|
+
const shownState = previous ? previous.getState() : state;
|
|
145
|
+
return {
|
|
146
|
+
error: state.error,
|
|
147
|
+
fetchNextPage: query.fetchNextPage,
|
|
148
|
+
fetchPreviousPage: query.fetchPreviousPage,
|
|
149
|
+
hash: query.hash,
|
|
150
|
+
hasNextPage: query.hasNextPage(),
|
|
151
|
+
hasPreviousPage: query.hasPreviousPage(),
|
|
152
|
+
input: shown.handle.input,
|
|
153
|
+
isFetching: state.isFetching,
|
|
154
|
+
isFetchingNextPage: state.fetchDirection === "forward",
|
|
155
|
+
isFetchingPreviousPage: state.fetchDirection === "backward",
|
|
156
|
+
isPending: !state.hasData,
|
|
157
|
+
isPrevious: previous !== void 0,
|
|
158
|
+
isStale: state.isStale,
|
|
159
|
+
key: shown.handle.key,
|
|
160
|
+
promise: shownState.promise,
|
|
161
|
+
refetch: query.refetch,
|
|
162
|
+
updatedAt: shownState.updatedAt
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
//#endregion
|
|
167
|
+
export { QueryObserver };
|
package/dist/options.d.mts
CHANGED
|
@@ -103,15 +103,27 @@ export type LoadOptions<TFactory extends QueryFactory> = IsPaged<TFactory> exten
|
|
|
103
103
|
readonly pages?: number;
|
|
104
104
|
} : never;
|
|
105
105
|
declare const factoryType: unique symbol;
|
|
106
|
-
|
|
107
|
-
* The serializable reference a loader or server component hands to the
|
|
108
|
-
* component that reads the query. It holds no data and no functions.
|
|
109
|
-
*/
|
|
110
|
-
export interface QueryHandle<TFactory extends QueryFactory = QueryFactory> {
|
|
106
|
+
interface HandleFields<TFactory extends QueryFactory> {
|
|
111
107
|
/** Type-only: ties the handle to its factory. Never present at runtime. */
|
|
112
108
|
readonly [factoryType]?: TFactory;
|
|
113
109
|
/** The factory's input; `unknown` on a handle of no particular factory. */
|
|
114
110
|
readonly input: QueryFactory extends TFactory ? unknown : InputOf<TFactory>;
|
|
115
111
|
readonly key: QueryKey;
|
|
116
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* The serializable reference a loader or server component hands to the
|
|
115
|
+
* component that reads the query. It holds no data and no functions.
|
|
116
|
+
*/
|
|
117
|
+
export interface QueryHandle<TFactory extends QueryFactory = QueryFactory> extends HandleFields<TFactory> {
|
|
118
|
+
readonly ensured?: false;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A handle from `ensure`: its data was loaded before the handle was
|
|
122
|
+
* returned, so `useQuery` returns the data rather than a promise. Not
|
|
123
|
+
* assignable to {@link QueryHandle}, so a prop typed for one can't receive
|
|
124
|
+
* the other.
|
|
125
|
+
*/
|
|
126
|
+
export interface EnsuredQueryHandle<TFactory extends QueryFactory = QueryFactory> extends HandleFields<TFactory> {
|
|
127
|
+
readonly ensured: true;
|
|
128
|
+
}
|
|
117
129
|
//#endregion
|
package/dist/query.d.mts
CHANGED
|
@@ -28,7 +28,7 @@ export interface QueryState<TData> {
|
|
|
28
28
|
readonly updatedAt: number;
|
|
29
29
|
}
|
|
30
30
|
/** Per-reader preferences, registered with {@link Query.subscribe}. */
|
|
31
|
-
export interface
|
|
31
|
+
export interface SubscribeOptions {
|
|
32
32
|
readonly refetchOnReconnect?: boolean;
|
|
33
33
|
readonly refetchOnWindowFocus?: boolean;
|
|
34
34
|
}
|
|
@@ -77,7 +77,7 @@ export interface Query<TData = unknown, TFactory extends QueryFactory = QueryFac
|
|
|
77
77
|
*/
|
|
78
78
|
reset(): Promise<void>;
|
|
79
79
|
/** Listen for state changes. Subscribed entries count as read. */
|
|
80
|
-
subscribe(listener: () => void,
|
|
80
|
+
subscribe(listener: () => void, options?: SubscribeOptions): () => void;
|
|
81
81
|
}
|
|
82
82
|
/** Thrown into a request's awaiters when the cache is cleared mid-request. */
|
|
83
83
|
export declare class CancelledError extends Error {
|