UNPKG

dagcoin-fsm

Version:

An implementation of a generic Finite State Machine with Promises

430 lines (317 loc) 14.5 kB
"use strict"; function StateMachine(jsonStateMachine) { const properties = jsonStateMachine.properties; const states = jsonStateMachine.states; const firstState = jsonStateMachine.firstState; const transitions = jsonStateMachine.transitions; this.eventBus = require('byteballcore/event_bus'); if (properties) { if (properties.id != null) { throw Error('PROPERTY id IS RESERVED FOR INTERNAL USE'); } if (properties.stateMachineIdCounter != null) { throw Error('PROPERTY stateMachineIdCounter IS RESERVED FOR INTERNAL USE'); } if (properties.properties != null) { throw Error('PROPERTY properties IS RESERVED FOR INTERNAL USE'); } this.properties = properties; for (let property in properties) { this[property] = properties[property]; } if (!this.name) { throw 'MISSING name IN A STATE MACHINE. IT IS A MANDATORY PROPERTY'; } if (!this.directory) { throw 'MISSING directory IN A STATE MACHINE. IT IS A MANDATORY PROPERTY'; } } if (!states) { throw 'MISSING PARAMETER states'; } if (!firstState) { throw 'MISSING PARAMETER firstState'; } if (!transitions) { throw 'MISSING PARAMETER transitions'; } if (states.length < 2) { throw `CANNOT BE A STATE MACHINE WITH LESS THAN 2 STATES: ${states.length} AVAILABLE`; } if (transitions.length < 1) { throw `CANNOT BE A STATE MACHINE WITH LESS THAN 1 TRANSITION: ${transitions.length} AVAILABLE`; } const DataFetcher = require('./dataFetcher'); const State = require('./state'); this.states = {}; for (let i = 0; i < states.length; i += 1) { const stateProperties = states[i]; if (!stateProperties.name) { throw `State WITHOUT name PROPERTY. CHECK YOUR StateMachine DEFINITION. FAULTY STATE DEFINITION: ${JSON.stringify(stateProperties)}`; } if (this.states[stateProperties.name]) { throw `DUPLICATE STATE: ${stateProperties.name}. YOU DEFINED THIS STATE TWICE, CHECK YOUR StateMachine DEFINITION`; } console.log(`State FOUND: ${stateProperties.name}`); let state = new State(stateProperties); state.setStateMachine(this); const Action = require('./action'); if (stateProperties.actionsIn) { if (stateProperties.actionsIn.constructor !== Array) { throw `State PROPERTY actionsIn IS SET BUT NOT AN ARRAY. CHECK THE DEFINITION : ${JSON.stringify(stateProperties)}`; } if (stateProperties.actionsIn.length > 0) { stateProperties.actionsIn.forEach(actionProperties => { let action = null; if (actionProperties.execute) { if (typeof actionProperties.execute !== 'function') { throw `Action IN PROPERTY execute MUST BE A METHOD: ${JSON.stringify(actionProperties)}`; } action = new Action(actionProperties, this, state); if (!action) { throw `COULD NOT CREATE SIMPLE Action IN WITH ${JSON.stringify(actionProperties)}`; } action.execute = actionProperties.execute; action.getName = () => { return actionProperties.name; }; } else { const actionPath = `${this.directory}/actions/${actionProperties.name}`; action = require(actionPath)(actionProperties, this, state); if (!action) { throw `Action IN DEFINITION NOT FOUND IN ${actionPath}. AND NO execute METHOD DEFINED IN THE PROPERTIES. CHECK ${actionProperties}`; } if (typeof action.execute !== 'function') { throw `Action DEFINED IN ${actionPath} HAS NO execute METHOD. CHECK ${actionPath}`; } } state.addActionIn(action); }); } } if (stateProperties.actionsOut) { if (stateProperties.actionsOut.constructor !== Array) { throw `State PROPERTY actionsIn IS SET BUT NOT AN ARRAY. CHECK THE DEFINITION : ${JSON.stringify(stateProperties)}`; } if (stateProperties.actionsOut.length > 0) { stateProperties.actionsOut.forEach(actionProperties => { let action = null; if (actionProperties.execute) { if (typeof actionProperties.execute !== 'function') { throw `Action OUT PROPERTY execute MUST BE A METHOD: ${JSON.stringify(actionProperties)}`; } action = new Action(actionProperties, this, state); if (!action) { throw `COULD NOT CREATE SIMPLE Action OUT WITH ${JSON.stringify(actionProperties)}`; } action.execute = actionProperties.execute; action.getName = () => { return actionProperties.name; }; } else { const actionPath = `${this.directory}/actions/${actionProperties.name}`; action = require(actionPath)(actionProperties, this, state); if (!fetcher) { throw `Action OUT DEFINITION NOT FOUND IN ${actionPath}. AND NO execute METHOD DEFINED IN THE PROPERTIES. CHECK ${actionProperties}`; } } state.addActionOut(action); }); } } if (stateProperties.isFinal && stateProperties.fetchers && stateProperties.fetchers.length > 0) { throw `FINAL State (isFinal IS true) WITH fetchers. IT SHOULD NOT HAVE ANY: ${JSON.stringify(stateProperties)}`; } if (!stateProperties.isFinal && (!stateProperties.fetchers || stateProperties.fetchers.length === 0)) { throw `NOT FINAL State (isFinal IS false) WITHOUT fetchers. IT SHOULD HAVE AT LEAST ONE: ${JSON.stringify(stateProperties)}`; } if (stateProperties.fetchers && stateProperties.fetchers.length > 0) { const fetchers = stateProperties.fetchers; for (let j = 0; j < fetchers.length; j += 1) { const fetcherProperties = fetchers[j]; if (!fetcherProperties.name) { throw `DataFetcher DEFINED INSIDE STATE WITHOUT NAME: ${JSON.stringify(stateProperties)}`; } let fetcher = null; if (fetcherProperties.retrieveData) { if (typeof fetcherProperties.retrieveData !== 'function') { throw `DataFetcher PROPERTY retrieveData MUST BE A METHOD: ${JSON.stringify(fetcherProperties)}`; } fetcher = new DataFetcher(fetcherProperties, this, state); if (!fetcher) { throw `COULD NOT CREATE SIMPLE DataFetcher WITH ${JSON.stringify(fetcherProperties)}`; } fetcher.retrieveData = fetcherProperties.retrieveData; fetcher.getName = () => { return fetcherProperties.name; }; } else { fetcher = require(`${this.directory}/data/${fetcherProperties.name}`)(fetcherProperties, this, state); if (!fetcher) { throw `DataFetcher DEFINITION NOT FOUND IN ${this.directory}/data/${fetcherProperties.name}. AND NO retrieveData METHOD DEFINED IN THE PROPERTIES.`; } } state.addDataFetcher(fetcher); } } this.states[state.getName()] = state; } this.currentState = this.states[firstState]; if (!this.currentState) { throw `NO STATE DECLARED WITH NAME ${firstState}`; } const Transition = require('./transition'); this.transitions = {}; for (let i in transitions) { const transitionProperties = transitions[i]; if (!transitionProperties.name) { transitionProperties.name = `${transitionProperties.fromState}-to-${transitionProperties.toState}`; } console.log(`TRANSITION FOUND: ${transitionProperties.name}`); let transition = null; if (transitionProperties.checkCondition) { if (typeof transitionProperties.checkCondition !== 'function') { throw `Transition PROPERTY checkCondition MUST BE A FUNCTION: ${JSON.stringify(transitionProperties)}`; } transition = new Transition(transitionProperties); if (!transition) { throw `COULD NOT CREATE SIMPLE STATE ${transition.name}`; } transition.checkCondition = transitionProperties.checkCondition; } else { transition = require(`${this.directory}/transitions/${transitionProperties.name}`); if (!transition) { throw `NO checkConditionMethod NOR DEFINITION FOUND AT ${this.directory}/transitions/${transitionProperties.name}. CHECK ${JSON.stringify(transitionProperties)}`; } transition.getName = () => { return transitionProperties.name; }; } this.transitions[transition.name] = transition; transition.fromStateObject = this.states[transition.fromState]; if (!transition.fromStateObject) { throw `STATE ${transition.fromStateObject} NOT FOUND. CHECK THE DEFINITION OF TRANSITION ${transition.name} (fromState).`; } transition.fromStateObject.addTransition(transition); transition.toStateObject = this.states[transition.toState]; if (!transition.toStateObject) { throw `STATE ${transition.toStateObject} NOT FOUND. CHECK THE DEFINITION OF TRANSITION ${transition.name} (toState).`; } transition.setStateMachine(this); } this.data = {}; this.id = this.nextId(); } StateMachine.prototype.stateMachineIdCounter = 0; StateMachine.prototype.nextId = function () { const id = this.stateMachineIdCounter; this.stateMachineIdCounter += 1; return id; }; StateMachine.prototype.setData = function (key, value) { if (key == null) { throw Error(`WHILE CALLING setData WITHIN MACHINE ${this.name}: PARAMETER key IS NULL`); } if (value == null) { console.log(`WHILE CALLING setData FOR ${key} WITHIN MACHINE ${this.name}: PARAMETER value IS NULL, UNSETTING ${key}`); delete this.data[key]; } else { this.data[key] = value; } }; StateMachine.prototype.getData = function (key, defaultValue) { if (key == null) { throw Error(`WHILE CALLING getData WITHIN MACHINE ${this.name}: PARAMETER key IS NULL`); } const value = this.data[key]; if (value == null && defaultValue != null) { return defaultValue; } return value; }; StateMachine.prototype.start = function () { this.currentState.enable(); }; StateMachine.prototype.waitForFinalState = function () { const self = this; if (this.currentState.isFinal) { return Promise.resolve(); } return new Promise(resolve => { self.eventBus.once(`internal.dagcoin.fsm.${self.id}.final`, resolve); }); }; StateMachine.prototype.ping = function () { const self = this; if (self.currentState.isFinal) { return Promise.resolve(false); } return self.currentState.ping().then(triggeringTransition => { if (!triggeringTransition) { return Promise.resolve(false); } const previousState = self.currentState; previousState.disable(); self.currentState = triggeringTransition.getNextState(); self.currentState.enable(); console.log(`STATE MACHINE ${self.name} MOVED FROM ${previousState.getName()} TO ${self.currentState.getName()}`); if (self.currentState.isFinal === true) { self.eventBus.emit(`internal.dagcoin.fsm.${self.id}.final`); } return Promise.resolve(true); }); }; StateMachine.prototype.recursivePing = function (transitions) { if (!transitions) { transitions = 0; } const self = this; return self.ping().then(transitionOccurred => { self.pinging = false; if (transitionOccurred) { return self.recursivePing(transitions + 1); } else { return Promise.resolve(transitions); } }); }; /** * * @param updatedInformation The requestor might ping after updating the database or reporting relevant change to the state machine while * a previous test is currently ongoing (and is using or has used outdated information) * In this case the transition test should be repeated in the end. */ StateMachine.prototype.pingUntilOver = function (updatedInformation) { const self = this; if (self.pinging) { if (updatedInformation) { self.updatedInformation = true; } return self.pingingPromise; } self.pinging = true; self.pingingPromise = self.recursivePing().then(() => { if (!self.updatedInformation) { return Promise.resolve(); } else { self.updatedInformation = false; return self.recursivePing(); } }, error => { console.error(`SOMETHING WENT WRONG IN ${self.name} IN STATE ${self.currentState.getName()}: ${error}`); return Promise.resolve(); }).then(() => { self.pinging = false; self.updatedInformation = false; self.pingingPromise = null; return Promise.resolve(); }); return self.pingingPromise; }; StateMachine.prototype.getName = function () { return this.name; }; StateMachine.prototype.getCurrentState = function () { return this.currentState; }; module.exports = StateMachine;