Skip to content

Commit

Permalink
add endpoint, type and forced to BaseQueryApi and `prepareHea…
Browse files Browse the repository at this point in the history
…ders` (#1656)

Co-authored-by: Matt Sutkowski <msutkowski@gmail.com>
  • Loading branch information
phryneas and msutkowski authored Oct 28, 2021
1 parent 199ad89 commit ae94709
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 266 deletions.
11 changes: 11 additions & 0 deletions packages/toolkit/src/query/baseQueryTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ export interface BaseQueryApi {
dispatch: ThunkDispatch<any, any, any>
getState: () => unknown
extra: unknown
endpoint: string
type: 'query' | 'mutation'
/**
* Only available for queries: indicates if a query has been forced,
* i.e. it would have been fetched even if there would already be a cache entry
* (this does not mean that there is already a cache entry though!)
*
* This can be used to for example add a `Cache-Control: no-cache` header for
* invalidated queries.
*/
forced?: boolean
}

export type QueryReturnValue<T = unknown, E = unknown, M = unknown> =
Expand Down
2 changes: 2 additions & 0 deletions packages/toolkit/src/query/core/buildInitiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ Features like automatic cache collection, automatic refetching etc. will not be
endpointName,
})
const thunk = queryThunk({
type: 'query',
subscribe,
forceRefetch,
subscriptionOptions,
Expand Down Expand Up @@ -330,6 +331,7 @@ Features like automatic cache collection, automatic refetching etc. will not be
return (arg, { track = true, fixedCacheKey } = {}) =>
(dispatch, getState) => {
const thunk = mutationThunk({
type: 'mutation',
endpointName,
originalArgs: arg,
track,
Expand Down
1 change: 1 addition & 0 deletions packages/toolkit/src/query/core/buildMiddleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export function buildMiddleware<
override: Partial<QueryThunkArg> = {}
) {
return queryThunk({
type: 'query',
endpointName: querySubState.endpointName,
originalArgs: querySubState.originalArgs,
subscribe: false,
Expand Down
49 changes: 34 additions & 15 deletions packages/toolkit/src/query/core/buildThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,13 @@ export interface Matchers<
export interface QueryThunkArg
extends QuerySubstateIdentifier,
StartQueryActionCreatorOptions {
type: 'query'
originalArgs: unknown
endpointName: string
}

export interface MutationThunkArg {
type: 'mutation'
originalArgs: unknown
endpointName: string
track?: boolean
Expand Down Expand Up @@ -276,6 +278,10 @@ export function buildThunks<
dispatch,
getState,
extra,
endpoint: arg.endpointName,
type: arg.type,
forced:
arg.type === 'query' ? isForcedQuery(arg, getState()) : undefined,
}
if (endpointDefinition.query) {
result = await baseQuery(
Expand Down Expand Up @@ -325,6 +331,28 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".`
}
}

function isForcedQuery(
arg: QueryThunkArg,
state: RootState<any, string, ReducerPath>
) {
const requestState = state[reducerPath]?.queries?.[arg.queryCacheKey]
const baseFetchOnMountOrArgChange =
state[reducerPath]?.config.refetchOnMountOrArgChange

const fulfilledVal = requestState?.fulfilledTimeStamp
const refetchVal =
arg.forceRefetch ?? (arg.subscribe && baseFetchOnMountOrArgChange)

if (refetchVal) {
// Return if its true or compare the dates because it must be a number
return (
refetchVal === true ||
(Number(new Date()) - Number(fulfilledVal)) / 1000 >= refetchVal
)
}
return false
}

const queryThunk = createAsyncThunk<
ThunkResult,
QueryThunkArg,
Expand All @@ -334,29 +362,20 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".`
return { startedTimeStamp: Date.now() }
},
condition(arg, { getState }) {
const state = getState()[reducerPath]
const requestState = state?.queries?.[arg.queryCacheKey]
const baseFetchOnMountOrArgChange = state.config.refetchOnMountOrArgChange

const state = getState()
const requestState = state[reducerPath]?.queries?.[arg.queryCacheKey]
const fulfilledVal = requestState?.fulfilledTimeStamp
const refetchVal =
arg.forceRefetch ?? (arg.subscribe && baseFetchOnMountOrArgChange)

// Don't retry a request that's currently in-flight
if (requestState?.status === 'pending') return false

// if this is forced, continue
if (isForcedQuery(arg, state)) return true

// Pull from the cache unless we explicitly force refetch or qualify based on time
if (fulfilledVal) {
if (refetchVal) {
// Return if its true or compare the dates because it must be a number
return (
refetchVal === true ||
(Number(new Date()) - Number(fulfilledVal)) / 1000 >= refetchVal
)
}
if (fulfilledVal)
// Value is cached and we didn't specify to refresh, skip it.
return false
}

return true
},
Expand Down
8 changes: 4 additions & 4 deletions packages/toolkit/src/query/fetchBaseQuery.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { joinUrls } from './utils'
import { isPlainObject } from '@reduxjs/toolkit'
import type { BaseQueryFn } from './baseQueryTypes'
import type { BaseQueryApi, BaseQueryFn } from './baseQueryTypes'
import type { MaybePromise, Override } from './tsHelpers'

export type ResponseHandler =
Expand Down Expand Up @@ -113,7 +113,7 @@ export type FetchBaseQueryArgs = {
baseUrl?: string
prepareHeaders?: (
headers: Headers,
api: { getState: () => unknown }
api: Pick<BaseQueryApi, 'getState' | 'endpoint' | 'type' | 'forced'>
) => MaybePromise<Headers>
fetchFn?: (
input: RequestInfo,
Expand Down Expand Up @@ -178,7 +178,7 @@ export function fetchBaseQuery({
'Warning: `fetch` is not available. Please supply a custom `fetchFn` property to use `fetchBaseQuery` on SSR environments.'
)
}
return async (arg, { signal, getState }) => {
return async (arg, { signal, getState, endpoint, forced, type }) => {
let meta: FetchBaseQueryMeta | undefined
let {
url,
Expand All @@ -200,7 +200,7 @@ export function fetchBaseQuery({

config.headers = await prepareHeaders(
new Headers(stripUndefined(headers)),
{ getState }
{ getState, endpoint, forced, type }
)

// Only set the content-type to json if appropriate. Will not be true for FormData, ArrayBuffer, Blob, etc.
Expand Down
68 changes: 59 additions & 9 deletions packages/toolkit/src/query/tests/createApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,14 @@ describe('endpoint definition typings', () => {

describe('enhancing endpoint definitions', () => {
const baseQuery = jest.fn((x: string) => ({ data: 'success' }))
const baseQueryApiMatcher = {
const commonBaseQueryApi = {
dispatch: expect.any(Function),
endpoint: expect.any(String),
extra: undefined,
forced: expect.any(Boolean),
getState: expect.any(Function),
signal: expect.any(Object),
type: expect.any(String),
}
beforeEach(() => {
baseQuery.mockClear()
Expand Down Expand Up @@ -337,11 +341,56 @@ describe('endpoint definition typings', () => {
storeRef.store.dispatch(api.endpoints.query2.initiate('in2'))
storeRef.store.dispatch(api.endpoints.mutation1.initiate('in1'))
storeRef.store.dispatch(api.endpoints.mutation2.initiate('in2'))

expect(baseQuery.mock.calls).toEqual([
['in1', baseQueryApiMatcher, undefined],
['in2', baseQueryApiMatcher, undefined],
['in1', baseQueryApiMatcher, undefined],
['in2', baseQueryApiMatcher, undefined],
[
'in1',
{
dispatch: expect.any(Function),
endpoint: expect.any(String),
getState: expect.any(Function),
signal: expect.any(Object),
forced: expect.any(Boolean),
type: expect.any(String),
},
undefined,
],
[
'in2',
{
dispatch: expect.any(Function),
endpoint: expect.any(String),
getState: expect.any(Function),
signal: expect.any(Object),
forced: expect.any(Boolean),
type: expect.any(String),
},
undefined,
],
[
'in1',
{
dispatch: expect.any(Function),
endpoint: expect.any(String),
getState: expect.any(Function),
signal: expect.any(Object),
// forced: undefined,
type: expect.any(String),
},
undefined,
],
[
'in2',
{
dispatch: expect.any(Function),
endpoint: expect.any(String),
getState: expect.any(Function),
signal: expect.any(Object),
// forced: undefined,
type: expect.any(String),
},
undefined,
],
])
})

Expand Down Expand Up @@ -439,11 +488,12 @@ describe('endpoint definition typings', () => {
storeRef.store.dispatch(api.endpoints.query2.initiate('in2'))
storeRef.store.dispatch(api.endpoints.mutation1.initiate('in1'))
storeRef.store.dispatch(api.endpoints.mutation2.initiate('in2'))

expect(baseQuery.mock.calls).toEqual([
['modified1', baseQueryApiMatcher, undefined],
['modified2', baseQueryApiMatcher, undefined],
['modified1', baseQueryApiMatcher, undefined],
['modified2', baseQueryApiMatcher, undefined],
['modified1', commonBaseQueryApi, undefined],
['modified2', commonBaseQueryApi, undefined],
['modified1', { ...commonBaseQueryApi, forced: undefined }, undefined],
['modified2', { ...commonBaseQueryApi, forced: undefined }, undefined],
])
})
})
Expand Down
34 changes: 14 additions & 20 deletions packages/toolkit/src/query/tests/errorHandling.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { server } from './mocks/server'
import { fireEvent, render, waitFor, screen } from '@testing-library/react'
import { useDispatch } from 'react-redux'
import type { AnyAction, ThunkDispatch } from '@reduxjs/toolkit'
import type { BaseQueryApi } from '../baseQueryTypes'

const baseQuery = fetchBaseQuery({ baseUrl: 'http://example.com' })

Expand All @@ -33,18 +34,20 @@ const failQueryOnce = rest.get('/query', (_, req, ctx) =>
)

describe('fetchBaseQuery', () => {
let commonBaseQueryApiArgs: BaseQueryApi = {} as any
beforeEach(() => {
commonBaseQueryApiArgs = {
signal: new AbortController().signal,
dispatch: storeRef.store.dispatch,
getState: storeRef.store.getState,
extra: undefined,
type: 'query',
endpoint: 'doesntmatterhere',
}
})
test('success', async () => {
await expect(
baseQuery(
'/success',
{
signal: new AbortController().signal,
dispatch: storeRef.store.dispatch,
getState: storeRef.store.getState,
extra: undefined,
},
{}
)
baseQuery('/success', commonBaseQueryApiArgs, {})
).resolves.toEqual({
data: { value: 'success' },
meta: {
Expand All @@ -56,16 +59,7 @@ describe('fetchBaseQuery', () => {
test('error', async () => {
server.use(failQueryOnce)
await expect(
baseQuery(
'/error',
{
signal: new AbortController().signal,
dispatch: storeRef.store.dispatch,
getState: storeRef.store.getState,
extra: undefined,
},
{}
)
baseQuery('/error', commonBaseQueryApiArgs, {})
).resolves.toEqual({
error: {
data: { value: 'error' },
Expand Down
Loading

0 comments on commit ae94709

Please sign in to comment.