@raf-query/core 0.0.15 → 0.0.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.0.17](https://github.com/KATT/raf-query/compare/0.0.16...0.0.17) (2026-10-12)
4
+
5
+ ### Features
6
+
7
+ * **core:** pause retries while the browser is offline ([#18](https://github.com/KATT/raf-query/issues/18)) ([a25c664](https://github.com/KATT/raf-query/commit/a25c664359fe7248cdc8d91a14d524e37b467c6c))
8
+
9
+ ## [0.0.16](https://github.com/KATT/raf-query/compare/0.0.15...0.0.16) (2026-10-12)
10
+
11
+ ### Bug Fixes
12
+
13
+ - **core:** don't revalidate cached data in server verbs ([#19](https://github.com/KATT/raf-query/issues/19)) ([0ed7d9f](https://github.com/KATT/raf-query/commit/0ed7d9fefd0b70a8aa9a85c8d2c309a9fed42903))
14
+
3
15
  ## [0.0.15](https://github.com/KATT/raf-query/compare/0.0.14...0.0.15) (2026-10-12)
4
16
 
5
- * **agent-docs:** replace local ~/dev path with upstream React link ([#17](https://github.com/KATT/raf-query/issues/17)) ([79c0314](https://github.com/KATT/raf-query/commit/79c031475c3ed62ce5a07fb91d68733534e452b3))
17
+ - **agent-docs:** replace local ~/dev path with upstream React link ([#17](https://github.com/KATT/raf-query/issues/17)) ([79c0314](https://github.com/KATT/raf-query/commit/79c031475c3ed62ce5a07fb91d68733534e452b3))
6
18
 
7
19
  ## [0.0.14](https://github.com/KATT/raf-query/compare/0.0.13...0.0.14) (2026-10-12)
8
20
 
package/README.md CHANGED
@@ -113,6 +113,7 @@ Behavior:
113
113
 
114
114
  - Every verb dedupes against the cache. A fresh entry is reused; a stale entry is refetched in the background.
115
115
  - `ensure` and `fetch` wait only on a cache miss. With stale cached data they resolve immediately and revalidate in the background.
116
+ - 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.
116
117
  - 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`.
117
118
  - A failed `ensure` or `fetch` rejects, so it reaches the route's error boundary.
118
119
 
@@ -445,7 +446,7 @@ export default async function Page(props: {
445
446
  - `getQueryClient` is scoped to one request by React's `cache()`.
446
447
  - `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.
447
448
  - `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.
448
- - Server components that only render data can `await qc.fetch(...)` anywhere; the per-request client dedupes them.
449
+ - Server components that only render data can `await qc.fetch(...)` anywhere; the per-request client makes one request per query, whatever its `staleTime`.
449
450
  - `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.
450
451
  - 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.
451
452
 
@@ -540,7 +541,7 @@ Both render nothing on the server, since the server's cache isn't the browser's.
540
541
  | `getAll()`, `getQuery(factory, input?)` | Cache entries, for devtools and integrations |
541
542
  | `clear()` | Drop every entry and abort in-flight requests |
542
543
  | `mount()` | Start window-focus and reconnect refetching. The providers call it |
543
- | `isServer` | Detected from `window`. Server clients don't retry, never gc, skip stale timers |
544
+ | `isServer` | Detected from `window`. Server clients don't retry, never gc, skip stale timers, and never revalidate cached data |
544
545
 
545
546
  Query options, accepted by `queryOptions`, `infiniteQueryOptions` and `config.defaults`:
546
547
 
@@ -555,6 +556,8 @@ Query options, accepted by `queryOptions`, `infiniteQueryOptions` and `config.de
555
556
 
556
557
  `infiniteQueryOptions` also takes `getPreviousPageParam` and `maxPages`.
557
558
 
559
+ In the browser, retries pause while `navigator.onLine` is `false`: after the retry delay, the next attempt waits for the window's `online` event. The request stays fetching meanwhile, and cancelling it (`remove`, `clear`, a restart) stops the wait. The first attempt runs regardless.
560
+
558
561
  `@raf-query/core`:
559
562
 
560
563
  | Export | Use |
package/dist/client.d.mts CHANGED
@@ -8,7 +8,9 @@ export interface QueryClientConfig {
8
8
  readonly defaults?: QueryBehavior;
9
9
  /**
10
10
  * Whether this client runs on the server. Detected from `window`. Server
11
- * clients don't retry, never garbage-collect, and skip stale timers.
11
+ * clients don't retry, never garbage-collect, skip stale timers, and reuse
12
+ * cached data in `prefetch` / `ensure` / `fetch` instead of revalidating
13
+ * it, unless `invalidate` marked it.
12
14
  */
13
15
  readonly isServer?: boolean;
14
16
  /** Called with every failed request, including failed prefetches. */
@@ -63,14 +65,16 @@ export declare class QueryClient {
63
65
  getQuery<TFactory extends QueryFactory>(factory: TFactory, ...args: InputArgs<TFactory>): Query<DataOf<TFactory>, TFactory>;
64
66
  /**
65
67
  * Start a query without waiting and return its handle. Fresh data is
66
- * reused; stale data refetches in the background. Errors surface where the
67
- * data is read, never here.
68
+ * reused; stale data refetches in the background (in the browser only, see
69
+ * {@link QueryClientConfig.isServer}). Errors surface where the data is
70
+ * read, never here.
68
71
  */
69
72
  prefetch<TFactory extends QueryFactory>(factory: TFactory, ...args: InputArgs<TFactory>): QueryHandle<TFactory>;
70
73
  /**
71
74
  * Start a query and wait for it on a cache miss. Rejects if that request
72
- * fails. Stale cached data resolves immediately and revalidates. The
73
- * handle is marked `ensured`, so `useQuery` returns its data directly.
75
+ * fails. Stale cached data resolves immediately and revalidates in the
76
+ * browser. The handle is marked `ensured`, so `useQuery` returns its data
77
+ * directly.
74
78
  */
75
79
  ensure<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadArgs<TFactory>): Promise<EnsuredQueryHandle<TFactory>>;
76
80
  /** Like `ensure`, but resolves to the data. */
package/dist/client.mjs CHANGED
@@ -90,19 +90,21 @@ var QueryClient = class {
90
90
  }
91
91
  /**
92
92
  * Start a query without waiting and return its handle. Fresh data is
93
- * reused; stale data refetches in the background. Errors surface where the
94
- * data is read, never here.
93
+ * reused; stale data refetches in the background (in the browser only, see
94
+ * {@link QueryClientConfig.isServer}). Errors surface where the data is
95
+ * read, never here.
95
96
  */
96
97
  prefetch(factory, ...args) {
97
98
  const entry = this.#build(factory, args[0]);
98
- const state = entry.getState();
99
- if (!state.isFetching && (!state.hasData || entry.isStaleNow())) entry.fetch().catch(noop);
99
+ if (entry.getState().hasData) this.#revalidate(entry);
100
+ else entry.fetch().catch(noop);
100
101
  return handleOf(entry);
101
102
  }
102
103
  /**
103
104
  * Start a query and wait for it on a cache miss. Rejects if that request
104
- * fails. Stale cached data resolves immediately and revalidates. The
105
- * handle is marked `ensured`, so `useQuery` returns its data directly.
105
+ * fails. Stale cached data resolves immediately and revalidates in the
106
+ * browser. The handle is marked `ensured`, so `useQuery` returns its data
107
+ * directly.
106
108
  */
107
109
  async ensure(factory, ...args) {
108
110
  const entry = this.#build(factory, args[0]);
@@ -199,9 +201,19 @@ var QueryClient = class {
199
201
  #load(entry, pages) {
200
202
  const state = entry.getState();
201
203
  if (!state.hasData) return entry.fetch({ pages });
202
- if (!state.isFetching && entry.isStaleNow()) entry.fetch().catch(noop);
204
+ this.#revalidate(entry);
203
205
  return Promise.resolve(state.data);
204
206
  }
207
+ /**
208
+ * Background refetch of cached data a verb reuses. A server client renders
209
+ * once, so a refetch there could only duplicate a request whose result is
210
+ * never shown; it refetches only what `invalidate` marked.
211
+ */
212
+ #revalidate(entry) {
213
+ const state = entry.getState();
214
+ const wanted = this.isServer ? state.isInvalidated : entry.isStaleNow();
215
+ if (!state.isFetching && wanted) entry.fetch().catch(noop);
216
+ }
205
217
  #match(target, input) {
206
218
  if (Predicate.isFunction(target)) {
207
219
  const entry = this.#cache.get(hashKey(optionsFor(target, input).queryKey));
@@ -0,0 +1,16 @@
1
+ import { Effect } from "effect";
2
+ //#region src/online.ts
3
+ function isOffline() {
4
+ return "window" in globalThis && "navigator" in globalThis && "onLine" in navigator && !navigator.onLine;
5
+ }
6
+ /**
7
+ * Succeeds now if the browser is online, else on the window's next `online`
8
+ * event. Interrupting it removes the listener.
9
+ */
10
+ const whenOnline = Effect.suspend(() => isOffline() ? Effect.callback((resume) => {
11
+ const onOnline = () => resume(Effect.void);
12
+ window.addEventListener("online", onOnline, { once: true });
13
+ return Effect.sync(() => window.removeEventListener("online", onOnline));
14
+ }) : Effect.void);
15
+ //#endregion
16
+ export { whenOnline };
package/dist/query.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { scheduleNotify } from "./notify.mjs";
2
+ import { whenOnline } from "./online.mjs";
2
3
  import { replaceEqualDeep } from "./share.mjs";
3
4
  import { fulfilled, pending } from "./thenable.mjs";
4
5
  import { Cause, Effect, Exit, Predicate, Schedule } from "effect";
@@ -382,7 +383,16 @@ var QueryEntry = class {
382
383
  const retry = this.#behavior("retry") ?? (this.#host.isServer ? 0 : 3);
383
384
  const countFailure = Effect.sync(() => this.#set({ failureCount: this.#state.failureCount + 1 }));
384
385
  const counted = once.pipe(Effect.tapError(() => countFailure));
385
- if (Schedule.isSchedule(retry)) return Effect.retry(counted, retry);
386
+ const online = this.#host.isServer ? Effect.void : whenOnline;
387
+ if (Schedule.isSchedule(retry)) return Effect.suspend(() => {
388
+ let first = true;
389
+ const each = Effect.suspend(() => {
390
+ const wait = first ? Effect.void : online;
391
+ first = false;
392
+ return wait.pipe(Effect.andThen(counted));
393
+ });
394
+ return Effect.retry(each, retry);
395
+ });
386
396
  const retryDelay = this.#behavior("retryDelay");
387
397
  const delayFor = (failureCount, error) => {
388
398
  if (retryDelay === void 0) return Math.min(1e3 * 2 ** (failureCount - 1), 3e4);
@@ -391,7 +401,7 @@ var QueryEntry = class {
391
401
  const attempt = counted.pipe(Effect.catch((error) => {
392
402
  const failureCount = this.#state.failureCount;
393
403
  if (!shouldRetry(retry, failureCount, error)) return Effect.fail(error);
394
- return Effect.sleep(delayFor(failureCount, error)).pipe(Effect.andThen(Effect.suspend(() => attempt)));
404
+ return Effect.sleep(delayFor(failureCount, error)).pipe(Effect.andThen(online), Effect.andThen(Effect.suspend(() => attempt)));
395
405
  }));
396
406
  return attempt;
397
407
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/core",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "Framework-agnostic core of raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",