import { IActionHandler } from './action-handler'; import { IActionCreator } from './action'; import { IActionMapExtended, IActionMapInternal } from './action-map'; import { FlowfulError } from './error'; import { IActionHandlerMiddlewareRunner } from './action-handler-middleware'; import { IDispatchMiddlewareRunner } from './dispatch-middleware'; import { checkMode, getGlobalMode, MODE, MODE_DEV } from './mode'; export interface IFlowfulOptions { dispatchMiddleware?: IDispatchMiddlewareRunner; handlerMiddleware?: IActionHandlerMiddlewareRunner; } export interface IFlowful { hasHandler(handler: IActionHandler): boolean; addHandler(handler: IActionHandler): boolean; addHandlers(handlers: IActionHandler[]): boolean; removeHandler(handler: IActionHandler): boolean; removeHandlers(handlers: IActionHandler[]): boolean; dispatchActionMap(actionMap: IActionMapExtended): unknown; } export class Flowful implements IFlowful { protected _className = 'Flowful'; protected _handlers: Set; protected _handlersByInput: Map>; protected _dispatchMiddleware?: IDispatchMiddlewareRunner; protected _handlerMiddleware?: IActionHandlerMiddlewareRunner; constructor(options?: IFlowfulOptions) { this._handlers = new Set(); this._handlersByInput = new Map(); this._dispatchMiddleware = options?.dispatchMiddleware; this._handlerMiddleware = options?.handlerMiddleware; } hasHandler(handler: IActionHandler) { return this._handlers.has(handler); } protected _addHandlerInputs(handler: IActionHandler) { const inputs = handler.getInputs(); for (const input of inputs) { if (!this._handlersByInput.has(input)) { this._handlersByInput.set(input, new Set()); } this._handlersByInput.get(input)!.add(handler); } } protected _addHandlerOutputs(handler: IActionHandler) { // placeholder for extensions } protected _addHandlerActions(handler: IActionHandler) { this._addHandlerInputs(handler); this._addHandlerOutputs(handler); } addHandler(handler: IActionHandler) { if (!handler || !handler.invoke) { throw new FlowfulError(`[${this._className}][addHandler/s] Invalid action handler.`, handler); } if (this._handlers.has(handler)) { return false; } this._addHandlerActions(handler); this._handlers.add(handler); if (getGlobalMode() === MODE_DEV) { this._checkCircular(handler); } return true; } addHandlers(handlers: IActionHandler[]) { if (!Array.isArray(handlers)) { throw new FlowfulError( `[${this._className}][addHandlers] Invalid function argument. Action handlers should be an array.`, handlers ); } const len = handlers.length; if (!len) return true; let result = true; for (let i = 0; i < len; i++) { const handler = handlers[i]; result = this.addHandler(handler) && result; } return result; } protected _removeHandlerInputs(handler: IActionHandler) { const inputs = handler.getInputs(); for (let idx = 0, len = inputs.length; idx < len; idx++) { const ActionCreator = inputs[idx]; const current = this._handlersByInput.get(ActionCreator)!; current.delete(handler); if (current.size === 0) { this._handlersByInput.delete(ActionCreator); } } } protected _removeHandlerOutputs(handler: IActionHandler) { // placeholder for extensions } protected _removeHandlerActions(handler: IActionHandler) { this._removeHandlerInputs(handler); this._removeHandlerOutputs(handler); } removeHandler(handler: IActionHandler) { if (!this._handlers.has(handler)) { return false; } this._removeHandlerActions(handler); this._handlers.delete(handler); return true; } removeHandlers(handlers: IActionHandler[]) { if (!Array.isArray(handlers)) { throw new FlowfulError( `[${this._className}][removeHandlers] Invalid function argument. Action handlers should be an array.`, handlers ); } const handlersLen = handlers.length; if (!handlersLen) return true; let result = true; for (let handlerIndex = 0; handlerIndex < handlersLen; handlerIndex++) { const handler = handlers[handlerIndex]; result = this.removeHandler(handler) && result; } return result; } protected _isHandlerInterested(handler: IActionHandler, actionMap: IActionMapInternal) { const handlerInputSet = handler.getInputSet(); const handlerInputSize = handlerInputSet.size; // we want as few lookups as possible if (handlerInputSize <= actionMap.size) { const handlerInputArr = handler.getInputs(); for (let i = 0; i < handlerInputSize; i++) { if (actionMap.has(handlerInputArr[i])) { return true; } } } else { const allActionCreators = actionMap.getActionCreators(); const handlerInputSet = handler.getInputSet(); for (const actionCreator of allActionCreators) { if (handlerInputSet.has(actionCreator)) { return true; } } } return false; } protected _throwCircular(handler: IActionHandler, stack: Set) { let _stack = [...stack]; const index = _stack.indexOf(handler); _stack = _stack.slice(index); _stack.push(handler); throw new FlowfulError(`[${this._className}] Circular dependency detected.`, { handler, stack: _stack, }); } protected _checkCircular( handler: IActionHandler, visited: WeakSet = new WeakSet(), stack: Set = new Set() ) { stack.add(handler); visited.add(handler); const outputs = handler.getOutputs(); if (outputs) { const size = outputs.length; for (let idx = 0; idx < size; idx++) { const Output = outputs[idx]; const handlersByInput = this._handlersByInput.get(Output); if (handlersByInput) { for (const nextHandler of handlersByInput) { if (!visited.has(nextHandler)) { this._checkCircular(nextHandler, visited, stack); } if (stack.has(nextHandler)) { this._throwCircular(nextHandler, stack); } } } } } stack.delete(handler); } protected _collectDependentHandlers( handler: IActionHandler, handlers: IActionHandler[], visited: WeakSet ) { if (visited.has(handler)) { return; } visited.add(handler); const outputs = handler.getOutputs(); if (outputs) { const size = outputs.length; for (let idx = 0; idx < size; idx++) { const Output = outputs[idx]; const handlersByInput = this._handlersByInput.get(Output); if (handlersByInput) { for (const nextHandler of handlersByInput) { if (visited.has(nextHandler)) { continue; } this._collectDependentHandlers(nextHandler, handlers, visited); } } } } handlers.push(handler); } protected _collectHandlers(actionMap: IActionMapInternal): IActionHandler[] { const directHandlers = this._findDirectHandlers(actionMap); if (!directHandlers.size) { return []; } const visited: WeakSet = new WeakSet(); const handlers: IActionHandler[] = []; for (const handler of directHandlers) { this._collectDependentHandlers(handler, handlers, visited); } handlers.reverse(); return handlers; } protected _findDirectHandlers(actionMap: IActionMapInternal): Set { const handlers: Set = new Set(); for (const actionCreator of actionMap.getActionCreators()) { const current = this._handlersByInput.get(actionCreator); if (current) { for (const handler of current) { handlers.add(handler); } } } return handlers; } protected _performDispatch(actionMap: IActionMapInternal) { if (actionMap.size === 0) { return; } const handlers = this._collectHandlers(actionMap); for (let handlerIndex = 0, len = handlers.length; handlerIndex < len; handlerIndex++) { const handler = handlers[handlerIndex]; if (!this._isHandlerInterested(handler, actionMap)) { continue; } actionMap._attachHandler(handler); if (this._handlerMiddleware) { this._handlerMiddleware.run(actionMap, handler); } else { handler.invoke(actionMap); } actionMap._detachHandler(); } // actionMap shouldn't be returned because we can't rely on it // the middlewares might dispatch something and won't know which actionMap to return // middlewares may return something else than actionMap too } dispatchActionMap(actionMap: IActionMapExtended) { if (this._dispatchMiddleware) { return this._dispatchMiddleware.run(actionMap, (actionMap) => { return this._performDispatch(actionMap as IActionMapInternal); }); } return this._performDispatch(actionMap as unknown as IActionMapInternal); } }