All files action-map.ts

100% Statements 140/140
100% Branches 57/57
100% Functions 15/15
100% Lines 140/140

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 1951x 1x   1x 1x                                                           1x 123x 123x 123x 123x 123x   123x 123x 123x 123x 123x 123x 110x 293x 293x 110x 123x   123x 75x 2x 2x 2x 2x 2x 73x 75x   123x 53x 1x 1x 1x 1x 1x 52x 53x   123x 360x 3x 3x 3x 3x 360x   47x 2x 2x 2x 2x 2x   45x 47x 1x 1x 1x 1x 1x 47x 360x 360x   328x 3x 328x 325x 325x 360x 26x 26x 354x 360x   123x 193x 78x 78x 78x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 78x 193x   123x 130x 129x 129x 130x 130x   123x 47x 67x 67x 47x               123x 65x 65x 65x 64x 64x   65x 65x 4x 4x 65x 50x 50x 11x 65x   123x 6x 2x 2x 2x 2x 2x 4x 6x   123x 134x 134x   123x 69x 69x   123x 68x 68x   123x 137x 137x 123x   1x 94x 94x  
import { Action, ACTION_MODE_SINGLE, ActionCreator, ActionMode, ActionModeMulti, ActionModeSingle } from './action';
import { FlowfulError } from './error';
import { ActionHandler } from './action-handler';
import { getGlobalMode, MODE, MODE_DEV } from './mode';
import { devMessages } from './dev';
 
export interface ActionMapBasic {
  has(actionCreator: ActionCreator): boolean;
  add(...actions: Action[]): void;
  get<P, M extends ActionModeSingle, A extends Action<P, M>>(
    actionCreator: ActionCreator<P, M, any[], A>
  ): A | undefined;
  get<P, M extends ActionModeMulti, A extends Action<P, M>>(
    actionCreator: ActionCreator<P, M, any[], A>
  ): A[] | undefined;
  delete(actionCreator: ActionCreator): boolean;
  readonly size: number;
}
 
export interface ActionMapExtended extends ActionMapBasic {
  getActionCreators(): Iterable<ActionCreator>;
  getActions(): Iterable<Action>;
}
 
export interface ActionMapInternal extends ActionMapExtended {
  _attachHandler(handler: ActionHandler): void;
  _detachHandler(): void;
  _setIsHandlerExecuted(invoked: boolean): void;
}
 
export interface ActionMapOptions {
  mode?: MODE;
}
 
export class ActionMap implements ActionMapInternal {
  protected _map: Map<ActionCreator, Action[]>;
  protected _attachedHandler?: ActionHandler;
  protected _isHandlerExecuted?: boolean = false;
  protected _options: Required<ActionMapOptions>;
  protected _errorPrefix = '[Flowful][ActionMap]';
 
  constructor(actions?: Action[], options?: ActionMapOptions) {
    this._map = new Map<ActionCreator, Action[]>();
    this._options = {
      mode: options?.mode ?? getGlobalMode(),
    };
    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: Action) {
    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: ActionCreator) {
    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: ActionCreator) {
    if (this._options.mode === MODE_DEV) {
      this._warnAboutHandlerAction(actionCreator);
    }
    return this._map.has(actionCreator);
  }
 
  add(...actions: Action[]) {
    for (let i = 0, len = actions.length; i < len; i++) {
      this._add(actions[i]);
    }
  }
 
  get<P, M extends ActionModeSingle, A extends Action<P, M>>(
    actionCreator: ActionCreator<P, M, any[], A>
  ): A | undefined;
  get<P, M extends ActionModeMulti, A extends Action<P, M>>(
    actionCreator: ActionCreator<P, M, any[], A>
  ): A[] | undefined;
  get<P, M extends ActionMode, A extends Action<P, M>>(
    actionCreator: ActionCreator<P, M, any[], A>
  ): M extends ActionModeSingle ? A | undefined : A[] | undefined {
    if (this._options.mode === 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: ActionCreator) {
    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: ActionHandler) {
    this._attachedHandler = handler;
  }
 
  _detachHandler() {
    this._attachedHandler = undefined;
  }
 
  _setIsHandlerExecuted(triggered: boolean) {
    this._isHandlerExecuted = triggered;
  }
}
 
export function createActionMap(...actions: Action[]) {
  return new ActionMap(actions);
}