UNPKG

leopold

Version:
645 lines (570 loc) 19.6 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _bluebird = require('bluebird'); var _bluebird2 = _interopRequireDefault(_bluebird); var _stampit = require('stampit'); var _stampit2 = _interopRequireDefault(_stampit); var _cuid = require('cuid'); var _cuid2 = _interopRequireDefault(_cuid); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } //utility function isFunction(obj) { return obj && toString.call(obj === '[object Function]'); } /** * accepts `_id` as an initial id value. if an `id` function * exists (further up the composition chain) it does not override it; * otherwise, it provides its own method for `id()` * */ var identifiable = (0, _stampit2.default)().init(function () { //accept id initializer value var id = this._id;delete this._id; if (!isFunction(this.id)) { this.id = function () { return id || (id = (0, _cuid2.default)()); }; this.hasIdentity = function () { return typeof id !== 'undefined'; }; } }); /** * encapsulates behaviors for revisioning of components * */ var revisable = (0, _stampit2.default)().init(function () { var _this = this; var revision = 1; /** * either get the current revision or set the revision with `val` * @param {Number} val the revision to set * */ this.revision = function (val) { if (val) { return revision = val; } return revision; }; /** * gets next revision (doesnt mutate state) * */ this.nextRevision = function () { return _this.revision() + 1; }; }); /** * simple hashmap storage of event providers * */ var hashIdentityMap = (0, _stampit2.default)().init(function () { var providers = new Map(); this.register = function (id, provider) { if (!id) { throw new Error('`id` is required'); } if (!provider) { throw new Error('`provider` is required'); } providers.set(id, provider); return provider; }; this.get = function (id) { if (!id) { throw new Error('`id` is required'); } var provider = providers.get(id); if (!provider) { throw new Error('could not locate provider with id "' + id + '""'); } return provider; }; this.release = function () { providers.clear(); }; }); var nullStorage = (0, _stampit2.default)().init(function () { this.store = function () {}; this.events = regeneratorRuntime.mark(function _callee(from, to) { return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: return _context.abrupt('return', []); case 1: case 'end': return _context.stop(); } } }, _callee, this); }); this.clear = function () {}; }); var inMemoryStorage = (0, _stampit2.default)().compose(revisable).init(function () { var _this2 = this; var envelopes = []; this.store = function (env) { if (!env.revision) { env.revision = _this2.revision(_this2.nextRevision()); } envelopes.push(env); return _this2; }; /** * clear all envelops. DANGER ZONE! * */ this.clear = function () { envelopes = []; }; this.events = regeneratorRuntime.mark(function _callee2(from, to) { var _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, env, _iteratorNormalCompletion2, _didIteratorError2, _iteratorError2, _iterator2, _step2, ev; return regeneratorRuntime.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: from = from || 0; to = to || Number.MAX_VALUE; if (!(from > to)) { _context2.next = 4; break; } throw new Error('`from` must be less than or equal `to`'); case 4: if (envelopes.length) { _context2.next = 6; break; } return _context2.abrupt('return', []); case 6: _iteratorNormalCompletion = true; _didIteratorError = false; _iteratorError = undefined; _context2.prev = 9; _iterator = envelopes[Symbol.iterator](); case 11: if (_iteratorNormalCompletion = (_step = _iterator.next()).done) { _context2.next = 45; break; } env = _step.value; if (!(env.revision > to)) { _context2.next = 15; break; } return _context2.abrupt('return'); case 15: if (!(env.revision >= from)) { _context2.next = 42; break; } _iteratorNormalCompletion2 = true; _didIteratorError2 = false; _iteratorError2 = undefined; _context2.prev = 19; _iterator2 = env.events[Symbol.iterator](); case 21: if (_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done) { _context2.next = 28; break; } ev = _step2.value; _context2.next = 25; return ev; case 25: _iteratorNormalCompletion2 = true; _context2.next = 21; break; case 28: _context2.next = 34; break; case 30: _context2.prev = 30; _context2.t0 = _context2['catch'](19); _didIteratorError2 = true; _iteratorError2 = _context2.t0; case 34: _context2.prev = 34; _context2.prev = 35; if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } case 37: _context2.prev = 37; if (!_didIteratorError2) { _context2.next = 40; break; } throw _iteratorError2; case 40: return _context2.finish(37); case 41: return _context2.finish(34); case 42: _iteratorNormalCompletion = true; _context2.next = 11; break; case 45: _context2.next = 51; break; case 47: _context2.prev = 47; _context2.t1 = _context2['catch'](9); _didIteratorError = true; _iteratorError = _context2.t1; case 51: _context2.prev = 51; _context2.prev = 52; if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } case 54: _context2.prev = 54; if (!_didIteratorError) { _context2.next = 57; break; } throw _iteratorError; case 57: return _context2.finish(54); case 58: return _context2.finish(51); case 59: case 'end': return _context2.stop(); } } }, _callee2, this, [[9, 47, 51, 59], [19, 30, 34, 42], [35,, 37, 41], [52,, 54, 58]]); }); }); var writeableUnitOfWork = (0, _stampit2.default)().refs({ storage: undefined, identityMap: undefined }).methods({ envelope: function enveloper(events) { return { events: events }; } }).init(function () { var _this3 = this; var pending = []; this.append = function (e) { pending.push.apply(pending, e); return e; }; this.commit = function () { //move reference to array in case event arrives while flushing var committable = pending.splice(0, pending.length); var envelope = _this3.envelope(committable); _this3.storage.store(envelope); return _this3; }; this.register = function () { //no op }; }); var readableUnitOfWork = (0, _stampit2.default)().refs({ storage: undefined, identityMap: undefined }).init(function () { var _this4 = this; this.append = function (e) { //no op return e; }; this.commit = function () { return _this4; }; this.register = this.identityMap.register; //helper function to allow function binding during iteration function asyncApply(event, identityMap) { var target = identityMap.get(event.id); return target.applyEvent(event); } var iterate = function iterate(cur, iterator, accumulator) { if (cur.done) { return accumulator; } var event = cur.value; var result = undefined; if (accumulator.promise) { //chain promises //effectively creating a complicated reduce statement accumulator.promise = result = accumulator.promise.then(asyncApply.bind(_this4, event, _this4.identityMap)); } else { var target = _this4.identityMap.get(event.id); var fn = target.applyEvent.bind(target, event); try { result = fn(); } catch (err) { iterator.throw(err); throw err; } //was a promise returned? if (result && result.then) { accumulator.promise = result; } } return iterate(iterator.next(), iterator, accumulator); }; this.restore = function (root, from, to) { if (!root) { throw new Error('`root` is required'); } _this4.register(root.id(), root); var events = _this4.storage.events(from, to); var accumulator = {}; iterate(events.next(), events, accumulator); if (accumulator.promise) { return accumulator.promise; } return _this4; }; }); var unitOfWork = (0, _stampit2.default)().refs({ storage: undefined, identityMap: undefined }).methods({ envelope: function enveloper(events) { return { events: events }; } }).init(function () { var _this5 = this; var current = void 0; var writeable = writeableUnitOfWork({ envelope: this.envelope, identityMap: this.identityMap, storage: this.storage }); var readable = readableUnitOfWork({ identityMap: this.identityMap, storage: this.storage }); this.append = function (e) { var result = current.append(e); if (!_this5.atomic) { //each event gets stored _this5.commit(); return result; } return result; }; this.commit = function () { current.commit(); _this5.identityMap.release(); return _this5; }; this.register = function (id, provider) { return current.register(id, provider); }; this.restore = function (root, from, to) { current = readable; var result = current.restore(root, from, to); if (result.then) { return result.bind(_this5).then(function () { this.identityMap.release(); current = writeable; return this; }); } else { _this5.identityMap.release(); current = writeable; return _this5; } }; //by default we are in writeable state current = writeable; }); var eventable = (0, _stampit2.default)().init(function () { var _this6 = this; var uow = this.leo.unitOfWork(); //decorate event(s) with critical properties var decorate = function decorate(arr) { var rev = _this6.nextRevision(); return arr.map(function (e) { e.id = _this6.id(); e.revision = rev++; return e; }); }; var assertIdentity = function assertIdentity() { if (!_this6.hasIdentity()) { throw new Error('identity is unknown'); } }; var validateEvents = function validateEvents(arr) { var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = arr[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { var e = _step3.value; if (!e || !e.event) { throw new Error('`event` is required'); } } } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3.return) { _iterator3.return(); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } return arr; }; var pushEvent = function pushEvent(e) { assertIdentity(); if (!Array.isArray(e)) { e = [e]; } validateEvents(e); decorate(e); uow.append(e); return e; }; /** * raise is the main interface you will use to mutate the eventable * instance and push the events onto the unit of work into storage. * The eventable instance revision is incremented. * @param {Object|Array} e the event(s) to push * @return {eventable} the result of `${event}` calls * */ this.raise = function (e) { return _this6.applyEvent(pushEvent(e)); }; /** * pushEvent allows you to push event(s) directly into * the unit of work without mutating the provider. * this lets you stream into storage without getting into recursive loops * @param {Object|Array} e the event(s) to push * @return {eventable} the stampit eventable instance * */ this.pushEvent = function (e) { e = pushEvent(e); _this6.revision(e[e.length - 1].revision); return _this6; }; var applyEvent = function applyEvent(e, applied) { if (applied.length === e.length) { return applied; } var current = e[applied.length]; _this6.revision(current.revision); applied.length = applied.length + 1; var fn = _this6['$' + current.event]; var result = undefined; if (applied.promise) { if (!fn) { return applyEvent(e, applied); } applied.promise = result = applied.promise.return(current).bind(_this6).then(fn); } else { if (!fn) { return applyEvent(e, applied); } result = fn.call(_this6, current); //received a promise if (result && result.then) { applied.promise = result; } } applied.results.push(result); return applyEvent(e, applied); }; this.applyEvent = function (e) { if (!Array.isArray(e)) { e = [e]; } var applied = { results: [], async: false, length: 0 }; applyEvent(e, applied); if (applied.promise) { return _bluebird2.default.all(applied.results); } return _this6; }; //register this instance on the unit of work uow.register(this.id(), this); }); exports.default = (0, _stampit2.default)().static({ /** * null object pattern for storage * when memory footprint is a concern or YAGNI storage * but want the benefits of event provider. * Handy for testing * */ nullStorage: nullStorage }).compose(identifiable).refs({ /** * `false` immediately stores events; otherwise, they are * queued to be committed to storage later. * */ atomic: true }).init(function () { var _this7 = this; this.storage = this.storage || inMemoryStorage(); this.identityMap = this.identityMap || hashIdentityMap(); //default uow impl var uow = unitOfWork({ storage: this.storage, identityMap: this.identityMap, atomic: this.atomic }); /** * Expose an `stamp` that may be use for composition * with another stamp * @method eventable * @return {stamp} factory that may be composed to attach * `eventable` behaviors onto another stamp * */ this.eventable = function () { return (0, _stampit2.default)().props({ leo: _this7 }).compose(identifiable).compose(revisable).compose(eventable); }; /** * convenience method to commit pending events to storage * @return {leopold} * */ this.commit = function () { return _this7.unitOfWork().commit(); }; /** * convenience method to unitOfWork inside `eventable` impl * @return {unitOfWork} * */ this.unitOfWork = function () { return uow; }; /** * mount an envelope having events into storage * useful for testing, or perhaps seeding an app from a backend * */ this.mount = function (envelope) { _this7.storage.store(envelope); return _this7; }; /** * restore to revision `to` from revision `from` * using `root` at the entrypoint. `to` and `from` are inclusive. * @param {eventable} root any `eventable` object * @param {Number} from lower bound revision to include * @param {Number} to upper bound revision to include * @return {Promise} resolving this leo instance */ this.restore = function (root, from, to) { return _this7.unitOfWork().restore(root, from, to); }; this.revision = function () { return _this7.storage.revision(); }; });