From 1ab9b1d75c3c0d6e1bea646f5f2700dbc03dc26d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 Mar 2023 16:52:09 -0500 Subject: [PATCH] EntityStore: optimistic deletion --- app/soapbox/entity-store/actions.ts | 11 +++++++++-- .../entity-store/hooks/useEntityActions.ts | 16 ++++++++++++++-- app/soapbox/entity-store/reducer.ts | 10 +++++++--- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/app/soapbox/entity-store/actions.ts b/app/soapbox/entity-store/actions.ts index 4a01942b7..4d9355cd8 100644 --- a/app/soapbox/entity-store/actions.ts +++ b/app/soapbox/entity-store/actions.ts @@ -16,11 +16,16 @@ function importEntities(entities: Entity[], entityType: string, listKey?: string }; } -function deleteEntities(ids: Iterable, entityType: string) { +interface DeleteEntitiesOpts { + preserveLists?: boolean +} + +function deleteEntities(ids: Iterable, entityType: string, opts: DeleteEntitiesOpts = {}) { return { type: ENTITIES_DELETE, ids, entityType, + opts, }; } @@ -71,4 +76,6 @@ export { entitiesFetchSuccess, entitiesFetchFail, EntityAction, -}; \ No newline at end of file +}; + +export type { DeleteEntitiesOpts }; \ No newline at end of file diff --git a/app/soapbox/entity-store/hooks/useEntityActions.ts b/app/soapbox/entity-store/hooks/useEntityActions.ts index ab2007a9c..2b307afde 100644 --- a/app/soapbox/entity-store/hooks/useEntityActions.ts +++ b/app/soapbox/entity-store/hooks/useEntityActions.ts @@ -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( ) { const api = useApi(); const dispatch = useAppDispatch(); + const getState = useGetState(); const [entityType, listKey] = path; function createEntity(params: P): Promise> { @@ -56,13 +57,24 @@ function useEntityActions( function deleteEntity(entityId: string): Promise { 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; }); } diff --git a/app/soapbox/entity-store/reducer.ts b/app/soapbox/entity-store/reducer.ts index a7f82e6e6..891e42f4c 100644 --- a/app/soapbox/entity-store/reducer.ts +++ b/app/soapbox/entity-store/reducer.ts @@ -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, + 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 = {}, 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: