@raf-query/core 0.3.1 → 0.4.1
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 +15 -1
- package/README.md +12 -11
- package/dist/cache.d.mts +5 -1
- package/dist/cache.mjs +8 -7
- package/dist/entry.d.mts +5 -3
- package/dist/entry.mjs +8 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.1](https://github.com/KATT/raf-query/compare/0.4.0...0.4.1) (2026-10-13)
|
|
4
|
+
|
|
5
|
+
* one table for the query verbs; fetch and ensure wait alike in server components ([#64](https://github.com/KATT/raf-query/issues/64)) ([13e5549](https://github.com/KATT/raf-query/commit/13e5549e9f1b6df4fe0e8f23f79e1aef7bf94e53))
|
|
6
|
+
|
|
7
|
+
## [0.4.0](https://github.com/KATT/raf-query/compare/0.3.1...0.4.0) (2026-10-13)
|
|
8
|
+
|
|
9
|
+
### ⚠ BREAKING CHANGES
|
|
10
|
+
|
|
11
|
+
- **core:** fetch waits for stale data, as TanStack's fetchQuery does ([#63](https://github.com/KATT/raf-query/issues/63))
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
- **core:** fetch waits for stale data, as TanStack's fetchQuery does ([#63](https://github.com/KATT/raf-query/issues/63)) ([c9785d5](https://github.com/KATT/raf-query/commit/c9785d5e99c0578c1748de3330823cc0e65f2378))
|
|
16
|
+
|
|
3
17
|
## [0.3.1](https://github.com/KATT/raf-query/compare/0.3.0...0.3.1) (2026-10-13)
|
|
4
18
|
|
|
5
|
-
|
|
19
|
+
- teach avoiding dependent queries; frame prefetch vs ensure as loading UX ([#62](https://github.com/KATT/raf-query/issues/62)) ([ec62760](https://github.com/KATT/raf-query/commit/ec627609cbbee9fa4ff0cf568f69964341e27daa))
|
|
6
20
|
|
|
7
21
|
## [0.3.0](https://github.com/KATT/raf-query/compare/0.2.8...0.3.0) (2026-10-13)
|
|
8
22
|
|
package/README.md
CHANGED
|
@@ -118,13 +118,15 @@ declare module "@raf-query/react" {
|
|
|
118
118
|
|
|
119
119
|
## Starting queries
|
|
120
120
|
|
|
121
|
-
All three verbs live on the query client and accept a factory plus its input.
|
|
121
|
+
All three verbs live on the query client and accept a factory plus its input. They differ in what they return and what they wait for:
|
|
122
122
|
|
|
123
|
-
| Call
|
|
124
|
-
|
|
|
125
|
-
| `qc.prefetch(factory, input)`
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
123
|
+
| Call | Returns | Cache miss | Stale data | TanStack Query |
|
|
124
|
+
| ----------------------------- | ----------------------------- | --------------------- | --------------------------------------------- | ---------------------------------------------- |
|
|
125
|
+
| `qc.prefetch(factory, input)` | `QueryHandle` | Starts the request | Refetches in the background | `prefetchQuery`, not awaited |
|
|
126
|
+
| `qc.ensure(factory, input)` | `Promise<EnsuredQueryHandle>` | Waits for the request | Resolves at once, refetches in the background | `ensureQueryData({ revalidateIfStale: true })` |
|
|
127
|
+
| `qc.fetch(factory, input)` | `Promise<Data>` | Waits for the request | Waits for the refetch | `fetchQuery` |
|
|
128
|
+
|
|
129
|
+
Fresh cached data is reused by all three, without a request. So `prefetch` never holds anything up, `ensure` holds up only the first visit (returning to a page shows the cached data at once, then fresh data), and `fetch` always returns fresh data, at the cost of a round trip whenever the cache is stale. On the server, data counts as stale only once `invalidate` has marked it (see below).
|
|
128
130
|
|
|
129
131
|
```tsx
|
|
130
132
|
// TanStack Start
|
|
@@ -227,8 +229,8 @@ When the data really can't be reshaped (a third-party API you don't control), th
|
|
|
227
229
|
|
|
228
230
|
Behavior:
|
|
229
231
|
|
|
230
|
-
- Every verb dedupes against the cache
|
|
231
|
-
-
|
|
232
|
+
- Every verb dedupes against the cache: a request already in flight for the entry is joined, not repeated.
|
|
233
|
+
- A stale `ensure` doesn't hold up the navigation: the page renders the cached data and re-renders when the background refetch lands. A stale `fetch` in a loader would, so loaders use `ensure`.
|
|
232
234
|
- Loaders run on every navigation (TanStack Router reruns them when its own `staleTime` has passed, `0` by default), so returning to a page shows what's cached at once and then fresh data. With `defaultPreload: "intent"`, also set `defaultPreloadStaleTime: 0`: otherwise the router reuses a hover's preload for 30 seconds instead of running the loader on click, and the query cache never hears about the navigation.
|
|
233
235
|
- On the server, cached data is always reused, whatever `staleTime` says. A server client renders once, so a background refetch could only repeat a request whose result is never shown. Entries marked by `invalidate` still refetch.
|
|
234
236
|
- A failed `prefetch` never throws out of the loader. The error surfaces where the data is read, and is reported to the client's `onError`.
|
|
@@ -309,7 +311,7 @@ function PostPage() {
|
|
|
309
311
|
|
|
310
312
|
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.
|
|
311
313
|
|
|
312
|
-
`useQuery` never refetches on mount: whatever started the query decides. Stale data refetches
|
|
314
|
+
`useQuery` never refetches on mount: whatever started the query decides. Stale data refetches when a loader or server component starts the query again (`prefetch`, `ensure`, `fetch`, or hydrating the server's copy), when `useWaterfall` mounts, when invalidated, on window focus or reconnect, on `refetch()`, or on `refetchInterval`.
|
|
313
315
|
|
|
314
316
|
For an app-wide indicator, `useIsFetching()` returns how many queries have a request in flight, background refetches included, and re-renders when that changes. Pass a filter to count only some: `useIsFetching({ queryKey: ["comments"] })`. It's `0` on the server and during hydration, so the indicator never appears in server HTML. Outside React, `qc.isFetching(filter?)` returns the same number.
|
|
315
317
|
|
|
@@ -640,8 +642,7 @@ export default async function Page(props: {
|
|
|
640
642
|
- `HydrationBoundary` sends the request client's entries to the browser cache; pending ones stream as promises through the RSC payload. Render one in each server component that starts queries for client components. Entries already sent in this request are skipped.
|
|
641
643
|
- The browser takes whichever copy is newer, by when it was fetched (`updatedAt`, so server and browser clocks should agree). When it has no data and no request of its own, it resumes the server's pending request. When it has data, or its own request in flight, it waits for the server's instead of fetching again, and takes it if newer. If it keeps its own copy (the server's is older, as in a payload the router replays from its cache on back and forward, or the server's request failed), it refetches it if stale.
|
|
642
644
|
- `QueryProvider` creates one client per server render and one per browser tab. The server render starts no request: a client component reading a query no `HydrationBoundary` sent renders its Suspense fallback on the server, and the query loads in the browser. That boundary renders in the browser through React's error recovery, with the digest Next.js uses for `ssr: false`, so Next.js doesn't report it; other frameworks pass it to `onRecoverableError`. Pass `config` for the client's options; it must be serializable.
|
|
643
|
-
-
|
|
644
|
-
- `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.
|
|
645
|
+
- In a server component, `fetch` and `ensure` wait for the same thing: the per-request client only refetches what `invalidate` marked, so cached data is never stale there, and each query makes one request whatever its `staleTime`. They differ only in what they return. `await qc.fetch(...)` the data the server component renders itself, anywhere in the tree. When it 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.
|
|
645
646
|
- 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. The browser runs a page's server actions one at a time, so refetches wait for each other and for any pending mutation. For parallel requests, have the `queryFn` call a [Route Handler](https://nextjs.org/docs/app/api-reference/file-conventions/route), as [`examples/nextjs`](examples/nextjs) does.
|
|
646
647
|
|
|
647
648
|
#### Server-only queries
|
package/dist/cache.d.mts
CHANGED
|
@@ -78,7 +78,11 @@ export interface QueryCache {
|
|
|
78
78
|
ensure<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Effect.Effect<EnsuredQueryHandle<TFactory>, ErrorOf<TFactory> | CancelledError, ServicesOf<TFactory>>;
|
|
79
79
|
/** Cache events from the moment the stream starts. */
|
|
80
80
|
readonly events: Stream.Stream<Event>;
|
|
81
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Start a query and succeed with its data. Waits on a cache miss and on
|
|
83
|
+
* stale data (on the server, only invalidated data), so the data is fresh.
|
|
84
|
+
* Fails if that request fails.
|
|
85
|
+
*/
|
|
82
86
|
fetch<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Effect.Effect<DataOf<TFactory>, ErrorOf<TFactory> | CancelledError, ServicesOf<TFactory>>;
|
|
83
87
|
/**
|
|
84
88
|
* Cached data for a factory call. `Some(undefined)` when the data itself
|
package/dist/cache.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { Platform } from "./platform.mjs";
|
|
|
3
3
|
import { unsafeCast } from "./cast.mjs";
|
|
4
4
|
import { listen } from "./listen.mjs";
|
|
5
5
|
import { hashKey, matchesKey } from "./key.mjs";
|
|
6
|
-
import { changes, emit, fetch, invalidate, isObserved, make as make$1, onPlatformEvent, provide, remove, reset, revalidate, setData, setOptions, start } from "./entry.mjs";
|
|
6
|
+
import { changes, emit, fetch, invalidate, isObserved, make as make$1, needsRefetch, onPlatformEvent, provide, remove, reset, revalidate, setData, setOptions, start } from "./entry.mjs";
|
|
7
7
|
import { dehydrateEntries, dehydrateStream, hydrateEntries } from "./hydration.mjs";
|
|
8
8
|
import { optionsFor } from "./options.mjs";
|
|
9
9
|
import { Clock, Context, Effect, Layer, Option, Predicate, RcRef, Stream } from "effect";
|
|
@@ -55,11 +55,12 @@ function keyMatcher(filter) {
|
|
|
55
55
|
return (entry) => matchesKey(entry.key, prefix);
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
|
-
* Waits for an entry's data on a cache miss
|
|
59
|
-
*
|
|
58
|
+
* Waits for an entry's data on a cache miss, and with `fresh` also while it
|
|
59
|
+
* needs a refetch (see `Entry.needsRefetch`). Otherwise cached data returns at
|
|
60
|
+
* once, and revalidates in the background.
|
|
60
61
|
*/
|
|
61
|
-
function load(entry, pages) {
|
|
62
|
-
if (!entry.state.hasData) return unsafeCast(fetch(entry, { pages }));
|
|
62
|
+
function load(entry, pages, fresh) {
|
|
63
|
+
if (!entry.state.hasData || fresh && needsRefetch(entry)) return unsafeCast(fetch(entry, { pages }));
|
|
63
64
|
revalidate(entry);
|
|
64
65
|
return Effect.succeed(dataOf(entry));
|
|
65
66
|
}
|
|
@@ -134,12 +135,12 @@ const make = /*#__PURE__*/ Effect.fnUntraced(function* (config = {}) {
|
|
|
134
135
|
}),
|
|
135
136
|
dehydrate: (options = {}) => Effect.sync(() => dehydrateEntries(state.entries.values(), options)),
|
|
136
137
|
dehydrateStream: (options = {}) => dehydrateStream(() => state.entries.values(), subscribeUnsafe, options),
|
|
137
|
-
ensure: (factory, ...args) => Effect.flatMap(buildFor(factory, args[0]), (entry) => load(entry, args[1]?.pages).pipe(Effect.as({
|
|
138
|
+
ensure: (factory, ...args) => Effect.flatMap(buildFor(factory, args[0]), (entry) => load(entry, args[1]?.pages, false).pipe(Effect.as({
|
|
138
139
|
...handleOf(entry),
|
|
139
140
|
ensured: true
|
|
140
141
|
}))),
|
|
141
142
|
events: listen(subscribeUnsafe),
|
|
142
|
-
fetch: (factory, ...args) => Effect.flatMap(buildFor(factory, args[0]), (entry) => load(entry, args[1]?.pages)),
|
|
143
|
+
fetch: (factory, ...args) => Effect.flatMap(buildFor(factory, args[0]), (entry) => load(entry, args[1]?.pages, true)),
|
|
143
144
|
getData: (factory, ...args) => Effect.sync(() => {
|
|
144
145
|
const options = optionsFor(factory, args[0]);
|
|
145
146
|
const entry = state.entries.get(hashKey(options.queryKey));
|
package/dist/entry.d.mts
CHANGED
|
@@ -6,7 +6,7 @@ import { Pending, Thenable } from "./thenable.mjs";
|
|
|
6
6
|
import { FetchRequest, Query, State, SubscribeOptions } from "./query.mjs";
|
|
7
7
|
import { Cause, Clock, Context, Deferred, Effect, Scope, Stream } from "effect";
|
|
8
8
|
declare namespace entry_d_exports {
|
|
9
|
-
export { CacheState, CancelledError, Entry, TypeId, changes, emit, entryOf, fetch, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isObserved, isQuery, isStaleUnsafe, make, metaOf, now, onPlatformEvent, provide, readUnsafe, remove, reset, resume, revalidate, setData, setOptions, start, subscribeUnsafe };
|
|
9
|
+
export { CacheState, CancelledError, Entry, TypeId, changes, emit, entryOf, fetch, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isObserved, isQuery, isStaleUnsafe, make, metaOf, needsRefetch, now, onPlatformEvent, provide, readUnsafe, remove, reset, resume, revalidate, setData, setOptions, start, subscribeUnsafe };
|
|
10
10
|
}
|
|
11
11
|
export declare const TypeId = "~@raf-query/core/Query";
|
|
12
12
|
declare const CancelledError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
|
|
@@ -112,10 +112,12 @@ export declare function readUnsafe(entry: Entry): Thenable<unknown>;
|
|
|
112
112
|
/** Start a request unless one is in flight, without waiting for it. */
|
|
113
113
|
export declare function start(entry: Entry): void;
|
|
114
114
|
/**
|
|
115
|
-
*
|
|
116
|
-
* renders once, so a refetch there could only duplicate a request whose
|
|
115
|
+
* Whether cached data that was asked for again should be refetched. A server
|
|
116
|
+
* cache renders once, so a refetch there could only duplicate a request whose
|
|
117
117
|
* result is never shown; it refetches only what `invalidate` marked.
|
|
118
118
|
*/
|
|
119
|
+
export declare function needsRefetch(entry: Entry): boolean;
|
|
120
|
+
/** Background refetch of cached data that was asked for again. */
|
|
119
121
|
export declare function revalidate(entry: Entry): void;
|
|
120
122
|
export declare function fetch(entry: Entry, request?: FetchRequest): Effect.Effect<unknown, unknown>;
|
|
121
123
|
export declare function invalidate(entry: Entry): Effect.Effect<void>;
|
package/dist/entry.mjs
CHANGED
|
@@ -189,14 +189,17 @@ function start(entry) {
|
|
|
189
189
|
begin(entry, {});
|
|
190
190
|
}
|
|
191
191
|
/**
|
|
192
|
-
*
|
|
193
|
-
* renders once, so a refetch there could only duplicate a request whose
|
|
192
|
+
* Whether cached data that was asked for again should be refetched. A server
|
|
193
|
+
* cache renders once, so a refetch there could only duplicate a request whose
|
|
194
194
|
* result is never shown; it refetches only what `invalidate` marked.
|
|
195
195
|
*/
|
|
196
|
+
function needsRefetch(entry) {
|
|
197
|
+
return entry.cache.isServer ? entry.state.isInvalidated : isStaleUnsafe(entry);
|
|
198
|
+
}
|
|
199
|
+
/** Background refetch of cached data that was asked for again. */
|
|
196
200
|
function revalidate(entry) {
|
|
197
201
|
const state = entry.state;
|
|
198
|
-
if (
|
|
199
|
-
if (entry.cache.isServer ? state.isInvalidated : isStaleUnsafe(entry)) start(entry);
|
|
202
|
+
if (state.hasData && !state.isFetching && needsRefetch(entry)) start(entry);
|
|
200
203
|
}
|
|
201
204
|
function fetch(entry, request = {}) {
|
|
202
205
|
return Effect.suspend(() => {
|
|
@@ -496,4 +499,4 @@ function scheduleStale(entry) {
|
|
|
496
499
|
entry.tasks.fork("stale", Effect.sleep(Math.max(remaining, 0) + 1).pipe(Effect.andThen(Effect.sync(() => set(entry, {})))));
|
|
497
500
|
}
|
|
498
501
|
//#endregion
|
|
499
|
-
export { CancelledError, TypeId, changes, emit, entryOf, fetch, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isObserved, isQuery, isStaleUnsafe, make, metaOf, now, onPlatformEvent, provide, readUnsafe, remove, reset, resume, revalidate, setData, setOptions, start, subscribeUnsafe };
|
|
502
|
+
export { CancelledError, TypeId, changes, emit, entryOf, fetch, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isObserved, isQuery, isStaleUnsafe, make, metaOf, needsRefetch, now, onPlatformEvent, provide, readUnsafe, remove, reset, resume, revalidate, setData, setOptions, start, subscribeUnsafe };
|