Skip to content

Commit d61b127

Browse files
authored
feat: implement tanstack query cancellation (#231)
* feat: implement tanstack query cancellation * refactor: simplify throwOnUnknownStatus code * changeset
1 parent 8b00221 commit d61b127

5 files changed

Lines changed: 76 additions & 39 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@ts-rest/core': minor
3+
'@ts-rest/react-query': minor
4+
'@ts-rest/solid-query': minor
5+
---
6+
7+
Implement tanstack query cancellation

apps/example-next/tests/react-query.spec.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ describe('react-query', () => {
187187
'content-type': 'application/json',
188188
'x-test': 'test',
189189
},
190+
route: router.health,
191+
signal: expect.any(AbortSignal),
190192
});
191193

192194
await waitFor(() => {
@@ -226,6 +228,8 @@ describe('react-query', () => {
226228
'content-type': 'application/json',
227229
'x-test': 'test',
228230
},
231+
route: router.posts.getPost,
232+
signal: expect.any(AbortSignal),
229233
});
230234
});
231235

@@ -262,6 +266,8 @@ describe('react-query', () => {
262266
'content-type': 'application/xml',
263267
'x-test': 'foo',
264268
},
269+
route: router.posts.getPost,
270+
signal: expect.any(AbortSignal),
265271
});
266272
});
267273

@@ -295,6 +301,8 @@ describe('react-query', () => {
295301
'content-type': 'application/json',
296302
'x-test': 'test',
297303
},
304+
route: router.posts.getPost,
305+
signal: expect.any(AbortSignal),
298306
});
299307
});
300308

@@ -320,6 +328,8 @@ describe('react-query', () => {
320328
'content-type': 'application/json',
321329
'x-test': 'test',
322330
},
331+
route: router.health,
332+
signal: expect.any(AbortSignal),
323333
});
324334

325335
return waitFor(() => {
@@ -351,6 +361,8 @@ describe('react-query', () => {
351361
'content-type': 'application/json',
352362
'x-test': 'test',
353363
},
364+
route: router.health,
365+
signal: expect.any(AbortSignal),
354366
});
355367

356368
await waitFor(() => {
@@ -406,6 +418,8 @@ describe('react-query', () => {
406418
title: 'test',
407419
},
408420
contentType: 'application/json',
421+
route: router.posts.createPost,
422+
signal: undefined,
409423
});
410424

411425
await waitFor(() => {
@@ -457,6 +471,8 @@ describe('react-query', () => {
457471
'content-type': 'application/json',
458472
'x-test': 'test',
459473
},
474+
route: router.posts.getPost,
475+
signal: expect.any(AbortSignal),
460476
});
461477

462478
expect(api).toHaveBeenCalledWith({
@@ -467,6 +483,8 @@ describe('react-query', () => {
467483
'content-type': 'application/json',
468484
'x-test': 'test',
469485
},
486+
route: router.posts.getPost,
487+
signal: expect.any(AbortSignal),
470488
});
471489

472490
await waitFor(() => {
@@ -526,6 +544,8 @@ describe('react-query', () => {
526544
'content-type': 'application/json',
527545
'x-test': 'test',
528546
},
547+
route: router.posts.getPost,
548+
signal: expect.any(AbortSignal),
529549
});
530550

531551
expect(api).toHaveBeenCalledWith({
@@ -536,6 +556,8 @@ describe('react-query', () => {
536556
'content-type': 'application/json',
537557
'x-test': 'test',
538558
},
559+
route: router.posts.getPost,
560+
signal: expect.any(AbortSignal),
539561
});
540562

541563
await waitFor(() => {
@@ -599,6 +621,8 @@ describe('react-query', () => {
599621
'content-type': 'application/json',
600622
'x-test': 'test',
601623
},
624+
route: router.posts.getPost,
625+
signal: expect.any(AbortSignal),
602626
});
603627

604628
expect(api).toHaveBeenCalledWith({
@@ -609,6 +633,8 @@ describe('react-query', () => {
609633
'content-type': 'application/json',
610634
'x-test': 'test',
611635
},
636+
route: router.posts.getPost,
637+
signal: expect.any(AbortSignal),
612638
});
613639

614640
await waitFor(() => {
@@ -650,6 +676,8 @@ describe('react-query', () => {
650676
'content-type': 'application/json',
651677
'x-test': 'test',
652678
},
679+
route: router.posts.getPost,
680+
signal: expect.any(AbortSignal),
653681
});
654682
});
655683

@@ -678,6 +706,8 @@ describe('react-query', () => {
678706
'content-type': 'application/json',
679707
'x-test': 'test',
680708
},
709+
route: router.posts.getPost,
710+
signal: expect.any(AbortSignal),
681711
});
682712
});
683713

@@ -712,6 +742,8 @@ describe('react-query', () => {
712742
'content-type': 'application/json',
713743
'x-test': 'test',
714744
},
745+
route: router.posts.getPost,
746+
signal: expect.any(AbortSignal),
715747
});
716748
});
717749

@@ -740,6 +772,8 @@ describe('react-query', () => {
740772
'content-type': 'application/json',
741773
'x-test': 'test',
742774
},
775+
route: router.posts.getPost,
776+
signal: expect.any(AbortSignal),
743777
});
744778
});
745779

@@ -785,6 +819,8 @@ describe('react-query', () => {
785819
'content-type': 'application/json',
786820
'x-test': 'test',
787821
},
822+
route: router.posts.getPost,
823+
signal: expect.any(AbortSignal),
788824
});
789825
});
790826

libs/ts-rest/core/src/lib/client.ts

Lines changed: 26 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,6 @@ type RecursiveProxyObj<T extends AppRouter, TClientArgs extends ClientArgs> = {
2323
: never;
2424
};
2525

26-
type RecursiveProxyObjNoUnknownStatus<
27-
T extends AppRouter,
28-
TClientArgs extends ClientArgs & { throwOnUnknownStatus: true }
29-
> = {
30-
[TKey in keyof T]: T[TKey] extends AppRoute
31-
? AppRouteFunctionNoUnknownStatus<T[TKey], TClientArgs>
32-
: T[TKey] extends AppRouter
33-
? RecursiveProxyObjNoUnknownStatus<T[TKey], TClientArgs>
34-
: never;
35-
};
36-
3726
type AppRouteMutationType<T> = ZodInputOrType<T>;
3827

3928
/**
@@ -141,35 +130,26 @@ export function getRouteResponses<T extends AppRouter>(router: T) {
141130
};
142131
}
143132

144-
/**
145-
* Returned from a mutation or query call
146-
*/
147-
export type AppRouteFunction<
133+
type AppRouteFunctionReturn<
148134
TRoute extends AppRoute,
149135
TClientArgs extends ClientArgs
150-
> = AreAllPropertiesOptional<DataReturnArgs<TRoute, TClientArgs>> extends true
151-
? (
152-
args?: Prettify<DataReturnArgs<TRoute, TClientArgs>>
153-
) => Promise<Prettify<ApiRouteResponse<TRoute['responses']>>>
154-
: (
155-
args: Prettify<DataReturnArgs<TRoute, TClientArgs>>
156-
) => Promise<Prettify<ApiRouteResponse<TRoute['responses']>>>;
136+
> = TClientArgs extends { throwOnUnknownStatus: true }
137+
? ApiRouteResponseNoUnknownStatus<TRoute['responses']>
138+
: ApiRouteResponse<TRoute['responses']>;
157139

158140
/**
159-
* Returned from a mutation or query call when NoUnknownStatus mode is enabled
141+
* Returned from a mutation or query call
160142
*/
161-
export type AppRouteFunctionNoUnknownStatus<
143+
export type AppRouteFunction<
162144
TRoute extends AppRoute,
163145
TClientArgs extends ClientArgs
164146
> = AreAllPropertiesOptional<DataReturnArgs<TRoute, TClientArgs>> extends true
165147
? (
166148
args?: Prettify<DataReturnArgs<TRoute, TClientArgs>>
167-
) => Promise<Prettify<ApiRouteResponseNoUnknownStatus<TRoute['responses']>>>
149+
) => Promise<Prettify<AppRouteFunctionReturn<TRoute, TClientArgs>>>
168150
: (
169151
args: Prettify<DataReturnArgs<TRoute, TClientArgs>>
170-
) => Promise<
171-
Prettify<ApiRouteResponseNoUnknownStatus<TRoute['responses']>>
172-
>;
152+
) => Promise<Prettify<AppRouteFunctionReturn<TRoute, TClientArgs>>>;
173153

174154
export interface ClientArgs {
175155
baseUrl: string;
@@ -180,6 +160,7 @@ export interface ClientArgs {
180160
}
181161

182162
export type ApiFetcherArgs = {
163+
route: AppRoute;
183164
path: string;
184165
method: string;
185166
headers: Record<string, string>;
@@ -188,6 +169,7 @@ export type ApiFetcherArgs = {
188169
rawQuery: unknown;
189170
contentType: AppRouteMutation['contentType'];
190171
credentials?: RequestCredentials;
172+
signal?: AbortSignal;
191173
};
192174

193175
export type ApiFetcher = (
@@ -207,8 +189,15 @@ export const tsRestFetchApi: ApiFetcher = async ({
207189
headers,
208190
body,
209191
credentials,
192+
signal,
210193
}) => {
211-
const result = await fetch(path, { method, headers, body, credentials });
194+
const result = await fetch(path, {
195+
method,
196+
headers,
197+
body,
198+
credentials,
199+
signal,
200+
});
212201
const contentType = result.headers.get('content-type');
213202

214203
if (contentType?.includes('application/json')) {
@@ -250,6 +239,7 @@ export const fetchApi = ({
250239
query,
251240
extraInputArgs,
252241
headers,
242+
signal,
253243
}: {
254244
path: string;
255245
clientArgs: ClientArgs;
@@ -258,6 +248,7 @@ export const fetchApi = ({
258248
body: unknown;
259249
extraInputArgs: Record<string, unknown>;
260250
headers: Record<string, string | undefined>;
251+
signal?: AbortSignal;
261252
}) => {
262253
const apiFetcher = clientArgs.api || tsRestFetchApi;
263254

@@ -275,6 +266,7 @@ export const fetchApi = ({
275266

276267
if (route.method !== 'GET' && route.contentType === 'multipart/form-data') {
277268
return apiFetcher({
269+
route,
278270
path,
279271
method: route.method,
280272
credentials: clientArgs.credentials,
@@ -283,11 +275,13 @@ export const fetchApi = ({
283275
rawBody: body,
284276
rawQuery: query,
285277
contentType: 'multipart/form-data',
278+
signal,
286279
...extraInputArgs,
287280
});
288281
}
289282

290283
return apiFetcher({
284+
route,
291285
path,
292286
method: route.method,
293287
credentials: clientArgs.credentials,
@@ -300,6 +294,7 @@ export const fetchApi = ({
300294
rawBody: body,
301295
rawQuery: query,
302296
contentType: route.method !== 'GET' ? 'application/json' : undefined,
297+
signal,
303298
...extraInputArgs,
304299
});
305300
};
@@ -359,6 +354,7 @@ export const getRouteQuery = <TAppRoute extends AppRoute>(
359354
if (knownResponseStatuses.includes(response.status.toString())) {
360355
return response;
361356
}
357+
362358
throw new UnknownStatusError(response, knownResponseStatuses);
363359
};
364360
};
@@ -368,11 +364,6 @@ export type InitClientReturn<
368364
TClientArgs extends ClientArgs
369365
> = RecursiveProxyObj<T, TClientArgs>;
370366

371-
export type InitClientReturnNoUnknownStatus<
372-
T extends AppRouter,
373-
TClientArgs extends ClientArgs & { throwOnUnknownStatus: true }
374-
> = RecursiveProxyObjNoUnknownStatus<T, TClientArgs>;
375-
376367
export type InitClientArgs = ClientArgs & {
377368
/**
378369
* Ensures that the responses from the server match those defined in the
@@ -387,9 +378,7 @@ export const initClient = <
387378
>(
388379
router: T,
389380
args: TClientArgs
390-
): TClientArgs extends { throwOnUnknownStatus: true }
391-
? InitClientReturnNoUnknownStatus<T, TClientArgs>
392-
: InitClientReturn<T, TClientArgs> => {
381+
): InitClientReturn<T, TClientArgs> => {
393382
return Object.fromEntries(
394383
Object.entries(router).map(([key, subRouter]) => {
395384
if (isAppRoute(subRouter)) {

libs/ts-rest/react-query/src/lib/react-query.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const queryFn = <TAppRoute extends AppRoute, TClientArgs extends ClientArgs>(
3838
clientArgs: TClientArgs,
3939
args?: DataReturnArgsBase<TAppRoute, TClientArgs>
4040
): QueryFunction<TAppRoute['responses']> => {
41-
return async () => {
41+
return async (queryFnContext?: QueryFunctionContext) => {
4242
const { query, params, body, headers, extraHeaders, ...extraInputArgs } =
4343
args || {};
4444

@@ -51,6 +51,7 @@ const queryFn = <TAppRoute extends AppRoute, TClientArgs extends ClientArgs>(
5151
);
5252

5353
const result = await fetchApi({
54+
signal: queryFnContext?.signal,
5455
path,
5556
clientArgs,
5657
route,

libs/ts-rest/solid-query/src/lib/solid-query.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,9 @@ const getRouteUseQuery = <
187187
args: DataReturnArgs<TAppRoute, TClientArgs>,
188188
options?: CreateQueryOptions<TAppRoute['responses']>
189189
) => {
190-
const dataFn: QueryFunction<TAppRoute['responses']> = async () => {
190+
const dataFn: QueryFunction<TAppRoute['responses']> = async ({
191+
signal,
192+
}) => {
191193
const { query, params, body, headers, extraHeaders, ...extraInputArgs } =
192194
args || {};
193195

@@ -209,6 +211,7 @@ const getRouteUseQuery = <
209211
...extraHeaders,
210212
...headers,
211213
},
214+
signal,
212215
extraInputArgs,
213216
});
214217

@@ -255,6 +258,7 @@ const getRouteUseInfiniteQuery = <
255258
);
256259

257260
const result = await fetchApi({
261+
signal: infiniteQueryParams.signal,
258262
path,
259263
clientArgs,
260264
route,

0 commit comments

Comments
 (0)