UNPKG

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