@raf-query/core 0.1.2 → 0.1.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,10 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.4](https://github.com/KATT/raf-query/compare/0.1.3...0.1.4) (2026-10-12)
4
+
5
+ ### Features
6
+
7
+ * **core:** add per-query meta ([#26](https://github.com/KATT/raf-query/issues/26)) ([863c915](https://github.com/KATT/raf-query/commit/863c915d884bdfa3bcb3053da348d51aa0e697cf))
8
+
9
+ ## [0.1.3](https://github.com/KATT/raf-query/compare/0.1.2...0.1.3) (2026-10-12)
10
+
11
+ ### Features
12
+
13
+ - **core:** cancel requests nobody reads anymore ([#27](https://github.com/KATT/raf-query/issues/27)) ([c3c1e4b](https://github.com/KATT/raf-query/commit/c3c1e4bb9250164b6df17d7d4b7bd4043f908961))
14
+
3
15
  ## [0.1.2](https://github.com/KATT/raf-query/compare/0.1.1...0.1.2) (2026-10-12)
4
16
 
5
17
  ### Features
6
18
 
7
- * add isFetching(filter?) and useIsFetching(filter?) ([#25](https://github.com/KATT/raf-query/issues/25)) ([70c3b98](https://github.com/KATT/raf-query/commit/70c3b981027f717b1d619d5a2b23822f685c032a))
19
+ - add isFetching(filter?) and useIsFetching(filter?) ([#25](https://github.com/KATT/raf-query/issues/25)) ([70c3b98](https://github.com/KATT/raf-query/commit/70c3b981027f717b1d619d5a2b23822f685c032a))
8
20
 
9
21
  ## [0.1.1](https://github.com/KATT/raf-query/compare/0.1.0...0.1.1) (2026-10-12)
10
22
 
package/README.md CHANGED
@@ -66,10 +66,46 @@ export function commentsQuery(input: { slug: string }) {
66
66
 
67
67
  Factories must take at most one argument; the type rejects two or more. The single argument is the query's **input** and must be serializable, because it is what a handle carries.
68
68
 
69
- A `queryFn` returns a value, a promise, or an [Effect](#effect). It receives `queryKey` and an abort `signal`; paged ones also get `pageParam` and `direction`. TypeScript infers the page type from `queryFn`, so if `getNextPageParam` comes first in the object literal (sorted keys), pass the types instead: `infiniteQueryOptions<CommentPage, number>({ ... })`.
69
+ A `queryFn` returns a value, a promise, or an [Effect](#effect). It receives `queryKey`, the query's [`meta`](#query-meta) and an abort `signal`; paged ones also get `pageParam` and `direction`. TypeScript infers the page type from `queryFn`, so if `getNextPageParam` comes first in the object literal (sorted keys), pass the types instead: `infiniteQueryOptions<CommentPage, number>({ ... })`.
70
70
 
71
71
  The factory is the only place a query is declared paged. Every other API — `prefetch`, `ensure`, `fetch`, `useQuery`, `useWaterfall`, `invalidate`, `setData` — takes either kind and infers the rest from the factory's type.
72
72
 
73
+ ### Query meta
74
+
75
+ `meta` attaches anything you want to know about a query elsewhere. It reaches the `queryFn` context, `Query.metaUnsafe(query)` (from the latest factory call) and the devtools. A common use is a per-query message for one global error handler:
76
+
77
+ ```ts
78
+ import { Query, QueryClient, queryOptions } from "@raf-query/react";
79
+
80
+ export function issuesQuery() {
81
+ return queryOptions({
82
+ queryKey: ["issues"],
83
+ queryFn: () => listIssues(),
84
+ meta: { errorMessage: "Couldn't load issues" },
85
+ });
86
+ }
87
+
88
+ const queryClient = new QueryClient({
89
+ onError: (error, query) => {
90
+ toast.error(
91
+ Query.metaUnsafe(query)?.errorMessage ?? "Something went wrong",
92
+ );
93
+ },
94
+ });
95
+ ```
96
+
97
+ `meta` is `Record<string, unknown>` by default. Type it for the whole app by augmenting `Register` in the package you import from (`@raf-query/react` re-exports it from `@raf-query/core`, so either works):
98
+
99
+ ```ts
100
+ declare module "@raf-query/react" {
101
+ interface Register {
102
+ queryMeta: { errorMessage?: string };
103
+ }
104
+ }
105
+ ```
106
+
107
+ `config.defaults` takes `meta` too, for queries that set none; a query's own `meta` replaces the default as a whole. `meta` is sent with dehydrated state, so keep it serializable: a hydrated entry reports the sender's `meta` until the receiving side calls its factory.
108
+
73
109
  ## Starting queries
74
110
 
75
111
  All three verbs live on the query client and accept a factory plus its input.
@@ -593,7 +629,7 @@ A button in the corner opens a panel docked to the edge of the viewport. The pan
593
629
  | `fresh` | Read, and within `staleTime` |
594
630
  | `inactive` | Nothing reads it; it's removed at `gcTime` |
595
631
 
596
- Select an entry to see its key, input, data, error, and last update, and to **Refetch**, **Invalidate**, **Reset** (drop its data, so readers suspend and refetch) or **Remove** it. The header filters entries by key, sorts them, and has **Invalidate all** and **Clear cache**. The panel keeps its open state and size in `localStorage`.
632
+ Select an entry to see its key, input, meta, data, error, and last update, and to **Refetch**, **Invalidate**, **Reset** (drop its data, so readers suspend and refetch) or **Remove** it. The header filters entries by key, sorts them, and has **Invalidate all** and **Clear cache**. The panel keeps its open state and size in `localStorage`.
597
633
 
598
634
  `QueryDevtools` props, all optional:
599
635
 
@@ -639,29 +675,33 @@ Query options, accepted by `queryOptions`, `infiniteQueryOptions` and `config.de
639
675
  | `retryDelay` (ms, or a function of the failure count) | Doubling from 1 s, at most 30 s |
640
676
  | `refetchOnWindowFocus`, `refetchOnReconnect` | `true` (only read, stale queries refetch) |
641
677
  | `structuralSharing` (keep deeply equal parts of data) | `true` |
678
+ | [`meta`](#query-meta) | None |
642
679
 
643
680
  `infiniteQueryOptions` also takes `getPreviousPageParam` and `maxPages`.
644
681
 
645
682
  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.
646
683
 
684
+ When the last reader of a query unsubscribes during a background refetch, a `queryFn` that read its `signal` (or returned an Effect) is aborted: the entry keeps its data and goes back to how it was before the request, and `fetch` callers waiting on it get the cached data. Other requests, including every first load (a render may be suspended on it), finish so their result is cached, but don't retry unless a reader comes back. Requests nothing has subscribed to yet, like a loader's `prefetch`, keep running.
685
+
647
686
  `@raf-query/core`:
648
687
 
649
- | Export | Use |
650
- | ------------------------------------------------------------ | -------------------------------------------------------------------------- |
651
- | `QueryCache` | The cache service (`qc.effect`): `make`, `layer`, and the verbs as Effects |
652
- | `Query` | Cache entries: `Query.fetch`, `Query.changes`, `Query.getUnsafe`, … |
653
- | `Platform` | The page the cache watches (focus, network); swap it in tests |
654
- | `queryOptions(options)` | Typed options for a query factory |
655
- | `infiniteQueryOptions(options)` | Typed options for a paged query factory |
656
- | `hashKey(key)`, `matchesKey(key, prefix)` | Key helpers |
657
- | `QueryObserver` | One reader's view of a query, with tracked results; for bindings |
658
- | `Query.CancelledError` | Failure of a request whose entry was removed or cleared before it settled |
659
- | `RedactedError` | What a streamed pending query rejects with when its server request failed |
660
- | `QueryHandle<TFactory>` | Handle type for props |
661
- | `EnsuredQueryHandle<TFactory>` | Handle type for props fed by `ensure` |
662
- | `Thenable<T>` | Promise with `status` / `value` / `reason`, read by `use()` |
663
- | `DataOf<TFactory>`, `InputOf<TFactory>`, `ErrorOf<TFactory>` | A factory's data, input and error types |
664
- | `ServicesOf<TFactory>` | The services a factory's `queryFn` Effect needs; `never` for promises |
688
+ | Export | Use |
689
+ | ------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
690
+ | `QueryCache` | The cache service (`qc.effect`): `make`, `layer`, and the verbs as Effects |
691
+ | `Query` | Cache entries: `Query.fetch`, `Query.changes`, `Query.getUnsafe`, `Query.metaUnsafe`, … |
692
+ | `Platform` | The page the cache watches (focus, network); swap it in tests |
693
+ | `queryOptions(options)` | Typed options for a query factory |
694
+ | `infiniteQueryOptions(options)` | Typed options for a paged query factory |
695
+ | `hashKey(key)`, `matchesKey(key, prefix)` | Key helpers |
696
+ | `QueryObserver` | One reader's view of a query, with tracked results; for bindings |
697
+ | `Query.CancelledError` | Failure of a request whose entry was removed or cleared before it settled |
698
+ | `RedactedError` | What a streamed pending query rejects with when its server request failed |
699
+ | `QueryHandle<TFactory>` | Handle type for props |
700
+ | `EnsuredQueryHandle<TFactory>` | Handle type for props fed by `ensure` |
701
+ | `Thenable<T>` | Promise with `status` / `value` / `reason`, read by `use()` |
702
+ | `DataOf<TFactory>`, `InputOf<TFactory>`, `ErrorOf<TFactory>` | A factory's data, input and error types |
703
+ | `ServicesOf<TFactory>` | The services a factory's `queryFn` Effect needs; `never` for promises |
704
+ | `Register`, `QueryMeta` | Augment `Register` with `queryMeta` to type [`meta`](#query-meta) |
665
705
 
666
706
  `@raf-query/react` (plus all of the above):
667
707
 
package/dist/entry.d.mts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { Platform, PlatformEvent } from "./platform.mjs";
2
2
  import { QueryKey } from "./key.mjs";
3
- import { AnyQueryOptions, QueryBehavior, QueryFactory, QueryHandle } from "./options.mjs";
3
+ import { AnyQueryOptions, QueryBehavior, QueryFactory, QueryHandle, QueryMeta } from "./options.mjs";
4
4
  import { Event } from "./cache.mjs";
5
5
  import { Pending, Thenable } from "./thenable.mjs";
6
6
  import { FetchRequest, Query, State, SubscribeOptions } from "./query.mjs";
7
7
  import { Cause, Clock, Context, Deferred, Effect, Scope, Stream } from "effect";
8
8
  declare namespace entry_d_exports {
9
- export { CacheState, CancelledError, Entry, TypeId, changes, emit, entryOf, fetch, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isObserved, isQuery, isStaleUnsafe, make, now, onPlatformEvent, provide, readUnsafe, remove, reset, resume, setData, setOptions, start, subscribeUnsafe };
9
+ export { CacheState, CancelledError, Entry, TypeId, changes, emit, entryOf, fetch, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isObserved, isQuery, isStaleUnsafe, make, metaOf, now, onPlatformEvent, provide, readUnsafe, remove, reset, resume, setData, setOptions, start, subscribeUnsafe };
10
10
  }
11
11
  export declare const TypeId = "~@raf-query/core/Query";
12
12
  declare const CancelledError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
@@ -54,13 +54,19 @@ export interface Entry extends Query {
54
54
  readonly commands: Commands;
55
55
  /** A failed first load was shown; the next read retries it. */
56
56
  errorShown: boolean;
57
+ /** `failureCount` before the request in flight, restored if it's cancelled. */
58
+ failureCountBefore: number;
57
59
  handle: QueryHandle;
60
+ /** `meta` sent with dehydrated state, for an entry no factory call has reached. */
61
+ hydratedMeta: QueryMeta | undefined;
58
62
  /** Awaited by every caller of the request in flight; outlives restarts. */
59
63
  inFlight: Deferred.Deferred<unknown, unknown> | undefined;
60
64
  readonly listeners: Map<() => void, SubscribeOptions>;
61
65
  options: AnyQueryOptions | undefined;
62
66
  /** The first load's promise, until it settles. */
63
67
  pending: Pending<unknown> | undefined;
68
+ /** The last reader left during the request in flight: it retries no more. */
69
+ retriesStopped: boolean;
64
70
  /** Open while the entry is cached; owns its fibers. */
65
71
  readonly scope: Scope.Closeable;
66
72
  /**
@@ -68,6 +74,11 @@ export interface Entry extends Query {
68
74
  * latest verb that called for this entry ({@link provide}).
69
75
  */
70
76
  services: Context.Context<never>;
77
+ /**
78
+ * The request in flight read its `signal`, or returned an Effect, so
79
+ * interrupting it cancels its work (TanStack's `abortSignalConsumed`).
80
+ */
81
+ signalRead: boolean;
71
82
  state: State<unknown>;
72
83
  readonly tasks: Tasks;
73
84
  }
@@ -78,6 +89,8 @@ export declare function make(cache: CacheState, key: QueryKey, hash: string): En
78
89
  export declare function now(entry: Entry): number;
79
90
  /** Readers are subscribed; `invalidate` and focus refetch only what is read. */
80
91
  export declare function isObserved(entry: Entry): boolean;
92
+ /** Until a factory call attaches options, `meta` comes from hydration. */
93
+ export declare function metaOf(entry: Entry): QueryMeta | undefined;
81
94
  export declare function isStaleUnsafe(entry: Entry): boolean;
82
95
  export declare function hasNextPageUnsafe(entry: Entry): boolean;
83
96
  export declare function hasPreviousPageUnsafe(entry: Entry): boolean;
package/dist/entry.mjs CHANGED
@@ -7,7 +7,7 @@ import { fromQueryFn, requestServices, runRequest } from "./request.mjs";
7
7
  import { retrySchedule } from "./retry.mjs";
8
8
  import { replaceEqualDeep } from "./share.mjs";
9
9
  import { fulfilled, pending } from "./thenable.mjs";
10
- import { Cause, Data, Deferred, Effect, Exit, FiberMap, Pipeable, Predicate, Scope } from "effect";
10
+ import { Cause, Data, Deferred, Effect, Exit, FiberMap, Pipeable, Predicate, Schedule, Scope } from "effect";
11
11
  //#region src/entry.ts
12
12
  const TypeId = "~@raf-query/core/Query";
13
13
  /**
@@ -55,18 +55,22 @@ function make(cache, key, hash) {
55
55
  const entry = Object.assign(Object.create(Proto), {
56
56
  cache,
57
57
  errorShown: false,
58
+ failureCountBefore: 0,
58
59
  handle: {
59
60
  input: void 0,
60
61
  key
61
62
  },
62
63
  hash,
64
+ hydratedMeta: void 0,
63
65
  inFlight: void 0,
64
66
  key,
65
67
  listeners: /* @__PURE__ */ new Map(),
66
68
  options: void 0,
67
69
  pending: first,
70
+ retriesStopped: false,
68
71
  scope,
69
72
  services: cache.services,
73
+ signalRead: false,
70
74
  state: {
71
75
  data: void 0,
72
76
  error: null,
@@ -101,6 +105,10 @@ function isObserved(entry) {
101
105
  function behavior(entry, name) {
102
106
  return entry.options?.[name] ?? entry.cache.defaults[name];
103
107
  }
108
+ /** Until a factory call attaches options, `meta` comes from hydration. */
109
+ function metaOf(entry) {
110
+ return entry.options ? behavior(entry, "meta") : entry.hydratedMeta ?? entry.cache.defaults.meta;
111
+ }
104
112
  function computeStale(entry, state) {
105
113
  if (state.isInvalidated || !state.hasData) return true;
106
114
  const staleTime = behavior(entry, "staleTime") ?? 0;
@@ -121,6 +129,7 @@ function hasPreviousPageUnsafe(entry) {
121
129
  }
122
130
  function subscribeUnsafe(entry, listener, options = defaultSubscribeOptions) {
123
131
  entry.listeners.set(listener, options);
132
+ entry.retriesStopped = false;
124
133
  entry.tasks.cancel("gc");
125
134
  scheduleStale(entry);
126
135
  emit(entry.cache, {
@@ -132,6 +141,8 @@ function subscribeUnsafe(entry, listener, options = defaultSubscribeOptions) {
132
141
  if (entry.listeners.size === 0) {
133
142
  entry.tasks.cancel("stale");
134
143
  scheduleGc(entry);
144
+ const inFlight = entry.inFlight;
145
+ if (inFlight) scheduleNotify(() => unobserved(entry, inFlight));
135
146
  }
136
147
  emit(entry.cache, {
137
148
  query: entry,
@@ -273,8 +284,11 @@ function set(entry, patch) {
273
284
  }
274
285
  /** Run `request` as the entry's request fiber, settling into its awaiter. */
275
286
  function startRequest(entry, request, direction) {
287
+ if (!entry.inFlight) entry.failureCountBefore = entry.state.failureCount;
276
288
  const awaiter = entry.inFlight ?? Deferred.makeUnsafe();
277
289
  entry.inFlight = awaiter;
290
+ entry.retriesStopped = false;
291
+ entry.signalRead = false;
278
292
  entry.tasks.cancel("gc");
279
293
  if (!entry.state.hasData && entry.state.status === "error") {
280
294
  entry.pending = pending();
@@ -312,6 +326,29 @@ function settle(entry, awaiter, exit) {
312
326
  }
313
327
  scheduleGc(entry);
314
328
  }
329
+ /**
330
+ * The last reader left while `awaiter`'s request was in flight. A refetch
331
+ * whose work an interrupt cancels (`signalRead`) is interrupted, and the
332
+ * entry goes back to its state before the request; its awaiters get the
333
+ * cached data. Any other request finishes, so its result is cached, but
334
+ * runs no more retries.
335
+ */
336
+ function unobserved(entry, awaiter) {
337
+ if (isObserved(entry) || entry.inFlight !== awaiter) return;
338
+ if (!entry.signalRead || !entry.state.hasData) {
339
+ entry.retriesStopped = true;
340
+ return;
341
+ }
342
+ entry.inFlight = void 0;
343
+ entry.tasks.cancel("request");
344
+ set(entry, {
345
+ failureCount: entry.failureCountBefore,
346
+ fetchDirection: void 0,
347
+ isFetching: false
348
+ });
349
+ Deferred.doneUnsafe(awaiter, Exit.succeed(entry.state.data));
350
+ scheduleGc(entry);
351
+ }
315
352
  function succeed(entry, next, updatedAt) {
316
353
  const previous = entry.state;
317
354
  const data = previous.hasData && behavior(entry, "structuralSharing") !== false ? replaceEqualDeep(previous.data, next) : next;
@@ -364,25 +401,41 @@ function markErrorShown(entry) {
364
401
  }))), { onlyIfMissing: true });
365
402
  }
366
403
  function requestEffect(entry, options, request) {
404
+ const meta = metaOf(entry);
367
405
  const queryKey = options.queryKey;
368
- if (options.kind === "query") return attempt(entry, (signal) => options.queryFn({
406
+ if (options.kind === "query") return attempt(entry, (readSignal) => options.queryFn({
407
+ meta,
369
408
  queryKey,
370
- signal
409
+ get signal() {
410
+ return readSignal();
411
+ }
371
412
  }));
372
413
  const current = isInfiniteData(entry.state.data) ? entry.state.data : void 0;
373
- return fetchPages(options, current, request, (pageParam, direction) => attempt(entry, (signal) => options.queryFn({
414
+ return fetchPages(options, current, request, (pageParam, direction) => attempt(entry, (readSignal) => options.queryFn({
374
415
  direction,
416
+ meta,
375
417
  pageParam,
376
418
  queryKey,
377
- signal
419
+ get signal() {
420
+ return readSignal();
421
+ }
378
422
  })));
379
423
  }
380
424
  /**
381
425
  * One `queryFn` call, retried per the query's policy. Every failure counts.
382
- * Its services are erased with its data type; `runRequest` provides them.
426
+ * `call` reads the signal through `readSignal`, which marks the entry's
427
+ * `signalRead`. Its services are erased with its data type; `runRequest`
428
+ * provides them.
383
429
  */
384
430
  function attempt(entry, call) {
385
- const once = fromQueryFn(call).pipe(Effect.tapError(() => Effect.sync(() => set(entry, { failureCount: entry.state.failureCount + 1 }))));
431
+ const once = fromQueryFn((signal) => {
432
+ const result = call(() => {
433
+ entry.signalRead = true;
434
+ return signal;
435
+ });
436
+ if (Effect.isEffect(result)) entry.signalRead = true;
437
+ return result;
438
+ }).pipe(Effect.tapError(() => Effect.sync(() => set(entry, { failureCount: entry.state.failureCount + 1 }))));
386
439
  const schedule = retrySchedule({
387
440
  retry: behavior(entry, "retry"),
388
441
  retryDelay: behavior(entry, "retryDelay")
@@ -391,12 +444,18 @@ function attempt(entry, call) {
391
444
  const online = entry.cache.isServer ? Effect.void : whenOnline;
392
445
  return Effect.suspend(() => {
393
446
  let retrying = false;
447
+ let lastError;
448
+ const unlessStopped = (next) => Effect.suspend(() => entry.retriesStopped ? Effect.fail(lastError) : next);
394
449
  const gated = Effect.suspend(() => {
395
- const wait = retrying ? online : Effect.void;
396
- retrying = true;
397
- return Effect.andThen(wait, once);
398
- });
399
- return Effect.retry(gated, schedule);
450
+ if (!retrying) {
451
+ retrying = true;
452
+ return once;
453
+ }
454
+ return unlessStopped(Effect.andThen(online, unlessStopped(once)));
455
+ }).pipe(Effect.tapError((error) => Effect.sync(() => {
456
+ lastError = error;
457
+ })));
458
+ return Effect.retry(gated, Schedule.while(schedule, () => !entry.retriesStopped));
400
459
  });
401
460
  }
402
461
  function scheduleGc(entry) {
@@ -418,4 +477,4 @@ function scheduleStale(entry) {
418
477
  entry.tasks.fork("stale", Effect.sleep(Math.max(remaining, 0) + 1).pipe(Effect.andThen(Effect.sync(() => set(entry, {})))));
419
478
  }
420
479
  //#endregion
421
- export { CancelledError, TypeId, changes, emit, entryOf, fetch, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isObserved, isQuery, isStaleUnsafe, make, now, onPlatformEvent, provide, readUnsafe, remove, reset, resume, setData, setOptions, start, subscribeUnsafe };
480
+ export { CancelledError, TypeId, changes, emit, entryOf, fetch, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isObserved, isQuery, isStaleUnsafe, make, metaOf, now, onPlatformEvent, provide, readUnsafe, remove, reset, resume, setData, setOptions, start, subscribeUnsafe };
@@ -1,5 +1,5 @@
1
1
  import { QueryKey } from "./key.mjs";
2
- import { QueryFactory } from "./options.mjs";
2
+ import { QueryFactory, QueryMeta } from "./options.mjs";
3
3
  import "./cache.mjs";
4
4
  import "./entry.mjs";
5
5
  import { Query, query_d_exports } from "./query.mjs";
@@ -13,6 +13,8 @@ import { Cause, Effect, Scope, Stream } from "effect";
13
13
  export interface DehydratedQuery {
14
14
  readonly data?: unknown;
15
15
  readonly key: QueryKey;
16
+ /** The query's `meta`, for receivers that haven't called its factory. */
17
+ readonly meta?: QueryMeta;
16
18
  readonly promise?: Promise<unknown>;
17
19
  /** When `data` was fetched; `0` while pending. */
18
20
  readonly updatedAt: number;
@@ -1,5 +1,5 @@
1
1
  import { resume, setData } from "./entry.mjs";
2
- import { getUnsafe } from "./query.mjs";
2
+ import { getUnsafe, metaUnsafe } from "./query.mjs";
3
3
  import { Data, Effect, Queue, Stream } from "effect";
4
4
  //#region src/hydration.ts
5
5
  /**
@@ -21,9 +21,14 @@ function canDehydrate(query) {
21
21
  function dehydrateUnsafe(query, options) {
22
22
  const state = getUnsafe(query);
23
23
  const serialize = options.serializeData ?? identity;
24
+ const meta = metaUnsafe(query);
25
+ const sent = meta === void 0 ? { key: query.key } : {
26
+ key: query.key,
27
+ meta
28
+ };
24
29
  if (state.hasData) return {
30
+ ...sent,
25
31
  data: serialize(state.data),
26
- key: query.key,
27
32
  updatedAt: state.updatedAt
28
33
  };
29
34
  const promise = state.promise.then(serialize, (error) => {
@@ -31,7 +36,7 @@ function dehydrateUnsafe(query, options) {
31
36
  });
32
37
  promise.catch(noop);
33
38
  return {
34
- key: query.key,
39
+ ...sent,
35
40
  promise,
36
41
  updatedAt: 0
37
42
  };
@@ -101,6 +106,7 @@ function hydrateEntries(entryFor, state, options) {
101
106
  const deserialize = options.deserializeData ?? identity;
102
107
  for (const query of state.queries) {
103
108
  const entry = entryFor(query.key);
109
+ if (!entry.options && query.meta !== void 0) entry.hydratedMeta = query.meta;
104
110
  const current = entry.state;
105
111
  if (query.updatedAt > 0) {
106
112
  if (!current.hasData || current.updatedAt < query.updatedAt) setData(entry, deserialize(query.data), query.updatedAt);
package/dist/index.d.mts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { platform_d_exports } from "./platform.mjs";
2
2
  import { QueryKey, hashKey, matchesKey } from "./key.mjs";
3
- import { AnyQueryOptions, DataOf, EnsuredQueryHandle, ErrorOf, InfiniteData, InfiniteQueryOptions, InputArgs, InputOf, IsPaged, LoadOptions, PageFunctionContext, PageOf, QueryBehavior, QueryFactory, QueryFnResult, QueryFunctionContext, QueryHandle, QueryOptions, RetryPolicy, ServicesOf, infiniteQueryOptions, queryOptions } from "./options.mjs";
3
+ import { AnyQueryOptions, DataOf, EnsuredQueryHandle, ErrorOf, InfiniteData, InfiniteQueryOptions, InputArgs, InputOf, IsPaged, LoadOptions, PageFunctionContext, PageOf, QueryBehavior, QueryFactory, QueryFnResult, QueryFunctionContext, QueryHandle, QueryMeta, QueryOptions, Register, RetryPolicy, ServicesOf, infiniteQueryOptions, queryOptions } from "./options.mjs";
4
4
  import { DehydrateOptions, DehydratedQuery, DehydratedState, DehydratedStream, HydrateOptions, RedactedError, canDehydrate, dehydrateQuery } from "./hydration.mjs";
5
5
  import { cache_d_exports } from "./cache.mjs";
6
6
  import { Thenable } from "./thenable.mjs";
7
7
  import { query_d_exports } from "./query.mjs";
8
8
  import { observer_d_exports } from "./observer.mjs";
9
- export { type AnyQueryOptions, type DataOf, type DehydrateOptions, type DehydratedQuery, type DehydratedState, type DehydratedStream, type EnsuredQueryHandle, type ErrorOf, type HydrateOptions, type InfiniteData, type InfiniteQueryOptions, type InputArgs, type InputOf, type IsPaged, type LoadOptions, type PageFunctionContext, type PageOf, platform_d_exports as Platform, query_d_exports as Query, type QueryBehavior, cache_d_exports as QueryCache, type QueryFactory, type QueryFnResult, type QueryFunctionContext, type QueryHandle, type QueryKey, observer_d_exports as QueryObserver, type QueryOptions, RedactedError, type RetryPolicy, type ServicesOf, type Thenable, canDehydrate, dehydrateQuery, hashKey, infiniteQueryOptions, matchesKey, queryOptions };
9
+ export { type AnyQueryOptions, type DataOf, type DehydrateOptions, type DehydratedQuery, type DehydratedState, type DehydratedStream, type EnsuredQueryHandle, type ErrorOf, type HydrateOptions, type InfiniteData, type InfiniteQueryOptions, type InputArgs, type InputOf, type IsPaged, type LoadOptions, type PageFunctionContext, type PageOf, platform_d_exports as Platform, query_d_exports as Query, type QueryBehavior, cache_d_exports as QueryCache, type QueryFactory, type QueryFnResult, type QueryFunctionContext, type QueryHandle, type QueryKey, type QueryMeta, observer_d_exports as QueryObserver, type QueryOptions, RedactedError, type Register, type RetryPolicy, type ServicesOf, type Thenable, canDehydrate, dehydrateQuery, hashKey, infiniteQueryOptions, matchesKey, queryOptions };
@@ -7,7 +7,30 @@ import { Effect, Schedule } from "effect";
7
7
  * and the services it needs become requirements of the verbs that start it.
8
8
  */
9
9
  export type QueryFnResult<TData, TError = unknown, TServices = never> = TData | PromiseLike<TData> | Effect.Effect<TData, TError, TServices>;
10
+ /**
11
+ * App-wide types, filled in by module augmentation. Declare `queryMeta` to
12
+ * type every query's `meta`. Augment the package you import from;
13
+ * `@raf-query/react` re-exports this interface, so either works:
14
+ *
15
+ * ```ts
16
+ * declare module "@raf-query/react" {
17
+ * interface Register {
18
+ * queryMeta: { errorMessage?: string };
19
+ * }
20
+ * }
21
+ * ```
22
+ */
23
+ export interface Register {}
24
+ /**
25
+ * Type of a query's `meta`. `Record<string, unknown>` unless `queryMeta` is
26
+ * declared on {@link Register}.
27
+ */
28
+ export type QueryMeta = Register extends {
29
+ queryMeta: infer TQueryMeta;
30
+ } ? TQueryMeta : Record<string, unknown>;
10
31
  export interface QueryFunctionContext {
32
+ /** The query's `meta` when this request started ({@link QueryBehavior.meta}). */
33
+ readonly meta: QueryMeta | undefined;
11
34
  /** The query's key, as returned by the factory. */
12
35
  readonly queryKey: QueryKey;
13
36
  /** Aborted when the request is cancelled or superseded. */
@@ -28,6 +51,12 @@ export type RetryPolicy = boolean | number | Schedule.Schedule<unknown, unknown>
28
51
  export interface QueryBehavior {
29
52
  /** How long an unread entry stays cached, in ms. `Infinity` keeps it. */
30
53
  readonly gcTime?: number;
54
+ /**
55
+ * Anything to know about the query elsewhere: the `queryFn` context,
56
+ * `Query.metaUnsafe(query)` (e.g. in the client's `onError`), devtools. A
57
+ * query's own `meta` replaces the default's as a whole.
58
+ */
59
+ readonly meta?: QueryMeta;
31
60
  /** Refetch read, stale queries when the browser comes back online. */
32
61
  readonly refetchOnReconnect?: boolean;
33
62
  /** Refetch read, stale queries when the window regains focus. */
package/dist/query.d.mts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { QueryKey } from "./key.mjs";
2
- import { AnyQueryOptions, DataOf, ErrorOf, QueryFactory, QueryHandle, ServicesOf } from "./options.mjs";
2
+ import { AnyQueryOptions, DataOf, ErrorOf, QueryFactory, QueryHandle, QueryMeta, ServicesOf } from "./options.mjs";
3
3
  import { Thenable } from "./thenable.mjs";
4
4
  import { CancelledError as CancelledError$1, TypeId as TypeId$1, entry_d_exports } from "./entry.mjs";
5
5
  import { Direction } from "./infinite.mjs";
6
6
  import { Effect, Pipeable, Stream } from "effect";
7
7
  declare namespace query_d_exports {
8
- export { CancelledError, FetchRequest, Of, Query, State, SubscribeOptions, TypeId, changes, fetch, fetchNextPage, fetchPreviousPage, get, getUnsafe, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isQuery, isStaleUnsafe, observerCountUnsafe, readUnsafe, refetch, reset, subscribeUnsafe };
8
+ export { CancelledError, FetchRequest, Of, Query, State, SubscribeOptions, TypeId, changes, fetch, fetchNextPage, fetchPreviousPage, get, getUnsafe, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isQuery, isStaleUnsafe, metaUnsafe, observerCountUnsafe, readUnsafe, refetch, reset, subscribeUnsafe };
9
9
  }
10
10
  export declare const TypeId: typeof TypeId$1;
11
11
  export type TypeId = typeof TypeId;
@@ -89,6 +89,11 @@ export declare function invalidate<TData, TFactory extends QueryFactory, TError>
89
89
  * and refetch; succeeds when that refetch settles.
90
90
  */
91
91
  export declare function reset<TData, TFactory extends QueryFactory, TError>(self: Query<TData, TFactory, TError>): Effect.Effect<void>;
92
+ /**
93
+ * The query's `meta`: the latest factory call's (the one it was hydrated
94
+ * with, before any), else the cache default's.
95
+ */
96
+ export declare function metaUnsafe<TData, TFactory extends QueryFactory, TError>(self: Query<TData, TFactory, TError>): QueryMeta | undefined;
92
97
  /** The entry's current state. */
93
98
  export declare function get<TData, TFactory extends QueryFactory, TError>(self: Query<TData, TFactory, TError>): Effect.Effect<State<TData>>;
94
99
  /** {@link get}, synchronously. */
package/dist/query.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
2
  import { unsafeCast } from "./cast.mjs";
3
- import { CancelledError as CancelledError$1, TypeId as TypeId$1, changes as changes$1, entryOf, fetch as fetch$1, hasNextPageUnsafe as hasNextPageUnsafe$1, hasPreviousPageUnsafe as hasPreviousPageUnsafe$1, invalidate as invalidate$1, isQuery as isQuery$1, isStaleUnsafe as isStaleUnsafe$1, provide, readUnsafe as readUnsafe$1, reset as reset$1, subscribeUnsafe as subscribeUnsafe$1 } from "./entry.mjs";
3
+ import { CancelledError as CancelledError$1, TypeId as TypeId$1, changes as changes$1, entryOf, fetch as fetch$1, hasNextPageUnsafe as hasNextPageUnsafe$1, hasPreviousPageUnsafe as hasPreviousPageUnsafe$1, invalidate as invalidate$1, isQuery as isQuery$1, isStaleUnsafe as isStaleUnsafe$1, metaOf, provide, readUnsafe as readUnsafe$1, reset as reset$1, subscribeUnsafe as subscribeUnsafe$1 } from "./entry.mjs";
4
4
  import { Effect } from "effect";
5
5
  import { dual } from "effect/Function";
6
6
  //#region src/query.ts
@@ -26,6 +26,7 @@ var query_exports = /* @__PURE__ */ __exportAll({
26
26
  invalidate: () => invalidate,
27
27
  isQuery: () => isQuery,
28
28
  isStaleUnsafe: () => isStaleUnsafe,
29
+ metaUnsafe: () => metaUnsafe,
29
30
  observerCountUnsafe: () => observerCountUnsafe,
30
31
  readUnsafe: () => readUnsafe,
31
32
  refetch: () => refetch,
@@ -75,6 +76,13 @@ function invalidate(self) {
75
76
  function reset(self) {
76
77
  return reset$1(entryOf(self));
77
78
  }
79
+ /**
80
+ * The query's `meta`: the latest factory call's (the one it was hydrated
81
+ * with, before any), else the cache default's.
82
+ */
83
+ function metaUnsafe(self) {
84
+ return metaOf(entryOf(self));
85
+ }
78
86
  /** The entry's current state. */
79
87
  function get(self) {
80
88
  return Effect.sync(() => getUnsafe(self));
@@ -116,4 +124,4 @@ function observerCountUnsafe(self) {
116
124
  return entryOf(self).listeners.size;
117
125
  }
118
126
  //#endregion
119
- export { CancelledError, TypeId, changes, fetch, fetchNextPage, fetchPreviousPage, get, getUnsafe, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isQuery, isStaleUnsafe, observerCountUnsafe, query_exports, readUnsafe, refetch, reset, subscribeUnsafe };
127
+ export { CancelledError, TypeId, changes, fetch, fetchNextPage, fetchPreviousPage, get, getUnsafe, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isQuery, isStaleUnsafe, metaUnsafe, observerCountUnsafe, query_exports, readUnsafe, refetch, reset, subscribeUnsafe };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/core",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Framework-agnostic core of raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",