@raf-query/core 0.0.17 → 0.0.19

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.19](https://github.com/KATT/raf-query/compare/0.0.18...0.0.19) (2026-10-12)
4
+
5
+ ### Features
6
+
7
+ * **core:** type the cache's promises as Thenable ([#21](https://github.com/KATT/raf-query/issues/21)) ([c0541b0](https://github.com/KATT/raf-query/commit/c0541b0c14e687f25d47f25fe457e49b135d8c06))
8
+
9
+ ## [0.0.18](https://github.com/KATT/raf-query/compare/0.0.17...0.0.18) (2026-10-12)
10
+
11
+ ### Bug Fixes
12
+
13
+ - **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)
14
+
3
15
  ## [0.0.17](https://github.com/KATT/raf-query/compare/0.0.16...0.0.17) (2026-10-12)
4
16
 
5
17
  ### Features
6
18
 
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))
19
+ - **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
20
 
9
21
  ## [0.0.16](https://github.com/KATT/raf-query/compare/0.0.15...0.0.16) (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.
@@ -572,16 +574,17 @@ In the browser, retries pause while `navigator.onLine` is `false`: after the ret
572
574
  | `CancelledError` | Rejection of a request cancelled by `clear()` or a restart |
573
575
  | `QueryHandle<TFactory>` | Handle type for props |
574
576
  | `EnsuredQueryHandle<TFactory>` | Handle type for props fed by `ensure` |
577
+ | `Thenable<T>` | Promise with `status` / `value` / `reason`, read by `use()` |
575
578
  | `DataOf<TFactory>`, `InputOf<TFactory>` | A factory's data and input types |
576
579
 
577
580
  `@raf-query/react` (plus all of the above):
578
581
 
579
- | Export | Use |
580
- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
581
- | `QueryClientProvider` | Provides a client to the tree |
582
- | `useQueryClient()` | Read the client |
583
- | `useQuery(factory, handle, options?)` | `[Promise<Data>, controls]`; for paged factories `Data` is `{ pages, pageParams }` and controls include paging |
584
- | `useWaterfall(factory, input)` | Start a query during render and return its handle |
582
+ | Export | Use |
583
+ | ------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
584
+ | `QueryClientProvider` | Provides a client to the tree |
585
+ | `useQueryClient()` | Read the client |
586
+ | `useQuery(factory, handle, options?)` | `[Thenable<Data>, controls]`; for paged factories `Data` is `{ pages, pageParams }` and controls include paging |
587
+ | `useWaterfall(factory, input)` | Start a query during render and return its handle |
585
588
 
586
589
  ## Development
587
590
 
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
@@ -113,11 +113,8 @@ var QueryClient = class {
113
113
  }
114
114
  /** Like `ensure`, but resolves to the data. */
115
115
  async fetch(factory, ...args) {
116
- const entry = this.#build(factory, args[0]);
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;
116
+ const entry = typed(this.#build(factory, args[0]));
117
+ return this.#load(entry, args[1]?.pages);
121
118
  }
122
119
  async invalidate(target, input) {
123
120
  await Promise.all(this.#match(target, input).map((entry) => entry.invalidate()));
@@ -134,7 +131,10 @@ var QueryClient = class {
134
131
  const next = isUpdateFunction(updater) ? updater(typed(entry).getState().data) : updater;
135
132
  entry.setData(next);
136
133
  }
137
- /** Cached data for a factory call, or `undefined`. */
134
+ /**
135
+ * Cached data for a factory call, or `undefined` if there is none. Data can
136
+ * itself be `undefined`; `getQuery(...).getState().hasData` tells them apart.
137
+ */
138
138
  getData(factory, ...args) {
139
139
  const options = optionsFor(factory, args[0]);
140
140
  const entry = this.#cache.get(hashKey(options.queryKey));
@@ -198,11 +198,12 @@ var QueryClient = class {
198
198
  entry.setOptions(options, input);
199
199
  return entry;
200
200
  }
201
+ /** Resolves to the entry's data: the cached thenable, or the first fetch. */
201
202
  #load(entry, pages) {
202
203
  const state = entry.getState();
203
204
  if (!state.hasData) return entry.fetch({ pages });
204
205
  this.#revalidate(entry);
205
- return Promise.resolve(state.data);
206
+ return state.promise;
206
207
  }
207
208
  /**
208
209
  * Background refetch of cached data a verb reuses. A server client renders
package/dist/index.d.mts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Settled, allKeyed } from "./all-keyed.mjs";
2
2
  import { QueryKey, hashKey, matchesKey } from "./key.mjs";
3
3
  import { AnyQueryOptions, DataOf, EnsuredQueryHandle, InfiniteData, InfiniteQueryOptions, InputArgs, InputOf, IsPaged, LoadOptions, PageFunctionContext, PageOf, QueryBehavior, QueryFactory, QueryFnResult, QueryFunctionContext, QueryHandle, QueryOptions, RetryPolicy, infiniteQueryOptions, queryOptions } from "./options.mjs";
4
+ import { Thenable } from "./thenable.mjs";
4
5
  import { CancelledError, FetchRequest, Query, QueryState, SubscribeOptions } from "./query.mjs";
5
6
  import { CacheEvent, LoadArgs, QueryClient, QueryClientConfig, QueryClientEffect, QueryFilter, Updater } from "./client.mjs";
6
7
  import { DehydrateOptions, DehydratedQuery, DehydratedState, HydrateOptions, canDehydrate, dehydrate, dehydrateQuery, hydrate } from "./hydration.mjs";
7
8
  import { QueryObserver, QueryObserverOptions, QueryObserverResult } from "./observer.mjs";
8
- export { type AnyQueryOptions, type CacheEvent, CancelledError, type DataOf, type DehydrateOptions, type DehydratedQuery, type DehydratedState, type EnsuredQueryHandle, type FetchRequest, type HydrateOptions, type InfiniteData, type InfiniteQueryOptions, type InputArgs, type InputOf, type IsPaged, type LoadArgs, type LoadOptions, type PageFunctionContext, type PageOf, type Query, type QueryBehavior, QueryClient, type QueryClientConfig, type QueryClientEffect, type QueryFactory, type QueryFilter, type QueryFnResult, type QueryFunctionContext, type QueryHandle, type QueryKey, QueryObserver, type QueryObserverOptions, type QueryObserverResult, type QueryOptions, type QueryState, type RetryPolicy, type Settled, type SubscribeOptions, type Updater, allKeyed, canDehydrate, dehydrate, dehydrateQuery, hashKey, hydrate, infiniteQueryOptions, matchesKey, queryOptions };
9
+ export { type AnyQueryOptions, type CacheEvent, CancelledError, type DataOf, type DehydrateOptions, type DehydratedQuery, type DehydratedState, type EnsuredQueryHandle, type FetchRequest, type HydrateOptions, type InfiniteData, type InfiniteQueryOptions, type InputArgs, type InputOf, type IsPaged, type LoadArgs, type LoadOptions, type PageFunctionContext, type PageOf, type Query, type QueryBehavior, QueryClient, type QueryClientConfig, type QueryClientEffect, type QueryFactory, type QueryFilter, type QueryFnResult, type QueryFunctionContext, type QueryHandle, type QueryKey, QueryObserver, type QueryObserverOptions, type QueryObserverResult, type QueryOptions, type QueryState, type RetryPolicy, type Settled, type SubscribeOptions, type Thenable, type Updater, allKeyed, canDehydrate, dehydrate, dehydrateQuery, hashKey, hydrate, infiniteQueryOptions, matchesKey, queryOptions };
@@ -1,5 +1,6 @@
1
1
  import { QueryKey } from "./key.mjs";
2
2
  import { QueryFactory } from "./options.mjs";
3
+ import { Thenable } from "./thenable.mjs";
3
4
  import { Query } from "./query.mjs";
4
5
  //#region src/observer.d.ts
5
6
  /** How one reader observes its query. */
@@ -42,7 +43,7 @@ export interface QueryObserverResult<TData = unknown> {
42
43
  /** Key of the data in `promise`; differs from the query's while `isPrevious`. */
43
44
  readonly key: QueryKey;
44
45
  /** Settles with the data to show; see `QueryState.promise`. */
45
- readonly promise: Promise<TData>;
46
+ readonly promise: Thenable<TData>;
46
47
  refetch(): Promise<void>;
47
48
  /** When the data in `promise` was fetched (ms since epoch); `0` before that. */
48
49
  readonly updatedAt: number;
package/dist/query.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { QueryKey } from "./key.mjs";
2
2
  import { AnyQueryOptions, QueryFactory, QueryHandle } from "./options.mjs";
3
+ import { Thenable } from "./thenable.mjs";
3
4
  //#region src/query.d.ts
4
5
  /** A snapshot of one cache entry. Replaced, never mutated, on every change. */
5
6
  export interface QueryState<TData> {
@@ -20,9 +21,9 @@ export interface QueryState<TData> {
20
21
  readonly isStale: boolean;
21
22
  /**
22
23
  * Settles with `data`. A new promise is created per data version, already
23
- * fulfilled once data exists, so React's `use()` reads it synchronously.
24
+ * fulfilled once data exists, so it can be read synchronously.
24
25
  */
25
- readonly promise: Promise<TData>;
26
+ readonly promise: Thenable<TData>;
26
27
  readonly status: "error" | "pending" | "success";
27
28
  /** When `data` was fetched (ms since epoch); `0` without data. */
28
29
  readonly updatedAt: number;
@@ -0,0 +1,24 @@
1
+ //#region src/thenable.d.ts
2
+ /**
3
+ * Promises that carry their own result, in the fields React's `use()` reads
4
+ * before suspending (`status`, `value`, `reason`). A settled one can be read
5
+ * synchronously, so React renders it without a fallback.
6
+ */
7
+ interface PendingStatus {
8
+ readonly status: "pending";
9
+ }
10
+ interface FulfilledStatus<T> {
11
+ readonly status: "fulfilled";
12
+ readonly value: T;
13
+ }
14
+ interface RejectedStatus {
15
+ readonly reason: unknown;
16
+ readonly status: "rejected";
17
+ }
18
+ /**
19
+ * A promise that says whether it has settled, and with what. `status` moves
20
+ * from `"pending"` to `"fulfilled"` (with `value`) or `"rejected"` (with
21
+ * `reason`) once, when the promise settles.
22
+ */
23
+ export type Thenable<T> = Promise<T> & (FulfilledStatus<T> | PendingStatus | RejectedStatus);
24
+ //#endregion
package/dist/thenable.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  //#region src/thenable.ts
2
2
  /**
3
- * Promises that React's `use()` can read synchronously. React checks
4
- * `status` / `value` / `reason` on a thenable before suspending, so a
5
- * settled one renders without a fallback.
3
+ * Promises that carry their own result, in the fields React's `use()` reads
4
+ * before suspending (`status`, `value`, `reason`). A settled one can be read
5
+ * synchronously, so React renders it without a fallback.
6
6
  */
7
7
  const noop = () => {};
8
8
  function fulfilled(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/core",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "description": "Framework-agnostic core of raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",