{"_id":"@ama-team/voxengine-scenario-framework","_rev":"7-e00f35655a8d57da7e4514083314a69c","name":"@ama-team/voxengine-scenario-framework","description":"Framework to run VoxImplant scenarios","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@ama-team/voxengine-scenario-framework","version":"0.1.0","description":"Framework to run VoxImplant scenarios","main":"lib/index.js","scripts":{"clean":"rm -rf report","test":"mocha","test:coverage:report":"istanbul cover node_modules/.bin/_mocha","coveralls":"cat report/coverage/lcov.info | node_modules/.bin/coveralls","test:coverage":"npm run test:coverage:report && npm run coveralls","test:ignore":"npm run test || true","test:report":"npm run allure:clean && npm run test:ignore && npm run allure:report","allure:clean":"rm -rf report/allure-results report/allure","allure:report":"allure generate -o report/allure -- report/allure-results","allure:open":"allure report open -o report/allure"},"engines":{},"repository":{"type":"git","url":"git+https://github.com/ama-team/voxengine-scenario-framework.git"},"keywords":["voxengine","voximplant"],"author":{"name":"AMA Team","email":"dev@amagroup.ru"},"maintainers":[{"name":"Etki","email":"etki@etki.me"}],"contributors":[{"name":"Etki","email":"etki@etki.me"}],"license":"MIT","bugs":{"url":"https://github.com/ama-team/voxengine-scenario-framework/issues"},"homepage":"https://github.com/ama-team/voxengine-scenario-framework#readme","dependencies":{"@ama-team/voxengine-sdk":"^0.1.0"},"devDependencies":{"@ama-team/voxengine-definitions":"^0.1.0","chai":"^3.5.0","chai-as-promised":"^6.0.0","coveralls":"^2.11.15","istanbul":"^0.4.5","karma":"^1.3.0","karma-coverage":"^1.1.1","mocha":"^3.1.2","mocha-allure-reporter":"^1.2.4","mocha-lcov-reporter":"^1.2.0","mocha-multi":"^0.9.1","mocha-multi-reporters":"^1.1.1","promise":"^7.1.1","sinon":"^1.17.6"},"gitHead":"f2f9ef0d08fa664d42b3eeea524304726d2fda18","_id":"@ama-team/voxengine-scenario-framework@0.1.0","_shasum":"ec673b2f3c8d75008ce752730fc4197a8930ee00","_from":".","_npmVersion":"3.10.10","_nodeVersion":"6.9.5","_npmUser":{"name":"ama-team","email":"dev@amagroup.ru"},"dist":{"shasum":"ec673b2f3c8d75008ce752730fc4197a8930ee00","tarball":"https://registry.npmjs.org/@ama-team/voxengine-scenario-framework/-/voxengine-scenario-framework-0.1.0.tgz","integrity":"sha512-WEUMyUdHZedESp9JUk+5paFvZ+WlHcPeR2J41DWDTn597fwFbefU3uBMeIbq0BB0BS40ICbZqgtqvilVBZAJfw==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCvqYfOMMYgc0qw5/y2BMeEHL43KqjWjF9WB7vbXqZKNQIhAKbVI1gTvZWkQgKMXEQVlPvnAMazHpY7jxI0KF42Eg6r"}]},"_npmOperationalInternal":{"host":"packages-18-east.internal.npmjs.com","tmp":"tmp/voxengine-scenario-framework-0.1.0.tgz_1487348529303_0.10805476689711213"},"directories":{}},"0.2.0":{"name":"@ama-team/voxengine-scenario-framework","version":"0.2.0","description":"Framework to run VoxImplant scenarios","main":"lib/index.js","scripts":{"clean":"rm -rf build","test":"istanbul cover node_modules/.bin/_mocha","test:report:publish:coverage":"cat build/report/coverage/lcov.info | node_modules/.bin/coveralls","test:report:publish":"npm run test:report:publish:coverage","test:report":"npm run test:report:allure","test:report:allure":"allure generate -o build/report/allure -- build/data/allure","doc":"jsdoc -d build/doc lib"},"engines":{},"repository":{"type":"git","url":"git+https://github.com/ama-team/voxengine-scenario-framework.git"},"keywords":["voxengine","voximplant"],"author":{"name":"AMA Team","email":"dev@amagroup.ru"},"maintainers":[{"name":"ama-bot","email":"ops@amagroup.ru"},{"name":"ama-team","email":"dev@amagroup.ru"},{"name":"etki","email":"etki@etki.me"}],"contributors":[{"name":"Etki","email":"etki@etki.me"}],"license":"MIT","bugs":{"url":"https://github.com/ama-team/voxengine-scenario-framework/issues"},"homepage":"https://github.com/ama-team/voxengine-scenario-framework#readme","dependencies":{"@ama-team/voxengine-sdk":"^0.4.0"},"devDependencies":{"@ama-team/voxengine-definitions":"^0.1.0","@ama-team/voxengine-stubs":"^0.1.0","bluebird":"^3.5.0","chai":"^3.5.0","chai-as-promised":"^6.0.0","coveralls":"^2.13.1","fs-extra":"^4.0.1","glob":"^7.1.2","istanbul":"^0.4.5","jake":"^8.0.15","js-yaml":"^3.9.1","jsdoc":"^3.5.4","karma":"^1.7.0","karma-coverage":"^1.1.1","mocha":"^3.5.0","mocha-allure-reporter":"^1.3.2","mocha-junit-reporter":"^1.13.0","mocha-lcov-reporter":"^1.3.0","mocha-multi":"^0.9.1","mocha-multi-reporters":"^1.1.4","promise":"^7.3.1","sinon":"^3.0.0","standard":"^10.0.3"},"standard":{"ignore":["examples"]},"gitHead":"8d6f66485c54099cda5af99dd0a61058fce722cb","_id":"@ama-team/voxengine-scenario-framework@0.2.0","_shasum":"eda4c15174148032f9c404ff9bd4f030ab87acfa","_from":".","_npmVersion":"2.15.11","_nodeVersion":"4.8.4","_npmUser":{"name":"ama-bot","email":"ops@amagroup.ru"},"dist":{"shasum":"eda4c15174148032f9c404ff9bd4f030ab87acfa","tarball":"https://registry.npmjs.org/@ama-team/voxengine-scenario-framework/-/voxengine-scenario-framework-0.2.0.tgz","integrity":"sha512-YS48dQ3p7Fv1T1w4h/ihGArhBljX65frJT/LpmSZLh5GObSJ0pON3t0trj4pWcWycFtr5JwasHyTB9Nq2/8+AA==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCYMPVnohsFTMbtt4DlRQZagDe9bZHyXEubEeYZbPL6sQIgDsK71NpRQVOnOjSZ+8zSVtvJfAJAaGSDm8LnpjL6cd8="}]},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/voxengine-scenario-framework-0.2.0.tgz_1504510974240_0.2770406936760992"},"directories":{}}},"readme":"# (Unofficial) VoxEngine Scenario Framework\n\n[![npm](https://img.shields.io/npm/v/@ama-team/voxengine-scenario-framework.svg?style=flat-square)](https://www.npmjs.com/package/@ama-team/voxengine-scenario-framework)\n[![CircleCI/Master](https://img.shields.io/circleci/project/github/ama-team/voxengine-scenario-framework/master.svg?style=flat-square)](https://circleci.com/gh/ama-team/voxengine-scenario-framework/tree/master)\n[![Coveralls/Master](https://img.shields.io/coveralls/ama-team/voxengine-scenario-framework/master.svg?style=flat-square)](https://coveralls.io/github/ama-team/voxengine-scenario-framework)\n[![Scrutinizer/Master](https://img.shields.io/scrutinizer/g/ama-team/voxengine-scenario-framework/master.svg?style=flat-square)](https://scrutinizer-ci.com/g/ama-team/voxengine-scenario-framework?branch=master)\n[![Code Climate](https://img.shields.io/codeclimate/github/ama-team/voxengine-scenario-framework.svg?style=flat-square)](https://codeclimate.com/github/ama-team/voxengine-scenario-framework)\n\nThis repository contains a framework which takes opinionated approach\nto handling VoxEngine scenarios. It takes care of different states,\ntransitions between them, data saving and logging.\n\nTo install this framework, simply run following command:\n\n```\nnpm i @ama-team/voxengine-scenario-framework -S\n```\n\nPlease note that framework uses [@ama-team/voxengine-sdk][] under the\nhood, which may solve some other low-level problems for you.\n\n## What's the problem?\n\nScenarios are complex and usually quite asynchronous things. Developing\nthem the usual way usually results in ton of spaghetti code and unclear\nconditions hidden here and there, as well as no specific ordering\non certain events that have to be ordered (for example, you can't emit\nmore than one HTTP request after `VoxEngine.terminate()`). Promises\nare partial response to it, but it easily becomes as ugly when \nunnecessary conditions come into play - e.g. you need to terminate the\ncallback scenario when both calls are finished, but there is a chance\nthat second call won't be made, so you need to take this into account\nand either create humongous control conditions or resolving promises \nthat didn't actually resolve.\n\nWe need something stronger.\n\n## State model\n\nWe can break down each scenario into set of states. Imagine the \nsimplest scenario fo notifying clients by call:\n\n1. Target phone number called\n2. Call failed, go to #5\n3. Call succeeded, go to #4\n3. Say the phrase, then go to #5\n4. Report to HTTP backend and terminate\n\nIt can be easily represented as in code:\n\n```js\nvar Framework = require('@ama-team/voxengine-scenario-framework')\n\nvar scenario = {\n  trigger: Framework.TriggerType.Http,\n  states: {\n    entrypoint: {\n      entrypoint: true,\n      transition: function () {\n        var number = this.arguments.number\n        this.state.call = VoxEngine.callPSTN(number)\n        return {trigger: 'connected'}\n      }\n    },\n    failed: {\n      transition: function () {},\n      triggers: {\n        id: 'terminated',\n        hints: {success: false}\n      }\n    },\n    connected: {\n      transition: function () {\n        return new Promise(function (resolve) {\n          var call = VoxEngine.callPSTN(number)\n          this.state.call = call\n          call.addEventListener(CallEvents.Connected, function () {\n            resolve({trigger: 'communicated'})\n          })\n          call.addEventListener(CallEvents.Failed, function () {\n            resolve({transitionedTo: 'failed'})\n          })\n        })\n      }\n    },\n    communicated: {\n      transition: function() {\n        return new Promise(function (resolve) {\n          var phrase = this.arguments.phrase\n          this.state.call.say(phrase, Language.US_ENGLISH_FEMALE)\n          this.state.call.addEventListener(CallEvents.PlaybackFinished, function() {\n            this.state.call.hangup()\n            this.state.success = true\n            resolve()\n          })\n        })\n      },\n      triggers: {\n        id: 'terminated',\n        hints: {success: true}\n      }\n    },\n    terminated: {\n      terminal: true,\n      transition: function (_, hints) {\n        var options = new Net.HttpRequestOptions()\n        options.postData = JSON.stringify({success: hints.success})\n        return Net.httpRequestAsync('http://some-backend', options)\n      }\n    }\n  }\n}\n```\n\nWhile this scenario is more complex than all the same logic but \nwritten in straightforward way, it shows how another approach looks\nlike. Now you have explicit states and transitions that are required\nto travel from one state to another; each transition may tell engine\nthat it ended not so well, resulting in a completely another state.\nThe scenario itself now boils only to crucial points (states) and\npossible outcomes during a transition. Besides that, framework also\nhelps in passing arguments inside scenario and cornering some sharp \nedges. So, let's inspect what we have here.\n\n## Scenario schema\n\n*this section contains examples in YAML rather than in javascript \nfor higher readability*\n\nFirst of all, scenario has three auxiliary properties describing \nitself. They are completely optional, but may save you some debugging\ntime.\n\n```yml\nid: <string, optional>\nversion: <string, optional>\nenvironment: <string, optional>\n```\n\nThen there is states structure:\n\n```yml\nstates:\n  <name:string>:\n    entrypoint: <boolean, optional>\n    terminal: <boolean, optional>\n    transition: <handler>\n    abort: <handler, optional>\n    triggers: # optional\n      id: <state name:string>\n      hints: <object/function, optional>\n```\n\nScenario has to have exactly one entrypoint state and at least one\nterminal state - otherwise it won't be launched.\n\nThen there is metadata/metaprocessing section:\n\n```yml\n# whether the scenario is launched by http call or phone call\ntrigger: <Framework.TriggerType>\n# default arguments\narguments: <object, optional>\n# default context state, doesn't relate to states discussed above\nstate: <object, optional>\n# DI-wannabe container wih services you may like to use in transitions\ncontainer: <object, optional> \nonTermination: <handler, optional>\nonError: <handler, optional>\n# used to deserialize arguments from custom data\ndeserializer: <handler, optional>\ntimeouts: <object<string, int/null>\n```\n\nHandler is a slightly complex structure:\n\n```yml\nhandler: <function>\ntimeout: <int/null, optional>\nonTimeout:\n  handler: <function>\n  timeout: <int/null, optional>\n```\n\nHowever, you can always specify it just as a function, and engine will\nsimply expand it.\n\nAfter specifying this structure running it is as simple as calling\n`Framework.run(scenario)`.\n\n## Passing data between states\n\nIn lots of scenarios you will need to pass some data around and/or know\nprevious state from which engine is transitioning. Transition function\nsignature specifies three arguments:\n\n```js\nfunction transition (previousStateId, hints, cancellationToken) {}\n``` \n\nFirst argument will contain the name of previous state. Hints is an\nthat may contain any data you want, and you may set whenever you \ntrigger some state:\n\n```js\nvar states = {\n  preterminal: {\n    transition: function () {\n      return {trigger: {id: 'terminal', hints: {sendDebugInfo: true}}}\n    }\n  },\n  terminal: {\n    transition: function (previous, hints) {\n      if (hints.sendDebugInfo) {\n        // Do something\n      }\n    }\n  }\n}\n```\n\n```js\nvar states = {\n  preterminal: {\n    triggers: {\n      id: 'terminal',\n      hints: {sendDebugInfo: true}\n    }\n  }\n}\n```\n\nMoreover, hints may be a function that will be called in the same \ncontext in the moment of trigger processing.\n\nThe third argument is an advanced aspect discussed later.\n\n## Storing states between the states\n\nEnd user will certainly need to store some data between states (e.g.\ncall instances), and there are two options for that. First, you may\ngo with standard javascript way of enclosing scopes: you may define\n`var` outside of scenario:\n\n```js\nvar call\nvar scenario = {\n  states: {\n    entrypoint: function () {\n      call = VoxEngine.callPSTN('911')\n    }\n  }\n}\n```\n\nHowever, if you want to isolate your handlers, `this.state` context\nproperty can be used for that:\n\n```js\nvar scenario = {\n  states: {\n    entrypoint: function () {\n      this.state.call = VoxEngine.callPSTN('911')\n    }\n  }\n}\n```\n\nIt's completely up to you which way to choose.\n\n## The context\n\nAll user-supplied code is executed inside the context - that means that\nsame specific object will be passed as `this`. This object has \nfollowing properties:\n\n```yml\narguments: <object>\nstate: <object>\ncontainer: <object>\ntrigger: <TScenarioTrigger>\n# log url\nlog: <string>\ntransitionTo: <function<string, hints>>\ntrace: <function<message, ...replacements>>\ndebug: <function<message, ...replacements>>\ninfo: <function<message, ...replacements>>\nnotice: <function<message, ...replacements>>\nwarn: <function<message, ...replacements>>\nerror: <function<message, ...replacements>>\n```\n\nLogger methods are forwarded to Slf4j-alike logger from \n[@ama-team/voxengine-sdk][]. It has a nice feature of resolving\n`{}` placeholders into arguments, so\n\n```js\nthis.warn('{} has jumped over {}', 'quick fox', 'lazy dog')\n````\n\nWill result in a phrase you've seen a thousand times. You will find\nmore information on the [library page][@ama-team/voxengine-sdk]. \n\n## Non-triggering states / coding outside of the box\n\nBasically, the term state itself doesn't imply that there is any kind\nof immediate transition to another state. In case transition doesn't\nreturn the `{trigger: something}` structure and there is no `.triggers`\nproperty on the state, the framework will stay in specific state until\nsomething calls the `.transitionTo` method on context:\n\n```js\nvar state = {\n  transition: function () {\n    var trigger = this.transitionTo.bind(this, {trigger: 'terminated'})\n    this.state.call.addEventListener(CallEvents.Disconnected, trigger)\n  }\n}\n```\n\nThis also means scenario may hang near-infinite in some state until \nVoxEngine kicks whole scenario out, so be careful playing with this.\nCurrently, scenario/state timeouts are not supported, and i don't\nknow how to implement them properly (because in 95%+ of cases it\nwould be necessary to take some action on timeout rather than just\nterminate whole scenario).\n\nIf `.transitionTo()` is called during another transition, previous \ntransition gets aborted: it's abort handler is called, and it's \ncancellation token (third argument) gets cancelled. Because there is no \ndirect way to abort running code, transition that may be aborted should \nregularly check if it's token has been cancelled (`token.isCancelled()`) \nbefore proceeding further:\n\n```js\nvar httpCallsMadeState = function (p, h, token) {\n  return client\n    .performRequest('/ping')\n    .then(function () {\n      return token.isCancelled() ? null : client.performRequest('/pong')\n    })\n}\n```\n\n## Arguments\n\nScenarios (at least HTTP-triggered) usually need arguments to run.\nThis framework allows to specify some hardcoded arguments and to \ndeserialize them from customData using the `.deserializer` scenario\nproperty. Framework will take hardcoded arguments, apply deserializer\non customData (either VoxEngine.customData or call.customData, \ndepending on scenario trigger type), and then recursively merge them.\nPlease note that if deserializer fails (throws error or returns \nrejected thenable), the whole scenario will be aborted. Deserializer\nis **allowed** to take some time and return a promise (basically, it's\na regular `handler` with possible timeout and stuff), so you can \nperform HTTP calls inside.\n\nBy default, framework will try to decode JSON out of customData and\nsilently proceed on fail.\n\nPlease note that VoxEngine docs [state][VoxEngine.customData] that \nthere is 200-byte limit on custom data, so if you need a lot of data, \nit's better just to store it using HTTP backend and pass only ID.\n\n## Timeouts\n\nEvery lengthy action should have a timeout to prevent infinite hangs.\nThere are two options that control it: individual timeout settings on\nhandlers and scenario-wide default values (specified in `.timeouts` \nproperty):\n\n```js\nvar scenario = {\n  states: {\n    entrypoint: {\n      transition: {\n        // timeout of 20 will be used\n        handler: function () {},\n        onTimeout: {\n          // timeout of 20 will be used because of override\n          handler: function () {},\n          timeout: 20\n        }\n      }\n    }\n  },\n  timeouts: {\n    transition: 20,\n    onTransitionTimeout: 10\n  }\n}\n```\n \nTimeouts are set in milliseconds, every value but number >=\n0 is treated as a 'no timeout'.\n\nIn case of timeout cancellation token is cancelled as well.\n\n## Terminating\n\nUsually there is some kind of post-scenario things to be done, like \nwaiting for all background HTTP requests or printing results to log.\nFor tasks like that you can specify a termination handler in scenario:\n\n```js\nvar scenario = {\n  onTermination: function () {\n    return awaitSomething()\n  }\n}\n```\n\nTermination handler acts the very same as other handlers, but receives\n`TInitializationStageResult` and `TScenarioStageResult` as arguments.\n\nFramework calls VoxEngine.terminate in the end, if `behavior.terminate`\noption is set to true (which is by default).\n\n## Errors and error handling\n\nThere are several places when error may be thrown:\n\n- Argument deserializer. In that case termination handler will be\ncalled instantly and scenario won't be executed.\n- Active transition. This will cause `scenario.onError` handler to be \ntriggered, and, if it doesn't respond with \n`{trigger: some other state}`, halt the scenario with an error,\nstill calling `scenario.onTermination` handler.\n- Termination handler. This will do nothing but halt it as javascript\ndoes with every piece of code throwing an exception.\n- And, finally, the framework itself. This will cause piece of code to\nend with `Tripped` status, so watch for these to report.\n\nThe onError handler signature is simple:\n\n```js\nvar onError = function (error, previousState, targetState, hints) {}\n``` \n\nonError may take some time to figure out what to do and may return a \npromise, as any other handler, as well as be timed out. \n\n## Logging\n\nFramework logs everything it can, which is usually not what you really\nwant. However, the logger is taken from [@ama-team/voxengine-sdk][] and\nis controlled accordingly:\n\n```js\nvar SDK = require('@ama-team/voxengine-sdk')\nvar Logger = SDK.Logger\nvar Slf4j = Logger.Slf4j\n\n// decreasing overall verbosity\nSlf4j.setLevel(Logger.Level.Warn)\n// increasing scenario log verbosity\nSlf4j.setLevel('ama-team.vsf.context', Logger.Level.Debug)\n```\n\nBy default, all INFO and higher level messages are logged.\n\n## Validation\n\nTo prevent invalid scenario from uploading, you may validate it first.\nTo do so, just run `Framework.validate` to receive a `ValidationSet`\nobject. If it's severity is \n`Framework.Schema.Validator.Severity.Fatal`, scenario is invalid and \ncan't be used.\n\n## Concurrency notes\n\nThis library is built with run-to-completion model in mind. VoxImplant\nengineers confirmed that this model is used by their interpreter.\n\n## Other notes\n\n- **Do not use VoxEngine.easyProcess.** It will terminate scenario \nprematurely, as it [binds onto VoxEngine.terminate][VoxEngine.easyProcess]\n- Please note that at the moment of this document being written ES6\n**had not been supported by VoxImplant**. While you can use transpiler\nto transform your scripts to ES5, i personally recommend not to use\nES6 until it is officially supported and write everything in ES5 - it\nmay save you nerves during debug.\n- It is also strongly recommended to strip all comments from framework,\nbut not to minify it (at least agressively) to simplify debug in case \nsomething won't work as expected.\n- There is also a package `@ama-team/voxengine-definitions` that \ncontains jsdoc definitions for VoxEngine internals. Be sure to install \nit if you need autocompletion in IDE other than provided by official \nweb UI.\n- `@ama-team/voximplant-publisher` *should* be finished someday.\n\n## Dev branch state\n\n[![CircleCI/Dev](https://img.shields.io/circleci/project/github/ama-team/voxengine-scenario-framework/dev.svg?style=flat-square)](https://circleci.com/gh/ama-team/voxengine-scenario-framework/tree/dev)\n[![Coveralls/Dev](https://img.shields.io/coveralls/ama-team/voxengine-scenario-framework/dev.svg?style=flat-square)](https://coveralls.io/github/ama-team/voxengine-scenario-framework)\n[![Scrutinizer/Dev](https://img.shields.io/scrutinizer/g/ama-team/voxengine-scenario-framework/dev.svg?style=flat-square)](https://scrutinizer-ci.com/g/ama-team/voxengine-scenario-framework?branch=dev)\n\n  [@ama-team/voxengine-sdk]: https://npmjs.org/package/@ama-team/voxengine-sdk\n  [VoxEngine.customData]: https://voximplant.com/docs/references/appengine/VoxEngine.html#VoxEngine_customData\n  [VoxEngine.easyProcess]: http://voximplant.com/help/faq/what-code-is-behind-voxengine-easyprocess-function/\n","maintainers":[{"email":"dev@amagroup.ru","name":"ama-master"},{"email":"ops@amagroup.ru","name":"ama-bot"},{"email":"etki@etki.me","name":"etki"}],"time":{"modified":"2022-06-12T14:36:16.808Z","created":"2017-02-17T16:22:10.019Z","0.1.0":"2017-02-17T16:22:10.019Z","0.2.0":"2017-09-04T07:42:55.315Z"},"homepage":"https://github.com/ama-team/voxengine-scenario-framework#readme","keywords":["voxengine","voximplant"],"repository":{"type":"git","url":"git+https://github.com/ama-team/voxengine-scenario-framework.git"},"contributors":[{"name":"Etki","email":"etki@etki.me"}],"author":{"name":"AMA Team","email":"dev@amagroup.ru"},"bugs":{"url":"https://github.com/ama-team/voxengine-scenario-framework/issues"},"license":"MIT","readmeFilename":"README.md"}