openapi-typesafe-react-query 1.0.0-rc.4 → 1.0.0-rc.5
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 +18 -0
- package/README.md +1 -1
- package/dist/index.cjs +139 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -5
- package/dist/index.d.mts +40 -5
- package/dist/index.d.ts +40 -5
- package/dist/index.mjs +139 -31
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +113 -51
- package/src/query-key.ts +136 -0
- package/src/query-signal.ts +47 -0
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type DataTag,
|
|
3
3
|
type DefaultError,
|
|
4
|
+
type DefinedUseQueryResult,
|
|
4
5
|
type InfiniteData,
|
|
5
6
|
type QueryClient,
|
|
6
7
|
type QueryFunctionContext,
|
|
@@ -35,6 +36,8 @@ import type {
|
|
|
35
36
|
PathsWithMethod,
|
|
36
37
|
RequiredKeysOf,
|
|
37
38
|
} from "openapi-typesafe-fetch/helpers";
|
|
39
|
+
import { queryKeyInit } from "./query-key.js";
|
|
40
|
+
import { withQuerySignal } from "./query-signal.js";
|
|
38
41
|
|
|
39
42
|
// Helper type to dynamically infer the type from the `select` property
|
|
40
43
|
type InferSelectReturnType<TData, TSelect> = TSelect extends (data: TData) => infer R ? R : TData;
|
|
@@ -100,6 +103,12 @@ type InitArguments<Init, Rest extends unknown[]> =
|
|
|
100
103
|
? [InitWithUnknowns<Init>?, ...Rest]
|
|
101
104
|
: [InitWithUnknowns<Init>, ...Rest];
|
|
102
105
|
|
|
106
|
+
/**
|
|
107
|
+
* The key of a query: the method, the path and the request. Its init is the init of the call with
|
|
108
|
+
* the headers and the body as plain values, and the client's `keyScope`, or its `baseUrl` when the
|
|
109
|
+
* init has none, so two requests never share a key; it is left out when nothing is left. Read a
|
|
110
|
+
* query's key from `queryOptions(...).queryKey` rather than writing it out.
|
|
111
|
+
*/
|
|
103
112
|
export type QueryKey<
|
|
104
113
|
Paths extends {},
|
|
105
114
|
Method extends HttpMethod,
|
|
@@ -121,6 +130,36 @@ export type InfiniteQueryKey<
|
|
|
121
130
|
Path extends PathsWithMethod<Paths, Method>,
|
|
122
131
|
> = readonly [...QueryKey<Paths, Method, Path>, typeof INFINITE_QUERY_KEY_MARKER];
|
|
123
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Whether the options give an `initialData` that is never `undefined`: a value, or a function that
|
|
135
|
+
* never returns `undefined`. Then `data` is never `undefined` either, as for TanStack Query's own
|
|
136
|
+
* `useQuery` with `DefinedInitialDataOptions`.
|
|
137
|
+
*/
|
|
138
|
+
type HasDefinedInitialData<Options> = Options extends { initialData: infer Initial }
|
|
139
|
+
? Initial extends (...args: never) => infer Returned
|
|
140
|
+
? undefined extends Returned
|
|
141
|
+
? false
|
|
142
|
+
: true
|
|
143
|
+
: undefined extends Initial
|
|
144
|
+
? false
|
|
145
|
+
: true
|
|
146
|
+
: false;
|
|
147
|
+
|
|
148
|
+
/** The result of `useQuery`: its `data` is defined when the options give defined initial data. */
|
|
149
|
+
type QueryResult<Data, Error, Options> =
|
|
150
|
+
HasDefinedInitialData<Options> extends true
|
|
151
|
+
? DefinedUseQueryResult<Data, Error>
|
|
152
|
+
: UseQueryResult<Data, Error>;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A defined `initialData` of the options, in the type `queryOptions` returns, so TanStack Query's
|
|
156
|
+
* `useQuery` takes the result as options with defined initial data. It is typed as the query data,
|
|
157
|
+
* as TanStack's `DefinedInitialDataOptions` types it: the type of a literal such as `[{ id: 1 }]`
|
|
158
|
+
* lacks the optional properties of the data, and TanStack's overload rejected it.
|
|
159
|
+
*/
|
|
160
|
+
type InitialDataOf<Options, Data> =
|
|
161
|
+
HasDefinedInitialData<Options> extends true ? { initialData: Data | (() => Data) } : unknown;
|
|
162
|
+
|
|
124
163
|
export type QueryOptionsFunction<Paths extends {}, Media extends MediaType> = <
|
|
125
164
|
Method extends HttpMethod,
|
|
126
165
|
Path extends PathsWithMethod<Paths, Method>,
|
|
@@ -154,7 +193,8 @@ export type QueryOptionsFunction<Paths extends {}, Media extends MediaType> = <
|
|
|
154
193
|
QueryKey<Paths, Method, Path>,
|
|
155
194
|
QueryData<Response["data"]>,
|
|
156
195
|
QueryError<Response["error"]>
|
|
157
|
-
> &
|
|
196
|
+
> &
|
|
197
|
+
InitialDataOf<Options, QueryData<Response["data"]>> & {
|
|
158
198
|
queryFn: Exclude<
|
|
159
199
|
UseQueryOptions<
|
|
160
200
|
QueryData<Response["data"]>,
|
|
@@ -186,9 +226,10 @@ export type UseQueryMethod<Paths extends {}, Media extends MediaType> = <
|
|
|
186
226
|
method: Method,
|
|
187
227
|
url: Path,
|
|
188
228
|
...[init, options, queryClient]: InitArguments<Init, [Options?, QueryClient?]>
|
|
189
|
-
) =>
|
|
229
|
+
) => QueryResult<
|
|
190
230
|
InferSelectReturnType<QueryData<Response["data"]>, Options["select"]>,
|
|
191
|
-
QueryError<Response["error"]
|
|
231
|
+
QueryError<Response["error"]>,
|
|
232
|
+
Options
|
|
192
233
|
>;
|
|
193
234
|
|
|
194
235
|
/**
|
|
@@ -408,7 +449,6 @@ const DEFAULT_PAGE_PARAM_NAME = "cursor";
|
|
|
408
449
|
interface PageRequest {
|
|
409
450
|
readonly pageParamName: string;
|
|
410
451
|
readonly pageParam: unknown;
|
|
411
|
-
readonly signal: AbortSignal;
|
|
412
452
|
}
|
|
413
453
|
|
|
414
454
|
/**
|
|
@@ -416,10 +456,21 @@ interface PageRequest {
|
|
|
416
456
|
* `initialPageParam: undefined`) is left out instead of being sent as `0` (upstream #2715, #2340,
|
|
417
457
|
* ADR-022), so a cursor API gets no cursor on the first page.
|
|
418
458
|
*/
|
|
419
|
-
function pageInit(init: unknown, { pageParamName, pageParam
|
|
459
|
+
function pageInit(init: unknown, { pageParamName, pageParam }: PageRequest) {
|
|
420
460
|
const { params, ...rest } = (init ?? {}) as { params?: { query?: DefaultParamsOption } };
|
|
421
461
|
const page = pageParam === undefined ? {} : { [pageParamName]: pageParam };
|
|
422
|
-
return { ...rest,
|
|
462
|
+
return { ...rest, params: { ...params, query: { ...params?.query, ...page } } };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Options of `createClient`. */
|
|
466
|
+
export interface CreateClientOptions {
|
|
467
|
+
/**
|
|
468
|
+
* The name of the API in every query key, in place of the fetch client's `baseUrl`. Give the
|
|
469
|
+
* same name where the server and the browser reach one API by different URLs, so the browser
|
|
470
|
+
* reads the data the server rendered with; give different names to clients of one base URL that
|
|
471
|
+
* fetch different data, such as one per tenant header.
|
|
472
|
+
*/
|
|
473
|
+
keyScope?: string;
|
|
423
474
|
}
|
|
424
475
|
|
|
425
476
|
/**
|
|
@@ -429,49 +480,70 @@ function pageInit(init: unknown, { pageParamName, pageParam, signal }: PageReque
|
|
|
429
480
|
// biome-ignore lint/complexity/noExcessiveLinesPerFunction: inherited, kept until 1.1 (ADR-036)
|
|
430
481
|
export default function createClient<Paths extends {}, Media extends MediaType = MediaType>(
|
|
431
482
|
client: FetchClient<Paths, Media>,
|
|
483
|
+
{ keyScope }: CreateClientOptions = {},
|
|
432
484
|
): OpenapiQueryClient<Paths, Media> {
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
485
|
+
const keyClient = { baseUrl: client.baseUrl, keyScope };
|
|
486
|
+
/**
|
|
487
|
+
* Send one request of a query: the call's own init, with a signal that TanStack Query can abort.
|
|
488
|
+
* The key only names the cache entry, as for `useInfiniteQuery`: a function in the init (a
|
|
489
|
+
* `fetch`, a `middleware`) cannot be part of a key, so the request is read from the call.
|
|
490
|
+
*/
|
|
491
|
+
const fetchPage = async <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>>(
|
|
492
|
+
method: Method,
|
|
493
|
+
path: Path,
|
|
494
|
+
init: unknown,
|
|
495
|
+
signal: AbortSignal,
|
|
496
|
+
) => {
|
|
440
497
|
const mth = method.toUpperCase() as Uppercase<typeof method>;
|
|
441
498
|
const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
499
|
+
const request = withQuerySignal(init, signal);
|
|
500
|
+
try {
|
|
501
|
+
// The init is `unknown` here. An `any` init lets the call infer the generic response type
|
|
502
|
+
// that `queryOptions` declares; a typed init makes TypeScript compare two unresolved
|
|
503
|
+
// conditional types, which it cannot do.
|
|
504
|
+
// biome-ignore lint/suspicious/noExplicitAny: see the comment above
|
|
505
|
+
const result = await fn(path, request.init as any);
|
|
506
|
+
throwIfFailed(result);
|
|
507
|
+
return result.data;
|
|
508
|
+
} finally {
|
|
509
|
+
request.release();
|
|
510
|
+
}
|
|
452
511
|
};
|
|
453
512
|
|
|
454
|
-
const queryOptions: QueryOptionsFunction<Paths, Media> = (method, path, ...[init, options]) =>
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
: (
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
513
|
+
const queryOptions: QueryOptionsFunction<Paths, Media> = (method, path, ...[init, options]) => {
|
|
514
|
+
const keyInit = queryKeyInit(init, keyClient);
|
|
515
|
+
return {
|
|
516
|
+
queryKey: (keyInit === undefined
|
|
517
|
+
? ([method, path] as const)
|
|
518
|
+
: ([method, path, keyInit] as const)) as DataTag<
|
|
519
|
+
QueryKey<Paths, typeof method, typeof path>,
|
|
520
|
+
// biome-ignore lint/suspicious/noExplicitAny: the declared return type sets the data type
|
|
521
|
+
any,
|
|
522
|
+
// biome-ignore lint/suspicious/noExplicitAny: the declared return type sets the error type
|
|
523
|
+
any
|
|
524
|
+
>,
|
|
525
|
+
// TanStack Query rejects `undefined` query data. The fetch client gives `undefined` for
|
|
526
|
+
// every success without a body: a 204, `Content-Length: 0`, an empty chunked body, a HEAD
|
|
527
|
+
// request (upstream #2234). A parsed body is never `undefined`, so this changes nothing else.
|
|
528
|
+
queryFn: async ({ signal }: QueryFunctionContext) =>
|
|
529
|
+
(await fetchPage(method, path, init, signal)) ?? null,
|
|
530
|
+
// Without options, `Options` is its constraint, which has no `initialData` to keep.
|
|
531
|
+
...(options as typeof options & {}),
|
|
532
|
+
// The declared type types a defined `initialData` as the query data, a conditional type of
|
|
533
|
+
// the generic options that TypeScript cannot relate to the object; it is the same object.
|
|
534
|
+
} as never;
|
|
535
|
+
};
|
|
467
536
|
|
|
468
537
|
return {
|
|
469
538
|
queryOptions,
|
|
470
539
|
useQuery: (method, path, ...[init, options, queryClient]) =>
|
|
540
|
+
// TanStack Query's `useQuery` returns `DefinedUseQueryResult` for options with defined
|
|
541
|
+
// `initialData`; while `Options` is generic it cannot pick that overload, and the declared
|
|
542
|
+
// `QueryResult` gives the caller the same answer. Both are the same object at runtime.
|
|
471
543
|
useQuery(
|
|
472
544
|
queryOptions(method, path, init as InitWithUnknowns<typeof init>, options),
|
|
473
545
|
queryClient,
|
|
474
|
-
),
|
|
546
|
+
) as never,
|
|
475
547
|
useSuspenseQuery: (method, path, ...[init, options, queryClient]) =>
|
|
476
548
|
useSuspenseQuery(
|
|
477
549
|
queryOptions(method, path, init as InitWithUnknowns<typeof init>, options),
|
|
@@ -481,23 +553,13 @@ export default function createClient<Paths extends {}, Media extends MediaType =
|
|
|
481
553
|
useInfiniteQuery: (method, path, init, options, queryClient) => {
|
|
482
554
|
const { pageParamName = DEFAULT_PAGE_PARAM_NAME, placeholderData, ...restOptions } = options;
|
|
483
555
|
const { queryKey } = queryOptions(method, path, init);
|
|
484
|
-
const mth = method.toUpperCase() as Uppercase<typeof method>;
|
|
485
|
-
const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;
|
|
486
556
|
return useInfiniteQuery(
|
|
487
557
|
{
|
|
488
558
|
queryKey: [...queryKey, INFINITE_QUERY_KEY_MARKER] as const,
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
// biome-ignore lint/suspicious/noExplicitAny: a generic init; see queryFn
|
|
494
|
-
pageInit(init, { pageParamName, pageParam, signal }) as any,
|
|
495
|
-
);
|
|
496
|
-
throwIfFailed(result);
|
|
497
|
-
// An empty page stays `undefined`, as in upstream: TanStack Query rejects only
|
|
498
|
-
// `undefined` query data, and the infinite query data is the `{ pages }` object.
|
|
499
|
-
return result.data;
|
|
500
|
-
},
|
|
559
|
+
// An empty page stays `undefined`, as in upstream: TanStack Query rejects only
|
|
560
|
+
// `undefined` query data, and the infinite query data is the `{ pages }` object.
|
|
561
|
+
queryFn: ({ pageParam, signal }) =>
|
|
562
|
+
fetchPage(method, path, pageInit(init, { pageParamName, pageParam }), signal),
|
|
501
563
|
...restOptions,
|
|
502
564
|
// Typed with an `unknown` page param for the caller (`InfinitePlaceholderData`). TanStack
|
|
503
565
|
// passes the previous data of this query, with its own page params, and takes it back.
|
package/src/query-key.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The request part of a query key (ADR-046). TanStack Query hashes a key as JSON: a `Headers`
|
|
3
|
+
* object or a list of header pairs hashed as `{}` or by its order, and so did a FormData, a Blob,
|
|
4
|
+
* an ArrayBuffer or a URLSearchParams body. Two requests that differed in them shared one cache
|
|
5
|
+
* entry, and the `queryFn` of that entry sent the request of the first. Two clients with
|
|
6
|
+
* different base URLs shared entries too. The key's init holds the headers and such a body as
|
|
7
|
+
* plain values, and the base URL the request goes to or the client's key scope, so the key names
|
|
8
|
+
* what was fetched.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Headers as a plain object, which TanStack Query hashes by its names and values. */
|
|
12
|
+
type HeadersRecord = Readonly<Record<string, unknown>>;
|
|
13
|
+
|
|
14
|
+
/** What the key takes from the fetch client and the options of `createClient`. */
|
|
15
|
+
export interface KeyClient {
|
|
16
|
+
/** The fetch client's base URL, `""` without one. */
|
|
17
|
+
readonly baseUrl: string;
|
|
18
|
+
/** The name of the API in every key, in place of the base URL. */
|
|
19
|
+
readonly keyScope?: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `[object Name]`: the type tag is between these indexes of `Object.prototype.toString`. */
|
|
23
|
+
const TYPE_TAG_START = "[object ".length;
|
|
24
|
+
const TYPE_TAG_END = -1;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether a value is of the class `name`: an instance of this realm's global class of that name
|
|
28
|
+
* (React Native's classes have no type tag), or a value with its type tag (a class of another
|
|
29
|
+
* realm or of the `undici` package), as the fetch client tells bodies (ADR-030).
|
|
30
|
+
*/
|
|
31
|
+
function isClass(value: object, name: string): boolean {
|
|
32
|
+
const type = (globalThis as Record<string, unknown>)[name];
|
|
33
|
+
return (
|
|
34
|
+
(typeof type === "function" && value instanceof type) ||
|
|
35
|
+
Object.prototype.toString.call(value).slice(TYPE_TAG_START, TYPE_TAG_END) === name
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The headers of an init as a plain object. A `Headers` object and a list of pairs, which are both
|
|
41
|
+
* iterable, become the names and values the request sends: names in lowercase, a repeated one
|
|
42
|
+
* joined with `, `, as `Headers` writes them. They are read without `Headers`, which threw for an
|
|
43
|
+
* invalid name or value while the component rendered; the request reports it as a query error. A
|
|
44
|
+
* plain object stays as it is, since its `null` and array values mean more than a string.
|
|
45
|
+
*/
|
|
46
|
+
function plainHeaders(headers: object): HeadersRecord {
|
|
47
|
+
if (!(Symbol.iterator in headers)) {
|
|
48
|
+
return headers as HeadersRecord;
|
|
49
|
+
}
|
|
50
|
+
const record: Record<string, string> = {};
|
|
51
|
+
for (const pair of headers as Iterable<unknown>) {
|
|
52
|
+
const [name, value] = Array.from(pair as ArrayLike<unknown>);
|
|
53
|
+
const key = String(name).toLowerCase();
|
|
54
|
+
record[key] = key in record ? `${record[key]}, ${String(value)}` : String(value);
|
|
55
|
+
}
|
|
56
|
+
return record;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A Blob or a File in a key: its type, size, and the name and date of a File. Its bytes cannot be
|
|
61
|
+
* read while the component renders, so two Blobs of the same type and size share an entry.
|
|
62
|
+
*/
|
|
63
|
+
function blobKey(blob: Blob): object {
|
|
64
|
+
const { name, lastModified } = blob as Partial<File>;
|
|
65
|
+
return { type: blob.type, size: blob.size, name, lastModified };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The fields of a FormData in a key, in order, with a file as `blobKey`. */
|
|
69
|
+
function formDataKey(form: FormData): unknown[] {
|
|
70
|
+
// React Native's FormData has no `entries`, so its fields cannot be read.
|
|
71
|
+
if (typeof form.entries !== "function") {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
return Array.from(form.entries(), ([name, value]) => [
|
|
75
|
+
name,
|
|
76
|
+
typeof value === "string" ? value : blobKey(value),
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The bytes of an ArrayBuffer or a typed array, or null for another value. */
|
|
81
|
+
function bodyBytes(body: object): Uint8Array | null {
|
|
82
|
+
if (ArrayBuffer.isView(body)) {
|
|
83
|
+
return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
84
|
+
}
|
|
85
|
+
return isClass(body, "ArrayBuffer") ? new Uint8Array(body as ArrayBuffer) : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A request body as a value TanStack Query hashes by its content. A URLSearchParams is its text,
|
|
90
|
+
* a FormData its fields, a Blob `blobKey` and bytes their numbers, each under its kind, so it
|
|
91
|
+
* differs from a JSON body with the same content. Other values stay as they are.
|
|
92
|
+
*/
|
|
93
|
+
function bodyKey(body: unknown): unknown {
|
|
94
|
+
if (typeof body !== "object" || body === null) {
|
|
95
|
+
return body;
|
|
96
|
+
}
|
|
97
|
+
if (isClass(body, "URLSearchParams")) {
|
|
98
|
+
return { URLSearchParams: String(body) };
|
|
99
|
+
}
|
|
100
|
+
if (isClass(body, "FormData")) {
|
|
101
|
+
return { FormData: formDataKey(body as FormData) };
|
|
102
|
+
}
|
|
103
|
+
if (isClass(body, "Blob") || isClass(body, "File")) {
|
|
104
|
+
return { Blob: blobKey(body as Blob) };
|
|
105
|
+
}
|
|
106
|
+
const bytes = bodyBytes(body);
|
|
107
|
+
return bytes === null ? body : { bytes: Array.from(bytes) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The init of a query key: the request init with its headers and body as plain values, and the
|
|
112
|
+
* client's key scope, or its base URL when the init has none. The `signal` is left out: TanStack
|
|
113
|
+
* Query gives each request its own. Undefined when nothing is left, so a call without an init and
|
|
114
|
+
* one with an empty init have the key `[method, path]`.
|
|
115
|
+
*/
|
|
116
|
+
export function queryKeyInit(init: unknown, client: KeyClient): object | undefined {
|
|
117
|
+
const { signal: _signal, ...rest } = (init ?? {}) as {
|
|
118
|
+
signal?: unknown;
|
|
119
|
+
headers?: unknown;
|
|
120
|
+
baseUrl?: unknown;
|
|
121
|
+
body?: unknown;
|
|
122
|
+
};
|
|
123
|
+
const keyInit: Record<string, unknown> = { ...rest };
|
|
124
|
+
if (typeof rest.headers === "object" && rest.headers !== null) {
|
|
125
|
+
keyInit.headers = plainHeaders(rest.headers);
|
|
126
|
+
}
|
|
127
|
+
if ("body" in rest) {
|
|
128
|
+
keyInit.body = bodyKey(rest.body);
|
|
129
|
+
}
|
|
130
|
+
if (client.keyScope !== undefined) {
|
|
131
|
+
keyInit.keyScope = client.keyScope;
|
|
132
|
+
} else if (!rest.baseUrl && client.baseUrl) {
|
|
133
|
+
keyInit.baseUrl = client.baseUrl;
|
|
134
|
+
}
|
|
135
|
+
return Object.keys(keyInit).length === 0 ? undefined : keyInit;
|
|
136
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signal of a query's request. TanStack Query gives every request a signal, which it aborts
|
|
3
|
+
* when the query is cancelled or no longer used; the init of the call may bring its own. The hooks
|
|
4
|
+
* sent one of them: `useQuery` the call's, so a cancelled query kept its request, and
|
|
5
|
+
* `useInfiniteQuery` TanStack's, so the call's signal did nothing. The request now stops when
|
|
6
|
+
* either is aborted.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The init to send, and how to stop listening to the signals once the request has settled. */
|
|
10
|
+
export interface QueryRequest {
|
|
11
|
+
readonly init: Record<string, unknown>;
|
|
12
|
+
readonly release: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ABORT_EVENT = "abort";
|
|
16
|
+
|
|
17
|
+
function withoutRelease(init: Record<string, unknown>): QueryRequest {
|
|
18
|
+
return { init, release: () => undefined };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The init with a signal that aborts when the init's own signal or the query's does. */
|
|
22
|
+
export function withQuerySignal(init: unknown, querySignal: AbortSignal): QueryRequest {
|
|
23
|
+
const { signal: ownSignal, ...rest } = (init ?? {}) as { signal?: AbortSignal | null };
|
|
24
|
+
if (!ownSignal) {
|
|
25
|
+
return withoutRelease({ ...rest, signal: querySignal });
|
|
26
|
+
}
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const sources = [ownSignal, querySignal];
|
|
29
|
+
const aborted = sources.find((source) => source.aborted);
|
|
30
|
+
if (aborted !== undefined) {
|
|
31
|
+
controller.abort(aborted.reason);
|
|
32
|
+
return withoutRelease({ ...rest, signal: controller.signal });
|
|
33
|
+
}
|
|
34
|
+
const listeners = sources.map((source) => {
|
|
35
|
+
const listener = () => controller.abort(source.reason);
|
|
36
|
+
source.addEventListener(ABORT_EVENT, listener, { once: true });
|
|
37
|
+
return () => source.removeEventListener(ABORT_EVENT, listener);
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
init: { ...rest, signal: controller.signal },
|
|
41
|
+
release: () => {
|
|
42
|
+
for (const remove of listeners) {
|
|
43
|
+
remove();
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|