UNPKG

voxa

Version:

A fsm (state machine) framework for Alexa, Dialogflow, Facebook Messenger and Botframework apps using Node.js

150 lines 7.2 kB
"use strict"; /* * Copyright (c) 2018 Rain Agency <contact@rain.agency> * Author: Rain Agency <contact@rain.agency> * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ Object.defineProperty(exports, "__esModule", { value: true }); const bluebird = require("bluebird"); const _ = require("lodash"); const errors_1 = require("../errors"); const State_1 = require("./State"); const transitions_1 = require("./transitions"); class StateMachine { constructor(config) { this.states = _.cloneDeep(config.states); this.onBeforeStateChangedCallbacks = config.onBeforeStateChanged || []; this.onAfterStateChangeCallbacks = config.onAfterStateChanged || []; this.onUnhandledStateCallback = config.onUnhandledState; this.states.push(new State_1.State("die", { flow: "terminate" })); } async runTransition(fromState, voxaEvent, reply, recursions = 0) { if (recursions > 10) { throw new Error("State Machine Recursion Error"); } const transition = await this.stateTransition(fromState, voxaEvent, reply, recursions); let sysTransition = await this.checkOnUnhandledState(voxaEvent, reply, transition); sysTransition = await this.onAfterStateChanged(voxaEvent, reply, sysTransition); if (sysTransition.shouldTerminate) { reply.terminate(); } if (sysTransition.shouldContinue) { return this.runTransition(sysTransition.to, voxaEvent, reply, recursions + 1); } return sysTransition; } async stateTransition(fromState, voxaEvent, reply, recursions) { try { if (fromState === "entry") { this.currentState = this.getCurrentState(voxaEvent.intent.name, voxaEvent.intent.name, voxaEvent.platform.name); } else { this.currentState = this.getCurrentState(fromState, voxaEvent.intent.name, voxaEvent.platform.name); } } catch (error) { /* * Returning to the global handler here only makes sense if we didn't already made that, * meaning that it could only be done in the first recursion. There's tests covering this scenario * in tests/States.spec.ts */ if (fromState !== "entry" && recursions < 1) { return this.stateTransition("entry", voxaEvent, reply, recursions); } if (error instanceof errors_1.UnknownState) { if (!this.onUnhandledStateCallback) { throw new Error(`${voxaEvent.intent.name} went unhandled`); } return this.onUnhandledStateCallback(voxaEvent, voxaEvent.intent.name); } throw error; } await this.runOnBeforeStateChanged(voxaEvent, reply); let transition = await this.currentState.handle(voxaEvent); voxaEvent.log.debug(`${this.currentState.name} transition resulted in`, { transition, }); try { if (!transition && fromState !== "entry") { this.currentState = this.getCurrentState(voxaEvent.intent.name, voxaEvent.intent.name, voxaEvent.platform.name); transition = await this.currentState.handle(voxaEvent); } } catch (error) { if (error instanceof errors_1.UnknownState) { return transition; } throw error; } return transition; } async onAfterStateChanged(voxaEvent, reply, transition) { voxaEvent.log.debug("Running onAfterStateChangeCallbacks"); await bluebird.mapSeries(this.onAfterStateChangeCallbacks, (fn) => { return fn(voxaEvent, reply, transition); }); voxaEvent.log.debug("Transition is now", { transition }); return transition; } async checkOnUnhandledState(voxaEvent, reply, transition) { if (!_.isEmpty(transition) || transition) { return new transitions_1.SystemTransition(transition); } if (!this.onUnhandledStateCallback) { throw new Error(`${voxaEvent.intent.name} went unhandled`); } const tr = await this.onUnhandledStateCallback(voxaEvent, this.currentState.name); return new transitions_1.SystemTransition(tr); } async runOnBeforeStateChanged(voxaEvent, reply) { const onBeforeState = this.onBeforeStateChangedCallbacks; voxaEvent.log.debug("Running onBeforeStateChanged"); await bluebird.mapSeries(onBeforeState, (fn) => { return fn(voxaEvent, reply, this.currentState); }); } getCurrentState(currentStateName, intentName, platform) { const states = _(this.states) .filter({ name: currentStateName }) .filter((s) => { return s.platform === platform || s.platform === "core"; }) // Sometimes a user might have defined more than one controller for the same state, // in that case we want to get the one for the current intent .filter((s) => { return s.intents.length === 0 || _.includes(s.intents, intentName); }) .value(); if (states.length === 0) { throw new errors_1.UnknownState(currentStateName); } if (states.length === 1) { return states[0]; } // If the code reaches this point, that means the `states` array may contain // one state without an intents array filter and/or // one or more controllers with an intents array that contains the intent name. // The controller with an intents array is given more priority than the one with no intents array, // so the first controller that contains the intent name in its intents array is returned. return (states.find((s) => !!s.intents.length && s.intents.includes(intentName)) || states[0] // If no state with name is found, the first state is returned by default as an State object is always needed ); } } exports.StateMachine = StateMachine; //# sourceMappingURL=StateMachine.js.map