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);
}