Skip to content

Commit

Permalink
feat: add progressive refetch interval and repeat invalidation (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
vladimirfilosof authored Feb 21, 2025
1 parent 1f13f2e commit c472fae
Show file tree
Hide file tree
Showing 17 changed files with 357 additions and 52 deletions.
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type {
} from './types/DataSource';
export type {DataManager} from './types/DataManger';
export type {DataLoaderStatus} from './types/DataLoaderStatus';
export type {InvalidateRepeatOptions, InvalidateOptions} from './types/DataManagerOptions';

export {idle} from './constants';

Expand Down
11 changes: 11 additions & 0 deletions src/core/types/DataManagerOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface InvalidateRepeatOptions {
interval: number;
/**
* Number of repeated calls, not counting the first one
*/
count: number;
}

export interface InvalidateOptions {
repeat?: InvalidateRepeatOptions;
}
12 changes: 9 additions & 3 deletions src/core/types/DataManger.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import type {InvalidateOptions} from './DataManagerOptions';
import type {AnyDataSource, DataSourceParams, DataSourceTag} from './DataSource';

export interface DataManager {
invalidateTag(tag: DataSourceTag): Promise<void>;
invalidateTags(tags: DataSourceTag[]): Promise<void>;
invalidateTag(tag: DataSourceTag, invalidateOptions?: InvalidateOptions): Promise<void>;
invalidateTags(tags: DataSourceTag[], invalidateOptions?: InvalidateOptions): Promise<void>;

invalidateSource<TDataSource extends AnyDataSource>(dataSource: TDataSource): Promise<void>;
invalidateSource<TDataSource extends AnyDataSource>(
dataSource: TDataSource,
invalidateOptions?: InvalidateOptions,
): Promise<void>;

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

invalidateParams<TDataSource extends AnyDataSource>(
dataSource: TDataSource,
params: DataSourceParams<TDataSource>,
invalidateOptions?: InvalidateOptions,
): Promise<void>;

resetParams<TDataSource extends AnyDataSource>(
Expand All @@ -21,5 +26,6 @@ export interface DataManager {
invalidateSourceTags<TDataSource extends AnyDataSource>(
dataSource: TDataSource,
params: DataSourceParams<TDataSource>,
invalidateOptions?: InvalidateOptions,
): Promise<void>;
}
89 changes: 67 additions & 22 deletions src/react-query/ClientDataManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {QueryClientConfig} from '@tanstack/react-query';
import type {InvalidateQueryFilters, QueryClientConfig} from '@tanstack/react-query';
import {QueryClient} from '@tanstack/react-query';

import {
Expand All @@ -9,6 +9,7 @@ import {
composeFullKey,
hasTag,
} from '../core';
import type {InvalidateOptions, InvalidateRepeatOptions} from '../core/types/DataManagerOptions';

export type ClientDataManagerConfig = QueryClientConfig;

Expand All @@ -32,23 +33,35 @@ export class ClientDataManager implements DataManager {
});
}

invalidateTag(tag: DataSourceTag) {
return this.queryClient.invalidateQueries({
predicate: ({queryKey}) => hasTag(queryKey, tag),
});
invalidateTag(tag: DataSourceTag, invalidateOptions?: InvalidateOptions) {
return this.invalidateQueries(
{
predicate: ({queryKey}) => hasTag(queryKey, tag),
},
invalidateOptions,
);
}

invalidateTags(tags: DataSourceTag[]) {
return this.queryClient.invalidateQueries({
predicate: ({queryKey}) => tags.every((tag) => hasTag(queryKey, tag)),
});
invalidateTags(tags: DataSourceTag[], invalidateOptions?: InvalidateOptions) {
return this.invalidateQueries(
{
predicate: ({queryKey}) => tags.every((tag) => hasTag(queryKey, tag)),
},
invalidateOptions,
);
}

invalidateSource<TDataSource extends AnyDataSource>(dataSource: TDataSource) {
return this.queryClient.invalidateQueries({
// First element is a data source name
queryKey: [dataSource.name],
});
invalidateSource<TDataSource extends AnyDataSource>(
dataSource: TDataSource,
invalidateOptions?: InvalidateOptions,
) {
return this.invalidateQueries(
{
// First element is a data source name
queryKey: [dataSource.name],
},
invalidateOptions,
);
}

resetSource<TDataSource extends AnyDataSource>(dataSource: TDataSource) {
Expand All @@ -61,11 +74,15 @@ export class ClientDataManager implements DataManager {
invalidateParams<TDataSource extends AnyDataSource>(
dataSource: TDataSource,
params: DataSourceParams<TDataSource>,
invalidateOptions?: InvalidateOptions,
) {
return this.queryClient.invalidateQueries({
queryKey: composeFullKey(dataSource, params),
exact: true,
});
return this.invalidateQueries(
{
queryKey: composeFullKey(dataSource, params),
exact: true,
},
invalidateOptions,
);
}

resetParams<TDataSource extends AnyDataSource>(
Expand All @@ -81,10 +98,38 @@ export class ClientDataManager implements DataManager {
invalidateSourceTags<TDataSource extends AnyDataSource>(
dataSource: TDataSource,
params: DataSourceParams<TDataSource>,
invalidateOptions?: InvalidateOptions,
) {
return this.queryClient.invalidateQueries({
// Last element is a full key
queryKey: composeFullKey(dataSource, params).slice(0, -1),
});
return this.invalidateQueries(
{
// Last element is a full key
queryKey: composeFullKey(dataSource, params).slice(0, -1),
},
invalidateOptions,
);
}

private invalidateQueries(
filters: InvalidateQueryFilters,
invalidateOptions?: InvalidateOptions,
) {
const {repeat} = invalidateOptions || {};

const invalidate = () => this.queryClient.invalidateQueries(filters);

this.repeatInvalidate(invalidate, repeat);

return invalidate();
}

private repeatInvalidate(invalidate: () => Promise<void>, repeat?: InvalidateRepeatOptions) {
if (!repeat) {
return;
}
const {interval, count} = repeat;

for (let i = 1; i <= count; i++) {
setTimeout(invalidate, interval * i);
}
}
}
8 changes: 7 additions & 1 deletion src/react-query/hooks/useQueryData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {DataSourceOptions, DataSourceParams, DataSourceState} from '../../c
import {useInfiniteQueryData} from '../impl/infinite/hooks';
import type {AnyInfiniteQueryDataSource} from '../impl/infinite/types';
import {usePlainQueryData} from '../impl/plain/hooks';
import type {AnyPlainQueryDataSource} from '../impl/plain/types';
import type {AnyQueryDataSource} from '../types';
import {notReachable} from '../utils/notReachable';

Expand All @@ -20,7 +21,12 @@ export const useQueryData = <TDataSource extends AnyQueryDataSource>(
// Do not change data source type in the same hook call
if (type === 'plain') {
// eslint-disable-next-line react-hooks/rules-of-hooks
state = usePlainQueryData(context, dataSource, params, options);
state = usePlainQueryData(
context,
dataSource,
params,
options as Partial<DataSourceOptions<AnyPlainQueryDataSource>> | undefined,
);
} else if (type === 'infinite') {
// eslint-disable-next-line react-hooks/rules-of-hooks
state = useInfiniteQueryData(
Expand Down
63 changes: 63 additions & 0 deletions src/react-query/hooks/useRefetchInterval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';

import type {Query, QueryFunction, QueryFunctionContext, SkipToken} from '@tanstack/react-query';

import type {DataSourceError, DataSourceKey, DataSourceResponse} from '../../core';
import type {AnyQueryDataSource, RefetchInterval} from '../types';

export const useRefetchInterval = <TDataSource extends AnyQueryDataSource, TQueryData, TPageParams>(
refetchIntervalOption?: RefetchInterval<
DataSourceResponse<TDataSource>,
DataSourceError<TDataSource>,
TQueryData,
DataSourceKey
>,
queryFnOption?:
| QueryFunction<DataSourceResponse<TDataSource>, DataSourceKey, TPageParams>
| SkipToken,
): {
refetchInterval?:
| number
| false
| ((
query: Query<
DataSourceResponse<TDataSource>,
DataSourceError<TDataSource>,
TQueryData,
DataSourceKey
>,
) => number | false | undefined);
queryFn?:
| QueryFunction<DataSourceResponse<TDataSource>, DataSourceKey, TPageParams>
| SkipToken;
} => {
const count = React.useRef<number>(0);

const queryFn = React.useMemo(() => {
if (typeof queryFnOption === 'function') {
return (context: QueryFunctionContext<DataSourceKey, TPageParams>) => {
count.current++;
return queryFnOption(context);
};
}
return undefined;
}, [queryFnOption]);

const refetchInterval = React.useMemo(() => {
if (typeof refetchIntervalOption === 'function') {
return (
query: Query<
DataSourceResponse<TDataSource>,
DataSourceError<TDataSource>,
TQueryData,
DataSourceKey
>,
) => {
return refetchIntervalOption(query, count.current);
};
}
return refetchIntervalOption;
}, [refetchIntervalOption]);

return {refetchInterval, queryFn};
};
45 changes: 43 additions & 2 deletions src/react-query/impl/infinite/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import {useMemo} from 'react';

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

import type {
DataSourceContext,
DataSourceData,
DataSourceError,
DataSourceKey,
DataSourceOptions,
DataSourceParams,
DataSourceResponse,
DataSourceState,
} from '../../../core';
import {useRefetchInterval} from '../../hooks/useRefetchInterval';
import {normalizeStatus} from '../../utils/normalizeStatus';

import type {AnyInfiniteQueryDataSource} from './types';
import type {
AnyInfiniteQueryDataSource,
AnyPageParam,
InfiniteQueryObserverExtendedOptions,
} from './types';
import {composeOptions} from './utils';

export const useInfiniteQueryData = <TDataSource extends AnyInfiniteQueryDataSource>(
Expand All @@ -20,7 +30,10 @@ export const useInfiniteQueryData = <TDataSource extends AnyInfiniteQueryDataSou
options?: Partial<DataSourceOptions<TDataSource>>,
): DataSourceState<TDataSource> => {
const composedOptions = composeOptions(context, dataSource, params, options);
const result = useInfiniteQuery(composedOptions);

const extendedOptions = useInfiniteQueryDataOptions(composedOptions);

const result = useInfiniteQuery(extendedOptions);

const transformedData = useMemo<DataSourceState<TDataSource>['data']>(
() => result.data?.pages.flat(1) ?? [],
Expand All @@ -35,3 +48,31 @@ export const useInfiniteQueryData = <TDataSource extends AnyInfiniteQueryDataSou
originalData: result.data,
} as DataSourceState<TDataSource>;
};

export function useInfiniteQueryDataOptions<TDataSource extends AnyInfiniteQueryDataSource>(
composedOptions: InfiniteQueryObserverExtendedOptions<
DataSourceResponse<TDataSource>,
DataSourceError<TDataSource>,
InfiniteData<DataSourceData<TDataSource>, AnyPageParam>,
DataSourceResponse<TDataSource>,
DataSourceKey,
AnyPageParam
>,
): InfiniteQueryObserverOptions<
DataSourceResponse<TDataSource>,
DataSourceError<TDataSource>,
InfiniteData<DataSourceData<TDataSource>, AnyPageParam>,
DataSourceResponse<TDataSource>,
DataSourceKey,
AnyPageParam
> {
const {
refetchInterval: refetchIntervalOption,
queryFn: queryFnOption,
...restOptions
} = composedOptions || {};

const {refetchInterval, queryFn} = useRefetchInterval(refetchIntervalOption, queryFnOption);

return {...restOptions, refetchInterval, queryFn};
}
24 changes: 22 additions & 2 deletions src/react-query/impl/infinite/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
import type {
DefaultError,
InfiniteData,
InfiniteQueryObserverOptions,
InfiniteQueryObserverResult,
InfiniteQueryPageParamsOptions,
QueryFunctionContext,
QueryKey,
} from '@tanstack/react-query';
import type {Overwrite} from 'utility-types';

import type {ActualData, DataLoaderStatus, DataSource, DataSourceKey} from '../../../core';
import type {QueryDataSourceContext} from '../../types';
import type {QueryObserverExtendedOptions} from '../plain/types';

export interface InfiniteQueryObserverExtendedOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> extends QueryObserverExtendedOptions<
TQueryFnData,
TError,
TData,
InfiniteData<TQueryData, TPageParam>,
TQueryKey,
TPageParam
>,
InfiniteQueryPageParamsOptions<TQueryFnData, TPageParam> {}

export type InfiniteQueryDataSource<TParams, TRequest, TResponse, TData, TError> = DataSource<
QueryDataSourceContext,
Expand All @@ -16,7 +36,7 @@ export type InfiniteQueryDataSource<TParams, TRequest, TResponse, TData, TError>
TResponse,
TData,
TError,
InfiniteQueryObserverOptions<
InfiniteQueryObserverExtendedOptions<
TResponse,
TError,
InfiniteData<ActualData<TData, TResponse>, Partial<TRequest>>,
Expand Down
Loading

0 comments on commit c472fae

Please sign in to comment.