@freshlysqueezedgames/hermes
Version:
independant state management pipeline
705 lines (524 loc) • 20.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
/**
* @name Query
* @description Takes a path and runs a server request if required. Will then apply resultant data on the Action as it's payload for the reducers
* @param {string} path: The path that the action will be applied to
* @param {Action} action: the action invoked
* @return {Promise}
* @public
*/
var Query = function () {
var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(path, action) {
var t, steps, OnApply, paths, i, l, requestPath, item, state, result;
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
t = this;
if (t.verbose) {
console.log('getting this path!', path);
}
steps = path.split('/');
OnApply = function OnApply() {
var payload = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : Object.create(null);
action.payload = payload; // Override the payload with the new one given
if (t.verbose) {
console.log('Updating with this action payload', action, t.store);
}
// Update the store, and ensure that the new state is in place.
Update.call(t, steps, action, path);
// This part, working in parent-first left-to-right, we should trigger any subscribers at each path within the CHANGED heap.
//Publish.call(t, steps, action)
Dispatch.call(t);
};
if (!(!t.remote || !t.remote.paths || !t.remote.paths.length)) {
_context.next = 7;
break;
}
OnApply(action.payload);
return _context.abrupt('return');
case 7:
paths = t.remote.paths;
i = -1;
l = paths.length;
requestPath = void 0;
case 11:
if (!(++i < l)) {
_context.next = 18;
break;
}
item = paths[i];
if (!item.re.test(path)) {
_context.next = 16;
break;
}
if (action.context && Object.keys(action.context).length) {
requestPath = item.ToPath(action.context);
} else {
requestPath = item.originalPath;
}
return _context.abrupt('break', 18);
case 16:
_context.next = 11;
break;
case 18:
if (requestPath) {
_context.next = 21;
break;
}
// this is not a requestable piece of data!
OnApply(action.payload);
return _context.abrupt('return');
case 21:
state = void 0;
Branch(t.store, steps, undefined, false, function (node) {
return state = node || Object.create();
});
_context.prev = 23;
_context.next = 26;
return new Promise(function (resolve, reject) {
if (t.remote.request(requestPath, action, state, function (payload) {
return resolve && resolve(_extends({}, action.payload, payload));
}) === false) {
resolve(action.payload);
}
});
case 26:
result = _context.sent;
if (t.verbose) {
console.log('Request result!', result);
}
OnApply(result);
_context.next = 35;
break;
case 31:
_context.prev = 31;
_context.t0 = _context['catch'](23);
console.log('what the hell just happened?', _context.t0);
throw new Error(_context.t0);
case 35:
case 'end':
return _context.stop();
}
}
}, _callee, this, [[23, 31]]);
}));
return function Query(_x2, _x3) {
return _ref.apply(this, arguments);
};
}();
/**
* @name Update
* @description This follows the designated path along each step in the heap's branch, and then applies the data
* heap to the store from there
* @param {Array} steps: The path represented as an array of keys
* @param {Action} action: The action to perform on the heap
* @return {Hermes}
*/
var _pathToRegexp = require('path-to-regexp');
var _pathToRegexp2 = _interopRequireDefault(_pathToRegexp);
var _Action = require('./Action');
var _Action2 = _interopRequireDefault(_Action);
var _Reducer = require('./Reducer');
var _Reducer2 = _interopRequireDefault(_Reducer);
var _Route = require('./Route');
var _Route2 = _interopRequireDefault(_Route);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var _Object$prototype = Object.prototype,
toString = _Object$prototype.toString,
hasOwnProperty = _Object$prototype.hasOwnProperty;
var FUNCTION = '[object Function]';
var ARRAY = '[object Array]';
var OBJECT = '[object Object]';
/**
* @class Hermes
* @description This singleton manages stores in the context of a external data source and marries thet.pathHeap not only to the urls for the server,
* but for the object traversal itself. Making it possible to listen for changes to data at any specific point in a heap. As any flux design, actions
* trigger changes in the t.store (including from the server) and views can subscribe to object changes in the heap(s)
*/
var Hermes = function () {
/**
* @constructor
* @description Takes the property configuration and build the store, path and reducer heaps. Keeping appropriate linear leaf references for fast lookup times
* @param {Object} props
*/
function Hermes(props) {
_classCallCheck(this, Hermes);
var t = this;
t.events = [];
t.callbacks = Object.create(null);
t.context = "";
// Each hermes instance has a private store that is used to manage a state heap.
t.store = Object.create(null);
t.reducerHeap = Object.create(null);
t.reducerEnds = [];
// Apply props to the instance
t.verbose = props.verbose || false;
t.dispatchActions = props.dispatchActions === false ? false : true;
t.ignoreEvents = false;
if (props.remote) {
var _props$remote = props.remote,
paths = _props$remote.paths,
request = _props$remote.request;
if (!paths) {
throw new Error('Paths must be specified on the remote where you expect a server connection to occur');
}
if (!request) {
throw new Error('A request function must be defined so that hermes can give appropriate data back to you');
}
paths = [].concat(_toConsumableArray(paths));
paths.sort(function (a, b) {
if (a.length > b.length) {
return -1;
}
if (b.length > a.length) {
return 1;
}
return 0;
});
var i = -1;
var l = paths.length;
while (++i < l) {
paths[i] = new _Route2.default(paths[i]);
}
t.remote = { request: request, paths: paths };
}
var reducers = props.reducers;
if (reducers) {
for (var key in reducers) {
if (hasOwnProperty.call(reducers, key)) {
(function () {
var targetReducer = reducers[key];
if (!(targetReducer instanceof _Reducer2.default)) {
throw new Error('Property at path: ', key, ' is not a Reducer instance!');
}
if (targetReducer.path !== '') {
throw new Error('Reducer instances must be unique to each path for Hermes to efficiently find the correct location to allocate actions: ' + key + ' & ' + targetReducer.path + ' share the same instance');
}
// Bind a function for accessing the events list. There is one list per Hermes.
targetReducer.hermes = t;
Branch(t.reducerHeap, key.split('/'), function () {
var node = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : Object.create(null);
var step = arguments[1];
var i = arguments[2];
node.step = step;
node.position = i + 1;
return node;
}, true, function (current) {
t.reducerEnds.push(current);
current.reducer = targetReducer;
return current;
});
targetReducer.path = key;
targetReducer.keys = [];
targetReducer.regex = (0, _pathToRegexp2.default)(key, targetReducer.keys);
targetReducer.keys = targetReducer.keys.map(function (key) {
return key.name;
});
})();
}
}
} else if (props.verbose) {
console.warn('No reducers have been defined for the instance, this means all data will be copied as submitted in action payloads: ', props.name);
}
t.reducer = new _Reducer2.default();
t.reducer.hermes = t;
t.map = {}; // this will be used to index matching reducers based on paths
}
/**
* @name Subscribe
* @description Applies a listener to
* @param {*} name
* @param {*} callback
* @param {*} context
*/
_createClass(Hermes, [{
key: 'Subscribe',
value: function Subscribe(name, callback, path) {
var t = this;
if (!name || typeof name !== 'string' || toString.call(callback) !== FUNCTION) {
throw new Error('you must always call Subscribe with a string path and a callback function');
}
var list = t.callbacks[name] = t.callbacks[name] || []; // create a new array if there isn't one
if (path) {
callback.keys = [];
callback.regex = (0, _pathToRegexp2.default)(path, callback.keys);
}
list.push(callback);
return t;
}
}, {
key: 'Unsubscribe',
value: function Unsubscribe(name, callback) {
var t = this;
var list = t.callbacks[name];
if (!list) {
return t;
}
if (typeof callback === 'undefined') {
delete t.callbacks[name];
return t;
}
var i = list.length;
while (i--) {
var item = list[i];
item === callback && list.splice(i, 1); // could be more than one subscription of this I suppose...
}
return t;
}
/**
* @name Do
* @description Performs an action and applies it to the heap
* @param {Action} action : The action to take
* @param {string[Optional]} path : optionally, you can set the path to avoid the lookup cycle, this will avoid a linear lookup through the reducer list
* @return {Promise} A promise that resolves when the action has been executed
* @public
*/
}, {
key: 'Do',
value: function Do(action, path) {
var t = this;
if (!(action instanceof _Action2.default)) {
throw new Error('Parameter 1 must be an Action instance', action);
}
if (t.current) {
return t.current = t.current.then(function () {
return t.Do(action, path);
});
}
// one time call for the first request of the data
return t.current = Query.call(t, _pathToRegexp2.default.compile(path || action.Reducer().path)(action.context), action).then(function () {
t.current = null;
});
}
/**
* @name AddEvent
* @description The number
* @param {*} name
* @param {*} payload
* @param {*} context
* @return {Hermes}
* @public
*/
}, {
key: 'AddEvent',
value: function AddEvent(name) {
var t = this;
if (t.ignoreEvents) {
return t;
}
t.events.push({ name: name, payload: t.payload, path: t.currentPath, context: t.context });
return t;
}
}, {
key: 'GetState',
value: function GetState() {
return this.store;
}
}, {
key: 'Print',
value: function Print() {
console.log(this.store);
return this;
}
}]);
return Hermes;
}();
exports.default = Hermes;
function Dispatch() {
var t = this;
var i = -1;
var l = t.events.length;
while (++i < l) {
var event = t.events[i];
if (!t.callbacks[event.name]) {
continue;
}
var list = t.callbacks[event.name];
var j = -1;
var l2 = list.length;
while (++j < l2) {
var _callback = list[j];
var result = void 0;
if (_callback.regex && !(result = _callback.regex.exec(event.path))) {
continue;
}
var content = _extends({}, event);
content.payload = content.payload(); // invoke this to collect the resultant state, and remove the reference for GC
var keys = _callback.keys;
if (keys && keys.length) {
// if the urls have keys, we need to gather the values so we can show the context
var context = Object.create(null);
result.shift();
var _i = keys.length;
while (_i--) {
context[keys[_i].name] = result[_i];
}
content.context = context;
}
if (_callback(content)) {
list.splice(j--, 1);
l2--;
}
}
}
t.events = [];
t.currentPath = '';
t.payload = null;
t.context = null;
}
/**
* @name SetContext
* @description This marks the current path when a reducer event is fired, along with path params if the path is ambiguous
* @param {string} name L
*/
function SetContext(path, context, payload) {
var t = this;
t.currentPath = path;
t.context = context;
t.payload = payload;
}
function MatchingReducers(path) {
var t = this;
var reducerEnds = t.reducerEnds;
var reducers = void 0;
if (t.map[path]) {
// cuts out having to do regex's more than once on a path.
return t.map[path];
}
var i = -1;
var l = reducerEnds.length;
while (++i < l) {
var reducer = reducerEnds[i].reducer;
if (reducer.regex.exec(path) !== null) {
(reducers = reducers || []).push(reducer);
}
}
return t.map[path] = reducers;
}function Update(steps, action, originalPath) {
var t = this;
var actionDispatched = void 0;
function OnStep(node, path, payload) {
var test = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
var strPath = path.join('/');
var result = MatchingReducers.call(t, strPath) || [t.reducer]; // look for an exact path match in the reducers
var state = node;
if (!state) {
t.ignoreEvents = true;
state = result[0].Reduce(new _Action2.default('__init__'));
// remove events created as a result.
t.ignoreEvents = false;
return state;
}
if (t.verbose) {
console.log('the reducers to be called are', result);
}
var i = -1;
var l = result.length;
var context = action.context;
while (++i < l) {
if (result[i].regex) {
var item = result[i];
var matches = strPath.match(item.regex);
matches.shift();
var j = -1;
var keys = item.keys;
var l2 = keys.length;
while (++j < l2) {
context[keys[j]] = matches.shift();
}
}
}
context.$$path = originalPath;
SetContext.call(t, strPath, action.context, function () {
return state;
});
i = -1;
while (++i < l) {
if (t.dispatchActions && !actionDispatched && path.join('/') === originalPath) {
// we should only trigger once, on the Reducer level that created the action.
t.AddEvent(action.name);
actionDispatched = true;
}
state = result[i].Reduce(action, state, payload);
}
return state;
}
function OnAppend(store) {
// then the returned heap needs to be updated (tree structure, no longer branch path)
// payloads exist because we are now in the returned object heap.
if (!action.apply) {
return store;
}
return Tree(store, action.payload, function (node, payload, keys) {
return OnStep(node, [].concat(_toConsumableArray(steps), _toConsumableArray(keys)), payload);
}, t.reducerHeap);
}
// we need to update the branch for the store where the path is concerned
// no payload as this is just a path update
Branch(t.store, steps, function (node, step, i) {
return OnStep(node, steps.slice(0, i + 1), i === steps.length - 1 ? action.payload : undefined);
}, false, OnAppend);
return t;
}
function Tree(target, heap, onNode) {
var keys = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
if (toString.call(heap) === ARRAY) {
var i = -1;
var member = void 0;
while (member = heap[++i]) {
if ((typeof member === 'undefined' ? 'undefined' : _typeof(member)) !== 'object') {
continue;
}
var childKeys = [].concat(_toConsumableArray(keys), [i]);
target[i] = Tree(target[i] || (toString.call(member) === ARRAY ? new Array(member.length) : Object.create(null)), member, onNode, childKeys);
target[i] = onNode(target[i], member, childKeys);
}
return target;
}
for (var key in heap) {
var node = heap[key];
var typeString = toString.call(node);
if (typeString === OBJECT || typeString === ARRAY) {
var _childKeys = [].concat(_toConsumableArray(keys), [key]);
target[key] = Tree(target[key] || (typeString === ARRAY ? new Array(node.length) : Object.create(null)), node, onNode, _childKeys);
target[key] = onNode(target[key], node, _childKeys);
}
}
return target;
}
function Branch(target, steps) {
var onNode = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : OnNode;
var parenting = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
var onAppend = arguments[4];
var i = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 0;
var step = steps[i];
if (!step) {
return Branch(target, steps, onNode, parenting, onAppend, i + 1);
}
target[step] = target[step] || (toString.call(onNode(target[step], step, i, true)) === ARRAY ? [] : Object.create(null));
if (i + 1 < steps.length) {
target[step] = Branch(target[step], steps, onNode, parenting, onAppend, i + 1);
} else if (onAppend) {
target[step] = onAppend(target[step]);
}
target[step] = onNode(target[step], step, i);
if (parenting) {
target[step].parent = target;
}
return target;
}
function OnNode(node) {
// stops inline creation of objects in branch
return node;
}
//# sourceMappingURL=Hermes.js.map