UNPKG

@data-client/core

Version:

Async State Management without the Management. REST, GraphQL, SSE, Websockets, Fetch

152 lines (147 loc) 4.76 kB
import { normalize } from '@data-client/normalizr'; import { OPTIMISTIC } from '../../actionTypes.js'; import AbortOptimistic from '../../controller/AbortOptimistic.js'; import type Controller from '../../controller/Controller.js'; import type { State, SetResponseAction, OptimisticAction, } from '../../types.js'; export function setResponseReducer( state: State<unknown>, action: OptimisticAction | SetResponseAction, controller: Controller, ) { if (action.error) { return reduceError(state, action, action.response); } try { let response: any; // for true set's response is contained in action if (action.type === OPTIMISTIC) { // this should never happen /* istanbul ignore if */ if (!action.endpoint.getOptimisticResponse) return state; try { // compute optimistic response based on current state response = action.endpoint.getOptimisticResponse.call( action.endpoint, controller.snapshot(state, action.meta.fetchedAt), ...action.args, ); } catch (e: any) { // AbortOptimistic means 'do nothing', otherwise we count the exception as endpoint failure if (e.constructor === AbortOptimistic) { return state; } throw e; } } else { response = action.response; } const { result, entities, indexes, entityMeta } = normalize( action.endpoint.schema, response, action.args, state, action.meta, ); const endpoints: Record<string, unknown> = { ...state.endpoints, [action.key]: result, }; try { if (action.endpoint.update) { const updaters = action.endpoint.update(result, ...action.args); Object.keys(updaters).forEach(key => { endpoints[key] = updaters[key](endpoints[key]); }); } // no reason to completely fail because of user-code error // integrity of this state update is still guaranteed } catch (error) { console.error( `The following error occured during Endpoint.update() for ${action.key}`, ); console.error(error); } return { entities, endpoints, indexes, meta: { ...state.meta, [action.key]: { date: action.meta.date, fetchedAt: action.meta.fetchedAt, expiresAt: action.meta.expiresAt, prevExpiresAt: state.meta[action.key]?.expiresAt, }, }, entityMeta, optimistic: filterOptimistic(state, action), lastReset: state.lastReset, }; // reducer must update the state, so in case of processing errors we simply compute the endpoints inline } catch (error: any) { if (typeof error === 'object') { error.message = `Error processing ${ action.key }\n\nFull Schema: ${JSON.stringify( action.endpoint.schema, undefined, 2, )}\n\nError:\n${error.message}`; if ('response' in action) error.response = action.response; error.status = 400; } // this is not always bubbled up, so let's double sure this doesn't fail silently /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { console.error(error); } return reduceError(state, action, error); } } function reduceError( state: State<unknown>, action: SetResponseAction | OptimisticAction, error: any, ): State<unknown> { if (error.name === 'AbortError') { // In case we abort simply undo the optimistic update and act like no fetch even occured // We still want those watching promises from fetch directly to observed the abort, but we don't want to // Trigger errors in this case. This means theoretically improperly built abortes useResource() could suspend forever. return { ...state, optimistic: filterOptimistic(state, action), }; } return { ...state, meta: { ...state.meta, [action.key]: { date: action.meta.date, fetchedAt: action.meta.fetchedAt, expiresAt: action.meta.expiresAt, error, errorPolicy: action.endpoint.errorPolicy?.(error), }, }, optimistic: filterOptimistic(state, action), }; } /** Filter all requests with same serialization that did not start after the resolving request */ function filterOptimistic( state: State<unknown>, resolvingAction: SetResponseAction | OptimisticAction, ) { return state.optimistic.filter( optimisticAction => optimisticAction.key !== resolvingAction.key || (optimisticAction.type === OPTIMISTIC ? optimisticAction.meta.fetchedAt !== resolvingAction.meta.fetchedAt : optimisticAction.meta.date > resolvingAction.meta.date), ); }