All files action-handler-middleware.ts

100% Statements 82/82
100% Branches 26/26
100% Functions 8/8
100% Lines 82/82

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 1191x                                                       1x   36x 36x 36x 36x 36x 36x 36x 36x   36x 36x 36x 36x 36x 36x 36x   36x 20x 33x 33x 20x 24x 24x 20x 20x   36x 26x 25x 25x 25x 25x 25x 1x 26x   36x 4x 3x 3x 3x 3x 3x 1x 4x   36x 40x   5x 5x 35x 35x 33x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 31x 33x   1x 1x 1x 1x 30x 33x 35x 17x 17x 35x 40x   1x 1x 1x 1x 31x 40x 36x  
import { FlowfulError } from './error';
import { ActionHandler } from './action-handler';
import { ActionMapExtended, ActionMapInternal } from './action-map';
 
export type ChainedActionHandlerMiddlewareCallback<AM extends ActionMapExtended = ActionMapExtended> = (
  actionMap: AM,
  handler: ActionHandler
) => any;
 
export type ActionHandlerMiddlewareCallback<AM extends ActionMapExtended = ActionMapExtended> = (
  next: ChainedActionHandlerMiddlewareCallback<AM>
) => ChainedActionHandlerMiddlewareCallback<AM>;
 
export interface IActionHandlerMiddlewareRunner {
  run(actionMap: ActionMapExtended, handler: ActionHandler): any;
}
 
export interface IActionHandlerMiddleware<AM extends ActionMapExtended = ActionMapExtended>
  extends IActionHandlerMiddlewareRunner {
  add(callback: ActionHandlerMiddlewareCallback<AM>): boolean;
  remove(callback: ActionHandlerMiddlewareCallback<AM>): boolean;
  compose(): void;
}
 
export interface ActionHandlerMiddlewareOptions {
  cancellable?: boolean;
}
 
export class ActionHandlerMiddleware<AM extends ActionMapExtended = ActionMapExtended>
  implements IActionHandlerMiddlewareRunner
{
  protected _middlewareCallbacks: Set<ActionHandlerMiddlewareCallback<AM>>;
  protected _middlewareCallbacksArray: ActionHandlerMiddlewareCallback<AM>[];
  protected _composed?: ChainedActionHandlerMiddlewareCallback<AM>;
  protected _last?: ChainedActionHandlerMiddlewareCallback<AM>;
  protected _className = 'ActionHandlerMiddleware';
  protected _errorPrefix = `[Flowful][${this._className}]`;
  public options: Required<ActionHandlerMiddlewareOptions>;
 
  constructor(options?: ActionHandlerMiddlewareOptions) {
    this.options = {
      cancellable: options?.cancellable ?? true,
    };
    this._middlewareCallbacks = new Set();
    this._middlewareCallbacksArray = [];
  }
 
  compose() {
    let next = (actionMap: AM, handler: ActionHandler) => {
      return this._last!(actionMap, handler);
    };
    for (const middleware of this._middlewareCallbacksArray) {
      next = middleware(next);
    }
    this._composed = next;
  }
 
  add(callback: ActionHandlerMiddlewareCallback<AM>) {
    if (!this._middlewareCallbacks.has(callback)) {
      this._middlewareCallbacks.add(callback);
      this._middlewareCallbacksArray.unshift(callback);
      this._composed = undefined;
      return true;
    }
    return false;
  }
 
  remove(callback: ActionHandlerMiddlewareCallback<AM>) {
    if (this._middlewareCallbacks.has(callback)) {
      this._middlewareCallbacks.delete(callback);
      this._middlewareCallbacksArray = [...this._middlewareCallbacks];
      this._composed = undefined;
      return true;
    }
    return false;
  }
 
  run(actionMap: ActionMapExtended, handler: ActionHandler) {
    if (!this._middlewareCallbacks.size) {
      // size is slightly faster than array.length
      return handler.invoke(actionMap as ActionMapInternal);
    }
    let calledTimes = 0;
    this._last = (actionMapFromMiddlewares: ActionMapExtended, handlerFromMiddlewares: ActionHandler) => {
      if (!this.options.cancellable) {
        if (actionMapFromMiddlewares !== actionMap) {
          throw new FlowfulError(
            `${this._errorPrefix} The next function in the action handler middleware was called with a different ActionMap, which is not possible when cancellable option is set to false.`
          );
        }
        if (handlerFromMiddlewares !== handler) {
          throw new FlowfulError(
            `${this._errorPrefix} The next function in the action handler middleware was called with a different ActionHandler, which is not possible when cancellable option is set to false.`
          );
        }
      }
      calledTimes++;
      if (calledTimes > 1) {
        // We have to check it here, otherwise it will throw an error that the action was emitted several times
        throw new FlowfulError(
          `${this._errorPrefix} The next function in the action handler middleware was called more than once.`
        );
      }
      return handlerFromMiddlewares.invoke(actionMapFromMiddlewares as ActionMapInternal);
    };
    if (!this._composed) {
      this.compose();
    }
    const middlewareOutput = this._composed!(actionMap as unknown as AM, handler);
    if (!this.options.cancellable && calledTimes === 0) {
      // the middleware should not be able to cancel the action
      throw new FlowfulError(
        `${this._errorPrefix} The next function in the action handler middleware was not called, which is not possible when cancellable option is set to false.`
      );
    }
    return middlewareOutput;
  }
}