@raf-query/core 0.2.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -1
- package/README.md +11 -11
- package/dist/entry.d.mts +8 -2
- package/dist/entry.mjs +17 -8
- package/dist/hydration.d.mts +7 -1
- package/dist/hydration.mjs +27 -11
- package/dist/index.d.mts +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/KATT/raf-query/compare/0.2.8...0.3.0) (2026-10-13)
|
|
4
|
+
|
|
5
|
+
### ⚠ BREAKING CHANGES
|
|
6
|
+
|
|
7
|
+
* RSC apps hydrate only through HydrationBoundary; QueryProvider moves to /rsc ([#61](https://github.com/KATT/raf-query/issues/61))
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* RSC apps hydrate only through HydrationBoundary; QueryProvider moves to /rsc ([#61](https://github.com/KATT/raf-query/issues/61)) ([772de79](https://github.com/KATT/raf-query/commit/772de796adfdf267a9184fd75624ff30ae53f4fc)), references [#59](https://github.com/KATT/raf-query/issues/59) [#59](https://github.com/KATT/raf-query/issues/59) [#59](https://github.com/KATT/raf-query/issues/59) [#59](https://github.com/KATT/raf-query/issues/59)
|
|
12
|
+
|
|
13
|
+
## [0.2.8](https://github.com/KATT/raf-query/compare/0.2.7...0.2.8) (2026-10-13)
|
|
14
|
+
|
|
15
|
+
- **codesandbox:** speed up the branch sync ([#60](https://github.com/KATT/raf-query/issues/60)) ([9099cb2](https://github.com/KATT/raf-query/commit/9099cb2ce937104d5feb8c7033233e701f412a28))
|
|
16
|
+
|
|
3
17
|
## [0.2.7](https://github.com/KATT/raf-query/compare/0.2.6...0.2.7) (2026-10-13)
|
|
4
18
|
|
|
5
|
-
|
|
19
|
+
- **examples:** open the CodeSandbox links at the issue list routes ([#58](https://github.com/KATT/raf-query/issues/58)) ([8cd518c](https://github.com/KATT/raf-query/commit/8cd518c3e8c6d5fed5e6d38c847401d390731909))
|
|
6
20
|
|
|
7
21
|
## [0.2.6](https://github.com/KATT/raf-query/compare/0.2.5...0.2.6) (2026-10-13)
|
|
8
22
|
|
package/README.md
CHANGED
|
@@ -32,12 +32,11 @@ The same issue tracker, built on two frameworks so the integrations can be compa
|
|
|
32
32
|
|
|
33
33
|
`@raf-query/react` subpath entries for framework integrations and tooling:
|
|
34
34
|
|
|
35
|
-
| Entry | Use
|
|
36
|
-
| ---------------------------------- |
|
|
37
|
-
| `@raf-query/react/tanstack-router` | Streams the router's query cache to the client during SSR
|
|
38
|
-
| `@raf-query/react/rsc` | `HydrationBoundary` for
|
|
39
|
-
| `@raf-query/react/
|
|
40
|
-
| `@raf-query/react/devtools` | [Devtools](#devtools) panel for inspecting the cache in development |
|
|
35
|
+
| Entry | Use |
|
|
36
|
+
| ---------------------------------- | ------------------------------------------------------------------- |
|
|
37
|
+
| `@raf-query/react/tanstack-router` | Streams the router's query cache to the client during SSR |
|
|
38
|
+
| `@raf-query/react/rsc` | `QueryProvider` and `HydrationBoundary` for React Server Components |
|
|
39
|
+
| `@raf-query/react/devtools` | [Devtools](#devtools) panel for inspecting the cache in development |
|
|
41
40
|
|
|
42
41
|
## Principles
|
|
43
42
|
|
|
@@ -435,7 +434,7 @@ Like `prefetch` in a loader, `useWaterfall` refetches stale cached data in the b
|
|
|
435
434
|
|
|
436
435
|
The name is the warning: each waterfall is an extra round trip after render. First try to get the input from route params so the loader can start it, or have the backend return it with its parent.
|
|
437
436
|
|
|
438
|
-
|
|
437
|
+
In TanStack Start the `queryFn` runs on the server during SSR, and the router streams the result. Under the `/rsc` `QueryProvider` it starts nothing on the server: the reader's Suspense boundary renders its fallback there, and the query loads in the browser. To have it in the HTML, start the dependent query in the server component instead:
|
|
439
438
|
|
|
440
439
|
```tsx
|
|
441
440
|
const post = await qc.fetch(postQuery, { slug });
|
|
@@ -518,7 +517,7 @@ export function getRouter() {
|
|
|
518
517
|
|
|
519
518
|
`setupRouterIntegration` streams queries during SSR, clears the request's cache when the response finishes, and turns `redirect()` thrown from a `queryFn` into a router navigation. `getRouter` runs per request on the server, so each request gets its own client. Options: `dehydrateOptions` (`filter`, `keepErrors`, `serializeData`), `hydrateOptions` (`deserializeData`), and `handleRedirects: false` to handle redirects yourself. By default a server error crosses to the browser as a `RedactedError` naming the query, so its message doesn't leak into the page.
|
|
520
519
|
|
|
521
|
-
### Next.js App Router
|
|
520
|
+
### React Server Components (Next.js App Router)
|
|
522
521
|
|
|
523
522
|
```tsx
|
|
524
523
|
// app/get-query-client.ts
|
|
@@ -530,7 +529,7 @@ export const getQueryClient = cache(() => new QueryClient());
|
|
|
530
529
|
|
|
531
530
|
```tsx
|
|
532
531
|
// app/layout.tsx
|
|
533
|
-
import { QueryProvider } from "@raf-query/react/
|
|
532
|
+
import { QueryProvider } from "@raf-query/react/rsc";
|
|
534
533
|
|
|
535
534
|
export default function RootLayout(props: { children: React.ReactNode }) {
|
|
536
535
|
return (
|
|
@@ -570,8 +569,9 @@ export default async function Page(props: {
|
|
|
570
569
|
```
|
|
571
570
|
|
|
572
571
|
- `getQueryClient` is scoped to one request by React's `cache()`.
|
|
573
|
-
- `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.
|
|
574
|
-
- `
|
|
572
|
+
- `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.
|
|
573
|
+
- 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.
|
|
574
|
+
- `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.
|
|
575
575
|
- Server components that only render data can `await qc.fetch(...)` anywhere; the per-request client makes one request per query, whatever its `staleTime`.
|
|
576
576
|
- `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.
|
|
577
577
|
- 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.
|
package/dist/entry.d.mts
CHANGED
|
@@ -122,8 +122,14 @@ export declare function invalidate(entry: Entry): Effect.Effect<void>;
|
|
|
122
122
|
export declare function reset(entry: Entry): Effect.Effect<void>;
|
|
123
123
|
/** Write data directly, as if a request had returned it. */
|
|
124
124
|
export declare function setData(entry: Entry, data: unknown, updatedAt?: number): void;
|
|
125
|
-
/**
|
|
126
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Adopt a request started elsewhere (the server's, during hydration). Its
|
|
127
|
+
* data counts as fetched when the sender got it, not when it arrived.
|
|
128
|
+
*/
|
|
129
|
+
export declare function resume(entry: Entry, promise: PromiseLike<{
|
|
130
|
+
readonly data?: unknown;
|
|
131
|
+
readonly updatedAt: number;
|
|
132
|
+
}>): void;
|
|
127
133
|
/** Refetch a read, stale entry whose readers want it on this event. */
|
|
128
134
|
export declare function onPlatformEvent(entry: Entry, event: PlatformEvent): void;
|
|
129
135
|
/** Take `entry` out of its cache: interrupt its fibers and fail its awaiters. */
|
package/dist/entry.mjs
CHANGED
|
@@ -235,10 +235,18 @@ function reset(entry) {
|
|
|
235
235
|
function setData(entry, data, updatedAt = now(entry)) {
|
|
236
236
|
succeed(entry, data, updatedAt);
|
|
237
237
|
}
|
|
238
|
-
/**
|
|
238
|
+
/**
|
|
239
|
+
* Adopt a request started elsewhere (the server's, during hydration). Its
|
|
240
|
+
* data counts as fetched when the sender got it, not when it arrived.
|
|
241
|
+
*/
|
|
239
242
|
function resume(entry, promise) {
|
|
240
243
|
if (entry.state.hasData || entry.inFlight) return;
|
|
241
|
-
|
|
244
|
+
let updatedAt;
|
|
245
|
+
startRequest(entry, Effect.promise(async () => {
|
|
246
|
+
const result = await promise;
|
|
247
|
+
updatedAt = result.updatedAt;
|
|
248
|
+
return result.data;
|
|
249
|
+
}), void 0, () => updatedAt);
|
|
242
250
|
}
|
|
243
251
|
/** Refetch a read, stale entry whose readers want it on this event. */
|
|
244
252
|
function onPlatformEvent(entry, event) {
|
|
@@ -293,7 +301,7 @@ function set(entry, patch) {
|
|
|
293
301
|
scheduleStale(entry);
|
|
294
302
|
}
|
|
295
303
|
/** Run `request` as the entry's request fiber, settling into its awaiter. */
|
|
296
|
-
function startRequest(entry, request, direction) {
|
|
304
|
+
function startRequest(entry, request, direction, updatedAt) {
|
|
297
305
|
if (!entry.inFlight) entry.failureCountBefore = entry.state.failureCount;
|
|
298
306
|
const awaiter = entry.inFlight ?? Deferred.makeUnsafe();
|
|
299
307
|
entry.inFlight = awaiter;
|
|
@@ -315,14 +323,14 @@ function startRequest(entry, request, direction) {
|
|
|
315
323
|
fetchDirection: direction,
|
|
316
324
|
isFetching: true
|
|
317
325
|
});
|
|
318
|
-
entry.tasks.fork("request", Effect.exit(request).pipe(Effect.flatMap((exit) => Effect.sync(() => settle(entry, awaiter, exit)))));
|
|
326
|
+
entry.tasks.fork("request", Effect.exit(request).pipe(Effect.flatMap((exit) => Effect.sync(() => settle(entry, awaiter, exit, updatedAt?.() ?? now(entry))))));
|
|
319
327
|
return awaiter;
|
|
320
328
|
}
|
|
321
|
-
function settle(entry, awaiter, exit) {
|
|
329
|
+
function settle(entry, awaiter, exit, updatedAt) {
|
|
322
330
|
if (entry.inFlight !== awaiter) return;
|
|
323
331
|
entry.inFlight = void 0;
|
|
324
332
|
if (Exit.isSuccess(exit)) {
|
|
325
|
-
const data = succeed(entry, exit.value,
|
|
333
|
+
const data = succeed(entry, exit.value, updatedAt);
|
|
326
334
|
Deferred.doneUnsafe(awaiter, Exit.succeed(data));
|
|
327
335
|
} else if (Cause.hasInterruptsOnly(exit.cause)) {
|
|
328
336
|
set(entry, {
|
|
@@ -366,13 +374,14 @@ function succeed(entry, next, updatedAt) {
|
|
|
366
374
|
entry.pending = void 0;
|
|
367
375
|
firstLoad?.resolve(data);
|
|
368
376
|
const unchanged = previous.hasData && data === previous.data;
|
|
377
|
+
const isFetching = entry.inFlight !== void 0;
|
|
369
378
|
set(entry, {
|
|
370
379
|
data,
|
|
371
380
|
error: null,
|
|
372
381
|
failureCount: 0,
|
|
373
|
-
fetchDirection: void 0,
|
|
382
|
+
fetchDirection: isFetching ? previous.fetchDirection : void 0,
|
|
374
383
|
hasData: true,
|
|
375
|
-
isFetching
|
|
384
|
+
isFetching,
|
|
376
385
|
isInvalidated: false,
|
|
377
386
|
promise: firstLoad?.promise ?? (unchanged ? previous.promise : fulfilled(data)),
|
|
378
387
|
status: "success",
|
package/dist/hydration.d.mts
CHANGED
|
@@ -5,6 +5,12 @@ import "./entry.mjs";
|
|
|
5
5
|
import { Query, query_d_exports } from "./query.mjs";
|
|
6
6
|
import { Cause, Effect, Scope, Stream } from "effect";
|
|
7
7
|
//#region src/hydration.d.ts
|
|
8
|
+
/** What a dehydrated pending query's promise resolves to. */
|
|
9
|
+
export interface DehydratedResult {
|
|
10
|
+
readonly data?: unknown;
|
|
11
|
+
/** When the sender got `data` (ms since epoch). */
|
|
12
|
+
readonly updatedAt: number;
|
|
13
|
+
}
|
|
8
14
|
/**
|
|
9
15
|
* One cache entry in transit. Settled entries carry `data`; entries still
|
|
10
16
|
* loading their first data carry the request's `promise`, so the receiving
|
|
@@ -15,7 +21,7 @@ export interface DehydratedQuery {
|
|
|
15
21
|
readonly key: QueryKey;
|
|
16
22
|
/** The query's `meta`, for receivers that haven't called its factory. */
|
|
17
23
|
readonly meta?: QueryMeta;
|
|
18
|
-
readonly promise?: Promise<
|
|
24
|
+
readonly promise?: Promise<DehydratedResult>;
|
|
19
25
|
/** When `data` was fetched; `0` while pending. */
|
|
20
26
|
readonly updatedAt: number;
|
|
21
27
|
}
|
package/dist/hydration.mjs
CHANGED
|
@@ -31,7 +31,10 @@ function dehydrateUnsafe(query, options) {
|
|
|
31
31
|
data: serialize(state.data),
|
|
32
32
|
updatedAt: state.updatedAt
|
|
33
33
|
};
|
|
34
|
-
const promise = state.promise.then(
|
|
34
|
+
const promise = state.promise.then((data) => ({
|
|
35
|
+
data: serialize(data),
|
|
36
|
+
updatedAt: getUnsafe(query).updatedAt
|
|
37
|
+
}), (error) => {
|
|
35
38
|
throw options.keepErrors ? error : new RedactedError({ hash: query.hash });
|
|
36
39
|
});
|
|
37
40
|
promise.catch(noop);
|
|
@@ -99,23 +102,36 @@ const dehydrateStream = /*#__PURE__*/ Effect.fnUntraced(function* (queries, subs
|
|
|
99
102
|
};
|
|
100
103
|
});
|
|
101
104
|
/**
|
|
105
|
+
* Take the sender's data if it's newer than the entry's. An entry that keeps
|
|
106
|
+
* its own (the sender's is older, as in a router's cached payload) was asked
|
|
107
|
+
* for again, so it revalidates if stale, as `prefetch` would.
|
|
108
|
+
*/
|
|
109
|
+
function takeNewer(entry, data, updatedAt) {
|
|
110
|
+
const current = entry.state;
|
|
111
|
+
if (!current.hasData || current.updatedAt < updatedAt) setData(entry, data, updatedAt);
|
|
112
|
+
else revalidate(entry);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
102
115
|
* Write dehydrated entries into a cache. Existing entries only take newer
|
|
103
|
-
* data
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
116
|
+
* data. A pending entry the cache has nothing for resumes the sender's
|
|
117
|
+
* request. One that has data, or a request of its own, waits for the
|
|
118
|
+
* sender's instead of fetching again, and then takes whichever is newer;
|
|
119
|
+
* if the sender's failed, it revalidates.
|
|
107
120
|
*/
|
|
108
121
|
function hydrateEntries(entryFor, state, options) {
|
|
109
122
|
const deserialize = options.deserializeData ?? identity;
|
|
110
123
|
for (const query of state.queries) {
|
|
111
124
|
const entry = entryFor(query.key);
|
|
112
125
|
if (!entry.options && query.meta !== void 0) entry.hydratedMeta = query.meta;
|
|
113
|
-
|
|
114
|
-
if (query.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
if (query.updatedAt > 0) takeNewer(entry, deserialize(query.data), query.updatedAt);
|
|
127
|
+
else if (query.promise) {
|
|
128
|
+
const result = Promise.resolve(query.promise).then((sent) => ({
|
|
129
|
+
data: deserialize(sent.data),
|
|
130
|
+
updatedAt: sent.updatedAt
|
|
131
|
+
}));
|
|
132
|
+
if (!entry.state.hasData && !entry.inFlight) resume(entry, result);
|
|
133
|
+
else result.then((sent) => takeNewer(entry, sent.data, sent.updatedAt), () => revalidate(entry));
|
|
134
|
+
} else revalidate(entry);
|
|
119
135
|
}
|
|
120
136
|
}
|
|
121
137
|
//#endregion
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { platform_d_exports } from "./platform.mjs";
|
|
2
2
|
import { QueryKey, hashKey, matchesKey } from "./key.mjs";
|
|
3
3
|
import { AnyQueryOptions, DataOf, EnsuredQueryHandle, ErrorOf, InfiniteData, InfiniteQueryOptions, InputArgs, InputOf, IsPaged, LoadOptions, PageFunctionContext, PageOf, QueryBehavior, QueryFactory, QueryFnResult, QueryFunctionContext, QueryHandle, QueryMeta, QueryOptions, Register, RetryPolicy, ServicesOf, infiniteQueryOptions, queryOptions } from "./options.mjs";
|
|
4
|
-
import { DehydrateOptions, DehydratedQuery, DehydratedState, DehydratedStream, HydrateOptions, RedactedError, canDehydrate, dehydrateQuery } from "./hydration.mjs";
|
|
4
|
+
import { DehydrateOptions, DehydratedQuery, DehydratedResult, DehydratedState, DehydratedStream, HydrateOptions, RedactedError, canDehydrate, dehydrateQuery } from "./hydration.mjs";
|
|
5
5
|
import { cache_d_exports } from "./cache.mjs";
|
|
6
6
|
import { Thenable } from "./thenable.mjs";
|
|
7
7
|
import { query_d_exports } from "./query.mjs";
|
|
8
8
|
import { observer_d_exports } from "./observer.mjs";
|
|
9
|
-
export { type AnyQueryOptions, type DataOf, type DehydrateOptions, type DehydratedQuery, type DehydratedState, type DehydratedStream, type EnsuredQueryHandle, type ErrorOf, type HydrateOptions, type InfiniteData, type InfiniteQueryOptions, type InputArgs, type InputOf, type IsPaged, type LoadOptions, type PageFunctionContext, type PageOf, platform_d_exports as Platform, query_d_exports as Query, type QueryBehavior, cache_d_exports as QueryCache, type QueryFactory, type QueryFnResult, type QueryFunctionContext, type QueryHandle, type QueryKey, type QueryMeta, observer_d_exports as QueryObserver, type QueryOptions, RedactedError, type Register, type RetryPolicy, type ServicesOf, type Thenable, canDehydrate, dehydrateQuery, hashKey, infiniteQueryOptions, matchesKey, queryOptions };
|
|
9
|
+
export { type AnyQueryOptions, type DataOf, type DehydrateOptions, type DehydratedQuery, type DehydratedResult, type DehydratedState, type DehydratedStream, type EnsuredQueryHandle, type ErrorOf, type HydrateOptions, type InfiniteData, type InfiniteQueryOptions, type InputArgs, type InputOf, type IsPaged, type LoadOptions, type PageFunctionContext, type PageOf, platform_d_exports as Platform, query_d_exports as Query, type QueryBehavior, cache_d_exports as QueryCache, type QueryFactory, type QueryFnResult, type QueryFunctionContext, type QueryHandle, type QueryKey, type QueryMeta, observer_d_exports as QueryObserver, type QueryOptions, RedactedError, type Register, type RetryPolicy, type ServicesOf, type Thenable, canDehydrate, dehydrateQuery, hashKey, infiniteQueryOptions, matchesKey, queryOptions };
|