├── .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 | [](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 | [](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 | [](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 | [](http://travis-ci.org/tj/commander.js)
5 | [](https://www.npmjs.org/package/commander)
6 | [](https://npmcharts.com/compare/commander?minimal=true)
7 | [](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 | };
--------------------------------------------------------------------------------