EntityStore: support query invalidation

This commit is contained in:
Alex Gleason 2023-03-22 18:17:28 -05:00
parent a256665aad
commit e2510489c5
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
5 changed files with 41 additions and 3 deletions

View file

@ -6,6 +6,7 @@ const ENTITIES_DISMISS = 'ENTITIES_DISMISS' as const;
const ENTITIES_FETCH_REQUEST = 'ENTITIES_FETCH_REQUEST' as const; const ENTITIES_FETCH_REQUEST = 'ENTITIES_FETCH_REQUEST' as const;
const ENTITIES_FETCH_SUCCESS = 'ENTITIES_FETCH_SUCCESS' as const; const ENTITIES_FETCH_SUCCESS = 'ENTITIES_FETCH_SUCCESS' as const;
const ENTITIES_FETCH_FAIL = 'ENTITIES_FETCH_FAIL' as const; const ENTITIES_FETCH_FAIL = 'ENTITIES_FETCH_FAIL' as const;
const ENTITIES_INVALIDATE_LIST = 'ENTITIES_INVALIDATE_LIST' as const;
/** Action to import entities into the cache. */ /** Action to import entities into the cache. */
function importEntities(entities: Entity[], entityType: string, listKey?: string) { function importEntities(entities: Entity[], entityType: string, listKey?: string) {
@ -66,6 +67,14 @@ function entitiesFetchFail(entityType: string, listKey: string | undefined, erro
}; };
} }
function invalidateEntityList(entityType: string, listKey: string) {
return {
type: ENTITIES_INVALIDATE_LIST,
entityType,
listKey,
};
}
/** Any action pertaining to entities. */ /** Any action pertaining to entities. */
type EntityAction = type EntityAction =
ReturnType<typeof importEntities> ReturnType<typeof importEntities>
@ -73,7 +82,8 @@ type EntityAction =
| ReturnType<typeof dismissEntities> | ReturnType<typeof dismissEntities>
| ReturnType<typeof entitiesFetchRequest> | ReturnType<typeof entitiesFetchRequest>
| ReturnType<typeof entitiesFetchSuccess> | ReturnType<typeof entitiesFetchSuccess>
| ReturnType<typeof entitiesFetchFail>; | ReturnType<typeof entitiesFetchFail>
| ReturnType<typeof invalidateEntityList>;
export { export {
ENTITIES_IMPORT, ENTITIES_IMPORT,
@ -82,12 +92,14 @@ export {
ENTITIES_FETCH_REQUEST, ENTITIES_FETCH_REQUEST,
ENTITIES_FETCH_SUCCESS, ENTITIES_FETCH_SUCCESS,
ENTITIES_FETCH_FAIL, ENTITIES_FETCH_FAIL,
ENTITIES_INVALIDATE_LIST,
importEntities, importEntities,
deleteEntities, deleteEntities,
dismissEntities, dismissEntities,
entitiesFetchRequest, entitiesFetchRequest,
entitiesFetchSuccess, entitiesFetchSuccess,
entitiesFetchFail, entitiesFetchFail,
invalidateEntityList,
EntityAction, EntityAction,
}; };

View file

@ -5,7 +5,7 @@ import { getNextLink, getPrevLink } from 'soapbox/api';
import { useApi, useAppDispatch, useAppSelector, useGetState } from 'soapbox/hooks'; import { useApi, useAppDispatch, useAppSelector, useGetState } from 'soapbox/hooks';
import { filteredArray } from 'soapbox/schemas/utils'; import { filteredArray } from 'soapbox/schemas/utils';
import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess } from '../actions'; import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess, invalidateEntityList } from '../actions';
import { parseEntitiesPath } from './utils'; import { parseEntitiesPath } from './utils';
@ -48,6 +48,7 @@ function useEntities<TEntity extends Entity>(
const isFetched = useListState(path, 'fetched'); const isFetched = useListState(path, 'fetched');
const isError = !!useListState(path, 'error'); const isError = !!useListState(path, 'error');
const totalCount = useListState(path, 'totalCount'); const totalCount = useListState(path, 'totalCount');
const isInvalid = useListState(path, 'invalid');
const next = useListState(path, 'next'); const next = useListState(path, 'next');
const prev = useListState(path, 'prev'); const prev = useListState(path, 'prev');
@ -72,6 +73,7 @@ function useEntities<TEntity extends Entity>(
fetched: true, fetched: true,
error: null, error: null,
lastFetchedAt: new Date(), lastFetchedAt: new Date(),
invalid: false,
})); }));
} catch (error) { } catch (error) {
dispatch(entitiesFetchFail(entityType, listKey, error)); dispatch(entitiesFetchFail(entityType, listKey, error));
@ -96,10 +98,19 @@ function useEntities<TEntity extends Entity>(
} }
}; };
const invalidate = () => {
dispatch(invalidateEntityList(entityType, listKey));
};
const staleTime = opts.staleTime ?? 60000; const staleTime = opts.staleTime ?? 60000;
useEffect(() => { useEffect(() => {
if (isEnabled && !isFetching && (!lastFetchedAt || lastFetchedAt.getTime() + staleTime <= Date.now())) { if (!isEnabled) return;
if (isFetching) return;
const isUnset = !lastFetchedAt;
const isStale = lastFetchedAt ? Date.now() >= lastFetchedAt.getTime() + staleTime : false;
if (isInvalid || isUnset || isStale) {
fetchEntities(); fetchEntities();
} }
}, [endpoint, isEnabled]); }, [endpoint, isEnabled]);
@ -116,6 +127,7 @@ function useEntities<TEntity extends Entity>(
isFetched, isFetched,
isFetching, isFetching,
isLoading: isFetching && entities.length === 0, isLoading: isFetching && entities.length === 0,
invalidate,
}; };
} }

View file

@ -8,6 +8,7 @@ import {
ENTITIES_FETCH_SUCCESS, ENTITIES_FETCH_SUCCESS,
ENTITIES_FETCH_FAIL, ENTITIES_FETCH_FAIL,
EntityAction, EntityAction,
ENTITIES_INVALIDATE_LIST,
} from './actions'; } from './actions';
import { createCache, createList, updateStore, updateList } from './utils'; import { createCache, createList, updateStore, updateList } from './utils';
@ -114,6 +115,14 @@ const setFetching = (
}); });
}; };
const invalidateEntityList = (state: State, entityType: string, listKey: string) => {
return produce(state, draft => {
const cache = draft[entityType] ?? createCache();
const list = cache.lists[listKey] ?? createList();
list.state.invalid = true;
});
};
/** Stores various entity data and lists in a one reducer. */ /** Stores various entity data and lists in a one reducer. */
function reducer(state: Readonly<State> = {}, action: EntityAction): State { function reducer(state: Readonly<State> = {}, action: EntityAction): State {
switch (action.type) { switch (action.type) {
@ -129,6 +138,8 @@ function reducer(state: Readonly<State> = {}, action: EntityAction): State {
return setFetching(state, action.entityType, action.listKey, true); return setFetching(state, action.entityType, action.listKey, true);
case ENTITIES_FETCH_FAIL: case ENTITIES_FETCH_FAIL:
return setFetching(state, action.entityType, action.listKey, false, action.error); return setFetching(state, action.entityType, action.listKey, false, action.error);
case ENTITIES_INVALIDATE_LIST:
return invalidateEntityList(state, action.entityType, action.listKey);
default: default:
return state; return state;
} }

View file

@ -33,6 +33,8 @@ interface EntityListState {
fetching: boolean fetching: boolean
/** Date of the last API fetch for this list. */ /** Date of the last API fetch for this list. */
lastFetchedAt: Date | undefined lastFetchedAt: Date | undefined
/** Whether the entities should be refetched on the next component mount. */
invalid: boolean
} }
/** Cache data pertaining to a paritcular entity type.. */ /** Cache data pertaining to a paritcular entity type.. */

View file

@ -43,6 +43,7 @@ const createListState = (): EntityListState => ({
fetched: false, fetched: false,
fetching: false, fetching: false,
lastFetchedAt: undefined, lastFetchedAt: undefined,
invalid: false,
}); });
export { export {