@raf-query/core 0.10.1 → 0.10.2

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,14 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.2](https://github.com/KATT/raf-query/compare/0.10.1...0.10.2) (2026-10-16)
4
+
5
+ ### Features
6
+
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))
9
+
3
10
  ## [0.10.1](https://github.com/KATT/raf-query/compare/0.10.0...0.10.1) (2026-10-16)
4
11
 
5
12
  ### Features
6
13
 
7
- * **core:** export QueryClient, dehydrate, hydrate, allKeyed and the useQuery controls from @raf-query/core ([#193](https://github.com/KATT/raf-query/issues/193)) ([54492ab](https://github.com/KATT/raf-query/commit/54492abbe2966a0645e8a4c77f9c5ab31d6e3a20)), references [#194](https://github.com/KATT/raf-query/issues/194)
14
+ - **core:** export QueryClient, dehydrate, hydrate, allKeyed and the useQuery controls from @raf-query/core ([#193](https://github.com/KATT/raf-query/issues/193)) ([54492ab](https://github.com/KATT/raf-query/commit/54492abbe2966a0645e8a4c77f9c5ab31d6e3a20)), references [#194](https://github.com/KATT/raf-query/issues/194)
8
15
 
9
16
  ### Documentation
10
17
 
11
- * React Router client-only and server sections with one server client per request, Effect under Integrations, official docs links, and ensure waits like fetch in server-only code ([#192](https://github.com/KATT/raf-query/issues/192)) ([841ffcf](https://github.com/KATT/raf-query/commit/841ffcf4d0fa0a40dfc58dc65bfcfadca8948874)), references [#190](https://github.com/KATT/raf-query/issues/190)
18
+ - React Router client-only and server sections with one server client per request, Effect under Integrations, official docs links, and ensure waits like fetch in server-only code ([#192](https://github.com/KATT/raf-query/issues/192)) ([841ffcf](https://github.com/KATT/raf-query/commit/841ffcf4d0fa0a40dfc58dc65bfcfadca8948874)), references [#190](https://github.com/KATT/raf-query/issues/190)
12
19
 
13
20
  ## [0.10.0](https://github.com/KATT/raf-query/compare/0.9.2...0.10.0) (2026-10-16)
14
21
 
@@ -4,7 +4,7 @@ import { Thenable } from "./thenable.mjs";
4
4
  import { Query, query_d_exports } from "./query.mjs";
5
5
  import { Effect, Pipeable, Stream } from "effect";
6
6
  declare namespace observer_d_exports {
7
- export { Options, QueryObserver, Result$1 as Result, ResultKey, TypeId, changes, get, getOptimisticResultUnsafe, getUnsafe, isQueryObserver, make, makeUnsafe, setOptionsUnsafe, subscribeUnsafe, trackProp, trackResult };
7
+ export { Options, QueryObserver, Result$1 as Result, ResultKey, TypeId, changes, get, getOptimisticResultUnsafe, getUnsafe, isQueryObserver, make, makeUnsafe, setOptionsUnsafe, subscribeUnsafe, trackProp, trackResult, trackThenable };
8
8
  }
9
9
  export declare const TypeId = "~@raf-query/core/QueryObserver";
10
10
  export type TypeId = typeof TypeId;
@@ -96,6 +96,14 @@ export declare function changes<TFactory extends QueryFactory>(self: QueryObserv
96
96
  * changes.
97
97
  */
98
98
  export declare function trackResult<TFactory extends QueryFactory, TResult extends Result$1<DataOf<TFactory>>>(self: QueryObserver<TFactory>, result: TResult): TResult;
99
+ /**
100
+ * Wrap `promise`, the result's or one derived from it, so that reading its
101
+ * `status`, `value` or `reason` marks its settling as tracked. A first load
102
+ * settles its promise in place, without a new promise, so only readers of
103
+ * those fields need to hear about it. The mark is the wrapper's, shared by
104
+ * the observers that hand it out.
105
+ */
106
+ export declare function trackThenable<TFactory extends QueryFactory, TThenable extends Thenable<unknown>>(self: QueryObserver<TFactory>, promise: TThenable): TThenable;
99
107
  /** Mark `name` as read, for bindings that track access themselves. */
100
108
  export declare function trackProp<TFactory extends QueryFactory>(self: QueryObserver<TFactory>, name: ResultKey): void;
101
109
  //#endregion
package/dist/observer.mjs CHANGED
@@ -10,7 +10,8 @@ import { Effect, Exit, FiberHandle, Pipeable, Predicate, Scope } from "effect";
10
10
  * Query's `QueryObserver`: it derives the reader's result from the query's
11
11
  * state, keeps a previous query's data for `whilePending: "previous"`, runs
12
12
  * `refetchInterval`, and notifies its listeners only when a result property
13
- * they read changed (see {@link trackResult}). Bindings make one per hook.
13
+ * they read changed (see {@link trackResult}), or a promise whose `status`
14
+ * they read settled (see {@link trackThenable}). Bindings make one per hook.
14
15
  *
15
16
  * The `*Unsafe` functions read and update synchronously, for renders and
16
17
  * external stores. While it has listeners, the observer holds a `Scope` (a
@@ -30,7 +31,8 @@ var observer_exports = /* @__PURE__ */ __exportAll({
30
31
  setOptionsUnsafe: () => setOptionsUnsafe,
31
32
  subscribeUnsafe: () => subscribeUnsafe,
32
33
  trackProp: () => trackProp,
33
- trackResult: () => trackResult
34
+ trackResult: () => trackResult,
35
+ trackThenable: () => trackThenable
34
36
  });
35
37
  const TypeId = "~@raf-query/core/QueryObserver";
36
38
  const Proto = {
@@ -65,9 +67,16 @@ function implOf(self) {
65
67
  function isResultKey(name) {
66
68
  return Predicate.isString(name) && Object.hasOwn(resultKeys, name);
67
69
  }
68
- function shallowEqual(a, b) {
69
- for (const name of Object.keys(resultKeys)) if (isResultKey(name) && a[name] !== b[name]) return false;
70
- return true;
70
+ /** Whether `next.promise` has a different status than it had in the current result. */
71
+ function settled(self, next) {
72
+ return self.currentStatus !== next.promise.status;
73
+ }
74
+ function anyChanged(self, next) {
75
+ return settled(self, next) || Object.keys(resultKeys).some((name) => isResultKey(name) && self.current[name] !== next[name]);
76
+ }
77
+ function setCurrent(self, next) {
78
+ self.current = next;
79
+ self.currentStatus = next.promise.status;
71
80
  }
72
81
  /** The result for `query` with `options`, given the last query that had data. */
73
82
  function resultOf(query, options, lastWithData) {
@@ -97,13 +106,16 @@ function resultOf(query, options, lastWithData) {
97
106
  }
98
107
  /** Observe `query`. Doesn't subscribe to it until the observer has a listener. */
99
108
  function makeUnsafe(query, options = {}) {
109
+ const current = resultOf(query, options, void 0);
100
110
  return Object.assign(Object.create(Proto), {
101
- current: resultOf(query, options, void 0),
111
+ current,
112
+ currentStatus: current.promise.status,
102
113
  lastWithData: getUnsafe$1(query).hasData ? query : void 0,
103
114
  listeners: /* @__PURE__ */ new Set(),
104
115
  options,
105
116
  query,
106
117
  subscription: void 0,
118
+ tracked: void 0,
107
119
  trackedProps: /* @__PURE__ */ new Set()
108
120
  });
109
121
  }
@@ -126,7 +138,7 @@ function get(self) {
126
138
  function getOptimisticResultUnsafe(self, query, options) {
127
139
  const impl = implOf(self);
128
140
  const result = resultOf(query, options, impl.lastWithData);
129
- if (!shallowEqual(impl.current, result)) impl.current = result;
141
+ if (anyChanged(impl, result)) setCurrent(impl, result);
130
142
  return result;
131
143
  }
132
144
  /** Observe `query` with `options` from now on. */
@@ -185,22 +197,62 @@ function trackResult(self, result) {
185
197
  return Reflect.get(target, name, receiver);
186
198
  } });
187
199
  }
200
+ const settledFields = /* @__PURE__ */ new Set([
201
+ "reason",
202
+ "status",
203
+ "value"
204
+ ]);
205
+ /**
206
+ * One wrapper per promise, shared by every observer, so its identity is as
207
+ * stable as the promise's: an observer made by a render that suspended
208
+ * before committing is replaced on the next attempt, and `use()` expects
209
+ * the same promise again.
210
+ */
211
+ const wrappers = /* @__PURE__ */ new WeakMap();
212
+ /** Wrappers whose `status`, `value` or `reason` a reader has read. */
213
+ const settledReads = /* @__PURE__ */ new WeakSet();
214
+ /**
215
+ * Wrap `promise`, the result's or one derived from it, so that reading its
216
+ * `status`, `value` or `reason` marks its settling as tracked. A first load
217
+ * settles its promise in place, without a new promise, so only readers of
218
+ * those fields need to hear about it. The mark is the wrapper's, shared by
219
+ * the observers that hand it out.
220
+ */
221
+ function trackThenable(self, promise) {
222
+ let wrapper = wrappers.get(promise);
223
+ if (!wrapper) {
224
+ const created = new Proxy(promise, { get: (target, name) => {
225
+ if (settledFields.has(name)) settledReads.add(created);
226
+ const value = Reflect.get(target, name, target);
227
+ return Predicate.isFunction(value) ? value.bind(target) : value;
228
+ } });
229
+ wrappers.set(promise, created);
230
+ wrapper = created;
231
+ }
232
+ implOf(self).tracked = wrapper;
233
+ return wrapper;
234
+ }
235
+ function readsSettled(self) {
236
+ return self.tracked !== void 0 && settledReads.has(self.tracked);
237
+ }
188
238
  /** Mark `name` as read, for bindings that track access themselves. */
189
239
  function trackProp(self, name) {
190
240
  implOf(self).trackedProps.add(name);
191
241
  }
192
242
  /** Recompute the result, and notify listeners if a tracked property changed. */
193
243
  function updateResult(self) {
194
- const previous = self.current;
195
244
  const next = resultOf(self.query, self.options, self.lastWithData);
196
245
  if (getUnsafe$1(self.query).hasData) self.lastWithData = self.query;
197
- if (shallowEqual(previous, next)) return;
198
- self.current = next;
199
- if (shouldNotify(self, previous, next)) for (const listener of self.listeners) listener();
246
+ if (!anyChanged(self, next)) return;
247
+ const notify = shouldNotify(self, next);
248
+ setCurrent(self, next);
249
+ if (notify) for (const listener of self.listeners) listener();
200
250
  }
201
- function shouldNotify(self, previous, next) {
202
- if (self.trackedProps.size === 0) return true;
203
- for (const name of self.trackedProps) if (previous[name] !== next[name]) return true;
251
+ /** Whether something tracked differs in `next` from the current result. */
252
+ function shouldNotify(self, next) {
253
+ if (self.trackedProps.size === 0 && !readsSettled(self)) return true;
254
+ if (readsSettled(self) && settled(self, next)) return true;
255
+ for (const name of self.trackedProps) if (self.current[name] !== next[name]) return true;
204
256
  return false;
205
257
  }
206
258
  /**
@@ -245,4 +297,4 @@ function poll(query, interval) {
245
297
  }).pipe(Effect.delay(interval), Effect.forever);
246
298
  }
247
299
  //#endregion
248
- export { TypeId, changes, get, getOptimisticResultUnsafe, getUnsafe, isQueryObserver, make, makeUnsafe, observer_exports, setOptionsUnsafe, subscribeUnsafe, trackProp, trackResult };
300
+ export { TypeId, changes, get, getOptimisticResultUnsafe, getUnsafe, isQueryObserver, make, makeUnsafe, observer_exports, setOptionsUnsafe, subscribeUnsafe, trackProp, trackResult, trackThenable };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/core",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Framework-agnostic core of raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",