diff --git a/packages/toolkit/src/query/core/buildInitiate.ts b/packages/toolkit/src/query/core/buildInitiate.ts index e954dc5460..97880e488e 100644 --- a/packages/toolkit/src/query/core/buildInitiate.ts +++ b/packages/toolkit/src/query/core/buildInitiate.ts @@ -75,6 +75,18 @@ export interface StartInfiniteQueryActionCreatorOptions { previous?: boolean } +export interface StartInfiniteQueryActionCreatorOptions { + subscribe?: boolean + forceRefetch?: boolean | number + subscriptionOptions?: SubscriptionOptions + infiniteQueryOptions?: InfiniteQueryConfigOptions + direction?: "forward" | "backwards" + [forceQueryFnSymbol]?: () => QueryReturnValue + data?: InfiniteData + param?: unknown + previous?: boolean +} + type StartQueryActionCreator< D extends QueryDefinition, > = ( @@ -91,6 +103,15 @@ type StartInfiniteQueryActionCreator< options?: StartInfiniteQueryActionCreatorOptions ) => (dispatch: ThunkDispatch, getState: () => any) => InfiniteQueryActionCreatorResult +// placeholder type which +// may attempt to derive the list of args to query in pagination +type StartInfiniteQueryActionCreator< + D extends QueryDefinition +> = ( + arg: QueryArgFrom, + options?: StartInfiniteQueryActionCreatorOptions +) => (dispatch: ThunkDispatch, getState: () => any) => InfiniteQueryActionCreatorResult + export type QueryActionCreatorResult< D extends QueryDefinition, > = SafePromise> & { @@ -123,6 +144,22 @@ export type InfiniteQueryActionCreatorResult< queryCacheKey: string } +export type InfiniteQueryActionCreatorResult< + D extends QueryDefinition +> = Promise> & { + arg: QueryArgFrom + requestId: string + subscriptionOptions: SubscriptionOptions | undefined + abort(): void + unwrap(): Promise> + unsubscribe(): void + refetch(): QueryActionCreatorResult + fetchNextPage(): QueryActionCreatorResult + fetchPreviousPage(): QueryActionCreatorResult + updateSubscriptionOptions(options: SubscriptionOptions): void + queryCacheKey: string +} + type StartMutationActionCreator< D extends MutationDefinition, > = ( @@ -603,6 +640,145 @@ You must add the middleware for RTK-Query to function correctly!`, return infiniteQueryAction } + // Concept for the pagination thunk which queries for each page + + function buildInitiateInfiniteQuery( + endpointName: string, + endpointDefinition: QueryDefinition, + pages?: number, + ) { + const infiniteQueryAction: StartInfiniteQueryActionCreator = + ( + arg, + { + subscribe = true, + forceRefetch, + subscriptionOptions, + [forceQueryFnSymbol]: forceQueryFn, + direction, + data = { pages: [], pageParams: [] }, + param = arg, + previous + } = {} + ) => + (dispatch, getState) => { + const queryCacheKey = serializeQueryArgs({ + queryArgs: param, + endpointDefinition, + endpointName, + }) + + + const thunk = infiniteQueryThunk({ + type: 'query', + subscribe, + forceRefetch: forceRefetch, + subscriptionOptions, + endpointName, + originalArgs: arg, + queryCacheKey, + [forceQueryFnSymbol]: forceQueryFn, + data, + param, + previous, + direction + }) + const selector = ( + api.endpoints[endpointName] as ApiEndpointQuery + ).select(arg) + + const thunkResult = dispatch(thunk) + const stateAfter = selector(getState()) + + middlewareWarning(dispatch) + + const { requestId, abort } = thunkResult + + const skippedSynchronously = stateAfter.requestId !== requestId + + const runningQuery = runningQueries.get(dispatch)?.[queryCacheKey] + const selectFromState = () => selector(getState()) + + const statePromise: InfiniteQueryActionCreatorResult = Object.assign( + forceQueryFn + ? // a query has been forced (upsertQueryData) + // -> we want to resolve it once data has been written with the data that will be written + thunkResult.then(selectFromState) + : skippedSynchronously && !runningQuery + ? // a query has been skipped due to a condition and we do not have any currently running query + // -> we want to resolve it immediately with the current data + Promise.resolve(stateAfter) + : // query just started or one is already in flight + // -> wait for the running query, then resolve with data from after that + Promise.all([runningQuery, thunkResult]).then(selectFromState), + { + arg, + requestId, + subscriptionOptions, + queryCacheKey, + abort, + async unwrap() { + const result = await statePromise + + if (result.isError) { + throw result.error + } + + return result.data + }, + refetch: () => + dispatch( + infiniteQueryAction(arg, { subscribe: false, forceRefetch: true }) + ), + fetchNextPage: () => + dispatch( + infiniteQueryAction(arg, { subscribe: false, forceRefetch: true, direction: "forward"}) + ), + fetchPreviousPage: () => + dispatch( + infiniteQueryAction(arg, {subscribe: false, forceRefetch: true, direction: "backwards"}) + ), + unsubscribe() { + if (subscribe) + dispatch( + unsubscribeQueryResult({ + queryCacheKey, + requestId, + }) + ) + }, + updateSubscriptionOptions(options: SubscriptionOptions) { + statePromise.subscriptionOptions = options + dispatch( + updateSubscriptionOptions({ + endpointName, + requestId, + queryCacheKey, + options, + }) + ) + }, + } + ) + + if (!runningQuery && !skippedSynchronously && !forceQueryFn) { + const running = runningQueries.get(dispatch) || {} + running[queryCacheKey] = statePromise + runningQueries.set(dispatch, running) + + statePromise.then(() => { + delete running[queryCacheKey] + if (!countObjectKeys(running)) { + runningQueries.delete(dispatch) + } + }) + } + return statePromise + + } + return infiniteQueryAction + } + function buildInitiateMutation( endpointName: string, ): StartMutationActionCreator { diff --git a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts index 78850130be..01b9bb6d05 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts @@ -166,6 +166,20 @@ declare module '../../endpointDefinitions' { ): Promise | void } + // copying QueryDefinition to get past initial build + interface InfiniteQueryExtraOptions< + TagTypes extends string, + ResultType, + QueryArg, + BaseQuery extends BaseQueryFn, + ReducerPath extends string = string + > { + onCacheEntryAdded?( + arg: QueryArg, + api: QueryCacheLifecycleApi + ): Promise | void + } + interface MutationExtraOptions< TagTypes extends string, ResultType, diff --git a/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts index 0d12dcdd12..d559011881 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts @@ -132,6 +132,20 @@ declare module '../../endpointDefinitions' { ): Promise | void } + // temporarily cloned QueryOptions again to just get the definition to build for now + interface InfiniteQueryExtraOptions< + TagTypes extends string, + ResultType, + QueryArg, + BaseQuery extends BaseQueryFn, + ReducerPath extends string = string + > { + onQueryStarted?( + arg: QueryArg, + api: QueryLifecycleApi + ): Promise | void + } + interface MutationExtraOptions< TagTypes extends string, ResultType, diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index dc24c5bc2e..eb673ef492 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -495,6 +495,39 @@ export function buildSlice({ }) + const infiniteQuerySlice = createSlice({ + name: `${reducerPath}/infinitequeries`, + initialState: initialState as QueryState, + reducers: { + changeDirection: { + reducer( + draft, + { payload: { queryCacheKey } }: PayloadAction + ) { + }, + prepare: prepareAutoBatched(), + }, + combineArgsFromSelection: { + reducer( + draft, + { + payload: { queryCacheKey, patches }, + }: PayloadAction< + QuerySubstateIdentifier & { patches: readonly Patch[] } + > + ) { + updateQuerySubstateIfExists(draft, queryCacheKey, (substate) => { + substate.originalArgs = substate + }) + }, + prepare: prepareAutoBatched< + QuerySubstateIdentifier & { patches: readonly Patch[] } + >(), + }, + }, + }) + + // Dummy slice to generate actions const subscriptionSlice = createSlice({ name: `${reducerPath}/subscriptions`, diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index 687d071c8e..fb80447b49 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -133,6 +133,18 @@ export interface InfiniteQueryThunkArg direction?: 'forward' | "backwards" } +export interface InfiniteQueryThunkArg + extends QuerySubstateIdentifier, + StartInfiniteQueryActionCreatorOptions { + type: `query` + originalArgs: unknown + endpointName: string + data: InfiniteData + param: unknown + previous?: boolean + direction?: 'forward' | "backwards" +} + export interface MutationThunkArg { type: 'mutation' originalArgs: unknown @@ -308,6 +320,16 @@ export function buildThunks< return max && newItems.length > max ? newItems.slice(1) : newItems } + function addToStart(items: Array, item: T, max = 0): Array { + const newItems = [item, ...items] + return max && newItems.length > max ? newItems.slice(0, -1) : newItems + } + + function addToEnd(items: Array, item: T, max = 0): Array { + const newItems = [...items, item] + return max && newItems.length > max ? newItems.slice(1) : newItems + } + const updateQueryData: UpdateQueryDataThunk = (endpointName, args, updateRecipe, updateProvided = true) => (dispatch, getState) => { @@ -774,6 +796,65 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".` dispatchConditionRejection: true, }) + const infiniteQueryThunk = createAsyncThunk< + ThunkResult, + InfiniteQueryThunkArg, + ThunkApiMetaConfig & { state: RootState } + >(`${reducerPath}/executeQuery`, executeEndpoint, { + getPendingMeta() { + return { startedTimeStamp: Date.now(), [SHOULD_AUTOBATCH]: true } + }, + condition(queryThunkArgs, { getState }) { + const state = getState() + + const requestState = + state[reducerPath]?.queries?.[queryThunkArgs.queryCacheKey] + const fulfilledVal = requestState?.fulfilledTimeStamp + const currentArg = queryThunkArgs.originalArgs + const previousArg = requestState?.originalArgs + const endpointDefinition = + endpointDefinitions[queryThunkArgs.endpointName] + + // Order of these checks matters. + // In order for `upsertQueryData` to successfully run while an existing request is in flight, + /// we have to check for that first, otherwise `queryThunk` will bail out and not run at all. + // if (isUpsertQuery(queryThunkArgs)) { + // return true + // } + + // Don't retry a request that's currently in-flight + if (requestState?.status === 'pending') { + return false + } + + // if this is forced, continue + // if (isForcedQuery(queryThunkArgs, state)) { + // return true + // } + + if ( + isQueryDefinition(endpointDefinition) && + endpointDefinition?.forceRefetch?.({ + currentArg, + previousArg, + endpointState: requestState, + state, + }) + ) { + return true + } + + // Pull from the cache unless we explicitly force refetch or qualify based on time + if (fulfilledVal) { + // Value is cached and we didn't specify to refresh, skip it. + return false + } + + return true + }, + dispatchConditionRejection: true, + }) + const mutationThunk = createAsyncThunk< ThunkResult, MutationThunkArg, diff --git a/packages/toolkit/src/query/core/module.ts b/packages/toolkit/src/query/core/module.ts index b7bee0d754..fe97bad78d 100644 --- a/packages/toolkit/src/query/core/module.ts +++ b/packages/toolkit/src/query/core/module.ts @@ -644,6 +644,16 @@ export const coreModule = ({ }, buildMatchThunkActions(queryThunk, endpointName) ) + } if (isInfiniteQueryDefinition(definition)) { + safeAssign( + anyApi.endpoints[endpointName], + { + name: endpointName, + select: buildInfiniteQuerySelector(endpointName, definition), + initiate: buildInitiateInfiniteQuery(endpointName, definition), + }, + buildMatchThunkActions(queryThunk, endpointName) + ) } }, }