UNPKG

@fabrix/spool-broadcast

Version:

Spool: broadcast for Fabrix to implement CQRS and Event Sourcing

961 lines 81.3 kB
"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!',