@raf-query/core 0.0.9 → 0.0.11

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.11](https://github.com/KATT/raf-query/compare/0.0.10...0.0.11) (2026-10-12)
4
+
5
+ ### Features
6
+
7
+ * **core:** structurally share new data with the old (structuralSharing) ([#15](https://github.com/KATT/raf-query/issues/15)) ([bf4392a](https://github.com/KATT/raf-query/commit/bf4392a56936ead6fb8b2726981cb19dadf51304))
8
+
9
+ ## [0.0.10](https://github.com/KATT/raf-query/compare/0.0.9...0.0.10) (2026-10-12)
10
+
11
+ - move the TanStack Query experiment into .experimental folders ([#12](https://github.com/KATT/raf-query/issues/12)) ([e9613a6](https://github.com/KATT/raf-query/commit/e9613a608f100fb17de6baf37c0f1fcfa479e909))
12
+
3
13
  ## [0.0.9](https://github.com/KATT/raf-query/compare/0.0.8...0.0.9) (2026-10-12)
4
14
 
5
15
  ### Bug Fixes
6
16
 
7
- * **examples:** let the client pick new comment ids ([#9](https://github.com/KATT/raf-query/issues/9)) ([de3a723](https://github.com/KATT/raf-query/commit/de3a7238b2568458a9771450a8c94d82576f5f7d))
17
+ - **examples:** let the client pick new comment ids ([#9](https://github.com/KATT/raf-query/issues/9)) ([de3a723](https://github.com/KATT/raf-query/commit/de3a7238b2568458a9771450a8c94d82576f5f7d))
8
18
 
9
19
  ## [0.0.8](https://github.com/KATT/raf-query/compare/0.0.7...0.0.8) (2026-10-12)
10
20
 
package/README.md CHANGED
@@ -179,7 +179,7 @@ function PostPage() {
179
179
  | `hash` | Stable string form of the handle's key, for `key` props on Suspense boundaries |
180
180
  | `refetch()` | Refetch now. Returns a promise, so it can be awaited inside a transition or action |
181
181
 
182
- When the cache updates — refetch, invalidation, `setData` — the hook returns a new, pre-resolved promise and the component re-renders without suspending. When the **first** load fails, the promise rejects and `use()` throws to the nearest error boundary. Resetting that boundary refetches the query.
182
+ When the cache updates — refetch, invalidation, `setData` — the hook returns a new, pre-resolved promise and the component re-renders without suspending. New data is structurally shared with the old: objects and arrays that are deeply equal keep their identity, and a refetch that returns equal data keeps the same promise, so nothing re-renders. When the **first** load fails, the promise rejects and `use()` throws to the nearest error boundary. Resetting that boundary refetches the query.
183
183
 
184
184
  Queries do not refetch on mount. They refetch when a loader starts them again, when invalidated, on window focus or reconnect while stale, on `refetch()`, or on `refetchInterval`.
185
185
 
@@ -539,6 +539,7 @@ Query options, accepted by `queryOptions`, `infiniteQueryOptions` and `config.de
539
539
  | `retry` (count, `boolean`, predicate, `Schedule`) | `3`; `0` on the server |
540
540
  | `retryDelay` (ms, or a function of the failure count) | Doubling from 1 s, at most 30 s |
541
541
  | `refetchOnWindowFocus`, `refetchOnReconnect` | `true` (only read, stale queries refetch) |
542
+ | `structuralSharing` (keep deeply equal parts of data) | `true` |
542
543
 
543
544
  `infiniteQueryOptions` also takes `getPreviousPageParam` and `maxPages`.
544
545
 
package/dist/key.mjs CHANGED
@@ -33,4 +33,4 @@ function matchesKey(key, prefix) {
33
33
  return prefix.length <= key.length && prefix.every((part, index) => partialMatch(key[index], part));
34
34
  }
35
35
  //#endregion
36
- export { hashKey, matchesKey };
36
+ export { hashKey, isPlainRecord, matchesKey };
@@ -34,6 +34,12 @@ export interface QueryBehavior {
34
34
  readonly retryDelay?: number | ((failureCount: number, error: unknown) => number);
35
35
  /** How long data counts as fresh, in ms. Fresh data is never refetched. */
36
36
  readonly staleTime?: number;
37
+ /**
38
+ * Keep the parts of new data that are deeply equal to the old data, so
39
+ * unchanged objects keep their identity and unchanged data keeps its
40
+ * promise. Compares plain objects and arrays. Default `true`.
41
+ */
42
+ readonly structuralSharing?: boolean;
37
43
  }
38
44
  export interface QueryOptions<TData> extends QueryBehavior {
39
45
  readonly kind: "query";
package/dist/query.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { scheduleNotify } from "./notify.mjs";
2
+ import { replaceEqualDeep } from "./share.mjs";
2
3
  import { fulfilled, pending } from "./thenable.mjs";
3
4
  import { Cause, Effect, Exit, Predicate, Schedule } from "effect";
4
5
  //#region src/query.ts
@@ -260,10 +261,8 @@ var QueryEntry = class {
260
261
  #settle(run, exit) {
261
262
  if (this.#inFlight !== run) return;
262
263
  this.#inFlight = void 0;
263
- if (Exit.isSuccess(exit)) {
264
- this.#succeed(exit.value, Date.now());
265
- run.deferred.resolve(exit.value);
266
- } else if (Cause.hasInterruptsOnly(exit.cause)) {
264
+ if (Exit.isSuccess(exit)) run.deferred.resolve(this.#succeed(exit.value, Date.now()));
265
+ else if (Cause.hasInterruptsOnly(exit.cause)) {
267
266
  this.#set({
268
267
  fetchDirection: void 0,
269
268
  isFetching: false
@@ -276,10 +275,13 @@ var QueryEntry = class {
276
275
  }
277
276
  this.#scheduleGc();
278
277
  }
279
- #succeed(data, updatedAt) {
278
+ #succeed(next, updatedAt) {
279
+ const previous = this.#state;
280
+ const data = previous.hasData && this.#behavior("structuralSharing") !== false ? replaceEqualDeep(previous.data, next) : next;
280
281
  const firstLoad = this.#pending;
281
282
  this.#pending = void 0;
282
283
  firstLoad?.resolve(data);
284
+ const unchanged = previous.hasData && data === previous.data;
283
285
  this.#set({
284
286
  data,
285
287
  error: null,
@@ -288,10 +290,11 @@ var QueryEntry = class {
288
290
  hasData: true,
289
291
  isFetching: false,
290
292
  isInvalidated: false,
291
- promise: firstLoad ? firstLoad.promise : fulfilled(data),
293
+ promise: firstLoad?.promise ?? (unchanged ? previous.promise : fulfilled(data)),
292
294
  status: "success",
293
295
  updatedAt
294
296
  });
297
+ return data;
295
298
  }
296
299
  #fail(error) {
297
300
  this.#host.reportError(error, this);
package/dist/share.mjs ADDED
@@ -0,0 +1,41 @@
1
+ import { unsafeCast } from "./cast.mjs";
2
+ import { isPlainRecord } from "./key.mjs";
3
+ //#region src/share.ts
4
+ function isPlainArray(value) {
5
+ return Array.isArray(value) && value.length === Object.keys(value).length;
6
+ }
7
+ function share(previous, next, depth) {
8
+ if (previous === next || depth > 500) return next;
9
+ if (isPlainArray(previous) && isPlainArray(next)) {
10
+ let equal = previous.length === next.length;
11
+ const copy = next.map((item, index) => {
12
+ const shared = share(previous[index], item, depth + 1);
13
+ equal &&= shared === previous[index];
14
+ return shared;
15
+ });
16
+ return equal ? previous : copy;
17
+ }
18
+ if (isPlainRecord(previous) && isPlainRecord(next)) {
19
+ const names = Object.keys(next);
20
+ let equal = names.length === Object.keys(previous).length;
21
+ const copy = {};
22
+ for (const name of names) {
23
+ const shared = share(previous[name], next[name], depth + 1);
24
+ equal &&= shared === previous[name] && Object.hasOwn(previous, name);
25
+ copy[name] = shared;
26
+ }
27
+ return equal ? previous : copy;
28
+ }
29
+ return next;
30
+ }
31
+ /**
32
+ * `next`, reusing every part of `previous` that is deeply equal to it: all
33
+ * of `previous` when nothing changed, else a copy of `next` whose unchanged
34
+ * subtrees are `previous`'s. Only plain objects and arrays are compared;
35
+ * anything else is taken from `next`. TanStack Query's `replaceEqualDeep`.
36
+ */
37
+ function replaceEqualDeep(previous, next) {
38
+ return unsafeCast(share(previous, next, 0));
39
+ }
40
+ //#endregion
41
+ export { replaceEqualDeep };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/core",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Framework-agnostic core of raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",