@raf-query/core 0.0.16 → 0.0.18

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,10 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.18](https://github.com/KATT/raf-query/compare/0.0.17...0.0.18) (2026-10-12)
4
+
5
+ ### Bug Fixes
6
+
7
+ * **core:** allow undefined query data ([#22](https://github.com/KATT/raf-query/issues/22)) ([9b85e57](https://github.com/KATT/raf-query/commit/9b85e57bf2fed4413cdbc1b49f925cc60dad695f)), references [#20](https://github.com/KATT/raf-query/issues/20)
8
+
9
+ ## [0.0.17](https://github.com/KATT/raf-query/compare/0.0.16...0.0.17) (2026-10-12)
10
+
11
+ ### Features
12
+
13
+ - **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))
14
+
3
15
  ## [0.0.16](https://github.com/KATT/raf-query/compare/0.0.15...0.0.16) (2026-10-12)
4
16
 
5
17
  ### Bug Fixes
6
18
 
7
- * **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))
19
+ - **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))
8
20
 
9
21
  ## [0.0.15](https://github.com/KATT/raf-query/compare/0.0.14...0.0.15) (2026-10-12)
10
22
 
package/README.md CHANGED
@@ -363,6 +363,8 @@ function EditTitle(props: { post: Post }) {
363
363
  | `qc.setData(factory, input, updater)` | Write data directly |
364
364
  | `qc.getData(factory, input)` | Read cached data, or `undefined` |
365
365
 
366
+ Data can be `undefined` when the `queryFn`'s type allows it (`Promise<User | undefined>`); it's cached and reused like any other value, unlike in TanStack Query, where an `undefined` result is an error. So `getData` and an updater's `previous` return `undefined` both with no data and with `undefined` data; `qc.getQuery(factory, input).getState().hasData` tells them apart.
367
+
366
368
  ## SSR and hydration
367
369
 
368
370
  The server's query client streams its cache to the browser's query client as entries settle. Pending queries cross as promises, so the browser resumes the server's request instead of starting a new one. Components never hydrate anything themselves; a handle only needs its key to find the entry.
@@ -556,6 +558,8 @@ Query options, accepted by `queryOptions`, `infiniteQueryOptions` and `config.de
556
558
 
557
559
  `infiniteQueryOptions` also takes `getPreviousPageParam` and `maxPages`.
558
560
 
561
+ 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.
562
+
559
563
  `@raf-query/core`:
560
564
 
561
565
  | Export | Use |
package/dist/client.d.mts CHANGED
@@ -34,7 +34,10 @@ export interface QueryFilter {
34
34
  }
35
35
  /** `ensure` / `fetch` arguments: the input, then load options for paged factories. */
36
36
  export type LoadArgs<TFactory extends QueryFactory> = InputArgs<TFactory> | [input: InputOf<TFactory>, options: LoadOptions<TFactory>];
37
- /** A new value, or a function of the current one. */
37
+ /**
38
+ * A new value, or a function of the current one. `previous` is `undefined`
39
+ * both with no data and with `undefined` data.
40
+ */
38
41
  export type Updater<TData> = TData | ((previous: TData | undefined) => TData);
39
42
  /** Effect-returning twins of the client's async methods. */
40
43
  export interface QueryClientEffect {
@@ -100,7 +103,10 @@ export declare class QueryClient {
100
103
  remove(filter: QueryFilter): void;
101
104
  /** Write data for a factory call. Readers re-render without suspending. */
102
105
  setData<TFactory extends QueryFactory>(factory: TFactory, input: InputOf<TFactory>, updater: Updater<DataOf<TFactory>>): void;
103
- /** Cached data for a factory call, or `undefined`. */
106
+ /**
107
+ * Cached data for a factory call, or `undefined` if there is none. Data can
108
+ * itself be `undefined`; `getQuery(...).getState().hasData` tells them apart.
109
+ */
104
110
  getData<TFactory extends QueryFactory>(factory: TFactory, ...args: InputArgs<TFactory>): DataOf<TFactory> | undefined;
105
111
  /** Every entry, for devtools and integrations. */
106
112
  getAll(): ReadonlyArray<Query>;
package/dist/client.mjs CHANGED
@@ -115,9 +115,9 @@ var QueryClient = class {
115
115
  async fetch(factory, ...args) {
116
116
  const entry = this.#build(factory, args[0]);
117
117
  await this.#load(entry, args[1]?.pages);
118
- const data = typed(entry).getState().data;
119
- if (data === void 0) throw new Error(`Query ${entry.hash} has no data`);
120
- return data;
118
+ const state = entry.getState();
119
+ if (!state.hasData) throw new Error(`Query ${entry.hash} has no data`);
120
+ return unsafeCast(state.data);
121
121
  }
122
122
  async invalidate(target, input) {
123
123
  await Promise.all(this.#match(target, input).map((entry) => entry.invalidate()));
@@ -134,7 +134,10 @@ var QueryClient = class {
134
134
  const next = isUpdateFunction(updater) ? updater(typed(entry).getState().data) : updater;
135
135
  entry.setData(next);
136
136
  }
137
- /** Cached data for a factory call, or `undefined`. */
137
+ /**
138
+ * Cached data for a factory call, or `undefined` if there is none. Data can
139
+ * itself be `undefined`; `getQuery(...).getState().hasData` tells them apart.
140
+ */
138
141
  getData(factory, ...args) {
139
142
  const options = optionsFor(factory, args[0]);
140
143
  const entry = this.#cache.get(hashKey(options.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.16",
3
+ "version": "0.0.18",
4
4
  "description": "Framework-agnostic core of raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",