@raf-query/core 0.10.2 → 0.10.4

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,11 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.4](https://github.com/KATT/raf-query/compare/0.10.3...0.10.4) (2026-10-16)
4
+
5
+ ### Bug Fixes
6
+
7
+ * **core:** keep pages written during a paged refetch ([#215](https://github.com/KATT/raf-query/issues/215)) ([b6a5946](https://github.com/KATT/raf-query/commit/b6a5946e806e12e9b7df40e80ef0e532c0871b25)), references [#75](https://github.com/KATT/raf-query/issues/75)
8
+ * **react:** read promise queryFns as plain data on a client with a layer (1/2) ([#205](https://github.com/KATT/raf-query/issues/205)) ([a9b156e](https://github.com/KATT/raf-query/commit/a9b156edfa18b1e41e9ea929afc412252a61b83c))
9
+
10
+ ## [0.10.3](https://github.com/KATT/raf-query/compare/0.10.2...0.10.3) (2026-10-16)
11
+
12
+ ### Features
13
+
14
+ - **core:** add QueryBinding, a framework-free useQuery for bindings ([#197](https://github.com/KATT/raf-query/issues/197)) ([4865802](https://github.com/KATT/raf-query/commit/48658024f4d21d43ce8274de379a19c8898b6173)), references [#194](https://github.com/KATT/raf-query/issues/194)
15
+
3
16
  ## [0.10.2](https://github.com/KATT/raf-query/compare/0.10.1...0.10.2) (2026-10-16)
4
17
 
5
18
  ### Features
6
19
 
7
- * **react:** re-render useQuery readers of the thenable's status when the first load settles (2/3) ([#198](https://github.com/KATT/raf-query/issues/198)) ([436d83c](https://github.com/KATT/raf-query/commit/436d83cfd6a3944265eb56d5597c322342c5a6c6))
8
- * **react:** type useQuery's promise as a Thenable (1/3) ([#200](https://github.com/KATT/raf-query/issues/200)) ([47e5309](https://github.com/KATT/raf-query/commit/47e530957565a2bd345642ae7a4be5d3c501ffb2))
20
+ - **react:** re-render useQuery readers of the thenable's status when the first load settles (2/3) ([#198](https://github.com/KATT/raf-query/issues/198)) ([436d83c](https://github.com/KATT/raf-query/commit/436d83cfd6a3944265eb56d5597c322342c5a6c6))
21
+ - **react:** type useQuery's promise as a Thenable (1/3) ([#200](https://github.com/KATT/raf-query/issues/200)) ([47e5309](https://github.com/KATT/raf-query/commit/47e530957565a2bd345642ae7a4be5d3c501ffb2))
9
22
 
10
23
  ## [0.10.1](https://github.com/KATT/raf-query/compare/0.10.0...0.10.1) (2026-10-16)
11
24
 
@@ -0,0 +1,92 @@
1
+ import { QueryFactory, QueryHandle } from "./options.mjs";
2
+ import { Query, query_d_exports } from "./query.mjs";
3
+ import { QueryClient } from "./client.mjs";
4
+ import { Options, QueryObserver, observer_d_exports } from "./observer.mjs";
5
+ import { ReadValue, UseQueryControls, UseQueryOptions, UseQueryThrowOptions } from "./controls.mjs";
6
+ declare namespace binding_d_exports {
7
+ export { Hooks, QueryBinding, ReadOptions, Result, ResultWithData, Source, makeUnsafe, observerOptions, queryFor, subscribeUnsafe, syncUnsafe };
8
+ }
9
+ /** A read's options: the observer's, and `throwOnError` for typed errors. */
10
+ export type ReadOptions<TFactory extends QueryFactory, TThrow extends boolean | undefined = undefined> = UseQueryOptions & UseQueryThrowOptions<TFactory> & {
11
+ readonly throwOnError?: TThrow;
12
+ };
13
+ /**
14
+ * What a binding's `useQuery` returns: the query's controls, the `promise`
15
+ * of the data to show, and that `data` once it's there. Each property is a
16
+ * getter that calls {@link Hooks.track}, so destructuring reads it once.
17
+ */
18
+ export type Result<TFactory extends QueryFactory, TThrow extends boolean | undefined = undefined> = UseQueryControls<TFactory> & {
19
+ /** The promise's data once it has settled, else `undefined`. */
20
+ readonly data: ReadValue<TFactory, TThrow> | undefined;
21
+ /**
22
+ * Settles with the data to show. A new promise per data version, already
23
+ * settled once the cache has data.
24
+ */
25
+ readonly promise: Promise<ReadValue<TFactory, TThrow>>;
26
+ };
27
+ /** {@link Result} for a handle from `ensure` or `fetch`. */
28
+ export type ResultWithData<TFactory extends QueryFactory, TThrow extends boolean | undefined = undefined> = Result<TFactory, TThrow> & {
29
+ /** The query's data. The handle's `data` until the cache has data again. */
30
+ readonly data: ReadValue<TFactory, TThrow>;
31
+ };
32
+ /** How a framework plugs into a binding. */
33
+ export interface Hooks {
34
+ /**
35
+ * Runs when the result changed: a property something read, or a pending
36
+ * promise something read settling in place. Typically bumps the version.
37
+ */
38
+ readonly notify: () => void;
39
+ /**
40
+ * Runs on every read of the result, before the read, so the framework
41
+ * re-runs whatever read it after {@link Hooks.notify}. Typically reads a
42
+ * version signal.
43
+ */
44
+ readonly track: () => void;
45
+ /**
46
+ * Runs cache writes (starting a read can notify observers synchronously)
47
+ * outside the framework's tracking. Default: runs `f` as is.
48
+ */
49
+ readonly untracked?: <A>(f: () => A) => A;
50
+ }
51
+ /** Where a binding reads its handle and options from. */
52
+ export interface Source<TFactory extends QueryFactory> {
53
+ readonly handle: () => QueryHandle<TFactory>;
54
+ readonly options: () => UseQueryOptions & {
55
+ readonly throwOnError?: boolean;
56
+ };
57
+ }
58
+ /** One component's read of a query. */
59
+ export interface QueryBinding<TFactory extends QueryFactory = QueryFactory> {
60
+ readonly client: QueryClient<unknown>;
61
+ readonly observer: QueryObserver<TFactory>;
62
+ /** The result, the same object for the binding's life. */
63
+ readonly result: Result<TFactory, boolean>;
64
+ }
65
+ /**
66
+ * The query a handle points at. Calls the factory, so callers keep it for
67
+ * as long as the handle is the same.
68
+ */
69
+ export declare function queryFor<TFactory extends QueryFactory>(client: QueryClient<unknown>, factory: TFactory, handle: QueryHandle<TFactory>): Query<TFactory>;
70
+ /** The observer's part of a read's options. */
71
+ export declare function observerOptions(options: UseQueryOptions): Options;
72
+ /**
73
+ * Bind `factory` to the handle and options `source` returns. Reads the
74
+ * query and observes it, but doesn't subscribe: call {@link subscribeUnsafe}
75
+ * where the framework mounts, in the browser.
76
+ */
77
+ export declare function makeUnsafe<TFactory extends QueryFactory>(client: QueryClient<unknown>, factory: TFactory, source: Source<TFactory>, hooks: Hooks): QueryBinding<TFactory>;
78
+ /**
79
+ * Follow the source: switch to a new handle's query, or new options. Reads
80
+ * call it themselves, so markup that reads before the framework's effects
81
+ * run still sees the new query. Bindings also call it from an effect, so a
82
+ * new handle is observed even when nothing reads the result.
83
+ */
84
+ export declare function syncUnsafe<TFactory extends QueryFactory>(self: QueryBinding<TFactory>): void;
85
+ /**
86
+ * Notify {@link Hooks.notify} on result changes from now on. A server
87
+ * render reads once and never unmounts, so bindings call it in the browser
88
+ * only. Returns the unsubscribe.
89
+ */
90
+ export declare function subscribeUnsafe<TFactory extends QueryFactory>(self: QueryBinding<TFactory>): () => void;
91
+ //#endregion
92
+ export { binding_d_exports };
@@ -0,0 +1,186 @@
1
+ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
+ import { hashKey } from "./key.mjs";
3
+ import { readUnsafe } from "./query.mjs";
4
+ import { promiseControls, resultThenable } from "./controls.mjs";
5
+ import { getOptimisticResultUnsafe, getUnsafe, makeUnsafe as makeUnsafe$1, setOptionsUnsafe, subscribeUnsafe as subscribeUnsafe$1, trackProp } from "./observer.mjs";
6
+ import { Result } from "effect";
7
+ //#region src/binding.ts
8
+ /**
9
+ * What a framework's `useQuery` does, without the framework: one observer
10
+ * per component, following the handle and options it reads through
11
+ * getters, and a result of getters over the observer. A binding supplies
12
+ * {@link Hooks}: how a read is tracked, how a change is announced, and how
13
+ * cache writes run outside the framework's tracking. The Vue and Svelte
14
+ * packages are this plus their reactivity and lifecycle.
15
+ *
16
+ * Synchronous throughout, for renders.
17
+ */
18
+ var binding_exports = /* @__PURE__ */ __exportAll({
19
+ makeUnsafe: () => makeUnsafe,
20
+ observerOptions: () => observerOptions,
21
+ queryFor: () => queryFor,
22
+ subscribeUnsafe: () => subscribeUnsafe,
23
+ syncUnsafe: () => syncUnsafe
24
+ });
25
+ /**
26
+ * The query a handle points at. Calls the factory, so callers keep it for
27
+ * as long as the handle is the same.
28
+ */
29
+ function queryFor(client, factory, handle) {
30
+ const args = [handle.input];
31
+ return client.getQuery(factory, ...args);
32
+ }
33
+ /** The observer's part of a read's options. */
34
+ function observerOptions(options) {
35
+ return {
36
+ refetchInterval: options.refetchInterval,
37
+ refetchOnReconnect: options.refetchOnReconnect,
38
+ refetchOnWindowFocus: options.refetchOnWindowFocus,
39
+ whilePending: options.whilePending
40
+ };
41
+ }
42
+ function sameObserverOptions(a, b) {
43
+ return a.refetchInterval === b.refetchInterval && a.refetchOnReconnect === b.refetchOnReconnect && a.refetchOnWindowFocus === b.refetchOnWindowFocus && a.whilePending === b.whilePending;
44
+ }
45
+ function stateOf(self) {
46
+ return self;
47
+ }
48
+ const runNow = (f) => f();
49
+ /**
50
+ * Bind `factory` to the handle and options `source` returns. Reads the
51
+ * query and observes it, but doesn't subscribe: call {@link subscribeUnsafe}
52
+ * where the framework mounts, in the browser.
53
+ */
54
+ function makeUnsafe(client, factory, source, hooks) {
55
+ return (hooks.untracked ?? runNow)(() => {
56
+ const handle = source.handle();
57
+ const query = queryFor(client, factory, handle);
58
+ const options = observerOptions(source.options());
59
+ readUnsafe(query);
60
+ const state = {
61
+ awaited: /* @__PURE__ */ new WeakSet(),
62
+ client,
63
+ factory,
64
+ handle,
65
+ hooks,
66
+ observer: makeUnsafe$1(query, options),
67
+ options,
68
+ query,
69
+ source
70
+ };
71
+ return Object.assign(state, { result: resultOf(state) });
72
+ });
73
+ }
74
+ /**
75
+ * Follow the source: switch to a new handle's query, or new options. Reads
76
+ * call it themselves, so markup that reads before the framework's effects
77
+ * run still sees the new query. Bindings also call it from an effect, so a
78
+ * new handle is observed even when nothing reads the result.
79
+ */
80
+ function syncUnsafe(self) {
81
+ sync(stateOf(self));
82
+ }
83
+ function sync(impl) {
84
+ const handle = impl.source.handle();
85
+ const options = observerOptions(impl.source.options());
86
+ const newQuery = handle !== impl.handle && hashKey(handle.key) !== impl.query.hash;
87
+ impl.handle = handle;
88
+ if (!newQuery && sameObserverOptions(options, impl.options)) return;
89
+ (impl.hooks.untracked ?? runNow)(() => {
90
+ if (newQuery) {
91
+ impl.query = queryFor(impl.client, impl.factory, handle);
92
+ readUnsafe(impl.query);
93
+ }
94
+ impl.options = options;
95
+ setOptionsUnsafe(impl.observer, impl.query, options);
96
+ });
97
+ }
98
+ /**
99
+ * Notify {@link Hooks.notify} on result changes from now on. A server
100
+ * render reads once and never unmounts, so bindings call it in the browser
101
+ * only. Returns the unsubscribe.
102
+ */
103
+ function subscribeUnsafe(self) {
104
+ const impl = stateOf(self);
105
+ return subscribeUnsafe$1(impl.observer, impl.hooks.notify);
106
+ }
107
+ function resultOf(impl) {
108
+ const { hooks, observer } = impl;
109
+ function read(name) {
110
+ sync(impl);
111
+ hooks.track();
112
+ trackProp(observer, name);
113
+ return impl.client.isServer ? getOptimisticResultUnsafe(observer, impl.query, impl.options) : getUnsafe(observer);
114
+ }
115
+ function throwOnError() {
116
+ return impl.source.options().throwOnError ?? impl.client.throwOnError;
117
+ }
118
+ function shown() {
119
+ const promise = read("promise").promise;
120
+ if (promise.status === "pending" && !impl.awaited.has(promise)) {
121
+ impl.awaited.add(promise);
122
+ promise.then(hooks.notify, hooks.notify);
123
+ }
124
+ return throwOnError() ? promise : resultThenable(promise);
125
+ }
126
+ function controls() {
127
+ return promiseControls(getUnsafe(observer));
128
+ }
129
+ return {
130
+ get data() {
131
+ const promise = shown();
132
+ if (promise.status === "fulfilled") return promise.value;
133
+ const handle = impl.handle;
134
+ if (!("data" in handle)) return;
135
+ const data = handle.data;
136
+ return throwOnError() ? data : Result.succeed(data);
137
+ },
138
+ get error() {
139
+ return read("error").error;
140
+ },
141
+ fetchNextPage: () => controls().fetchNextPage(),
142
+ fetchPreviousPage: () => controls().fetchPreviousPage(),
143
+ get hash() {
144
+ return read("hash").hash;
145
+ },
146
+ get hasNextPage() {
147
+ return read("hasNextPage").hasNextPage;
148
+ },
149
+ get hasPreviousPage() {
150
+ return read("hasPreviousPage").hasPreviousPage;
151
+ },
152
+ get input() {
153
+ return read("input").input;
154
+ },
155
+ get isFetching() {
156
+ return read("isFetching").isFetching;
157
+ },
158
+ get isFetchingNextPage() {
159
+ return read("isFetchingNextPage").isFetchingNextPage;
160
+ },
161
+ get isFetchingPreviousPage() {
162
+ return read("isFetchingPreviousPage").isFetchingPreviousPage;
163
+ },
164
+ get isPending() {
165
+ return read("isPending").isPending;
166
+ },
167
+ get isPrevious() {
168
+ return read("isPrevious").isPrevious;
169
+ },
170
+ get isStale() {
171
+ return read("isStale").isStale;
172
+ },
173
+ get key() {
174
+ return read("key").key;
175
+ },
176
+ get promise() {
177
+ return shown();
178
+ },
179
+ refetch: () => controls().refetch(),
180
+ get updatedAt() {
181
+ return read("updatedAt").updatedAt;
182
+ }
183
+ };
184
+ }
185
+ //#endregion
186
+ export { binding_exports, makeUnsafe, observerOptions, queryFor, subscribeUnsafe, syncUnsafe };
package/dist/client.d.mts CHANGED
@@ -17,7 +17,8 @@ export interface QueryClientConfig<TServices = never> extends Config {
17
17
  * When `false`, `useQuery` for an Effect `queryFn` (a typed `ErrorOf`)
18
18
  * returns a `Result` in the first tuple slot instead of a promise that
19
19
  * rejects. Defaults to `false` when `layer` is set, `true` otherwise.
20
- * Promise `queryFn`s always throw; this flag does not apply to them.
20
+ * Promise `queryFn`s always throw, whatever this flag says: reads tell
21
+ * them apart by what the `queryFn` returned ({@link Query.returnsEffectUnsafe}).
21
22
  */
22
23
  readonly throwOnError?: boolean;
23
24
  }
@@ -1,6 +1,7 @@
1
1
  import { QueryKey } from "./key.mjs";
2
2
  import { DataOf, ErrorOf, InputOf, IsPaged, QueryFactory } from "./options.mjs";
3
3
  import { Thenable } from "./thenable.mjs";
4
+ import { Query, query_d_exports } from "./query.mjs";
4
5
  import { Options, Result as Result$1, observer_d_exports } from "./observer.mjs";
5
6
  import { Result } from "effect";
6
7
  //#region src/controls.d.ts
@@ -65,6 +66,13 @@ export type HasTypedError<TFactory extends QueryFactory> = unknown extends Error
65
66
  * (Result), matching a client with a `layer`.
66
67
  */
67
68
  export type ThrowsOnError<TFactory extends QueryFactory, TThrow extends boolean | undefined> = HasTypedError<TFactory> extends false ? true : TThrow extends boolean ? TThrow : false;
69
+ /**
70
+ * {@link ThrowsOnError} at runtime, for bindings: a read of `query` throws
71
+ * when `throwOnError` (the call's, else the client's) is `true`, or when its
72
+ * `queryFn` returns a promise or plain value, whose error isn't typed. A
73
+ * query whose `queryFn` hasn't run yet follows `throwOnError`.
74
+ */
75
+ export declare function throwsOnError<TFactory extends QueryFactory>(query: Query<TFactory>, throwOnError: boolean): boolean;
68
76
  /** What a read settles with: the data, or a `Result` when it doesn't throw. */
69
77
  export type ReadValue<TFactory extends QueryFactory, TThrow extends boolean | undefined = undefined> = ThrowsOnError<TFactory, TThrow> extends true ? DataOf<TFactory> : Result.Result<DataOf<TFactory>, ErrorOf<TFactory>>;
70
78
  type Command = "fetchNextPage" | "fetchPreviousPage" | "refetch";
package/dist/controls.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { fulfilled, pending } from "./thenable.mjs";
2
+ import { returnsEffectUnsafe } from "./query.mjs";
2
3
  import { Effect, Result } from "effect";
3
4
  //#region src/controls.ts
4
5
  /**
@@ -6,6 +7,15 @@ import { Effect, Result } from "effect";
6
7
  * observer's result with Promise commands, and the types every binding
7
8
  * shares. The React, Svelte and Vue packages build their reads on this.
8
9
  */
10
+ /**
11
+ * {@link ThrowsOnError} at runtime, for bindings: a read of `query` throws
12
+ * when `throwOnError` (the call's, else the client's) is `true`, or when its
13
+ * `queryFn` returns a promise or plain value, whose error isn't typed. A
14
+ * query whose `queryFn` hasn't run yet follows `throwOnError`.
15
+ */
16
+ function throwsOnError(query, throwOnError) {
17
+ return throwOnError || returnsEffectUnsafe(query) === false;
18
+ }
9
19
  const commands = /* @__PURE__ */ new WeakMap();
10
20
  /** One stable Promise function per command Effect, so identities track the query's. */
11
21
  function commandFor(effect) {
@@ -56,4 +66,4 @@ function resultThenable(promise) {
56
66
  return next;
57
67
  }
58
68
  //#endregion
59
- export { promiseControls, resultThenable };
69
+ export { promiseControls, resultThenable, throwsOnError };
package/dist/entry.d.mts CHANGED
@@ -79,6 +79,12 @@ export interface Entry extends Query {
79
79
  pending: Pending<unknown> | undefined;
80
80
  /** The last reader left during the request in flight: it retries no more. */
81
81
  retriesStopped: boolean;
82
+ /**
83
+ * The last `queryFn` call returned an Effect (`true`) or a promise or
84
+ * plain value (`false`), here or on the cache it was hydrated from.
85
+ * `undefined` until one has run.
86
+ */
87
+ returnsEffect: boolean | undefined;
82
88
  /** Open while the entry is cached; owns its fibers. */
83
89
  readonly scope: Scope.Closeable;
84
90
  /**
package/dist/entry.mjs CHANGED
@@ -76,6 +76,7 @@ function make(cache, key, hash) {
76
76
  options: void 0,
77
77
  pending: first,
78
78
  retriesStopped: false,
79
+ returnsEffect: void 0,
79
80
  scope,
80
81
  services: cache.services,
81
82
  signalRead: false,
@@ -475,7 +476,8 @@ function attempt(entry, call) {
475
476
  entry.signalRead = true;
476
477
  return signal;
477
478
  });
478
- if (Effect.isEffect(result)) entry.signalRead = true;
479
+ entry.returnsEffect = Effect.isEffect(result);
480
+ if (entry.returnsEffect) entry.signalRead = true;
479
481
  return result;
480
482
  }).pipe(Effect.tapError(() => Effect.sync(() => set(entry, { failureCount: entry.state.failureCount + 1 }))));
481
483
  const schedule = retrySchedule({
@@ -18,6 +18,8 @@ export interface DehydratedResult {
18
18
  */
19
19
  export interface DehydratedQuery {
20
20
  readonly data?: unknown;
21
+ /** Whether the sender's `queryFn` returned an Effect, when it ran there. */
22
+ readonly effect?: boolean;
21
23
  readonly key: QueryKey;
22
24
  /** The query's `meta`, for receivers that haven't called its factory. */
23
25
  readonly meta?: QueryMeta;
@@ -1,5 +1,5 @@
1
1
  import { entryOf, isBrowserOnly, resume, revalidate, setData } from "./entry.mjs";
2
- import { getUnsafe, metaUnsafe } from "./query.mjs";
2
+ import { getUnsafe, metaUnsafe, returnsEffectUnsafe } from "./query.mjs";
3
3
  import { Data, Effect, Queue, Stream } from "effect";
4
4
  //#region src/hydration.ts
5
5
  /**
@@ -19,6 +19,15 @@ function canDehydrate(query) {
19
19
  return state.hasData || state.isFetching;
20
20
  }
21
21
  function dehydrateUnsafe(query, options) {
22
+ const sent = dehydrateState(query, options);
23
+ const effect = returnsEffectUnsafe(query);
24
+ return effect === void 0 ? sent : {
25
+ ...sent,
26
+ effect
27
+ };
28
+ }
29
+ /** {@link dehydrateUnsafe} without `effect`. */
30
+ function dehydrateState(query, options) {
22
31
  const state = getUnsafe(query);
23
32
  const serialize = options.serializeData ?? identity;
24
33
  const meta = metaUnsafe(query);
@@ -123,6 +132,7 @@ function hydrateEntries(entryFor, state, options) {
123
132
  for (const query of state.queries) {
124
133
  const entry = entryFor(query.key);
125
134
  if (!entry.options && query.meta !== void 0) entry.hydratedMeta = query.meta;
135
+ entry.returnsEffect ??= query.effect;
126
136
  if (query.updatedAt > 0) takeNewer(entry, deserialize(query.data), query.updatedAt);
127
137
  else if (query.promise) {
128
138
  const result = Promise.resolve(query.promise).then((sent) => ({
package/dist/index.d.mts CHANGED
@@ -8,5 +8,6 @@ import { DehydrateOptions, DehydratedQuery, DehydratedResult, DehydratedState, D
8
8
  import { cache_d_exports } from "./cache.mjs";
9
9
  import { QueryClient, QueryClientConfig, dehydrate, hydrate } from "./client.mjs";
10
10
  import { observer_d_exports } from "./observer.mjs";
11
- import { HasTypedError, PagingControls, PromiseControls, QueryControls, ReadValue, ThrowsOnError, UseQueryControls, UseQueryOptions, UseQueryThrowOptions, promiseControls, resultThenable } from "./controls.mjs";
12
- export { type AnyQueryOptions, type DataOf, type DehydrateOptions, type DehydratedQuery, type DehydratedResult, type DehydratedState, type DehydratedStream, type ErrorOf, type HasTypedError, type HydrateOptions, type InfiniteData, type InfiniteQueryOptions, type InfiniteQueryTypes, type InputArgs, type InputOf, type IsPaged, type LoadOptions, type PageFunctionContext, type PageOf, type PagedQueryFactory, type PagingControls, type Pending, platform_d_exports as Platform, type PromiseControls, query_d_exports as Query, type QueryBehavior, cache_d_exports as QueryCache, QueryClient, type QueryClientConfig, type QueryControls, type QueryFactory, type QueryFnResult, type QueryFunctionContext, type QueryHandle, type QueryHandleWithData, type QueryKey, type QueryMeta, observer_d_exports as QueryObserver, type QueryOptions, type QueryTypes, type ReadValue, RedactedError, type Register, type RetryPolicy, type ServicesOf, type Settled, type Thenable, type ThrowsOnError, type UseQueryControls, type UseQueryOptions, type UseQueryThrowOptions, allKeyed, canDehydrate, dehydrate, dehydrateQuery, fulfilled, hashKey, hydrate, infiniteQueryOptions, matchesKey, pending, promiseControls, queryOptions, resultThenable };
11
+ import { HasTypedError, PagingControls, PromiseControls, QueryControls, ReadValue, ThrowsOnError, UseQueryControls, UseQueryOptions, UseQueryThrowOptions, promiseControls, resultThenable, throwsOnError } from "./controls.mjs";
12
+ import { binding_d_exports } from "./binding.mjs";
13
+ export { type AnyQueryOptions, type DataOf, type DehydrateOptions, type DehydratedQuery, type DehydratedResult, type DehydratedState, type DehydratedStream, type ErrorOf, type HasTypedError, type HydrateOptions, type InfiniteData, type InfiniteQueryOptions, type InfiniteQueryTypes, type InputArgs, type InputOf, type IsPaged, type LoadOptions, type PageFunctionContext, type PageOf, type PagedQueryFactory, type PagingControls, type Pending, platform_d_exports as Platform, type PromiseControls, query_d_exports as Query, type QueryBehavior, binding_d_exports as QueryBinding, cache_d_exports as QueryCache, QueryClient, type QueryClientConfig, type QueryControls, type QueryFactory, type QueryFnResult, type QueryFunctionContext, type QueryHandle, type QueryHandleWithData, type QueryKey, type QueryMeta, observer_d_exports as QueryObserver, type QueryOptions, type QueryTypes, type ReadValue, RedactedError, type Register, type RetryPolicy, type ServicesOf, type Settled, type Thenable, type ThrowsOnError, type UseQueryControls, type UseQueryOptions, type UseQueryThrowOptions, allKeyed, canDehydrate, dehydrate, dehydrateQuery, fulfilled, hashKey, hydrate, infiniteQueryOptions, matchesKey, pending, promiseControls, queryOptions, resultThenable, throwsOnError };
package/dist/index.mjs CHANGED
@@ -7,6 +7,7 @@ import { RedactedError, canDehydrate, dehydrateQuery } from "./hydration.mjs";
7
7
  import { infiniteQueryOptions, queryOptions } from "./options.mjs";
8
8
  import { cache_exports } from "./cache.mjs";
9
9
  import { QueryClient, dehydrate, hydrate } from "./client.mjs";
10
- import { promiseControls, resultThenable } from "./controls.mjs";
10
+ import { promiseControls, resultThenable, throwsOnError } from "./controls.mjs";
11
11
  import { observer_exports } from "./observer.mjs";
12
- export { platform_exports as Platform, query_exports as Query, cache_exports as QueryCache, QueryClient, observer_exports as QueryObserver, RedactedError, allKeyed, canDehydrate, dehydrate, dehydrateQuery, fulfilled, hashKey, hydrate, infiniteQueryOptions, matchesKey, pending, promiseControls, queryOptions, resultThenable };
12
+ import { binding_exports } from "./binding.mjs";
13
+ export { platform_exports as Platform, query_exports as Query, binding_exports as QueryBinding, cache_exports as QueryCache, QueryClient, observer_exports as QueryObserver, RedactedError, allKeyed, canDehydrate, dehydrate, dehydrateQuery, fulfilled, hashKey, hydrate, infiniteQueryOptions, matchesKey, pending, promiseControls, queryOptions, resultThenable, throwsOnError };
package/dist/infinite.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { replaceEqualDeep } from "./share.mjs";
2
- import { Effect, Predicate } from "effect";
2
+ import { Effect, Option, Predicate } from "effect";
3
3
  //#region src/infinite.ts
4
4
  function isInfiniteData(data) {
5
5
  return Predicate.hasProperty(data, "pages") && Predicate.hasProperty(data, "pageParams") && Array.isArray(data.pages) && Array.isArray(data.pageParams);
@@ -31,6 +31,31 @@ function addPage(data, page, pageParam, direction, maxPages) {
31
31
  function edgeParam(data, direction) {
32
32
  return direction === "forward" ? data.pageParams.at(-1) : data.pageParams[0];
33
33
  }
34
+ /** The page `data` holds for `pageParam`, compared deeply. */
35
+ function pageFor(data, pageParam) {
36
+ const index = data?.pageParams.findIndex((param) => replaceEqualDeep(param, pageParam) === param) ?? -1;
37
+ return index === -1 ? Option.none() : Option.some(data?.pages[index]);
38
+ }
39
+ /**
40
+ * `data`, a full refetch's pages, with each page that `latest` replaced
41
+ * after the refetch started reading it: that write is newer than the read.
42
+ * A page written before its read started is read again, and the read wins.
43
+ */
44
+ function keepWrites(data, readFrom, latest) {
45
+ let changed = false;
46
+ const pages = data.pages.map((page, index) => {
47
+ const pageParam = data.pageParams[index];
48
+ const read = readFrom.find((entry) => replaceEqualDeep(entry.pageParam, pageParam) === entry.pageParam);
49
+ const now = pageFor(latest, pageParam);
50
+ if (!read || Option.isNone(now) || Option.isSome(read.seen) && read.seen.value === now.value) return page;
51
+ changed = true;
52
+ return now.value;
53
+ });
54
+ return changed ? {
55
+ pageParams: data.pageParams,
56
+ pages
57
+ } : data;
58
+ }
34
59
  /**
35
60
  * A paged query's request. With a `direction`, adds one page at that end of
36
61
  * the entry's data, read again with `latest` once the page loads, so data
@@ -38,7 +63,8 @@ function edgeParam(data, direction) {
38
63
  * longer ends at the page param the request started from, the write
39
64
  * replaced the list, and the page is dropped. Without a `direction`, loads
40
65
  * from the first page param again: as many pages as `current` has, or
41
- * `pages` if that's more, stopping at the last page.
66
+ * `pages` if that's more, stopping at the last page. A page written while
67
+ * the refetch was reading it, or after, stays as written ({@link keepWrites}).
42
68
  */
43
69
  const fetchPages = /*#__PURE__*/ Effect.fnUntraced(function* (options, current, request, fetchPage, latest) {
44
70
  const direction = request.direction;
@@ -56,6 +82,7 @@ const fetchPages = /*#__PURE__*/ Effect.fnUntraced(function* (options, current,
56
82
  pageParams: [],
57
83
  pages: []
58
84
  };
85
+ const readFrom = [];
59
86
  let pageParam = current && current.pageParams.length > 0 ? current.pageParams[0] : options.initialPageParam;
60
87
  for (let index = 0; index < count; index++) {
61
88
  if (index > 0) {
@@ -63,10 +90,14 @@ const fetchPages = /*#__PURE__*/ Effect.fnUntraced(function* (options, current,
63
90
  if (next == null) break;
64
91
  pageParam = next;
65
92
  }
93
+ readFrom.push({
94
+ pageParam,
95
+ seen: pageFor(latest(), pageParam)
96
+ });
66
97
  const page = yield* fetchPage(pageParam, "forward");
67
98
  data = addPage(data, page, pageParam, "forward", options.maxPages);
68
99
  }
69
- return data;
100
+ return keepWrites(data, readFrom, latest());
70
101
  });
71
102
  //#endregion
72
103
  export { fetchPages, isInfiniteData, nextPageParam, previousPageParam };
package/dist/query.d.mts CHANGED
@@ -5,7 +5,7 @@ import { Thenable } from "./thenable.mjs";
5
5
  import { BrowserOnlyError as BrowserOnlyError$1, CancelledError as CancelledError$1, TypeId as TypeId$1, entry_d_exports } from "./entry.mjs";
6
6
  import { Effect, Pipeable, Stream } from "effect";
7
7
  declare namespace query_d_exports {
8
- export { BrowserOnlyError, CancelledError, FetchRequest, Query, State, SubscribeOptions, TypeId, changes, fetch, fetchNextPage, fetchPreviousPage, get, getUnsafe, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isQuery, isStaleUnsafe, metaUnsafe, observerCountUnsafe, readUnsafe, refetch, reset, revalidateUnsafe, subscribeUnsafe };
8
+ export { BrowserOnlyError, CancelledError, FetchRequest, Query, State, SubscribeOptions, TypeId, changes, fetch, fetchNextPage, fetchPreviousPage, get, getUnsafe, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isQuery, isStaleUnsafe, metaUnsafe, observerCountUnsafe, readUnsafe, refetch, reset, returnsEffectUnsafe, revalidateUnsafe, subscribeUnsafe };
9
9
  }
10
10
  export declare const TypeId: typeof TypeId$1;
11
11
  export type TypeId = typeof TypeId;
@@ -98,6 +98,13 @@ export declare function reset<TFactory extends QueryFactory>(self: Query<TFactor
98
98
  * with, before any), else the cache default's.
99
99
  */
100
100
  export declare function metaUnsafe<TFactory extends QueryFactory>(self: Query<TFactory>): QueryMeta | undefined;
101
+ /**
102
+ * Whether the query's `queryFn` returns an Effect (`true`) or a promise or
103
+ * plain value (`false`), as of its last call, on this cache or the one its
104
+ * data was hydrated from. `undefined` until one has run. Bindings read typed
105
+ * errors as a `Result` only for Effects.
106
+ */
107
+ export declare function returnsEffectUnsafe<TFactory extends QueryFactory>(self: Query<TFactory>): boolean | undefined;
101
108
  /** The entry's current state. */
102
109
  export declare function get<TFactory extends QueryFactory>(self: Query<TFactory>): Effect.Effect<State<DataOf<TFactory>>>;
103
110
  /** {@link get}, synchronously. */
package/dist/query.mjs CHANGED
@@ -31,6 +31,7 @@ var query_exports = /* @__PURE__ */ __exportAll({
31
31
  readUnsafe: () => readUnsafe,
32
32
  refetch: () => refetch,
33
33
  reset: () => reset,
34
+ returnsEffectUnsafe: () => returnsEffectUnsafe,
34
35
  revalidateUnsafe: () => revalidateUnsafe,
35
36
  subscribeUnsafe: () => subscribeUnsafe
36
37
  });
@@ -85,6 +86,15 @@ function reset(self) {
85
86
  function metaUnsafe(self) {
86
87
  return metaOf(entryOf(self));
87
88
  }
89
+ /**
90
+ * Whether the query's `queryFn` returns an Effect (`true`) or a promise or
91
+ * plain value (`false`), as of its last call, on this cache or the one its
92
+ * data was hydrated from. `undefined` until one has run. Bindings read typed
93
+ * errors as a `Result` only for Effects.
94
+ */
95
+ function returnsEffectUnsafe(self) {
96
+ return entryOf(self).returnsEffect;
97
+ }
88
98
  /** The entry's current state. */
89
99
  function get(self) {
90
100
  return Effect.sync(() => getUnsafe(self));
@@ -134,4 +144,4 @@ function observerCountUnsafe(self) {
134
144
  return entryOf(self).listeners.size;
135
145
  }
136
146
  //#endregion
137
- export { BrowserOnlyError, CancelledError, TypeId, changes, fetch, fetchNextPage, fetchPreviousPage, get, getUnsafe, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isQuery, isStaleUnsafe, metaUnsafe, observerCountUnsafe, query_exports, readUnsafe, refetch, reset, revalidateUnsafe, subscribeUnsafe };
147
+ export { BrowserOnlyError, CancelledError, TypeId, changes, fetch, fetchNextPage, fetchPreviousPage, get, getUnsafe, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isQuery, isStaleUnsafe, metaUnsafe, observerCountUnsafe, query_exports, readUnsafe, refetch, reset, returnsEffectUnsafe, revalidateUnsafe, subscribeUnsafe };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/core",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
4
4
  "description": "Framework-agnostic core of raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",