├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── Makefile ├── README.md ├── TODO.md ├── bin └── am-types.js ├── examples ├── async-dialog │ ├── debug.sh │ ├── dialog.js │ └── start.sh ├── exception-state │ ├── debug.sh │ ├── exception-state.js │ └── start.sh ├── negotiation │ ├── debug.sh │ ├── negotation.js │ └── start.sh ├── piping │ ├── debug.sh │ ├── piping.js │ └── start.sh └── transitions │ ├── debug.sh │ ├── start.sh │ └── transitions.js ├── package-lock.json ├── package.json ├── pkg ├── README.md ├── asyncmachine.d.ts ├── asyncmachine.js ├── asyncmachine.js.map ├── bin ├── dist │ ├── asyncmachine.cjs.d.ts │ ├── asyncmachine.cjs.js │ ├── asyncmachine.cjs.js.map │ ├── asyncmachine.es6.d.ts │ ├── asyncmachine.es6.js │ ├── asyncmachine.es6.js.map │ ├── asyncmachine.umd.d.ts │ ├── asyncmachine.umd.js │ └── asyncmachine.umd.js.map ├── ee.d.ts ├── ee.js ├── ee.js.map ├── node_modules │ ├── commander │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── Readme.md │ │ ├── index.js │ │ ├── package.json │ │ └── typings │ │ │ └── index.d.ts │ └── simple-random-id │ │ ├── .jshintignore │ │ ├── .jshintrc │ │ ├── .npmignore │ │ ├── .travis.yml │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.js │ │ ├── package.json │ │ └── tests │ │ └── random-id_test.js ├── package-lock.json ├── package.json ├── shims.d.ts ├── shims.js ├── shims.js.map ├── transition.d.ts ├── transition.js ├── transition.js.map ├── types.d.ts ├── types.js └── types.js.map ├── rollup-es6.config.js ├── rollup-shims.config.js ├── rollup.config.js ├── src ├── asyncmachine.ts ├── ee.ts ├── shims.ts ├── transition.ts └── types.ts ├── test ├── exceptions.ts ├── mocha.opts ├── piping.ts ├── state-binding.ts ├── tests.ts ├── transition-steps.ts ├── tsconfig.json └── utils.ts ├── tsconfig.json ├── typings.json └── wallaby.conf.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /test/node_modules 3 | *~ 4 | .c9revisions 5 | /.idea 6 | /asyncmachine.iml 7 | /build/* 8 | /.vscode/ 9 | .DS_Store 10 | /src/**/*.js 11 | /src/**/*.js.map 12 | /src/**/*.d.ts 13 | /test/**/*.js 14 | /test/**/*.js.map 15 | /test/**/*.d.ts 16 | /tsickle_externs.js 17 | /docs 18 | /typings/ 19 | /.rpt2_cache/ 20 | /wiki 21 | /pkg-tmp/ 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### CHANGELOG 2 | 3 | ## 3.4 4 | 5 | - more granular piping 6 | - `add` relation parsed only for new states 7 | - fixed early resolve for `when()` and `whenNot()` 8 | - avoid of queuing duplicate mutations 9 | 10 | ## v3.3 11 | 12 | - new npm package structure 13 | - fixed TS warnings 14 | - bundled d.ts files 15 | 16 | ## v3.2 17 | 18 | - better ./bin/am-types generator and type safety 19 | - auto resuming of postponed queues 20 | - `auto` states prepended to the currently executing queue 21 | - cancelled transition doesn't discard the transitions queued in the meantime 22 | - fixed piping without negotiation 23 | - better log handlers API 24 | - fixes for the `add` relation 25 | - bugfixes 26 | 27 | ## v3.1 28 | 29 | - extracted the `Transition` class 30 | - stricter compiler checks (nulls, implicit any, returns) 31 | - structurized transition steps 32 | - use currently executing queue when available 33 | - type safety for events and states (TS only) 34 | - types generator from JSON and classes (TS only) 35 | - fixed `addByCallback`/`Listener` getting fired only once 36 | - bugfixes 37 | 38 | ## v3.0 39 | 40 | - tail call optimizations and reduced number of stack frames 41 | - moved to the regular typescript (2.0) 42 | - better logging API 43 | - states array not passed to transition any more (use #from() and #to()) 44 | - machine IDs 45 | - multi states 46 | - exception state enhancements 47 | - reworked piping 48 | - state binding fixes 49 | - new build system (shims supported) 50 | - bugfixes 51 | 52 | ## v2.0 53 | 54 | - states clock 55 | - synchronous queue across composed asyncmachines 56 | - abort functions 57 | - exception handling 58 | - state negotiation fixes 59 | - state piping fixes 60 | - event namespaces are gone 61 | - non-negotiable transition phase 62 | - updated and extended API 63 | - log readability optimized 64 | - composition over inheritance 65 | - (almost) backwards compatible 66 | 67 | ## v1.0 68 | 69 | - initial release 70 | - TODO 71 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN=./node_modules/.bin 2 | MOCHA=./node_modules/mocha/bin/mocha 3 | 4 | all: 5 | make build 6 | make build-test 7 | 8 | build: 9 | -make build-ts 10 | make dist-es6 11 | make dist 12 | make dist-dts 13 | 14 | build-dev: 15 | $(BIN)/tsc --watch --isolatedModules 16 | 17 | dist: 18 | $(BIN)/rollup -c rollup.config.js 19 | 20 | dist-es6: 21 | $(BIN)/rollup -c rollup-es6.config.js 22 | 23 | dist-shims: 24 | $(BIN)/rollup -c rollup-shims.config.js 25 | 26 | build-ts: 27 | tsc 28 | 29 | build-ts-watch: 30 | tsc --watch 31 | 32 | dist-dts: 33 | # TODO move to dts-bundle.json 34 | ./node_modules/.bin/dts-bundle \ 35 | --name asyncmachine \ 36 | --main build/asyncmachine.d.ts \ 37 | --out asyncmachine-bundle.d.ts 38 | 39 | compile: 40 | $(BIN)/tsc --noEmit --pretty 41 | 42 | compile-watch: 43 | $(BIN)/tsc --watch --noEmit --pretty 44 | 45 | setup: 46 | npm install 47 | 48 | # make version version=x.x.x 49 | version: 50 | npm --no-git-tag-version --allow-same-version version $(version) 51 | 52 | cd pkg && \ 53 | npm --no-git-tag-version --allow-same-version version $(version) 54 | 55 | package: 56 | make build 57 | rm -Rf pkg-tmp 58 | cp -RL pkg pkg-tmp 59 | 60 | publish: 61 | make package 62 | cd pkg-tmp && \ 63 | npm publish 64 | 65 | test: 66 | @echo "Dont forget to build tests with `make test-build`" 67 | $(MOCHA) \ 68 | test/*.js 69 | 70 | test-build: 71 | -$(BIN)/tsc \ 72 | --isolatedModules \ 73 | --skipLibCheck \ 74 | -p test 75 | 76 | test-build-watch: 77 | -$(BIN)/tsc \ 78 | --isolatedModules \ 79 | --skipLibCheck \ 80 | --watch \ 81 | -p test 82 | 83 | # make test-grep GREP="test name" 84 | test-grep: 85 | $(MOCHA) \ 86 | --grep "$(GREP)" 87 | test/*.js 88 | 89 | test-debug: 90 | $(MOCHA) \ 91 | --inspect-brk \ 92 | --grep "$(GREP)" \ 93 | test/*.js 94 | 95 | test-grep-debug: 96 | $(MOCHA) \ 97 | --debug-brk \ 98 | --grep "$(GREP)" \ 99 | test/*.js 100 | 101 | docs: 102 | rm -R docs/api 103 | mkdir -p docs/api 104 | $(BIN)/typedoc \ 105 | --out docs/api \ 106 | --ignoreCompilerErrors \ 107 | --name AsyncMachine \ 108 | --theme minimal \ 109 | --excludeNotExported \ 110 | --excludePrivate \ 111 | --readme none \ 112 | --mode file \ 113 | src/asyncmachine.ts 114 | 115 | pdf: 116 | cd wiki && make pdf 117 | cp \ 118 | wiki/AsyncMachine-The-Definitive-Guide.pdf \ 119 | docs/AsyncMachine-The-Definitive-Guide.pdf 120 | 121 | .PHONY: build test docs 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **AsyncMachine** is a **relational state machine** (dependency graph) for a declarative flow control. 2 | 3 | Usages: 4 | 5 | * state management 6 | * parallel tasks 7 | * loose coupling 8 | * resource allocation / disposal 9 | * exception handling 10 | * fault tolerance 11 | * method cancellation 12 | 13 | It can be used as a single state machine, or a network composed of many. Gzipped code is 7.5kb. 14 | 15 | ## Install 16 | 17 | ``` 18 | npm i asyncmachine 19 | ``` 20 | 21 | ## Documentation 22 | 23 | * [AsyncMachine - The Definitive Guide](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide) (wiki)
24 | [PDF version](https://github.com/TobiaszCudnik/asyncmachine/raw/gh-pages/AsyncMachine-The-Definitive-Guide.pdf) (25 pages, 1.5mb) 25 | * [API docs](https://tobiaszcudnik.github.io/asyncmachine/api) (TypeScript) 26 | * [machine() factory](https://tobiaszcudnik.github.io/asyncmachine/api/index.html#machine) 27 | * [AsyncMachine class](https://tobiaszcudnik.github.io/asyncmachine/api/classes/asyncmachine.html) 28 | * [Transition class](https://tobiaszcudnik.github.io/asyncmachine/api/classes/transition.html) 29 | * [List of emitted events](https://tobiaszcudnik.github.io/asyncmachine/api/interfaces/iemit.html) 30 | * [Roadmap](https://github.com/TobiaszCudnik/asyncmachine/blob/master/TODO.md) 31 | 32 | Components: 33 | 34 | * [states](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#states) 35 | * [transitions](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#transitions) 36 | * [relations](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#state-relations) 37 | * [clocks](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#state-clocks) 38 | * [pipes](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#pipes---connections-between-machines) 39 | * [queues](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#queue-and-machine-locks) 40 | 41 | Features: 42 | 43 | * [synchronous mutations](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#mutating-the-state) 44 | * [state negotiation](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#aborting-by-negotiation-handlers) 45 | * [cancellation](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#abort-functions) 46 | * [automatic states](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#auto-states) 47 | * [exception handling](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#exception-as-a-state) 48 | * [visual inspector / debugger](https://github.com/TobiaszCudnik/asyncmachine-inspector) 49 | 50 | ## Examples 51 | 52 | ### Dry Wet 53 | 54 | This basic examples makes use of: [states](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#states), [transitions](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#transitions), [relations](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#state-relations) and [synchronous mutations](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#mutating-the-state). 55 | 56 | * [Edit on RunKit](https://runkit.com/tobiaszcudnik/5b1edd421eaec500126c11ce) 57 | * [Inspect on StackBlitz](https://stackblitz.com/edit/asyncmachine-example-dry-wet?file=index.ts) 58 | 59 | ```typescript 60 | import { machine } from 'asyncmachine' 61 | // define 62 | const state = { 63 | // state Wet when activated, requires state Water to be active 64 | Wet: { 65 | require: ['Water'] 66 | }, 67 | // state Dry when activated, will drop (de-activate) state Wet 68 | Dry: { 69 | drop: ['Wet'] 70 | }, 71 | // state Water when activated, will add (activate) state Wet and 72 | // drop (de-activate) state Dry 73 | Water: { 74 | add: ['Wet'], 75 | drop: ['Dry'] 76 | } 77 | } 78 | // initialize 79 | const example = machine(state) 80 | // initially the machine has no active states 81 | example.is() // -> [] 82 | // activate state Dry 83 | example.add('Dry') 84 | example.is() // -> [ 'Dry' ] 85 | // activate state Water, which will resolve the relations: 86 | // 1. Water activates Wet 87 | // 2. Wet requires Water 88 | // 3. Dry de-activates Wet 89 | // 4. Water de-activates Dry 90 | // 5. Water activates Wet 91 | example.add('Water') 92 | example.is() // -> [ 'Wet', 'Water' ] 93 | ``` 94 | 95 | [![example](https://raw.githubusercontent.com/TobiaszCudnik/asyncmachine/gh-pages/images/example.gif)](https://stackblitz.com/edit/asyncmachine-example-dry-wet?file=index.ts) 96 | 97 | ### Negotiation 98 | 99 | Presents how the [state negotiation](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#aborting-by-negotiation-handlers) works. 100 | 101 | * [Edit on RunKit](https://runkit.com/tobiaszcudnik/5b1ed850c6dc1f0012db1346) 102 | * [Inspect on StackBlitz](https://stackblitz.com/edit/asyncmachine-example-negotiation?file=index.ts) 103 | * [Source on GitHub](https://github.com/TobiaszCudnik/asyncmachine/tree/master/examples/negotiation) 104 | 105 | ### Async Dialog 106 | 107 | Presents the following concepts: [automatic states](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#auto-states), [synchronous mutations](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#mutating-the-state), [delayed mutations](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#delayed-mutations) and loose coupling. 108 | 109 | * [Edit on RunKit](https://runkit.com/tobiaszcudnik/5b1ede5f62717e0013877cdc) 110 | * [Inspect on StackBlitz](https://stackblitz.com/edit/asyncmachine-example-async-dialog?file=index.ts) 111 | * [Source on GitHub](https://github.com/TobiaszCudnik/asyncmachine/tree/master/examples/async-dialog) 112 | 113 | ### Exception State 114 | 115 | A simple fault tolerance (retrying) using the [Exception state](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#exception-as-a-state). 116 | 117 | * [Edit on RunKit](https://runkit.com/tobiaszcudnik/5b1ee7113321180012ebafcf) 118 | * [Inspect on Stackblitz](https://stackblitz.com/edit/asyncmachine-example-exception?file=index.ts) 119 | * [Source on GitHub](https://github.com/TobiaszCudnik/asyncmachine/tree/master/examples/exception-state) 120 | 121 | ### Piping 122 | 123 | Shows how [pipes](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#pipes---connections-between-machines) forward states between machines. 124 | 125 | * [Edit on RunKit](https://runkit.com/tobiaszcudnik/5b1eea671eaec500126c1be7) 126 | * [Inspect on Stackblitz](https://stackblitz.com/edit/asyncmachine-example-piping?file=index.ts) 127 | * [Source on GitHub](https://github.com/TobiaszCudnik/asyncmachine/tree/master/examples/piping) 128 | 129 | ### Transitions 130 | 131 | Shows various types of [transition handlers](https://github.com/TobiaszCudnik/asyncmachine/wiki/AsyncMachine-The-Definitive-Guide#transition-handlers) and the way params get passed to them. 132 | 133 | * [Edit on RunKit](https://runkit.com/tobiaszcudnik/5b1eeaba3b97b60012c83ec0) 134 | * [Inspect on Stackblitz](https://stackblitz.com/edit/asyncmachine-example-transitions?file=index.ts) 135 | * [Source on GitHub](https://github.com/TobiaszCudnik/asyncmachine/tree/master/examples/transitions) 136 | 137 | ### TodoMVC and React 138 | 139 | Classic TodoMCV example using **AsyncMachine** as the controller and **React** as the view. 140 | 141 | * [Edit on Stackblitz](https://stackblitz.com/edit/asyncmachine-example-todomvc?file=src/controller.js) 142 | * [Source on GitHub](https://github.com/TobiaszCudnik/todomvc-asyncmachine) 143 | 144 | ### State streams with RxJS 145 | 146 | Observe state changes and navigate through specific paths with RxJS, then feed the result back as a state. 147 | 148 | * Comming soon! 149 | 150 | ### Restaurant 151 | 152 | A complex example showing how to solve the **producer / consumer problem** using AsyncMachine. 153 | 154 | * [Inspect on StackBlitz](https://stackblitz.com/edit/asyncmachine-inspector-restaurant) 155 | * [Source on GitHub](https://github.com/TobiaszCudnik/asyncmachine-inspector/tree/master/examples/restaurant) 156 | 157 | [![inspector view](https://raw.githubusercontent.com/TobiaszCudnik/asyncmachine/gh-pages/images/restaurant.png)](https://stackblitz.com/edit/asyncmachine-inspector-restaurant) 158 | 159 | ### TaskBot 160 | 161 | For a real world example check [TaskBot](https://github.com/TaskSync/TaskBot.app/tree/master/src) - a real-time sync engine for Google APIs. 162 | 163 | [![Preview](http://tobiaszcudnik.github.io/asyncmachine-inspector/sample.png)](http://tobiaszcudnik.github.io/asyncmachine-inspector/sample.mp4) 164 | 165 | ## License 166 | 167 | MIT 168 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## 3.x 4 | - add more comments to the initial example in the readme 5 | - handle exceptions happening in an ASYNC Exception_state handler 6 | - honor `this.machine.print_exception` 7 | - accept transition handlers in the `machine()` param 8 | - indicate "in transition" for statesToString() 9 | - fix the `@ts-ignore` injection for d.ts files 10 | - ability to `npm link` directly from /build 11 | - move the dist files to /build/dist 12 | - validate state names in relations 13 | - rxjs integration examples 14 | - parsing state sets coming from a group of machines 15 | - parse the stream of states and "mine" complex states eg user behavior 16 | - demos on stackblitz 17 | - test `createChild()` 18 | - test `Exception_state` params 19 | - test params for requested state handlers 20 | - implement `pipeRemoveBinding(binding)` 21 | - implement `unregister(state)` 22 | - fix / correct the broken tests 23 | - option to throw in case the queue is being used 24 | - used when every mutation is expected to be synchronous 25 | - managed child-machines 26 | - cant mutate the state directly, only the parent machine can (one parent) 27 | - parent mutates the state via piping only 28 | - this way the mutations is ALWAYS synchronous 29 | 30 | ## 4.x 31 | - state aliases - same state called by >1 name 32 | - ability to change the name of the transition handler per state 33 | - eg state `Foo_55: {handler: 'Foo'}` 34 | - allows to define dynamic state (eg many elements) with a predefined handler 35 | - alias `add`/`drop` to `activate`/`deactivate` 36 | - alias `is()` as `currentState()` 37 | - rename `transition()` to `get transition()` 38 | - and alias as `get current_transition` 39 | - rename `PipeFlags` to match the event names 40 | - eg `NEGOTIATION_ENTER` is `enter`, `FINAL_EXIT` is `end` 41 | - `debug` method 42 | - uses queries and if they match goes into `debugger` 43 | - `debug(DEBUG.ADD, [machine_id], ['Foo'])` 44 | - `debug(DEBUG.LOG, [machine_id], '[bind:on] HistoryIdFetched')` 45 | - easy way to wait on a drained queue from >1 machine 46 | - new way of handling queue race conditions 47 | - check for every queue entry instead of transition.ts 48 | - TypeScript 3.0 compat 49 | - align /bin/am-types 50 | - include prettier in the workflow 51 | - state groups - `FooA`, `FooB`, `FooC`, when all in group `Foo` #engine #api 52 | - then only one can be active at a time 53 | - defined by `group` or `switch` or `switch_group` 54 | - `#now()` -> `{ state: clock, state2: clock }` #api 55 | - `#wasAfter(#now(), #now())` but with a better name 56 | - `#is({A: 1, b: 34}): boolean` 57 | - maybe: in case of an Exception all the target states should be set and then 58 | - dropped, causing a Broken_Exception() transition handlers to kick in 59 | - currently you have to define Exception_state and check target_states param 60 | - move configs to ./configs 61 | - resolve relations using BFS/DFS to achieve full propagation 62 | - sideEffects: false in package.json 63 | - #toggle(name) #api 64 | - #has(name) #api 65 | - merge #when and #whenNot 66 | - #when(['Foo', 'Bar'], ['Baz']) fires when +Foo+Bar-Baz 67 | - #whenQueueDone - an async method returning when the whole queue got processed 68 | - used when `if (this.duringTransition()) await this.whenQueueDone();` 69 | - return an ES6 Set when applicable 70 | - #tick() triggers auto states but doesnt explicitly change anything 71 | - tests 72 | - align broken tests 73 | - handle all of those `// TODO write a test` places 74 | - manually specified queue for piping (piping from A to B using the queue from C) 75 | - ensure all the state lists and params are shallow copied #api #refactoring 76 | - `ins.implements(JSONState | AsyncMachine)` 77 | - return true if 78 | - all the states are implemented 79 | - relations BETWEEN THEM are the same 80 | 81 | ## Later 82 | 83 | - mount submodules - gh-pages into /docs and wiki into /wiki 84 | - stop auto states when Exception is active 85 | - make it possible to serialize a machine to JSON 86 | - no instance refs, indirect addressing, binary format 87 | - `machine` factory when used without am-types highlights with an error 88 | - FSM interface? 89 | - TimeArray decorator for states, counting times with moment.js API 90 | - eg number of state sets in last 10 minutes 91 | - useful for counting request quota limits for API clients 92 | - separate npm module 93 | - support timezones 94 | - investigate `[drop:implied]` 95 | - maybe: State_rejected() method, triggered when a certain state wasnt accepted 96 | - only for target, non-auto states 97 | - implement push signal for abort functions `abort.onAbort(listener)` 98 | - define machines as JSONs 99 | - state inheritance example (via object spread) 100 | - history API, optional #features 101 | - logged as an used queue 102 | - add destination states 103 | - time? 104 | - improve logs #debug 105 | - more consistent 106 | - more levels and better assignments 107 | - make stack traces as short as possible #debug 108 | - skip 2 stack frames from deferred 109 | - make Any state a real thing #engine 110 | - arguments? 111 | - synchronous throw when not in a promise #engine 112 | - edge case: piping negotiation, when a further state is cancelling 113 | - makes the piping inconsistent 114 | - for now, rely on the self transition 115 | - in the future, wait with the 3rd transition phase in the machine B 116 | till the negotiation is finished 117 | - GC and memory management #engine #api 118 | - track the context of all the bindings 119 | - auto unbinding unreachable transitions 120 | - auto unbinding unreachable promises' error handlers 121 | - memory leaks load tests 122 | - extend the multi states which create new machines (eg for requests) #? 123 | - separate / mixin / util function / decorator 124 | - remote state machines #? 125 | - separate / mixin / util function / decorator 126 | - case insensitive state names (when strings) #api 127 | - state as an object (shorter API calls, like `states.A.add()`) #maybe #api 128 | - considers signals composed out of event emitters (per each signal) 129 | - chai assertion helper #project #api 130 | 131 | ## WIKI / Def Guide 132 | 133 | * Queue duplicates detection 134 | * Mention in the docs - pipes, mutations 135 | * Make a comparison of `pipe`, `pipe negotiation` to `add`, `add & require` 136 | * Examples on stackblitz 137 | 138 | ## Transition 139 | 140 | * refactor the execution of calls to sth structured instead of strings 141 | * multi-step (keep couple of steps as one step) 142 | -------------------------------------------------------------------------------- /bin/am-types.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // TODO expose API 4 | // TODO provide a mapped type to for jsons 5 | // TODO make it a separate package 6 | // TODO watch mode 7 | // TODO read ts files directly 8 | // TODO dont import from /src/ 9 | // TODO integrate with pretter 10 | // - https://github.com/prettier/prettier/issues/4078 11 | 12 | const fs = require('fs') 13 | const path = require('path') 14 | const asyncmachine = require('../asyncmachine') 15 | const cli = require('commander') 16 | 17 | let cli_filename 18 | cli 19 | .usage(' [options]') 20 | .arguments(' [env]') 21 | .action(function(filename) { 22 | cli_filename = filename 23 | }) 24 | .option( 25 | '-o, --output [output]', 26 | 'save the result to a file, default `./-types.ts`' 27 | ) 28 | .option('-e, --export ', 'use the following export') 29 | .version('0.1.0') 30 | .on('--help', function() { 31 | console.log('') 32 | console.log(' Examples:') 33 | console.log('') 34 | console.log(' $ am-types file.js') 35 | console.log(' $ am-types file.json') 36 | console.log(' $ am-types file.js -s') 37 | console.log(' $ am-types file.js -s -e my_state') 38 | console.log('') 39 | }) 40 | cli.parse(process.argv) 41 | 42 | if (!cli_filename) { 43 | // TODO this should show the help screen 44 | console.error('Filename required, try --help') 45 | process.exit(1) 46 | } 47 | const filename = path.join(process.cwd(), cli_filename) 48 | const export_name = cli.export 49 | let output_path = cli.output 50 | if (output_path === true) { 51 | output_path = filename.replace(/(\.[^.]+$)/, '-types.ts') 52 | } 53 | 54 | if (!filename.match(/\.(js|json)$/i)) { 55 | console.error('Only JS and JSON input allowed at the moment') 56 | process.exit(1) 57 | } 58 | 59 | let states 60 | if (filename.match(/\.json$/)) 61 | states = Object.keys( 62 | JSON.parse(fs.readFileSync(filename, { encoding: 'utf8' })) 63 | ) 64 | else { 65 | let mod = require(filename) 66 | states = 67 | mod[export_name] || 68 | mod.state || 69 | mod.State || 70 | mod.states || 71 | mod.States || 72 | mod.default || 73 | mod 74 | 75 | if (typeof states == 'function') { 76 | let instance = new states() 77 | // if (instance instanceof asyncmachine.default) 78 | if (instance.states_all) states = new states().states_all 79 | } else { 80 | states = Object.keys(states) 81 | } 82 | } 83 | 84 | states.push('Exception') 85 | let transitions = [] 86 | 87 | for (let state1 of states) { 88 | for (let state2 of states) { 89 | if (state1 == state2) { 90 | if (state1 == 'Exception') continue 91 | state2 = 'Any' 92 | } 93 | transitions.push(state1 + '_' + state2) 94 | } 95 | transitions.push(state1 + '_exit') 96 | transitions.push(state1 + '_end') 97 | } 98 | 99 | states = states.filter(state => state != 'Exception') 100 | 101 | // ----- ----- ----- 102 | // PER STATE OUTPUT 103 | // ----- ----- ----- 104 | 105 | let output = 106 | `import { 107 | IState as IStateBase, 108 | IBind as IBindBase, 109 | IEmit as IEmitBase 110 | } from 'asyncmachine/types' 111 | import AsyncMachine from 'asyncmachine' 112 | 113 | export { IBindBase, IEmitBase, AsyncMachine } 114 | ` 115 | 116 | output += states 117 | .map( 118 | name => ` 119 | // ----- ----- ----- ----- ----- 120 | // STATE: ${name} 121 | // ----- ----- ----- ----- ----- 122 | 123 | /** machine.bind('${name}', (param1, param2) => {}) */ 124 | export interface IBind extends IBindBase { 125 | (event: '${name}_enter', listener: (/* param1: any?, param2: any? */) => boolean | undefined, context?: Object): this; 126 | (event: '${name}_state', listener: (/* param1: any?, param2: any? */) => any, context?: Object): this; 127 | } 128 | 129 | /** machine.emit('${name}', param1, param2) */ 130 | export interface IEmit extends IEmitBase { 131 | (event: '${name}_enter' /*, param1: any?, param2: any? */): boolean | void; 132 | (event: '${name}_state' /*, param1: any?, param2: any? */): boolean | void; 133 | } 134 | 135 | /** Method declarations */ 136 | export interface ITransitions { 137 | ${name}_enter?(/* param1: any?, param2: any? */): boolean | void; 138 | ${name}_state?(/* param1: any?, param2: any? */): boolean | void | Promise; 139 | } 140 | ` 141 | ) 142 | .join('') 143 | 144 | // ----- ----- ----- 145 | // COMBINED OUTPUT 146 | // ----- ----- ----- 147 | 148 | output += ` 149 | // ----- ----- ----- 150 | // GENERAL TYPES 151 | // ----- ----- ----- 152 | 153 | /** All the possible transition methods the machine can define */ 154 | export interface ITransitions {${transitions 155 | .map(t => t.endsWith('_end') 156 | ? `\n ${t}?(): boolean | void | Promise;` 157 | : `\n ${t}?(): boolean | void;`) 158 | .join('')} 159 | } 160 | 161 | /** All the state names */ 162 | export type TStates = '${states.join("'\n | '")}'; 163 | 164 | /** All the transition names */ 165 | export type TTransitions = '${transitions.join("'\n | '")}'; 166 | 167 | /** Typesafe state interface */ 168 | export interface IState extends IStateBase {} 169 | 170 | /** Subclassable typesafe state interface */ 171 | export interface IStateExt extends IStateBase {} 172 | 173 | export interface IBind extends IBindBase { 174 | // Non-params events and transitions 175 | (event: TTransitions, listener: () => boolean | void, context?: Object): this; 176 | } 177 | 178 | export interface IEmit extends IEmitBase { 179 | // Non-params events and transitions 180 | (event: TTransitions): boolean | void; 181 | } 182 | 183 | export interface IJSONStates { 184 | ${states.join(`: IState;\n `)}: IState; 185 | Exception?: IState; 186 | } 187 | ` 188 | 189 | if (output_path) { 190 | fs.writeFileSync(output_path, output) 191 | console.log(`Saved to ${output_path}`) 192 | } else { 193 | console.log(output) 194 | } 195 | -------------------------------------------------------------------------------- /examples/async-dialog/debug.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | node --inspect-brk dialog.js 4 | -------------------------------------------------------------------------------- /examples/async-dialog/dialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | AsyncMachine Async Dialog Example 3 | 4 | Scenario: 5 | 1. User clicks the button 6 | 2. Data download begins 7 | 3. Pre-loader appears 8 | 4. Once data is fetched, the dialog replaces the preloader 9 | 10 | This example presents the following concepts: 11 | - automatic states 12 | - synchronous mutations 13 | - delayed mutations 14 | - loose coupling (logic responsible for downloading data doesn't know 15 | anything about the pre-loader) 16 | 17 | Scroll down to see log outputs. 18 | 19 | @link https://github.com/TobiaszCudnik/asyncmachine 20 | */ 21 | 22 | const { machine } = require('asyncmachine') 23 | require('source-map-support').install() 24 | 25 | const state = { 26 | Enabled: {}, 27 | 28 | ButtonClicked: { 29 | require: ['Enabled'] 30 | }, 31 | 32 | ShowingDialog: {}, 33 | DialogVisible: { 34 | auto: true, 35 | drop: ['ShowingDialog'], 36 | require: ['DataDownloaded'] 37 | }, 38 | 39 | DownloadingData: { 40 | auto: true, 41 | require: ['ShowingDialog'] 42 | }, 43 | DataDownloaded: { 44 | drop: ['DownloadingData'] 45 | }, 46 | 47 | PreloaderVisible: { 48 | auto: true, 49 | require: ['DownloadingData'] 50 | } 51 | } 52 | 53 | class DialogManager { 54 | constructor(button, preloader) { 55 | this.preloader = preloader 56 | this.state = new machine(state) 57 | .logLevel(1) 58 | .id('DialogManager') 59 | .setTarget(this) 60 | 61 | this.dialog = new Dialog() 62 | button.addEventListener('click', this.state.addByListener('ButtonClicked')) 63 | } 64 | 65 | // Methods 66 | 67 | enable() { 68 | this.state.add('Enabled') 69 | } 70 | 71 | disable() { 72 | this.state.drop('Enabled') 73 | } 74 | 75 | // Transitions 76 | 77 | ButtonClicked_state() { 78 | this.state.add('ShowingDialog') 79 | // drop the state immediately after this transition 80 | this.state.drop('ButtonClicked') 81 | } 82 | 83 | DialogVisible_state() { 84 | // this.data is guaranteed by the DataDownloaded state 85 | this.dialog.show(this.data) 86 | } 87 | 88 | DownloadingData_state() { 89 | let abort = this.state.getAbort('DownloadingData') 90 | fetchData().then(data => { 91 | // break if the state is no longer set (or has been re-set) 92 | // during the async call 93 | if (abort()) return 94 | this.data = data 95 | this.state.add('DataDownloaded') 96 | }) 97 | } 98 | 99 | PreloaderVisible_state() { 100 | this.preloader.show() 101 | } 102 | 103 | PreloaderVisible_end() { 104 | this.preloader.hide() 105 | } 106 | } 107 | 108 | // --- Mock classes used in this example 109 | 110 | class Dialog { 111 | show() { 112 | console.log('Dialog shown') 113 | } 114 | } 115 | 116 | class Button { 117 | addEventListener(fn) {} 118 | } 119 | 120 | class Preloader { 121 | show() { 122 | console.log('Preloader show()') 123 | } 124 | hide() { 125 | console.log('Preloader hide()') 126 | } 127 | } 128 | 129 | function fetchData() { 130 | const data = [1, 2, 3] 131 | return new Promise(resolve => setTimeout(resolve.bind(data), 1000)) 132 | } 133 | 134 | // Create and run the instance 135 | 136 | const dm = new DialogManager(new Button(), new Preloader()) 137 | dm.enable() 138 | // simulate a button click 139 | dm.state.add('ButtonClicked') 140 | 141 | /* 142 | 143 | Log output (level 1): 144 | 145 | [DialogManager] [states] +Enabled 146 | [DialogManager] [states] +ButtonClicked 147 | [DialogManager] [states] +ShowingDialog 148 | [DialogManager] [states] +DownloadingData +PreloaderVisible 149 | Preloader show() 150 | [DialogManager] [states] -ButtonClicked 151 | [DialogManager] [states] +DataDownloaded -DownloadingData -PreloaderVisible 152 | Preloader hide() 153 | [DialogManager] [states] +DialogVisible -ShowingDialog 154 | Dialog shown 155 | 156 | Log output (level 2): 157 | 158 | [DialogManager] [add] Enabled 159 | [DialogManager] [states] +Enabled 160 | [DialogManager] [add] ButtonClicked 161 | [DialogManager] [states] +ButtonClicked 162 | [DialogManager] [transition] ButtonClicked_state 163 | [DialogManager] [queue:add] ShowingDialog 164 | [DialogManager] [queue:drop] ButtonClicked 165 | [DialogManager] [add] ShowingDialog 166 | [DialogManager] [states] +ShowingDialog 167 | [DialogManager] [states] +DownloadingData +PreloaderVisible 168 | [DialogManager] [transition] DownloadingData_state 169 | [DialogManager] [transition] PreloaderVisible_state 170 | Preloader show() 171 | [DialogManager] [drop] ButtonClicked 172 | [DialogManager] [states] -ButtonClicked 173 | [DialogManager] [add] DataDownloaded 174 | [DialogManager] [drop] DownloadingData by DataDownloaded 175 | [DialogManager] [rejected] PreloaderVisible(-DownloadingData) 176 | [DialogManager] [states] +DataDownloaded -DownloadingData -PreloaderVisible 177 | [DialogManager] [transition] PreloaderVisible_end 178 | Preloader hide() 179 | [DialogManager] [drop] ShowingDialog by DialogVisible 180 | [DialogManager] [states] +DialogVisible -ShowingDialog 181 | [DialogManager] [transition] DialogVisible_state 182 | Dialog shown 183 | 184 | */ 185 | -------------------------------------------------------------------------------- /examples/async-dialog/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | node dialog.js 4 | -------------------------------------------------------------------------------- /examples/exception-state/debug.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | node --inspect-brk exception-state.js 4 | -------------------------------------------------------------------------------- /examples/exception-state/exception-state.js: -------------------------------------------------------------------------------- 1 | /** 2 | AsyncMachine Exception Example 3 | * 4 | This example presents a simple fault tolerance (retrying) using the 5 | Exception state 6 | 7 | Scroll down to see the log output. 8 | 9 | @link https://github.com/TobiaszCudnik/asyncmachine 10 | */ 11 | 12 | const { machine } = require('asyncmachine') 13 | require('source-map-support').install() 14 | 15 | const example = machine(['Stable', 'Broken']) 16 | .id('') 17 | .logLevel(2) 18 | 19 | // state negotiation 20 | example.Broken_enter = function() { 21 | let clock = this.clock('Exception') 22 | console.log('Exception clock ==', clock) 23 | if (clock > 5) { 24 | console.log('Too many errors, quitting') 25 | return false 26 | } 27 | } 28 | 29 | // state set 30 | example.Broken_state = function() { 31 | throw Error('random exception') 32 | } 33 | 34 | // use the state negotiation for fault tolerance 35 | example.Exception_state = function( 36 | err, 37 | target_states, 38 | base_states, 39 | exception_src_handler, 40 | async_target_states 41 | ) { 42 | // try to rescue the Broken state 43 | if (target_states.includes('Broken')) { 44 | console.log('Retrying the Broken state') 45 | this.drop('Exception') 46 | this.add('Broken') 47 | } 48 | } 49 | 50 | example.add(['Stable', 'Broken']) 51 | console.log('state', example.is()) 52 | 53 | /* 54 | Log output (level 2): 55 | 56 | [add] Stable, Broken 57 | [transition] Broken_enter 58 | Exception clock == 0 59 | [state] +Stable +Broken 60 | [transition] Broken_state 61 | [exception] from Broken, forced states to Stable 62 | [state:force] Stable 63 | [add] Exception 64 | [state] +Exception 65 | [transition] Exception_state 66 | Retrying the Broken state 67 | [queue:drop] Exception 68 | [queue:add] Broken 69 | [drop] Exception 70 | [state] -Exception 71 | [add] Broken 72 | [transition] Broken_enter 73 | Exception clock == 1 74 | [state] +Broken 75 | [transition] Broken_state 76 | [exception] from Broken, forced states to Stable 77 | [state:force] Stable 78 | [add] Exception 79 | [state] +Exception 80 | [transition] Exception_state 81 | Retrying the Broken state 82 | [queue:drop] Exception 83 | [queue:add] Broken 84 | [drop] Exception 85 | [state] -Exception 86 | [add] Broken 87 | [transition] Broken_enter 88 | Exception clock == 2 89 | [state] +Broken 90 | [transition] Broken_state 91 | [exception] from Broken, forced states to Stable 92 | [state:force] Stable 93 | [add] Exception 94 | [state] +Exception 95 | [transition] Exception_state 96 | Retrying the Broken state 97 | [queue:drop] Exception 98 | [queue:add] Broken 99 | [drop] Exception 100 | [state] -Exception 101 | [add] Broken 102 | [transition] Broken_enter 103 | Exception clock == 3 104 | [state] +Broken 105 | [transition] Broken_state 106 | [exception] from Broken, forced states to Stable 107 | [state:force] Stable 108 | [add] Exception 109 | [state] +Exception 110 | [transition] Exception_state 111 | Retrying the Broken state 112 | [queue:drop] Exception 113 | [queue:add] Broken 114 | [drop] Exception 115 | [state] -Exception 116 | [add] Broken 117 | [transition] Broken_enter 118 | Exception clock == 4 119 | [state] +Broken 120 | [transition] Broken_state 121 | [exception] from Broken, forced states to Stable 122 | [state:force] Stable 123 | [add] Exception 124 | [state] +Exception 125 | [transition] Exception_state 126 | Retrying the Broken state 127 | [queue:drop] Exception 128 | [queue:add] Broken 129 | [drop] Exception 130 | [state] -Exception 131 | [add] Broken 132 | [transition] Broken_enter 133 | Exception clock == 5 134 | [state] +Broken 135 | [transition] Broken_state 136 | [exception] from Broken, forced states to Stable 137 | [state:force] Stable 138 | [add] Exception 139 | [state] +Exception 140 | [transition] Exception_state 141 | Retrying the Broken state 142 | [queue:drop] Exception 143 | [queue:add] Broken 144 | [drop] Exception 145 | [state] -Exception 146 | [add] Broken 147 | [transition] Broken_enter 148 | Exception clock == 6 149 | Too many errors, quitting 150 | [cancelled] Broken, Stable by the method Broken_enter 151 | state [ 'Stable' ] 152 | 153 | */ 154 | -------------------------------------------------------------------------------- /examples/exception-state/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | node exception-state.js 4 | -------------------------------------------------------------------------------- /examples/negotiation/debug.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | node --inspect-brk negotation.js 4 | -------------------------------------------------------------------------------- /examples/negotiation/negotation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AsyncMachine Negotiation Example 3 | * 4 | * This example presents how the state negotiation works. 5 | * Scroll down for the log output. 6 | * 7 | * @link https://github.com/TobiaszCudnik/asyncmachine 8 | */ 9 | 10 | const { machine } = require('asyncmachine') 11 | require('source-map-support').install() 12 | 13 | // setup the states 14 | const example = machine({ 15 | A: { add: ['Foo'] }, 16 | B: { add: ['Foo'] }, 17 | Foo: {} 18 | }) 19 | // setup logging 20 | example.id('').logLevel(2) 21 | 22 | // `Foo` agrees to be set indirectly only via `A` 23 | example.Foo_enter = function() { 24 | // the return type has to be boolean 25 | return Boolean(this.to().includes('A')) 26 | } 27 | 28 | example.add('B') // -> false 29 | example.add('A') // -> true 30 | console.log(example.is()) // -> ['A', 'Foo'] 31 | 32 | /* 33 | 34 | Log output (level 2): 35 | 36 | [add] B 37 | [add:implied] Foo 38 | [transition] Foo_enter 39 | [cancelled] B, Foo by the method Foo_enter 40 | [add] A 41 | [add:implied] Foo 42 | [transition] Foo_enter 43 | [state] +A +Foo 44 | [ 'A', 'Foo' ] 45 | 46 | */ 47 | -------------------------------------------------------------------------------- /examples/negotiation/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | node negotation.js 4 | -------------------------------------------------------------------------------- /examples/piping/debug.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | node --inspect-brk piping.js 4 | -------------------------------------------------------------------------------- /examples/piping/piping.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AsyncMachine Piping Example 3 | * 4 | * This example shows how piping states between machines works. 5 | * 6 | * Scroll down to see log the output. 7 | * 8 | * @link https://github.com/TobiaszCudnik/asyncmachine 9 | */ 10 | 11 | const { machine, PipeFlags } = require('asyncmachine') 12 | require('source-map-support').install() 13 | 14 | const foo = machine({ 15 | A: {}, 16 | B: {} 17 | }) 18 | .id('foo') 19 | .logLevel(2) 20 | 21 | const bar = machine({ 22 | AA: {}, 23 | NotB: {} 24 | }) 25 | .id('bar') 26 | .logLevel(2) 27 | 28 | const baz = machine({ 29 | NotB: {} 30 | }) 31 | .id('baz') 32 | .logLevel(2) 33 | 34 | console.log('\ncreate the pipes (and sync the state)\n') 35 | 36 | foo.pipe('A', bar, 'AA') 37 | foo.pipe('B', baz, 'NotB', PipeFlags.INVERT) 38 | baz.pipe('NotB', bar, 'NotB') 39 | 40 | console.log('\n start mutating [foo] only, and others will follow \n') 41 | 42 | foo.add('A') 43 | foo.add('B') 44 | foo.drop('B') 45 | 46 | /* 47 | Console output: 48 | 49 | create the pipes (and sync the state) 50 | 51 | [foo] [pipe] 'A' as 'AA' to 'bar' 52 | [bar] [drop] AA 53 | [foo] [pipe:invert] 'B' as 'NotB' to 'baz' 54 | [baz] [add] NotB 55 | [baz] [state] +NotB 56 | [baz] [pipe] 'NotB' as 'NotB' to 'bar' 57 | [bar] [add] NotB 58 | [bar] [state] +NotB 59 | 60 | start mutating [foo] only, and others will follow 61 | 62 | [foo] [add] A 63 | [foo] [state] +A 64 | [bar] [add] AA 65 | [bar] [state] +AA 66 | [foo] [add] B 67 | [foo] [state] +B 68 | [baz] [drop] NotB 69 | [baz] [state] -NotB 70 | [bar] [drop] NotB 71 | [bar] [state] -NotB 72 | [foo] [drop] B 73 | [foo] [state] -B 74 | [baz] [add] NotB 75 | [baz] [state] +NotB 76 | [bar] [add] NotB 77 | [bar] [state] +NotB 78 | */ -------------------------------------------------------------------------------- /examples/piping/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | node piping.js 4 | -------------------------------------------------------------------------------- /examples/transitions/debug.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | node --inspect-brk transitions.js 4 | -------------------------------------------------------------------------------- /examples/transitions/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | node transitions.js 4 | -------------------------------------------------------------------------------- /examples/transitions/transitions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AsyncMachine Transition Handlers Example 3 | * 4 | * This example shows various types of Transition Handlers and the way params 5 | * get passed to them: 6 | * - negotiation handlers 7 | * - final handlers 8 | * - self handlers 9 | * 10 | * It also makes use of Relations (`add`) to present the difference between 11 | * Requested and Implied states. 12 | * 13 | * The handlers are using `this.to()` and `this.is()` methods fetch the Target 14 | * States of transitions. 15 | * 16 | * Scroll down to see the log output. 17 | * 18 | * @link https://github.com/TobiaszCudnik/asyncmachine 19 | */ 20 | 21 | const { machine } = require('asyncmachine') 22 | const assert = require('assert') 23 | 24 | const example = machine({ 25 | Requested: { add: ['Implied'] }, 26 | Implied: {}, 27 | Delayed: {} 28 | }) 29 | example.id('').logLevel(3) 30 | 31 | // SETUP TRANSITIONS 32 | 33 | example.Requested_enter = function(name, value) { 34 | // target states inside of a negotiation handler are available as `this.to()` 35 | assert(this.to().includes('Requested')) 36 | assert(this.to().includes('Implied')) 37 | assert(name == 'john') 38 | assert(value == 5) 39 | } 40 | 41 | example.Requested_state = function(name, value) { 42 | assert(this.is().includes('Requested')) 43 | assert(this.is().includes('Implied')) 44 | assert(name == 'john') 45 | assert(value == 5) 46 | 47 | this.addByListener('Delayed')(name, value) 48 | } 49 | 50 | example.Implied_state = function(name, value) { 51 | assert(this.is().includes('Requested')) 52 | assert(this.is().includes('Implied')) 53 | // only the Requested states get the call params passed through 54 | assert(name === undefined) 55 | assert(value === undefined) 56 | } 57 | 58 | example.Delayed_state = function(name, value) { 59 | assert(this.is().includes('Delayed')) 60 | assert(this.is().includes('Requested')) 61 | assert(this.is().includes('Implied')) 62 | assert(name == 'john') 63 | assert(value == 5) 64 | 65 | // this will trigger a self transition, using the node-style callback 66 | this.addByCallback('Delayed')(null, name, value) 67 | } 68 | 69 | example.Delayed_Delayed = function(name, value) { 70 | assert(this.is().includes('Delayed')) 71 | assert(this.is().includes('Requested')) 72 | assert(this.is().includes('Implied')) 73 | assert(name == 'john') 74 | assert(value == 5) 75 | } 76 | 77 | // RUN 78 | 79 | example.add('Requested', 'john', 5) 80 | 81 | /* 82 | Log output (level 3): 83 | 84 | [add] Requested 85 | [add:implied] Implied 86 | [transition] Requested_enter 87 | [state] +Requested +Implied 88 | [transition] Requested_state 89 | [queue:add] Delayed 90 | [postpone] postponing the transition, the queue is running 91 | [transition] Implied_state 92 | [add] Delayed 93 | [state] +Delayed 94 | Requested, Implied 95 | [transition] Delayed_state 96 | [queue:add] Delayed 97 | [postpone] postponing the transition, the queue is running 98 | [add] Delayed 99 | [transition] Delayed_Delayed 100 | [state] Delayed, Requested, Implied 101 | */ 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asyncmachine", 3 | "version": "3.4.1", 4 | "description": "Relational State Machine", 5 | "keywords": [ 6 | "async", 7 | "statemachine", 8 | "eventemitter", 9 | "states", 10 | "transition", 11 | "eventloop", 12 | "declarative", 13 | "relational", 14 | "aop", 15 | "fsm", 16 | "actor-model", 17 | "dependency-graph" 18 | ], 19 | "author": "Tobiasz Cudnik ", 20 | "license": "MIT", 21 | "url": "https://github.com/TobiaszCudnik/asyncmachine", 22 | "repository": { 23 | "type": "git", 24 | "url": "http://github.com/TobiaszCudnik/asyncmachine.git" 25 | }, 26 | "devDependencies": { 27 | "chai": "^3.5.0", 28 | "core-js": "^2.4.1", 29 | "dts-bundle": "^0.7.3", 30 | "mocha": "^5.2.0", 31 | "prettier": "^1.12.1", 32 | "replace-in-file": "^3.4.0", 33 | "rollup": "^0.51.8", 34 | "rollup-plugin-commonjs": "^8.3.0", 35 | "rollup-plugin-execute": "^1.0.0", 36 | "rollup-plugin-node-resolve": "^3.0.3", 37 | "rollup-plugin-typescript2": "^0.11.1", 38 | "rollup-plugin-uglify": "^2.0.1", 39 | "rollup-watch": "^4.3.1", 40 | "sinon": "^1.17.4", 41 | "sinon-chai": "^2.8.0", 42 | "source-map-support": "^0.5.6", 43 | "typedoc": "^0.11.1", 44 | "typescript": "^2.8", 45 | "uglify-es": "^3.3.9" 46 | }, 47 | "scripts": { 48 | "build": "make build", 49 | "test": "make test-build; make test", 50 | "prepare": "make build" 51 | }, 52 | "main": "build/asyncmachine.js", 53 | "types": "build/asyncmachine.d.ts", 54 | "module": "build/asyncmachine.es6.js", 55 | "browser": "build/asyncmachine.umd.js", 56 | "dependencies": { 57 | "@types/mocha": "^7.0.2", 58 | "commander": "^2.14.1", 59 | "simple-random-id": "^1.0.1" 60 | }, 61 | "bin": { 62 | "am-types": "./bin/am-types.js" 63 | }, 64 | "prettier": { 65 | "singleQuote": true, 66 | "semi": false 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /pkg/README.md: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/README.md -------------------------------------------------------------------------------- /pkg/asyncmachine.d.ts: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/asyncmachine.d.ts -------------------------------------------------------------------------------- /pkg/asyncmachine.js: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/asyncmachine.js -------------------------------------------------------------------------------- /pkg/asyncmachine.js.map: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/asyncmachine.js.map -------------------------------------------------------------------------------- /pkg/bin: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/bin -------------------------------------------------------------------------------- /pkg/dist/asyncmachine.cjs.d.ts: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/asyncmachine-bundle.d.ts -------------------------------------------------------------------------------- /pkg/dist/asyncmachine.cjs.js: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/asyncmachine.cjs.js -------------------------------------------------------------------------------- /pkg/dist/asyncmachine.cjs.js.map: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/asyncmachine.cjs.js.map -------------------------------------------------------------------------------- /pkg/dist/asyncmachine.es6.d.ts: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/asyncmachine-bundle.d.ts -------------------------------------------------------------------------------- /pkg/dist/asyncmachine.es6.js: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/asyncmachine.es6.js -------------------------------------------------------------------------------- /pkg/dist/asyncmachine.es6.js.map: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/asyncmachine.es6.js.map -------------------------------------------------------------------------------- /pkg/dist/asyncmachine.umd.d.ts: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/asyncmachine-bundle.d.ts -------------------------------------------------------------------------------- /pkg/dist/asyncmachine.umd.js: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/asyncmachine.umd.js -------------------------------------------------------------------------------- /pkg/dist/asyncmachine.umd.js.map: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/asyncmachine.umd.js.map -------------------------------------------------------------------------------- /pkg/ee.d.ts: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/ee.d.ts -------------------------------------------------------------------------------- /pkg/ee.js: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/ee.js -------------------------------------------------------------------------------- /pkg/ee.js.map: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/ee.js.map -------------------------------------------------------------------------------- /pkg/node_modules/commander/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 2.15.0 / 2018-03-07 3 | ================== 4 | 5 | * Update downloads badge to point to graph of downloads over time instead of duplicating link to npm 6 | * Arguments description 7 | 8 | 2.14.1 / 2018-02-07 9 | ================== 10 | 11 | * Fix typing of help function 12 | 13 | 2.14.0 / 2018-02-05 14 | ================== 15 | 16 | * only register the option:version event once 17 | * Fixes issue #727: Passing empty string for option on command is set to undefined 18 | * enable eqeqeq rule 19 | * resolves #754 add linter configuration to project 20 | * resolves #560 respect custom name for version option 21 | * document how to override the version flag 22 | * document using options per command 23 | 24 | 2.13.0 / 2018-01-09 25 | ================== 26 | 27 | * Do not print default for --no- 28 | * remove trailing spaces in command help 29 | * Update CI's Node.js to LTS and latest version 30 | * typedefs: Command and Option types added to commander namespace 31 | 32 | 2.12.2 / 2017-11-28 33 | ================== 34 | 35 | * fix: typings are not shipped 36 | 37 | 2.12.1 / 2017-11-23 38 | ================== 39 | 40 | * Move @types/node to dev dependency 41 | 42 | 2.12.0 / 2017-11-22 43 | ================== 44 | 45 | * add attributeName() method to Option objects 46 | * Documentation updated for options with --no prefix 47 | * typings: `outputHelp` takes a string as the first parameter 48 | * typings: use overloads 49 | * feat(typings): update to match js api 50 | * Print default value in option help 51 | * Fix translation error 52 | * Fail when using same command and alias (#491) 53 | * feat(typings): add help callback 54 | * fix bug when description is add after command with options (#662) 55 | * Format js code 56 | * Rename History.md to CHANGELOG.md (#668) 57 | * feat(typings): add typings to support TypeScript (#646) 58 | * use current node 59 | 60 | 2.11.0 / 2017-07-03 61 | ================== 62 | 63 | * Fix help section order and padding (#652) 64 | * feature: support for signals to subcommands (#632) 65 | * Fixed #37, --help should not display first (#447) 66 | * Fix translation errors. (#570) 67 | * Add package-lock.json 68 | * Remove engines 69 | * Upgrade package version 70 | * Prefix events to prevent conflicts between commands and options (#494) 71 | * Removing dependency on graceful-readlink 72 | * Support setting name in #name function and make it chainable 73 | * Add .vscode directory to .gitignore (Visual Studio Code metadata) 74 | * Updated link to ruby commander in readme files 75 | 76 | 2.10.0 / 2017-06-19 77 | ================== 78 | 79 | * Update .travis.yml. drop support for older node.js versions. 80 | * Fix require arguments in README.md 81 | * On SemVer you do not start from 0.0.1 82 | * Add missing semi colon in readme 83 | * Add save param to npm install 84 | * node v6 travis test 85 | * Update Readme_zh-CN.md 86 | * Allow literal '--' to be passed-through as an argument 87 | * Test subcommand alias help 88 | * link build badge to master branch 89 | * Support the alias of Git style sub-command 90 | * added keyword commander for better search result on npm 91 | * Fix Sub-Subcommands 92 | * test node.js stable 93 | * Fixes TypeError when a command has an option called `--description` 94 | * Update README.md to make it beginner friendly and elaborate on the difference between angled and square brackets. 95 | * Add chinese Readme file 96 | 97 | 2.9.0 / 2015-10-13 98 | ================== 99 | 100 | * Add option `isDefault` to set default subcommand #415 @Qix- 101 | * Add callback to allow filtering or post-processing of help text #434 @djulien 102 | * Fix `undefined` text in help information close #414 #416 @zhiyelee 103 | 104 | 2.8.1 / 2015-04-22 105 | ================== 106 | 107 | * Back out `support multiline description` Close #396 #397 108 | 109 | 2.8.0 / 2015-04-07 110 | ================== 111 | 112 | * Add `process.execArg` support, execution args like `--harmony` will be passed to sub-commands #387 @DigitalIO @zhiyelee 113 | * Fix bug in Git-style sub-commands #372 @zhiyelee 114 | * Allow commands to be hidden from help #383 @tonylukasavage 115 | * When git-style sub-commands are in use, yet none are called, display help #382 @claylo 116 | * Add ability to specify arguments syntax for top-level command #258 @rrthomas 117 | * Support multiline descriptions #208 @zxqfox 118 | 119 | 2.7.1 / 2015-03-11 120 | ================== 121 | 122 | * Revert #347 (fix collisions when option and first arg have same name) which causes a bug in #367. 123 | 124 | 2.7.0 / 2015-03-09 125 | ================== 126 | 127 | * Fix git-style bug when installed globally. Close #335 #349 @zhiyelee 128 | * Fix collisions when option and first arg have same name. Close #346 #347 @tonylukasavage 129 | * Add support for camelCase on `opts()`. Close #353 @nkzawa 130 | * Add node.js 0.12 and io.js to travis.yml 131 | * Allow RegEx options. #337 @palanik 132 | * Fixes exit code when sub-command failing. Close #260 #332 @pirelenito 133 | * git-style `bin` files in $PATH make sense. Close #196 #327 @zhiyelee 134 | 135 | 2.6.0 / 2014-12-30 136 | ================== 137 | 138 | * added `Command#allowUnknownOption` method. Close #138 #318 @doozr @zhiyelee 139 | * Add application description to the help msg. Close #112 @dalssoft 140 | 141 | 2.5.1 / 2014-12-15 142 | ================== 143 | 144 | * fixed two bugs incurred by variadic arguments. Close #291 @Quentin01 #302 @zhiyelee 145 | 146 | 2.5.0 / 2014-10-24 147 | ================== 148 | 149 | * add support for variadic arguments. Closes #277 @whitlockjc 150 | 151 | 2.4.0 / 2014-10-17 152 | ================== 153 | 154 | * fixed a bug on executing the coercion function of subcommands option. Closes #270 155 | * added `Command.prototype.name` to retrieve command name. Closes #264 #266 @tonylukasavage 156 | * added `Command.prototype.opts` to retrieve all the options as a simple object of key-value pairs. Closes #262 @tonylukasavage 157 | * fixed a bug on subcommand name. Closes #248 @jonathandelgado 158 | * fixed function normalize doesn’t honor option terminator. Closes #216 @abbr 159 | 160 | 2.3.0 / 2014-07-16 161 | ================== 162 | 163 | * add command alias'. Closes PR #210 164 | * fix: Typos. Closes #99 165 | * fix: Unused fs module. Closes #217 166 | 167 | 2.2.0 / 2014-03-29 168 | ================== 169 | 170 | * add passing of previous option value 171 | * fix: support subcommands on windows. Closes #142 172 | * Now the defaultValue passed as the second argument of the coercion function. 173 | 174 | 2.1.0 / 2013-11-21 175 | ================== 176 | 177 | * add: allow cflag style option params, unit test, fixes #174 178 | 179 | 2.0.0 / 2013-07-18 180 | ================== 181 | 182 | * remove input methods (.prompt, .confirm, etc) 183 | 184 | 1.3.2 / 2013-07-18 185 | ================== 186 | 187 | * add support for sub-commands to co-exist with the original command 188 | 189 | 1.3.1 / 2013-07-18 190 | ================== 191 | 192 | * add quick .runningCommand hack so you can opt-out of other logic when running a sub command 193 | 194 | 1.3.0 / 2013-07-09 195 | ================== 196 | 197 | * add EACCES error handling 198 | * fix sub-command --help 199 | 200 | 1.2.0 / 2013-06-13 201 | ================== 202 | 203 | * allow "-" hyphen as an option argument 204 | * support for RegExp coercion 205 | 206 | 1.1.1 / 2012-11-20 207 | ================== 208 | 209 | * add more sub-command padding 210 | * fix .usage() when args are present. Closes #106 211 | 212 | 1.1.0 / 2012-11-16 213 | ================== 214 | 215 | * add git-style executable subcommand support. Closes #94 216 | 217 | 1.0.5 / 2012-10-09 218 | ================== 219 | 220 | * fix `--name` clobbering. Closes #92 221 | * fix examples/help. Closes #89 222 | 223 | 1.0.4 / 2012-09-03 224 | ================== 225 | 226 | * add `outputHelp()` method. 227 | 228 | 1.0.3 / 2012-08-30 229 | ================== 230 | 231 | * remove invalid .version() defaulting 232 | 233 | 1.0.2 / 2012-08-24 234 | ================== 235 | 236 | * add `--foo=bar` support [arv] 237 | * fix password on node 0.8.8. Make backward compatible with 0.6 [focusaurus] 238 | 239 | 1.0.1 / 2012-08-03 240 | ================== 241 | 242 | * fix issue #56 243 | * fix tty.setRawMode(mode) was moved to tty.ReadStream#setRawMode() (i.e. process.stdin.setRawMode()) 244 | 245 | 1.0.0 / 2012-07-05 246 | ================== 247 | 248 | * add support for optional option descriptions 249 | * add defaulting of `.version()` to package.json's version 250 | 251 | 0.6.1 / 2012-06-01 252 | ================== 253 | 254 | * Added: append (yes or no) on confirmation 255 | * Added: allow node.js v0.7.x 256 | 257 | 0.6.0 / 2012-04-10 258 | ================== 259 | 260 | * Added `.prompt(obj, callback)` support. Closes #49 261 | * Added default support to .choose(). Closes #41 262 | * Fixed the choice example 263 | 264 | 0.5.1 / 2011-12-20 265 | ================== 266 | 267 | * Fixed `password()` for recent nodes. Closes #36 268 | 269 | 0.5.0 / 2011-12-04 270 | ================== 271 | 272 | * Added sub-command option support [itay] 273 | 274 | 0.4.3 / 2011-12-04 275 | ================== 276 | 277 | * Fixed custom help ordering. Closes #32 278 | 279 | 0.4.2 / 2011-11-24 280 | ================== 281 | 282 | * Added travis support 283 | * Fixed: line-buffered input automatically trimmed. Closes #31 284 | 285 | 0.4.1 / 2011-11-18 286 | ================== 287 | 288 | * Removed listening for "close" on --help 289 | 290 | 0.4.0 / 2011-11-15 291 | ================== 292 | 293 | * Added support for `--`. Closes #24 294 | 295 | 0.3.3 / 2011-11-14 296 | ================== 297 | 298 | * Fixed: wait for close event when writing help info [Jerry Hamlet] 299 | 300 | 0.3.2 / 2011-11-01 301 | ================== 302 | 303 | * Fixed long flag definitions with values [felixge] 304 | 305 | 0.3.1 / 2011-10-31 306 | ================== 307 | 308 | * Changed `--version` short flag to `-V` from `-v` 309 | * Changed `.version()` so it's configurable [felixge] 310 | 311 | 0.3.0 / 2011-10-31 312 | ================== 313 | 314 | * Added support for long flags only. Closes #18 315 | 316 | 0.2.1 / 2011-10-24 317 | ================== 318 | 319 | * "node": ">= 0.4.x < 0.7.0". Closes #20 320 | 321 | 0.2.0 / 2011-09-26 322 | ================== 323 | 324 | * Allow for defaults that are not just boolean. Default peassignment only occurs for --no-*, optional, and required arguments. [Jim Isaacs] 325 | 326 | 0.1.0 / 2011-08-24 327 | ================== 328 | 329 | * Added support for custom `--help` output 330 | 331 | 0.0.5 / 2011-08-18 332 | ================== 333 | 334 | * Changed: when the user enters nothing prompt for password again 335 | * Fixed issue with passwords beginning with numbers [NuckChorris] 336 | 337 | 0.0.4 / 2011-08-15 338 | ================== 339 | 340 | * Fixed `Commander#args` 341 | 342 | 0.0.3 / 2011-08-15 343 | ================== 344 | 345 | * Added default option value support 346 | 347 | 0.0.2 / 2011-08-15 348 | ================== 349 | 350 | * Added mask support to `Command#password(str[, mask], fn)` 351 | * Added `Command#password(str, fn)` 352 | 353 | 0.0.1 / 2010-01-03 354 | ================== 355 | 356 | * Initial release 357 | -------------------------------------------------------------------------------- /pkg/node_modules/commander/LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2011 TJ Holowaychuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /pkg/node_modules/commander/Readme.md: -------------------------------------------------------------------------------- 1 | # Commander.js 2 | 3 | 4 | [![Build Status](https://api.travis-ci.org/tj/commander.js.svg?branch=master)](http://travis-ci.org/tj/commander.js) 5 | [![NPM Version](http://img.shields.io/npm/v/commander.svg?style=flat)](https://www.npmjs.org/package/commander) 6 | [![NPM Downloads](https://img.shields.io/npm/dm/commander.svg?style=flat)](https://npmcharts.com/compare/commander?minimal=true) 7 | [![Join the chat at https://gitter.im/tj/commander.js](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/tj/commander.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | 9 | The complete solution for [node.js](http://nodejs.org) command-line interfaces, inspired by Ruby's [commander](https://github.com/commander-rb/commander). 10 | [API documentation](http://tj.github.com/commander.js/) 11 | 12 | 13 | ## Installation 14 | 15 | $ npm install commander --save 16 | 17 | ## Option parsing 18 | 19 | Options with commander are defined with the `.option()` method, also serving as documentation for the options. The example below parses args and options from `process.argv`, leaving remaining args as the `program.args` array which were not consumed by options. 20 | 21 | ```js 22 | #!/usr/bin/env node 23 | 24 | /** 25 | * Module dependencies. 26 | */ 27 | 28 | var program = require('commander'); 29 | 30 | program 31 | .version('0.1.0') 32 | .option('-p, --peppers', 'Add peppers') 33 | .option('-P, --pineapple', 'Add pineapple') 34 | .option('-b, --bbq-sauce', 'Add bbq sauce') 35 | .option('-c, --cheese [type]', 'Add the specified type of cheese [marble]', 'marble') 36 | .parse(process.argv); 37 | 38 | console.log('you ordered a pizza with:'); 39 | if (program.peppers) console.log(' - peppers'); 40 | if (program.pineapple) console.log(' - pineapple'); 41 | if (program.bbqSauce) console.log(' - bbq'); 42 | console.log(' - %s cheese', program.cheese); 43 | ``` 44 | 45 | Short flags may be passed as a single arg, for example `-abc` is equivalent to `-a -b -c`. Multi-word options such as "--template-engine" are camel-cased, becoming `program.templateEngine` etc. 46 | 47 | Note that multi-word options starting with `--no` prefix negate the boolean value of the following word. For example, `--no-sauce` sets the value of `program.sauce` to false. 48 | 49 | ```js 50 | #!/usr/bin/env node 51 | 52 | /** 53 | * Module dependencies. 54 | */ 55 | 56 | var program = require('commander'); 57 | 58 | program 59 | .option('--no-sauce', 'Remove sauce') 60 | .parse(process.argv); 61 | 62 | console.log('you ordered a pizza'); 63 | if (program.sauce) console.log(' with sauce'); 64 | else console.log(' without sauce'); 65 | ``` 66 | 67 | ## Version option 68 | 69 | Calling the `version` implicitly adds the `-V` and `--version` options to the command. 70 | When either of these options is present, the command prints the version number and exits. 71 | 72 | $ ./examples/pizza -V 73 | 0.0.1 74 | 75 | If you want your program to respond to the `-v` option instead of the `-V` option, simply pass custom flags to the `version` method using the same syntax as the `option` method. 76 | 77 | ```js 78 | program 79 | .version('0.0.1', '-v, --version') 80 | ``` 81 | 82 | The version flags can be named anything, but the long option is required. 83 | 84 | ## Command-specific options 85 | 86 | You can attach options to a command. 87 | 88 | ```js 89 | #!/usr/bin/env node 90 | 91 | var program = require('commander'); 92 | 93 | program 94 | .command('rm ') 95 | .option('-r, --recursive', 'Remove recursively') 96 | .action(function (dir, cmd) { 97 | console.log('remove ' + dir + (cmd.recursive ? ' recursively' : '')) 98 | }) 99 | 100 | program.parse(process.argv) 101 | ``` 102 | 103 | A command's options are validated when the command is used. Any unknown options will be reported as an error. However, if an action-based command does not define an action, then the options are not validated. 104 | 105 | ## Coercion 106 | 107 | ```js 108 | function range(val) { 109 | return val.split('..').map(Number); 110 | } 111 | 112 | function list(val) { 113 | return val.split(','); 114 | } 115 | 116 | function collect(val, memo) { 117 | memo.push(val); 118 | return memo; 119 | } 120 | 121 | function increaseVerbosity(v, total) { 122 | return total + 1; 123 | } 124 | 125 | program 126 | .version('0.1.0') 127 | .usage('[options] ') 128 | .option('-i, --integer ', 'An integer argument', parseInt) 129 | .option('-f, --float ', 'A float argument', parseFloat) 130 | .option('-r, --range ..', 'A range', range) 131 | .option('-l, --list ', 'A list', list) 132 | .option('-o, --optional [value]', 'An optional value') 133 | .option('-c, --collect [value]', 'A repeatable value', collect, []) 134 | .option('-v, --verbose', 'A value that can be increased', increaseVerbosity, 0) 135 | .parse(process.argv); 136 | 137 | console.log(' int: %j', program.integer); 138 | console.log(' float: %j', program.float); 139 | console.log(' optional: %j', program.optional); 140 | program.range = program.range || []; 141 | console.log(' range: %j..%j', program.range[0], program.range[1]); 142 | console.log(' list: %j', program.list); 143 | console.log(' collect: %j', program.collect); 144 | console.log(' verbosity: %j', program.verbose); 145 | console.log(' args: %j', program.args); 146 | ``` 147 | 148 | ## Regular Expression 149 | ```js 150 | program 151 | .version('0.1.0') 152 | .option('-s --size ', 'Pizza size', /^(large|medium|small)$/i, 'medium') 153 | .option('-d --drink [drink]', 'Drink', /^(coke|pepsi|izze)$/i) 154 | .parse(process.argv); 155 | 156 | console.log(' size: %j', program.size); 157 | console.log(' drink: %j', program.drink); 158 | ``` 159 | 160 | ## Variadic arguments 161 | 162 | The last argument of a command can be variadic, and only the last argument. To make an argument variadic you have to 163 | append `...` to the argument name. Here is an example: 164 | 165 | ```js 166 | #!/usr/bin/env node 167 | 168 | /** 169 | * Module dependencies. 170 | */ 171 | 172 | var program = require('commander'); 173 | 174 | program 175 | .version('0.1.0') 176 | .command('rmdir [otherDirs...]') 177 | .action(function (dir, otherDirs) { 178 | console.log('rmdir %s', dir); 179 | if (otherDirs) { 180 | otherDirs.forEach(function (oDir) { 181 | console.log('rmdir %s', oDir); 182 | }); 183 | } 184 | }); 185 | 186 | program.parse(process.argv); 187 | ``` 188 | 189 | An `Array` is used for the value of a variadic argument. This applies to `program.args` as well as the argument passed 190 | to your action as demonstrated above. 191 | 192 | ## Specify the argument syntax 193 | 194 | ```js 195 | #!/usr/bin/env node 196 | 197 | var program = require('commander'); 198 | 199 | program 200 | .version('0.1.0') 201 | .arguments(' [env]') 202 | .action(function (cmd, env) { 203 | cmdValue = cmd; 204 | envValue = env; 205 | }); 206 | 207 | program.parse(process.argv); 208 | 209 | if (typeof cmdValue === 'undefined') { 210 | console.error('no command given!'); 211 | process.exit(1); 212 | } 213 | console.log('command:', cmdValue); 214 | console.log('environment:', envValue || "no environment given"); 215 | ``` 216 | Angled brackets (e.g. ``) indicate required input. Square brackets (e.g. `[env]`) indicate optional input. 217 | 218 | ## Git-style sub-commands 219 | 220 | ```js 221 | // file: ./examples/pm 222 | var program = require('commander'); 223 | 224 | program 225 | .version('0.1.0') 226 | .command('install [name]', 'install one or more packages') 227 | .command('search [query]', 'search with optional query') 228 | .command('list', 'list packages installed', {isDefault: true}) 229 | .parse(process.argv); 230 | ``` 231 | 232 | When `.command()` is invoked with a description argument, no `.action(callback)` should be called to handle sub-commands, otherwise there will be an error. This tells commander that you're going to use separate executables for sub-commands, much like `git(1)` and other popular tools. 233 | The commander will try to search the executables in the directory of the entry script (like `./examples/pm`) with the name `program-command`, like `pm-install`, `pm-search`. 234 | 235 | Options can be passed with the call to `.command()`. Specifying `true` for `opts.noHelp` will remove the option from the generated help output. Specifying `true` for `opts.isDefault` will run the subcommand if no other subcommand is specified. 236 | 237 | If the program is designed to be installed globally, make sure the executables have proper modes, like `755`. 238 | 239 | ### `--harmony` 240 | 241 | You can enable `--harmony` option in two ways: 242 | * Use `#! /usr/bin/env node --harmony` in the sub-commands scripts. Note some os version don’t support this pattern. 243 | * Use the `--harmony` option when call the command, like `node --harmony examples/pm publish`. The `--harmony` option will be preserved when spawning sub-command process. 244 | 245 | ## Automated --help 246 | 247 | The help information is auto-generated based on the information commander already knows about your program, so the following `--help` info is for free: 248 | 249 | ``` 250 | $ ./examples/pizza --help 251 | 252 | Usage: pizza [options] 253 | 254 | An application for pizzas ordering 255 | 256 | Options: 257 | 258 | -h, --help output usage information 259 | -V, --version output the version number 260 | -p, --peppers Add peppers 261 | -P, --pineapple Add pineapple 262 | -b, --bbq Add bbq sauce 263 | -c, --cheese Add the specified type of cheese [marble] 264 | -C, --no-cheese You do not want any cheese 265 | 266 | ``` 267 | 268 | ## Custom help 269 | 270 | You can display arbitrary `-h, --help` information 271 | by listening for "--help". Commander will automatically 272 | exit once you are done so that the remainder of your program 273 | does not execute causing undesired behaviours, for example 274 | in the following executable "stuff" will not output when 275 | `--help` is used. 276 | 277 | ```js 278 | #!/usr/bin/env node 279 | 280 | /** 281 | * Module dependencies. 282 | */ 283 | 284 | var program = require('commander'); 285 | 286 | program 287 | .version('0.1.0') 288 | .option('-f, --foo', 'enable some foo') 289 | .option('-b, --bar', 'enable some bar') 290 | .option('-B, --baz', 'enable some baz'); 291 | 292 | // must be before .parse() since 293 | // node's emit() is immediate 294 | 295 | program.on('--help', function(){ 296 | console.log(' Examples:'); 297 | console.log(''); 298 | console.log(' $ custom-help --help'); 299 | console.log(' $ custom-help -h'); 300 | console.log(''); 301 | }); 302 | 303 | program.parse(process.argv); 304 | 305 | console.log('stuff'); 306 | ``` 307 | 308 | Yields the following help output when `node script-name.js -h` or `node script-name.js --help` are run: 309 | 310 | ``` 311 | 312 | Usage: custom-help [options] 313 | 314 | Options: 315 | 316 | -h, --help output usage information 317 | -V, --version output the version number 318 | -f, --foo enable some foo 319 | -b, --bar enable some bar 320 | -B, --baz enable some baz 321 | 322 | Examples: 323 | 324 | $ custom-help --help 325 | $ custom-help -h 326 | 327 | ``` 328 | 329 | ## .outputHelp(cb) 330 | 331 | Output help information without exiting. 332 | Optional callback cb allows post-processing of help text before it is displayed. 333 | 334 | If you want to display help by default (e.g. if no command was provided), you can use something like: 335 | 336 | ```js 337 | var program = require('commander'); 338 | var colors = require('colors'); 339 | 340 | program 341 | .version('0.1.0') 342 | .command('getstream [url]', 'get stream URL') 343 | .parse(process.argv); 344 | 345 | if (!process.argv.slice(2).length) { 346 | program.outputHelp(make_red); 347 | } 348 | 349 | function make_red(txt) { 350 | return colors.red(txt); //display the help text in red on the console 351 | } 352 | ``` 353 | 354 | ## .help(cb) 355 | 356 | Output help information and exit immediately. 357 | Optional callback cb allows post-processing of help text before it is displayed. 358 | 359 | ## Examples 360 | 361 | ```js 362 | var program = require('commander'); 363 | 364 | program 365 | .version('0.1.0') 366 | .option('-C, --chdir ', 'change the working directory') 367 | .option('-c, --config ', 'set config path. defaults to ./deploy.conf') 368 | .option('-T, --no-tests', 'ignore test hook'); 369 | 370 | program 371 | .command('setup [env]') 372 | .description('run setup commands for all envs') 373 | .option("-s, --setup_mode [mode]", "Which setup mode to use") 374 | .action(function(env, options){ 375 | var mode = options.setup_mode || "normal"; 376 | env = env || 'all'; 377 | console.log('setup for %s env(s) with %s mode', env, mode); 378 | }); 379 | 380 | program 381 | .command('exec ') 382 | .alias('ex') 383 | .description('execute the given remote cmd') 384 | .option("-e, --exec_mode ", "Which exec mode to use") 385 | .action(function(cmd, options){ 386 | console.log('exec "%s" using %s mode', cmd, options.exec_mode); 387 | }).on('--help', function() { 388 | console.log(' Examples:'); 389 | console.log(); 390 | console.log(' $ deploy exec sequential'); 391 | console.log(' $ deploy exec async'); 392 | console.log(); 393 | }); 394 | 395 | program 396 | .command('*') 397 | .action(function(env){ 398 | console.log('deploying "%s"', env); 399 | }); 400 | 401 | program.parse(process.argv); 402 | ``` 403 | 404 | More Demos can be found in the [examples](https://github.com/tj/commander.js/tree/master/examples) directory. 405 | 406 | ## License 407 | 408 | MIT 409 | -------------------------------------------------------------------------------- /pkg/node_modules/commander/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "commander@^2.14.1", 3 | "_id": "commander@2.15.1", 4 | "_inBundle": false, 5 | "_integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 6 | "_location": "/commander", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "range", 10 | "registry": true, 11 | "raw": "commander@^2.14.1", 12 | "name": "commander", 13 | "escapedName": "commander", 14 | "rawSpec": "^2.14.1", 15 | "saveSpec": null, 16 | "fetchSpec": "^2.14.1" 17 | }, 18 | "_requiredBy": [ 19 | "/" 20 | ], 21 | "_resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 22 | "_shasum": "df46e867d0fc2aec66a34662b406a9ccafff5b0f", 23 | "_spec": "commander@^2.14.1", 24 | "_where": "/Users/dev/workspace/asyncmachine/pkg", 25 | "author": { 26 | "name": "TJ Holowaychuk", 27 | "email": "tj@vision-media.ca" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/tj/commander.js/issues" 31 | }, 32 | "bundleDependencies": false, 33 | "dependencies": {}, 34 | "deprecated": false, 35 | "description": "the complete solution for node.js command-line programs", 36 | "devDependencies": { 37 | "@types/node": "^7.0.55", 38 | "eslint": "^3.19.0", 39 | "should": "^11.2.1", 40 | "sinon": "^2.4.1", 41 | "standard": "^10.0.3", 42 | "typescript": "^2.7.2" 43 | }, 44 | "files": [ 45 | "index.js", 46 | "typings/index.d.ts" 47 | ], 48 | "homepage": "https://github.com/tj/commander.js#readme", 49 | "keywords": [ 50 | "commander", 51 | "command", 52 | "option", 53 | "parser" 54 | ], 55 | "license": "MIT", 56 | "main": "index", 57 | "name": "commander", 58 | "repository": { 59 | "type": "git", 60 | "url": "git+https://github.com/tj/commander.js.git" 61 | }, 62 | "scripts": { 63 | "lint": "eslint index.js", 64 | "test": "make test && npm run test-typings", 65 | "test-typings": "node_modules/typescript/bin/tsc -p tsconfig.json" 66 | }, 67 | "typings": "typings/index.d.ts", 68 | "version": "2.15.1" 69 | } 70 | -------------------------------------------------------------------------------- /pkg/node_modules/commander/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for commander 2.11 2 | // Project: https://github.com/visionmedia/commander.js 3 | // Definitions by: Alan Agius , Marcelo Dezem , vvakame , Jules Randolph 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | 6 | declare namespace local { 7 | 8 | class Option { 9 | flags: string; 10 | required: boolean; 11 | optional: boolean; 12 | bool: boolean; 13 | short?: string; 14 | long: string; 15 | description: string; 16 | 17 | /** 18 | * Initialize a new `Option` with the given `flags` and `description`. 19 | * 20 | * @param {string} flags 21 | * @param {string} [description] 22 | */ 23 | constructor(flags: string, description?: string); 24 | } 25 | 26 | class Command extends NodeJS.EventEmitter { 27 | [key: string]: any; 28 | 29 | args: string[]; 30 | 31 | /** 32 | * Initialize a new `Command`. 33 | * 34 | * @param {string} [name] 35 | */ 36 | constructor(name?: string); 37 | 38 | /** 39 | * Set the program version to `str`. 40 | * 41 | * This method auto-registers the "-V, --version" flag 42 | * which will print the version number when passed. 43 | * 44 | * @param {string} str 45 | * @param {string} [flags] 46 | * @returns {Command} for chaining 47 | */ 48 | version(str: string, flags?: string): Command; 49 | 50 | /** 51 | * Add command `name`. 52 | * 53 | * The `.action()` callback is invoked when the 54 | * command `name` is specified via __ARGV__, 55 | * and the remaining arguments are applied to the 56 | * function for access. 57 | * 58 | * When the `name` is "*" an un-matched command 59 | * will be passed as the first arg, followed by 60 | * the rest of __ARGV__ remaining. 61 | * 62 | * @example 63 | * program 64 | * .version('0.0.1') 65 | * .option('-C, --chdir ', 'change the working directory') 66 | * .option('-c, --config ', 'set config path. defaults to ./deploy.conf') 67 | * .option('-T, --no-tests', 'ignore test hook') 68 | * 69 | * program 70 | * .command('setup') 71 | * .description('run remote setup commands') 72 | * .action(function() { 73 | * console.log('setup'); 74 | * }); 75 | * 76 | * program 77 | * .command('exec ') 78 | * .description('run the given remote command') 79 | * .action(function(cmd) { 80 | * console.log('exec "%s"', cmd); 81 | * }); 82 | * 83 | * program 84 | * .command('teardown [otherDirs...]') 85 | * .description('run teardown commands') 86 | * .action(function(dir, otherDirs) { 87 | * console.log('dir "%s"', dir); 88 | * if (otherDirs) { 89 | * otherDirs.forEach(function (oDir) { 90 | * console.log('dir "%s"', oDir); 91 | * }); 92 | * } 93 | * }); 94 | * 95 | * program 96 | * .command('*') 97 | * .description('deploy the given env') 98 | * .action(function(env) { 99 | * console.log('deploying "%s"', env); 100 | * }); 101 | * 102 | * program.parse(process.argv); 103 | * 104 | * @param {string} name 105 | * @param {string} [desc] for git-style sub-commands 106 | * @param {CommandOptions} [opts] command options 107 | * @returns {Command} the new command 108 | */ 109 | command(name: string, desc?: string, opts?: commander.CommandOptions): Command; 110 | 111 | /** 112 | * Define argument syntax for the top-level command. 113 | * 114 | * @param {string} desc 115 | * @returns {Command} for chaining 116 | */ 117 | arguments(desc: string): Command; 118 | 119 | /** 120 | * Parse expected `args`. 121 | * 122 | * For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`. 123 | * 124 | * @param {string[]} args 125 | * @returns {Command} for chaining 126 | */ 127 | parseExpectedArgs(args: string[]): Command; 128 | 129 | /** 130 | * Register callback `fn` for the command. 131 | * 132 | * @example 133 | * program 134 | * .command('help') 135 | * .description('display verbose help') 136 | * .action(function() { 137 | * // output help here 138 | * }); 139 | * 140 | * @param {(...args: any[]) => void} fn 141 | * @returns {Command} for chaining 142 | */ 143 | action(fn: (...args: any[]) => void): Command; 144 | 145 | /** 146 | * Define option with `flags`, `description` and optional 147 | * coercion `fn`. 148 | * 149 | * The `flags` string should contain both the short and long flags, 150 | * separated by comma, a pipe or space. The following are all valid 151 | * all will output this way when `--help` is used. 152 | * 153 | * "-p, --pepper" 154 | * "-p|--pepper" 155 | * "-p --pepper" 156 | * 157 | * @example 158 | * // simple boolean defaulting to false 159 | * program.option('-p, --pepper', 'add pepper'); 160 | * 161 | * --pepper 162 | * program.pepper 163 | * // => Boolean 164 | * 165 | * // simple boolean defaulting to true 166 | * program.option('-C, --no-cheese', 'remove cheese'); 167 | * 168 | * program.cheese 169 | * // => true 170 | * 171 | * --no-cheese 172 | * program.cheese 173 | * // => false 174 | * 175 | * // required argument 176 | * program.option('-C, --chdir ', 'change the working directory'); 177 | * 178 | * --chdir /tmp 179 | * program.chdir 180 | * // => "/tmp" 181 | * 182 | * // optional argument 183 | * program.option('-c, --cheese [type]', 'add cheese [marble]'); 184 | * 185 | * @param {string} flags 186 | * @param {string} [description] 187 | * @param {((arg1: any, arg2: any) => void) | RegExp} [fn] function or default 188 | * @param {*} [defaultValue] 189 | * @returns {Command} for chaining 190 | */ 191 | option(flags: string, description?: string, fn?: ((arg1: any, arg2: any) => void) | RegExp, defaultValue?: any): Command; 192 | option(flags: string, description?: string, defaultValue?: any): Command; 193 | 194 | /** 195 | * Allow unknown options on the command line. 196 | * 197 | * @param {boolean} [arg] if `true` or omitted, no error will be thrown for unknown options. 198 | * @returns {Command} for chaining 199 | */ 200 | allowUnknownOption(arg?: boolean): Command; 201 | 202 | /** 203 | * Parse `argv`, settings options and invoking commands when defined. 204 | * 205 | * @param {string[]} argv 206 | * @returns {Command} for chaining 207 | */ 208 | parse(argv: string[]): Command; 209 | 210 | /** 211 | * Parse options from `argv` returning `argv` void of these options. 212 | * 213 | * @param {string[]} argv 214 | * @returns {ParseOptionsResult} 215 | */ 216 | parseOptions(argv: string[]): commander.ParseOptionsResult; 217 | 218 | /** 219 | * Return an object containing options as key-value pairs 220 | * 221 | * @returns {{[key: string]: string}} 222 | */ 223 | opts(): { [key: string]: string }; 224 | 225 | /** 226 | * Set the description to `str`. 227 | * 228 | * @param {string} str 229 | * @return {(Command | string)} 230 | */ 231 | description(str: string): Command; 232 | description(): string; 233 | 234 | /** 235 | * Set an alias for the command. 236 | * 237 | * @param {string} alias 238 | * @return {(Command | string)} 239 | */ 240 | alias(alias: string): Command; 241 | alias(): string; 242 | 243 | /** 244 | * Set or get the command usage. 245 | * 246 | * @param {string} str 247 | * @return {(Command | string)} 248 | */ 249 | usage(str: string): Command; 250 | usage(): string; 251 | 252 | /** 253 | * Set the name of the command. 254 | * 255 | * @param {string} str 256 | * @return {Command} 257 | */ 258 | name(str: string): Command; 259 | 260 | /** 261 | * Get the name of the command. 262 | * 263 | * @return {string} 264 | */ 265 | name(): string; 266 | 267 | /** 268 | * Output help information for this command. 269 | * 270 | * @param {(str: string) => string} [cb] 271 | */ 272 | outputHelp(cb?: (str: string) => string): void; 273 | 274 | /** Output help information and exit. 275 | * 276 | * @param {(str: string) => string} [cb] 277 | */ 278 | help(cb?: (str: string) => string): void; 279 | } 280 | 281 | } 282 | 283 | declare namespace commander { 284 | 285 | type Command = local.Command 286 | 287 | type Option = local.Option 288 | 289 | interface CommandOptions { 290 | noHelp?: boolean; 291 | isDefault?: boolean; 292 | } 293 | 294 | interface ParseOptionsResult { 295 | args: string[]; 296 | unknown: string[]; 297 | } 298 | 299 | interface CommanderStatic extends Command { 300 | Command: typeof local.Command; 301 | Option: typeof local.Option; 302 | CommandOptions: CommandOptions; 303 | ParseOptionsResult: ParseOptionsResult; 304 | } 305 | 306 | } 307 | 308 | declare const commander: commander.CommanderStatic; 309 | export = commander; 310 | -------------------------------------------------------------------------------- /pkg/node_modules/simple-random-id/.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /pkg/node_modules/simple-random-id/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "camelcase": false, 3 | "curly": true, 4 | "forin": false, 5 | "latedef": false, 6 | "newcap": false, 7 | "noarg": true, 8 | "nonew": true, 9 | "quotmark": "single", 10 | "undef": true, 11 | "unused": "vars", 12 | "strict": true, 13 | "trailing": true, 14 | "maxlen": 80, 15 | 16 | "eqnull": true, 17 | "esnext": true, 18 | "expr": true, 19 | "globalstrict": true, 20 | 21 | "maxerr": 1000, 22 | "regexdash": true, 23 | "laxcomma": true, 24 | "proto": true, 25 | 26 | "node": true, 27 | "devel": true, 28 | "nonstandard": true, 29 | "worker": true 30 | } 31 | -------------------------------------------------------------------------------- /pkg/node_modules/simple-random-id/.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | -------------------------------------------------------------------------------- /pkg/node_modules/simple-random-id/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | branches: 3 | only: 4 | - master 5 | script: 6 | - "npm run lint" 7 | - "npm test" 8 | -------------------------------------------------------------------------------- /pkg/node_modules/simple-random-id/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Michał Budzyński 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /pkg/node_modules/simple-random-id/README.md: -------------------------------------------------------------------------------- 1 | SimpleRandomId by [@michalbe](http://github.com/michalbe) 2 | ========= 3 | 4 | Simple random alphanumeric string generator. 5 | 6 | How to use: 7 | ``` 8 | npm install simple-random-id 9 | ``` 10 | 11 | then: 12 | ```javascript 13 | var sri = require('simple-random-id'); 14 | 15 | // Only parameter it takes is length of the random string 16 | // Default length is 10 17 | sri(); // 'UIWSNLJ9L8' 18 | sri(3); // 'PX0' 19 | sri(24); // 'SXB0KT4SJ1SGU8FRAK6LFVJB' 20 | ``` 21 | -------------------------------------------------------------------------------- /pkg/node_modules/simple-random-id/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var generate = function(length) { 4 | if (length !== 0) { 5 | length = Math.abs(length) || 10; 6 | } 7 | 8 | var output = generateTen(); 9 | if (length === 0) { 10 | throw new Error('Lenght need to be an integer different than 0.'); 11 | } else if (length > 10) { 12 | var tens = ~~(length/10); 13 | while (tens--) { 14 | output += generateTen(); 15 | } 16 | } 17 | 18 | return output.substr(0, length); 19 | }; 20 | 21 | var generateTen = function() { 22 | // This could be 10 or 11 (depends on the value returned by Math.random()) 23 | // but since we truncate it in the main function we don't need to do it 24 | // in here 25 | return Math.random().toString(36).slice(2).toUpperCase(); 26 | }; 27 | 28 | module.exports = generate; 29 | -------------------------------------------------------------------------------- /pkg/node_modules/simple-random-id/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "simple-random-id@^1.0.1", 3 | "_id": "simple-random-id@1.0.1", 4 | "_inBundle": false, 5 | "_integrity": "sha1-2vmOICj6GHf1JchP7LhQ3hSuxKw=", 6 | "_location": "/simple-random-id", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "range", 10 | "registry": true, 11 | "raw": "simple-random-id@^1.0.1", 12 | "name": "simple-random-id", 13 | "escapedName": "simple-random-id", 14 | "rawSpec": "^1.0.1", 15 | "saveSpec": null, 16 | "fetchSpec": "^1.0.1" 17 | }, 18 | "_requiredBy": [ 19 | "/" 20 | ], 21 | "_resolved": "https://registry.npmjs.org/simple-random-id/-/simple-random-id-1.0.1.tgz", 22 | "_shasum": "daf98e2028fa1877f525c84fecb850de14aec4ac", 23 | "_spec": "simple-random-id@^1.0.1", 24 | "_where": "/Users/dev/workspace/asyncmachine/pkg", 25 | "author": { 26 | "name": "Michal Budzynski", 27 | "email": "michal@virtualdesign.pl", 28 | "url": "https://github.com/michalbe" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/michalbe/simple-random-id/issues" 32 | }, 33 | "bundleDependencies": false, 34 | "deprecated": false, 35 | "description": "Random alphanumeric string generator", 36 | "devDependencies": { 37 | "assert": "~1.1.1", 38 | "jshint": "~2.5.2", 39 | "precommit-hook": "~1.0.2" 40 | }, 41 | "homepage": "https://github.com/michalbe/simple-random-id", 42 | "name": "simple-random-id", 43 | "repository": { 44 | "type": "git", 45 | "url": "git+ssh://git@github.com/michalbe/simple-random-id.git" 46 | }, 47 | "scripts": { 48 | "lint": "node node_modules/jshint/bin/jshint .", 49 | "test": "node tests/random-id_test.js" 50 | }, 51 | "version": "1.0.1" 52 | } 53 | -------------------------------------------------------------------------------- /pkg/node_modules/simple-random-id/tests/random-id_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'), 4 | generate = require('../index.js'); 5 | 6 | // Called without argument 7 | assert.equal(generate().length, 10); 8 | 9 | // Called with number less than 10 10 | assert.equal(generate(4).length, 4); 11 | 12 | // Called with number greater than 10 13 | assert.equal(generate(34).length, 34); 14 | 15 | // Called with string instead of number 16 | // Argument should be ignored 17 | assert.equal(generate('JCZC.7UP').length, 10); 18 | 19 | // Called with object instead of number 20 | // Argument should be ignored 21 | assert.equal(generate({ 22 | 'MlodybeTomal': 'Najlepszy rap z Jelonek' 23 | }).length, 10); 24 | 25 | // Called with array instead of number 26 | // Argument should be ignored 27 | assert.equal(generate([3,2,1,6,7]).length, 10); 28 | 29 | // Called with '0' should throw an error 30 | assert.throws(function(){ 31 | generate(0); 32 | }); 33 | -------------------------------------------------------------------------------- /pkg/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asyncmachine", 3 | "version": "3.4.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "commander": { 8 | "version": "2.15.1", 9 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 10 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" 11 | }, 12 | "simple-random-id": { 13 | "version": "1.0.1", 14 | "resolved": "https://registry.npmjs.org/simple-random-id/-/simple-random-id-1.0.1.tgz", 15 | "integrity": "sha1-2vmOICj6GHf1JchP7LhQ3hSuxKw=" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asyncmachine", 3 | "version": "3.4.1", 4 | "description": "Hybrid State Machine", 5 | "keywords": [ 6 | "async", 7 | "statemachine", 8 | "eventemitter", 9 | "states", 10 | "transition", 11 | "eventloop", 12 | "declarative", 13 | "relational", 14 | "aop", 15 | "fsm", 16 | "actor-model", 17 | "dependency-graph" 18 | ], 19 | "author": "Tobiasz Cudnik ", 20 | "license": "MIT", 21 | "url": "https://github.com/TobiaszCudnik/asyncmachine", 22 | "repository": { 23 | "type": "git", 24 | "url": "http://github.com/TobiaszCudnik/asyncmachine.git" 25 | }, 26 | "main": "asyncmachine.js", 27 | "types": "asyncmachine.d.ts", 28 | "module": "dist/asyncmachine.es6.js", 29 | "browser": "dist/asyncmachine.umd.js", 30 | "dependencies": { 31 | "commander": "^2.14.1", 32 | "simple-random-id": "^1.0.1" 33 | }, 34 | "bin": { 35 | "am-types": "./bin/am-types.js" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/shims.d.ts: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/shims.d.ts -------------------------------------------------------------------------------- /pkg/shims.js: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/shims.js -------------------------------------------------------------------------------- /pkg/shims.js.map: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/shims.js.map -------------------------------------------------------------------------------- /pkg/transition.d.ts: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/transition.d.ts -------------------------------------------------------------------------------- /pkg/transition.js: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/transition.js -------------------------------------------------------------------------------- /pkg/transition.js.map: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/transition.js.map -------------------------------------------------------------------------------- /pkg/types.d.ts: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/types.d.ts -------------------------------------------------------------------------------- /pkg/types.js: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/types.js -------------------------------------------------------------------------------- /pkg/types.js.map: -------------------------------------------------------------------------------- 1 | /Users/dev/workspace/asyncmachine/build/types.js.map -------------------------------------------------------------------------------- /rollup-es6.config.js: -------------------------------------------------------------------------------- 1 | import config from './rollup.config' 2 | import typescript from 'rollup-plugin-typescript2' 3 | import tsc from 'typescript' 4 | 5 | // remove the exec plugin 6 | delete config.plugins[4] 7 | config.plugins[0] = typescript({ 8 | check: false, 9 | tsconfigDefaults: { 10 | target: 'es6', 11 | isolatedModules: true, 12 | module: 'es6' 13 | }, 14 | typescript: tsc 15 | }) 16 | config.output = [ 17 | { 18 | file: 'build/asyncmachine.es6.js', 19 | format: 'es' 20 | } 21 | ] 22 | 23 | export default config 24 | -------------------------------------------------------------------------------- /rollup-shims.config.js: -------------------------------------------------------------------------------- 1 | import config from './rollup.config' 2 | 3 | config.input = 'src/shims.ts' 4 | // remove the exec plugin 5 | delete config.plugins[4] 6 | config.output = [ 7 | { 8 | file: 'build/asyncmachine-shims.cjs.js', 9 | format: 'cjs' }, 10 | { 11 | file: 'build/asyncmachine-shims.umd.js', 12 | name: 'asyncmachine', 13 | format: 'umd' 14 | }, 15 | { 16 | file: 'build/asyncmachine-shims.es6.js', 17 | format: 'es' 18 | } 19 | ] 20 | 21 | export default config 22 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs' 2 | import nodeResolve from 'rollup-plugin-node-resolve' 3 | import typescript from 'rollup-plugin-typescript2' 4 | import tsc from 'typescript' 5 | import uglify from 'rollup-plugin-uglify' 6 | import { minify } from 'uglify-es' 7 | import execute from 'rollup-plugin-execute' 8 | 9 | export default { 10 | input: 'src/asyncmachine.ts', 11 | plugins: [ 12 | typescript({ 13 | check: false, 14 | tsconfigDefaults: { 15 | target: 'es5', 16 | isolatedModules: true, 17 | module: 'es6' 18 | }, 19 | typescript: tsc 20 | }), 21 | nodeResolve({ 22 | jsnext: true, 23 | main: true, 24 | preferBuiltins: false 25 | }), 26 | commonjs({ 27 | include: 'node_modules/**', 28 | ignoreGlobal: true 29 | }), 30 | uglify({}, minify), 31 | // dirty dirty dirty 32 | // propagate @ts-ignore statements to the generated definitions 33 | execute([ 34 | `./node_modules/.bin/replace-in-file " on(" "// @ts-ignore 35 | /**/on(" build/asyncmachine.d.ts`, 36 | `./node_modules/.bin/replace-in-file " on: " "// @ts-ignore 37 | /**/on: " build/asyncmachine.d.ts`, 38 | `./node_modules/.bin/replace-in-file " once: " "// @ts-ignore 39 | /**/once: " build/asyncmachine.d.ts`, 40 | `./node_modules/.bin/replace-in-file " once(" "// @ts-ignore 41 | /**/once(" build/asyncmachine.d.ts`, 42 | `./node_modules/.bin/replace-in-file " emit: " "// @ts-ignore 43 | /**/emit: " build/asyncmachine.d.ts`, 44 | ]) 45 | ], 46 | exports: 'named', 47 | sourcemap: true, 48 | output: [ 49 | { 50 | file: 'build/asyncmachine.cjs.js', 51 | format: 'cjs' }, 52 | { 53 | file: 'build/asyncmachine.umd.js', 54 | name: 'asyncmachine', 55 | format: 'umd' 56 | } 57 | ] 58 | }; 59 | -------------------------------------------------------------------------------- /src/ee.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Representation of a single EventEmitter function. 3 | */ 4 | class EE { 5 | constructor( 6 | public fn: Function, 7 | public context: Object, 8 | public once: Boolean = false 9 | ) { 10 | // empty 11 | } 12 | } 13 | 14 | /** 15 | * Minimal EventEmitter interface that is molded against the Node.js 16 | * EventEmitter interface. 17 | */ 18 | export default class EventEmitter { 19 | // @ts-ignore 20 | private _events: { 21 | [index: string]: EE[] | EE 22 | } 23 | 24 | /** 25 | * Return a list of assigned event listeners. 26 | */ 27 | listeners(event: string): Function[] { 28 | if (!this._events || !this._events[event]) return [] 29 | 30 | let listeners = this._events[event] 31 | if (listeners instanceof EE) return [listeners.fn] 32 | else { 33 | for (var i = 0, l = listeners.length, ee = new Array(l); i < l; i++) { 34 | ee[i] = listeners[i].fn 35 | } 36 | 37 | return ee 38 | } 39 | } 40 | 41 | /** 42 | * Emit an event to all registered event listeners. 43 | */ 44 | emit(event: string, ...args: any[]): boolean { 45 | if (!this._events || !this._events[event]) return true 46 | 47 | let listeners = this._events[event] 48 | if (listeners instanceof EE) { 49 | if (listeners.once) this.removeListener(event, listeners.fn, true) 50 | 51 | return this.callListener(listeners.fn, listeners.context, args) 52 | } else { 53 | for (let listener of listeners) { 54 | if (listener.once) this.removeListener(event, listener.fn, true) 55 | 56 | if (false === this.callListener(listener.fn, listener.context, args)) 57 | return false 58 | } 59 | } 60 | 61 | return true 62 | } 63 | 64 | /** 65 | * Callback executor for overriding. 66 | */ 67 | protected callListener(listener: Function, context: Object, params: any[]) { 68 | return listener.apply(context, params) 69 | } 70 | 71 | /** 72 | * Remove event listeners. 73 | */ 74 | removeListener(event: string, fn: Function, once: boolean = false): this { 75 | if (!this._events || !this._events[event]) return this 76 | 77 | var listeners = this._events[event], 78 | events: EE[] = [] 79 | 80 | if (fn) { 81 | if (listeners instanceof EE) { 82 | if (listeners.fn !== fn || (once && !listeners.once)) 83 | events.push(listeners) 84 | } else { 85 | for (var i = 0, length = listeners.length; i < length; i++) { 86 | if (listeners[i].fn !== fn || (once && !listeners[i].once)) { 87 | events.push(listeners[i]) 88 | } 89 | } 90 | } 91 | } 92 | 93 | // 94 | // Reset the array, or remove it completely if we have no more listeners. 95 | // 96 | if (events.length) { 97 | this._events[event] = events.length === 1 ? events[0] : events 98 | } else { 99 | delete this._events[event] 100 | } 101 | 102 | return this 103 | } 104 | 105 | /** 106 | * Register a new EventListener for the given event. 107 | */ 108 | on(event: string, fn: Function, context?: Object): this { 109 | var listener = new EE(fn, context || this) 110 | 111 | if (!this._events) this._events = {} 112 | if (!this._events[event]) this._events[event] = listener 113 | else { 114 | let listeners = this._events[event] 115 | if (listeners instanceof Array) listeners.push(listener) 116 | else { 117 | this._events[event] = [listeners, listener] 118 | } 119 | } 120 | 121 | return this 122 | } 123 | 124 | /** 125 | * Add an EventListener that's only called once. 126 | */ 127 | once(event: string, fn: Function, context?: Object): this { 128 | var listener = new EE(fn, context || this, true) 129 | 130 | if (!this._events) this._events = {} 131 | if (!this._events[event]) this._events[event] = listener 132 | else { 133 | let listeners = this._events[event] 134 | if (listeners instanceof Array) listeners.push(listener) 135 | else this._events[event] = [listeners, listener] 136 | } 137 | 138 | return this 139 | } 140 | 141 | /** 142 | * Remove all listeners or only the listeners for the specified event. 143 | */ 144 | removeAllListeners(event: string): this { 145 | if (!this._events) return this 146 | 147 | if (event) delete this._events[event] 148 | else this._events = {} 149 | 150 | return this 151 | } 152 | 153 | // class end 154 | } 155 | 156 | // TODO aliases 157 | // // 158 | // // Alias methods names because people roll like that. 159 | // // 160 | // EventEmitter.prototype.off = EventEmitter.prototype.removeListener; 161 | // EventEmitter.prototype.addListener = EventEmitter.prototype.on; 162 | 163 | // // 164 | // // This function doesn't apply anymore. 165 | // // 166 | // EventEmitter.prototype.setMaxListeners = function setMaxListeners() { 167 | // return this; 168 | // }; 169 | 170 | // // 171 | // // Expose the module. 172 | // // 173 | // EventEmitter.EventEmitter = EventEmitter; 174 | // EventEmitter.EventEmitter2 = EventEmitter; 175 | // EventEmitter.EventEmitter3 = EventEmitter; 176 | 177 | // // 178 | // // Expose the module. 179 | // // 180 | // module.exports = {EventEmitter: EventEmitter}; 181 | -------------------------------------------------------------------------------- /src/shims.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/fn/array/keys' 2 | import 'core-js/fn/array/includes' 3 | import 'core-js/fn/object/entries' 4 | import 'core-js/fn/object/keys' 5 | import 'core-js/es6/promise' 6 | export * from './asyncmachine' 7 | -------------------------------------------------------------------------------- /src/transition.ts: -------------------------------------------------------------------------------- 1 | import AsyncMachine from './asyncmachine' 2 | import { 3 | TransitionException, 4 | MutationTypes, 5 | IQueueRow, 6 | QueueRowFields, 7 | TransitionStepTypes, 8 | IStateStruct, 9 | ITransitionStep, 10 | StateRelations, 11 | TransitionStepFields, 12 | StateStructFields, 13 | IBind, 14 | IEmit 15 | } from './types' 16 | // @ts-ignore 17 | import * as uuidProxy from 'simple-random-id' 18 | 19 | const uuid = (uuidProxy).default || uuidProxy 20 | 21 | /** 22 | * TODO make it easier to parse 23 | */ 24 | interface IEvent { 25 | 0: string 26 | 1: any[] 27 | } 28 | 29 | /** 30 | * The Transition class is responsible for encapsulating a single mutation 31 | * for a single machine. In can be created by a different machine than it's 32 | * mutating. End users usually don't have to deal with it at all, as the most 33 | * important data it carries for them is exposed as `instance.to()` and 34 | * `instance.from()` methods and it's events are also emitted on the 35 | * machine itself. 36 | * 37 | * TODO freeze the attributes 38 | */ 39 | export default class Transition { 40 | id = uuid() 41 | // ID of the machine which initiated the transition 42 | source_machine: AsyncMachine 43 | // queue of events to fire 44 | private events: IEvent[] = [] 45 | // states before the transition 46 | before: string[] 47 | // target states after parsing the relations 48 | states: string[] = [] 49 | // array of enter transition to execute 50 | enters: string[] = [] 51 | // array of exit transition to execute 52 | exits: string[] = [] 53 | // was the transition accepted? 54 | accepted = true 55 | // source queue row 56 | row: IQueueRow 57 | // list of steps with machine IDs 58 | steps: ITransitionStep[] = [] 59 | // was transition cancelled during negotiation? 60 | cancelled: boolean = false 61 | 62 | // target machine on which the transition is supposed to happen 63 | get machine(): AsyncMachine { 64 | return this.row[QueueRowFields.TARGET] 65 | } 66 | // is it an auto-state transition? 67 | get auto(): boolean { 68 | return this.row[QueueRowFields.AUTO] || false 69 | } 70 | // type of the transition 71 | get type(): MutationTypes { 72 | return this.row[QueueRowFields.STATE_CHANGE_TYPE] 73 | } 74 | // explicitly requested states 75 | get requested_states(): string[] { 76 | return this.row[QueueRowFields.STATES] 77 | } 78 | // params passed to the transition 79 | get params(): any[] { 80 | return this.row[QueueRowFields.PARAMS] 81 | } 82 | 83 | constructor(source_machine: AsyncMachine, row: IQueueRow) { 84 | this.source_machine = source_machine 85 | this.row = row 86 | this.before = this.machine.is() 87 | 88 | this.machine.emit('transition-init', this) 89 | 90 | let type = this.type 91 | let states = this.requested_states 92 | this.addStepsFor(states, null, TransitionStepTypes.REQUESTED) 93 | 94 | const types = MutationTypes 95 | const type_label = types[type].toLowerCase() 96 | if (this.auto) 97 | this.machine.log(`[${type_label}:auto] state ${states.join(', ')}`, 3) 98 | else this.machine.log(`[${type_label}] ${states.join(', ')}`, 2) 99 | 100 | let states_to_set: string[] = [] 101 | 102 | switch (type) { 103 | case types.DROP: 104 | states_to_set = this.machine.states_active.filter( 105 | state => !states.includes(state) 106 | ) 107 | this.addStepsFor(states, null, TransitionStepTypes.DROP) 108 | break 109 | case types.ADD: 110 | states_to_set = [...states, ...this.machine.states_active] 111 | this.addStepsFor( 112 | this.machine.diffStates(states_to_set, this.machine.states_active), 113 | null, 114 | TransitionStepTypes.SET 115 | ) 116 | break 117 | case types.SET: 118 | states_to_set = states 119 | this.addStepsFor( 120 | this.machine.diffStates(states_to_set, this.machine.states_active), 121 | null, 122 | TransitionStepTypes.SET 123 | ) 124 | this.addStepsFor( 125 | this.machine.diffStates(this.machine.states_active, states_to_set), 126 | null, 127 | TransitionStepTypes.DROP 128 | ) 129 | break 130 | } 131 | 132 | this.resolveRelations(states_to_set) 133 | 134 | let implied_states = this.machine.diffStates(this.states, states_to_set) 135 | if (implied_states.length) 136 | this.machine.log( 137 | `[${type_label}:implied] ${implied_states.join(', ')}`, 138 | 2 139 | ) 140 | 141 | this.setupAccepted() 142 | 143 | if (this.accepted) { 144 | this.setupExitEnter() 145 | } 146 | } 147 | 148 | exec(): boolean { 149 | let machine = this.machine 150 | let aborted = !this.accepted 151 | let hasStateChanged = false 152 | 153 | machine.emit('transition-start', this) 154 | 155 | // check if the machine isnt already during a transition 156 | // TODO ideally we would postpone the transition instead of cancelling it 157 | // TODO move to parseQueue() 158 | if (machine.lock) { 159 | this.addStepsFor(this.requested_states, null, TransitionStepTypes.CANCEL) 160 | machine.emit('transition-cancelled', this) 161 | machine.emit('transition-end', this) 162 | const msg = 163 | `[cancelled:${this.source_machine.id(true)}] Target machine` + 164 | `"${machine.id()}" already during a transition, use a shared` + 165 | `queue. Requested states: ${this.requested_states.join( 166 | ', ' 167 | )}, source states: ${this.before.join(', ')}` 168 | console.warn(msg) 169 | machine.log(msg, 1) 170 | return false 171 | } 172 | 173 | machine.transition = this 174 | this.events = [] 175 | machine.lock = true 176 | 177 | try { 178 | // NEGOTIATION CALLS PHASE (cancellable) 179 | 180 | // self transitions 181 | if (!aborted && this.type != MutationTypes.DROP) { 182 | aborted = this.self() === false 183 | } 184 | 185 | // exit transitions 186 | if (!aborted) { 187 | for (let state of this.exits) { 188 | if (false === this.exit(state)) { 189 | aborted = true 190 | this.addStep(state, null, TransitionStepTypes.CANCEL) 191 | continue 192 | } 193 | } 194 | } 195 | // enter transitions 196 | if (!aborted) { 197 | for (let state of this.enters) { 198 | if (false === this.enter(state)) { 199 | aborted = true 200 | this.addStep(state, null, TransitionStepTypes.CANCEL) 201 | continue 202 | } 203 | } 204 | } 205 | 206 | // STATE CALLS PHASE (non cancellable) 207 | if (!aborted) { 208 | // TODO extract 209 | machine.setActiveStates_(this.requested_states, [...this.states]) 210 | this.processPostTransition() 211 | hasStateChanged = machine.hasStateChanged(this.before) 212 | if (hasStateChanged) { 213 | machine.emit('tick', this.before) 214 | } 215 | } 216 | } catch (ex) { 217 | // TODO extract 218 | let err = ex as TransitionException 219 | aborted = true 220 | // Its an exception to an exception when the exception throws... 221 | // an exception 222 | if ( 223 | err.transition && 224 | (err.transition.match(/^Exception_/) || 225 | err.transition.match(/_Exception$/)) 226 | ) { 227 | // TODO honor this.machine.print_exception 228 | machine.setImmediate(() => { 229 | throw err.err 230 | }) 231 | } else { 232 | let queued_exception: IQueueRow = [ 233 | MutationTypes.ADD, 234 | ['Exception'], 235 | [err.err, this.states, this.before, err.transition], 236 | false, 237 | machine, 238 | uuid() 239 | ] 240 | // drop the queue created during the transition 241 | // @ts-ignore 242 | this.source_machine.queue_.unshift(queued_exception) 243 | } 244 | } 245 | 246 | machine.transition = null 247 | machine.lock = false 248 | 249 | if (aborted) { 250 | machine.emit('transition-cancelled', this) 251 | } else if (hasStateChanged && !this.row[QueueRowFields.AUTO]) { 252 | var auto_states = this.prepareAutoStates() 253 | if (auto_states) 254 | // prepend auto states to the beginning of the queue 255 | // @ts-ignore 256 | this.source_machine.queue_.unshift(auto_states) 257 | // target.queue_.unshift(auto_states) 258 | } 259 | 260 | machine.emit('transition-end', this) 261 | this.events = [] 262 | 263 | if (aborted) return false 264 | 265 | // If this's a DROP transition, check if all explicit states has been 266 | // dropped. 267 | if (this.row[QueueRowFields.STATE_CHANGE_TYPE] === MutationTypes.DROP) { 268 | return machine.not(this.row[QueueRowFields.STATES]) 269 | } else { 270 | return machine.is(this.states) 271 | } 272 | } 273 | 274 | setupAccepted() { 275 | // Dropping states doesn't require an acceptance 276 | // Auto-states can be set partially 277 | if (this.type !== MutationTypes.DROP && !this.auto) { 278 | let not_accepted = this.machine.diffStates( 279 | this.requested_states, 280 | this.states 281 | ) 282 | if (not_accepted.length) { 283 | this.machine.log(`[cancelled:rejected] ${not_accepted.join(', ')}`, 3) 284 | this.addStepsFor(not_accepted, null, TransitionStepTypes.CANCEL) 285 | this.accepted = false 286 | } 287 | } 288 | } 289 | 290 | resolveRelations(states: string[]): void { 291 | states = this.machine.parseStates(states) 292 | states = this.parseAddRelation(states) 293 | states = this.removeDuplicateStates_(states) 294 | 295 | // Check if state is blocked or excluded 296 | var already_blocked: string[] = [] 297 | 298 | // Parsing required states allows to avoid cross-dropping of states 299 | states = this.parseRequires_(states) 300 | 301 | // Remove states already blocked. 302 | states = states.reverse().filter(name => { 303 | let blocked_by = this.isStateBlocked_(states, name) 304 | blocked_by = blocked_by.filter( 305 | blocker_name => !already_blocked.includes(blocker_name) 306 | ) 307 | 308 | if (blocked_by.length) { 309 | already_blocked.push(name) 310 | // if state wasn't implied by another state (was one of the current 311 | // states) then make it a higher priority log msg 312 | let level = this.machine.is(name) ? 2 : 3 313 | this.machine.log(`[rel:drop] ${name} by ${blocked_by.join(', ')}`, level) 314 | if (this.machine.is(name)) { 315 | this.addStep(name, null, TransitionStepTypes.DROP) 316 | } else { 317 | this.addStep(name, null, TransitionStepTypes.NO_SET) 318 | } 319 | } 320 | return !blocked_by.length 321 | }) 322 | 323 | // states dropped by the states which are about to be set 324 | const to_drop = states.reduce((ret: string[], name: string) => { 325 | const state = this.machine.get(name) 326 | if (state.drop) { 327 | ret.push(...state.drop) 328 | } 329 | return ret 330 | }, []) 331 | states = this.parseAddRelation(states).filter(n => !to_drop.includes(n)) 332 | states = this.removeDuplicateStates_(states) 333 | // Parsing required states allows to avoid cross-dropping of states 334 | this.states = this.parseRequires_(states.reverse()) 335 | this.orderStates_(this.states) 336 | } 337 | 338 | // TODO log it better 339 | // Returns a queue entry with auto states 340 | // TODO should pass them through resolveRelations() ? 341 | prepareAutoStates(): IQueueRow | null { 342 | var add: string[] = [] 343 | 344 | for (let state of this.machine.states_all) { 345 | let is_current = () => this.machine.is(state) 346 | let is_blocked = () => 347 | this.machine.is().some(current => { 348 | let relations = this.machine.get(current) 349 | if (!relations.drop) { 350 | return false 351 | } 352 | return relations.drop.includes(state) 353 | }) 354 | 355 | if (this.machine.get(state).auto && !is_current() && !is_blocked()) { 356 | add.push(state) 357 | } 358 | } 359 | 360 | if (add.length) { 361 | return [MutationTypes.ADD, add, [], true, this.machine, uuid()] 362 | } 363 | 364 | return null 365 | } 366 | 367 | // Collect implied states 368 | protected parseAddRelation(states: string[]): string[] { 369 | let ret = [...states] 370 | let changed = true 371 | let visited: string[] = [] 372 | while (changed) { 373 | changed = false 374 | for (let name of ret) { 375 | // get implied states only from states which are about to be activated 376 | if (this.before.includes(name)) continue 377 | let state = this.machine.get(name) 378 | if (visited.includes(name) || !state.add) continue 379 | this.addStepsFor( 380 | state.add, 381 | name, 382 | TransitionStepTypes.RELATION, 383 | StateRelations.ADD 384 | ) 385 | this.addStepsFor(state.add, null, TransitionStepTypes.SET) 386 | ret.push(...state.add) 387 | visited.push(name) 388 | changed = true 389 | } 390 | } 391 | 392 | return ret 393 | } 394 | 395 | // Check required states 396 | // Loop until no change happens, as states can require themselves in a vector. 397 | parseRequires_(states: string[]): string[] { 398 | let length_before = 0 399 | let not_found_by_states: { [name: string]: string[] } = {} 400 | while (length_before !== states.length) { 401 | length_before = states.length 402 | states = states.filter(name => { 403 | let state = this.machine.get(name) 404 | let not_found: string[] = [] 405 | for (let req of state.require || []) { 406 | this.addStep( 407 | req, 408 | name, 409 | TransitionStepTypes.RELATION, 410 | StateRelations.REQUIRE 411 | ) 412 | // TODO if required state is auto, add it (avoid an inf loop) 413 | if (!states.includes(req)) { 414 | not_found.push(req) 415 | this.addStep(name, null, TransitionStepTypes.NO_SET) 416 | if (this.requested_states.includes(name)) { 417 | this.addStep(req, null, TransitionStepTypes.CANCEL) 418 | } 419 | } 420 | } 421 | 422 | if (not_found.length) { 423 | not_found_by_states[name] = not_found 424 | } 425 | 426 | return !not_found.length 427 | }) 428 | } 429 | 430 | if (Object.keys(not_found_by_states).length) { 431 | let names: string[] = [] 432 | for (let [state, not_found] of Object.entries(not_found_by_states)) 433 | names.push(`${state}(-${not_found.join(' -')})`) 434 | 435 | if (this.auto) { 436 | this.machine.log(`[rejected:auto] ${names.join(' ')}`, 3) 437 | } else { 438 | this.machine.log(`[rejected] ${names.join(' ')}`, 2) 439 | } 440 | } 441 | 442 | return states 443 | } 444 | 445 | /** 446 | * Returns the subset of states which block the state name. 447 | */ 448 | isStateBlocked_(states: string[], name: string): string[] { 449 | var blocked_by: string[] = [] 450 | for (let name2 of states) { 451 | let state = this.machine.get(name2) 452 | if (state.drop && state.drop.includes(name)) { 453 | this.addStep( 454 | name, 455 | name2, 456 | TransitionStepTypes.RELATION, 457 | StateRelations.DROP 458 | ) 459 | blocked_by.push(name2) 460 | } 461 | } 462 | 463 | return blocked_by 464 | } 465 | 466 | orderStates_(states: string[]): void { 467 | states.sort((name1, name2) => { 468 | var state1 = this.machine.get(name1) 469 | var state2 = this.machine.get(name2) 470 | var ret = 0 471 | if (state1.after && state1.after.includes(name2)) { 472 | ret = 1 473 | this.addStep( 474 | name2, 475 | name1, 476 | TransitionStepTypes.RELATION, 477 | StateRelations.AFTER 478 | ) 479 | } else { 480 | if (state2.after && state2.after.includes(name1)) { 481 | ret = -1 482 | this.addStep( 483 | name1, 484 | name2, 485 | TransitionStepTypes.RELATION, 486 | StateRelations.AFTER 487 | ) 488 | } 489 | } 490 | return ret 491 | }) 492 | } 493 | 494 | // TODO use a module 495 | removeDuplicateStates_(states: string[]): string[] { 496 | let found = {} 497 | 498 | return states.filter(name => { 499 | if (found[name]) return false 500 | found[name] = true 501 | return true 502 | }) 503 | } 504 | 505 | setupExitEnter(): void { 506 | let from = this.machine.states_active.filter( 507 | state => !this.states.includes(state) 508 | ) 509 | 510 | this.orderStates_(from) 511 | 512 | // queue the exit transitions 513 | for (let state of from) this.exits.push(state) 514 | 515 | // queue the enter transitions 516 | for (let state of this.states) { 517 | // dont enter to already set states, except when it's a MULTI state 518 | // TODO write tests for multi state 519 | if ( 520 | this.machine.is(state) && 521 | !( 522 | this.machine.get(state).multi && this.requested_states.includes(state) 523 | ) 524 | ) { 525 | continue 526 | } 527 | 528 | this.enters.push(state) 529 | } 530 | } 531 | 532 | // Executes self transitions (eg ::A_A) based on active states. 533 | self() { 534 | return !this.requested_states.some(state => { 535 | // only the active states 536 | if (!this.machine.is(state)) return false 537 | 538 | let ret = true 539 | let name = `${state}_${state}` 540 | // pass the transition params only to the explicite states 541 | let params = this.requested_states.includes(state) ? this.params : [] 542 | let context = this.machine.getMethodContext(name) 543 | 544 | try { 545 | if (context) { 546 | this.machine.log('[transition] ' + name, 2) 547 | this.addStep(state, state, TransitionStepTypes.TRANSITION, name) 548 | ret = context[name](...params) 549 | this.machine.catchPromise(ret, this.states) 550 | } else { 551 | this.machine.log('[transition] ' + name, 4) 552 | } 553 | 554 | if (ret === false) { 555 | this.machine.log(`[cancelled:self] ${state}`, 2) 556 | this.addStep(state, null, TransitionStepTypes.CANCEL) 557 | return true 558 | } 559 | 560 | ret = this.machine.emit(name as 'ts-dynamic', ...params) 561 | } catch (err) { 562 | throw new TransitionException(err, name) 563 | } 564 | 565 | if (ret !== false) { 566 | this.events.push([name, params]) 567 | } 568 | 569 | if (ret === false) { 570 | this.machine.log(`[cancelled:self] ${state}`, 2) 571 | this.addStep(state, null, TransitionStepTypes.CANCEL) 572 | return true 573 | } 574 | return false 575 | }) 576 | } 577 | 578 | enter(to: string) { 579 | let params = this.requested_states.includes(to) ? this.params : [] 580 | let ret = this.transitionExec_('Any', to, 'any_' + to, params) 581 | if (ret === false) return false 582 | 583 | return this.transitionExec_(to, null, to + '_enter', params) 584 | } 585 | 586 | // Exit transition handles state-to-state methods. 587 | exit(from: string) { 588 | let transition_params: any[] = [] 589 | // this means a 'drop' transition 590 | if (this.requested_states.includes(from)) { 591 | transition_params = this.params 592 | } 593 | 594 | let ret = this.transitionExec_( 595 | from, 596 | null, 597 | from + '_exit', 598 | transition_params 599 | ) 600 | if (ret === false) return false 601 | 602 | ret = this.states.some(state => { 603 | let transition = from + '_' + state 604 | transition_params = this.requested_states.includes(state) 605 | ? this.params 606 | : [] 607 | ret = this.transitionExec_(from, state, transition, transition_params) 608 | return ret === false 609 | }) 610 | 611 | if (ret === true) return false 612 | 613 | return !(this.transitionExec_(from, 'Any', from + '_any') === false) 614 | } 615 | 616 | transitionExec_( 617 | from: string, 618 | to: string | null, 619 | method: string, 620 | params: string[] = [] 621 | ) { 622 | const context = this.machine.getMethodContext(method) 623 | let ret 624 | 625 | try { 626 | if (context) { 627 | this.machine.log('[transition] ' + method, 2) 628 | this.addStep(from, to, TransitionStepTypes.TRANSITION, method) 629 | ret = context[method](...params) 630 | this.machine.catchPromise(ret, this.states) 631 | } else { 632 | this.machine.log('[transition] ' + method, 4) 633 | } 634 | 635 | if (ret !== false) { 636 | let is_exit = method.slice(-5) === '_exit' 637 | let is_enter = !is_exit && method.slice(-6) === '_enter' 638 | // TODO bad bad bad 639 | if (is_exit || is_enter) { 640 | this.events.push([method, params]) 641 | } 642 | ret = this.machine.emit(method as 'ts-dynamic', ...params) 643 | if (ret === false) { 644 | this.machine.log( 645 | `[cancelled] ${this.states.join(', ')} by the event ${method}`, 646 | 2 647 | ) 648 | } 649 | } else { 650 | this.machine.log( 651 | `[cancelled] ${this.states.join(', ')} by the method ${method}`, 652 | 2 653 | ) 654 | } 655 | } catch (err) { 656 | throw new TransitionException(err, method) 657 | } 658 | 659 | return ret 660 | } 661 | 662 | // TODO this is hacky, should be integrated into processTransition 663 | // the names arent the best way of queueing transition calls 664 | processPostTransition() { 665 | let transition: IEvent | undefined 666 | while ((transition = this.events.shift())) { 667 | let name = transition[0] 668 | let params = transition[1] 669 | let is_enter = false 670 | let state: string 671 | let method: string 672 | 673 | if (name.slice(-5) === '_exit') { 674 | state = name.slice(0, -5) 675 | method = state + '_end' 676 | } else if (name.slice(-6) === '_enter') { 677 | is_enter = true 678 | state = name.slice(0, -6) 679 | method = state + '_state' 680 | } else { 681 | // self transition 682 | continue 683 | } 684 | 685 | try { 686 | const context = this.machine.getMethodContext(method) 687 | if (context) { 688 | this.machine.log('[transition] ' + method, 2) 689 | this.addStep(state, null, TransitionStepTypes.TRANSITION, name) 690 | let ret = context[method](...params) 691 | this.machine.catchPromise(ret, this.machine.is()) 692 | } else { 693 | this.machine.log('[transition] ' + method, 4) 694 | } 695 | 696 | this.machine.emit(method as 'ts-dynamic', ...params) 697 | } catch (err) { 698 | err = new TransitionException(err, method) 699 | // TODO addStep for TransitionStepTypes.EXCEPTION 700 | this.processPostTransitionException(state, is_enter) 701 | throw err 702 | } 703 | } 704 | } 705 | 706 | // TODO REFACTOR 707 | processPostTransitionException(state: string, is_enter: boolean) { 708 | const states_active = [...this.machine.states_active] 709 | let transition: IEvent | undefined 710 | // remove non transitioned states from the active list 711 | // in case there was an exception thrown while settings them 712 | while ((transition = this.events.shift())) { 713 | let name = transition[0] 714 | let state: string 715 | 716 | if (name.slice(-5) === '_exit') { 717 | state = name.slice(0, -5) 718 | states_active.push(state) 719 | } else if (name.slice(-6) === '_enter') { 720 | state = name.slice(0, -6) 721 | states_active.splice(states_active.indexOf(state), 1) 722 | } 723 | } 724 | // handle the state which caused the exception 725 | if (is_enter) { 726 | states_active.splice(states_active.indexOf(state), 1) 727 | } else { 728 | states_active.push(state) 729 | } 730 | // override the active states, reverting the un-executed transitions 731 | this.machine.states_active = states_active 732 | this.machine.log( 733 | `[exception] from ${state}, forced states to ${states_active.join(', ')}` 734 | ) 735 | this.machine.log('[state:force] ' + states_active.join(', '), 1) 736 | } 737 | 738 | /** 739 | * Marks a steps relation between two states during the transition. 740 | */ 741 | addStep( 742 | target: string | IStateStruct, 743 | source?: string | IStateStruct | null, 744 | type?: TransitionStepTypes, 745 | data?: any 746 | ): void { 747 | let step = this.addStepData(target, source, type, data) 748 | this.machine.emit('transition-step', step) 749 | } 750 | 751 | /** 752 | * Marks a steps relation between two states during the transition. 753 | */ 754 | addStepData( 755 | target: string | IStateStruct, 756 | source?: string | IStateStruct | null, 757 | type?: TransitionStepTypes, 758 | data?: any 759 | ): ITransitionStep { 760 | let state = Array.isArray(target) 761 | ? (target as IStateStruct) 762 | : ([this.machine.id(true), target as string] as IStateStruct) 763 | let source_state: IStateStruct | undefined 764 | 765 | if (source) { 766 | source_state = Array.isArray(source) 767 | ? (source as IStateStruct) 768 | : [this.machine.id(true), source as string] 769 | } 770 | 771 | let step: ITransitionStep = [state, source_state, type, data] 772 | this.steps.push(step) 773 | return step 774 | } 775 | 776 | /** 777 | * Same as [[addStep]], but produces a step for many targets. 778 | */ 779 | addStepsFor( 780 | targets: string[] | IStateStruct[], 781 | source?: string | IStateStruct | null, 782 | type?: TransitionStepTypes, 783 | data?: any 784 | ): void { 785 | // TODO `targets as string[]` required as of TS 2.0 786 | let steps: ITransitionStep[] = (targets as string[]).map(target => { 787 | return this.addStepData(target, source, type, data) 788 | }) 789 | this.machine.emit('transition-step', ...steps) 790 | } 791 | 792 | /** 793 | * Produces a readable list of steps states. 794 | * 795 | * Example: 796 | * ``` 797 | * A REQUESTED 798 | * D REQUESTED 799 | * A SET 800 | * D SET 801 | * D -> B RELATION add 802 | * D -> C RELATION add 803 | * B SET 804 | * C SET 805 | * E -> D RELATION drop 806 | * E DROP 807 | * ``` 808 | * 809 | * TODO loose casts once condition guards work again 810 | */ 811 | toString() { 812 | let fields = TransitionStepFields 813 | let s = StateStructFields 814 | let types = TransitionStepTypes 815 | 816 | return this.steps 817 | .map(touch => { 818 | let line = '' 819 | if (touch[fields.SOURCE_STATE]) { 820 | line += 821 | (touch[fields.SOURCE_STATE] as IStateStruct)[s.STATE_NAME] + ' -> ' 822 | } 823 | line += touch[fields.STATE][s.STATE_NAME] 824 | line += ' ' 825 | if (touch[fields.TYPE]) { 826 | line += types[touch[fields.TYPE] as TransitionStepTypes] 827 | } 828 | if (touch[fields.DATA]) { 829 | line += ' ' + touch[fields.DATA] 830 | } 831 | 832 | return line 833 | }) 834 | .join('\n') 835 | } 836 | } 837 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import AsyncMachine from './asyncmachine' 2 | import Transition from './transition' 3 | 4 | export type BaseStates = 'Exception' 5 | 6 | export type TAsyncMachine = AsyncMachine 7 | 8 | export interface IBind { 9 | ( 10 | event: 'tick', 11 | listener: (before: string[]) => boolean | void, 12 | context?: Object 13 | ): this 14 | ( 15 | event: 'id-changed', 16 | listener: (new_id: string, old_id: string) => boolean | void, 17 | context?: Object 18 | ): this 19 | ( 20 | event: 'transition-init', 21 | listener: (transition: Transition) => boolean | void, 22 | context?: Object 23 | ): this 24 | ( 25 | event: 'transition-start', 26 | listener: (transition: Transition) => boolean | void, 27 | context?: Object 28 | ): this 29 | ( 30 | event: 'transition-end', 31 | listener: (transition: Transition) => boolean | void, 32 | context?: Object 33 | ): this 34 | ( 35 | event: 'transition-step', 36 | listener: (...steps: ITransitionStep[]) => boolean | void, 37 | context?: Object 38 | ): this 39 | ( 40 | event: 'pipe', 41 | listener: (pipe: TPipeBindings) => boolean | void, 42 | context?: Object 43 | ): this 44 | // TODO 45 | // (event: 'pipe-in', listener: 46 | // (pipe: TPipeBindings) => boolean | void, context?: Object): this; 47 | // (event: 'pipe-out', listener: 48 | // (pipe: TPipeBindings) => boolean | void, context?: Object): this; 49 | // (event: 'pipe-in-removed', listener: 50 | // (pipe: TPipeBindings) => boolean | void, context?: Object): this; 51 | // (event: 'pipe-out-removed', listener: 52 | // (pipe: TPipeBindings) => boolean | void, context?: Object): this; 53 | ( 54 | event: 'state-registered', 55 | listener: (state: string) => boolean | void, 56 | context?: Object 57 | ): this 58 | ( 59 | event: 'state-deregistered', 60 | listener: (state: string) => boolean | void, 61 | context?: Object 62 | ): this 63 | ( 64 | event: 'transition-cancelled', 65 | listener: (transition: Transition) => boolean | void, 66 | context?: Object 67 | ): this 68 | ( 69 | event: 'queue-changed', 70 | listener: () => boolean | void, 71 | context?: Object 72 | ): this 73 | (event: 'queue-end', listener: () => boolean | void, context?: Object): this 74 | // State events 75 | // TODO optional params 76 | ( 77 | event: 'Exception_enter', 78 | listener: ( 79 | err: Error, 80 | target_states: string[], 81 | base_states: string[], 82 | exception_transition: string, 83 | async_target_states?: string[] 84 | ) => boolean | void, 85 | context?: Object 86 | ): this 87 | ( 88 | event: 'Exception_state', 89 | listener: ( 90 | err: Error, 91 | target_states: string[], 92 | base_states: string[], 93 | exception_transition: string, 94 | async_target_states?: string[] 95 | ) => any, 96 | context?: Object 97 | ): this 98 | ( 99 | event: 'Exception_exit', 100 | listener: () => boolean | void, 101 | context?: Object 102 | ): this 103 | (event: 'Exception_end', listener: () => any, context?: Object): this 104 | ( 105 | event: 'Exception_Any', 106 | listener: () => boolean | void, 107 | context?: Object 108 | ): this 109 | ( 110 | event: 'Any_Exception', 111 | listener: () => boolean | void, 112 | context?: Object 113 | ): this 114 | // TODO better compiler errors for incorrect calls 115 | (event: 'ts-dynamic', listener: Function): this 116 | } 117 | 118 | export interface IEmit { 119 | (event: 'tick', before: string[]): boolean 120 | (event: 'id-changed', new_id: string, old_id: string): boolean 121 | (event: 'transition-init', transition: Transition): boolean 122 | (event: 'transition-start', transition: Transition): boolean 123 | (event: 'transition-end', transition: Transition): boolean 124 | (event: 'transition-step', ...steps: ITransitionStep[]): boolean 125 | // (event: 'pipe', pipe: TPipeBindings): boolean; 126 | (event: 'pipe'): boolean 127 | // TODO 128 | // (event: 'pipe-in', pipe: TPipeBindings): boolean; 129 | // (event: 'pipe-out', pipe: TPipeBindings): boolean; 130 | // (event: 'pipe-in-removed', pipe: TPipeBindings): boolean; 131 | // (event: 'pipe-out-removed', pipe: TPipeBindings): boolean; 132 | (event: 'state-registered', state: string): boolean 133 | (event: 'state-deregistered', state: string): boolean 134 | (event: 'transition-cancelled', transition: Transition): boolean 135 | (event: 'queue-changed'): boolean 136 | // State events 137 | // TODO optional params 138 | ( 139 | event: 'Exception_enter', 140 | err: Error, 141 | target_states: string[], 142 | base_states: string[], 143 | exception_transition: string, 144 | async_target_states?: string[] 145 | ): boolean 146 | ( 147 | event: 'Exception_state', 148 | err: Error, 149 | target_states: string[], 150 | base_states: string[], 151 | exception_transition: string, 152 | async_target_states?: string[] 153 | ): any 154 | (event: 'Exception_exit'): boolean 155 | (event: 'Exception_end'): boolean 156 | (event: 'Exception_Any'): boolean 157 | (event: 'Any_Exception'): boolean 158 | (event: 'queue-end'): boolean 159 | // skip compiler errors for dynamic calls 160 | (event: 'ts-dynamic', ...params: any[]): boolean 161 | } 162 | 163 | export interface IState { 164 | /** Decides about the order of activations (transitions) */ 165 | after?: (T | BaseStates)[] 166 | /** When set, sets also the following states */ 167 | add?: (T | BaseStates)[] 168 | /** When set, blocks activation (or deactivates) given states */ 169 | drop?: (T | BaseStates)[] 170 | /** State will be rejected if any of those aren't set */ 171 | require?: (T | BaseStates)[] 172 | /** When true, the state will be set automatically, if it's not blocked */ 173 | auto?: boolean 174 | /** 175 | * Multi states always triggers the enter and state transitions, plus 176 | * the clock is always incremented 177 | */ 178 | multi?: boolean 179 | } 180 | 181 | export enum MutationTypes { 182 | DROP, 183 | ADD, 184 | SET 185 | } 186 | 187 | /** 188 | * Queue enum defining param positions in queue's entries. 189 | */ 190 | export enum QueueRowFields { 191 | STATE_CHANGE_TYPE, 192 | STATES, 193 | PARAMS, 194 | AUTO, 195 | TARGET, 196 | ID 197 | } 198 | 199 | export interface IQueueRow { 200 | 0: MutationTypes 201 | 1: string[] 202 | 2: any[] 203 | 3: boolean 204 | 4: TAsyncMachine 205 | // ID 206 | 5: string 207 | } 208 | 209 | export class Deferred { 210 | promise: Promise 211 | 212 | // @ts-ignore 213 | resolve: (...params: any[]) => void 214 | 215 | // @ts-ignore 216 | reject: (err?: any) => void 217 | 218 | constructor() { 219 | this.promise = new Promise((resolve, reject) => { 220 | this.resolve = resolve 221 | this.reject = reject 222 | }) 223 | } 224 | } 225 | 226 | export enum StateRelations { 227 | AFTER = 'after', 228 | ADD = 'add', 229 | REQUIRE = 'require', 230 | DROP = 'drop' 231 | } 232 | 233 | export enum TransitionStepTypes { 234 | RELATION = 1, 235 | TRANSITION = 1 << 2, 236 | SET = 1 << 3, 237 | DROP = 1 << 4, 238 | NO_SET = 1 << 5, 239 | REQUESTED = 1 << 6, 240 | CANCEL = 1 << 7, 241 | PIPE = 1 << 8 242 | } 243 | 244 | export enum StateStructFields { 245 | MACHINE_ID, 246 | STATE_NAME 247 | } 248 | 249 | export interface IStateStruct { 250 | /* StateStructFields.MACHINE_ID */ 251 | 0: string 252 | /* StateStructFields.STATE_NAME */ 253 | 1: string 254 | } 255 | 256 | export enum TransitionStepFields { 257 | STATE, 258 | SOURCE_STATE, 259 | TYPE, 260 | DATA 261 | } 262 | 263 | export interface ITransitionStep { 264 | /* TransitionStepFields.STATE */ 265 | 0: IStateStruct 266 | /* TransitionStepFields.SOURCE_STATE */ 267 | 1?: IStateStruct 268 | /* TransitionStepFields.TYPE */ 269 | 2?: TransitionStepTypes 270 | /* TransitionStepFields.DATA (eg a transition method name, relation type) */ 271 | 3?: any 272 | } 273 | 274 | export type TAbortFunction = () => boolean 275 | 276 | // TODO merge with the enum 277 | export type TStateAction = 'add' | 'drop' | 'set' 278 | export type TStateMethod = 'enter' | 'exit' | 'state' | 'end' 279 | 280 | export interface IPipeNegotiationBindings { 281 | enter: TStateAction 282 | exit: TStateAction 283 | } 284 | 285 | export interface IPipeStateBindings { 286 | state: TStateAction 287 | end: TStateAction 288 | } 289 | 290 | export type TPipeBindings = IPipeStateBindings | IPipeNegotiationBindings 291 | 292 | export interface IPipedStateTarget { 293 | state: string 294 | machine: TAsyncMachine 295 | event_type: TStateMethod 296 | listener: Function 297 | flags?: PipeFlags 298 | } 299 | 300 | /** 301 | * By default piped are "_state" and "_end" methods, not the negotiation ones. 302 | * Use the PipeFlags.NEGOTIATION flag to pipe "_enter" and "_exit" methods, and 303 | * thus, to participate in the state negotiation. This mode DOES NOT guarantee, 304 | * that the state was successfully negotiated in the source machine. 305 | * 306 | * To invert the state, use the PipeFlags.INVERT flag. 307 | * 308 | * To append the transition to the local queue (instead of the target 309 | * machine's one), use the PipeFlags.LOCAL_QUEUE. This will alter the 310 | * transition order. 311 | */ 312 | export enum PipeFlags { 313 | NEGOTIATION = 1, 314 | INVERT = 1 << 2, 315 | LOCAL_QUEUE = 1 << 3, 316 | // TODO write tests for those 317 | FINAL = 1 << 4, 318 | NEGOTIATION_ENTER = 1 << 5, 319 | NEGOTIATION_EXIT = 1 << 6, 320 | FINAL_ENTER = 1 << 7, 321 | FINAL_EXIT = 1 << 8 322 | } 323 | 324 | export const PipeFlagsLabels = { 325 | NEGOTIATION: 'neg', 326 | INVERT: 'inv', 327 | LOCAL_QUEUE: 'loc', 328 | FINAL: 'fin', 329 | NEGOTIATION_ENTER: 'neg_enter', 330 | NEGOTIATION_EXIT: 'neg_exit', 331 | FINAL_ENTER: 'fin_enter', 332 | FINAL_EXIT: 'fin_exit' 333 | } 334 | 335 | export class TransitionException extends Error { 336 | constructor(public err: Error, public transition: string) { 337 | super() 338 | } 339 | } 340 | 341 | export class NonExistingStateError extends Error { 342 | constructor(name: string) { 343 | super('NonExistingStateError: ' + name) 344 | } 345 | } 346 | 347 | export type TLogHandler = (msg: string, level?: number) => any 348 | -------------------------------------------------------------------------------- /test/exceptions.ts: -------------------------------------------------------------------------------- 1 | import AsyncMachine, { machine } from '../src/asyncmachine' 2 | import * as chai from 'chai' 3 | import * as sinon from 'sinon' 4 | import * as sinonChai from 'sinon-chai' 5 | 6 | let { expect } = chai 7 | chai.use(sinonChai) 8 | 9 | describe('Exceptions', function() { 10 | beforeEach(function() { 11 | this.foo = machine(['A']) 12 | }) 13 | 14 | it('should be thrown on the next tick', function() { 15 | let setImmediate = sinon.stub(this.foo, 'setImmediate') 16 | this.foo.A_enter = function() { 17 | throw new Error() 18 | } 19 | this.foo.add('A') 20 | expect(setImmediate.calledOnce).to.be.ok 21 | expect(setImmediate.firstCall.args[0]).to.throw(Error) 22 | }) 23 | 24 | it('should set the Exception state', function() { 25 | this.foo.A_enter = function() { 26 | throw new Error() 27 | } 28 | this.foo.Exception_state = function() {} 29 | this.foo.add('A') 30 | expect(this.foo.is()).to.eql(['Exception']) 31 | }) 32 | 33 | it('should pass all the params to the method', function(done) { 34 | let states = machine(['A', 'B', 'C']) 35 | states.C_enter = function() { 36 | throw new Error() 37 | } 38 | 39 | states.Exception_state = function( 40 | err, 41 | target_states, 42 | base_states, 43 | exception_transition, 44 | async_target_states 45 | ) { 46 | expect(target_states).to.eql(['B', 'C']) 47 | expect(base_states).to.eql(['A']) 48 | expect(exception_transition).to.eql('C_enter') 49 | expect(async_target_states).to.eql(undefined) 50 | done() 51 | } 52 | 53 | states.set(['A']) 54 | states.set(['B', 'C']) 55 | }) 56 | 57 | it('should set accept completed transition') 58 | 59 | describe('should be caught', function() { 60 | it('from next-tick state changes', function(done) { 61 | this.foo.A_enter = function() { 62 | throw new Error() 63 | } 64 | this.foo.Exception_state = () => done() 65 | this.foo.addNext('A') 66 | }) 67 | 68 | it('from a callbacks error param', function(done) { 69 | let delayed = callback => setTimeout(callback.bind(null, new Error()), 0) 70 | delayed(this.foo.addByCallback('A')) 71 | this.foo.Exception_state = () => done() 72 | }) 73 | 74 | it('from deferred changes', function(done) { 75 | this.foo.A_enter = function() { 76 | throw new Error() 77 | } 78 | let delayed = callback => setTimeout(callback, 0) 79 | delayed(this.foo.addByListener('A')) 80 | this.foo.Exception_state = () => done() 81 | }) 82 | 83 | describe('in promises', function() { 84 | it('returned by transitions', function(done) { 85 | this.foo.Exception_state = function( 86 | exception, 87 | target_states, 88 | base_states, 89 | exception_state 90 | ) { 91 | expect(target_states).to.eql(['A']) 92 | expect(exception).to.be.instanceOf(Error) 93 | done() 94 | } 95 | this.foo.A_enter = () => 96 | new Promise((resolve, reject) => 97 | setTimeout(() => reject(new Error())) 98 | ) 99 | this.foo.add('A') 100 | }) 101 | 102 | it('returned by listeners', function(done) { 103 | this.foo.Exception_state = function( 104 | exception, 105 | target_states, 106 | base_states, 107 | exception_state 108 | ) { 109 | expect(target_states).to.eql(['A']) 110 | expect(exception).to.be.instanceOf(Error) 111 | done() 112 | } 113 | this.foo.on( 114 | 'A_enter', 115 | new Promise((resolve, reject) => 116 | setTimeout(() => reject(new Error())) 117 | ) 118 | ) 119 | this.foo.add('A') 120 | }) 121 | 122 | it('returned by the state binding "when()"') 123 | // TODO 124 | }) 125 | }) 126 | 127 | describe('complex scenario', function() { 128 | before(function() { 129 | let asyncMock = cb => setTimeout(cb.bind(null), 0) 130 | 131 | this.foo = machine(['A', 'B', 'C']) 132 | this.bar = machine(['D']) 133 | this.bar.pipe('D', this.foo, 'A') 134 | this.foo.A_enter = function() { 135 | this.add('B') 136 | } 137 | this.foo.B_state = function() { 138 | new Promise((resolve, reject) => { 139 | setTimeout(() => { 140 | resolve(this.add('C')) 141 | }) 142 | }) 143 | } 144 | this.foo.C_enter = function() { 145 | throw { fake: true } 146 | } 147 | }) 148 | 149 | it('should be caught', function(done) { 150 | this.bar.addByListener('D')() 151 | this.foo.Exception_state = () => done() 152 | }) 153 | }) 154 | }) 155 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter dot 2 | --require source-map-support/register -------------------------------------------------------------------------------- /test/piping.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai' 2 | import * as sinon from 'sinon' 3 | import * as sinonChai from 'sinon-chai' 4 | import AsyncMachine, { machine, PipeFlags } from '../src/asyncmachine' 5 | import { IBind, IEmit } from '../src/types' 6 | import { assert_order, mock_states } from './utils' 7 | 8 | let { expect } = chai 9 | chai.use(sinonChai) 10 | 11 | type ABCD = 'A' | 'B' | 'C' | 'D' 12 | type XYZ = 'X' | 'Y' | 'Z' 13 | 14 | describe('piping', function() { 15 | it('should forward a specific state', function() { 16 | let source = machine(['A', 'B', 'C', 'D']) 17 | source.set('A') 18 | let target = machine(['A', 'B', 'C', 'D']) 19 | 20 | source.pipe('A', target) 21 | source.pipe('B', target) 22 | 23 | expect(source.piped['A']).to.have.length(2) 24 | expect(source.piped['B']).to.have.length(2) 25 | expect(target.is()).to.be.eql(['A']) 26 | source.set('B') 27 | expect(target.is()).to.eql(['B']) 28 | }) 29 | 30 | it('should forward a specific state as a different one', function() { 31 | let source = machine(['A', 'B', 'C', 'D']) 32 | source.set('A') 33 | let target = machine(['X', 'Y', 'Z']) 34 | 35 | source.pipe('B', target, 'X') 36 | source.set('B') 37 | 38 | expect(target.is()).to.eql(['X']) 39 | }) 40 | 41 | it('should invert a specific state as a different one', function() { 42 | let source = machine(['A', 'B', 'C', 'D']).id('source') 43 | source.set('A') 44 | let target = machine(['X', 'Y', 'Z']).id('target') 45 | 46 | source.pipe('A', target, 'X', PipeFlags.INVERT) 47 | source.drop('A') 48 | 49 | expect(target.is()).to.eql(['X']) 50 | }) 51 | 52 | it('should work for negotiation', function() { 53 | let source = machine(['A', 'B', 'C', 'D']) 54 | let target = machine(['A', 'B', 'C', 'D']) 55 | source.pipe('A', target, null, PipeFlags.NEGOTIATION) 56 | target['A_enter'] = () => false 57 | source.add('A') 58 | 59 | expect(source.is()).to.eql([]) 60 | expect(target.is()).to.eql([]) 61 | }) 62 | 63 | it('should work for both negotiation and final', function() { 64 | // piping negotiation-only phrase does not give you certainty, 65 | // that the state was actually set for the machine A 66 | // in that case, fow now, assert the success via the self-transition 67 | const source = machine(['A', 'B', 'C', 'D']).id('source') 68 | const target_1 = machine(['A', 'B', 'C', 'D']).id('target-1') 69 | const target_2 = machine(['A', 'B', 'C', 'D']).id('target-2') 70 | 71 | source.pipe( 72 | ['A', 'B'], 73 | target_1, 74 | null, 75 | PipeFlags.NEGOTIATION | PipeFlags.FINAL 76 | ) 77 | source.pipe( 78 | ['A', 'B'], 79 | target_2, 80 | null, 81 | PipeFlags.NEGOTIATION | PipeFlags.FINAL 82 | ) 83 | 84 | target_1['A_enter'] = sinon.spy() 85 | // reject the transition after it's done with target_1 86 | target_2['A_enter'] = sinon.stub().returns(false) 87 | target_1['A_A'] = sinon.spy() 88 | target_2['B_enter'] = sinon.spy() 89 | target_2['B_B'] = sinon.spy() 90 | 91 | source.add('A') 92 | source.add('B') 93 | 94 | assert_order([ 95 | target_1['A_enter'], 96 | target_2['A_enter'], 97 | target_2['B_enter'], 98 | target_2['B_B'] 99 | ]) 100 | 101 | expect(target_1['A_A']).not.called 102 | 103 | expect(source.is()).to.eql(['B']) 104 | expect(target_1.is()).to.eql(['B', 'A']) 105 | expect(target_2.is()).to.eql(['B']) 106 | }) 107 | 108 | it('should forward a whole machine', function() { 109 | let source = machine(['A', 'B', 'C', 'D']) 110 | source.set('A') 111 | let target = machine(['A', 'B', 'C', 'D']) 112 | target.set(['A', 'D']) 113 | 114 | expect(target.is()).to.eql(['A', 'D']) 115 | source.pipeAll(target) 116 | // TODO assert the number of pipes 117 | source.set(['B', 'C']) 118 | 119 | expect(target.is()).to.eql(['C', 'B']) 120 | }) 121 | 122 | describe('queue handling', function() { 123 | it("target machine's queue", function() { 124 | let source = machine(['A', 'B', 'C', 'D']) 125 | let target = machine(['A', 'B', 'C', 'D']) 126 | 127 | source.pipe('B', target, null) 128 | source['A_enter'] = function() { 129 | source.add('B') 130 | source.add('C') 131 | } 132 | mock_states(source, ['A', 'B', 'C', 'D']) 133 | mock_states(target, ['A', 'B', 'C', 'D']) 134 | source.add('A') 135 | 136 | assert_order([ 137 | source['A_enter'], 138 | source['B_enter'], 139 | target['B_enter'], 140 | source['C_enter'] 141 | ]) 142 | }) 143 | 144 | it('local queue', function() { 145 | let source = machine(['A', 'B', 'C', 'D']) 146 | let target = machine(['A', 'B', 'C', 'D']) 147 | 148 | source.pipe('B', target, null, PipeFlags.LOCAL_QUEUE) 149 | source['A_enter'] = function() { 150 | source.add('B') 151 | source.add('C') 152 | } 153 | mock_states(source, ['A', 'B', 'C', 'D']) 154 | mock_states(target, ['A', 'B', 'C', 'D']) 155 | source.add('A') 156 | 157 | assert_order([ 158 | source['A_enter'], 159 | source['B_enter'], 160 | source['C_enter'], 161 | target['B_enter'] 162 | ]) 163 | }) 164 | }) 165 | 166 | describe('can be removed', function() { 167 | let source: AsyncMachine 168 | let target: AsyncMachine 169 | 170 | beforeEach(function() { 171 | source = machine(['A', 'B', 'C', 'D']) 172 | target = machine(['A', 'B', 'C', 'D']) 173 | }) 174 | 175 | it('for single state', function() { 176 | source.pipe('A', target) 177 | source.pipeRemove('A') 178 | expect(source.piped.A).to.eql(undefined) 179 | }) 180 | 181 | it('for a whole target machine', function() { 182 | source.pipeAll(target) 183 | source.pipeRemove(null, target) 184 | expect(Object.keys(source.piped)).to.eql([]) 185 | }) 186 | 187 | it('for flags', function() { 188 | source.pipe('A', target, null, PipeFlags.INVERT) 189 | source.pipeRemove('A', null, PipeFlags.INVERT) 190 | expect(source.piped.A).to.eql(undefined) 191 | }) 192 | 193 | it('for all the states', function() { 194 | source.pipeAll(target) 195 | source.pipeRemove() 196 | expect(Object.keys(source.piped)).to.eql([]) 197 | }) 198 | }) 199 | }) 200 | -------------------------------------------------------------------------------- /test/state-binding.ts: -------------------------------------------------------------------------------- 1 | import AsyncMachine, { machine, PipeFlags } from '../src/asyncmachine' 2 | import * as chai from 'chai' 3 | import * as sinon from 'sinon' 4 | import * as sinonChai from 'sinon-chai' 5 | 6 | let { expect } = chai 7 | chai.use(sinonChai) 8 | 9 | describe('State binding', function() { 10 | it('should work for single states', function(done) { 11 | let states = machine(['A', 'B']) 12 | states.when('A').then(function(value) { 13 | expect(value).to.eql(undefined) 14 | done() 15 | }) 16 | states.set('A', 1, 2, 3) 17 | states.set([]) 18 | // assert the double execution 19 | states.set(['A'], 1, 2, 3) 20 | }) 21 | 22 | it('should work for multiple states', function(done) { 23 | let states = machine(['A', 'B']) 24 | states.when(['A', 'B']).then(function(value) { 25 | expect(value).to.eql(undefined) 26 | done() 27 | }) 28 | states.set('A') 29 | states.set('B', 1, 2, 3) 30 | states.set([]) 31 | // assert the double execution 32 | states.set(['A', 'B'], 1, 2, 3) 33 | }) 34 | 35 | describe('disposal', function() { 36 | it('should work with the abort function') 37 | it('should take place after execution') 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /test/tests.ts: -------------------------------------------------------------------------------- 1 | import AsyncMachine, { machine, StateRelations } from '../src/asyncmachine' 2 | import * as chai from 'chai' 3 | import * as sinon from 'sinon' 4 | import * as sinonChai from 'sinon-chai' 5 | 6 | import { 7 | FooMachine, 8 | EventMachine, 9 | Sub, 10 | CrossBlocked, 11 | SubClassRegisterAll, 12 | mock_states, 13 | assert_order 14 | } from './utils' 15 | 16 | let { expect } = chai 17 | chai.use(sinonChai) 18 | 19 | describe('asyncmachine', function() { 20 | beforeEach(function() { 21 | this.machine = new FooMachine() 22 | this.machine.set('A') 23 | }) 24 | 25 | it('should allow to check if single state is active', function() { 26 | expect(this.machine.is('A')).to.be.ok 27 | expect(this.machine.is(['A'])).to.be.ok 28 | }) 29 | 30 | it('should allow to check if multiple states are active', function() { 31 | this.machine.add('B') 32 | expect(this.machine.is(['A', 'B'])).to.be.ok 33 | }) 34 | 35 | it('should expose all available states', function() { 36 | expect(this.machine.states_all).to.eql(['Exception', 'A', 'B', 'C', 'D']) 37 | }) 38 | 39 | it('should allow to set the state', function() { 40 | expect(this.machine.set('B')).to.eql(true) 41 | expect(this.machine.is()).to.eql(['B']) 42 | }) 43 | 44 | it('should allow to add a new state', function() { 45 | expect(this.machine.add('B')).to.eql(true) 46 | expect(this.machine.is()).to.eql(['B', 'A']) 47 | }) 48 | 49 | it('should allow to drop a state', function() { 50 | this.machine.set(['B', 'C']) 51 | expect(this.machine.drop('C')).to.eql(true) 52 | expect(this.machine.is()).to.eql(['B']) 53 | }) 54 | 55 | it('should properly register all the states', function() { 56 | let machine = new AsyncMachine(null, false) 57 | machine['A'] = {} 58 | machine['B'] = {} 59 | 60 | machine.registerAll() 61 | 62 | expect(machine.states_all).to.eql(['Exception', 'A', 'B']) 63 | }) 64 | 65 | it('should properly register all the states from a sub class', function() { 66 | let machine = new SubClassRegisterAll() 67 | expect(machine.states_all).to.eql(['Exception', 'A']) 68 | }) 69 | 70 | it('should throw when activating an unknown state', function() { 71 | let { machine } = this 72 | let func = () => { 73 | return machine.set('unknown') 74 | } 75 | expect(func).to.throw() 76 | }) 77 | 78 | it('should allow to define a new state', function() { 79 | let example = machine(['A']) 80 | example.A = {} 81 | example.register('A') 82 | example.add('A') 83 | expect(example.is()).eql(['A']) 84 | }) 85 | 86 | it('should allow to get relations of a state', function() { 87 | let example = machine<'A' | 'B'>({ 88 | A: { 89 | add: ['B'], 90 | auto: true 91 | }, 92 | B: {} 93 | }) 94 | expect(example.getRelationsOf('A')).eql([StateRelations.ADD]) 95 | }) 96 | 97 | it('should allow to get relations between 2 states', function() { 98 | let example = machine({ 99 | A: { 100 | add: ['B'], 101 | require: ['C'], 102 | auto: true 103 | }, 104 | B: {}, 105 | C: {} 106 | }) 107 | expect(example.getRelationsOf('A', 'B')).eql([StateRelations.ADD]) 108 | }) 109 | 110 | describe('when single to single state transition', function() { 111 | beforeEach(function() { 112 | this.machine = new FooMachine('A') 113 | // mock 114 | mock_states(this.machine, ['A', 'B']) 115 | // exec 116 | this.machine.set('B') 117 | }) 118 | 119 | it('should trigger the state to state transition', function() { 120 | expect(this.machine.A_B.calledOnce).to.be.ok 121 | }) 122 | 123 | it('should trigger the state exit transition', function() { 124 | expect(this.machine.A_exit.calledOnce).to.be.ok 125 | }) 126 | 127 | it('should trigger the transition to the new state', function() { 128 | expect(this.machine.B_enter.calledOnce).to.be.ok 129 | }) 130 | 131 | it('should trigger the transition to "Any" state', function() { 132 | expect(this.machine.A_any.calledOnce).to.be.ok 133 | }) 134 | 135 | it('should trigger the transition from "Any" state', function() { 136 | expect(this.machine.any_B.calledOnce).to.be.ok 137 | }) 138 | 139 | it('should set the correct state', function() { 140 | expect(this.machine.is()).to.eql(['B']) 141 | }) 142 | 143 | it('should remain the correct transition order', function() { 144 | let order = [ 145 | this.machine.A_exit, 146 | this.machine.A_B, 147 | this.machine.A_any, 148 | this.machine.any_B, 149 | this.machine.B_enter 150 | ] 151 | assert_order(order) 152 | }) 153 | }) 154 | 155 | describe('when single to multi state transition', function() { 156 | beforeEach(function() { 157 | this.machine = new FooMachine('A') 158 | // mock 159 | mock_states(this.machine, ['A', 'B', 'C']) 160 | // exec 161 | this.machine.set(['B', 'C']) 162 | }) 163 | 164 | it('should trigger the state to state transitions', function() { 165 | expect(this.machine.A_B.calledOnce).to.be.ok 166 | expect(this.machine.A_C.calledOnce).to.be.ok 167 | }) 168 | 169 | it('should trigger the state exit transition', function() { 170 | expect(this.machine.A_exit.calledOnce).to.be.ok 171 | }) 172 | 173 | it('should trigger the transition to new states', function() { 174 | expect(this.machine.B_enter.calledOnce).to.be.ok 175 | expect(this.machine.C_enter.calledOnce).to.be.ok 176 | }) 177 | 178 | it('should trigger the transition to "Any" state', function() { 179 | expect(this.machine.A_any.calledOnce).to.be.ok 180 | }) 181 | 182 | it('should trigger the transition from "Any" state', function() { 183 | expect(this.machine.any_B.calledOnce).to.be.ok 184 | expect(this.machine.any_C.calledOnce).to.be.ok 185 | }) 186 | 187 | it("should trigger the states' handlers", function() { 188 | expect(this.machine.B_state.calledOnce).to.be.ok 189 | expect(this.machine.C_state.calledOnce).to.be.ok 190 | }) 191 | 192 | it('should set the correct state', function() { 193 | expect(this.machine.is()).to.eql(['B', 'C']) 194 | }) 195 | 196 | it('should remain the correct order', function() { 197 | let order = [ 198 | this.machine.A_exit, 199 | this.machine.A_B, 200 | this.machine.A_C, 201 | this.machine.A_any, 202 | this.machine.any_B, 203 | this.machine.B_enter, 204 | this.machine.any_C, 205 | this.machine.C_enter, 206 | this.machine.B_state, 207 | this.machine.C_state 208 | ] 209 | assert_order(order) 210 | }) 211 | }) 212 | 213 | describe('when multi to single state transition', function() { 214 | beforeEach(function() { 215 | this.machine = new FooMachine(['A', 'B']) 216 | // mock 217 | mock_states(this.machine, ['A', 'B', 'C']) 218 | // exec 219 | this.machine.set(['C']) 220 | }) 221 | 222 | it('should trigger the state to state transitions', function() { 223 | expect(this.machine.B_C.calledOnce).to.be.ok 224 | expect(this.machine.A_C.calledOnce).to.be.ok 225 | }) 226 | 227 | it('should trigger the state exit transition', function() { 228 | expect(this.machine.A_exit.calledOnce).to.be.ok 229 | expect(this.machine.B_exit.calledOnce).to.be.ok 230 | }) 231 | 232 | it('should trigger the transition to the new state', function() { 233 | expect(this.machine.C_enter.calledOnce).to.be.ok 234 | }) 235 | 236 | it('should trigger the transition to "Any" state', function() { 237 | expect(this.machine.A_any.calledOnce).to.be.ok 238 | expect(this.machine.B_any.calledOnce).to.be.ok 239 | }) 240 | 241 | it('should trigger the transition from "Any" state', function() { 242 | expect(this.machine.any_C.calledOnce).to.be.ok 243 | }) 244 | 245 | it('should set the correct state', function() { 246 | expect(this.machine.is()).to.eql(['C']) 247 | }) 248 | 249 | it('should remain the correct transition order', function() { 250 | let order = [ 251 | this.machine.A_exit, 252 | this.machine.A_C, 253 | this.machine.A_any, 254 | this.machine.B_exit, 255 | this.machine.B_C, 256 | this.machine.B_any, 257 | this.machine.any_C, 258 | this.machine.C_enter 259 | ] 260 | assert_order(order) 261 | }) 262 | }) 263 | 264 | describe('when multi to multi state transition', function() { 265 | beforeEach(function() { 266 | this.machine = new FooMachine(['A', 'B']) 267 | // mock 268 | mock_states(this.machine, ['A', 'B', 'C', 'D']) 269 | // exec 270 | this.machine.set(['D', 'C']) 271 | }) 272 | 273 | it('should trigger the state to state transitions', function() { 274 | expect(this.machine.A_C.calledOnce).to.be.ok 275 | expect(this.machine.A_D.calledOnce).to.be.ok 276 | expect(this.machine.B_C.calledOnce).to.be.ok 277 | expect(this.machine.B_D.calledOnce).to.be.ok 278 | }) 279 | 280 | it('should trigger the state exit transition', function() { 281 | expect(this.machine.A_exit.calledOnce).to.be.ok 282 | expect(this.machine.B_exit.calledOnce).to.be.ok 283 | }) 284 | 285 | it('should trigger the transition to the new state', function() { 286 | expect(this.machine.C_enter.calledOnce).to.be.ok 287 | expect(this.machine.D_enter.calledOnce).to.be.ok 288 | }) 289 | 290 | it('should trigger the transition to "Any" state', function() { 291 | expect(this.machine.A_any.calledOnce).to.be.ok 292 | expect(this.machine.B_any.calledOnce).to.be.ok 293 | }) 294 | 295 | it('should trigger the transition from "Any" state', function() { 296 | expect(this.machine.any_C.calledOnce).to.be.ok 297 | expect(this.machine.any_D.calledOnce).to.be.ok 298 | }) 299 | 300 | it('should set the correct state', function() { 301 | expect(this.machine.is()).to.eql(['D', 'C']) 302 | }) 303 | 304 | it('should remain the correct transition order', function() { 305 | let order = [ 306 | this.machine.A_exit, 307 | this.machine.A_D, 308 | this.machine.A_C, 309 | this.machine.A_any, 310 | this.machine.B_exit, 311 | this.machine.B_D, 312 | this.machine.B_C, 313 | this.machine.B_any, 314 | this.machine.any_D, 315 | this.machine.D_enter, 316 | this.machine.any_C, 317 | this.machine.C_enter 318 | ] 319 | assert_order(order) 320 | }) 321 | }) 322 | 323 | describe('when transitioning to an active state', function() { 324 | beforeEach(function() { 325 | this.machine = new FooMachine(['A', 'B']) 326 | // mock 327 | mock_states(this.machine, ['A', 'B', 'C', 'D']) 328 | // exec 329 | this.machine.set(['A']) 330 | }) 331 | 332 | it("shouldn't trigger transition methods", function() { 333 | expect(this.machine.A_exit.called).not.to.be.ok 334 | expect(this.machine.A_any.called).not.to.be.ok 335 | expect(this.machine.any_A.called).not.to.be.ok 336 | }) 337 | 338 | it('should remain in the requested state', function() { 339 | expect(this.machine.is()).to.eql(['A']) 340 | }) 341 | }) 342 | 343 | describe('when order is defined by the depends attr', function() { 344 | beforeEach(function() { 345 | this.machine = new FooMachine(['A', 'B']) 346 | // mock 347 | mock_states(this.machine, ['A', 'B', 'C', 'D']) 348 | this.machine.C.after = ['D'] 349 | this.machine.A.after = ['B'] 350 | // exec 351 | this.machine.set(['C', 'D']) 352 | }) 353 | after(function() { 354 | delete this.machine.C.depends 355 | delete this.machine.A.depends 356 | }) 357 | 358 | describe('when entering', function() { 359 | it('should handle dependand states first', function() { 360 | let order = [ 361 | this.machine.A_D, 362 | this.machine.A_C, 363 | this.machine.any_D, 364 | this.machine.D_enter, 365 | this.machine.any_C, 366 | this.machine.C_enter 367 | ] 368 | assert_order(order) 369 | }) 370 | }) 371 | 372 | describe('when exiting', function() { 373 | it('should handle dependand states last', function() { 374 | let order = [ 375 | this.machine.B_exit, 376 | this.machine.B_D, 377 | this.machine.B_C, 378 | this.machine.B_any, 379 | this.machine.A_exit, 380 | this.machine.A_D, 381 | this.machine.A_C, 382 | this.machine.A_any 383 | ] 384 | assert_order(order) 385 | }) 386 | }) 387 | }) 388 | 389 | describe('when one state blocks another', function() { 390 | beforeEach(function() { 391 | this.log = [] 392 | this.machine = new FooMachine(['A', 'B']) 393 | this.machine.id('').logLevel(3) 394 | this.machine.def_log_handler_off = true 395 | this.machine.log_handlers.push(this.log.push.bind(this.log)) 396 | // mock 397 | mock_states(this.machine, ['A', 'B', 'C', 'D']) 398 | this.machine.C = { drop: ['D'] } 399 | this.machine.set('D') 400 | }) 401 | 402 | describe('and they are set simultaneously', function() { 403 | beforeEach(function() { 404 | this.ret = this.machine.set(['C', 'D']) 405 | }) 406 | 407 | it('should cancel the transition', function() { 408 | expect(this.machine.is()).to.eql(['D']) 409 | }) 410 | 411 | it('should return false', function() { 412 | expect(this.ret).to.eql(false) 413 | }) 414 | 415 | it('should explain the reason in the log', function() { 416 | expect(this.log).to.contain('[rel:drop] D by C') 417 | }) 418 | 419 | afterEach(function() { 420 | delete this.ret 421 | }) 422 | }) 423 | 424 | describe('and blocking one is added', function() { 425 | it('should unset the blocked one', function() { 426 | this.machine.add(['C']) 427 | expect(this.machine.is()).to.eql(['C']) 428 | }) 429 | }) 430 | 431 | describe('and cross blocking one is added', function() { 432 | beforeEach(function() { 433 | this.machine.D = { drop: ['C'] } 434 | }) 435 | after(function() { 436 | this.machine.D = {} 437 | }) 438 | 439 | describe('using set', function() { 440 | it('should unset the old one', function() { 441 | this.machine.set('C') 442 | expect(this.machine.is()).to.eql(['C']) 443 | }) 444 | 445 | it('should work in both ways', function() { 446 | this.machine.set('C') 447 | expect(this.machine.is()).to.eql(['C']) 448 | this.machine.set('D') 449 | expect(this.machine.is()).to.eql(['D']) 450 | }) 451 | }) 452 | 453 | describe('using add', function() { 454 | it('should unset the old one', function() { 455 | this.machine.add('C') 456 | expect(this.machine.is()).to.eql(['C']) 457 | }) 458 | 459 | it('should work in both ways', function() { 460 | this.machine.add('C') 461 | expect(this.machine.is()).to.eql(['C']) 462 | this.machine.add('D') 463 | expect(this.machine.is()).to.eql(['D']) 464 | }) 465 | }) 466 | }) 467 | }) 468 | 469 | describe('when state is implied', function() { 470 | beforeEach(function() { 471 | this.machine = new FooMachine(['A']) 472 | // mock 473 | mock_states(this.machine, ['A', 'B', 'C', 'D']) 474 | this.machine.C = { add: ['D'] } 475 | this.machine.A = { drop: ['D'] } 476 | // exec 477 | this.machine.set(['C']) 478 | }) 479 | 480 | it('should be activated', function() { 481 | expect(this.machine.is()).to.eql(['C', 'D']) 482 | }) 483 | 484 | it('should be skipped if blocked at the same time', function() { 485 | this.machine.set(['A', 'C']) 486 | expect(this.machine.is()).to.eql(['A', 'C']) 487 | }) 488 | }) 489 | //expect( fn ).to.throw 490 | 491 | describe('when state requires another one', function() { 492 | beforeEach(function() { 493 | this.machine = new FooMachine(['A']) 494 | // mock 495 | mock_states(this.machine, ['A', 'B', 'C', 'D']) 496 | this.machine.C = { require: ['D'] } 497 | }) 498 | after(function() { 499 | this.machine.C = {} 500 | }) 501 | 502 | it('should be set when required state is active', function() { 503 | this.machine.set(['C', 'D']) 504 | expect(this.machine.is()).to.eql(['C', 'D']) 505 | }) 506 | 507 | describe("when required state isn't active", function() { 508 | beforeEach(function() { 509 | this.log = [] 510 | this.machine.id('').logLevel(3) 511 | this.machine.def_log_handler_off = true 512 | this.machine.log_handlers.push(this.log.push.bind(this.log)) 513 | this.machine.set(['C', 'A']) 514 | }) 515 | 516 | afterEach(function() { 517 | delete this.log 518 | }) 519 | 520 | it("should't be set", function() { 521 | expect(this.machine.is()).to.eql(['A']) 522 | }) 523 | 524 | it('should explain the reason in the log', function() { 525 | let msg = '[rejected] C(-D)' 526 | expect(this.log).to.contain(msg) 527 | }) 528 | }) 529 | }) 530 | 531 | describe('when state is changed', function() { 532 | beforeEach(function() { 533 | this.machine = new FooMachine('A') 534 | // mock 535 | mock_states(this.machine, ['A', 'B', 'C', 'D']) 536 | }) 537 | 538 | describe('during another state change', function() { 539 | it('should be scheduled synchronously', function() { 540 | this.machine.B_enter = function(states) { 541 | this.add('C') 542 | } 543 | this.machine.C_enter = sinon.spy() 544 | this.machine.A_exit = sinon.spy() 545 | this.machine.set('B') 546 | expect(this.machine.C_enter.calledOnce).to.be.ok 547 | expect(this.machine.A_exit.calledOnce).to.be.ok 548 | expect(this.machine.is()).to.eql(['C', 'B']) 549 | }) 550 | 551 | it('should be checkable') 552 | }) 553 | // TODO use #duringTransition 554 | 555 | describe('and transition is canceled', function() { 556 | beforeEach(function() { 557 | this.machine.D_enter = () => false 558 | this.log = [] 559 | this.machine.id('').logLevel(3) 560 | this.machine.def_log_handler_off = true 561 | this.machine.log_handlers.push(this.log.push.bind(this.log)) 562 | }) 563 | 564 | describe('when activating a new state', function() { 565 | beforeEach(function() { 566 | this.ret = this.machine.set('D') 567 | }) 568 | 569 | it('should return false', function() { 570 | expect(this.machine.set('D')).not.to.be.ok 571 | }) 572 | 573 | it('should not change the previous state', function() { 574 | expect(this.machine.is()).to.eql(['A']) 575 | }) 576 | 577 | it('should explain the reason in the log', function() { 578 | expect(this.log).to.contain('[cancelled] D by the method D_enter') 579 | }) 580 | 581 | it('should not change the auto states') 582 | }) 583 | 584 | // TODO make this and the previous a main contexts 585 | describe('when adding an additional state', function() { 586 | beforeEach(function() { 587 | this.ret = this.machine.add('D') 588 | }) 589 | 590 | it('should return false', function() { 591 | expect(this.ret).not.to.be.ok 592 | }) 593 | 594 | it('should not change the previous state', function() { 595 | expect(this.machine.is()).to.eql(['A']) 596 | }) 597 | 598 | it('should not change the auto states') 599 | }) 600 | 601 | describe('when droping a current state', function() { 602 | it('should return false') 603 | 604 | it('should not change the previous state') 605 | 606 | it('should explain the reason in the log') 607 | 608 | it('should not change the auto states') 609 | }) 610 | }) 611 | 612 | describe('and transition is successful', function() { 613 | it('should return true', function() { 614 | expect(this.machine.set('D')).to.be.ok 615 | }) 616 | 617 | it('should set the auto states') 618 | }) 619 | 620 | it('should provide previous state information', function(done) { 621 | this.machine.D_enter = function() { 622 | expect(this.is()).to.eql(['A']) 623 | done() 624 | } 625 | this.machine.set('D') 626 | }) 627 | 628 | it('should provide target state information', function(done) { 629 | this.machine.D_enter = function() { 630 | expect(this.to()).to.eql(['D']) 631 | done() 632 | } 633 | this.machine.set('D') 634 | }) 635 | 636 | describe('with arguments', function() { 637 | beforeEach(function() { 638 | this.machine.D = { 639 | add: ['B'], 640 | drop: ['A'] 641 | } 642 | }) 643 | after(function() { 644 | this.machine.D = {} 645 | }) 646 | 647 | describe('and synchronous', function() { 648 | beforeEach(function() { 649 | this.machine.set(['A', 'C']) 650 | this.machine.set('D', 'foo', 2) 651 | this.machine.set('D', 'foo', 2) 652 | this.machine.drop('D', 'foo', 2) 653 | }) 654 | 655 | describe('and is explicit', function() { 656 | it('should forward arguments to exit methods', function() { 657 | expect(this.machine.D_exit.calledWith('foo', 2)).to.be.ok 658 | }) 659 | 660 | it('should forward arguments to enter methods', function() { 661 | expect(this.machine.D_enter.calledWith('foo', 2)).to.be.ok 662 | }) 663 | 664 | it('should forward arguments to self transition methods', function() { 665 | expect(this.machine.D_D.calledWith('foo', 2)).to.be.ok 666 | }) 667 | 668 | it('should forward arguments to transition methods', function() { 669 | expect(this.machine.C_D.calledWith('foo', 2)).to.be.ok 670 | }) 671 | }) 672 | 673 | describe('and is non-explicit', function() { 674 | it('should not forward arguments to exit methods', function() { 675 | expect(this.machine.A_exit.calledWith()).to.be.ok 676 | }) 677 | 678 | it('should not forward arguments to enter methods', function() { 679 | expect(this.machine.B_enter.calledWith()).to.be.ok 680 | }) 681 | 682 | it('should not forward arguments to transition methods', function() { 683 | expect(this.machine.A_B.calledWith()).to.be.ok 684 | }) 685 | }) 686 | }) 687 | 688 | describe('and delayed', function() { 689 | beforeEach(function() { 690 | this.machine.setByListener(['A', 'C'], 'foo')() 691 | this.machine.setByListener('D', 'foo', 2)() 692 | this.machine.setByListener('D', 'foo', 2)() 693 | this.machine.dropByListener('D', 'foo', 2)() 694 | }) 695 | 696 | describe('and is explicit', function() { 697 | it('should forward arguments to exit methods', function() { 698 | expect(this.machine.D_exit.calledWith('foo', 2)).to.be.ok 699 | }) 700 | 701 | it('should forward arguments to enter methods', function() { 702 | expect(this.machine.D_enter.calledWith('foo', 2)).to.be.ok 703 | }) 704 | 705 | it('should forward arguments to self transition methods', function() { 706 | expect(this.machine.D_D.calledWith('foo', 2)).to.be.ok 707 | }) 708 | 709 | it('should forward arguments to transition methods', function() { 710 | expect(this.machine.C_D.calledWith('foo', 2)).to.be.ok 711 | }) 712 | }) 713 | 714 | describe('and is non-explicit', function() { 715 | it('should not forward arguments to exit methods', function() { 716 | expect(this.machine.A_exit.calledWith()).to.be.ok 717 | }) 718 | 719 | it('should not forward arguments to enter methods', function() { 720 | expect(this.machine.B_enter.calledWith()).to.be.ok 721 | }) 722 | 723 | it('should not forward arguments to transition methods', function() { 724 | expect(this.machine.A_B.calledWith()).to.be.ok 725 | }) 726 | }) 727 | }) 728 | }) 729 | 730 | describe('and delayed', function() { 731 | beforeEach(function(done) { 732 | this.callback = this.machine.setByCallback('D') 733 | this.promise = this.machine.last_promise 734 | // TODO without some action after beforeEach, we'd hit a timeout 735 | done() 736 | }) 737 | 738 | afterEach(function() { 739 | delete this.promise 740 | }) 741 | 742 | it('should return a promise', function() { 743 | expect(this.promise instanceof Promise).to.be.ok 744 | }) 745 | 746 | it('should execute the change', function(done) { 747 | // call without an error 748 | this.callback(null) 749 | this.promise.then(() => { 750 | expect(this.machine.any_D.calledOnce).to.be.ok 751 | expect(this.machine.D_enter.calledOnce).to.be.ok 752 | done() 753 | }) 754 | }) 755 | 756 | it('should expose a ref to the last promise', function() { 757 | expect(this.machine.last_promise).to.equal(this.promise) 758 | }) 759 | 760 | it('should be called with params passed to the delayed function', function(done) { 761 | this.machine.D_enter = function(...params) { 762 | expect(this.to()).to.eql(['D']) 763 | expect(params).to.be.eql(['foo', 2]) 764 | done() 765 | } 766 | this.callback(null, 'foo', 2) 767 | }) 768 | 769 | describe('and then cancelled', function() { 770 | it('should not execute the change', function(done) { 771 | this.machine.Exception_state = function() {} 772 | this.promise.catch(() => { 773 | expect(this.machine.any_D).not.have.been.called 774 | expect(this.machine.D_enter).not.have.been.called 775 | done() 776 | }) 777 | this.callback(new Error()) 778 | }) 779 | }) 780 | }) 781 | 782 | describe('and active state is also the target one', function() { 783 | it('should trigger self transition at the very beginning', function() { 784 | this.machine.set(['A', 'B']) 785 | let order = [this.machine.A_A, this.machine.any_B, this.machine.B_enter] 786 | assert_order(order) 787 | }) 788 | 789 | it('should be executed only for explicitly called states', function() { 790 | this.machine.add('B') 791 | this.machine.add('A') 792 | expect(this.machine.A_A.calledOnce).to.be.ok 793 | expect(this.machine.B_B.callCount).to.eql(0) 794 | }) 795 | 796 | it('should be cancellable', function() { 797 | this.machine.A_A = sinon.stub().returns(false) 798 | this.machine.set(['A', 'B']) 799 | expect(this.machine.A_A.calledOnce).to.be.ok 800 | expect(this.machine.any_B.called).not.to.be.ok 801 | }) 802 | 803 | after(function() { 804 | delete this.machine.A_A 805 | }) 806 | }) 807 | 808 | // TODO move to events 809 | describe('should trigger events', function() { 810 | beforeEach(function() { 811 | this.A_A = sinon.spy() 812 | this.B_enter = sinon.spy() 813 | this.C_exit = sinon.spy() 814 | this.change = sinon.spy() 815 | this.cancelTransition = sinon.spy() 816 | 817 | this.machine = new FooMachine('A') 818 | // mock 819 | mock_states(this.machine, ['A', 'B', 'C', 'D']) 820 | this.machine.set(['A', 'C']) 821 | this.machine.on('A_A', this.A_A) 822 | this.machine.on('B_enter', this.B_enter) 823 | this.machine.on('C_exit', this.C_exit) 824 | this.machine.on('D_exit', () => false) 825 | // emitter event 826 | this.machine.on('tick', this.change) 827 | this.machine.on('transition-cancelled', this.cancelTransition) 828 | this.machine.set(['A', 'B']) 829 | this.machine.add(['C']) 830 | this.machine.add(['D']) 831 | }) 832 | 833 | afterEach(function() { 834 | delete this.C_exit 835 | delete this.A_A 836 | delete this.B_enter 837 | delete this.add 838 | delete this.set 839 | delete this.cancelTransition 840 | }) 841 | 842 | it('for self transitions', function() { 843 | expect(this.A_A.called).to.be.ok 844 | }) 845 | 846 | it('for enter transitions', function() { 847 | expect(this.B_enter.called).to.be.ok 848 | }) 849 | 850 | it('for exit transitions', function() { 851 | expect(this.C_exit.called).to.be.ok 852 | }) 853 | 854 | it('which can cancel the transition', function() { 855 | expect(this.machine.D_any.called).not.to.be.ok 856 | }) 857 | 858 | it('for changing states', function() { 859 | expect(this.change.called).to.be.ok 860 | }) 861 | 862 | it('for cancelling the transition', function() { 863 | this.machine.drop('D') 864 | expect(this.cancelTransition.called).to.be.ok 865 | }) 866 | }) 867 | }) 868 | 869 | describe('Events', function() { 870 | beforeEach(function() { 871 | this.machine = new EventMachine('A') 872 | }) 873 | 874 | describe('should support states', function() { 875 | it('by triggering the *_state bindings immediately', function() { 876 | let l = [] 877 | // init spies 878 | let iterable = [0, 1, 2] 879 | for (let j = 0; j < iterable.length; j++) { 880 | var i = iterable[j] 881 | l[i] = sinon.stub() 882 | } 883 | var i = 0 884 | this.machine.set('B') 885 | this.machine.on('A_state', l[i++]) 886 | this.machine.on('B_state', l[i++]) 887 | i = 0 888 | expect(l[i++].called).not.to.be.ok 889 | expect(l[i++].calledOnce).to.be.ok 890 | }) 891 | 892 | it("shouldn't duplicate events") 893 | }) 894 | 895 | describe('clock', function() { 896 | beforeEach(function() { 897 | this.machine = new FooMachine() 898 | }) 899 | 900 | it('should tick when activating a new state', function() { 901 | this.machine.set('A') 902 | expect(this.machine.clock('A')).to.be.eql(1) 903 | }) 904 | 905 | it('should tick when activating many new states', function() { 906 | this.machine.set(['A', 'B']) 907 | expect(this.machine.clock('A')).to.be.eql(1) 908 | expect(this.machine.clock('B')).to.be.eql(1) 909 | }) 910 | 911 | it("shouldn't tick when activating an already active state", function() { 912 | this.machine.set('A') 913 | this.machine.set('A') 914 | expect(this.machine.clock('A')).to.be.eql(1) 915 | }) 916 | 917 | it('should tick for Multi states when activating an already active state', function() { 918 | this.machine.A.multi = true 919 | // A is already active 920 | expect(this.machine.clock('A')).to.be.eql(1) 921 | this.machine.set('A') 922 | this.machine.set('A') 923 | // the clock should be incremented by +2 924 | expect(this.machine.clock('A')).to.be.eql(3) 925 | }) 926 | }) 927 | 928 | describe('proto child', function() { 929 | beforeEach(function() { 930 | this.machine = new FooMachine() 931 | this.child = this.machine.createChild() 932 | }) 933 | 934 | after(function() { 935 | delete this.child 936 | }) 937 | 938 | it('should inherit all the instance properties', function() { 939 | expect(this.machine.A).to.equal(this.child.A) 940 | }) 941 | 942 | it('should have own active states and the clock', function() { 943 | this.child.add('B') 944 | expect(this.machine.is()).to.not.eql(this.child.is()) 945 | }) 946 | }) 947 | }) 948 | 949 | describe('queue', function() {}) 950 | describe('nested queue', function() {}) 951 | 952 | describe('bugs', function() { 953 | // TODO use a constructor in Sub 954 | it('should trigger the enter state of a subclass', function() { 955 | let a_enter_spy = sinon.spy() 956 | let b_enter_spy = sinon.spy() 957 | let sub = new Sub('A', a_enter_spy, b_enter_spy) 958 | sub.set('B') 959 | expect(a_enter_spy.called).to.be.ok 960 | expect(b_enter_spy.called).to.be.ok 961 | }) 962 | 963 | it('should drop states cross-blocked by implied states', function() { 964 | const state = { 965 | A: { drop: ['B'] }, 966 | B: { drop: ['A'] }, 967 | Z: { add: ['B'] } 968 | } 969 | const example = machine(state) 970 | example.add('Z') 971 | expect(example.is()).to.eql(['Z', 'B']) 972 | }) 973 | 974 | it('implied block by one about to be dropped should be set', function() { 975 | const state = { 976 | Wet: { require: ['Water'] }, 977 | Dry: { drop: ['Wet'] }, 978 | Water: { add: ['Wet'], drop: ['Dry'] } 979 | } 980 | const example = machine(state) 981 | example.add('Dry') 982 | example.add('Water') 983 | expect(example.is()).to.eql(['Wet', 'Water']) 984 | }) 985 | 986 | it('should pass args to transition methods') 987 | 988 | it('should drop states blocked by a new one if the one blocks it', function() { 989 | let sub = new CrossBlocked() 990 | expect(sub.is()).to.eql(['B']) 991 | }) 992 | }) 993 | 994 | describe('Promises', function() { 995 | it('can be resolved') 996 | it('can be rejected') 997 | it('can be chainable') 998 | describe('delayed methods', function() { 999 | it('should return correctly bound resolve method') 1000 | }) 1001 | }) 1002 | }) 1003 | -------------------------------------------------------------------------------- /test/transition-steps.ts: -------------------------------------------------------------------------------- 1 | import AsyncMachine, { 2 | machine, 3 | PipeFlags, 4 | Transition 5 | } from '../src/asyncmachine' 6 | import { TransitionStepTypes, TransitionStepFields } from '../src/types' 7 | import * as chai from 'chai' 8 | import * as sinon from 'sinon' 9 | import * as sinonChai from 'sinon-chai' 10 | 11 | let { expect } = chai 12 | chai.use(sinonChai) 13 | 14 | describe('Transition steps', function() { 15 | it('for relations', function() { 16 | let states = machine({ 17 | A: { require: ['B'] }, 18 | B: { require: ['C'] }, 19 | C: { after: ['A'] }, 20 | D: { 21 | add: ['B', 'C'], 22 | drop: ['E'] 23 | }, 24 | E: {} 25 | }) 26 | states.id('') 27 | states.set('E') 28 | let transition: Transition 29 | states.on('transition-end', (t: Transition) => { 30 | transition = t 31 | }) 32 | states.add(['A', 'D']) 33 | expect(states.is()).to.eql(['A', 'D', 'B', 'C']) 34 | expect(transition.steps.length).to.be.gt(0) 35 | let types = TransitionStepTypes 36 | let steps = [ 37 | [['', 'A'], undefined, types.REQUESTED, undefined], 38 | [['', 'D'], undefined, types.REQUESTED, undefined], 39 | [['', 'A'], undefined, types.SET, undefined], 40 | [['', 'D'], undefined, types.SET, undefined], 41 | [['', 'B'], ['', 'D'], types.RELATION, 'add'], 42 | [['', 'C'], ['', 'D'], types.RELATION, 'add'], 43 | [['', 'B'], undefined, types.SET, undefined], 44 | [['', 'C'], undefined, types.SET, undefined], 45 | [['', 'E'], ['', 'D'], types.RELATION, 'drop'], 46 | [['', 'E'], undefined, types.DROP, undefined] 47 | ] 48 | expect(transition.steps).to.eql(steps) 49 | }) 50 | 51 | it('for transitions', function() { 52 | let states = machine<'A' | 'B' | 'C' | 'D'>(['A', 'B', 'C', 'D']) 53 | let f = function() {} 54 | let target = { 55 | Any_C: f, 56 | B_C: f, 57 | B_exit: f, 58 | C_enter: f, 59 | A_A: f 60 | } 61 | states.id('').setTarget(target) 62 | states.set(['A', 'B']) 63 | let transition: Transition 64 | states.on('transition-end', (t: Transition) => { 65 | transition = t 66 | }) 67 | states.set(['A', 'C']) 68 | expect(states.is()).to.eql(['A', 'C']) 69 | expect(transition.steps.length).to.be.gt(0) 70 | let types = TransitionStepTypes 71 | let steps = [ 72 | [['', 'A'], undefined, types.REQUESTED, undefined], 73 | [['', 'C'], undefined, types.REQUESTED, undefined], 74 | [['', 'C'], undefined, types.SET, undefined], 75 | [['', 'B'], undefined, types.DROP, undefined], 76 | [['', 'A'], ['', 'A'], types.TRANSITION, 'A_A'], 77 | [['', 'B'], undefined, types.TRANSITION, 'B_exit'], 78 | [['', 'B'], ['', 'C'], types.TRANSITION, 'B_C'], 79 | [['', 'C'], undefined, types.TRANSITION, 'C_enter'] 80 | ] 81 | expect(transition.steps).to.eql(steps) 82 | }) 83 | 84 | // describe('events') 85 | }) 86 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": false, 6 | "noImplicitAny": false, 7 | "removeComments": false, 8 | "lib": ["es7", "dom"], 9 | "sourceMap": true 10 | }, 11 | "filesGlob": ["**/*.ts", "../typings/index.d.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TODO move all of these to factory calls. 3 | */ 4 | import AsyncMachine, { machine } from '../src/asyncmachine' 5 | import { IBind, IEmit } from '../src/types' 6 | import { IState } from '../src/types' 7 | import * as sinon from 'sinon' 8 | import * as chai from 'chai' 9 | import * as sinonChai from 'sinon-chai' 10 | 11 | let { expect } = chai 12 | chai.use(sinonChai) 13 | 14 | type AB = 'A' | 'B' 15 | type ABC = 'A' | 'B' | 'C' 16 | type ABCD = 'A' | 'B' | 'C' | 'D' 17 | 18 | type FooMachineState = IState 19 | interface IFooMachineState extends IState {} 20 | 21 | class FooMachineExt extends AsyncMachine< 22 | States | ABCD, 23 | IBind, 24 | IEmit 25 | > { 26 | A: FooMachineState = {} 27 | B: FooMachineState = {} 28 | C: FooMachineState = {} 29 | D: FooMachineState = {} 30 | 31 | constructor(initialState?) { 32 | super() 33 | 34 | this.register('A', 'B', 'C', 'D') 35 | if (initialState) { 36 | this.set(initialState) 37 | } 38 | 39 | this.add('A') 40 | } 41 | } 42 | 43 | class FooMachine extends FooMachineExt {} 44 | 45 | class SubClassRegisterAll extends AsyncMachine<'A', IBind, IEmit> { 46 | A: IState<'A'> = {} 47 | 48 | constructor() { 49 | super() 50 | this.registerAll() 51 | } 52 | } 53 | 54 | class EventMachine extends FooMachineExt<'TestNamespace'> { 55 | TestNamespace: IFooMachineState<'TestNamespace'> = {} 56 | 57 | constructor(initial?, config?) { 58 | super() 59 | this.register('TestNamespace') 60 | if (initial) { 61 | this.set(initial) 62 | } 63 | } 64 | } 65 | 66 | class Sub extends AsyncMachine { 67 | A: IState = {} 68 | B: IState = {} 69 | 70 | constructor(initial?, a_spy?, b_spy?) { 71 | super() 72 | 73 | this.register('A', 'B') 74 | this.A_enter = a_spy 75 | this.B_enter = b_spy 76 | if (initial) { 77 | this.set(initial) 78 | } 79 | } 80 | 81 | A_enter() {} 82 | 83 | B_enter() {} 84 | } 85 | 86 | class CrossBlocked extends AsyncMachine { 87 | A: IState = { 88 | drop: ['B'] 89 | } 90 | B: IState = { 91 | drop: ['A'] 92 | } 93 | 94 | constructor() { 95 | super() 96 | 97 | this.register('A', 'B') 98 | this.set('A') 99 | this.set('B') 100 | } 101 | } 102 | 103 | function mock_states(instance, states) { 104 | for (let state of states) { 105 | // deeply clone all the state's attrs 106 | // proto = instance["#{state}"] 107 | // instance["#{state}"] = {} 108 | instance[`${state}_${state}`] = sinon.spy(instance[`${state}_${state}`]) 109 | instance[`${state}_enter`] = sinon.spy(instance[`${state}_enter`]) 110 | instance[`${state}_exit`] = sinon.spy(instance[`${state}_exit`]) 111 | instance[`${state}_state`] = sinon.spy(instance[`${state}_state`]) 112 | instance[`${state}_any`] = sinon.spy(instance[`${state}_any`]) 113 | // TODO any -> Any 114 | instance[`any_${state}`] = sinon.spy(instance[`any_${state}`]) 115 | for (let inner of states) 116 | instance[`${inner}_${state}`] = sinon.spy(instance[`${inner}_${state}`]) 117 | } 118 | } 119 | 120 | function assert_order(order) { 121 | let m = null 122 | let k = null 123 | let iterable = order.slice(0, -1) 124 | for (k = 0; k < iterable.length; k++) { 125 | m = iterable[k] 126 | order[k] = m.calledBefore(order[k + 1]) 127 | } 128 | for (let check of order.slice(0, -1)) { 129 | expect(check).to.be.ok 130 | } 131 | } 132 | 133 | export { 134 | SubClassRegisterAll, 135 | FooMachine, 136 | EventMachine, 137 | Sub, 138 | CrossBlocked, 139 | mock_states, 140 | assert_order 141 | } 142 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "removeComments": false, 7 | "lib": ["es2017", "dom"], 8 | "sourceMap": true, 9 | "strict": true, 10 | "suppressImplicitAnyIndexErrors": true, 11 | "preserveConstEnums": true, 12 | "outDir": "build" 13 | }, 14 | "include": [ 15 | "src/**/*" 16 | ], 17 | "jsdoc": "docs" 18 | } -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalDependencies": { 3 | "mocha": "registry:dt/mocha#2.2.5+20160720003353" 4 | }, 5 | "dependencies": { 6 | "chai": "registry:npm/chai#3.5.0+20160723033700", 7 | "sinon": "registry:npm/sinon#1.16.0+20160723033700", 8 | "sinon-chai": "registry:npm/sinon-chai#2.8.0+20160310030142", 9 | "source-map-support": "registry:npm/source-map-support#0.3.0+20160723033700" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /wallaby.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (wallaby) { 2 | return { 3 | files: [ 4 | 'src/**/*.ts', 5 | 'test/utils.ts' 6 | ], 7 | tests: [ 8 | 'test/**/*.ts', 9 | '!test/utils.ts' 10 | ], 11 | env: { 12 | type: 'node', 13 | runner: 'node' 14 | }, 15 | testFramework: 'mocha', 16 | 17 | compilers: { 18 | '**/*.ts': wallaby.compilers.typeScript({ 19 | outDir: null, 20 | module: 'commonjs', 21 | target: 'es5' 22 | }) 23 | } 24 | }; 25 | }; --------------------------------------------------------------------------------