import { IAction, ACTION_MODE_SINGLE, IActionCreator, ActionMode, ActionModeMulti, ActionModeSingle } from './action'; import { FlowfulError } from './error'; import { IActionHandler } from './action-handler'; import { getGlobalMode, MODE, MODE_DEV } from './mode'; import { devMessages } from './dev'; export interface IActionMapBasic { has(actionCreator: IActionCreator): boolean; add(...actions: IAction[]): void; get>( actionCreator: IActionCreator ): A | undefined; get>( actionCreator: IActionCreator ): A[] | undefined; delete(actionCreator: IActionCreator): boolean; readonly size: number; } export interface IActionMapExtended extends IActionMapBasic { getActionCreators(): Iterable; getActions(): Iterable; } export interface IActionMapInternal extends IActionMapExtended { _attachHandler(handler: IActionHandler): void; _detachHandler(): void; _setIsHandlerExecuted(invoked: boolean): void; } export class ActionMap implements IActionMapInternal { protected _map: Map; protected _attachedHandler?: IActionHandler; protected _isHandlerExecuted?: boolean = false; protected _errorPrefix = '[Flowful][ActionMap]'; constructor(actions?: IAction[]) { this._map = new Map(); if (actions) { for (let i = 0, len = actions.length; i < len; i++) { this._add(actions[i]); } } } getActionCreators() { if (this._isHandlerExecuted) { throw new FlowfulError( `${this._errorPrefix} The handler is trying to obtain action creators from the action map. This is not allowed. Use the middleware.`, { handler: this._attachedHandler } ); } return this._map.keys(); } getActions() { if (this._isHandlerExecuted) { throw new FlowfulError( `${this._errorPrefix} The action handler is trying to obtain actions from the action map. This is not allowed. Use the middleware instead.`, { handler: this._attachedHandler } ); } return [...this._map.values()].flat(); } protected _add(action: IAction) { if (!action.creator) { throw new FlowfulError(`${this._errorPrefix} Invalid action - action creator is missing.`, { action, }); } if (this._attachedHandler) { // middlewares can do anything but handlers should not if (this._map.has(action.creator) && this._isHandlerExecuted && action.mode === ACTION_MODE_SINGLE) { throw new FlowfulError( `${this._errorPrefix} Action "${action.type}" already exists in ActionMap. To dispatch the same action multiple times in a single cycle, use a action of type "multi" (e.g., actionCreatorMulti).`, { action: action, actionMap: this } ); } // the attached action handler is here because we want to know exactly where the error was raised const handlerOutputSet = this._attachedHandler.getOutputSet(); if (!handlerOutputSet || !handlerOutputSet.has(action.creator)) { throw new FlowfulError( `${this._errorPrefix} The action handler (or action handler middleware) should not add actions that are not defined in the action handler outputs.`, { handler: this._attachedHandler, action: action } ); } } const currentActions = this._map.get(action.creator) || []; if (action.mode === ACTION_MODE_SINGLE) { // overwrite the action if it is a single action if (currentActions.length) { currentActions[0] = action; } else { currentActions.push(action); } } else { currentActions.push(action); } this._map.set(action.creator, currentActions); } protected _warnAboutHandlerAction(actionCreator: IActionCreator) { if (this._attachedHandler && this._isHandlerExecuted) { const handlerInputSet = this._attachedHandler.getInputSet(); const handlerOutputSet = this._attachedHandler.getOutputSet(); if (!handlerInputSet.has(actionCreator) && (!handlerOutputSet || !handlerOutputSet.has(actionCreator))) { devMessages.warn( `${this._errorPrefix} The action handler is trying to obtain action${ actionCreator.type ? ` "${actionCreator.type}"` : '' } that is not defined in either the inputs or outputs.`, { handler: this._attachedHandler, actionCreator, } ); } } } has(actionCreator: IActionCreator) { if (getGlobalMode() === MODE_DEV) { this._warnAboutHandlerAction(actionCreator); } return this._map.has(actionCreator); } add(...actions: IAction[]) { for (let i = 0, len = actions.length; i < len; i++) { this._add(actions[i]); } } get>( actionCreator: IActionCreator ): A | undefined; get>( actionCreator: IActionCreator ): A[] | undefined; get>( actionCreator: IActionCreator ): M extends ActionModeSingle ? A | undefined : A[] | undefined { if (getGlobalMode() === MODE_DEV) { this._warnAboutHandlerAction(actionCreator); } const result = this._map.get(actionCreator); if (!result) { return undefined; } if (actionCreator.mode === ACTION_MODE_SINGLE) { return result[0] as A as M extends ActionModeSingle ? A | undefined : A[] | undefined; } return result as A[] as M extends ActionModeSingle ? A | undefined : A[] | undefined; } delete(actionCreator: IActionCreator) { if (this._attachedHandler && this._isHandlerExecuted) { throw new FlowfulError( `${this._errorPrefix} Deleting actions from the action handler is not allowed. Use middleware or delete actions before dispatch.`, { actionCreator: actionCreator, actionMap: this } ); } return this._map.delete(actionCreator); } get size() { return this._map.size; } _attachHandler(handler: IActionHandler) { this._attachedHandler = handler; } _detachHandler() { this._attachedHandler = undefined; } _setIsHandlerExecuted(triggered: boolean) { this._isHandlerExecuted = triggered; } } export function createActionMap(...actions: IAction[]) { return new ActionMap(actions); }