@raf-query/core 0.1.15 → 0.1.17
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 +6 -3
- package/dist/cache.mjs +2 -12
- package/dist/entry.d.mts +7 -1
- package/dist/entry.mjs +11 -1
- package/dist/hydration.mjs +7 -2
- package/dist/query.d.mts +7 -1
- package/dist/query.mjs +11 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.17](https://github.com/KATT/raf-query/compare/0.1.16...0.1.17) (2026-10-13)
|
|
4
|
+
|
|
5
|
+
### Performance Improvements
|
|
6
|
+
|
|
7
|
+
* **react:** browser build of the TanStack Router integration ([#48](https://github.com/KATT/raf-query/issues/48)) ([75e78cf](https://github.com/KATT/raf-query/commit/75e78cf968c069188a2688ad3298fb4bee2e3b8c))
|
|
8
|
+
|
|
9
|
+
## [0.1.16](https://github.com/KATT/raf-query/compare/0.1.15...0.1.16) (2026-10-13)
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
- stale data always refreshes when switching pages ([#49](https://github.com/KATT/raf-query/issues/49)) ([68652cd](https://github.com/KATT/raf-query/commit/68652cdf6ea06ef86d277329098ee294dba98f21))
|
|
14
|
+
|
|
3
15
|
## [0.1.15](https://github.com/KATT/raf-query/compare/0.1.14...0.1.15) (2026-10-13)
|
|
4
16
|
|
|
5
|
-
|
|
17
|
+
- **tooling:** list a fixture app's modules by size ([#45](https://github.com/KATT/raf-query/issues/45)) ([0e1f899](https://github.com/KATT/raf-query/commit/0e1f899f2fecc1368f9be71a2c78e3efd2f3783b))
|
|
6
18
|
|
|
7
19
|
## [0.1.14](https://github.com/KATT/raf-query/compare/0.1.13...0.1.14) (2026-10-13)
|
|
8
20
|
|
package/README.md
CHANGED
|
@@ -161,7 +161,8 @@ return { org: qc.prefetch(orgQuery, { orgId: user.orgId }) };
|
|
|
161
161
|
Behavior:
|
|
162
162
|
|
|
163
163
|
- Every verb dedupes against the cache. A fresh entry is reused; a stale entry is refetched in the background.
|
|
164
|
-
- `ensure` and `fetch` wait only on a cache miss. With stale cached data they resolve immediately and revalidate in the background.
|
|
164
|
+
- `ensure` and `fetch` wait only on a cache miss. With stale cached data they resolve immediately, so the navigation isn't held up, and revalidate in the background: the page renders the cached data and re-renders when the refetch lands.
|
|
165
|
+
- 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.
|
|
165
166
|
- 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.
|
|
166
167
|
- 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`.
|
|
167
168
|
- A failed `ensure` or `fetch` rejects, so it reaches the route's error boundary.
|
|
@@ -241,7 +242,7 @@ function PostPage() {
|
|
|
241
242
|
|
|
242
243
|
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.
|
|
243
244
|
|
|
244
|
-
|
|
245
|
+
`useQuery` never refetches on mount: whatever started the query decides. Stale data refetches in the background 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`.
|
|
245
246
|
|
|
246
247
|
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.
|
|
247
248
|
|
|
@@ -430,6 +431,8 @@ function PostAuthor(props: { post: Post }) {
|
|
|
430
431
|
}
|
|
431
432
|
```
|
|
432
433
|
|
|
434
|
+
Like `prefetch` in a loader, `useWaterfall` refetches stale cached data in the background, once each time the component mounts or its input changes. Re-renders don't refetch.
|
|
435
|
+
|
|
433
436
|
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.
|
|
434
437
|
|
|
435
438
|
During SSR the `queryFn` runs on the server. TanStack Start server functions work there. Next.js rejects server actions called while rendering, so in Next.js start the dependent query in the server component instead:
|
|
@@ -567,7 +570,7 @@ export default async function Page(props: {
|
|
|
567
570
|
```
|
|
568
571
|
|
|
569
572
|
- `getQueryClient` is scoped to one request by React's `cache()`.
|
|
570
|
-
- `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.
|
|
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. The browser takes the server's data when it's newer than its own. Otherwise it keeps its copy and refetches it if stale. That covers a payload the router replays from its cache (back and forward) and a `prefetch` still pending on the server.
|
|
571
574
|
- `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.
|
|
572
575
|
- Server components that only render data can `await qc.fetch(...)` anywhere; the per-request client makes one request per query, whatever its `staleTime`.
|
|
573
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.
|
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,
|
|
6
|
+
import { changes, emit, fetch, invalidate, isObserved, make as make$1, 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,18 +55,8 @@ function keyMatcher(filter) {
|
|
|
55
55
|
return (entry) => matchesKey(entry.key, prefix);
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
|
-
* Background refetch of cached data a verb reuses. A server cache renders
|
|
59
|
-
* once, so a refetch there could only duplicate a request whose result is
|
|
60
|
-
* never shown; it refetches only what `invalidate` marked.
|
|
61
|
-
*/
|
|
62
|
-
function revalidate(entry) {
|
|
63
|
-
const state = entry.state;
|
|
64
|
-
const wanted = entry.cache.isServer ? state.isInvalidated : isStaleUnsafe(entry);
|
|
65
|
-
if (!state.isFetching && wanted) start(entry);
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
58
|
* Waits for an entry's data on a cache miss. Cached data returns at once,
|
|
69
|
-
* and revalidates in the background (see `revalidate`).
|
|
59
|
+
* and revalidates in the background (see `Entry.revalidate`).
|
|
70
60
|
*/
|
|
71
61
|
function load(entry, pages) {
|
|
72
62
|
if (!entry.state.hasData) return unsafeCast(fetch(entry, { pages }));
|
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, setData, setOptions, start, subscribeUnsafe };
|
|
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 };
|
|
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 & {
|
|
@@ -111,6 +111,12 @@ export declare function setOptions(entry: Entry, options: AnyQueryOptions, input
|
|
|
111
111
|
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
|
+
/**
|
|
115
|
+
* Background refetch of cached data that was asked for again. A server cache
|
|
116
|
+
* renders once, so a refetch there could only duplicate a request whose
|
|
117
|
+
* result is never shown; it refetches only what `invalidate` marked.
|
|
118
|
+
*/
|
|
119
|
+
export declare function revalidate(entry: Entry): void;
|
|
114
120
|
export declare function fetch(entry: Entry, request?: FetchRequest): Effect.Effect<unknown, unknown>;
|
|
115
121
|
export declare function invalidate(entry: Entry): Effect.Effect<void>;
|
|
116
122
|
export declare function reset(entry: Entry): Effect.Effect<void>;
|
package/dist/entry.mjs
CHANGED
|
@@ -188,6 +188,16 @@ function readUnsafe(entry) {
|
|
|
188
188
|
function start(entry) {
|
|
189
189
|
begin(entry, {});
|
|
190
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Background refetch of cached data that was asked for again. A server cache
|
|
193
|
+
* renders once, so a refetch there could only duplicate a request whose
|
|
194
|
+
* result is never shown; it refetches only what `invalidate` marked.
|
|
195
|
+
*/
|
|
196
|
+
function revalidate(entry) {
|
|
197
|
+
const state = entry.state;
|
|
198
|
+
if (!state.hasData || state.isFetching) return;
|
|
199
|
+
if (entry.cache.isServer ? state.isInvalidated : isStaleUnsafe(entry)) start(entry);
|
|
200
|
+
}
|
|
191
201
|
function fetch(entry, request = {}) {
|
|
192
202
|
return Effect.suspend(() => {
|
|
193
203
|
const inFlight = entry.inFlight;
|
|
@@ -477,4 +487,4 @@ function scheduleStale(entry) {
|
|
|
477
487
|
entry.tasks.fork("stale", Effect.sleep(Math.max(remaining, 0) + 1).pipe(Effect.andThen(Effect.sync(() => set(entry, {})))));
|
|
478
488
|
}
|
|
479
489
|
//#endregion
|
|
480
|
-
export { CancelledError, TypeId, changes, emit, entryOf, fetch, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isObserved, isQuery, isStaleUnsafe, make, metaOf, now, onPlatformEvent, provide, readUnsafe, remove, reset, resume, setData, setOptions, start, subscribeUnsafe };
|
|
490
|
+
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 };
|
package/dist/hydration.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resume, setData } from "./entry.mjs";
|
|
1
|
+
import { resume, revalidate, setData } from "./entry.mjs";
|
|
2
2
|
import { getUnsafe, metaUnsafe } from "./query.mjs";
|
|
3
3
|
import { Data, Effect, Queue, Stream } from "effect";
|
|
4
4
|
//#region src/hydration.ts
|
|
@@ -100,7 +100,10 @@ const dehydrateStream = /*#__PURE__*/ Effect.fnUntraced(function* (queries, subs
|
|
|
100
100
|
});
|
|
101
101
|
/**
|
|
102
102
|
* Write dehydrated entries into a cache. Existing entries only take newer
|
|
103
|
-
* data; pending entries resume the sender's request.
|
|
103
|
+
* data; pending entries resume the sender's request. An entry that keeps its
|
|
104
|
+
* own data (the sender's is older, as in a router's cached payload, or still
|
|
105
|
+
* pending) was asked for again, so it revalidates if stale, as `prefetch`
|
|
106
|
+
* would.
|
|
104
107
|
*/
|
|
105
108
|
function hydrateEntries(entryFor, state, options) {
|
|
106
109
|
const deserialize = options.deserializeData ?? identity;
|
|
@@ -110,7 +113,9 @@ function hydrateEntries(entryFor, state, options) {
|
|
|
110
113
|
const current = entry.state;
|
|
111
114
|
if (query.updatedAt > 0) {
|
|
112
115
|
if (!current.hasData || current.updatedAt < query.updatedAt) setData(entry, deserialize(query.data), query.updatedAt);
|
|
116
|
+
else revalidate(entry);
|
|
113
117
|
} else if (query.promise && !current.hasData) resume(entry, Promise.resolve(query.promise).then(deserialize));
|
|
118
|
+
else revalidate(entry);
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
121
|
//#endregion
|
package/dist/query.d.mts
CHANGED
|
@@ -5,7 +5,7 @@ import { CancelledError as CancelledError$1, TypeId as TypeId$1, entry_d_exports
|
|
|
5
5
|
import { Direction } from "./infinite.mjs";
|
|
6
6
|
import { Effect, Pipeable, Stream } from "effect";
|
|
7
7
|
declare namespace query_d_exports {
|
|
8
|
-
export { CancelledError, FetchRequest, Of, Query, State, SubscribeOptions, TypeId, changes, fetch, fetchNextPage, fetchPreviousPage, get, getUnsafe, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isQuery, isStaleUnsafe, metaUnsafe, observerCountUnsafe, readUnsafe, refetch, reset, subscribeUnsafe };
|
|
8
|
+
export { CancelledError, FetchRequest, Of, Query, State, SubscribeOptions, TypeId, changes, fetch, fetchNextPage, fetchPreviousPage, get, getUnsafe, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isQuery, isStaleUnsafe, metaUnsafe, observerCountUnsafe, readUnsafe, refetch, reset, revalidateUnsafe, subscribeUnsafe };
|
|
9
9
|
}
|
|
10
10
|
export declare const TypeId: typeof TypeId$1;
|
|
11
11
|
export type TypeId = typeof TypeId;
|
|
@@ -108,6 +108,12 @@ export declare function changes<TData, TFactory extends QueryFactory, TError>(se
|
|
|
108
108
|
* has, and retries a failed first load once its error has been shown.
|
|
109
109
|
*/
|
|
110
110
|
export declare function readUnsafe<TData, TFactory extends QueryFactory, TError>(self: Query<TData, TFactory, TError>): Thenable<TData>;
|
|
111
|
+
/**
|
|
112
|
+
* Refetch cached data in the background if it's stale, as `prefetch` does
|
|
113
|
+
* (on a server cache, only if invalidated). Does nothing without data or
|
|
114
|
+
* while a request is in flight.
|
|
115
|
+
*/
|
|
116
|
+
export declare function revalidateUnsafe<TData, TFactory extends QueryFactory, TError>(self: Query<TData, TFactory, TError>): void;
|
|
111
117
|
/** Listen for state changes. Subscribed entries count as read. */
|
|
112
118
|
export declare function subscribeUnsafe<TData, TFactory extends QueryFactory, TError>(self: Query<TData, TFactory, TError>, listener: () => void, options?: SubscribeOptions): () => void;
|
|
113
119
|
/** Staleness as of now; `state.isStale` is only kept current while read. */
|
package/dist/query.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
|
|
2
2
|
import { unsafeCast } from "./cast.mjs";
|
|
3
|
-
import { CancelledError as CancelledError$1, TypeId as TypeId$1, changes as changes$1, entryOf, fetch as fetch$1, hasNextPageUnsafe as hasNextPageUnsafe$1, hasPreviousPageUnsafe as hasPreviousPageUnsafe$1, invalidate as invalidate$1, isQuery as isQuery$1, isStaleUnsafe as isStaleUnsafe$1, metaOf, provide, readUnsafe as readUnsafe$1, reset as reset$1, subscribeUnsafe as subscribeUnsafe$1 } from "./entry.mjs";
|
|
3
|
+
import { CancelledError as CancelledError$1, TypeId as TypeId$1, changes as changes$1, entryOf, fetch as fetch$1, hasNextPageUnsafe as hasNextPageUnsafe$1, hasPreviousPageUnsafe as hasPreviousPageUnsafe$1, invalidate as invalidate$1, isQuery as isQuery$1, isStaleUnsafe as isStaleUnsafe$1, metaOf, provide, readUnsafe as readUnsafe$1, reset as reset$1, revalidate, subscribeUnsafe as subscribeUnsafe$1 } from "./entry.mjs";
|
|
4
4
|
import { Effect } from "effect";
|
|
5
5
|
import { dual } from "effect/Function";
|
|
6
6
|
//#region src/query.ts
|
|
@@ -31,6 +31,7 @@ var query_exports = /* @__PURE__ */ __exportAll({
|
|
|
31
31
|
readUnsafe: () => readUnsafe,
|
|
32
32
|
refetch: () => refetch,
|
|
33
33
|
reset: () => reset,
|
|
34
|
+
revalidateUnsafe: () => revalidateUnsafe,
|
|
34
35
|
subscribeUnsafe: () => subscribeUnsafe
|
|
35
36
|
});
|
|
36
37
|
const TypeId = TypeId$1;
|
|
@@ -105,6 +106,14 @@ function changes(self) {
|
|
|
105
106
|
function readUnsafe(self) {
|
|
106
107
|
return unsafeCast(readUnsafe$1(entryOf(self)));
|
|
107
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Refetch cached data in the background if it's stale, as `prefetch` does
|
|
111
|
+
* (on a server cache, only if invalidated). Does nothing without data or
|
|
112
|
+
* while a request is in flight.
|
|
113
|
+
*/
|
|
114
|
+
function revalidateUnsafe(self) {
|
|
115
|
+
revalidate(entryOf(self));
|
|
116
|
+
}
|
|
108
117
|
/** Listen for state changes. Subscribed entries count as read. */
|
|
109
118
|
function subscribeUnsafe(self, listener, options) {
|
|
110
119
|
return subscribeUnsafe$1(entryOf(self), listener, options);
|
|
@@ -124,4 +133,4 @@ function observerCountUnsafe(self) {
|
|
|
124
133
|
return entryOf(self).listeners.size;
|
|
125
134
|
}
|
|
126
135
|
//#endregion
|
|
127
|
-
export { CancelledError, TypeId, changes, fetch, fetchNextPage, fetchPreviousPage, get, getUnsafe, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isQuery, isStaleUnsafe, metaUnsafe, observerCountUnsafe, query_exports, readUnsafe, refetch, reset, subscribeUnsafe };
|
|
136
|
+
export { CancelledError, TypeId, changes, fetch, fetchNextPage, fetchPreviousPage, get, getUnsafe, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isQuery, isStaleUnsafe, metaUnsafe, observerCountUnsafe, query_exports, readUnsafe, refetch, reset, revalidateUnsafe, subscribeUnsafe };
|