@raf-query/core 0.11.1 → 0.12.0

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +28 -18
  3. package/dist/client.d.mts +132 -121
  4. package/dist/client.mjs +59 -162
  5. package/dist/entry.d.mts +67 -148
  6. package/dist/entry.mjs +303 -474
  7. package/dist/features/focus-refetch/focus-refetch.d.mts +15 -0
  8. package/dist/features/focus-refetch/focus-refetch.mjs +18 -0
  9. package/dist/features/polling/polling.d.mts +12 -0
  10. package/dist/features/polling/polling.mjs +35 -0
  11. package/dist/features/reconnect-refetch/reconnect-refetch.d.mts +15 -0
  12. package/dist/features/reconnect-refetch/reconnect-refetch.mjs +14 -0
  13. package/dist/features/shared.mjs +13 -0
  14. package/dist/guards.mjs +23 -0
  15. package/dist/hydration/hydration.d.mts +73 -0
  16. package/dist/hydration/hydration.mjs +128 -0
  17. package/dist/index.d.mts +18 -13
  18. package/dist/index.mjs +15 -13
  19. package/dist/infinite/infinite.d.mts +71 -0
  20. package/dist/infinite/infinite.mjs +88 -0
  21. package/dist/internal.d.mts +13 -0
  22. package/dist/internal.mjs +7 -0
  23. package/dist/keys/keys.d.mts +46 -0
  24. package/dist/keys/keys.mjs +53 -0
  25. package/dist/keys/targets.d.mts +56 -0
  26. package/dist/keys/targets.mjs +85 -0
  27. package/dist/links/retry/retry.d.mts +26 -0
  28. package/dist/links/retry/retry.mjs +51 -0
  29. package/dist/links/timeout/timeout.d.mts +9 -0
  30. package/dist/links/timeout/timeout.mjs +27 -0
  31. package/dist/load.d.mts +124 -0
  32. package/dist/load.mjs +165 -0
  33. package/dist/options.d.mts +112 -351
  34. package/dist/options.mjs +25 -41
  35. package/dist/presets.d.mts +29 -0
  36. package/dist/presets.mjs +25 -0
  37. package/dist/root.d.mts +82 -0
  38. package/dist/structural-sharing/structural-sharing.mjs +32 -0
  39. package/dist/subscribe.d.mts +15 -0
  40. package/dist/subscribe.mjs +29 -0
  41. package/dist/thenable.d.mts +13 -17
  42. package/dist/thenable.mjs +25 -6
  43. package/dist/verbs.d.mts +109 -0
  44. package/dist/verbs.mjs +87 -0
  45. package/package.json +13 -9
  46. package/dist/_virtual/_rolldown/runtime.mjs +0 -13
  47. package/dist/all-keyed.d.mts +0 -18
  48. package/dist/all-keyed.mjs +0 -19
  49. package/dist/binding.d.mts +0 -92
  50. package/dist/binding.mjs +0 -186
  51. package/dist/cache.d.mts +0 -164
  52. package/dist/cache.mjs +0 -184
  53. package/dist/controls.d.mts +0 -95
  54. package/dist/controls.mjs +0 -70
  55. package/dist/hydration.d.mts +0 -84
  56. package/dist/hydration.mjs +0 -148
  57. package/dist/infinite.d.mts +0 -5
  58. package/dist/infinite.mjs +0 -103
  59. package/dist/key.d.mts +0 -18
  60. package/dist/key.mjs +0 -36
  61. package/dist/listen.mjs +0 -19
  62. package/dist/notify.mjs +0 -21
  63. package/dist/observer.d.mts +0 -110
  64. package/dist/observer.mjs +0 -300
  65. package/dist/platform.d.mts +0 -27
  66. package/dist/platform.mjs +0 -44
  67. package/dist/query.d.mts +0 -137
  68. package/dist/query.mjs +0 -147
  69. package/dist/request.mjs +0 -47
  70. package/dist/retry.mjs +0 -25
  71. package/dist/share.mjs +0 -40
@@ -0,0 +1,46 @@
1
+ //#region src/keys/keys.d.ts
2
+ /**
3
+ * Keys: plain arrays, the same in memory, on the wire and in devtools.
4
+ * `group()` builds them; a hand-written array works too. Every key is
5
+ * hashed to one stable string, the entry's identity in the cache.
6
+ *
7
+ * Keys and targets (`./targets.ts`) are the seam for grouping: everything
8
+ * that builds or matches a key lives in this folder, so the shape of
9
+ * grouping can change without touching the cache.
10
+ */
11
+ /** A query's key: a plain, serializable array. */
12
+ export type QueryKey = ReadonlyArray<unknown>;
13
+ declare const groupBrand: unique symbol;
14
+ /**
15
+ * A named set of queries: those whose key starts with its path. Call it
16
+ * for a key under that path; a subgroup's path extends its parent's, so a
17
+ * group covers its subgroups and their queries.
18
+ */
19
+ export interface Group<TPath extends ReadonlyArray<string> = ReadonlyArray<string>> {
20
+ <const TRest extends QueryKey>(...rest: TRest): readonly [...TPath, ...TRest];
21
+ readonly [groupBrand]: TPath;
22
+ /** Its parents' names and its own. */
23
+ readonly path: TPath;
24
+ }
25
+ /**
26
+ * A group, or a subgroup inside `parent`. Call it for a query's key: a
27
+ * flat, plain array that starts with the group's path. Invalidating a group
28
+ * covers its subgroups and every query below them.
29
+ *
30
+ * ```ts
31
+ * export const posts = group("posts");
32
+ * export const drafts = group(posts, "drafts");
33
+ * const draftById = (input: { id: string }) =>
34
+ * queryOptions({ queryKey: drafts("byId", input), queryFn }); // ["posts", "drafts", "byId", input]
35
+ * await invalidateMany(client, posts); // drafts too
36
+ * ```
37
+ */
38
+ export declare function group<const TName extends string>(name: TName): Group<readonly [TName]>;
39
+ export declare function group<const TPath extends ReadonlyArray<string>, const TName extends string>(parent: Group<TPath>, name: TName): Group<readonly [...TPath, TName]>;
40
+ /** Stable string form of a key: JSON with sorted fields. Functions drop out. */
41
+ export declare function hashKey(key: QueryKey): string;
42
+ /** Any function that returns a query's options. */
43
+ export type AnyFactory = (input: never) => {
44
+ readonly queryKey: QueryKey;
45
+ };
46
+ //#endregion
@@ -0,0 +1,53 @@
1
+ import { isString } from "../guards.mjs";
2
+ //#region src/keys/keys.ts
3
+ /**
4
+ * Keys: plain arrays, the same in memory, on the wire and in devtools.
5
+ * `group()` builds them; a hand-written array works too. Every key is
6
+ * hashed to one stable string, the entry's identity in the cache.
7
+ *
8
+ * Keys and targets (`./targets.ts`) are the seam for grouping: everything
9
+ * that builds or matches a key lives in this folder, so the shape of
10
+ * grouping can change without touching the cache.
11
+ */
12
+ let namesUnder;
13
+ /**
14
+ * A subgroup and a query with one name under one parent would share a key
15
+ * prefix, so invalidating the subgroup would reach the query. Development
16
+ * builds throw at whichever comes second. Only keys `group()` builds are
17
+ * checked.
18
+ */
19
+ function claim(parent, name, kind) {
20
+ if (!isString(name)) return;
21
+ namesUnder ??= /* @__PURE__ */ new WeakMap();
22
+ const names = namesUnder.get(parent) ?? /* @__PURE__ */ new Map();
23
+ namesUnder.set(parent, names);
24
+ if ((names.get(name) ?? kind) !== kind) throw new Error(`raf-query: "${[...parent.path, name].join("/")}" is both a subgroup and a query key. Give one of them another name.`);
25
+ names.set(name, kind);
26
+ }
27
+ function group(first, name) {
28
+ const path = isString(first) ? [first] : [...first.path, name];
29
+ const self = (...rest) => {
30
+ if (process.env.NODE_ENV !== "production") claim(built, rest[0], "query");
31
+ return [...path, ...rest];
32
+ };
33
+ const built = Object.assign(self, { path: Object.freeze(path) });
34
+ if (process.env.NODE_ENV !== "production" && !isString(first)) claim(first, name, "group");
35
+ return built;
36
+ }
37
+ function isPlainRecord(value) {
38
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
39
+ const prototype = Object.getPrototypeOf(value);
40
+ return prototype === Object.prototype || prototype === null;
41
+ }
42
+ function sortFields(_name, value) {
43
+ if (!isPlainRecord(value)) return value;
44
+ const sorted = {};
45
+ for (const name of Object.keys(value).toSorted()) sorted[name] = value[name];
46
+ return sorted;
47
+ }
48
+ /** Stable string form of a key: JSON with sorted fields. Functions drop out. */
49
+ function hashKey(key) {
50
+ return JSON.stringify(key, sortFields);
51
+ }
52
+ //#endregion
53
+ export { group, hashKey, isPlainRecord };
@@ -0,0 +1,56 @@
1
+ import { AnyFactory, Group, QueryKey } from "./keys.mjs";
2
+ import { QueryMeta } from "../root.mjs";
3
+ //#region src/keys/targets.d.ts
4
+ /** What a target's `where` sees of a query. */
5
+ export interface QueryInfo<TMeta = QueryMeta> {
6
+ /** The query's data, before `select`; `undefined` without data. */
7
+ readonly data: unknown;
8
+ readonly input: unknown;
9
+ /** The `meta` its factory gave it. An entry only hydrated has `{}` until it's read. */
10
+ readonly meta: TMeta;
11
+ readonly queryKey: QueryKey;
12
+ }
13
+ /**
14
+ * Picks out queries by what they are: `where` gets each query and the
15
+ * parameter the verb was called with.
16
+ *
17
+ * ```ts
18
+ * const ownedBy: Target<AppMeta, Team> = { where: (query, team) => query.meta.owner === team };
19
+ * await invalidateMany(client, ownedBy, "posts-team");
20
+ * ```
21
+ */
22
+ export interface Target<TMeta = QueryMeta, TParam = never> {
23
+ readonly where: (query: QueryInfo<TMeta>, param: TParam) => boolean;
24
+ }
25
+ /** A target of any meta and parameter. */
26
+ export interface AnyTarget {
27
+ readonly where: (query: never, param: never) => boolean;
28
+ }
29
+ /** The arguments after a target: its parameter, if it takes one. */
30
+ export type ParamArgs<TParam> = [TParam] extends [never] ? [] : unknown extends TParam ? [] : undefined extends TParam ? [param?: TParam] : [param: TParam];
31
+ /** One invalidation, by what it targets. */
32
+ export type Stamp = {
33
+ readonly at: number;
34
+ readonly kind: "all";
35
+ } | {
36
+ readonly at: number;
37
+ readonly hash: string;
38
+ readonly kind: "entry";
39
+ } | {
40
+ readonly at: number;
41
+ readonly factory: AnyFactory;
42
+ readonly kind: "factory";
43
+ readonly partial: unknown;
44
+ } | {
45
+ readonly at: number;
46
+ readonly kind: "group";
47
+ readonly path: ReadonlyArray<string>;
48
+ } | {
49
+ readonly at: number;
50
+ readonly kind: "target";
51
+ readonly param: unknown;
52
+ readonly target: AnyTarget;
53
+ };
54
+ /** Anything a verb can invalidate by. */
55
+ export type AnyInvalidation = AnyFactory | AnyTarget | Group;
56
+ //#endregion
@@ -0,0 +1,85 @@
1
+ import { isPlainRecord } from "./keys.mjs";
2
+ //#region src/keys/targets.ts
3
+ /**
4
+ * Invalidation targets, and how a stamp matches an entry.
5
+ *
6
+ * Core knows three kinds of target:
7
+ *
8
+ * - A factory, matched by identity, optionally with part of its input.
9
+ * The partial is matched against the input the factory was called with,
10
+ * never against the key, so it means the same however the key is built.
11
+ * - A group, made by `group()`: a function that builds flat keys under its
12
+ * path, `drafts("byId", input)` is `["posts", "drafts", "byId", input]`.
13
+ * It matches every key that starts with its path, whoever built the key,
14
+ * so it covers its subgroups and their queries.
15
+ * - Any object with `where(query, param)`, typed by the client's root
16
+ * `meta`: predicates over a query's key, input, `meta` or data. Tags
17
+ * are one, written in user code over a `meta.tags` a feature declares.
18
+ *
19
+ * Invalidation records a stamp per target. An entry is invalidated when a
20
+ * matching stamp is newer than the request its data came from. Entries with
21
+ * readers refetch at once; the rest are checked when they are next read,
22
+ * which always goes through their factory, so `where` sees their `meta`.
23
+ *
24
+ * An entry that was only hydrated has no factory or input until it's read.
25
+ * A factory target covers it from that first read: invalidation through its
26
+ * stamp, which outlives the call. `resetMany` and `removeMany` act at once,
27
+ * so a factory target skips such an entry; a group still reaches it by key.
28
+ */
29
+ /** Whether `actual` matches `expected`: equal, or for objects, every field `expected` has. */
30
+ function partialMatch(actual, expected) {
31
+ if (Object.is(actual, expected)) return true;
32
+ if (Array.isArray(actual) && Array.isArray(expected)) return actual.length === expected.length && expected.every((part, index) => partialMatch(actual[index], part));
33
+ if (isPlainRecord(actual) && isPlainRecord(expected)) return Object.keys(expected).every((name) => partialMatch(actual[name], expected[name]));
34
+ return false;
35
+ }
36
+ /** A stamp for `target`. `arg` is a factory's partial input or a target's parameter. A `where` target may be callable too, so `where` is checked first. */
37
+ function stampFor(target, arg, at) {
38
+ if ("where" in target) return {
39
+ at,
40
+ kind: "target",
41
+ param: arg,
42
+ target
43
+ };
44
+ return "path" in target ? {
45
+ at,
46
+ kind: "group",
47
+ path: target.path
48
+ } : {
49
+ at,
50
+ factory: target,
51
+ kind: "factory",
52
+ partial: arg
53
+ };
54
+ }
55
+ /** A stamp for exactly one entry. */
56
+ function entryStamp(hash, at) {
57
+ return {
58
+ at,
59
+ hash,
60
+ kind: "entry"
61
+ };
62
+ }
63
+ function infoOf(entry) {
64
+ return {
65
+ data: entry.state.data,
66
+ input: entry.input,
67
+ meta: entry.options?.meta ?? {},
68
+ queryKey: entry.key
69
+ };
70
+ }
71
+ /** Whether a stamp's target covers an entry. */
72
+ function matches(entry, stamp) {
73
+ switch (stamp.kind) {
74
+ case "all": return true;
75
+ case "entry": return entry.hash === stamp.hash;
76
+ case "factory": return entry.factory === stamp.factory && (stamp.partial === void 0 || partialMatch(entry.input, stamp.partial));
77
+ case "group": return stamp.path.every((segment, i) => entry.key[i] === segment);
78
+ case "target": {
79
+ const where = stamp.target.where;
80
+ return where(infoOf(entry), stamp.param);
81
+ }
82
+ }
83
+ }
84
+ //#endregion
85
+ export { entryStamp, matches, stampFor };
@@ -0,0 +1,26 @@
1
+ import { Link } from "../../client.mjs";
2
+ //#region src/links/retry/retry.d.ts
3
+ export interface RetryOptions {
4
+ /** Retries after the first attempt. A query's `meta.retry` wins. Default 3. */
5
+ readonly attempts?: number;
6
+ /** Wait before retry `n` (0-based), in ms. Default: doubling from 1 s, at most 30 s. */
7
+ readonly delay?: (attempt: number) => number;
8
+ /** Whether an error is worth retrying. Default: every error but a thrown `Response`. */
9
+ readonly when?: (error: unknown) => boolean;
10
+ }
11
+ /** What `retry()` reads from a query's `meta`. */
12
+ export interface RetryMeta {
13
+ /** Attempts for this query, or `false`. */
14
+ readonly retry?: number | false;
15
+ }
16
+ /**
17
+ * Retry failed requests. Thrown `Response`s (router redirects and
18
+ * not-founds) and cancelled requests aren't retried,
19
+ * server clients never retry, and a browser that is offline
20
+ * waits until it's back online before retrying. A query sets its own
21
+ * attempts with `meta: { retry }`.
22
+ */
23
+ export declare function retry(options?: RetryOptions): Link<{
24
+ readonly meta: RetryMeta;
25
+ }>;
26
+ //#endregion
@@ -0,0 +1,51 @@
1
+ import { link } from "../../client.mjs";
2
+ import { isResponse } from "../../guards.mjs";
3
+ //#region src/links/retry/retry.ts
4
+ function wait(clock, ms, signal) {
5
+ return new Promise((resolve, reject) => {
6
+ const onAbort = () => {
7
+ clock.clearTimeout(handle);
8
+ reject(signal.reason);
9
+ };
10
+ const handle = clock.setTimeout(() => {
11
+ signal.removeEventListener("abort", onAbort);
12
+ resolve();
13
+ }, ms);
14
+ signal.addEventListener("abort", onAbort, { once: true });
15
+ });
16
+ }
17
+ function whenOnline(signal) {
18
+ if (navigator.onLine !== false) return Promise.resolve();
19
+ return new Promise((resolve) => {
20
+ window.addEventListener("online", () => resolve(), {
21
+ once: true,
22
+ signal
23
+ });
24
+ });
25
+ }
26
+ const defaultDelay = (attempt) => Math.min(1e3 * 2 ** attempt, 3e4);
27
+ /** Routers throw a `Response` for a redirect or a not-found: control flow, not a failure. */
28
+ const notThrownResponse = (error) => !isResponse(error);
29
+ /**
30
+ * Retry failed requests. Thrown `Response`s (router redirects and
31
+ * not-founds) and cancelled requests aren't retried,
32
+ * server clients never retry, and a browser that is offline
33
+ * waits until it's back online before retrying. A query sets its own
34
+ * attempts with `meta: { retry }`.
35
+ */
36
+ function retry(options = {}) {
37
+ return link(async (op, next) => {
38
+ const configured = op.meta.retry;
39
+ const attempts = configured === false ? 0 : configured ?? options.attempts ?? 3;
40
+ if (op.client.isServer || attempts === 0) return next(op);
41
+ for (let attempt = 0;; attempt++) try {
42
+ return await next(op);
43
+ } catch (error) {
44
+ if (attempt >= attempts || op.signal.aborted || !(options.when ?? notThrownResponse)(error)) throw error;
45
+ await wait(op.client.clock, (options.delay ?? defaultDelay)(attempt), op.signal);
46
+ await whenOnline(op.signal);
47
+ }
48
+ });
49
+ }
50
+ //#endregion
51
+ export { retry };
@@ -0,0 +1,9 @@
1
+ import { Link } from "../../client.mjs";
2
+ //#region src/links/timeout/timeout.d.ts
3
+ /** What a request that took too long fails with. */
4
+ export declare class QueryTimeoutError extends Error {
5
+ readonly name = "QueryTimeoutError";
6
+ }
7
+ /** Fail a request (and abort its `signal`) when it takes longer than `ms`. */
8
+ export declare function timeout(ms: number): Link;
9
+ //#endregion
@@ -0,0 +1,27 @@
1
+ //#region src/links/timeout/timeout.ts
2
+ /** What a request that took too long fails with. */
3
+ var QueryTimeoutError = class extends Error {
4
+ name = "QueryTimeoutError";
5
+ };
6
+ /** Fail a request (and abort its `signal`) when it takes longer than `ms`. */
7
+ function timeout(ms) {
8
+ return (op, next) => {
9
+ const controller = new AbortController();
10
+ const signal = AbortSignal.any([op.signal, controller.signal]);
11
+ return new Promise((resolve, reject) => {
12
+ const handle = op.client.clock.setTimeout(() => {
13
+ const error = new QueryTimeoutError(`Query ${op.hash} took longer than ${ms} ms`);
14
+ controller.abort(error);
15
+ reject(error);
16
+ }, ms);
17
+ next({
18
+ ...op,
19
+ signal
20
+ }).then(resolve, reject).finally(() => {
21
+ op.client.clock.clearTimeout(handle);
22
+ });
23
+ });
24
+ };
25
+ }
26
+ //#endregion
27
+ export { QueryTimeoutError, timeout };
@@ -0,0 +1,124 @@
1
+ import { DataOf, InputArgs, InputOf, KeyOf, QueryFactory } from "./options.mjs";
2
+ import { Entry } from "./entry.mjs";
3
+ import { QueryClient } from "./client.mjs";
4
+ //#region src/load.d.ts
5
+ /**
6
+ * A small serializable reference to a query: its key and its factory's
7
+ * input. It carries no data; readers get that from the cache. Handles of
8
+ * different factories have different types.
9
+ */
10
+ export interface QueryHandle<TFactory extends QueryFactory = QueryFactory> {
11
+ readonly input: InputOf<TFactory>;
12
+ readonly queryKey: KeyOf<TFactory>;
13
+ }
14
+ type LoadKind = "prefetch" | "revalidate" | "swr";
15
+ /** A query for `load` to start. Building one starts nothing. */
16
+ export interface LoadEntry<TFactory extends QueryFactory = QueryFactory, TKind extends LoadKind = LoadKind> {
17
+ readonly factory: TFactory;
18
+ readonly input: InputOf<TFactory>;
19
+ readonly kind: TKind;
20
+ /** For paged factories: how many pages a first load fetches. */
21
+ readonly pages?: number;
22
+ }
23
+ export interface LoadEntryOptions {
24
+ /** For paged factories: how many pages a first load fetches. Default 1. */
25
+ readonly pages?: number;
26
+ }
27
+ type LoadEntryArgs<TFactory extends QueryFactory> = [input: InputOf<TFactory>, options?: LoadEntryOptions] | InputArgs<TFactory>;
28
+ /**
29
+ * Start the query if the cache has no fresh data, and don't wait for it.
30
+ * It never makes the load reject: errors show up where a component reads
31
+ * the handle.
32
+ */
33
+ export declare function prefetch<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadEntryArgs<TFactory>): LoadEntry<TFactory, "prefetch">;
34
+ /**
35
+ * Use cached data even when it's stale, and wait only when the cache has
36
+ * none. Stale data resolves at once and refetches in the background, as
37
+ * HTTP's
38
+ * [`stale-while-revalidate`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control#stale-while-revalidate)
39
+ * does. The load rejects if the fetch it waits for fails.
40
+ */
41
+ export declare function swr<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadEntryArgs<TFactory>): LoadEntry<TFactory, "swr">;
42
+ /**
43
+ * Wait for fresh data, as HTTP's
44
+ * [`must-revalidate`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control#must-revalidate)
45
+ * does. Fresh cached data resolves at once without a refetch (`staleTime`
46
+ * is the max-age); with stale data or none it waits for the fetch. On a
47
+ * server client, data counts as fresh until a write invalidates it. The
48
+ * load rejects if the fetch fails.
49
+ */
50
+ export declare function revalidate<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadEntryArgs<TFactory>): LoadEntry<TFactory, "revalidate">;
51
+ /**
52
+ * In place of a `load` entry: skip the query. Its handle is `undefined` and
53
+ * `data` has no key for it, so a component renders its reader only when
54
+ * there's a handle.
55
+ *
56
+ * ```ts
57
+ * const [handles] = await load(queryClient, {
58
+ * drafts: user.isAdmin ? prefetch(draftsQuery, { slug }) : skipToken,
59
+ * });
60
+ * ```
61
+ */
62
+ export declare const skipToken: unique symbol;
63
+ /** What `load` takes per name: an entry, or `skipToken`. */
64
+ type LoadEntries = Record<string, LoadEntry | typeof skipToken>;
65
+ type HandleOf<TEntry> = TEntry extends LoadEntry ? QueryHandle<TEntry["factory"]> : undefined;
66
+ type DataOfEntry<TEntry> = TEntry extends LoadEntry ? DataOf<TEntry["factory"]> : never;
67
+ type LoadedData<TEntries extends LoadEntries> = { [TName in keyof TEntries as TEntries[TName] extends LoadEntry<QueryFactory, "revalidate" | "swr"> ? TName : never]: DataOfEntry<TEntries[TName]>; };
68
+ /**
69
+ * What `load` resolves to: a handle per entry (`undefined` for `skipToken`),
70
+ * and the data of `swr` and `revalidate` entries.
71
+ */
72
+ export type Loaded<TEntries extends LoadEntries> = readonly [handles: { [TName in keyof TEntries]: HandleOf<TEntries[TName]>; }, data: LoadedData<TEntries>];
73
+ /** A `load` call as devtools show it. */
74
+ export interface LoadRecord {
75
+ readonly entries: ReadonlyArray<{
76
+ readonly hash: string;
77
+ readonly kind: LoadKind;
78
+ readonly name: string;
79
+ }>;
80
+ readonly startedAt: number;
81
+ }
82
+ /** The latest `load` calls on a client, newest last. Recorded in development only. */
83
+ export declare function loadsOf(client: QueryClient): ReadonlyArray<LoadRecord>;
84
+ /** Read an entry's data the way its readers see it: through `select`. */
85
+ export declare function readData(entry: Entry): unknown;
86
+ /** Start the entry's request if its data isn't fresh. Never waits. */
87
+ export declare function startIfStale(entry: Entry): void;
88
+ /**
89
+ * Start a loader's queries and return a serializable handle for each. It
90
+ * starts every entry before it waits for any, so the order of the entries
91
+ * can't create a waterfall. Resolves with `[handles, data]` once every
92
+ * `swr` and `revalidate` entry has data, and rejects with the error as soon
93
+ * as one fails.
94
+ * `prefetch` entries never make it reject. A `skipToken` in place of an
95
+ * entry starts nothing and gets no handle.
96
+ *
97
+ * ```ts
98
+ * const [handles, data] = await load(queryClient, {
99
+ * comments: prefetch(commentsQuery, { issueId: id }),
100
+ * issue: swr(issueQuery, { id }),
101
+ * });
102
+ * ```
103
+ */
104
+ export declare function load<const TEntries extends LoadEntries>(client: QueryClient, entries: TEntries): Promise<Loaded<TEntries>>;
105
+ /** Whether a handle came from `waterfallHandle()`: reading it is expected to fetch. */
106
+ export declare function isWaterfallHandle(handle: Pick<QueryHandle, "queryKey">): boolean;
107
+ /**
108
+ * The handle of a query whose input is only known while rendering, like an
109
+ * id in another query's data. It starts nothing: the component that reads
110
+ * it fetches when it renders, which makes it a request waterfall, and the
111
+ * name keeps that visible. Prefer starting queries in a loader with `load`.
112
+ *
113
+ * With a handle and an updater, it derives a handle from that one: the
114
+ * updater gets the handle's input and returns the new one (say, with a
115
+ * search term being typed). While the new input maps to the same query, it
116
+ * returns the handle as is, so readers keep it.
117
+ *
118
+ * ```ts
119
+ * const searchHandle = waterfallHandle(searchQuery, props.search, (input) => ({ ...input, q }));
120
+ * ```
121
+ */
122
+ export declare function waterfallHandle<TFactory extends QueryFactory>(factory: TFactory, ...input: InputArgs<TFactory>): QueryHandle<TFactory>;
123
+ export declare function waterfallHandle<TFactory extends QueryFactory>(factory: TFactory, handle: QueryHandle<TFactory>, update: (input: InputOf<TFactory>) => InputOf<TFactory>): QueryHandle<TFactory>;
124
+ //#endregion
package/dist/load.mjs ADDED
@@ -0,0 +1,165 @@
1
+ import { hashKey } from "./keys/keys.mjs";
2
+ import { optionsOf } from "./options.mjs";
3
+ import { entryOf, fetchEntry, isInvalidated, isStale } from "./entry.mjs";
4
+ //#region src/load.ts
5
+ function loadEntry(kind, factory, args) {
6
+ return {
7
+ factory,
8
+ input: args[0],
9
+ kind,
10
+ pages: args[1]?.pages
11
+ };
12
+ }
13
+ /**
14
+ * Start the query if the cache has no fresh data, and don't wait for it.
15
+ * It never makes the load reject: errors show up where a component reads
16
+ * the handle.
17
+ */
18
+ function prefetch(factory, ...args) {
19
+ return loadEntry("prefetch", factory, args);
20
+ }
21
+ /**
22
+ * Use cached data even when it's stale, and wait only when the cache has
23
+ * none. Stale data resolves at once and refetches in the background, as
24
+ * HTTP's
25
+ * [`stale-while-revalidate`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control#stale-while-revalidate)
26
+ * does. The load rejects if the fetch it waits for fails.
27
+ */
28
+ function swr(factory, ...args) {
29
+ return loadEntry("swr", factory, args);
30
+ }
31
+ /**
32
+ * Wait for fresh data, as HTTP's
33
+ * [`must-revalidate`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control#must-revalidate)
34
+ * does. Fresh cached data resolves at once without a refetch (`staleTime`
35
+ * is the max-age); with stale data or none it waits for the fetch. On a
36
+ * server client, data counts as fresh until a write invalidates it. The
37
+ * load rejects if the fetch fails.
38
+ */
39
+ function revalidate(factory, ...args) {
40
+ return loadEntry("revalidate", factory, args);
41
+ }
42
+ /**
43
+ * In place of a `load` entry: skip the query. Its handle is `undefined` and
44
+ * `data` has no key for it, so a component renders its reader only when
45
+ * there's a handle.
46
+ *
47
+ * ```ts
48
+ * const [handles] = await load(queryClient, {
49
+ * drafts: user.isAdmin ? prefetch(draftsQuery, { slug }) : skipToken,
50
+ * });
51
+ * ```
52
+ */
53
+ const skipToken = Symbol("skipToken");
54
+ const loadRecords = /* @__PURE__ */ new WeakMap();
55
+ /** The latest `load` calls on a client, newest last. Recorded in development only. */
56
+ function loadsOf(client) {
57
+ return loadRecords.get(client) ?? [];
58
+ }
59
+ /** Read an entry's data the way its readers see it: through `select`. */
60
+ function readData(entry) {
61
+ const select = entry.options?.select;
62
+ return select ? select(entry.state.data) : entry.state.data;
63
+ }
64
+ /** Start the entry's request if its data isn't fresh. Never waits. */
65
+ function startIfStale(entry) {
66
+ if (isStale(entry) && !(entry.client.isServer && entry.state.hasData)) fetchEntry(entry);
67
+ }
68
+ /**
69
+ * Whether `revalidate` can use the entry's data. A server client belongs to
70
+ * one request, so what it fetched is fresh for that request until a write
71
+ * invalidates it.
72
+ */
73
+ function isFresh(entry) {
74
+ return entry.client.isServer ? entry.state.hasData && !isInvalidated(entry) : !isStale(entry);
75
+ }
76
+ function waitFresh(entry, pages) {
77
+ if (isFresh(entry)) return Promise.resolve({ ok: true });
78
+ return fetchEntry(entry, { pages });
79
+ }
80
+ function waitAny(entry, pages) {
81
+ if (!entry.state.hasData) return waitFresh(entry, pages);
82
+ startIfStale(entry);
83
+ return Promise.resolve({ ok: true });
84
+ }
85
+ /**
86
+ * Start a loader's queries and return a serializable handle for each. It
87
+ * starts every entry before it waits for any, so the order of the entries
88
+ * can't create a waterfall. Resolves with `[handles, data]` once every
89
+ * `swr` and `revalidate` entry has data, and rejects with the error as soon
90
+ * as one fails.
91
+ * `prefetch` entries never make it reject. A `skipToken` in place of an
92
+ * entry starts nothing and gets no handle.
93
+ *
94
+ * ```ts
95
+ * const [handles, data] = await load(queryClient, {
96
+ * comments: prefetch(commentsQuery, { issueId: id }),
97
+ * issue: swr(issueQuery, { id }),
98
+ * });
99
+ * ```
100
+ */
101
+ async function load(client, entries) {
102
+ const started = Object.entries(entries).flatMap(([name, item]) => item === skipToken ? [] : [{
103
+ item,
104
+ name
105
+ }]).map((loaded) => {
106
+ const item = loaded.item;
107
+ const cached = entryOf(client, item.factory, item.input);
108
+ let wait;
109
+ if (item.kind === "prefetch") startIfStale(cached);
110
+ else wait = (item.kind === "revalidate" ? waitFresh : waitAny)(cached, item.pages);
111
+ return {
112
+ entry: cached,
113
+ item,
114
+ name: loaded.name,
115
+ wait
116
+ };
117
+ });
118
+ if (process.env.NODE_ENV !== "production") {
119
+ const records = loadRecords.get(client) ?? [];
120
+ records.push({
121
+ entries: started.map((one) => ({
122
+ hash: one.entry.hash,
123
+ kind: one.item.kind,
124
+ name: one.name
125
+ })),
126
+ startedAt: client.clock.now()
127
+ });
128
+ loadRecords.set(client, records.slice(-20));
129
+ }
130
+ const waits = started.map((one) => one.wait?.then((outcome) => {
131
+ if (!outcome.ok) throw outcome.error;
132
+ }));
133
+ await Promise.all(waits);
134
+ const handles = {};
135
+ const data = {};
136
+ for (const one of started) {
137
+ handles[one.name] = {
138
+ input: one.item.input,
139
+ queryKey: one.entry.key
140
+ };
141
+ if (one.wait) data[one.name] = readData(one.entry);
142
+ }
143
+ return [handles, data];
144
+ }
145
+ const waterfallHandles = /* @__PURE__ */ new WeakSet();
146
+ /** Whether a handle came from `waterfallHandle()`: reading it is expected to fetch. */
147
+ function isWaterfallHandle(handle) {
148
+ return waterfallHandles.has(handle);
149
+ }
150
+ function toHandle(factory, input) {
151
+ const handle = {
152
+ input,
153
+ queryKey: optionsOf(factory, input).queryKey
154
+ };
155
+ waterfallHandles.add(handle);
156
+ return handle;
157
+ }
158
+ function waterfallHandle(factory, ...args) {
159
+ if (args.length < 2) return toHandle(factory, args[0]);
160
+ const [handle, update] = args;
161
+ const next = toHandle(factory, update(handle.input));
162
+ return hashKey(next.queryKey) === hashKey(handle.queryKey) ? handle : next;
163
+ }
164
+ //#endregion
165
+ export { isWaterfallHandle, load, loadsOf, prefetch, readData, revalidate, skipToken, startIfStale, swr, waterfallHandle };