bpmn-elements
Version:
Executable workflow elements based on BPMN 2.0
255 lines (216 loc) • 8.38 kB
JavaScript
import Activity from '../activity/Activity.js';
import EventDefinitionExecution from '../eventDefinitions/EventDefinitionExecution.js';
import { cloneContent, cloneMessage } from '../messageHelper.js';
import { brokerSafeId } from '../shared.js';
const kAttachedTags = Symbol.for('attachedConsumers');
const kCompleteContent = Symbol.for('completeContent');
const kExecuteMessage = Symbol.for('executeMessage');
const kExecution = Symbol.for('execution');
const kShovels = Symbol.for('shovels');
export default function BoundaryEvent(activityDef, context) {
return new Activity(BoundaryEventBehaviour, activityDef, context);
}
export function BoundaryEventBehaviour(activity) {
this.id = activity.id;
this.type = activity.type;
this.attachedTo = activity.attachedTo;
this.activity = activity;
this.environment = activity.environment;
this.broker = activity.broker;
this[kExecution] =
activity.eventDefinitions && new EventDefinitionExecution(activity, activity.eventDefinitions, 'execute.bound.completed');
this[kShovels] = new Set();
this[kAttachedTags] = new Set();
}
Object.defineProperties(BoundaryEventBehaviour.prototype, {
executionId: {
get() {
return this[kExecuteMessage]?.content.executionId;
},
},
cancelActivity: {
get() {
const behaviour = this.activity.behaviour || {};
return 'cancelActivity' in behaviour ? behaviour.cancelActivity : true;
},
},
});
BoundaryEventBehaviour.prototype.execute = function execute(executeMessage) {
const { isRootScope, executionId } = executeMessage.content;
const eventDefinitionExecution = this[kExecution];
if (isRootScope && executeMessage.content.id === this.id) {
this[kExecuteMessage] = executeMessage;
const broker = this.broker;
if (executeMessage.fields.routingKey === 'execute.bound.completed') {
this._stop();
return broker.publish('execution', 'execute.completed', executeMessage.content, executeMessage.properties);
}
const consumerTag = `_bound-listener-${executionId}`;
this.attachedTo.broker.subscribeTmp('event', 'activity.leave', this._onAttachedLeave.bind(this), {
noAck: true,
consumerTag,
priority: 300,
});
this[kAttachedTags].add(consumerTag);
broker.subscribeOnce('api', `activity.#.${executionId}`, this._onApiMessage.bind(this), {
consumerTag: `_api-${executionId}`,
});
const execQ = broker.assertQueue(`_bound-execution-${executionId}`, { durable: false, autoDelete: true });
broker.bindQueue(execQ.name, 'execution', 'execute.detach');
broker.bindQueue(execQ.name, 'execution', 'execute.bound.completed');
if (!this.cancelActivity) {
broker.bindQueue(execQ.name, 'execution', 'execute.repeat');
}
if (eventDefinitionExecution && !this.environment.settings.strict) {
broker.bindQueue(execQ.name, 'execution', 'execute.expect');
}
execQ.consume(this._onExecutionMessage.bind(this), { consumerTag: '_execution-tag' });
}
if (eventDefinitionExecution) {
return eventDefinitionExecution.execute(executeMessage);
}
};
BoundaryEventBehaviour.prototype._onExecutionMessage = function onExecutionMessage(routingKey, message) {
message.ack();
switch (routingKey) {
case 'execute.detach':
return this._onDetachMessage(routingKey, message);
case 'execute.bound.completed':
return this._onCompleted(routingKey, message);
case 'execute.repeat':
return this._onRepeatMessage(routingKey, message);
case 'execute.expect':
return this._onExpectMessage(routingKey, message);
}
};
BoundaryEventBehaviour.prototype._onCompleted = function onCompleted(_, { content }) {
if (content.cancelActivity === false || (!this.cancelActivity && !content.cancelActivity)) {
this._stop();
return this.broker.publish(
'execution',
'execute.completed',
cloneContent(content, { isDefinitionScope: false, cancelActivity: false })
);
}
this[kCompleteContent] = content;
const { inbound, executionId } = this[kExecuteMessage].content;
const attachedToContent = inbound?.[0];
const attachedTo = this.attachedTo;
this.activity.logger.debug(
`<${executionId} (${this.id})> cancel ${attachedTo.status} activity <${attachedToContent.executionId} (${attachedToContent.id})>`
);
if (content.isRecovered && !attachedTo.isRunning) {
const attachedExecuteTag = `_on-attached-execute-${executionId}`;
this[kAttachedTags].add(attachedExecuteTag);
attachedTo.broker.subscribeOnce(
'execution',
'#',
() => {
attachedTo.getApi({ content: attachedToContent }).discard();
},
{ consumerTag: attachedExecuteTag }
);
} else {
attachedTo.getApi({ content: attachedToContent }).discard();
}
};
BoundaryEventBehaviour.prototype._onAttachedLeave = function onAttachedLeave(_, { content }) {
if (content.id !== this.attachedTo.id) return;
this._stop();
const completeContent = this[kCompleteContent];
if (!completeContent) return this.broker.publish('execution', 'execute.discard', this[kExecuteMessage].content);
return this.broker.publish('execution', 'execute.completed', cloneContent(completeContent));
};
BoundaryEventBehaviour.prototype._onExpectMessage = function onExpectMessage(_, { content }) {
const { executionId, expectRoutingKey, pattern, exchange } = content;
const attachedTo = this.attachedTo;
const errorConsumerTag = `_bound-error-listener-${executionId}`;
this[kAttachedTags].add(errorConsumerTag);
attachedTo.broker.subscribeTmp(
'event',
pattern,
(__, message) => {
if (message.content.id !== attachedTo.id) return;
this.broker.publish(exchange, expectRoutingKey, cloneContent(message.content, { attachedTo: attachedTo.id }), {
...message.properties,
mandatory: false,
});
},
{
noAck: true,
consumerTag: errorConsumerTag,
priority: 400,
}
);
};
BoundaryEventBehaviour.prototype._onDetachMessage = function onDetachMessage(_, message) {
const content = message.content;
const { executionId, parent } = this[kExecuteMessage].content;
const id = this.id,
attachedTo = this.attachedTo;
this.activity.logger.debug(`<${executionId} (${id})> detach from activity <${attachedTo.id}>`);
this._stop(true);
const { executionId: detachId, bindExchange, sourceExchange, sourcePattern } = content;
const shovelName = `_detached-${brokerSafeId(id)}_${detachId}`;
this[kShovels].add(shovelName);
const broker = this.broker;
attachedTo.broker.createShovel(
shovelName,
{
exchange: sourceExchange,
pattern: sourcePattern,
},
{
broker,
exchange: bindExchange,
},
{
cloneMessage,
}
);
const detachContent = cloneContent(content, {
executionId,
});
detachContent.parent = parent;
this.activity.removeInboundListeners();
broker.publish('event', 'activity.detach', detachContent);
broker.subscribeOnce(
'execution',
'execute.bound.completed',
(__, { content: completeContent }) => {
this._stop();
this.broker.publish('execution', 'execute.completed', cloneContent(completeContent));
},
{
consumerTag: `_execution-completed-${executionId}`,
}
);
};
BoundaryEventBehaviour.prototype._onApiMessage = function onApiMessage(_, message) {
switch (message.properties.type) {
case 'discard':
case 'stop':
this._stop();
break;
}
};
BoundaryEventBehaviour.prototype._onRepeatMessage = function onRepeatMessage(_, message) {
const executeMessage = this[kExecuteMessage];
const repeat = message.content.repeat;
this.broker
.getQueue('inbound-q')
.queueMessage({ routingKey: 'activity.restart' }, cloneContent(executeMessage.content.inbound[0], { repeat }));
};
BoundaryEventBehaviour.prototype._stop = function stop(detach) {
const attachedTo = this.attachedTo,
broker = this.broker,
executionId = this.executionId;
for (const tag of this[kAttachedTags]) attachedTo.broker.cancel(tag);
this[kAttachedTags].clear();
for (const shovelName of this[kShovels]) attachedTo.broker.closeShovel(shovelName);
this[kShovels].clear();
broker.cancel('_execution-tag');
broker.cancel(`_execution-completed-${executionId}`);
if (detach) return;
broker.cancel(`_api-${executionId}`);
};