UNPKG

@freshlysqueezedgames/hermes

Version:
705 lines (524 loc) 20.6 kB
'use strict'; 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