state-machine.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. /*
  2. Javascript State Machine Library - https://github.com/jakesgordon/javascript-state-machine
  3. Copyright (c) 2012, 2013, 2014, 2015, Jake Gordon and contributors
  4. Released under the MIT license - https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE
  5. */
  6. (function () {
  7. var StateMachine = {
  8. //---------------------------------------------------------------------------
  9. VERSION: "2.4.0",
  10. //---------------------------------------------------------------------------
  11. Result: {
  12. SUCCEEDED: 1, // the event transitioned successfully from one state to another
  13. NOTRANSITION: 2, // the event was successfull but no state transition was necessary
  14. CANCELLED: 3, // the event was cancelled by the caller in a beforeEvent callback
  15. PENDING: 4 // the event is asynchronous and the caller is in control of when the transition occurs
  16. },
  17. Error: {
  18. INVALID_TRANSITION: 100, // caller tried to fire an event that was innapropriate in the current state
  19. PENDING_TRANSITION: 200, // caller tried to fire an event while an async transition was still pending
  20. INVALID_CALLBACK: 300 // caller provided callback function threw an exception
  21. },
  22. WILDCARD: '*',
  23. ASYNC: 'async',
  24. //---------------------------------------------------------------------------
  25. create: function(cfg, target) {
  26. var initial = (typeof cfg.initial == 'string') ? { state: cfg.initial } : cfg.initial; // allow for a simple string, or an object with { state: 'foo', event: 'setup', defer: true|false }
  27. var terminal = cfg.terminal || cfg['final'];
  28. var fsm = target || cfg.target || {};
  29. var events = cfg.events || [];
  30. var callbacks = cfg.callbacks || {};
  31. var map = {}; // track state transitions allowed for an event { event: { from: [ to ] } }
  32. var transitions = {}; // track events allowed from a state { state: [ event ] }
  33. var add = function(e) {
  34. var from = Array.isArray(e.from) ? e.from : (e.from ? [e.from] : [StateMachine.WILDCARD]); // allow 'wildcard' transition if 'from' is not specified
  35. map[e.name] = map[e.name] || {};
  36. for (var n = 0 ; n < from.length ; n++) {
  37. transitions[from[n]] = transitions[from[n]] || [];
  38. transitions[from[n]].push(e.name);
  39. map[e.name][from[n]] = e.to || from[n]; // allow no-op transition if 'to' is not specified
  40. }
  41. if (e.to)
  42. transitions[e.to] = transitions[e.to] || [];
  43. };
  44. if (initial) {
  45. initial.event = initial.event || 'startup';
  46. add({ name: initial.event, from: 'none', to: initial.state });
  47. }
  48. for(var n = 0 ; n < events.length ; n++)
  49. add(events[n]);
  50. for(var name in map) {
  51. if (map.hasOwnProperty(name))
  52. fsm[name] = StateMachine.buildEvent(name, map[name]);
  53. }
  54. for(var name in callbacks) {
  55. if (callbacks.hasOwnProperty(name))
  56. fsm[name] = callbacks[name]
  57. }
  58. fsm.current = 'none';
  59. fsm.is = function(state) { return Array.isArray(state) ? (state.indexOf(this.current) >= 0) : (this.current === state); };
  60. fsm.can = function(event) { return !this.transition && (map[event] !== undefined) && (map[event].hasOwnProperty(this.current) || map[event].hasOwnProperty(StateMachine.WILDCARD)); }
  61. fsm.cannot = function(event) { return !this.can(event); };
  62. fsm.transitions = function() { return (transitions[this.current] || []).concat(transitions[StateMachine.WILDCARD] || []); };
  63. fsm.isFinished = function() { return this.is(terminal); };
  64. fsm.error = cfg.error || function(name, from, to, args, error, msg, e) { throw e || msg; }; // default behavior when something unexpected happens is to throw an exception, but caller can override this behavior if desired (see github issue #3 and #17)
  65. fsm.states = function() { return Object.keys(transitions).sort() };
  66. if (initial && !initial.defer)
  67. fsm[initial.event]();
  68. return fsm;
  69. },
  70. //===========================================================================
  71. doCallback: function(fsm, func, name, from, to, args) {
  72. if (func) {
  73. try {
  74. return func.apply(fsm, [name, from, to].concat(args));
  75. }
  76. catch(e) {
  77. return fsm.error(name, from, to, args, StateMachine.Error.INVALID_CALLBACK, "an exception occurred in a caller-provided callback function", e);
  78. }
  79. }
  80. },
  81. beforeAnyEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbeforeevent'], name, from, to, args); },
  82. afterAnyEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafterevent'] || fsm['onevent'], name, from, to, args); },
  83. leaveAnyState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleavestate'], name, from, to, args); },
  84. enterAnyState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenterstate'] || fsm['onstate'], name, from, to, args); },
  85. changeState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onchangestate'], name, from, to, args); },
  86. beforeThisEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbefore' + name], name, from, to, args); },
  87. afterThisEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafter' + name] || fsm['on' + name], name, from, to, args); },
  88. leaveThisState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleave' + from], name, from, to, args); },
  89. enterThisState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenter' + to] || fsm['on' + to], name, from, to, args); },
  90. beforeEvent: function(fsm, name, from, to, args) {
  91. if ((false === StateMachine.beforeThisEvent(fsm, name, from, to, args)) ||
  92. (false === StateMachine.beforeAnyEvent( fsm, name, from, to, args)))
  93. return false;
  94. },
  95. afterEvent: function(fsm, name, from, to, args) {
  96. StateMachine.afterThisEvent(fsm, name, from, to, args);
  97. StateMachine.afterAnyEvent( fsm, name, from, to, args);
  98. },
  99. leaveState: function(fsm, name, from, to, args) {
  100. var specific = StateMachine.leaveThisState(fsm, name, from, to, args),
  101. general = StateMachine.leaveAnyState( fsm, name, from, to, args);
  102. if ((false === specific) || (false === general))
  103. return false;
  104. else if ((StateMachine.ASYNC === specific) || (StateMachine.ASYNC === general))
  105. return StateMachine.ASYNC;
  106. },
  107. enterState: function(fsm, name, from, to, args) {
  108. StateMachine.enterThisState(fsm, name, from, to, args);
  109. StateMachine.enterAnyState( fsm, name, from, to, args);
  110. },
  111. //===========================================================================
  112. buildEvent: function(name, map) {
  113. return function() {
  114. var from = this.current;
  115. var to = map[from] || (map[StateMachine.WILDCARD] != StateMachine.WILDCARD ? map[StateMachine.WILDCARD] : from) || from;
  116. var args = Array.prototype.slice.call(arguments); // turn arguments into pure array
  117. if (this.transition)
  118. return this.error(name, from, to, args, StateMachine.Error.PENDING_TRANSITION, "event " + name + " inappropriate because previous transition did not complete");
  119. if (this.cannot(name))
  120. return this.error(name, from, to, args, StateMachine.Error.INVALID_TRANSITION, "event " + name + " inappropriate in current state " + this.current);
  121. if (false === StateMachine.beforeEvent(this, name, from, to, args))
  122. return StateMachine.Result.CANCELLED;
  123. if (from === to) {
  124. StateMachine.afterEvent(this, name, from, to, args);
  125. return StateMachine.Result.NOTRANSITION;
  126. }
  127. // prepare a transition method for use EITHER lower down, or by caller if they want an async transition (indicated by an ASYNC return value from leaveState)
  128. var fsm = this;
  129. this.transition = function() {
  130. fsm.transition = null; // this method should only ever be called once
  131. fsm.current = to;
  132. StateMachine.enterState( fsm, name, from, to, args);
  133. StateMachine.changeState(fsm, name, from, to, args);
  134. StateMachine.afterEvent( fsm, name, from, to, args);
  135. return StateMachine.Result.SUCCEEDED;
  136. };
  137. this.transition.cancel = function() { // provide a way for caller to cancel async transition if desired (issue #22)
  138. fsm.transition = null;
  139. StateMachine.afterEvent(fsm, name, from, to, args);
  140. }
  141. var leave = StateMachine.leaveState(this, name, from, to, args);
  142. if (false === leave) {
  143. this.transition = null;
  144. return StateMachine.Result.CANCELLED;
  145. }
  146. else if (StateMachine.ASYNC === leave) {
  147. return StateMachine.Result.PENDING;
  148. }
  149. else {
  150. if (this.transition) // need to check in case user manually called transition() but forgot to return StateMachine.ASYNC
  151. return this.transition();
  152. }
  153. };
  154. }
  155. }; // StateMachine
  156. //===========================================================================
  157. //======
  158. // NODE
  159. //======
  160. if (typeof exports !== 'undefined') {
  161. if (typeof module !== 'undefined' && module.exports) {
  162. exports = module.exports = StateMachine;
  163. }
  164. exports.StateMachine = StateMachine;
  165. }
  166. //============
  167. // AMD/REQUIRE
  168. //============
  169. else if (typeof define === 'function' && define.amd) {
  170. define(function(require) { return StateMachine; });
  171. }
  172. //========
  173. // BROWSER
  174. //========
  175. else if (typeof window !== 'undefined') {
  176. window.StateMachine = StateMachine;
  177. }
  178. //===========
  179. // WEB WORKER
  180. //===========
  181. else if (typeof self !== 'undefined') {
  182. self.StateMachine = StateMachine;
  183. }
  184. }());