Skip to content
This repository has been archived by the owner on Nov 23, 2024. It is now read-only.

Commit

Permalink
Add utils.invalidateEntities (#190)
Browse files Browse the repository at this point in the history
  • Loading branch information
msutkowski authored Mar 27, 2021
1 parent e7cbf48 commit bcc3e00
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 44 deletions.
2 changes: 1 addition & 1 deletion src/core/buildInitiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function buildInitiate<InternalQueryArgs>({
serializeQueryArgs: InternalSerializeQueryArgs<InternalQueryArgs>;
queryThunk: AsyncThunk<any, QueryThunkArg<any>, {}>;
mutationThunk: AsyncThunk<any, MutationThunkArg<any>, {}>;
api: Api<any, EndpointDefinitions, any, string>;
api: Api<any, EndpointDefinitions, any, any>;
}) {
const { unsubscribeQueryResult, unsubscribeMutationResult, updateSubscriptionOptions } = api.internalActions;
return { buildInitiateQuery, buildInitiateMutation };
Expand Down
24 changes: 19 additions & 5 deletions src/core/buildMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AnyAction, AsyncThunk, Middleware, MiddlewareAPI, ThunkDispatch } from '@reduxjs/toolkit';
import { AnyAction, AsyncThunk, createAction, Middleware, MiddlewareAPI, ThunkDispatch } from '@reduxjs/toolkit';
import { QueryCacheKey, QueryStatus, QuerySubState, QuerySubstateIdentifier, RootState, Subscribers } from './apiState';
import { Api, ApiContext } from '../apiTypes';
import { MutationThunkArg, QueryThunkArg, ThunkResult } from './buildThunks';
Expand All @@ -14,7 +14,11 @@ import { flatten } from '../utils';
type QueryStateMeta<T> = Record<string, undefined | T>;
type TimeoutId = ReturnType<typeof setTimeout>;

export function buildMiddleware<Definitions extends EndpointDefinitions, ReducerPath extends string>({
export function buildMiddleware<
Definitions extends EndpointDefinitions,
ReducerPath extends string,
EntityTypes extends string
>({
reducerPath,
context,
context: { endpointDefinitions },
Expand All @@ -27,15 +31,21 @@ export function buildMiddleware<Definitions extends EndpointDefinitions, Reducer
context: ApiContext<Definitions>;
queryThunk: AsyncThunk<ThunkResult, QueryThunkArg<any>, {}>;
mutationThunk: AsyncThunk<ThunkResult, MutationThunkArg<any>, {}>;
api: Api<any, EndpointDefinitions, ReducerPath, string>;
api: Api<any, EndpointDefinitions, ReducerPath, EntityTypes>;
assertEntityType: AssertEntityTypes;
}) {
type MWApi = MiddlewareAPI<ThunkDispatch<any, any, AnyAction>, RootState<Definitions, string, ReducerPath>>;

const currentRemovalTimeouts: QueryStateMeta<TimeoutId> = {};
const { removeQueryResult, unsubscribeQueryResult, updateSubscriptionOptions } = api.internalActions;

const currentPolls: QueryStateMeta<{ nextPollTimestamp: number; timeout?: TimeoutId; pollingInterval: number }> = {};

const actions = {
invalidateEntities: createAction<Array<EntityTypes | FullEntityDescription<EntityTypes>>>(
`${reducerPath}/invalidateEntities`
),
};

const middleware: Middleware<{}, RootState<Definitions, string, ReducerPath>, ThunkDispatch<any, any, AnyAction>> = (
mwApi
) => (next) => (action) => {
Expand All @@ -53,6 +63,10 @@ export function buildMiddleware<Definitions extends EndpointDefinitions, Reducer
);
}

if (actions.invalidateEntities.match(action)) {
invalidateEntities(calculateProvidedBy(action.payload, undefined, undefined, assertEntityType), mwApi);
}

if (unsubscribeQueryResult.match(action)) {
handleUnsubscribe(action.payload, mwApi);
}
Expand All @@ -77,7 +91,7 @@ export function buildMiddleware<Definitions extends EndpointDefinitions, Reducer
return result;
};

return { middleware };
return { middleware, actions };

function refetchQuery(
querySubState: Exclude<QuerySubState<any>, { status: QueryStatus.uninitialized }>,
Expand Down
2 changes: 1 addition & 1 deletion src/core/buildThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export function buildThunks<
reducerPath: ReducerPath;
context: ApiContext<Definitions>;
serializeQueryArgs: InternalSerializeQueryArgs<BaseQueryArg<BaseQuery>>;
api: Api<BaseQuery, EndpointDefinitions, ReducerPath, string>;
api: Api<BaseQuery, EndpointDefinitions, ReducerPath, any>;
}) {
type InternalQueryArgs = BaseQueryArg<BaseQuery>;
type State = RootState<any, string, ReducerPath>;
Expand Down
11 changes: 8 additions & 3 deletions src/core/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Note: this file should import all other files for type discovery and declaration merging
*/
import { buildThunks, PatchQueryResultThunk, UpdateQueryResultThunk } from './buildThunks';
import { AnyAction, Middleware, Reducer, ThunkAction, ThunkDispatch } from '@reduxjs/toolkit';
import { ActionCreatorWithPayload, AnyAction, Middleware, Reducer, ThunkAction, ThunkDispatch } from '@reduxjs/toolkit';
import {
EndpointDefinitions,
QueryArgFrom,
Expand All @@ -11,6 +11,7 @@ import {
AssertEntityTypes,
isQueryDefinition,
isMutationDefinition,
FullEntityDescription,
} from '../endpointDefinitions';
import { CombinedState, QueryKeys, RootState } from './apiState';
import './buildSelectors';
Expand Down Expand Up @@ -55,6 +56,7 @@ declare module '../apiTypes' {
): ThunkAction<void, any, any, AnyAction>;
updateQueryResult: UpdateQueryResultThunk<Definitions, RootState<Definitions, string, ReducerPath>>;
patchQueryResult: PatchQueryResultThunk<Definitions, RootState<Definitions, string, ReducerPath>>;
invalidateEntities: ActionCreatorWithPayload<Array<EntityTypes | FullEntityDescription<EntityTypes>>, string>;
};
// If you actually care about the return value, use useQuery
usePrefetch<EndpointName extends QueryKeys<Definitions>>(
Expand Down Expand Up @@ -87,7 +89,7 @@ export interface ApiEndpointMutation<
Definitions extends EndpointDefinitions
> {}

export type InternalActions = SliceActions & {
export type ListenerActions = {
/**
* Will cause the RTK Query middleware to trigger any refetchOnReconnect-related behavior
* @link https://rtk-query-docs.netlify.app/api/setupListeners
Expand All @@ -102,6 +104,8 @@ export type InternalActions = SliceActions & {
onFocusLost: typeof onFocusLost;
};

export type InternalActions = SliceActions & ListenerActions;

export const coreModule = (): Module<CoreModule> => ({
name: coreModuleName,
init(
Expand Down Expand Up @@ -168,14 +172,15 @@ export const coreModule = (): Module<CoreModule> => ({
safeAssign(api.util, { patchQueryResult, updateQueryResult, prefetchThunk });
safeAssign(api.internalActions, sliceActions);

const { middleware } = buildMiddleware({
const { middleware, actions: middlewareActions } = buildMiddleware({
reducerPath,
context,
queryThunk,
mutationThunk,
api,
assertEntityType,
});
safeAssign(api.util, middlewareActions);

safeAssign(api, { reducer: reducer as any, middleware });

Expand Down
2 changes: 1 addition & 1 deletion src/react-hooks/ApiProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Api } from '../apiTypes';
* conflict with each other - please use the traditional redux setup
* in that case.
*/
export function ApiProvider<A extends Api<any, {}, any, string>>(props: {
export function ApiProvider<A extends Api<any, {}, any, any>>(props: {
children: any;
api: A;
setupListeners?: Parameters<typeof setupListeners>[1];
Expand Down
2 changes: 1 addition & 1 deletion src/react-hooks/buildHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
api,
moduleOptions: { batch, useDispatch, useSelector },
}: {
api: Api<any, Definitions, any, string, CoreModule>;
api: Api<any, Definitions, any, any, CoreModule>;
moduleOptions: Required<ReactHooksModuleOptions>;
}) {
return { buildQueryHooks, buildMutationHook, usePrefetch };
Expand Down
3 changes: 1 addition & 2 deletions test/buildHooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -509,15 +509,14 @@ describe('hooks tests', () => {
});
});

test('usePrefetch respects `ifOlderThan` when it evaluates to `true`', async () => {
test('usePrefetch respects ifOlderThan when it evaluates to true', async () => {
const { usePrefetch } = api;
const USER_ID = 47;

function User() {
// Load the initial query
const { isFetching } = api.endpoints.getUser.useQuery(USER_ID);
const prefetchUser = usePrefetch('getUser', { ifOlderThan: 0.2 });

return (
<div>
<div data-testid="isFetching">{String(isFetching)}</div>
Expand Down
82 changes: 82 additions & 0 deletions test/buildMiddleware.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { createApi } from '@rtk-incubator/rtk-query/react';
import { actionsReducer, matchSequence, setupApiStore, waitMs } from './helpers';

const baseQuery = (args?: any) => ({ data: args });
const api = createApi({
baseQuery,
entityTypes: ['Banana', 'Bread'],
endpoints: (build) => ({
getBanana: build.query<unknown, number>({
query(id) {
return { url: `banana/${id}` };
},
provides: ['Banana'],
}),
getBread: build.query<unknown, number>({
query(id) {
return { url: `bread/${id}` };
},
provides: ['Bread'],
}),
}),
});
const { getBanana, getBread } = api.endpoints;

const storeRef = setupApiStore(api, {
...actionsReducer,
});

it('invalidates the specified entities', async () => {
await storeRef.store.dispatch(getBanana.initiate(1));
matchSequence(storeRef.store.getState().actions, getBanana.matchPending, getBanana.matchFulfilled);

await storeRef.store.dispatch(api.util.invalidateEntities(['Banana', 'Bread']));

// Slight pause to let the middleware run and such
await waitMs(20);

const firstSequence = [
getBanana.matchPending,
getBanana.matchFulfilled,
api.util.invalidateEntities.match,
getBanana.matchPending,
getBanana.matchFulfilled,
];
matchSequence(storeRef.store.getState().actions, ...firstSequence);

await storeRef.store.dispatch(getBread.initiate(1));
await storeRef.store.dispatch(api.util.invalidateEntities([{ type: 'Bread' }]));

await waitMs(20);

matchSequence(
storeRef.store.getState().actions,
...firstSequence,
getBread.matchPending,
getBread.matchFulfilled,
api.util.invalidateEntities.match,
getBread.matchPending,
getBread.matchFulfilled
);
});

describe.skip('TS only tests', () => {
it('should allow for an array of string EntityTypes', () => {
api.util.invalidateEntities(['Banana', 'Bread']);
});
it('should allow for an array of full EntityTypes descriptions', () => {
api.util.invalidateEntities([{ type: 'Banana' }, { type: 'Bread', id: 1 }]);
});

it('should allow for a mix of full descriptions as well as plain strings', () => {
api.util.invalidateEntities(['Banana', { type: 'Bread', id: 1 }]);
});
it('should error when using non-existing EntityTypes', () => {
// @ts-expect-error
api.util.invalidateEntities(['Missing Entity']);
});
it('should error when using non-existing EntityTypes in the full format', () => {
// @ts-expect-error
api.util.invalidateEntities([{ type: 'Missing' }]);
});
});
26 changes: 26 additions & 0 deletions test/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,32 @@ export const hookWaitFor = async (cb: () => void, time = 2000) => {
}
};

export function matchSequence(_actions: AnyAction[], ...matchers: Array<(arg: any) => boolean>) {
const actions = _actions.concat();
actions.shift(); // remove INIT
expect(matchers.length).toBe(actions.length);
for (let i = 0; i < matchers.length; i++) {
expect(matchers[i](actions[i])).toBe(true);
}
}

export function notMatchSequence(_actions: AnyAction[], ...matchers: Array<Array<(arg: any) => boolean>>) {
const actions = _actions.concat();
actions.shift(); // remove INIT
expect(matchers.length).toBe(actions.length);
for (let i = 0; i < matchers.length; i++) {
for (const matcher of matchers[i]) {
expect(matcher(actions[i])).not.toBe(true);
}
}
}

export const actionsReducer = {
actions: (state: AnyAction[] = [], action: AnyAction) => {
return [...state, action];
},
};

export function setupApiStore<
A extends { reducerPath: any; reducer: Reducer<any, any>; middleware: Middleware<any> },
R extends Record<string, Reducer<any, any>>
Expand Down
35 changes: 10 additions & 25 deletions test/matchers.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { AnyAction, createSlice, SerializedError } from '@reduxjs/toolkit';
import { createSlice, SerializedError } from '@reduxjs/toolkit';
import { createApi, fetchBaseQuery } from '@rtk-incubator/rtk-query/react';
import { renderHook, act } from '@testing-library/react-hooks';
import { expectExactType, hookWaitFor, setupApiStore } from './helpers';
import {
actionsReducer,
expectExactType,
hookWaitFor,
matchSequence,
notMatchSequence,
setupApiStore,
} from './helpers';

interface ResultType {
result: 'complex';
Expand All @@ -28,31 +35,9 @@ const api = createApi({
});

const storeRef = setupApiStore(api, {
actions(state: AnyAction[] = [], action: AnyAction) {
return [...state, action];
},
...actionsReducer,
});

function matchSequence(_actions: AnyAction[], ...matchers: Array<(arg: any) => boolean>) {
const actions = _actions.concat();
actions.shift(); // remove INIT
expect(matchers.length).toBe(actions.length);
for (let i = 0; i < matchers.length; i++) {
expect(matchers[i](actions[i])).toBe(true);
}
}

function notMatchSequence(_actions: AnyAction[], ...matchers: Array<Array<(arg: any) => boolean>>) {
const actions = _actions.concat();
actions.shift(); // remove INIT
expect(matchers.length).toBe(actions.length);
for (let i = 0; i < matchers.length; i++) {
for (const matcher of matchers[i]) {
expect(matcher(actions[i])).not.toBe(true);
}
}
}

const { mutationFail, mutationSuccess, mutationSuccess2, queryFail, querySuccess, querySuccess2 } = api.endpoints;

const otherEndpointMatchers = [
Expand Down
7 changes: 2 additions & 5 deletions test/optimisticUpdates.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { AnyAction } from '@reduxjs/toolkit';
import { createApi } from '@rtk-incubator/rtk-query/react';
import { renderHook, act } from '@testing-library/react-hooks';
import { hookWaitFor, setupApiStore, waitMs } from './helpers';
import { actionsReducer, hookWaitFor, setupApiStore, waitMs } from './helpers';
import { Patch } from 'immer';

interface Post {
Expand Down Expand Up @@ -40,9 +39,7 @@ const api = createApi({
});

const storeRef = setupApiStore(api, {
actions(state: AnyAction[] = [], action: AnyAction) {
return [...state, action];
},
...actionsReducer,
});

describe('basic lifecycle', () => {
Expand Down

0 comments on commit bcc3e00

Please sign in to comment.