UNPKG

@snipsonian/observable-state

Version:

Observable-state snippets (redux-like)

136 lines (118 loc) 6.06 kB
import isFunction from '@snipsonian/core/src/is/isFunction'; import { Action, AnyAction, Dispatch, IActionableObservableStateStore, IObservableStateAction, Middleware, MiddlewareAPI, TFilterHookResult, } from './types'; import isObservableStateAction from './isObservableStateAction'; import { IObservableStateStore, ISetStateContext, ISetStateProps } from '../store/types'; /* eslint-disable max-len */ const WARN_MESSAGE = { FILTER_MAY_NOT_RETURN_OTHER_ACTION_TYPE: 'Observable state action: returning another action type from a filter hook is not supported!!!', }; export default function createObservableStateActionMiddleware< State, ExtraProcessInput, StateChangeNotificationKey>({ store, extraProcessInput = ({} as ExtraProcessInput), }: { store: IObservableStateStore<State, StateChangeNotificationKey>; extraProcessInput?: ExtraProcessInput; }): Middleware { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type const middleware: Middleware = ({ getState }: MiddlewareAPI<Dispatch<Action>, State>) => (next: Dispatch<Action>) => (action: Action) => { if (isObservableStateAction<string, object, State, ExtraProcessInput, StateChangeNotificationKey>(action)) { const filterHookResultingAction = executeFilterHook(action); if (filterHookResultingAction === false) { /* the incoming action is stopped/rejected */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return; } /** * To the next middleware in the pipeline, but at the moment this middleware * is the last one in the chain. (so it will actually call the dummy 'innerDispatch' * which returns the input action) */ const resultNextMiddleware = next(filterHookResultingAction); executeProcessHook(filterHookResultingAction); // eslint-disable-next-line consistent-return return resultNextMiddleware; } // eslint-disable-next-line consistent-return return next(action); function executeFilterHook( incomingAction: IObservableStateAction<string, object, State, ExtraProcessInput, StateChangeNotificationKey>, ): TFilterHookResult<IObservableStateAction<string, object, State, ExtraProcessInput, StateChangeNotificationKey>> { if (incomingAction.filter) { const filterResultingAction = incomingAction.filter({ action: incomingAction, getState, }); /** * Propagating another action type is not supported because, otherwise, we would have * to re-evaluate the action (type) again through the whole middleware chain, making * it a lot more complex (for a presumably very small use case). */ if (filterResultingAction && filterResultingAction.type !== incomingAction.type) { if (process.env.NODE_ENV !== 'test') { console.warn(WARN_MESSAGE.FILTER_MAY_NOT_RETURN_OTHER_ACTION_TYPE); } } else { return filterResultingAction; } } return incomingAction; } function executeProcessHook(filteredAction: IObservableStateAction<string, object, State, ExtraProcessInput, StateChangeNotificationKey>): void { /* the hook can return a promise, but here we're not really interested in the result of said promise as it will just (probably) call the setState directly on success and/or fail */ if (filteredAction.process) { filteredAction.process({ action: filteredAction, getState, /* a wrapped setState so that by default the action itself is passed as the context (if not specified) */ setState: ({ newState, notificationsToTrigger, context = getDefaultContext(filteredAction) }: ISetStateProps<State, StateChangeNotificationKey>) => { store.setState({ newState, notificationsToTrigger, context, }); }, /* the store.dispatch is created after creating this middleware, but is available at the moment that the actions get processed. */ dispatch: (store as IActionableObservableStateStore<State, StateChangeNotificationKey>).dispatch, ...extraProcessInput, }); } } }; return middleware; } /* eslint-enable max-len */ function getDefaultContext(action: AnyAction): ISetStateContext { const actionWithoutFunctions = Object.keys(action) .reduce( (accumulator, propKey) => { const propVal = action[propKey]; if (!isFunction(propVal)) { // eslint-disable-next-line no-param-reassign accumulator[propKey] = propVal; } return accumulator; }, {} as AnyAction, ); return { title: action.type, info: actionWithoutFunctions, }; }