Skip to content

Commit c472fae

Browse files
feat: add progressive refetch interval and repeat invalidation (#22)
1 parent 1f13f2e commit c472fae

File tree

17 files changed

+357
-52
lines changed

17 files changed

+357
-52
lines changed

src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type {
1717
} from './types/DataSource';
1818
export type {DataManager} from './types/DataManger';
1919
export type {DataLoaderStatus} from './types/DataLoaderStatus';
20+
export type {InvalidateRepeatOptions, InvalidateOptions} from './types/DataManagerOptions';
2021

2122
export {idle} from './constants';
2223

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface InvalidateRepeatOptions {
2+
interval: number;
3+
/**
4+
* Number of repeated calls, not counting the first one
5+
*/
6+
count: number;
7+
}
8+
9+
export interface InvalidateOptions {
10+
repeat?: InvalidateRepeatOptions;
11+
}

src/core/types/DataManger.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1+
import type {InvalidateOptions} from './DataManagerOptions';
12
import type {AnyDataSource, DataSourceParams, DataSourceTag} from './DataSource';
23

34
export interface DataManager {
4-
invalidateTag(tag: DataSourceTag): Promise<void>;
5-
invalidateTags(tags: DataSourceTag[]): Promise<void>;
5+
invalidateTag(tag: DataSourceTag, invalidateOptions?: InvalidateOptions): Promise<void>;
6+
invalidateTags(tags: DataSourceTag[], invalidateOptions?: InvalidateOptions): Promise<void>;
67

7-
invalidateSource<TDataSource extends AnyDataSource>(dataSource: TDataSource): Promise<void>;
8+
invalidateSource<TDataSource extends AnyDataSource>(
9+
dataSource: TDataSource,
10+
invalidateOptions?: InvalidateOptions,
11+
): Promise<void>;
812

913
resetSource<TDataSource extends AnyDataSource>(dataSource: TDataSource): Promise<void>;
1014

1115
invalidateParams<TDataSource extends AnyDataSource>(
1216
dataSource: TDataSource,
1317
params: DataSourceParams<TDataSource>,
18+
invalidateOptions?: InvalidateOptions,
1419
): Promise<void>;
1520

1621
resetParams<TDataSource extends AnyDataSource>(
@@ -21,5 +26,6 @@ export interface DataManager {
2126
invalidateSourceTags<TDataSource extends AnyDataSource>(
2227
dataSource: TDataSource,
2328
params: DataSourceParams<TDataSource>,
29+
invalidateOptions?: InvalidateOptions,
2430
): Promise<void>;
2531
}

src/react-query/ClientDataManager.ts

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {QueryClientConfig} from '@tanstack/react-query';
1+
import type {InvalidateQueryFilters, QueryClientConfig} from '@tanstack/react-query';
22
import {QueryClient} from '@tanstack/react-query';
33

44
import {
@@ -9,6 +9,7 @@ import {
99
composeFullKey,
1010
hasTag,
1111
} from '../core';
12+
import type {InvalidateOptions, InvalidateRepeatOptions} from '../core/types/DataManagerOptions';
1213

1314
export type ClientDataManagerConfig = QueryClientConfig;
1415

@@ -32,23 +33,35 @@ export class ClientDataManager implements DataManager {
3233
});
3334
}
3435

35-
invalidateTag(tag: DataSourceTag) {
36-
return this.queryClient.invalidateQueries({
37-
predicate: ({queryKey}) => hasTag(queryKey, tag),
38-
});
36+
invalidateTag(tag: DataSourceTag, invalidateOptions?: InvalidateOptions) {
37+
return this.invalidateQueries(
38+
{
39+
predicate: ({queryKey}) => hasTag(queryKey, tag),
40+
},
41+
invalidateOptions,
42+
);
3943
}
4044

41-
invalidateTags(tags: DataSourceTag[]) {
42-
return this.queryClient.invalidateQueries({
43-
predicate: ({queryKey}) => tags.every((tag) => hasTag(queryKey, tag)),
44-
});
45+
invalidateTags(tags: DataSourceTag[], invalidateOptions?: InvalidateOptions) {
46+
return this.invalidateQueries(
47+
{
48+
predicate: ({queryKey}) => tags.every((tag) => hasTag(queryKey, tag)),
49+
},
50+
invalidateOptions,
51+
);
4552
}
4653

47-
invalidateSource<TDataSource extends AnyDataSource>(dataSource: TDataSource) {
48-
return this.queryClient.invalidateQueries({
49-
// First element is a data source name
50-
queryKey: [dataSource.name],
51-
});
54+
invalidateSource<TDataSource extends AnyDataSource>(
55+
dataSource: TDataSource,
56+
invalidateOptions?: InvalidateOptions,
57+
) {
58+
return this.invalidateQueries(
59+
{
60+
// First element is a data source name
61+
queryKey: [dataSource.name],
62+
},
63+
invalidateOptions,
64+
);
5265
}
5366

5467
resetSource<TDataSource extends AnyDataSource>(dataSource: TDataSource) {
@@ -61,11 +74,15 @@ export class ClientDataManager implements DataManager {
6174
invalidateParams<TDataSource extends AnyDataSource>(
6275
dataSource: TDataSource,
6376
params: DataSourceParams<TDataSource>,
77+
invalidateOptions?: InvalidateOptions,
6478
) {
65-
return this.queryClient.invalidateQueries({
66-
queryKey: composeFullKey(dataSource, params),
67-
exact: true,
68-
});
79+
return this.invalidateQueries(
80+
{
81+
queryKey: composeFullKey(dataSource, params),
82+
exact: true,
83+
},
84+
invalidateOptions,
85+
);
6986
}
7087

7188
resetParams<TDataSource extends AnyDataSource>(
@@ -81,10 +98,38 @@ export class ClientDataManager implements DataManager {
8198
invalidateSourceTags<TDataSource extends AnyDataSource>(
8299
dataSource: TDataSource,
83100
params: DataSourceParams<TDataSource>,
101+
invalidateOptions?: InvalidateOptions,
84102
) {
85-
return this.queryClient.invalidateQueries({
86-
// Last element is a full key
87-
queryKey: composeFullKey(dataSource, params).slice(0, -1),
88-
});
103+
return this.invalidateQueries(
104+
{
105+
// Last element is a full key
106+
queryKey: composeFullKey(dataSource, params).slice(0, -1),
107+
},
108+
invalidateOptions,
109+
);
110+
}
111+
112+
private invalidateQueries(
113+
filters: InvalidateQueryFilters,
114+
invalidateOptions?: InvalidateOptions,
115+
) {
116+
const {repeat} = invalidateOptions || {};
117+
118+
const invalidate = () => this.queryClient.invalidateQueries(filters);
119+
120+
this.repeatInvalidate(invalidate, repeat);
121+
122+
return invalidate();
123+
}
124+
125+
private repeatInvalidate(invalidate: () => Promise<void>, repeat?: InvalidateRepeatOptions) {
126+
if (!repeat) {
127+
return;
128+
}
129+
const {interval, count} = repeat;
130+
131+
for (let i = 1; i <= count; i++) {
132+
setTimeout(invalidate, interval * i);
133+
}
89134
}
90135
}

src/react-query/hooks/useQueryData.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {DataSourceOptions, DataSourceParams, DataSourceState} from '../../c
22
import {useInfiniteQueryData} from '../impl/infinite/hooks';
33
import type {AnyInfiniteQueryDataSource} from '../impl/infinite/types';
44
import {usePlainQueryData} from '../impl/plain/hooks';
5+
import type {AnyPlainQueryDataSource} from '../impl/plain/types';
56
import type {AnyQueryDataSource} from '../types';
67
import {notReachable} from '../utils/notReachable';
78

@@ -20,7 +21,12 @@ export const useQueryData = <TDataSource extends AnyQueryDataSource>(
2021
// Do not change data source type in the same hook call
2122
if (type === 'plain') {
2223
// eslint-disable-next-line react-hooks/rules-of-hooks
23-
state = usePlainQueryData(context, dataSource, params, options);
24+
state = usePlainQueryData(
25+
context,
26+
dataSource,
27+
params,
28+
options as Partial<DataSourceOptions<AnyPlainQueryDataSource>> | undefined,
29+
);
2430
} else if (type === 'infinite') {
2531
// eslint-disable-next-line react-hooks/rules-of-hooks
2632
state = useInfiniteQueryData(
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react';
2+
3+
import type {Query, QueryFunction, QueryFunctionContext, SkipToken} from '@tanstack/react-query';
4+
5+
import type {DataSourceError, DataSourceKey, DataSourceResponse} from '../../core';
6+
import type {AnyQueryDataSource, RefetchInterval} from '../types';
7+
8+
export const useRefetchInterval = <TDataSource extends AnyQueryDataSource, TQueryData, TPageParams>(
9+
refetchIntervalOption?: RefetchInterval<
10+
DataSourceResponse<TDataSource>,
11+
DataSourceError<TDataSource>,
12+
TQueryData,
13+
DataSourceKey
14+
>,
15+
queryFnOption?:
16+
| QueryFunction<DataSourceResponse<TDataSource>, DataSourceKey, TPageParams>
17+
| SkipToken,
18+
): {
19+
refetchInterval?:
20+
| number
21+
| false
22+
| ((
23+
query: Query<
24+
DataSourceResponse<TDataSource>,
25+
DataSourceError<TDataSource>,
26+
TQueryData,
27+
DataSourceKey
28+
>,
29+
) => number | false | undefined);
30+
queryFn?:
31+
| QueryFunction<DataSourceResponse<TDataSource>, DataSourceKey, TPageParams>
32+
| SkipToken;
33+
} => {
34+
const count = React.useRef<number>(0);
35+
36+
const queryFn = React.useMemo(() => {
37+
if (typeof queryFnOption === 'function') {
38+
return (context: QueryFunctionContext<DataSourceKey, TPageParams>) => {
39+
count.current++;
40+
return queryFnOption(context);
41+
};
42+
}
43+
return undefined;
44+
}, [queryFnOption]);
45+
46+
const refetchInterval = React.useMemo(() => {
47+
if (typeof refetchIntervalOption === 'function') {
48+
return (
49+
query: Query<
50+
DataSourceResponse<TDataSource>,
51+
DataSourceError<TDataSource>,
52+
TQueryData,
53+
DataSourceKey
54+
>,
55+
) => {
56+
return refetchIntervalOption(query, count.current);
57+
};
58+
}
59+
return refetchIntervalOption;
60+
}, [refetchIntervalOption]);
61+
62+
return {refetchInterval, queryFn};
63+
};

src/react-query/impl/infinite/hooks.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
import {useMemo} from 'react';
22

33
import {useInfiniteQuery} from '@tanstack/react-query';
4+
import type {InfiniteData, InfiniteQueryObserverOptions} from '@tanstack/react-query';
45

56
import type {
67
DataSourceContext,
8+
DataSourceData,
9+
DataSourceError,
10+
DataSourceKey,
711
DataSourceOptions,
812
DataSourceParams,
13+
DataSourceResponse,
914
DataSourceState,
1015
} from '../../../core';
16+
import {useRefetchInterval} from '../../hooks/useRefetchInterval';
1117
import {normalizeStatus} from '../../utils/normalizeStatus';
1218

13-
import type {AnyInfiniteQueryDataSource} from './types';
19+
import type {
20+
AnyInfiniteQueryDataSource,
21+
AnyPageParam,
22+
InfiniteQueryObserverExtendedOptions,
23+
} from './types';
1424
import {composeOptions} from './utils';
1525

1626
export const useInfiniteQueryData = <TDataSource extends AnyInfiniteQueryDataSource>(
@@ -20,7 +30,10 @@ export const useInfiniteQueryData = <TDataSource extends AnyInfiniteQueryDataSou
2030
options?: Partial<DataSourceOptions<TDataSource>>,
2131
): DataSourceState<TDataSource> => {
2232
const composedOptions = composeOptions(context, dataSource, params, options);
23-
const result = useInfiniteQuery(composedOptions);
33+
34+
const extendedOptions = useInfiniteQueryDataOptions(composedOptions);
35+
36+
const result = useInfiniteQuery(extendedOptions);
2437

2538
const transformedData = useMemo<DataSourceState<TDataSource>['data']>(
2639
() => result.data?.pages.flat(1) ?? [],
@@ -35,3 +48,31 @@ export const useInfiniteQueryData = <TDataSource extends AnyInfiniteQueryDataSou
3548
originalData: result.data,
3649
} as DataSourceState<TDataSource>;
3750
};
51+
52+
export function useInfiniteQueryDataOptions<TDataSource extends AnyInfiniteQueryDataSource>(
53+
composedOptions: InfiniteQueryObserverExtendedOptions<
54+
DataSourceResponse<TDataSource>,
55+
DataSourceError<TDataSource>,
56+
InfiniteData<DataSourceData<TDataSource>, AnyPageParam>,
57+
DataSourceResponse<TDataSource>,
58+
DataSourceKey,
59+
AnyPageParam
60+
>,
61+
): InfiniteQueryObserverOptions<
62+
DataSourceResponse<TDataSource>,
63+
DataSourceError<TDataSource>,
64+
InfiniteData<DataSourceData<TDataSource>, AnyPageParam>,
65+
DataSourceResponse<TDataSource>,
66+
DataSourceKey,
67+
AnyPageParam
68+
> {
69+
const {
70+
refetchInterval: refetchIntervalOption,
71+
queryFn: queryFnOption,
72+
...restOptions
73+
} = composedOptions || {};
74+
75+
const {refetchInterval, queryFn} = useRefetchInterval(refetchIntervalOption, queryFnOption);
76+
77+
return {...restOptions, refetchInterval, queryFn};
78+
}

src/react-query/impl/infinite/types.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,33 @@
11
import type {
2+
DefaultError,
23
InfiniteData,
3-
InfiniteQueryObserverOptions,
44
InfiniteQueryObserverResult,
5+
InfiniteQueryPageParamsOptions,
56
QueryFunctionContext,
7+
QueryKey,
68
} from '@tanstack/react-query';
79
import type {Overwrite} from 'utility-types';
810

911
import type {ActualData, DataLoaderStatus, DataSource, DataSourceKey} from '../../../core';
1012
import type {QueryDataSourceContext} from '../../types';
13+
import type {QueryObserverExtendedOptions} from '../plain/types';
14+
15+
export interface InfiniteQueryObserverExtendedOptions<
16+
TQueryFnData = unknown,
17+
TError = DefaultError,
18+
TData = TQueryFnData,
19+
TQueryData = TQueryFnData,
20+
TQueryKey extends QueryKey = QueryKey,
21+
TPageParam = unknown,
22+
> extends QueryObserverExtendedOptions<
23+
TQueryFnData,
24+
TError,
25+
TData,
26+
InfiniteData<TQueryData, TPageParam>,
27+
TQueryKey,
28+
TPageParam
29+
>,
30+
InfiniteQueryPageParamsOptions<TQueryFnData, TPageParam> {}
1131

1232
export type InfiniteQueryDataSource<TParams, TRequest, TResponse, TData, TError> = DataSource<
1333
QueryDataSourceContext,
@@ -16,7 +36,7 @@ export type InfiniteQueryDataSource<TParams, TRequest, TResponse, TData, TError>
1636
TResponse,
1737
TData,
1838
TError,
19-
InfiniteQueryObserverOptions<
39+
InfiniteQueryObserverExtendedOptions<
2040
TResponse,
2141
TError,
2242
InfiniteData<ActualData<TData, TResponse>, Partial<TRequest>>,

0 commit comments

Comments
 (0)