@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 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
- * **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))
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
- 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`.
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, isStaleUnsafe, make as make$1, onPlatformEvent, provide, remove, reset, setData, setOptions, start } from "./entry.mjs";
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 };
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/core",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Framework-agnostic core of raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",