stateMachine.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  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. * @api 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. };
  34. ctor.prototype = new StateMachine();
  35. ctor.prototype.constructor = ctor;
  36. ctor.prototype.stateNames = states;
  37. states.forEach(function(state) {
  38. // Changes the `path`'s state to `state`.
  39. ctor.prototype[state] = function(path) {
  40. this._changeState(path, state);
  41. };
  42. });
  43. return ctor;
  44. };
  45. /**
  46. * This function is wrapped by the state change functions:
  47. *
  48. * - `require(path)`
  49. * - `modify(path)`
  50. * - `init(path)`
  51. *
  52. * @api private
  53. */
  54. StateMachine.prototype._changeState = function _changeState(path, nextState) {
  55. const prevState = this.paths[path];
  56. if (prevState === nextState) {
  57. return;
  58. }
  59. const prevBucket = this.states[prevState];
  60. if (prevBucket) delete prevBucket[path];
  61. this.paths[path] = nextState;
  62. this.states[nextState] = this.states[nextState] || {};
  63. this.states[nextState][path] = true;
  64. };
  65. /*!
  66. * ignore
  67. */
  68. StateMachine.prototype.clear = function clear(state) {
  69. if (this.states[state] == null) {
  70. return;
  71. }
  72. const keys = Object.keys(this.states[state]);
  73. let i = keys.length;
  74. let path;
  75. while (i--) {
  76. path = keys[i];
  77. delete this.states[state][path];
  78. delete this.paths[path];
  79. }
  80. };
  81. /*!
  82. * ignore
  83. */
  84. StateMachine.prototype.clearPath = function clearPath(path) {
  85. const state = this.paths[path];
  86. if (!state) {
  87. return;
  88. }
  89. delete this.paths[path];
  90. delete this.states[state][path];
  91. };
  92. /**
  93. * Gets the paths for the given state, or empty object `{}` if none.
  94. * @api private
  95. */
  96. StateMachine.prototype.getStatePaths = function getStatePaths(state) {
  97. if (this.states[state] != null) {
  98. return this.states[state];
  99. }
  100. return {};
  101. };
  102. /**
  103. * Checks to see if at least one path is in the states passed in via `arguments`
  104. * e.g., this.some('required', 'inited')
  105. *
  106. * @param {String} state that we want to check for.
  107. * @api private
  108. */
  109. StateMachine.prototype.some = function some() {
  110. const _this = this;
  111. const what = arguments.length ? arguments : this.stateNames;
  112. return Array.prototype.some.call(what, function(state) {
  113. if (_this.states[state] == null) {
  114. return false;
  115. }
  116. return Object.keys(_this.states[state]).length;
  117. });
  118. };
  119. /**
  120. * This function builds the functions that get assigned to `forEach` and `map`,
  121. * since both of those methods share a lot of the same logic.
  122. *
  123. * @param {String} iterMethod is either 'forEach' or 'map'
  124. * @return {Function}
  125. * @api private
  126. */
  127. StateMachine.prototype._iter = function _iter(iterMethod) {
  128. return function() {
  129. let states = [...arguments];
  130. const callback = states.pop();
  131. if (!states.length) states = this.stateNames;
  132. const _this = this;
  133. const paths = states.reduce(function(paths, state) {
  134. if (_this.states[state] == null) {
  135. return paths;
  136. }
  137. return paths.concat(Object.keys(_this.states[state]));
  138. }, []);
  139. return paths[iterMethod](function(path, i, paths) {
  140. return callback(path, i, paths);
  141. });
  142. };
  143. };
  144. /**
  145. * Iterates over the paths that belong to one of the parameter states.
  146. *
  147. * The function profile can look like:
  148. * this.forEach(state1, fn); // iterates over all paths in state1
  149. * this.forEach(state1, state2, fn); // iterates over all paths in state1 or state2
  150. * this.forEach(fn); // iterates over all paths in all states
  151. *
  152. * @param {String} [state]
  153. * @param {String} [state]
  154. * @param {Function} callback
  155. * @api private
  156. */
  157. StateMachine.prototype.forEach = function forEach() {
  158. this.forEach = this._iter('forEach');
  159. return this.forEach.apply(this, arguments);
  160. };
  161. /**
  162. * Maps over the paths that belong to one of the parameter states.
  163. *
  164. * The function profile can look like:
  165. * this.forEach(state1, fn); // iterates over all paths in state1
  166. * this.forEach(state1, state2, fn); // iterates over all paths in state1 or state2
  167. * this.forEach(fn); // iterates over all paths in all states
  168. *
  169. * @param {String} [state]
  170. * @param {String} [state]
  171. * @param {Function} callback
  172. * @return {Array}
  173. * @api private
  174. */
  175. StateMachine.prototype.map = function map() {
  176. this.map = this._iter('map');
  177. return this.map.apply(this, arguments);
  178. };
  179. /**
  180. * Returns a copy of this state machine
  181. *
  182. * @param {Function} callback
  183. * @return {StateMachine}
  184. * @api private
  185. */
  186. StateMachine.prototype.clone = function clone() {
  187. const result = new this.constructor();
  188. result.paths = { ...this.paths };
  189. for (const state of this.stateNames) {
  190. if (!(state in this.states)) {
  191. continue;
  192. }
  193. result.states[state] = this.states[state] == null ? this.states[state] : { ...this.states[state] };
  194. }
  195. return result;
  196. };