@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.
- package/CHANGELOG.md +39 -1
- package/README.md +28 -18
- package/dist/client.d.mts +163 -119
- package/dist/client.mjs +90 -162
- package/dist/entry.d.mts +73 -147
- package/dist/entry.mjs +314 -471
- package/dist/features/focus-refetch/focus-refetch.d.mts +17 -0
- package/dist/features/focus-refetch/focus-refetch.mjs +20 -0
- package/dist/features/polling/polling.d.mts +16 -0
- package/dist/features/polling/polling.mjs +39 -0
- package/dist/features/reconnect-refetch/reconnect-refetch.d.mts +17 -0
- package/dist/features/reconnect-refetch/reconnect-refetch.mjs +16 -0
- package/dist/features/shared.mjs +13 -0
- package/dist/guards.mjs +23 -0
- package/dist/hydration/hydration.d.mts +108 -0
- package/dist/hydration/hydration.mjs +142 -0
- package/dist/index.d.mts +19 -13
- package/dist/index.mjs +16 -13
- package/dist/infinite/infinite.d.mts +83 -0
- package/dist/infinite/infinite.mjs +120 -0
- package/dist/infinite/paging.d.mts +71 -0
- package/dist/infinite/paging.mjs +62 -0
- package/dist/internal.d.mts +13 -0
- package/dist/internal.mjs +7 -0
- package/dist/keys/keys.d.mts +54 -0
- package/dist/keys/keys.mjs +53 -0
- package/dist/keys/targets.d.mts +62 -0
- package/dist/keys/targets.mjs +85 -0
- package/dist/links/retry/retry.d.mts +33 -0
- package/dist/links/retry/retry.mjs +53 -0
- package/dist/links/timeout/timeout.d.mts +17 -0
- package/dist/links/timeout/timeout.mjs +35 -0
- package/dist/load.d.mts +149 -0
- package/dist/load.mjs +179 -0
- package/dist/options.d.mts +161 -332
- package/dist/options.mjs +28 -40
- package/dist/presets.d.mts +35 -0
- package/dist/presets.mjs +27 -0
- package/dist/root.d.mts +86 -0
- package/dist/structural-sharing/structural-sharing.mjs +32 -0
- package/dist/subscribe.d.mts +15 -0
- package/dist/subscribe.mjs +29 -0
- package/dist/thenable.d.mts +15 -17
- package/dist/thenable.mjs +25 -6
- package/dist/verbs.d.mts +150 -0
- package/dist/verbs.mjs +115 -0
- package/package.json +13 -9
- package/dist/_virtual/_rolldown/runtime.mjs +0 -13
- package/dist/all-keyed.d.mts +0 -18
- package/dist/all-keyed.mjs +0 -19
- package/dist/binding.d.mts +0 -92
- package/dist/binding.mjs +0 -186
- package/dist/cache.d.mts +0 -164
- package/dist/cache.mjs +0 -184
- package/dist/controls.d.mts +0 -95
- package/dist/controls.mjs +0 -70
- package/dist/hydration.d.mts +0 -84
- package/dist/hydration.mjs +0 -148
- package/dist/infinite.d.mts +0 -5
- package/dist/infinite.mjs +0 -103
- package/dist/key.d.mts +0 -18
- package/dist/key.mjs +0 -36
- package/dist/listen.mjs +0 -19
- package/dist/notify.mjs +0 -21
- package/dist/observer.d.mts +0 -110
- package/dist/observer.mjs +0 -300
- package/dist/platform.d.mts +0 -27
- package/dist/platform.mjs +0 -44
- package/dist/query.d.mts +0 -137
- package/dist/query.mjs +0 -147
- package/dist/request.mjs +0 -47
- package/dist/retry.mjs +0 -25
- 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 };
|