@raf-query/core 0.11.1 → 0.13.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 (73) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +28 -18
  3. package/dist/client.d.mts +163 -119
  4. package/dist/client.mjs +90 -162
  5. package/dist/entry.d.mts +73 -147
  6. package/dist/entry.mjs +314 -471
  7. package/dist/features/focus-refetch/focus-refetch.d.mts +17 -0
  8. package/dist/features/focus-refetch/focus-refetch.mjs +20 -0
  9. package/dist/features/polling/polling.d.mts +16 -0
  10. package/dist/features/polling/polling.mjs +39 -0
  11. package/dist/features/reconnect-refetch/reconnect-refetch.d.mts +17 -0
  12. package/dist/features/reconnect-refetch/reconnect-refetch.mjs +16 -0
  13. package/dist/features/shared.mjs +13 -0
  14. package/dist/guards.mjs +23 -0
  15. package/dist/hydration/hydration.d.mts +108 -0
  16. package/dist/hydration/hydration.mjs +142 -0
  17. package/dist/index.d.mts +19 -13
  18. package/dist/index.mjs +16 -13
  19. package/dist/infinite/infinite.d.mts +83 -0
  20. package/dist/infinite/infinite.mjs +120 -0
  21. package/dist/infinite/paging.d.mts +71 -0
  22. package/dist/infinite/paging.mjs +62 -0
  23. package/dist/internal.d.mts +13 -0
  24. package/dist/internal.mjs +7 -0
  25. package/dist/keys/keys.d.mts +54 -0
  26. package/dist/keys/keys.mjs +53 -0
  27. package/dist/keys/targets.d.mts +62 -0
  28. package/dist/keys/targets.mjs +85 -0
  29. package/dist/links/retry/retry.d.mts +33 -0
  30. package/dist/links/retry/retry.mjs +53 -0
  31. package/dist/links/timeout/timeout.d.mts +17 -0
  32. package/dist/links/timeout/timeout.mjs +35 -0
  33. package/dist/load.d.mts +149 -0
  34. package/dist/load.mjs +179 -0
  35. package/dist/options.d.mts +161 -332
  36. package/dist/options.mjs +28 -40
  37. package/dist/presets.d.mts +35 -0
  38. package/dist/presets.mjs +27 -0
  39. package/dist/root.d.mts +86 -0
  40. package/dist/structural-sharing/structural-sharing.mjs +32 -0
  41. package/dist/subscribe.d.mts +15 -0
  42. package/dist/subscribe.mjs +29 -0
  43. package/dist/thenable.d.mts +15 -17
  44. package/dist/thenable.mjs +25 -6
  45. package/dist/verbs.d.mts +150 -0
  46. package/dist/verbs.mjs +115 -0
  47. package/package.json +13 -9
  48. package/dist/_virtual/_rolldown/runtime.mjs +0 -13
  49. package/dist/all-keyed.d.mts +0 -18
  50. package/dist/all-keyed.mjs +0 -19
  51. package/dist/binding.d.mts +0 -92
  52. package/dist/binding.mjs +0 -186
  53. package/dist/cache.d.mts +0 -164
  54. package/dist/cache.mjs +0 -184
  55. package/dist/controls.d.mts +0 -95
  56. package/dist/controls.mjs +0 -70
  57. package/dist/hydration.d.mts +0 -84
  58. package/dist/hydration.mjs +0 -148
  59. package/dist/infinite.d.mts +0 -5
  60. package/dist/infinite.mjs +0 -103
  61. package/dist/key.d.mts +0 -18
  62. package/dist/key.mjs +0 -36
  63. package/dist/listen.mjs +0 -19
  64. package/dist/notify.mjs +0 -21
  65. package/dist/observer.d.mts +0 -110
  66. package/dist/observer.mjs +0 -300
  67. package/dist/platform.d.mts +0 -27
  68. package/dist/platform.mjs +0 -44
  69. package/dist/query.d.mts +0 -137
  70. package/dist/query.mjs +0 -147
  71. package/dist/request.mjs +0 -47
  72. package/dist/retry.mjs +0 -25
  73. package/dist/share.mjs +0 -40
@@ -0,0 +1,120 @@
1
+ import { hashKey } from "../keys/keys.mjs";
2
+ //#region src/infinite/infinite.ts
3
+ /**
4
+ * Typed options for a paged query factory. Readers get `{ pages, pageParams }`
5
+ * and, on their state, `fetchNextPage` and `fetchPreviousPage`:
6
+ *
7
+ * ```ts
8
+ * export const commentsQuery = (input: { slug: string }) =>
9
+ * infiniteQueryOptions({
10
+ * queryKey: comments(input.slug),
11
+ * queryFn: ({ pageParam, signal }) => listComments({ ...input, cursor: pageParam }, { signal }),
12
+ * initialPageParam: 0,
13
+ * getNextPageParam: (lastPage) => lastPage.nextCursor,
14
+ * });
15
+ * ```
16
+ *
17
+ * Its `meta` is any record. To check `meta` against a client's links and
18
+ * features, use the builder {@link bindInfiniteQueryOptions} returns.
19
+ *
20
+ * @see https://raf-query.katt.dev/docs/infinite-queries
21
+ */
22
+ const infiniteQueryOptions = (options) => ({
23
+ ...options,
24
+ "~run": (op) => runPages(op, options)
25
+ });
26
+ /**
27
+ * `infiniteQueryOptions` bound to a root, like `bindQueryOptions`. Bind it
28
+ * next to `queryOptions`, in apps that page:
29
+ *
30
+ * ```ts
31
+ * // raf.ts
32
+ * export const infiniteQueryOptions = bindInfiniteQueryOptions<typeof makeQueryClient>();
33
+ * ```
34
+ *
35
+ * @see https://raf-query.katt.dev/docs/links-and-features#typing-meta-by-its-root
36
+ */
37
+ function bindInfiniteQueryOptions() {
38
+ return infiniteQueryOptions;
39
+ }
40
+ /** The param of the page after (or before) `data`, or `undefined` at the end. */
41
+ function pageParamAfter(options, data, direction) {
42
+ const forward = direction === "next";
43
+ const index = forward ? data.pages.length - 1 : 0;
44
+ const page = data.pages[index];
45
+ const getParam = forward ? options.getNextPageParam : options.getPreviousPageParam;
46
+ if (page === void 0 || !getParam) return;
47
+ return getParam(page, data.pages, data.pageParams[index]) ?? void 0;
48
+ }
49
+ /**
50
+ * The entry's data as it is now, which writes may have changed since `op`
51
+ * started, or `fallback` while it has none.
52
+ */
53
+ function latest(op, fallback) {
54
+ const state = op.client["~entries"].get(op.hash)?.state;
55
+ return state?.hasData ? state.data : fallback;
56
+ }
57
+ function sameParam(a, b) {
58
+ return hashKey([a]) === hashKey([b]);
59
+ }
60
+ /** The page `data` holds for `pageParam`, if any. */
61
+ function pageFor(data, pageParam) {
62
+ const index = data?.pageParams.findIndex((param) => sameParam(param, pageParam)) ?? -1;
63
+ return index === -1 ? void 0 : data?.pages[index];
64
+ }
65
+ async function runPages(op, source) {
66
+ const fetchPage = (pageParam, direction) => Promise.resolve(source.queryFn({
67
+ client: op.client,
68
+ direction,
69
+ meta: op.meta,
70
+ pageParam,
71
+ queryKey: op.queryKey,
72
+ signal: op.signal
73
+ }));
74
+ const previous = op.hasData ? op.data : void 0;
75
+ const direction = op.direction;
76
+ if (previous && direction) {
77
+ const pageParam = pageParamAfter(source, previous, direction);
78
+ if (pageParam === void 0) return previous;
79
+ const page = await fetchPage(pageParam, direction === "next" ? "forward" : "backward");
80
+ const current = latest(op, previous);
81
+ const edge = (data) => direction === "next" ? data.pageParams.at(-1) : data.pageParams[0];
82
+ if (!sameParam(edge(current), edge(previous))) return current;
83
+ const max = source.maxPages ?? Infinity;
84
+ return direction === "next" ? {
85
+ pageParams: [...current.pageParams, pageParam].slice(-max),
86
+ pages: [...current.pages, page].slice(-max)
87
+ } : {
88
+ pageParams: [pageParam, ...current.pageParams].slice(0, max),
89
+ pages: [page, ...current.pages].slice(0, max)
90
+ };
91
+ }
92
+ const count = previous?.pages.length ?? op.pages ?? 1;
93
+ const pages = [];
94
+ const pageParams = [];
95
+ /** The page the entry held for each param when that page's read started. */
96
+ const before = [];
97
+ let pageParam = previous?.pageParams[0] ?? source.initialPageParam;
98
+ while (pages.length < count) {
99
+ before.push(pageFor(latest(op, void 0), pageParam));
100
+ const page = await fetchPage(pageParam, "forward");
101
+ pages.push(page);
102
+ pageParams.push(pageParam);
103
+ const next = pageParamAfter(source, {
104
+ pageParams,
105
+ pages
106
+ }, "next");
107
+ if (next === void 0) break;
108
+ pageParam = next;
109
+ }
110
+ const now = latest(op, void 0);
111
+ return {
112
+ pageParams,
113
+ pages: pages.map((page, index) => {
114
+ const written = pageFor(now, pageParams[index]);
115
+ return written !== void 0 && written !== before[index] ? written : page;
116
+ })
117
+ };
118
+ }
119
+ //#endregion
120
+ export { bindInfiniteQueryOptions, infiniteQueryOptions, pageParamAfter };
@@ -0,0 +1,71 @@
1
+ import { InfiniteQueryFactory, InputArgs } from "../options.mjs";
2
+ import { Entry, EntryState } from "../entry.mjs";
3
+ import { ClientFor } from "../verbs.mjs";
4
+ //#region src/infinite/paging.d.ts
5
+ /**
6
+ * Which way a page loads: after the last one, or before the first.
7
+ *
8
+ * @see https://raf-query.katt.dev/docs/infinite-queries#reading-pages
9
+ */
10
+ export type PageDirection = "next" | "previous";
11
+ /**
12
+ * Where a paged query can load more, and what it's loading now.
13
+ *
14
+ * @see https://raf-query.katt.dev/docs/infinite-queries#reading-pages
15
+ */
16
+ export interface PageState {
17
+ readonly hasNextPage: boolean;
18
+ readonly hasPreviousPage: boolean;
19
+ readonly isFetchingNextPage: boolean;
20
+ readonly isFetchingPreviousPage: boolean;
21
+ }
22
+ /**
23
+ * A paged query's {@link PageState} and the requests that load more:
24
+ * `useInfiniteQuery`'s paging controls.
25
+ *
26
+ * @see https://raf-query.katt.dev/docs/infinite-queries#reading-pages
27
+ */
28
+ export interface PageControls extends PageState {
29
+ /** Load the page after the last one. Resolves when it's there. */
30
+ fetchNextPage(): Promise<void>;
31
+ /** Load the page before the first one. Resolves when it's there. */
32
+ fetchPreviousPage(): Promise<void>;
33
+ }
34
+ /** A paged entry's {@link PageState}, from `state` (the entry's own by default). */
35
+ export declare function pageState(entry: Entry, state?: EntryState): PageState;
36
+ /**
37
+ * Load a paged entry's page in `direction`; resolves when the request
38
+ * settles, and does nothing without a page at that end. An error stays in
39
+ * the entry's state.
40
+ */
41
+ export declare function fetchPage(entry: Entry, direction: PageDirection): Promise<void>;
42
+ /**
43
+ * Load the page after the last one of a paged query, as `useInfiniteQuery`'s
44
+ * `fetchNextPage()` does. Resolves when the page is there, or the request
45
+ * settles; does nothing without a next page.
46
+ *
47
+ * @see https://raf-query.katt.dev/docs/infinite-queries#outside-react
48
+ */
49
+ export declare function fetchNextPage<TFactory extends InfiniteQueryFactory>(client: ClientFor<TFactory>, factory: TFactory, ...input: InputArgs<TFactory>): Promise<void>;
50
+ /**
51
+ * Load the page before the first one of a paged query, as
52
+ * `useInfiniteQuery`'s `fetchPreviousPage()` does. See {@link fetchNextPage}.
53
+ *
54
+ * @see https://raf-query.katt.dev/docs/infinite-queries#outside-react
55
+ */
56
+ export declare function fetchPreviousPage<TFactory extends InfiniteQueryFactory>(client: ClientFor<TFactory>, factory: TFactory, ...input: InputArgs<TFactory>): Promise<void>;
57
+ /**
58
+ * Whether a paged query has a page after its last one: `false` while it has
59
+ * no data, or once `getNextPageParam` returns `undefined`.
60
+ *
61
+ * @see https://raf-query.katt.dev/docs/infinite-queries#outside-react
62
+ */
63
+ export declare function hasNextPage<TFactory extends InfiniteQueryFactory>(client: ClientFor<TFactory>, factory: TFactory, ...input: InputArgs<TFactory>): boolean;
64
+ /**
65
+ * Whether a paged query has a page before its first one. See
66
+ * {@link hasNextPage}.
67
+ *
68
+ * @see https://raf-query.katt.dev/docs/infinite-queries#outside-react
69
+ */
70
+ export declare function hasPreviousPage<TFactory extends InfiniteQueryFactory>(client: ClientFor<TFactory>, factory: TFactory, ...input: InputArgs<TFactory>): boolean;
71
+ //#endregion
@@ -0,0 +1,62 @@
1
+ import { entryOf, fetchEntry } from "../entry.mjs";
2
+ import { pageParamAfter } from "./infinite.mjs";
3
+ //#region src/infinite/paging.ts
4
+ function hasPage(entry, state, direction) {
5
+ return state.hasData && pageParamAfter(entry.options, state.data, direction) !== void 0;
6
+ }
7
+ /** A paged entry's {@link PageState}, from `state` (the entry's own by default). */
8
+ function pageState(entry, state = entry.state) {
9
+ return {
10
+ hasNextPage: hasPage(entry, state, "next"),
11
+ hasPreviousPage: hasPage(entry, state, "previous"),
12
+ isFetchingNextPage: state.fetchingDirection === "next",
13
+ isFetchingPreviousPage: state.fetchingDirection === "previous"
14
+ };
15
+ }
16
+ /**
17
+ * Load a paged entry's page in `direction`; resolves when the request
18
+ * settles, and does nothing without a page at that end. An error stays in
19
+ * the entry's state.
20
+ */
21
+ function fetchPage(entry, direction) {
22
+ return fetchEntry(entry, { direction }).then(() => {});
23
+ }
24
+ /**
25
+ * Load the page after the last one of a paged query, as `useInfiniteQuery`'s
26
+ * `fetchNextPage()` does. Resolves when the page is there, or the request
27
+ * settles; does nothing without a next page.
28
+ *
29
+ * @see https://raf-query.katt.dev/docs/infinite-queries#outside-react
30
+ */
31
+ function fetchNextPage(client, factory, ...input) {
32
+ return fetchPage(entryOf(client, factory, input[0]), "next");
33
+ }
34
+ /**
35
+ * Load the page before the first one of a paged query, as
36
+ * `useInfiniteQuery`'s `fetchPreviousPage()` does. See {@link fetchNextPage}.
37
+ *
38
+ * @see https://raf-query.katt.dev/docs/infinite-queries#outside-react
39
+ */
40
+ function fetchPreviousPage(client, factory, ...input) {
41
+ return fetchPage(entryOf(client, factory, input[0]), "previous");
42
+ }
43
+ /**
44
+ * Whether a paged query has a page after its last one: `false` while it has
45
+ * no data, or once `getNextPageParam` returns `undefined`.
46
+ *
47
+ * @see https://raf-query.katt.dev/docs/infinite-queries#outside-react
48
+ */
49
+ function hasNextPage(client, factory, ...input) {
50
+ return pageState(entryOf(client, factory, input[0])).hasNextPage;
51
+ }
52
+ /**
53
+ * Whether a paged query has a page before its first one. See
54
+ * {@link hasNextPage}.
55
+ *
56
+ * @see https://raf-query.katt.dev/docs/infinite-queries#outside-react
57
+ */
58
+ function hasPreviousPage(client, factory, ...input) {
59
+ return pageState(entryOf(client, factory, input[0])).hasPreviousPage;
60
+ }
61
+ //#endregion
62
+ export { fetchNextPage, fetchPage, fetchPreviousPage, hasNextPage, hasPreviousPage, pageState };
@@ -0,0 +1,13 @@
1
+ import { AnyFactory, hashKey } from "./keys/keys.mjs";
2
+ import { Decl, DefaultTypes, MetaOf, RootOf, RootTypes } from "./root.mjs";
3
+ import { AnyTarget } from "./keys/targets.mjs";
4
+ import { fulfilled, mapThenable, pending, rejected } from "./thenable.mjs";
5
+ import { Entry, EntryState, entryOf, fetchEntry, isBrowserOnly, isInvalidated, isStale, remove, reset, subscribeEntry } from "./entry.mjs";
6
+ import { AnyFeature, AnyLink, ClientEvent, mount, systemClock } from "./client.mjs";
7
+ import { FocusRefetchMeta } from "./features/focus-refetch/focus-refetch.mjs";
8
+ import { ReconnectRefetchMeta } from "./features/reconnect-refetch/reconnect-refetch.mjs";
9
+ import { PollingMeta } from "./features/polling/polling.mjs";
10
+ import { fetchPage, pageState } from "./infinite/paging.mjs";
11
+ import { RetryMeta } from "./links/retry/retry.mjs";
12
+ import { LoadRecord, isWaterfallHandle, loadsOf, readData, startIfStale } from "./load.mjs";
13
+ export { type AnyFactory, type AnyFeature, type AnyLink, type AnyTarget, type ClientEvent, type Decl, type DefaultTypes, type Entry, type EntryState, type FocusRefetchMeta, type LoadRecord, type MetaOf, type PollingMeta, type ReconnectRefetchMeta, type RetryMeta, type RootOf, type RootTypes, entryOf, fetchEntry, fetchPage, fulfilled, hashKey, isBrowserOnly, isInvalidated, isStale, isWaterfallHandle, loadsOf, mapThenable, mount, pageState, pending, readData, rejected, remove, reset, startIfStale, subscribeEntry, systemClock };
@@ -0,0 +1,7 @@
1
+ import { mount, systemClock } from "./client.mjs";
2
+ import { hashKey } from "./keys/keys.mjs";
3
+ import { fulfilled, mapThenable, pending, rejected } from "./thenable.mjs";
4
+ import { entryOf, fetchEntry, isBrowserOnly, isInvalidated, isStale, remove, reset, subscribeEntry } from "./entry.mjs";
5
+ import { fetchPage, pageState } from "./infinite/paging.mjs";
6
+ import { isWaterfallHandle, loadsOf, readData, startIfStale } from "./load.mjs";
7
+ export { entryOf, fetchEntry, fetchPage, fulfilled, hashKey, isBrowserOnly, isInvalidated, isStale, isWaterfallHandle, loadsOf, mapThenable, mount, pageState, pending, readData, rejected, remove, reset, startIfStale, subscribeEntry, systemClock };
@@ -0,0 +1,54 @@
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
+ /**
12
+ * A query's key: a plain, serializable array.
13
+ *
14
+ * @see https://raf-query.katt.dev/docs/query-keys
15
+ */
16
+ export type QueryKey = ReadonlyArray<unknown>;
17
+ declare const groupBrand: unique symbol;
18
+ /**
19
+ * A named set of queries: those whose key starts with its path. Call it
20
+ * for a key under that path; a subgroup's path extends its parent's, so a
21
+ * group covers its subgroups and their queries.
22
+ *
23
+ * @see https://raf-query.katt.dev/docs/query-keys#groups
24
+ */
25
+ export interface Group<TPath extends ReadonlyArray<string> = ReadonlyArray<string>> {
26
+ <const TRest extends QueryKey>(...rest: TRest): readonly [...TPath, ...TRest];
27
+ readonly [groupBrand]: TPath;
28
+ /** Its parents' names and its own. */
29
+ readonly path: TPath;
30
+ }
31
+ /**
32
+ * A group, or a subgroup inside `parent`. Call it for a query's key: a
33
+ * flat, plain array that starts with the group's path. Invalidating a group
34
+ * covers its subgroups and every query below them.
35
+ *
36
+ * ```ts
37
+ * export const posts = group("posts");
38
+ * export const drafts = group(posts, "drafts");
39
+ * const draftById = (input: { id: string }) =>
40
+ * queryOptions({ queryKey: drafts("byId", input), queryFn }); // ["posts", "drafts", "byId", input]
41
+ * await invalidateMany(client, posts); // drafts too
42
+ * ```
43
+ *
44
+ * @see https://raf-query.katt.dev/docs/query-keys#groups
45
+ */
46
+ export declare function group<const TName extends string>(name: TName): Group<readonly [TName]>;
47
+ export declare function group<const TPath extends ReadonlyArray<string>, const TName extends string>(parent: Group<TPath>, name: TName): Group<readonly [...TPath, TName]>;
48
+ /** Stable string form of a key: JSON with sorted fields. Functions drop out. */
49
+ export declare function hashKey(key: QueryKey): string;
50
+ /** Any function that returns a query's options. */
51
+ export type AnyFactory = (input: never) => {
52
+ readonly queryKey: QueryKey;
53
+ };
54
+ //#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,62 @@
1
+ import { AnyFactory, Group, QueryKey } from "./keys.mjs";
2
+ import { QueryMeta } from "../root.mjs";
3
+ //#region src/keys/targets.d.ts
4
+ /**
5
+ * What a target's `where` sees of a query.
6
+ *
7
+ * @see https://raf-query.katt.dev/docs/query-invalidation#matching-with-where
8
+ */
9
+ export interface QueryInfo<TMeta = QueryMeta> {
10
+ /** The query's data, before `select`; `undefined` without data. */
11
+ readonly data: unknown;
12
+ readonly input: unknown;
13
+ /** The `meta` its factory gave it. An entry only hydrated has `{}` until it's read. */
14
+ readonly meta: TMeta;
15
+ readonly queryKey: QueryKey;
16
+ }
17
+ /**
18
+ * Picks out queries by what they are: `where` gets each query and the
19
+ * parameter the verb was called with.
20
+ *
21
+ * ```ts
22
+ * const ownedBy: Target<AppMeta, Team> = { where: (query, team) => query.meta.owner === team };
23
+ * await invalidateMany(client, ownedBy, "editorial");
24
+ * ```
25
+ *
26
+ * @see https://raf-query.katt.dev/docs/query-invalidation#targets
27
+ */
28
+ export interface Target<TMeta = QueryMeta, TParam = never> {
29
+ readonly where: (query: QueryInfo<TMeta>, param: TParam) => boolean;
30
+ }
31
+ /** A target of any meta and parameter. */
32
+ export interface AnyTarget {
33
+ readonly where: (query: never, param: never) => boolean;
34
+ }
35
+ /** The arguments after a target: its parameter, if it takes one. */
36
+ export type ParamArgs<TParam> = [TParam] extends [never] ? [] : unknown extends TParam ? [] : undefined extends TParam ? [param?: TParam] : [param: TParam];
37
+ /** One invalidation, by what it targets. */
38
+ export type Stamp = {
39
+ readonly at: number;
40
+ readonly kind: "all";
41
+ } | {
42
+ readonly at: number;
43
+ readonly hash: string;
44
+ readonly kind: "entry";
45
+ } | {
46
+ readonly at: number;
47
+ readonly factory: AnyFactory;
48
+ readonly kind: "factory";
49
+ readonly partial: unknown;
50
+ } | {
51
+ readonly at: number;
52
+ readonly kind: "group";
53
+ readonly path: ReadonlyArray<string>;
54
+ } | {
55
+ readonly at: number;
56
+ readonly kind: "target";
57
+ readonly param: unknown;
58
+ readonly target: AnyTarget;
59
+ };
60
+ /** Anything a verb can invalidate by. */
61
+ export type AnyInvalidation = AnyFactory | AnyTarget | Group;
62
+ //#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,33 @@
1
+ import { Link } from "../../client.mjs";
2
+ //#region src/links/retry/retry.d.ts
3
+ /**
4
+ * Options for {@link retry}.
5
+ *
6
+ * @see https://raf-query.katt.dev/docs/retries#options
7
+ */
8
+ export interface RetryOptions {
9
+ /** Retries after the first attempt. A query's `meta.retry` wins. Default 3. */
10
+ readonly attempts?: number;
11
+ /** Wait before retry `n` (0-based), in ms. Default: doubling from 1 s, at most 30 s. */
12
+ readonly delay?: (attempt: number) => number;
13
+ /** Whether an error is worth retrying. Default: every error but a thrown `Response`. */
14
+ readonly when?: (error: unknown) => boolean;
15
+ }
16
+ /** What `retry()` reads from a query's `meta`. */
17
+ export interface RetryMeta {
18
+ /** Attempts for this query, or `false`. */
19
+ readonly retry?: number | false;
20
+ }
21
+ /**
22
+ * Retry failed requests. Thrown `Response`s (router redirects and
23
+ * not-founds) and cancelled requests aren't retried,
24
+ * server clients never retry, and a browser that is offline
25
+ * waits until it's back online before retrying. A query sets its own
26
+ * attempts with `meta: { retry }`.
27
+ *
28
+ * @see https://raf-query.katt.dev/docs/retries
29
+ */
30
+ export declare function retry(options?: RetryOptions): Link<{
31
+ readonly meta: RetryMeta;
32
+ }>;
33
+ //#endregion
@@ -0,0 +1,53 @@
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
+ * @see https://raf-query.katt.dev/docs/retries
37
+ */
38
+ function retry(options = {}) {
39
+ return link(async (op, next) => {
40
+ const configured = op.meta.retry;
41
+ const attempts = configured === false ? 0 : configured ?? options.attempts ?? 3;
42
+ if (op.client.isServer || attempts === 0) return next(op);
43
+ for (let attempt = 0;; attempt++) try {
44
+ return await next(op);
45
+ } catch (error) {
46
+ if (attempt >= attempts || op.signal.aborted || !(options.when ?? notThrownResponse)(error)) throw error;
47
+ await wait(op.client.clock, (options.delay ?? defaultDelay)(attempt), op.signal);
48
+ await whenOnline(op.signal);
49
+ }
50
+ });
51
+ }
52
+ //#endregion
53
+ export { retry };
@@ -0,0 +1,17 @@
1
+ import { Link } from "../../client.mjs";
2
+ //#region src/links/timeout/timeout.d.ts
3
+ /**
4
+ * What a request that took too long fails with.
5
+ *
6
+ * @see https://raf-query.katt.dev/docs/retries#timeouts
7
+ */
8
+ export declare class QueryTimeoutError extends Error {
9
+ readonly name = "QueryTimeoutError";
10
+ }
11
+ /**
12
+ * Fail a request (and abort its `signal`) when it takes longer than `ms`.
13
+ *
14
+ * @see https://raf-query.katt.dev/docs/retries#timeouts
15
+ */
16
+ export declare function timeout(ms: number): Link;
17
+ //#endregion
@@ -0,0 +1,35 @@
1
+ //#region src/links/timeout/timeout.ts
2
+ /**
3
+ * What a request that took too long fails with.
4
+ *
5
+ * @see https://raf-query.katt.dev/docs/retries#timeouts
6
+ */
7
+ var QueryTimeoutError = class extends Error {
8
+ name = "QueryTimeoutError";
9
+ };
10
+ /**
11
+ * Fail a request (and abort its `signal`) when it takes longer than `ms`.
12
+ *
13
+ * @see https://raf-query.katt.dev/docs/retries#timeouts
14
+ */
15
+ function timeout(ms) {
16
+ return (op, next) => {
17
+ const controller = new AbortController();
18
+ const signal = AbortSignal.any([op.signal, controller.signal]);
19
+ return new Promise((resolve, reject) => {
20
+ const handle = op.client.clock.setTimeout(() => {
21
+ const error = new QueryTimeoutError(`Query ${op.hash} took longer than ${ms} ms`);
22
+ controller.abort(error);
23
+ reject(error);
24
+ }, ms);
25
+ next({
26
+ ...op,
27
+ signal
28
+ }).then(resolve, reject).finally(() => {
29
+ op.client.clock.clearTimeout(handle);
30
+ });
31
+ });
32
+ };
33
+ }
34
+ //#endregion
35
+ export { QueryTimeoutError, timeout };