You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
232 lines
5.5 KiB
232 lines
5.5 KiB
|
|
/*! |
|
* Module dependencies. |
|
*/ |
|
|
|
'use strict'; |
|
|
|
const utils = require('./utils'); // eslint-disable-line no-unused-vars |
|
|
|
/** |
|
* StateMachine represents a minimal `interface` for the |
|
* constructors it builds via StateMachine.ctor(...). |
|
* |
|
* @api private |
|
*/ |
|
|
|
const StateMachine = module.exports = exports = function StateMachine() { |
|
}; |
|
|
|
/** |
|
* StateMachine.ctor('state1', 'state2', ...) |
|
* A factory method for subclassing StateMachine. |
|
* The arguments are a list of states. For each state, |
|
* the constructor's prototype gets state transition |
|
* methods named after each state. These transition methods |
|
* place their path argument into the given state. |
|
* |
|
* @param {String} state |
|
* @param {String} [state] |
|
* @return {Function} subclass constructor |
|
* @api private |
|
*/ |
|
|
|
StateMachine.ctor = function() { |
|
const states = [...arguments]; |
|
|
|
const ctor = function() { |
|
StateMachine.apply(this, arguments); |
|
this.paths = {}; |
|
this.states = {}; |
|
}; |
|
|
|
ctor.prototype = new StateMachine(); |
|
ctor.prototype.constructor = ctor; |
|
|
|
ctor.prototype.stateNames = states; |
|
|
|
states.forEach(function(state) { |
|
// Changes the `path`'s state to `state`. |
|
ctor.prototype[state] = function(path) { |
|
this._changeState(path, state); |
|
}; |
|
}); |
|
|
|
return ctor; |
|
}; |
|
|
|
/** |
|
* This function is wrapped by the state change functions: |
|
* |
|
* - `require(path)` |
|
* - `modify(path)` |
|
* - `init(path)` |
|
* |
|
* @api private |
|
*/ |
|
|
|
StateMachine.prototype._changeState = function _changeState(path, nextState) { |
|
const prevState = this.paths[path]; |
|
if (prevState === nextState) { |
|
return; |
|
} |
|
const prevBucket = this.states[prevState]; |
|
if (prevBucket) delete prevBucket[path]; |
|
|
|
this.paths[path] = nextState; |
|
this.states[nextState] = this.states[nextState] || {}; |
|
this.states[nextState][path] = true; |
|
}; |
|
|
|
/*! |
|
* ignore |
|
*/ |
|
|
|
StateMachine.prototype.clear = function clear(state) { |
|
if (this.states[state] == null) { |
|
return; |
|
} |
|
const keys = Object.keys(this.states[state]); |
|
let i = keys.length; |
|
let path; |
|
|
|
while (i--) { |
|
path = keys[i]; |
|
delete this.states[state][path]; |
|
delete this.paths[path]; |
|
} |
|
}; |
|
|
|
/*! |
|
* ignore |
|
*/ |
|
|
|
StateMachine.prototype.clearPath = function clearPath(path) { |
|
const state = this.paths[path]; |
|
if (!state) { |
|
return; |
|
} |
|
delete this.paths[path]; |
|
delete this.states[state][path]; |
|
}; |
|
|
|
/** |
|
* Gets the paths for the given state, or empty object `{}` if none. |
|
* @api private |
|
*/ |
|
|
|
StateMachine.prototype.getStatePaths = function getStatePaths(state) { |
|
if (this.states[state] != null) { |
|
return this.states[state]; |
|
} |
|
return {}; |
|
}; |
|
|
|
/** |
|
* Checks to see if at least one path is in the states passed in via `arguments` |
|
* e.g., this.some('required', 'inited') |
|
* |
|
* @param {String} state that we want to check for. |
|
* @api private |
|
*/ |
|
|
|
StateMachine.prototype.some = function some() { |
|
const _this = this; |
|
const what = arguments.length ? arguments : this.stateNames; |
|
return Array.prototype.some.call(what, function(state) { |
|
if (_this.states[state] == null) { |
|
return false; |
|
} |
|
return Object.keys(_this.states[state]).length; |
|
}); |
|
}; |
|
|
|
/** |
|
* This function builds the functions that get assigned to `forEach` and `map`, |
|
* since both of those methods share a lot of the same logic. |
|
* |
|
* @param {String} iterMethod is either 'forEach' or 'map' |
|
* @return {Function} |
|
* @api private |
|
*/ |
|
|
|
StateMachine.prototype._iter = function _iter(iterMethod) { |
|
return function() { |
|
let states = [...arguments]; |
|
const callback = states.pop(); |
|
|
|
if (!states.length) states = this.stateNames; |
|
|
|
const _this = this; |
|
|
|
const paths = states.reduce(function(paths, state) { |
|
if (_this.states[state] == null) { |
|
return paths; |
|
} |
|
return paths.concat(Object.keys(_this.states[state])); |
|
}, []); |
|
|
|
return paths[iterMethod](function(path, i, paths) { |
|
return callback(path, i, paths); |
|
}); |
|
}; |
|
}; |
|
|
|
/** |
|
* Iterates over the paths that belong to one of the parameter states. |
|
* |
|
* The function profile can look like: |
|
* this.forEach(state1, fn); // iterates over all paths in state1 |
|
* this.forEach(state1, state2, fn); // iterates over all paths in state1 or state2 |
|
* this.forEach(fn); // iterates over all paths in all states |
|
* |
|
* @param {String} [state] |
|
* @param {String} [state] |
|
* @param {Function} callback |
|
* @api private |
|
*/ |
|
|
|
StateMachine.prototype.forEach = function forEach() { |
|
this.forEach = this._iter('forEach'); |
|
return this.forEach.apply(this, arguments); |
|
}; |
|
|
|
/** |
|
* Maps over the paths that belong to one of the parameter states. |
|
* |
|
* The function profile can look like: |
|
* this.forEach(state1, fn); // iterates over all paths in state1 |
|
* this.forEach(state1, state2, fn); // iterates over all paths in state1 or state2 |
|
* this.forEach(fn); // iterates over all paths in all states |
|
* |
|
* @param {String} [state] |
|
* @param {String} [state] |
|
* @param {Function} callback |
|
* @return {Array} |
|
* @api private |
|
*/ |
|
|
|
StateMachine.prototype.map = function map() { |
|
this.map = this._iter('map'); |
|
return this.map.apply(this, arguments); |
|
}; |
|
|
|
/** |
|
* Returns a copy of this state machine |
|
* |
|
* @param {Function} callback |
|
* @return {StateMachine} |
|
* @api private |
|
*/ |
|
|
|
StateMachine.prototype.clone = function clone() { |
|
const result = new this.constructor(); |
|
result.paths = { ...this.paths }; |
|
for (const state of this.stateNames) { |
|
if (!(state in this.states)) { |
|
continue; |
|
} |
|
result.states[state] = this.states[state] == null ? this.states[state] : { ...this.states[state] }; |
|
} |
|
return result; |
|
};
|
|
|