import { bench, describe, afterAll } from 'vitest'; import { createHandler, IActionCreator, ActionHandler, createActionMap, IActionMapExtended, IAction, actionCreatorMulti, IActionMapInternal, } from '../src'; import { skipDevMessages } from '../src/dev'; skipDevMessages(true); import { sortUp } from '../src/graph'; import { FlowfulError } from '../src/error'; import { IActionHandlerMiddleware } from '../src/action-handler-middleware'; import { IDispatchMiddleware } from '../src/dispatch-middleware'; import { getGlobalMode, MODE, MODE_DEV } from '../src/mode'; import { devMessages } from '../src/dev'; export interface FlowfulOptions { mode?: MODE; dispatchMiddleware?: IDispatchMiddleware; handlerMiddleware?: IActionHandlerMiddleware; } export class Flowful { protected _className = 'Flowful'; protected _sorted: boolean; protected _sortedHandlers: ActionHandler[]; protected _handlers: Set; protected _handlersByOutput: Map>; protected _dispatchMiddleware?: IDispatchMiddleware; protected _handlerMiddleware?: IActionHandlerMiddleware; protected _mode: MODE; constructor(options?: FlowfulOptions) { this._sorted = true; this._sortedHandlers = []; this._handlers = new Set(); this._handlersByOutput = new Map(); this._dispatchMiddleware = options?.dispatchMiddleware; this._handlerMiddleware = options?.handlerMiddleware; this._mode = options?.mode ?? getGlobalMode(); } get mode() { return this._mode; } hasHandler(handler: ActionHandler) { return this._handlers.has(handler); } addOneHandler(handler: ActionHandler) { return this.addManyHandlers([handler]); } addHandler(...handlers: ActionHandler[]) { return this.addManyHandlers(handlers); } protected _addHandlerInputs(handler: ActionHandler) { // placeholder for extensions } protected _addHandlerOutputs(handler: ActionHandler) { const outputs = handler.getOutputs(); if (outputs) { for (let idx = 0, signalsLen = outputs.length; idx < signalsLen; idx++) { const ActionCreator = outputs[idx]; const current = this._handlersByOutput.get(ActionCreator); if (!current) { this._handlersByOutput.set(ActionCreator, new Set([handler])); } else { current.add(handler); } } } } protected _addHandlerEvents(handler: ActionHandler) { this._addHandlerInputs(handler); this._addHandlerOutputs(handler); } protected _addHandler(handler: ActionHandler) { if (!handler) { throw new FlowfulError(`[${this._className}] Invalid signal handler.`, handler); } if (this._handlers.has(handler)) { return false; } this._addHandlerEvents(handler); this._handlers.add(handler); return true; } addManyHandlers(handlers: ActionHandler[]) { if (!Array.isArray(handlers)) { throw new FlowfulError( `[${this._className}][addManyHandlers] Invalid function argument. Signal 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 = result && this._addHandler(handler); } this._sorted = false; return result; } removeOneHandler(handler: ActionHandler) { return this.removeManyHandlers([handler]); } removeHandler(...handlers: ActionHandler[]) { return this.removeManyHandlers(handlers); } protected _removeHandlerInputs(handler: ActionHandler) { // placeholder for extensions } protected _removeHandlerOutputs(handler: ActionHandler) { const outputs = handler.getOutputs(); if (outputs) { for (let idx = 0, outputLen = outputs.length; idx < outputLen; idx++) { const ActionCreator = outputs[idx]; const current = this._handlersByOutput.get(ActionCreator)!; current.delete(handler); if (current.size === 0) { this._handlersByOutput.delete(ActionCreator); } } } } protected _removeHandlerEvents(handler: ActionHandler) { this._removeHandlerInputs(handler); this._removeHandlerOutputs(handler); } protected _removeHandler(handler: ActionHandler) { if (!this._handlers.has(handler)) { return false; } this._removeHandlerEvents(handler); this._handlers.delete(handler); return true; } removeManyHandlers(handlers: ActionHandler[]) { if (!Array.isArray(handlers)) { throw new FlowfulError( `[${this._className}][removeManyHandlers] Invalid function argument. Signal 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 = result && this._removeHandler(handler); } this._sorted = false; return result; } sort() { if (this._sorted) { if (this._mode === MODE_DEV) { devMessages.notice(`[${this._className}][sort] Signal handlers are already sorted.`); } return; } this._sortedHandlers = sortUp([...this._handlers], this._handlersByOutput); this._sorted = true; } protected _handlerIsInterested(handler: ActionHandler, actionMap: IActionMapExtended) { 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 signalCreator of allActionCreators) { if (handlerInputSet.has(signalCreator)) { return true; } } } return false; } protected _performDispatch(actionMap: IActionMapInternal) { if (actionMap.size === 0) { return; } const sortedHandlers = this._sortedHandlers; for (let handlerIndex = 0, len = sortedHandlers.length; handlerIndex < len; handlerIndex++) { const handler = sortedHandlers[handlerIndex]; if (!this._handlerIsInterested(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 } dispatch(actionMap: IActionMapExtended) { if (!this._sorted) { if (this._mode === MODE_DEV) { devMessages.notice( `[${this._className}][dispatch] Signal handlers were not sorted. After adding or removing signal handlers, use the "sort" method (first add/remove all handlers and sort at the very end).` ); } this.sort(); } if (this._dispatchMiddleware) { return this._dispatchMiddleware.run(actionMap, (actionMap) => { return this._performDispatch(actionMap as IActionMapInternal); }); } return this._performDispatch(actionMap as IActionMapInternal); } } let currentLongest = 0; const longest = [0, 0, 0, 0, 0, 0]; class Dyn extends Flowful { protected _handlersByInput: Map> = new Map(); protected _addHandlerInputs(handler: ActionHandler) { 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 _getDirectHandlersForSignalMap(actionMap: IActionMapExtended): Set { const handlers: Set = new Set(); for (const signalCreator of actionMap.getActionCreators()) { const current = this._handlersByInput.get(signalCreator); if (current) { for (const handler of current) { handlers.add(handler); } } } return handlers; } protected _performDispatch(actionMap: IActionMapInternal): void { if (actionMap.size === 0) { return; } const directHandlers = this._getDirectHandlersForSignalMap(actionMap); const visited: Set = new Set(); const stack: Set = new Set(); const handlers: ActionHandler[] = []; const goDown = (handler: ActionHandler) => { if (stack.has(handler)) { let _stack = [...stack]; const index = _stack.indexOf(handler); _stack = _stack.slice(index); _stack.push(handler); throw new FlowfulError(`[${this._className}][dispatch] Circular dependency detected.`, { handler, stack: _stack, }); } stack.add(handler); 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) { goDown(nextHandler); } } } } handlers.push(handler); stack.delete(handler); }; for (const handler of directHandlers) { goDown(handler); } handlers.reverse(); longest[currentLongest] = Math.max(longest[currentLongest], handlers.length); for (let handlerIndex = 0, len = handlers.length; handlerIndex < len; handlerIndex++) { const handler = handlers[handlerIndex]; if (!this._handlerIsInterested(handler, actionMap)) { continue; } actionMap._attachHandler(handler); if (this._handlerMiddleware) { this._handlerMiddleware.run(actionMap, handler); } else { handler.invoke(actionMap); } actionMap._detachHandler(); } } } class Dyn2 extends Flowful { protected _handlersByInput: Map> = new Map(); protected _addHandlerInputs(handler: ActionHandler) { 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 _getDirectHandlersForActionMap(actionMap: IActionMapExtended): Set { const handlers: Set = new Set(); for (const signalCreator of actionMap.getActionCreators()) { const current = this._handlersByInput.get(signalCreator); if (current) { for (const handler of current) { handlers.add(handler); } } } return handlers; } protected goDown( handler: ActionHandler, visited: WeakSet, stack: Set, handlers: ActionHandler[] ) { if (stack.has(handler)) { let _stack = [...stack]; const index = _stack.indexOf(handler); _stack = _stack.slice(index); _stack.push(handler); throw new FlowfulError(`[${this._className}][dispatch] Circular dependency detected.`, { handler, stack: _stack, }); } stack.add(handler); 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) { this.goDown(nextHandler, visited, stack, handlers); } } } } handlers.push(handler); stack.delete(handler); } protected _performDispatch(actionMap: IActionMapInternal): void { if (actionMap.size === 0) { return; } const directHandlers = this._getDirectHandlersForActionMap(actionMap); const visited: WeakSet = new WeakSet(); const stack: Set = new Set(); const handlers: ActionHandler[] = []; for (const handler of directHandlers) { this.goDown(handler, visited, stack, handlers); } handlers.reverse(); for (let handlerIndex = 0, len = handlers.length; handlerIndex < len; handlerIndex++) { const handler = handlers[handlerIndex]; if (!this._handlerIsInterested(handler, actionMap)) { continue; } actionMap._attachHandler(handler); if (this._handlerMiddleware) { this._handlerMiddleware.run(actionMap, handler); } else { handler.invoke(actionMap); } actionMap._detachHandler(); } } } const allSignals: IActionCreator[] = []; function getRandom(count: number) { return Math.floor(Math.random() * count); } function getRandomSignals(count: number) { const signals: IActionCreator[] = []; for (let i = 0; i < count; i++) { const signal = allSignals[getRandom(allSignals.length)]; signals.push(signal); } return signals; } for (let i = 0; i < 1000; i++) { const signal = actionCreatorMulti(`signal${i}`); allSignals.push(signal); } const flowfulAcyclicTest = new Flowful(); function _createHandler(allHandlers: ActionHandler[]) { const inputs = allHandlers.length % 2 === 0 ? getRandomSignals(getRandom(5)) : getRandomSignals(getRandom(2)); const outputs = allHandlers.length % 2 === 0 ? getRandomSignals(getRandom(2)) : []; const h = createHandler({ inputs, outputs, run(actionMap: IActionMapExtended) { for (const output of outputs) { actionMap.add(output(0)); } }, }); flowfulAcyclicTest.addHandler(h); let add = true; try { flowfulAcyclicTest.sort(); } catch (e) { add = false; flowfulAcyclicTest.removeHandler(h); } if (add) { allHandlers.push(h); } } const allHandlers: ActionHandler[][] = [[], [], [], [], [], []]; for (let i = 0; i < 5000; i++) { _createHandler(allHandlers[0]); } for (let i = 0; i < 1000; i++) { _createHandler(allHandlers[1]); } for (let i = 0; i < 500; i++) { _createHandler(allHandlers[2]); } for (let i = 0; i < 100; i++) { _createHandler(allHandlers[3]); } for (let i = 0; i < 50; i++) { _createHandler(allHandlers[4]); } for (let i = 0; i < 10; i++) { _createHandler(allHandlers[5]); } const randomActions: IAction[][] = []; for (let i = 0; i < 20; i++) { const actions = getRandomSignals(getRandom(10)).map((s) => s(0)); let ok = true; try { flowfulAcyclicTest.dispatch(createActionMap(...actions)); } catch (e) { ok = false; } if (ok) { randomActions.push(actions); } } interface Suite { flowful: Flowful; dyn: Dyn; dyn2: Dyn2; } function createSuite(size: number) { const flowful = new Flowful(); const dyn = new Dyn(); const dyn2 = new Dyn2(); flowful.addManyHandlers(allHandlers[size]); flowful.sort(); dyn.addManyHandlers(allHandlers[size]); dyn.sort(); dyn2.addManyHandlers(allHandlers[size]); dyn2.sort(); return { flowful, dyn, dyn2 }; } const suites: Suite[] = []; suites.push(createSuite(0)); suites.push(createSuite(1)); suites.push(createSuite(2)); suites.push(createSuite(3)); suites.push(createSuite(4)); suites.push(createSuite(5)); // function registerHandler(handler: ActionHandlerInstance) { // if (handler.registered) { // return; // } // const inputs = handler.getInputs(); // for (const Input of inputs) { // Input.handlers.push(handler); // } // handler.registered = true; // } // function unregisterHandler(handler: ActionHandlerInstance) { // if (!handler.registered) { // return; // } // const inputs = handler.getInputs(); // for (const Input of inputs) { // Input.handlers = Input.handlers.filter((h) => h !== handler); // } // handler.registered = false; // } // function registerHandlers(handlers: ActionHandlerInstance[]) { // for (let i = 0, len = handlers.length; i < len; i++) { // const handler = handlers[i]; // registerHandler(handler); // } // } // function unregisterHandlers(handlers: ActionHandlerInstance[]) { // for (let i = 0, len = handlers.length; i < len; i++) { // const handler = handlers[i]; // unregisterHandler(handler); // } // } // function handlerIsInterested(handler: ActionHandlerInstance, actionMap: ActionMapInternal) { // 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 signalCreator of allActionCreators) { // if (handlerInputSet.has(signalCreator)) { // return true; // } // } // } // return false; // } // function goDownEvent( // event: ActionCreator, // handlers: ActionHandlerInstance[] = [], // visitedSignals: WeakSet, // visitedHandlers: WeakSet // ) { // if (visitedSignals.has(event)) { // return; // } // visitedSignals.add(event); // if (!event.handlers) { // return; // } // for (const handler of event.handlers) { // goDownHandler(handler, handlers, visitedSignals, visitedHandlers); // } // } // function goDownHandler( // handler: ActionHandlerInstance, // handlers: ActionHandlerInstance[] = [], // visitedSignals: WeakSet, // visitedHandlers: WeakSet // ) { // if (visitedHandlers.has(handler)) { // return; // } // visitedHandlers.add(handler); // const outputs = handler.getOutputs(); // if (outputs) { // const size = outputs.length; // for (let idx = 0; idx < size; idx++) { // const Output = outputs[idx]; // goDownEvent(Output, handlers, visitedSignals, visitedHandlers); // } // } // handlers.push(handler); // } // function dispatch(actionMap: ActionMapInternal, handlerMiddleware?: ActionHandlerMiddlewareInstance) { // if (actionMap.size === 0) { // return; // } // const visitedSignals = new Set(); // const visitedHandlers = new Set(); // const handlers: ActionHandlerInstance[] = []; // for (const signalCreator of actionMap._getActionCreators()) { // goDownEvent(signalCreator, handlers, visitedSignals, visitedHandlers); // } // handlers.reverse(); // //console.log('direct', handlers.length); // for (let handlerIndex = 0, len = handlers.length; handlerIndex < len; handlerIndex++) { // const handler = handlers[handlerIndex]; // if (!handlerIsInterested(handler, actionMap)) { // continue; // } // actionMap._attachHandler(handler); // if (handlerMiddleware) { // handlerMiddleware.run(actionMap, handler); // } else { // handler.trigger(actionMap); // } // actionMap._detachHandler(); // } // } describe('dynamic sorting (5000)', () => { afterAll(() => { console.log('Longest chain:', longest); }); bench('Flowful', () => { currentLongest = 0; suites[0].flowful.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); bench('Dyn', () => { currentLongest = 0; suites[0].dyn.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); bench('Dyn2', () => { currentLongest = 0; suites[0].dyn2.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); // bench('Dyn (direct)', () => { // dispatch(actionMap(...randomSignals[getRandom(randomSignals.length)])); // }); }); describe('dynamic sorting (1000)', () => { afterAll(() => { console.log('Longest chain:', longest); }); bench('Flowful', () => { currentLongest = 1; suites[1].flowful.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); bench('Dyn', () => { currentLongest = 1; suites[1].dyn.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); bench('Dyn2', () => { currentLongest = 1; suites[1].dyn2.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); // bench('Dyn (direct)', () => { // dispatch(actionMap(...randomSignals[getRandom(randomSignals.length)])); // }); }); describe('dynamic sorting (500)', () => { afterAll(() => { console.log('Longest chain:', longest); }); bench('Flowful', () => { currentLongest = 2; suites[2].flowful.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); bench('Dyn', () => { currentLongest = 2; suites[2].dyn.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); bench('Dyn2', () => { currentLongest = 2; suites[2].dyn2.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); // bench('Dyn (direct)', () => { // dispatch(actionMap(...randomSignals[getRandom(randomSignals.length)])); // }); }); describe('dynamic sorting (100)', () => { afterAll(() => { console.log('Longest chain:', longest); }); bench('Flowful', () => { currentLongest = 3; suites[3].flowful.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); bench('Dyn', () => { currentLongest = 3; suites[3].dyn.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); bench('Dyn2', () => { currentLongest = 3; suites[3].dyn2.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); // bench('Dyn (direct)', () => { // dispatch(actionMap(...randomSignals[getRandom(randomSignals.length)])); // }); }); describe('dynamic sorting (50)', () => { afterAll(() => { console.log('Longest chain:', longest); }); bench('Flowful', () => { currentLongest = 4; suites[4].flowful.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); bench('Dyn', () => { currentLongest = 4; suites[4].dyn.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); bench('Dyn2', () => { currentLongest = 4; suites[4].dyn2.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); // bench('Dyn (direct)', () => { // dispatch(actionMap(...randomSignals[getRandom(randomSignals.length)])); // }); }); describe('dynamic sorting (10)', () => { afterAll(() => { console.log('Longest chain:', longest); }); bench('Flowful', () => { currentLongest = 5; suites[5].flowful.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); bench('Dyn', () => { currentLongest = 5; suites[5].dyn.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); bench('Dyn2', () => { currentLongest = 5; suites[5].dyn2.dispatch(createActionMap(...randomActions[getRandom(randomActions.length)])); }); // bench('Dyn (direct)', () => { // dispatch(actionMap(...randomSignals[getRandom(randomSignals.length)])); // }); });