import { FlowfulError } from './error'; import { IActionHandler } from './action-handler'; import { IActionMapExtended, IActionMapInternal } from './action-map'; export type ChainedActionHandlerMiddlewareCallback = ( actionMap: AM, handler: IActionHandler ) => any; export type ActionHandlerMiddlewareCallback = ( next: ChainedActionHandlerMiddlewareCallback ) => ChainedActionHandlerMiddlewareCallback; export interface IActionHandlerMiddlewareRunner { run(actionMap: IActionMapExtended, handler: IActionHandler): any; } export interface IActionHandlerMiddleware extends IActionHandlerMiddlewareRunner { add(callback: ActionHandlerMiddlewareCallback): boolean; remove(callback: ActionHandlerMiddlewareCallback): boolean; compose(): void; } export interface ActionHandlerMiddlewareOptions { cancellable?: boolean; } export class ActionHandlerMiddleware implements IActionHandlerMiddlewareRunner { protected _middlewareCallbacks: Set>; protected _middlewareCallbacksArray: ActionHandlerMiddlewareCallback[]; protected _composed?: ChainedActionHandlerMiddlewareCallback; protected _last?: ChainedActionHandlerMiddlewareCallback; protected _className = 'ActionHandlerMiddleware'; protected _errorPrefix = `[Flowful][${this._className}]`; public options: Required; constructor(options?: ActionHandlerMiddlewareOptions) { this.options = { cancellable: options?.cancellable ?? true, }; this._middlewareCallbacks = new Set(); this._middlewareCallbacksArray = []; } compose() { let next = (actionMap: AM, handler: IActionHandler) => { return this._last!(actionMap, handler); }; for (const middleware of this._middlewareCallbacksArray) { next = middleware(next); } this._composed = next; } add(callback: ActionHandlerMiddlewareCallback) { if (!this._middlewareCallbacks.has(callback)) { this._middlewareCallbacks.add(callback); this._middlewareCallbacksArray.unshift(callback); this._composed = undefined; return true; } return false; } remove(callback: ActionHandlerMiddlewareCallback) { if (this._middlewareCallbacks.has(callback)) { this._middlewareCallbacks.delete(callback); this._middlewareCallbacksArray = [...this._middlewareCallbacks]; this._composed = undefined; return true; } return false; } run(actionMap: IActionMapExtended, handler: IActionHandler) { if (!this._middlewareCallbacks.size) { // size is slightly faster than array.length return handler.invoke(actionMap as IActionMapInternal); } let calledTimes = 0; this._last = (actionMapFromMiddlewares: IActionMapExtended, handlerFromMiddlewares: IActionHandler) => { 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 IActionMapInternal); }; 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; } }