leopold
Version:
Event-sourced state support
645 lines (570 loc) • 19.6 kB
JavaScript
'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();
};
});