import { FlowfulError } from './error'; import { IActionMapExtended } from './action-map'; export type ChainedDispatchMiddlewareCallback = ( actionMap: AM ) => any; export interface IDispatchMiddlewareRunner { run(actionMap: AM, execute: ChainedDispatchMiddlewareCallback): any; } export interface IDispatchMiddleware extends IDispatchMiddlewareRunner { add(callback: DispatchMiddlewareCallback): boolean; remove(callback: DispatchMiddlewareCallback): boolean; compose(): void; } export interface IDispatchMiddlewareOptions { cancellable?: boolean; } export type DispatchMiddlewareCallback = ( next: ChainedDispatchMiddlewareCallback ) => ChainedDispatchMiddlewareCallback; export class DispatchMiddleware implements IDispatchMiddleware { protected _middlewareCallbacks: Set>; protected _middlewareCallbacksArray: DispatchMiddlewareCallback[]; protected _composed?: ChainedDispatchMiddlewareCallback; protected _last?: ChainedDispatchMiddlewareCallback; protected _className = 'DispatchMiddleware'; protected _errorPrefix = `[Flowful][${this._className}]`; public options: Required; constructor(options?: IDispatchMiddlewareOptions) { this.options = { cancellable: options?.cancellable ?? true, }; this._middlewareCallbacks = new Set(); this._middlewareCallbacksArray = []; } compose() { let next = (actionMap: AM) => { return this._last!(actionMap); }; for (const middleware of this._middlewareCallbacksArray) { next = middleware(next); } this._composed = next; } add(callback: DispatchMiddlewareCallback) { if (!this._middlewareCallbacks.has(callback)) { this._middlewareCallbacks.add(callback); this._middlewareCallbacksArray.unshift(callback); this._composed = undefined; return true; } return false; } remove(callback: DispatchMiddlewareCallback) { if (this._middlewareCallbacks.has(callback)) { this._middlewareCallbacks.delete(callback); this._middlewareCallbacksArray = [...this._middlewareCallbacks]; this._composed = undefined; return true; } return false; } run(actionMap: IActionMapExtended, execute: ChainedDispatchMiddlewareCallback) { if (!this._middlewareCallbacks.size) { // size is slightly faster than array.length return execute(actionMap as unknown as AM); } let calledTimes = 0; this._last = (actionMapFromMiddlewares: AM) => { if (!this.options.cancellable && actionMapFromMiddlewares !== actionMap) { throw new FlowfulError( `${this._errorPrefix} The next function in the dispatch middleware was called with a different ActionMap, 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 solo event was emitted several times throw new FlowfulError( `${this._errorPrefix} The next function in the dispatch middleware was called more than once.` ); } return execute(actionMapFromMiddlewares); }; if (!this._composed) { this.compose(); } const middlewareOutput = this._composed!(actionMap as unknown as AM); if (!this.options.cancellable && calledTimes === 0) { // the middleware should not be able to cancel the event throw new FlowfulError( `${this._errorPrefix} The next function in the dispatch middleware was not called, which is not possible when cancellable option is set to false.` ); } return middlewareOutput; } }