bpmn-elements
Version:
Executable workflow elements based on BPMN 2.0
150 lines (126 loc) • 4.58 kB
JavaScript
import { cloneMessage } from './messageHelper.js';
import { getUniqueId } from './shared.js';
import { ActivityError } from './error/Errors.js';
import { getRoutingKeyPattern } from 'smqp';
const kOnMessage = Symbol.for('onMessage');
const kExecution = Symbol.for('execution');
const EXEC_ROUTING_KEY = 'run._formatting.exec';
/**
* Message formatter used to enrich an element run message before continuing to the next run message
* @param {import('types').ElementBase} element
*/
export function Formatter(element) {
const { id, broker, logger } = element;
this.id = id;
this.broker = broker;
this.logger = logger;
this[kOnMessage] = this._onMessage.bind(this);
}
/**
* Format message
* @param {import('types').ElementBrokerMessage} message
* @param {CallableFunction} callback
*/
Formatter.prototype.format = function format(message, callback) {
const correlationId = (this._runId = getUniqueId(message.fields.routingKey));
const consumerTag = '_formatter-' + correlationId;
const broker = this.broker;
broker.publish('format', EXEC_ROUTING_KEY, {}, { correlationId, persistent: false });
this[kExecution] = {
correlationId,
formatKey: message.fields.routingKey,
runMessage: cloneMessage(message),
callback,
pending: new Set(),
formatted: false,
executeMessage: null,
};
broker.consume('format-run-q', this[kOnMessage], {
consumerTag,
prefetch: 100,
});
};
Formatter.prototype._onMessage = function onMessage(routingKey, message) {
const { formatKey, correlationId, pending, executeMessage } = this[kExecution];
const asyncFormatting = pending.size;
if (routingKey === EXEC_ROUTING_KEY) {
if (message.properties.correlationId !== correlationId) return message.ack();
message.ack();
if (!asyncFormatting) {
return this._complete(message);
}
this[kExecution].executeMessage = message;
} else {
message.ack();
const endRoutingKey = message.content?.endRoutingKey;
if (endRoutingKey) {
this._enrich(message.content);
pending.add(message);
return this._debug(`start formatting ${formatKey} message content with formatter ${routingKey}`);
}
if (asyncFormatting) {
const isError = this._popFormatStart(pending, routingKey).isError;
if (isError) {
return this._complete(message, true);
}
}
this._enrich(message.content);
this._debug(`format ${message.fields.routingKey} message content with formatter ${routingKey}`);
if (executeMessage && !pending.size) {
this._complete(message);
}
}
};
Formatter.prototype._complete = function complete(message, isError) {
const { runMessage, formatKey, callback, formatted, executeMessage } = this[kExecution];
this[kExecution] = null;
if (executeMessage) executeMessage.ack();
this.broker.cancel(message.fields.consumerTag);
if (isError) {
const error = message.content?.error || new Error('formatting failed');
const errMessage = error.message || 'formatting failed';
this._debug(`formatting of ${formatKey} failed with ${message.fields.routingKey}: ${errMessage}`);
return callback(new ActivityError(errMessage, cloneMessage(runMessage), error));
}
return callback(null, runMessage.content, formatted);
};
Formatter.prototype._enrich = function enrich(withContent) {
const content = this[kExecution].runMessage.content;
for (const key in withContent) {
switch (key) {
case 'id':
case 'type':
case 'parent':
case 'attachedTo':
case 'executionId':
case 'isSubProcess':
case 'isMultiInstance':
case 'inbound':
case 'outbound':
case 'endRoutingKey':
case 'errorRoutingKey':
break;
default: {
content[key] = withContent[key];
this[kExecution].formatted = true;
}
}
}
};
Formatter.prototype._popFormatStart = function popFormattingStart(pending, routingKey) {
for (const msg of pending) {
const { endRoutingKey, errorRoutingKey = '#.error' } = msg.content;
if (endRoutingKey && getRoutingKeyPattern(endRoutingKey).test(routingKey)) {
this._debug(`completed formatting ${msg.fields.routingKey} message content with formatter ${routingKey}`);
pending.delete(msg);
return { message: msg };
} else if (getRoutingKeyPattern(errorRoutingKey).test(routingKey)) {
pending.delete(msg);
return { isError: true, message: msg };
}
}
return {};
};
Formatter.prototype._debug = function debug(msg) {
this.logger.debug(`<${this.id}> ${msg}`);
};