@fabrix/spool-broadcast
Version:
Spool: broadcast for Fabrix to implement CQRS and Event Sourcing
961 lines • 81.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const common_1 = require("@fabrix/fabrix/dist/common");
const lodash_1 = require("lodash");
const regexdot_1 = require("@fabrix/regexdot");
const schemas_1 = require("./schemas");
const BroadcastHook_1 = require("./BroadcastHook");
const BroadcastPipeline_1 = require("./BroadcastPipeline");
const BroadcastChannel_1 = require("./BroadcastChannel");
const BroadcastProjector_1 = require("./BroadcastProjector");
const BroadcastProcesser_1 = require("./BroadcastProcesser");
const BroadcastDispatcher_1 = require("./BroadcastDispatcher");
const BroadcastCommand_1 = require("./BroadcastCommand");
const utils_1 = require("./utils");
class Broadcast extends common_1.FabrixGeneric {
constructor(app) {
super(app);
this._trace = false;
this.lifecycles = ['before', 'after'];
this.lifespans = ['ephemeral', 'eternal'];
this.consistencies = ['strong', 'eventual'];
this.processing = ['serial', 'parallel'];
this._pipelines = new Map();
this._pipes = new Map();
this._runners = new Map();
this._hooks = new Map();
this._commands = new Map();
this._handlers = new Map();
this._processors = new Map();
this._projectors = new Map();
this._dispatchers = new Map();
this._events = new Map();
this._managers = new Map();
this._channels = new Map();
this._subscribers = new Map();
this._brokers = new Map();
this._trace = this.app.config.get(`broadcast.broadcasters.${this.constructor.name}.trace`) || false;
}
get name() {
return this.constructor.name;
}
get trace() {
return this._trace;
}
subscribe(command, req = {}, body = {}, options = {}) {
if (!this._pipes.has(command)) {
throw new Error(`${command} is not a valid pipeline for ${this.name}`);
}
return new BroadcastPipeline_1.PipelineEmitter(this.app, {
command,
broadcaster: this,
pipeline: this._pipes.get(command),
runner: this._runners.get(command),
req,
body,
options
});
}
process(managers, ...args) {
return this.app.broadcastSeries(...args)
.catch(err => {
this.app.log.err('Fatal BroadcastProcess', err);
return Promise.reject(err);
});
}
reverseProcess(managers, ...args) {
return this.app.broadcastSeries(...args)
.catch(err => {
this.app.log.err('Fatal BroadcastProcess', err);
return Promise.reject(err);
});
}
createCommand(command, options) {
return new BroadcastCommand_1.BroadcastCommand(this.app, this, command, options);
}
updateCommand(command, options) {
return new BroadcastCommand_1.BroadcastCommand(this.app, this, Object.assign({ correlation_uuid: command.command_uuid }, command), options);
}
buildEvent({ command, event_type, correlation_uuid, correlation_pattern, correlation_pattern_raw, correlation_type, explain, causation_uuid, chain_before, chain_saga, chain_after, chain_events }) {
if (!event_type) {
throw new Error('Broadcast.buildEvent missing event_type');
}
if (!correlation_uuid) {
throw new Error('Broadcast.buildEvent missing correlation_uuid');
}
if (!(command instanceof BroadcastCommand_1.BroadcastCommand)) {
throw new Error('Broadcast.buildEvent called with a non Command object');
}
const { pattern, keys } = regexdot_1.regexdot(event_type);
const pattern_raw = event_type;
event_type = this.replaceParams(event_type, keys, command.data, command.data_previous);
const data = Object.assign({ correlation_uuid: correlation_uuid || command.command_uuid, correlation_pattern: correlation_pattern || command.pattern, correlation_pattern_raw: correlation_pattern_raw || command.pattern_raw, correlation_type: correlation_type || command.command_type, causation_uuid: causation_uuid || command.causation_uuid, explain: explain || command.explain, chain_before: chain_before || command.chain_before, chain_saga: chain_saga || command.chain_saga, chain_after: chain_after || command.chain_after, chain_events: chain_events || command.chain_events }, command.toEVENT(), { event_type, pattern: pattern, pattern_raw: pattern_raw });
return this.app.models.BroadcastEvent.stage(data, {
isNewRecord: true,
configure: ['generateUUID']
});
}
buildProjection({ event, object, data, metadata, options }) {
if (!event.event_type) {
throw new Error('Broadcast.buildProjection missing event_type');
}
if (!(event instanceof this.app.models.BroadcastEvent.instance)) {
throw new Error('Broadcast.buildProjection called with a non Event object');
}
const __event = event.toJSON();
const _event = {
correlation_uuid: __event.correlation_uuid,
correlation_pattern: __event.correlation_pattern,
correlation_pattern_raw: __event.correlation_pattern_raw,
correlation_type: __event.correlation_type,
causation_uuid: __event.causation_uuid,
explain: __event.explain,
chain_before: __event.chain_before,
chain_saga: __event.chain_saga,
chain_after: __event.chain_after,
chain_events: __event.chain_events,
event_type: __event.event_type,
pattern: __event.pattern,
pattern_raw: __event.pattern_raw,
object: object || __event.object,
data: data || __event.data,
metadata: metadata || __event.metadata,
is_projection: true,
is_dispatch: false
};
return this.app.models.BroadcastEvent.stage(_event, {
isNewRecord: event._options.isNewRecord || options.isNewRecord,
configure: ['generateUUID']
});
}
replaceParams(type = '', keys = [], object = {}, previous = {}) {
if (keys !== false
&& typeof keys !== 'boolean'
&& lodash_1.isArray(keys)) {
if (!lodash_1.isArray(object)) {
keys.forEach(k => {
if (k && object && typeof object[k] !== 'undefined' && object[k] !== null) {
type = type.replace(`:${k}`, `${object[k]}`);
}
else if (k && previous && typeof previous[k] !== 'undefined' && previous[k] !== null) {
type = type.replace(`:${k}`, `${previous[k]}`);
}
});
}
else if (lodash_1.isArray(object)) {
const o = object[0];
const p = previous[0];
keys.forEach(k => {
if (k && o && typeof o[k] !== 'undefined' && o[k] !== null) {
type = type.replace(`:${k}`, `${o[k]}`);
}
else if (k && p && typeof p[k] !== 'undefined' && p[k] !== null) {
type = type.replace(`:${k}`, `${p[k]}`);
}
});
}
}
return type;
}
_cleanObj(obj) {
const cleanObj = lodash_1.isArray(obj)
? obj.map(d => d.toJSON ? d.toJSON() : d)
: obj.toJSON
? obj.toJSON()
: obj;
return JSON.parse(JSON.stringify(cleanObj));
}
validate(validators = [], command, options) {
const _value = this._cleanObj(command.data);
return Promise.all(validators.map((v, k) => {
const validator = Object.values(v)[0];
const name = Object.keys(v)[0];
this.app.log.debug(`validating ${command.command_type} with ${name} schema`);
return validator(_value);
}))
.then((data) => {
return [command, options];
})
.catch(error => {
this.app.log.debug(`validating error ${command.command_type}`);
const err = this.app.transformJoiError({ value: _value, error });
return Promise.reject(err);
});
}
before(command, options, validators) {
if (!(command instanceof BroadcastCommand_1.BroadcastCommand)) {
throw new this.app.errors.GenericError('E_FAILED_DEPENDENCY', 'command is not an instance of Command', `${this.name} Broadcast Error`);
}
return this.beforeCommand(command, options, validators)
.catch((err) => {
this.app.log.error(`${this.name} Failure before ${command.command_type} - fatal`, err);
return [command, options];
});
}
after(command, options, validators) {
if (!(command instanceof BroadcastCommand_1.BroadcastCommand)) {
throw new Error('command is not an instance of Command');
}
return this.afterCommand(command, options, validators)
.catch((err) => {
this.app.log.error(`${this.name} Failure after ${command.command_type} - fatal`, err);
return [command, options];
});
}
beforeCommand(command, options, validators) {
const beforeHooks = this.getBeforeHooks(command.command_type);
const beforeHandlers = this.getBeforeHandlers(command.command_type);
if (!beforeHooks || !beforeHandlers) {
const err = new this.app.errors.GenericError('E_PRECONDITION_REQUIRED', 'Before Commands/Handlers are not defined', `${this.name} Broadcast Error`);
return Promise.reject(err);
}
return this.runBefore(beforeHooks, beforeHandlers, command, options, validators);
}
afterCommand(command, options, validators) {
const afterHooks = this.getAfterHooks(command.command_type);
const afterHandlers = this.getAfterHandlers(command.command_type);
if (!afterHooks || !afterHandlers) {
const err = new this.app.errors.GenericError('E_PRECONDITION_REQUIRED', 'After Commands/Handler are not defined', `${this.name} Broadcast Error`);
return Promise.reject(err);
}
return this.runAfter(afterHooks, afterHandlers, command, options, validators);
}
runBefore(beforeCommands, beforeHandlers, command, options, validators) {
let breakException;
const beforeCommandsAsc = new Map([...beforeCommands.entries()].sort((a, b) => {
return beforeHandlers
.get(a[0]).priority - beforeHandlers.get(b[0]).priority;
}));
const slog = [];
beforeCommandsAsc.forEach((v, k) => slog.push(k));
this.app.log.debug(`${this.app.config.get('broadcast.profile')}:`, `Broadcaster ${this.name} running`, `${beforeCommandsAsc.size} before hooks for command ${command.command_type}`, `-> ${slog.map(k => k).join(' -> ')}`);
const promises = Array.from(beforeCommandsAsc.entries());
options.trace = options.trace ? options.trace : new Map();
return this.process(beforeHandlers, promises, ([m, p], i) => {
if (breakException) {
return Promise.reject(breakException);
}
const handler = beforeHandlers.get(m);
if (this.trace) {
options.trace.set(`${handler.pattern_raw}::${handler.type}::${m}`, Object.assign({ handler: m }, handler));
}
if (!p || typeof p !== 'function') {
this.app.log.error(`${this.name}: ${m} attempted to call a non function ${p}! - returning`);
return [command, options];
}
const hookstart = process.hrtime();
return p({
command,
options,
lifecycle: 'before',
handler: handler,
broadcaster: this
})
.run()
.then(([_command, _options]) => {
if (!_command) {
throw new this.app.errors.GenericError('E_FAILED_DEPENDENCY', `${this.name}: ${p.name} Hook returned invalid response for ${m}! - fatal`, `${this.name} Broadcast Error`);
}
if (!(_command instanceof BroadcastCommand_1.BroadcastCommand)) {
throw new this.app.errors.GenericError('E_FAILED_DEPENDENCY', `${this.name}: ${p.name} Hook returned a Command instead of an Command for ${m}! - fatal`, `${this.name} Broadcast Error`);
}
if (typeof _command.action !== 'undefined' && _command.action === 'retry') {
this.app.log.warn(`${this.name}: ${p.name} BRK currently unhandled retry action for ${m}`);
return [command, options];
}
if (typeof _command.action !== 'undefined' && _command.action === false) {
this.app.log.debug(`${m} to continue without data`);
return [command, options];
}
return this.validate(validators, _command, options)
.catch(err => {
this.app.log.error(`${this.name}: Before Handler ${m} failed validation for ${command.command_type}`, err);
return Promise.reject(err);
});
})
.then(([_command, _options]) => {
command.chain_before.push(m);
if (handler && handler.merge && handler.merge !== false) {
command.mergeData(m, handler, _command);
}
if (handler && handler.push && handler.push !== false) {
command.pushOn(m, handler, _command);
}
if (handler && handler.zip && handler.zip !== false) {
command.zip(m, handler, _command);
}
if (handler && handler.include && handler.include !== false) {
command.includeOn(m, handler, _command);
this.app.log.debug(`${this.name}: Before Handler ${m} included data on ${handler.include}`);
}
if (handler.data) {
command.handleData(m, handler, _command);
}
if (handler.metadata) {
command.handleMetadata(m, handler, _command);
}
command.complete = true;
const hookend = process.hrtime(hookstart);
this.app.log.debug(`${this.name}.${m}: ${_command.command_type} Before Hook Execution time (hr): ${hookend[0]}s ${hookend[1] / 1000000}ms`);
if (options && options.pipeline) {
options.pipeline.subprogress(`${_command.command_type}::before`, i + 1, beforeCommandsAsc.size, m);
}
if (options && options.parent && options.parent.pipeline) {
options.parent.pipeline.subprogress(`${_command.command_type}::before`, i + 1, beforeCommandsAsc.size, m);
}
return [command, options];
})
.catch(err => {
command.breakException = breakException = err;
this.app.log.error(`${p.name} threw an error - fatal`, err);
return Promise.reject(err);
});
})
.then(results => [command, options]);
}
runAfter(afterCommands, afterHandlers, command, options, validators) {
let breakException;
const afterCommandsAsc = new Map([...afterCommands.entries()]
.sort((a, b) => {
return afterHandlers
.get(a[0]).priority - afterHandlers.get(b[0]).priority;
}));
const slog = [];
afterCommandsAsc.forEach((v, k) => slog.push(k));
this.app.log.debug(`${this.app.config.get('broadcast.profile')}:`, `Broadcaster ${this.name} running`, `${afterCommandsAsc.size} after hooks for command ${command.command_type}`, `-> ${slog.map(k => k).join(' -> ')}`);
const promises = Array.from(afterCommandsAsc.entries());
options.trace = options.trace ? options.trace : new Map();
return this.process(afterHandlers, promises, ([m, p], i) => {
if (breakException) {
return Promise.reject(breakException);
}
const handler = afterHandlers.get(m);
if (this.trace) {
options.trace.set(`${handler.pattern_raw}::${handler.type}::${m}`, Object.assign({ handler: m }, handler));
}
if (!p || typeof p !== 'function') {
this.app.log.error(`${m} attempted to call a non function ${p}! - returning`);
return [command, options];
}
const hookstart = process.hrtime();
return p({
command,
options,
lifecycle: 'after',
handler: handler,
broadcaster: this
})
.run()
.then(([_command, _options]) => {
if (!_command) {
throw new this.app.errors.GenericError('E_FAILED_DEPENDENCY', `${p.name} Hook returned invalid response for ${m}! - fatal`, `${this.name} Broadcast Error`);
}
if (!(_command instanceof BroadcastCommand_1.BroadcastCommand)) {
throw new this.app.errors.GenericError('E_FAILED_DEPENDENCY', `${p.name} Hook returned a Command instead of an Command for ${m}! - fatal`, `${this.name} Broadcast Error`);
}
if (typeof _command.action !== 'undefined' && _command.action === 'retry') {
this.app.log.warn(`${p.name} BRK unhandled retry action for ${m}`);
return [command, options];
}
if (typeof _command.action !== 'undefined' && _command.action === false) {
this.app.log.debug(`${m} to continue without data`);
return [command, options];
}
return this.validate(validators, _command, options)
.catch(err => {
this.app.log.error(`After Handler ${m} failed validation for ${command.command_type}`, err);
return Promise.reject(err);
});
})
.then(([_command, _options]) => {
command.chain_after.push(m);
if (handler && handler.merge && handler.merge !== false) {
command.mergeData(m, handler, _command);
}
if (handler && handler.push && handler.push !== false) {
command.pushOn(m, handler, _command);
}
if (handler && handler.zip && handler.zip !== false) {
command.zip(m, handler, _command);
}
if (handler && handler.include && handler.include !== false) {
command.includeOn(m, handler, _command);
this.app.log.debug(`${this.name}: After Handler ${m} included data on ${handler.include}`);
}
if (handler.data) {
command.handleData(m, handler, _command);
}
if (handler.metadata) {
command.handleMetadata(m, handler, _command);
}
command.complete = true;
const hookend = process.hrtime(hookstart);
this.app.log.debug(`${this.name}.${m}: ${_command.command_type} After Hook Execution time (hr): ${hookend[0]}s ${hookend[1] / 1000000}ms`);
if (options && options.pipeline) {
options.pipeline.subprogress(`${_command.command_type}::after`, i + 1, afterCommandsAsc.size, m);
}
if (options && options.parent && options.parent.pipeline) {
options.parent.pipeline.subprogress(`${_command.command_type}::after`, i + 1, afterCommandsAsc.size, m);
}
return [command, options];
})
.catch(err => {
command.breakException = breakException = err;
this.app.log.error(`${p.name} threw an error - fatal`, err);
return Promise.reject(err);
});
})
.then(results => [command, options]);
}
notify(event, options) {
const subscribers = this.getSubscribers(event.event_type);
const brokers = this.getBrokers(event.event_type);
if (!subscribers || !brokers) {
const err = new this.app.errors.GenericError('E_FAILED_DEPENDENCY', 'Eternal Subscribers/Brokers or Ephemeral Subscribers/Brokers are not defined', `${this.name} Broadcast Error`);
return Promise.reject(err);
}
const topLevelTransaction = this.unnestTransaction(options);
const elog = [];
subscribers.forEach((v, k) => elog.push(k));
if (topLevelTransaction
&& !topLevelTransaction.finished) {
topLevelTransaction.afterCommit((transaction) => {
this.app.log.debug(`${this.app.config.get('broadcast.profile')}:`, `Broadcaster ${this.name}`, `${subscribers.size} subscribers will be notified for event ${event.event_type}`, `after transaction commit ${topLevelTransaction.id}`, `: ${elog.map(k => k).join(' -> ')}`);
return this.notifySubscribers(subscribers, brokers, event, options);
});
return [event, options];
}
else {
return this.notifySubscribers(subscribers, brokers, event, options);
}
}
notifySubscribers(subscribers, brokers, event, options) {
let breakException;
const subscribersAsc = new Map([...subscribers.entries()].sort((a, b) => {
return brokers.get(a[0]).priority - brokers.get(b[0]).priority;
}));
const slog = [];
subscribersAsc.forEach((v, k) => slog.push(k));
this.app.log.debug(`${this.app.config.get('broadcast.profile')}:`, `Broadcaster ${this.name} notifying`, `${subscribersAsc.size} for event ${event.event_type}`, `: ${slog.map(k => k).join(' -> ')}`);
const promises = Array.from(subscribersAsc.entries());
return this.process(brokers, promises, ([m, p], k) => {
if (breakException) {
return Promise.reject(breakException);
}
const broker = brokers.get(m);
if (broker && broker.expects_input) {
if (typeof broker.expects_input === 'string'
&& broker.expects_input !== '*'
&& event.getDataValue('object') !== broker.expects_input) {
throw new this.app.errors.GenericError('E_PRECONDITION_FAILED', `${m} subscriber expects_input ${broker.expects_input}
but got ${event.getDataValue('object')} for ${event.event_type}`, `${this.name} Broadcast Error`);
}
else if (Array.isArray(broker.expects_input)
&& !broker.expects_input.includes(event.getDataValue('object') || '*')) {
throw new this.app.errors.GenericError('E_PRECONDITION_FAILED', `${m} subscriber expects_input one of ${broker.expects_input.join(', ')}
but got ${event.getDataValue('object')} for ${event.event_type}`, `${this.name} Broadcast Error`);
}
}
else {
this.app.log.debug(`${event.event_type} broker ${m} subscriber assuming it expects_input ${event.getDataValue('object')}`);
}
if (!p || typeof p !== 'function') {
this.app.log.warn(`${m} attempted to call a non function ${p}! - returning`);
return [event, options];
}
const notifystart = process.hrtime();
return p({
event,
options,
lifespan: broker.lifespan,
broker: broker,
broadcaster: this
})
.run()
.then((_p) => {
return _p.publish();
})
.then(() => {
const notifyend = process.hrtime(notifystart);
this.app.log.debug(`${this.name}.${m}: ${event.event_type} Execution time (hr): ${notifyend[0]}s ${notifyend[1] / 1000000}ms`);
if (broker.lifespan === 'ephemeral') {
this.app.log.debug('removing ephemeral subscriber', m, 'for event', event.event_type);
this.removeSubscriber({ event_type: event.event_type, lifespan: 'ephemeral', name: m });
}
return [event, options];
})
.catch(err => {
breakException = err;
this.app.log.error(`${p.name} threw an error - fatal`, err);
return Promise.reject(err);
});
})
.then(results => [event, options]);
}
broadcast(event, options) {
if (!(event instanceof this.app.models.BroadcastEvent.instance)) {
throw new this.app.errors.GenericError('E_FAILED_DEPENDENCY', 'event is not an instance of BroadcastEvent', `${this.name} Broadcast Error`);
}
if (typeof event.object.toBinaryData === 'undefined') {
throw new this.app.errors.GenericError('E_FAILED_DEPENDENCY', 'Data object does not have a toBinaryData function', `${this.name} Broadcast Error`);
}
if (typeof event.object.toBinaryMetadata === 'undefined') {
throw new this.app.errors.GenericError('E_FAILED_DEPENDENCY', 'Data object does not have a toBinaryMetadata function', `${this.name} Broadcast Error`);
}
return this.project(event, options)
.then(([_event, _options]) => {
if (options.save !== false) {
return event.save(options)
.then(() => {
return [event, options];
});
}
else if (options.transaction) {
return [event, options];
}
else {
return [event, options];
}
})
.catch(err => {
this.app.log.error(`Unhandled failure while running ${event.event_type}`, err);
return [event, options];
})
.then(([_e, _o]) => {
if (!(_e instanceof this.app.models.BroadcastEvent.instance)) {
throw new this.app.errors.GenericError('E_UNPROCESSABLE_ENTITY', `${event.event_type} Projection returned an instance different than BroadcastEvent! - fatal`, `${this.name} Broadcast Error`);
}
if (_e.event_uuid !== event.event_uuid) {
throw new this.app.errors.GenericError('E_UNPROCESSABLE_ENTITY', `${event.event_type} Projection returned a different event uuid than origin - fatal`, `${this.name} Broadcast Error`);
}
if (_e.event_type !== event.event_type) {
throw new this.app.errors.GenericError('E_UNPROCESSABLE_ENTITY', `${event.event_type} Projection returned a different event type than origin - fatal`, `${this.name} Broadcast Error`);
}
return [event, options];
})
.then(([_event, _options]) => this.notify(_event, _options))
.catch(err => {
this.app.log.error(err);
return Promise.reject(new this.app.errors.GenericError('E_UNPROCESSABLE_ENTITY', `BroadcastEvent: ${event.event_type} failed during broadcast - fatal`, `${this.name} Broadcast Error`));
});
}
bulkBroadcast(events = [], options) {
return this.process(new Map(), events, e => this.broadcast(e, options));
}
unnestTransaction(options) {
if (options && options.parent && options.parent.transaction) {
if (options.transaction) {
this.app.log.debug('transaction', options.transaction.id, 'will await', options.parent.transaction.id);
}
return this.unnestTransaction(options.parent);
}
else if (options.transaction) {
this.app.log.debug('awaiting transaction', options.transaction.id);
return options.transaction;
}
else {
return null;
}
}
unnestTraceChildren(children, parent) {
let trace = new Map();
children.forEach((v, k, map) => {
trace.set(`${parent ? parent + '->' : ''}${k}`, v);
if (v && v.children) {
trace = new Map([...trace, ...this.unnestTraceChildren(v.children, k)]);
}
});
return trace;
}
unnestTraceParent(parent, caller) {
let trace = new Map([...(parent && parent.trace ? parent.trace : new Map())]);
return trace;
}
unnestTrace(options) {
let trace = new Map([...(options && options.trace ? options.trace : new Map())]);
trace = new Map([...this.unnestTraceChildren(trace)]);
if (options && options.parent && options.parent.trace) {
const parent = this.unnestTraceParent(options.parent, options);
trace = new Map([...parent, ...trace]);
}
return trace;
}
flattenTraceChildren(children) {
let trace = new Set();
children.forEach((v, k, map) => {
trace.add(k);
if (v && v.children) {
const childStart = this.flattenTraceChildren(v.children);
trace = new Set([...trace, ...childStart]);
}
});
return trace;
}
flattenTrace(options) {
if (!this.trace) {
this.app.log.warn(`Trace is disabled for ${this.name}`, `Set config: broadcast.broadcasters.${this.name}.trace to true to enable`);
}
const start = this.unnestTrace(options);
return this.flattenTraceChildren(start);
}
project(event, options) {
const strong = this.getStrongEvents(event.event_type);
const strongManagers = this.getStrongManagers(event.event_type);
const eventual = this.getEventualEvents(event.event_type);
const eventualManagers = this.getEventualManagers(event.event_type);
if (!strong || !strongManagers || !eventual || !eventualManagers) {
const err = new this.app.errors.GenericError('E_FAILED_DEPENDENCY', 'Strong Events/Managers or Eventual Events/Manager are not defined', `${this.name} Broadcast Error`);
return Promise.reject(err);
}
if (!options.transaction) {
this.app.log.warn(`${this.name} broadcasting ${event.event_type} without a transaction!`);
}
options.trace = options.trace ? options.trace : new Map();
return this.projectStrong(strong, strongManagers, event, options)
.then(([_event, _options]) => {
const elog = [];
eventual.forEach((v, k) => elog.push(k));
Array.from(eventualManagers.keys()).forEach(e => {
const manager = eventualManagers.get(e);
if (this.trace) {
options.trace.set(`${manager.pattern_raw}::${manager.type}::${e}`, Object.assign({ handler: e }, manager));
}
event.chain_events.push(e);
});
const topLevelTransaction = this.unnestTransaction(options);
if (topLevelTransaction
&& !topLevelTransaction.finished
&& eventual.size > 0) {
topLevelTransaction.afterCommit((transaction) => {
this.app.log.debug(`${this.app.config.get('broadcast.profile')}:`, `Broadcaster ${this.name} broadcasting`, `${eventual.size} eventual for event ${_event.event_type}`, `after transaction commit ${topLevelTransaction.id}`, `: ${elog.map(k => k).join(' -> ')}`);
return this.publishEventual(eventual, eventualManagers, _event, _options)
.then(results => [_event, _options])
.catch(err => {
this.app.log.error(`Unhandled: ${event.event_type} Broadcast.project.publishEventual after commit`, err);
throw new Error(err);
});
});
return [_event, _options];
}
else if (eventual.size > 0) {
this.app.log.debug(`Broadcaster ${this.name} broadcasting on independent transaction`, `${eventual.size} eventual for event ${_event.event_type}`, `: ${elog.map(k => k).join(' -> ')}`);
return this.publishEventual(eventual, eventualManagers, _event, _options)
.then(results => [_event, _options])
.catch(err => {
this.app.log.error('Broadcast.project.publishEventual independent transaction', err);
throw new this.app.errors.GenericError(err);
});
}
else {
return [_event, _options];
}
});
}
projectStrong(strongEvents, strongManagers, event, options) {
let breakException;
const strongEventsAsc = new Map([...strongEvents.entries()].sort((a, b) => {
return strongManagers.get(a[0]).priority - strongManagers.get(b[0]).priority;
}));
const slog = [];
strongEventsAsc.forEach((v, k) => slog.push(k));
this.app.log.debug(`Broadcaster ${this.name} broadcasting`, `${strongEventsAsc.size} strong for event ${event.event_type}`, `: ${slog.map(k => k).join(' -> ')}`);
const promises = Array.from(strongEventsAsc.entries());
return this.process(strongManagers, promises, ([m, p], i) => {
if (breakException) {
return Promise.reject(breakException);
}
const manager = strongManagers.get(m);
if (this.trace) {
options.trace.set(`${manager.pattern_raw}::${manager.type}::${m}`, Object.assign({ handler: m }, manager));
}
if (manager && manager.expects_input) {
if (typeof manager.expects_input === 'string'
&& manager.expects_input !== '*'
&& String(event.getDataValue('object')) !== manager.expects_input) {
throw new this.app.errors.GenericError('E_PRECONDITION_FAILED', `${m} ${manager.type} expects_input ${manager.expects_input}
but got ${event.getDataValue('object')} for ${event.event_type}`, `${this.name} Broadcast Error`);
}
else if (Array.isArray(manager.expects_input)
&& !manager.expects_input.includes(event.getDataValue('object') || '*')) {
throw new this.app.errors.GenericError('E_PRECONDITION_FAILED', `${m} ${manager.type} expects one of ${manager.expects_input.join(', ')} as input,
but got ${event.getDataValue('object')} for ${event.event_type}`, `${this.name} Broadcast Error`);
}
}
else {
this.app.log.debug(`${event.event_type} manager ${m} ${manager.type}
assuming it expects_input ${event.getDataValue('object')}`);
}
if (!p || typeof p !== 'function') {
this.app.log.warn(`${this.name} ${m} attempted to call a non function ${p}! Unhandled - returning without running`);
return [event, options];
}
return this.run(event, options, p, m, manager, breakException)
.then(([e, o]) => {
if (options && options.pipeline) {
options.pipeline.subprogress(`${event.event_type}::strong`, i + 1, strongEventsAsc.size, m);
}
if (options && options.parent && options.parent.pipeline) {
options.parent.pipeline.subprogress(`${event.event_type}::strong`, i + 1, strongEventsAsc.size, m);
}
return [e, o];
});
})
.then(results => {
return [event, options];
});
}
run(event, options, p, m, manager, breakException) {
const projectstart = process.hrtime();
let projectend = process.hrtime(projectstart);
let t = `${manager ? manager.type : 'unknown'}`;
return p({
event,
options,
consistency: manager.consistency || 'strong',
message: null,
manager: manager,
broadcaster: this
})
.run()
.catch(err => {
this.app.log.error(`${p.name}`, err);
if (manager.consistency === 'eventual') {
return Promise.reject(err);
}
else {
return [{ action: 'retry' }, options];
}
})
.then(([_event, _options]) => {
p.isAcknowledged = true;
if (!_event) {
throw new this.app.errors.GenericError('E_FAILED_DEPENDENCY', `${p.name} Projection returned no event for ${m}! - fatal`, `${this.name} Broadcast Error`);
}
else if (!lodash_1.isArray(_event)) {
if (!_event) {
throw new this.app.errors.GenericError('E_NOT_ACCEPTABLE', `${p.name} Projection returned invalid response for ${m}! - fatal`, `${this.name} Broadcast Error`);
}
if (_event instanceof BroadcastCommand_1.BroadcastCommand) {
throw new this.app.errors.GenericError('E_NOT_ACCEPTABLE', `${p.name} Projection returned a Command instead of an Event for ${m}! - fatal`, `${this.name} Broadcast Error`);
}
if (_event.action && _event.action === 'retry') {
this.app.log.error(`${this.name} ${p.name} BRK unhandled retry action for ${m}`);
projectend = process.hrtime(projectstart);
this.app.log.debug(`${this.name}.${m}: ${event.event_type} ${t} Execution time (hr): ${projectend[0]}s ${projectend[1] / 1000000}ms`);
return [event, options];
}
if (_event.action === false) {
this.app.log.debug(`${this.name} ${m} to continue without data`);
projectend = process.hrtime(projectstart);
this.app.log.debug(`${this.name}.${m}: ${event.event_type} ${t} Execution time (hr): ${projectend[0]}s ${projectend[1] / 1000000}ms`);
return [event, options];
}
}
else {
_event.forEach(_e => {
if (!_e) {
throw new this.app.errors.GenericError('E_UNPROCESSABLE_ENTITY', `${p.name} Projection returned invalid response for ${m}! - fatal`, `${this.name} Broadcast Error`);
}
if (_e[0] instanceof BroadcastCommand_1.BroadcastCommand) {
throw new this.app.errors.GenericError('E_NOT_ACCEPTABLE', `${p.name} Projection returned a Command instead of an Event for ${m}! - fatal`, `${this.name} Broadcast Error`);
}
});
if (_event.length === 0) {
this.app.log.warn(`${this.name} ${p.name} returned no actions for ${m} in list of ${_event.length} events`);
return [event, options];
}
else if (_event.every(_e => _e[0]
&& typeof _e[0].action !== 'undefined'
&& _e[0].action === 'retry')) {
this.app.log.error(`${this.name} ${p.name} unhandled retry action for ${m} in list of ${_event.length} events`);
projectend = process.hrtime(projectstart);
this.app.log.debug(`${this.name}.${m}: ${lodash_1.isArray(_event)
? _event.map(e => e.event_type)
: _event.event_type} ${t} Execution time (hr): ${projectend[0]}s ${projectend[1] / 1000000}ms`);
return [event, options];
}
else if (_event.every(_e => _e[0]
&& typeof _e[0].action !== 'undefined'
&& _e[0].action === false)) {
this.app.log.debug(`${m} to continue without data in list of ${_event.length} events`);
projectend = process.hrtime(projectstart);
this.app.log.debug(`${this.name}.${m}: ${lodash_1.isArray(_event)
? _event.map(e => e.event_type)
: _event.event_type} ${t} Execution time (hr): ${projectend[0]}s ${projectend[1] / 1000000}ms`);
return [event, options];
}
else {
}
}
if (m) {
event.chain_events.push(m);
}
this.app.log.silly(this.name, m, 'current chain_events', event.chain_events);
if (manager && manager.merge && manager.merge !== false) {
event.mergeData(m, manager, _event);
}
if (manager && manager.push && manager.push !== false) {
event.pushOn(m, manager, _event);
}
if (manager && manager.zip && manager.zip !== false) {
event.zip(m, manager, _event);
}
if (manager && manager.include && manager.include !== false) {
event.includeOn(m, manager, _event);
}
if (manager.data) {
event.handleData(m, manager, _event);
}
if (manager.metadata) {
event.handleMetadata(m, manager, _event);
}
options.trace = options.trace || new Map();
options.children = options.children || [];
if (t === 'processor') {
if (this.trace) {
const parent = options.trace.get(`${manager.pattern_raw}::${manager.type}::${m}`, manager);
const trace = lodash_1.isArray(_options) ? _options.map(_o => _o.trace) : _options.trace;
parent.children = new Map([...(parent.children || new Map()), ...(trace || new Map())]);
options.trace.set(`${manager.pattern_raw}::${manager.type}::${m}`, Object.assign({ handler: m }, parent));
}
if (lodash_1.isArray(_options)) {
_options.forEach(_o => {
options.children.push({
transaction: _o.transaction,
useMaster: _o.useMaster,
trace: _o.trace ? _o.trace : new Map()
});
});
}
else {
options.children.push({
transaction: _options.transaction,
useMaster: _options.useMaster,
trace: _options.trace ? _options.trace : new Map()
});
}
}
if (t === 'dispatcher') {
}
projectend = process.hrtime(projectstart);
this.app.log.debug(`${this.name}.${m}: ${lodash_1.isArray(_event)
? _event.map(e => e.event_type)
: _event.event_type} ${t}Execution time (hr): ${projectend[0]}s ${projectend[1] / 1000000}ms`);
return [event, options];
})
.catch(err => {
breakException = err;
this.app.log.error(`${this.name} ${p.name} threw an error - fatal`, err);
return Promise.reject(err);
});
}
publishEventual(eventualEvents, eventualManagers, event, options) {
const patterns = lodash_1.uniqBy(Array.from(eventualManagers.values())
.map((v) => {
return v;
})
.filter(v => v.pattern_raw), 'pattern_raw');
patterns.forEach(manager => {
this.app.log.debug('Broadcaster', this.name, 'publishing pattern', manager.pattern_raw);
});
return this.app.broadcastSeries(patterns, (manager) => {
return this.app.broadcaster.publish({
broadcaster: this,
event_type: manager.pattern_raw,
event: event,
options: options,
consistency: 'eventual',
manager: manager
})
.catch(err => {
this.app.log.error(`Broadcast ${this.name} Publishing Error`, err);
return [event, options];
});
})
.then((res = []) => {
this.app.log.silly(`Broadcast ${this.name} Published Results:`);
res.forEach(([e, o]) => {
this.app.log.silly(e);
this.app.log.silly(o);
});
return [event, options];
});
}
runEventual(client, eventualEvents, eventualManagers, message) {
const eventualstart = process.hrtime();
const eventualEventsAsc = new Map([...eventualEvents.entries()].sort((a, b) => {
return eventualManagers.get(a[0]).priority - eventualManagers.get(b[0]).priority;
}));
const slog = [];
const events = Array.from(eventualEventsAsc.entries());
let breakException;
eventualEventsAsc.forEach((v, k) => slog.push(k));
this.app.log.debug(`${this.app.config.get('broadcast.profile')}:`, `Broadcaster ${this.name} running`, `${eventualEventsAsc.size} eventual for event ${message.type}`, `: ${slog.map(k => k).join(' -> ')}`);
return this.app.models.BroadcastEvent.sequelize.transaction({
isolationLevel: this.app.spools.sequelize._datastore.Transaction.ISOLATION_LEVELS.READ_UNCOMMITTED,
deferrable: this.app.spools.sequelize._datastore.Deferrable.SET_DEFERRED
}, t => {
const options = {
transaction: t,
trace: new Map()
};
return this.process(eventualManagers, events, ([key, projector]) => {
if (breakException) {
return Promise.reject(breakException);
}
const manager = eventualManagers.get(key);
if (manager.type === 'processor') {
return this.runEventualProcessor(client, projector, key, manager, message, options, breakException);
}
else if (manager.type === 'dispatcher') {
return this.runEventualDispatcher(client, projector, key, manager, message, options, breakException);
}
else {
return this.runEventualProjector(client, projector, key, manager, message, options, breakException);
}
});
})
.then(res => {
message.ack();
const eventualend = process.hrtime(eventualstart);
this.app.log.debug(`${message.type} Eventual Execution time (hr): ${eventualend[0]}s ${eventualend[1] / 1000000}ms`);
return res;
})
.catch(err => {
this.app.log.error(`Utils.registerEventualListeners ${message.type} err - fatal`, err);
message.nack();
return Promise.reject(err);
});
}
runEventualProcessor(client, project, key, manager, message, options, breakException) {
let consumerWork = client.messenger.configurations.default.queues
.find(d => d.name === (this.app.config.get('broadcast.connection.work_queue_name') || 'broadcasts-work-q'));
let consumerInterrupt = client.messenger.configurations.default.queues
.find(d => d.name === (this.app.config.get('broadcast.connection.interrupt_queue_name') || 'broadcasts-interrupt-q'));
let consumerPoison = client.messenger.configurations.default.queues
.find(d => d.name === (this.app.config.get('broadcast.connection.poison_queue_name') || 'broadcasts-poison-q'));
if (message.fields.redelivered) {
this.app.log.warn('Rabbit Message', message.type, 'was redelivered!', message);
}
const event = this.app.models.BroadcastEvent.stage(message.body, {
isNewRecord: false
});
if (this.trace) {
options.trace.set(`${manager.pattern_raw}::${manager.type}::${key}`, Object.assign({ handler: key }, manager));
}
return this.run(event, options, project, key, manager, breakException)
.then(([_event, _options]) => {
project.isAcknowledged = true;
this.app.log.debug(`Consumer Processor ${consumerWork.consumerTag}:`, event.event_type, 'broadcasted from', this.name, '->', project.name, '->', key);
return [_event, _options];
});
}
runEventualDispatcher(client, dispatch, key, manager, message, options, breakException) {
let consumerWork = client.messenger.configurations.default.queues
.find(d => d.name === (this.app.config.get('broadcast.connection.work_queue_name') || 'broadcasts-work-q'));
let consumerInterrupt = client.messenger.configurations.default.queues
.find(d => d.name === (this.app.config.get('broadcast.connection.interrupt_queue_name') || 'broadcasts-interrupt-q'));
let consumerPoison = client.messenger.configurations.default.queues
.find(d => d.name === (this.app.config.get('broadcast.connection.poison_queue_name') || 'broadcasts-poison-q'));
if (message.fields.redelivered) {
this.app.log.warn('Rabbit Message', message.type, 'was redelivered!',