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