UNPKG

esdf

Version:

a frugal event-sourced domain-driven design framework with elements of cqrs

147 lines (131 loc) 7.63 kB
/** * @module esdf/utils/loadAggregate */ //TODO: Documentation. var when = require('when'); var util = require('util'); /** * An EventSink has not been provided to the aggregate loader function. At least an EventSink is required - otherwise, Event Soutcing as such can not function. */ function AggregateLoaderSinkNotGivenError(){ this.name = 'AggregateLoaderSinkNotGivenError'; this.message = 'At least an EventSink needs to be passed to createAggregateLoader!'; } util.inherits(AggregateLoaderSinkNotGivenError, Error); function NoOpSnapshotter(){ } NoOpSnapshotter.prototype.loadSnapshot = function loadSnapshot(ARType, ARID){ return when.reject('Dummy no-op snapshotter - rejecting load promise. To use a real snapshotter, pass it to the loadAggregate function.'); }; NoOpSnapshotter.prototype.saveSnapshot = function saveSnapshot(snapshot){ return when.reject('Dummy no-op snapshotter - rejecting save promise. To use a real snapshotter, pass it to the loadAggregate function.'); }; /** * Load an EventSourcedAggregate using an EventSink, assisted by a Snapshotter for increased performance (optional). This operation performs rehydration under the hood - if a snapshot is found, rehydration is done since that snapshot. * Note that loading an empty Aggregate, that is, one that has zero commits, is a valid operation by design. In such case, the AR returned will be in its initial state, right after its constructor is called. * Loading empty ARs is also the preferred method of creating new instances of any domain objects (simply generate a random ARID and load it). * @param {function} ARConstructor The Aggregate's constructor. Called via new, without any parameters. * @param {string} ARID Aggregate ID, used for loading the snapshot and the event stream. * @param {module:esdf/interfaces/EventSinkInterface} eventSink The EventSink used for rehydration. If a snapshotter is provided, it is only asked for commits "since" the snapshot. * @param {module:esdf/interfaces/AggregateSnapshotterInterface} [snapshotter] The snapshot provider to use when loading, to optimize loading times and lower system load. By default, only rehydration via EventSink is used. * @param {Object} [options] - Optional settings that change the behaviour of the loader. * @param {boolean} [options.advanced] - Whether an "advanced format" promise resolution value is desired, rather than just the aggregate instance. * @returns {external:Promise} a promise that resolves with the Aggregate as resolution value if loading succeeded, and rejects with a passed-through error if failed. If snapshot loading fails, the aggregate is rehydrated from events and the loading can still succeed. */ //TODO: Insert instrumentation probes to indicate when a snapshotter is not used (attempted) at all, to aid performance troubleshooting. function loadAggregate(ARConstructor, ARID, eventSink, snapshotter, options) { options = options || {}; // Pick a default value for the diffSince option ("compute difference since commit number") if required: var diffSince; if (typeof(options.diffSince) === 'number' && !isNaN(options.diffSince)) { diffSince = options.diffSince; } else { // No commits' slot numbers are greater than infinity. Thus, by default, the diff will be empty. diffSince = Infinity; } // Determine the aggregate type. The snapshot loader and/or the rehydrator (sink) may need this to find the data. var aggregateType = ARConstructor.prototype._aggregateType; // Function definitions for later use in loadAggregate: // Aggregate construction. function constructAggregate(){ var ARObject = new ARConstructor(); ARObject.setAggregateID(ARID); ARObject.setEventSink(eventSink); // If no snapshotter has been passed (or is not needed/used), instead of complicating logic, we simply replace it locally with a stub that knows no aggregates and rejects all loads. // This happens in constructAggregate since it relies on the AR object existing. if(!snapshotter || !ARObject.supportsSnapshotApplication()){ snapshotter = new NoOpSnapshotter(); } ARObject.setSnapshotter(snapshotter); return ARObject; } function rehydrateAggregate(ARObject) { // We need to load the aggregate's events since the earlier of the two points: diffSince and the initial state as obtained from e.g. a snapshot. // However, we will only be applying those events which are actually newer than the current state. Some will simply be gathered and returned informationally as requested. var loadFromSlot = Math.min(ARObject.getNextSequenceNumber(), diffSince + 1); //TODO: Optionally limit the length of returned diffs by computing the difference between loadFromSlot and ARObject.getNextSequenceNumber() as a DoS prevention measure. // Obtain a stream of commits that we will be processing one by one: var commitStream = eventSink.getCommitStream(ARID, loadFromSlot); // Process the stream: var diffCommits = []; return when.promise(function(resolve, reject) { commitStream.on('data', function processStreamedCommit(commit) { try { // Only apply those commits which have not been applied already to the aggregate root's state: if (commit.sequenceSlot >= ARObject.getNextSequenceNumber()) { ARObject.applyCommit(commit); } // If we've moved above the "diffSince" moment, everything since then is a difference, so add it to the list: if (commit.sequenceSlot > diffSince) { diffCommits.push(commit); } } catch (error) { reject(error); } }); commitStream.on('end', function() { resolve({ diffCommits: diffCommits }); }); }); } var ARObject = constructAggregate(); return when.try(snapshotter.loadSnapshot.bind(snapshotter), aggregateType, ARID).then(function _applySnapshot(snapshot){ // A snapshot has been found and loaded, so let the AR apply it to itself, according to its internal logic. return when.try(ARObject.applySnapshot.bind(ARObject), snapshot); }, function _snapshotNonexistent() { // This function intentionally does nothing. It simply turns a rejection from loadSnapshot() into a resolution. //TODO: Put an event-emitting statement here once we have logging support in place. }).then(rehydrateAggregate.bind(undefined, ARObject)).then(function(rehydrationResult) { if (options.advanced) { return { instance: ARObject, rehydration: rehydrationResult }; } else { return ARObject; } }); } /** * Create a closure that will subsequently load any AR, without specifying the event sink and snapshotter each time. * This function simply binds the two last arguments of the loader function to specified values and returns the bound function. * @param {module:esdf/interfaces/EventSinkInterface} eventSink The event sink to be used by the generated loader function. * @param {module:esdf/interfaces/AggregateSnapshotterInterface} [snapshotter] The snapshotter to be used. If not passed, aggregate loading will occur using only the event sink. * @returns {module:esdf/utils/loadAggregate~loadAggregate} * @throws {module:esdf/utils/loadAggregate~AggregateLoaderSinkNotGivenError} If an eventSink is not passed to the loader creation function. */ function createAggregateLoader(eventSink, snapshotter) { if(!eventSink){ throw new AggregateLoaderSinkNotGivenError(); } return function _boundAggregateLoader(ARConstructor, ARID, options) { return loadAggregate(ARConstructor, ARID, eventSink, snapshotter, options); }; } module.exports.loadAggregate = loadAggregate; module.exports.createAggregateLoader = createAggregateLoader;