UNPKG

bpmn-elements

Version:

Executable workflow elements based on BPMN 2.0

457 lines (383 loc) 13 kB
import ProcessExecution from './ProcessExecution.js'; import { getUniqueId } from '../shared.js'; import { ProcessApi } from '../Api.js'; import { ProcessBroker } from '../EventBroker.js'; import { cloneMessage, cloneContent, cloneParent } from '../messageHelper.js'; import { makeErrorFromMessage } from '../error/Errors.js'; const kConsuming = Symbol.for('consuming'); const kCounters = Symbol.for('counters'); const kExec = Symbol.for('execution'); const kExecuteMessage = Symbol.for('executeMessage'); const kExtensions = Symbol.for('extensions'); const kLanes = Symbol.for('lanes'); const kMessageHandlers = Symbol.for('messageHandlers'); const kStateMessage = Symbol.for('stateMessage'); const kStatus = Symbol.for('status'); const kStopped = Symbol.for('stopped'); export default Process; export function Process(processDef, context) { const { id, type = 'process', name, parent, behaviour = {} } = processDef; this.id = id; this.type = type; this.name = name; this.parent = parent ? cloneParent(parent) : {}; this.behaviour = behaviour; const { isExecutable } = behaviour; this.isExecutable = isExecutable; const environment = (this.environment = context.environment); this.context = context; this[kCounters] = { completed: 0, discarded: 0, }; this[kConsuming] = false; this[kExec] = new Map(); this[kStatus] = undefined; this[kStopped] = false; const { broker, on, once, waitFor } = ProcessBroker(this); this.broker = broker; this.on = on; this.once = once; this.waitFor = waitFor; this[kMessageHandlers] = { onApiMessage: this._onApiMessage.bind(this), onRunMessage: this._onRunMessage.bind(this), onExecutionMessage: this._onExecutionMessage.bind(this), }; this.logger = environment.Logger(type.toLowerCase()); if (behaviour.lanes) { this[kLanes] = behaviour.lanes.map((lane) => new lane.Behaviour(this, lane)); } this[kExtensions] = context.loadExtensions(this); } Object.defineProperties(Process.prototype, { counters: { get() { return { ...this[kCounters] }; }, }, lanes: { get() { return this[kLanes]?.slice(); }, }, extensions: { get() { return this[kExtensions]; }, }, stopped: { get() { return this[kStopped]; }, }, isRunning: { get() { if (!this[kConsuming]) return false; return !!this.status; }, }, executionId: { get() { const exec = this[kExec]; return exec.get('executionId') || exec.get('initExecutionId'); }, }, execution: { get() { return this[kExec].get('execution'); }, }, status: { get() { return this[kStatus]; }, }, activityStatus: { get() { return this[kExec].get('execution')?.activityStatus || 'idle'; }, }, }); Process.prototype.init = function init(useAsExecutionId) { const initExecutionId = useAsExecutionId || getUniqueId(this.id); this[kExec].set('initExecutionId', initExecutionId); this._debug(`initialized with executionId <${initExecutionId}>`); this._publishEvent('init', this._createMessage({ executionId: initExecutionId })); }; Process.prototype.run = function run(runContent) { if (this.isRunning) throw new Error(`process <${this.id}> is already running`); const exec = this[kExec]; const executionId = exec.get('initExecutionId') || getUniqueId(this.id); exec.delete('initExecutionId'); exec.set('executionId', executionId); const content = this._createMessage({ ...runContent, executionId }); const broker = this.broker; broker.publish('run', 'run.enter', content); broker.publish('run', 'run.start', cloneContent(content)); broker.publish('run', 'run.execute', cloneContent(content)); this._activateRunConsumers(); }; Process.prototype.resume = function resume() { if (this.isRunning) throw new Error(`cannot resume running process <${this.id}>`); if (!this.status) return this; this[kStopped] = false; const content = this._createMessage(); this.broker.publish('run', 'run.resume', content, { persistent: false }); this._activateRunConsumers(); return this; }; Process.prototype.getState = function getState() { return { id: this.id, type: this.type, executionId: this.executionId, environment: this.environment.getState(), status: this.status, stopped: this.stopped, counters: this.counters, broker: this.broker.getState(true), execution: this.execution?.getState(), }; }; Process.prototype.recover = function recover(state) { if (this.isRunning) throw new Error(`cannot recover running process <${this.id}>`); if (!state) return this; this[kStopped] = !!state.stopped; this[kStatus] = state.status; const exec = this[kExec]; exec.set('executionId', state.executionId); this[kCounters] = { ...this[kCounters], ...state.counters }; this.environment.recover(state.environment); if (state.execution) { exec.set('execution', new ProcessExecution(this, this.context).recover(state.execution)); } this.broker.recover(state.broker); return this; }; Process.prototype.shake = function shake(startId) { if (this.isRunning) return this.execution.shake(startId); return new ProcessExecution(this, this.context).shake(startId); }; Process.prototype.stop = function stop() { if (!this.isRunning) return; this.getApi().stop(); }; Process.prototype.getApi = function getApi(message) { const execution = this.execution; if (execution) return execution.getApi(message); return ProcessApi(this.broker, message || this[kStateMessage]); }; Process.prototype.signal = function signal(message) { return this.getApi().signal(message, { delegate: true }); }; Process.prototype.cancelActivity = function cancelActivity(message) { return this.getApi().cancel(message, { delegate: true }); }; Process.prototype._activateRunConsumers = function activateRunConsumers() { this[kConsuming] = true; const broker = this.broker; const { onApiMessage, onRunMessage } = this[kMessageHandlers]; broker.subscribeTmp('api', `process.*.${this.executionId}`, onApiMessage, { noAck: true, consumerTag: '_process-api', priority: 100 }); broker.getQueue('run-q').assertConsumer(onRunMessage, { exclusive: true, consumerTag: '_process-run' }); }; Process.prototype._deactivateRunConsumers = function deactivateRunConsumers() { const broker = this.broker; broker.cancel('_process-api'); broker.cancel('_process-run'); broker.cancel('_process-execution'); this[kConsuming] = false; }; Process.prototype._onRunMessage = function onRunMessage(routingKey, message) { const { content, fields } = message; if (routingKey === 'run.resume') { return this._onResumeMessage(message); } this[kStateMessage] = message; switch (routingKey) { case 'run.enter': { this._debug('enter'); this[kStatus] = 'entered'; if (fields.redelivered) break; this[kExec].delete('execution'); this._publishEvent('enter', content); break; } case 'run.start': { this._debug('start'); this[kStatus] = 'start'; this._publishEvent('start', content); break; } case 'run.execute': { const exec = this[kExec]; this[kStatus] = 'executing'; const executeMessage = cloneMessage(message); let execution = exec.get('execution'); if (fields.redelivered && !execution) { executeMessage.fields.redelivered = undefined; } this[kExecuteMessage] = message; this.broker.getQueue('execution-q').assertConsumer(this[kMessageHandlers].onExecutionMessage, { exclusive: true, consumerTag: '_process-execution', }); execution = execution || new ProcessExecution(this, this.context); exec.set('execution', execution); return execution.execute(executeMessage); } case 'run.error': { this[kStatus] = 'errored'; this._publishEvent( 'error', cloneContent(content, { error: fields.redelivered ? makeErrorFromMessage(message) : content.error, }) ); break; } case 'run.end': { this[kStatus] = 'end'; if (fields.redelivered) break; this._debug('completed'); this[kCounters].completed++; this.broker.publish('run', 'run.leave', content); this._publishEvent('end', content); break; } case 'run.discarded': { this[kStatus] = 'discarded'; if (fields.redelivered) break; this[kCounters].discarded++; this.broker.publish('run', 'run.leave', content); this._publishEvent('discarded', content); break; } case 'run.leave': { this[kStatus] = undefined; message.ack(); this._deactivateRunConsumers(); const { output, ...rest } = content; this._publishEvent('leave', rest); return; } } message.ack(); }; Process.prototype._onResumeMessage = function onResumeMessage(message) { message.ack(); const stateMessage = this[kStateMessage]; switch (stateMessage.fields.routingKey) { case 'run.enter': case 'run.start': case 'run.discarded': case 'run.end': case 'run.leave': break; default: return; } if (!stateMessage.fields.redelivered) return; this._debug(`resume from ${this.status}`); return this.broker.publish('run', stateMessage.fields.routingKey, cloneContent(stateMessage.content), stateMessage.properties); }; Process.prototype._onExecutionMessage = function onExecutionMessage(routingKey, message) { const content = message.content; const messageType = message.properties.type; message.ack(); switch (messageType) { case 'stopped': { return this._onStop(); } case 'error': { this.broker.publish('run', 'run.error', content); this.broker.publish('run', 'run.discarded', content); break; } case 'discard': this.broker.publish('run', 'run.discarded', content); break; default: { this.broker.publish('run', 'run.end', content); } } const executeMessage = this[kExecuteMessage]; this[kExecuteMessage] = null; executeMessage.ack(); }; Process.prototype._publishEvent = function publishEvent(state, content) { const eventContent = this._createMessage({ ...content, state }); this.broker.publish('event', `process.${state}`, eventContent, { type: state, mandatory: state === 'error' }); }; Process.prototype.sendMessage = function sendMessage(message) { const messageContent = message?.content; if (!messageContent) return; let targetsFound = false; if (messageContent.target?.id && this.getActivityById(messageContent.target.id)) { targetsFound = true; } else if ( messageContent.message && this.getStartActivities({ referenceId: messageContent.message.id, referenceType: messageContent.message.messageType }).length ) { targetsFound = true; } if (!targetsFound) return; if (!this.status) this.run(); this.getApi().sendApiMessage(message.properties.type || 'message', cloneContent(messageContent), { delegate: true }); }; Process.prototype.getActivityById = function getActivityById(childId) { const execution = this.execution; if (execution) return execution.getActivityById(childId); return this.context.getActivityById(childId); }; Process.prototype.getActivities = function getActivities() { const execution = this.execution; if (execution) return execution.getActivities(); return this.context.getActivities(this.id); }; Process.prototype.getStartActivities = function getStartActivities(filterOptions) { return this.context.getStartActivities(filterOptions, this.id); }; Process.prototype.getSequenceFlows = function getSequenceFlows() { const execution = this.execution; if (execution) return execution.getSequenceFlows(); return this.context.getSequenceFlows(); }; Process.prototype.getLaneById = function getLaneById(laneId) { const lanes = this[kLanes]; if (!lanes) return; return lanes.find((lane) => lane.id === laneId); }; Process.prototype.getPostponed = function getPostponed(...args) { const execution = this.execution; if (!execution) return []; return execution.getPostponed(...args); }; Process.prototype._onApiMessage = function onApiMessage(routingKey, message) { const messageType = message.properties.type; switch (messageType) { case 'stop': { if (this.execution && !this.execution.completed) return; this._onStop(); break; } } }; Process.prototype._onStop = function onStop() { this[kStopped] = true; this._deactivateRunConsumers(); return this._publishEvent('stop'); }; Process.prototype._createMessage = function createMessage(override) { return { id: this.id, type: this.type, name: this.name, executionId: this.executionId, parent: { ...this.parent }, ...override, }; }; Process.prototype._debug = function debug(msg) { this.logger.debug(`<${this.id}> ${msg}`); };