EntityStore: optimistic deletion
This commit is contained in:
parent
709edaefad
commit
1ab9b1d75c
3 changed files with 30 additions and 7 deletions
|
@ -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 {
|
return {
|
||||||
type: ENTITIES_DELETE,
|
type: ENTITIES_DELETE,
|
||||||
ids,
|
ids,
|
||||||
entityType,
|
entityType,
|
||||||
|
opts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,4 +76,6 @@ export {
|
||||||
entitiesFetchSuccess,
|
entitiesFetchSuccess,
|
||||||
entitiesFetchFail,
|
entitiesFetchFail,
|
||||||
EntityAction,
|
EntityAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type { DeleteEntitiesOpts };
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useApi, useAppDispatch } from 'soapbox/hooks';
|
import { useApi, useAppDispatch, useGetState } from 'soapbox/hooks';
|
||||||
|
|
||||||
import { deleteEntities, importEntities } from '../actions';
|
import { deleteEntities, importEntities } from '../actions';
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ function useEntityActions<TEntity extends Entity = Entity, P = any>(
|
||||||
) {
|
) {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const getState = useGetState();
|
||||||
const [entityType, listKey] = path;
|
const [entityType, listKey] = path;
|
||||||
|
|
||||||
function createEntity(params: P): Promise<CreateEntityResult<TEntity>> {
|
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> {
|
function deleteEntity(entityId: string): Promise<DeleteEntityResult> {
|
||||||
if (!endpoints.delete) return Promise.reject(endpoints);
|
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));
|
dispatch(deleteEntities([entityId], entityType));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
response,
|
response,
|
||||||
};
|
};
|
||||||
|
}).catch((e) => {
|
||||||
|
if (entity) {
|
||||||
|
// If the API failed, reimport the entity.
|
||||||
|
dispatch(importEntities([entity], entityType));
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import { createCache, createList, updateStore, updateList } from './utils';
|
import { createCache, createList, updateStore, updateList } from './utils';
|
||||||
|
|
||||||
|
import type { DeleteEntitiesOpts } from './actions';
|
||||||
import type { Entity, EntityCache, EntityListState } from './types';
|
import type { Entity, EntityCache, EntityListState } from './types';
|
||||||
|
|
||||||
enableMapSet();
|
enableMapSet();
|
||||||
|
@ -48,6 +49,7 @@ const deleteEntities = (
|
||||||
state: State,
|
state: State,
|
||||||
entityType: string,
|
entityType: string,
|
||||||
ids: Iterable<string>,
|
ids: Iterable<string>,
|
||||||
|
opts: DeleteEntitiesOpts,
|
||||||
) => {
|
) => {
|
||||||
return produce(state, draft => {
|
return produce(state, draft => {
|
||||||
const cache = draft[entityType] ?? createCache();
|
const cache = draft[entityType] ?? createCache();
|
||||||
|
@ -55,8 +57,10 @@ const deleteEntities = (
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
delete cache.store[id];
|
delete cache.store[id];
|
||||||
|
|
||||||
for (const list of Object.values(cache.lists)) {
|
if (!opts?.preserveLists) {
|
||||||
list?.ids.delete(id);
|
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:
|
case ENTITIES_IMPORT:
|
||||||
return importEntities(state, action.entityType, action.entities, action.listKey);
|
return importEntities(state, action.entityType, action.entities, action.listKey);
|
||||||
case ENTITIES_DELETE:
|
case ENTITIES_DELETE:
|
||||||
return deleteEntities(state, action.entityType, action.ids);
|
return deleteEntities(state, action.entityType, action.ids, action.opts);
|
||||||
case ENTITIES_FETCH_SUCCESS:
|
case ENTITIES_FETCH_SUCCESS:
|
||||||
return importEntities(state, action.entityType, action.entities, action.listKey, action.newState);
|
return importEntities(state, action.entityType, action.entities, action.listKey, action.newState);
|
||||||
case ENTITIES_FETCH_REQUEST:
|
case ENTITIES_FETCH_REQUEST:
|
||||||
|
|
Loading…
Reference in a new issue