statemachine.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. /*!
  2. * Module dependencies.
  3. */
  4. 'use strict';
  5. const utils = require('./utils'); // eslint-disable-line no-unused-vars
  6. /*!
  7. * StateMachine represents a minimal `interface` for the
  8. * constructors it builds via StateMachine.ctor(...).
  9. *
  10. * @api private
  11. */
  12. const StateMachine = module.exports = exports = function StateMachine() {
  13. };
  14. /*!
  15. * StateMachine.ctor('state1', 'state2', ...)
  16. * A factory method for subclassing StateMachine.
  17. * The arguments are a list of states. For each state,
  18. * the constructor's prototype gets state transition
  19. * methods named after each state. These transition methods
  20. * place their path argument into the given state.
  21. *
  22. * @param {String} state
  23. * @param {String} [state]
  24. * @return {Function} subclass constructor
  25. * @private
  26. */
  27. StateMachine.ctor = function() {
  28. const states = [...arguments];
  29. const ctor = function() {
  30. StateMachine.apply(this, arguments);
  31. this.paths = {};
  32. this.states = {};
  33. this.stateNames = states;
  34. let i = states.length,
  35. state;
  36. while (i--) {
  37. state = states[i];
  38. this.states[state] = {};
  39. }
  40. };
  41. ctor.prototype = new StateMachine();
  42. states.forEach(function(state) {
  43. // Changes the `path`'s state to `state`.
  44. ctor.prototype[state] = function(path) {
  45. this._changeState(path, state);
  46. };
  47. });
  48. return ctor;
  49. };
  50. /*!
  51. * This function is wrapped by the state change functions:
  52. *
  53. * - `require(path)`
  54. * - `modify(path)`
  55. * - `init(path)`
  56. *
  57. * @api private
  58. */
  59. StateMachine.prototype._changeState = function _changeState(path, nextState) {
  60. const prevBucket = this.states[this.paths[path]];
  61. if (prevBucket) delete prevBucket[path];
  62. this.paths[path] = nextState;
  63. this.states[nextState][path] = true;
  64. };
  65. /*!
  66. * ignore
  67. */
  68. StateMachine.prototype.clear = function clear(state) {
  69. const keys = Object.keys(this.states[state]);
  70. let i = keys.length;
  71. let path;
  72. while (i--) {
  73. path = keys[i];
  74. delete this.states[state][path];
  75. delete this.paths[path];
  76. }
  77. };
  78. /*!
  79. * ignore
  80. */
  81. StateMachine.prototype.clearPath = function clearPath(path) {
  82. const state = this.paths[path];
  83. if (!state) {
  84. return;
  85. }
  86. delete this.paths[path];
  87. delete this.states[state][path];
  88. };
  89. /*!
  90. * Checks to see if at least one path is in the states passed in via `arguments`
  91. * e.g., this.some('required', 'inited')
  92. *
  93. * @param {String} state that we want to check for.
  94. * @private
  95. */
  96. StateMachine.prototype.some = function some() {
  97. const _this = this;
  98. const what = arguments.length ? arguments : this.stateNames;
  99. return Array.prototype.some.call(what, function(state) {
  100. return Object.keys(_this.states[state]).length;
  101. });
  102. };
  103. /*!
  104. * This function builds the functions that get assigned to `forEach` and `map`,
  105. * since both of those methods share a lot of the same logic.
  106. *
  107. * @param {String} iterMethod is either 'forEach' or 'map'
  108. * @return {Function}
  109. * @api private
  110. */
  111. StateMachine.prototype._iter = function _iter(iterMethod) {
  112. return function() {
  113. let states = [...arguments];
  114. const callback = states.pop();
  115. if (!states.length) states = this.stateNames;
  116. const _this = this;
  117. const paths = states.reduce(function(paths, state) {
  118. return paths.concat(Object.keys(_this.states[state]));
  119. }, []);
  120. return paths[iterMethod](function(path, i, paths) {
  121. return callback(path, i, paths);
  122. });
  123. };
  124. };
  125. /*!
  126. * Iterates over the paths that belong to one of the parameter states.
  127. *
  128. * The function profile can look like:
  129. * this.forEach(state1, fn); // iterates over all paths in state1
  130. * this.forEach(state1, state2, fn); // iterates over all paths in state1 or state2
  131. * this.forEach(fn); // iterates over all paths in all states
  132. *
  133. * @param {String} [state]
  134. * @param {String} [state]
  135. * @param {Function} callback
  136. * @private
  137. */
  138. StateMachine.prototype.forEach = function forEach() {
  139. this.forEach = this._iter('forEach');
  140. return this.forEach.apply(this, arguments);
  141. };
  142. /*!
  143. * Maps over the paths that belong to one of the parameter states.
  144. *
  145. * The function profile can look like:
  146. * this.forEach(state1, fn); // iterates over all paths in state1
  147. * this.forEach(state1, state2, fn); // iterates over all paths in state1 or state2
  148. * this.forEach(fn); // iterates over all paths in all states
  149. *
  150. * @param {String} [state]
  151. * @param {String} [state]
  152. * @param {Function} callback
  153. * @return {Array}
  154. * @private
  155. */
  156. StateMachine.prototype.map = function map() {
  157. this.map = this._iter('map');
  158. return this.map.apply(this, arguments);
  159. };