EntityStore: optimistic deletion

This commit is contained in:
Alex Gleason 2023-03-15 16:52:09 -05:00
parent 709edaefad
commit 1ab9b1d75c
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
3 changed files with 30 additions and 7 deletions

View file

@ -16,11 +16,16 @@ function importEntities(entities: Entity[], entityType: string, listKey?: string
};
}
function deleteEntities(ids: Iterable<string>, entityType: string) {
interface DeleteEntitiesOpts {
preserveLists?: boolean
}
function deleteEntities(ids: Iterable<string>, entityType: string, opts: DeleteEntitiesOpts = {}) {
return {
type: ENTITIES_DELETE,
ids,
entityType,
opts,
};
}
@ -72,3 +77,5 @@ export {
entitiesFetchFail,
EntityAction,
};
export type { DeleteEntitiesOpts };

View file

@ -1,6 +1,6 @@
import { z } from 'zod';
import { useApi, useAppDispatch } from 'soapbox/hooks';
import { useApi, useAppDispatch, useGetState } from 'soapbox/hooks';
import { deleteEntities, importEntities } from '../actions';
@ -35,6 +35,7 @@ function useEntityActions<TEntity extends Entity = Entity, P = any>(
) {
const api = useApi();
const dispatch = useAppDispatch();
const getState = useGetState();
const [entityType, listKey] = path;
function createEntity(params: P): Promise<CreateEntityResult<TEntity>> {
@ -56,13 +57,24 @@ function useEntityActions<TEntity extends Entity = Entity, P = any>(
function deleteEntity(entityId: string): Promise<DeleteEntityResult> {
if (!endpoints.delete) return Promise.reject(endpoints);
return api.delete(endpoints.delete.replaceAll(':id', entityId)).then((response) => {
// Get the entity before deleting, so we can reverse the action if the API request fails.
const entity = getState().entities[entityType]?.store[entityId];
// Optimistically delete the entity from the _store_ but keep the lists in tact.
dispatch(deleteEntities([entityId], entityType, { preserveLists: true }));
return api.delete(endpoints.delete.replaceAll(':id', entityId)).then((response) => {
// Success - finish deleting entity from the state.
dispatch(deleteEntities([entityId], entityType));
return {
response,
};
}).catch((e) => {
if (entity) {
// If the API failed, reimport the entity.
dispatch(importEntities([entity], entityType));
}
throw e;
});
}

View file

@ -10,6 +10,7 @@ import {
} from './actions';
import { createCache, createList, updateStore, updateList } from './utils';
import type { DeleteEntitiesOpts } from './actions';
import type { Entity, EntityCache, EntityListState } from './types';
enableMapSet();
@ -48,6 +49,7 @@ const deleteEntities = (
state: State,
entityType: string,
ids: Iterable<string>,
opts: DeleteEntitiesOpts,
) => {
return produce(state, draft => {
const cache = draft[entityType] ?? createCache();
@ -55,8 +57,10 @@ const deleteEntities = (
for (const id of ids) {
delete cache.store[id];
for (const list of Object.values(cache.lists)) {
list?.ids.delete(id);
if (!opts?.preserveLists) {
for (const list of Object.values(cache.lists)) {
list?.ids.delete(id);
}
}
}
@ -91,7 +95,7 @@ function reducer(state: Readonly<State> = {}, action: EntityAction): State {
case ENTITIES_IMPORT:
return importEntities(state, action.entityType, action.entities, action.listKey);
case ENTITIES_DELETE:
return deleteEntities(state, action.entityType, action.ids);
return deleteEntities(state, action.entityType, action.ids, action.opts);
case ENTITIES_FETCH_SUCCESS:
return importEntities(state, action.entityType, action.entities, action.listKey, action.newState);
case ENTITIES_FETCH_REQUEST: