Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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