UNPKG

bpmn-elements

Version:

Executable workflow elements based on BPMN 2.0

546 lines (455 loc) 15.1 kB
import DefinitionExecution from './DefinitionExecution.js'; import { DefinitionApi } from '../Api.js'; import { DefinitionBroker } from '../EventBroker.js'; import { getUniqueId, getOptionsAndCallback } from '../shared.js'; import { makeErrorFromMessage } from '../error/Errors.js'; import { cloneMessage, cloneContent } from '../messageHelper.js'; const kConsuming = Symbol.for('consuming'); const kCounters = Symbol.for('counters'); const kExec = Symbol.for('execution'); const kExecuteMessage = Symbol.for('executeMessage'); const kMessageHandlers = Symbol.for('messageHandlers'); const kStateMessage = Symbol.for('stateMessage'); const kStatus = Symbol.for('status'); const kStopped = Symbol.for('stopped'); export default Definition; export function Definition(context, options) { if (!(this instanceof Definition)) return new Definition(context, options); if (!context) throw new Error('No context'); const { id, name, type = 'definition' } = context; this.id = id; this.type = type; this.name = name; let environment; if (options) { environment = this.environment = context.environment.clone(options); this.context = context.clone(environment); } else { environment = this.environment = context.environment; this.context = context; } this[kCounters] = { completed: 0, discarded: 0, }; this[kStopped] = false; this[kExec] = new Map(); const onBrokerReturn = this._onBrokerReturnFn.bind(this); this[kMessageHandlers] = { onBrokerReturn, onApiMessage: this._onApiMessage.bind(this), onRunMessage: this._onRunMessage.bind(this), onExecutionMessage: this._onExecutionMessage.bind(this), }; const { broker, on, once, waitFor, emit, emitFatal } = DefinitionBroker(this, onBrokerReturn); this.broker = broker; this.on = on; this.once = once; this.waitFor = waitFor; this.emit = emit; this.emitFatal = emitFatal; this.logger = environment.Logger(type.toLowerCase()); } Object.defineProperties(Definition.prototype, { counters: { get() { return { ...this[kCounters] }; }, }, execution: { get() { return this[kExec].get('execution'); }, }, executionId: { get() { return this[kExec].get('executionId'); }, }, isRunning: { get() { if (!this[kConsuming]) return false; return !!this.status; }, }, status: { get() { return this[kStatus]; }, }, stopped: { get() { return this[kStopped]; }, }, activityStatus: { get() { const execution = this[kExec].get('execution'); return execution?.activityStatus || 'idle'; }, }, }); Definition.prototype.run = function run(optionsOrCallback, optionalCallback) { const [runOptions, callback] = getOptionsAndCallback(optionsOrCallback, optionalCallback); if (this.isRunning) { const err = new Error('definition is already running'); if (callback) return callback(err); throw err; } if (callback) { addConsumerCallbacks(this, callback); } const exec = this[kExec]; const executionId = getUniqueId(this.id); exec.set('executionId', executionId); const content = this._createMessage({ ...runOptions }); 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.logger.debug(`<${executionId} (${this.id})> run`); this._activateRunConsumers(); return this; }; Definition.prototype.resume = function resume(callback) { if (this.isRunning) { const err = new Error('cannot resume running definition'); if (callback) return callback(err); throw err; } this[kStopped] = false; if (!this.status) return this; if (callback) { addConsumerCallbacks(this, callback); } this.logger.debug(`<${this.executionId} (${this.id})> resume`); const content = this._createMessage(); this.broker.publish('run', 'run.resume', content, { persistent: false }); this._activateRunConsumers(); return this; }; Definition.prototype.getState = function getState() { return this._createMessage({ status: this.status, stopped: this.stopped, counters: this.counters, environment: this.environment.getState(), execution: this.execution?.getState(), broker: this.broker.getState(true), }); }; Definition.prototype.recover = function recover(state) { if (this.isRunning) throw new Error('cannot recover running definition'); if (!state) return this; this[kStopped] = !!state.stopped; this[kStatus] = state.status; const exec = this[kExec]; exec.set('executionId', state.executionId); if (state.counters) { this[kCounters] = { ...this[kCounters], ...state.counters }; } this.environment.recover(state.environment); if (state.execution) { exec.set('execution', new DefinitionExecution(this, this.context).recover(state.execution)); } this.broker.recover(state.broker); return this; }; Definition.prototype.shake = function shake(startId) { let result = {}; let bps; if (startId) { const startActivity = this.getActivityById(startId); if (!startActivity) return; const bp = this.getProcessById(startActivity.parent.id); if (!bp) return; bps = [bp]; } else bps = this.getProcesses(); bps.forEach((bp) => { result = { ...result, ...this._shakeProcess(bp, startId) }; }); return result; }; Definition.prototype._shakeProcess = function shakeProcess(shakeBp, startId) { let shovel; if (!shakeBp.isRunning) { shovel = shakeBp.broker.createShovel( 'shaker', { exchange: 'event', pattern: '*.shake#', }, { broker: this.broker, exchange: 'event', } ); } const shakeResult = shakeBp.shake(startId); if (shovel) shakeBp.broker.closeShovel('shaker'); return shakeResult; }; Definition.prototype.getProcesses = function getProcesses() { const execution = this.execution; if (execution) return execution.getProcesses(); return this.context.getProcesses(); }; Definition.prototype.getExecutableProcesses = function getExecutableProcesses() { const execution = this.execution; if (execution) return execution.getExecutableProcesses(); return this.context.getExecutableProcesses(); }; Definition.prototype.getRunningProcesses = function getRunningProcesses() { const execution = this.execution; if (!execution) return []; return execution.getRunningProcesses(); }; Definition.prototype.getProcessById = function getProcessById(processId) { return this.getProcesses().find((p) => p.id === processId); }; Definition.prototype.getActivityById = function getActivityById(childId) { const bps = this.getProcesses(); for (const bp of bps) { const child = bp.getActivityById(childId); if (child) return child; } return null; }; Definition.prototype.getElementById = function getElementById(elementId) { return this.context.getActivityById(elementId); }; Definition.prototype.getPostponed = function getPostponed(...args) { const execution = this.execution; if (!execution) return []; return execution.getPostponed(...args); }; Definition.prototype.getApi = function getApi(message) { const execution = this.execution; if (execution) return execution.getApi(message); message = message || this[kStateMessage]; if (!message) throw new Error('Definition is not running'); return DefinitionApi(this.broker, message); }; Definition.prototype.signal = function signal(message) { return this.getApi().signal(message, { delegate: true }); }; Definition.prototype.cancelActivity = function cancelActivity(message) { return this.getApi().cancel(message, { delegate: true }); }; Definition.prototype.sendMessage = function sendMessage(message) { const messageContent = { message }; let messageType = 'message'; const reference = message?.id && this.getElementById(message.id); if (reference?.resolve) { const resolvedReference = reference.resolve(this._createMessage({ message })); messageType = resolvedReference.messageType || messageType; messageContent.message = { ...message, ...resolvedReference }; } return this.getApi().sendApiMessage(messageType, messageContent, { delegate: true }); }; Definition.prototype.stop = function stop() { if (!this.isRunning) return; this.getApi().stop(); }; Definition.prototype._activateRunConsumers = function activateRunConsumers() { this[kConsuming] = true; const broker = this.broker; const { onApiMessage, onRunMessage } = this[kMessageHandlers]; broker.subscribeTmp('api', `definition.*.${this.executionId}`, onApiMessage, { noAck: true, consumerTag: '_definition-api', }); broker.getQueue('run-q').assertConsumer(onRunMessage, { exclusive: true, consumerTag: '_definition-run', }); }; Definition.prototype._deactivateRunConsumers = function deactivateRunConsumers() { const broker = this.broker; broker.cancel('_definition-api'); broker.cancel('_definition-run'); broker.cancel('_definition-execution'); this[kConsuming] = false; }; Definition.prototype._createMessage = function createMessage(override) { return { id: this.id, type: this.type, name: this.name, executionId: this.executionId, ...override, }; }; Definition.prototype._onRunMessage = function onRunMessage(routingKey, message) { const { content, fields } = message; if (routingKey === 'run.resume') { return this._onResumeMessage(message); } const exec = this[kExec]; this[kStateMessage] = message; switch (routingKey) { case 'run.enter': { this.logger.debug(`<${this.executionId} (${this.id})> enter`); this[kStatus] = 'entered'; if (fields.redelivered) break; exec.delete('execution'); this._publishEvent('enter', content); break; } case 'run.start': { this.logger.debug(`<${this.executionId} (${this.id})> start`); this[kStatus] = 'start'; this._publishEvent('start', content); break; } case 'run.execute': { 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: '_definition-execution', }); if (!execution) { execution = new DefinitionExecution(this, this.context); exec.set('execution', execution); } if (executeMessage.fields.redelivered) { this._publishEvent('resume', content); } return execution.execute(executeMessage); } case 'run.end': { if (this[kStatus] === 'end') break; this[kCounters].completed++; this.logger.debug(`<${this.executionId} (${this.id})> completed`); this[kStatus] = 'end'; this.broker.publish('run', 'run.leave', content); this._publishEvent('end', content); break; } case 'run.error': { this._publishEvent( 'error', { ...content, error: fields.redelivered ? makeErrorFromMessage(message) : content.error, }, { mandatory: true } ); break; } case 'run.discarded': { if (this[kStatus] === 'discarded') break; this[kCounters].discarded++; this[kStatus] = 'discarded'; this.broker.publish('run', 'run.leave', content); break; } case 'run.leave': { message.ack(); this[kStatus] = undefined; this._deactivateRunConsumers(); this._publishEvent('leave', this._createMessage()); return; } } message.ack(); }; Definition.prototype._onResumeMessage = function onResumeMessage(message) { message.ack(); const stateMessage = this[kStateMessage]; switch (stateMessage.fields.routingKey) { 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); }; Definition.prototype._onExecutionMessage = function onExecutionMessage(routingKey, message) { const { content, properties } = message; const messageType = 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; } default: { this.broker.publish('run', 'run.end', content); } } const executeMessage = this[kExecuteMessage]; this[kExecuteMessage] = null; executeMessage.ack(); }; Definition.prototype._onApiMessage = function onApiMessage(routingKey, message) { if (message.properties.type === 'stop') { const execution = this.execution; if (!execution || execution.completed) { this._onStop(); } } }; Definition.prototype._publishEvent = function publishEvent(action, content, msgOpts) { const execution = this.execution; this.broker.publish('event', `definition.${action}`, execution ? execution._createMessage(content) : cloneContent(content), { type: action, ...msgOpts, }); }; Definition.prototype._onStop = function onStop() { this[kStopped] = true; this._deactivateRunConsumers(); return this._publishEvent('stop', this._createMessage()); }; Definition.prototype._onBrokerReturnFn = function onBrokerReturn(message) { if (message.properties.type === 'error') { this._deactivateRunConsumers(); const err = makeErrorFromMessage(message); throw err; } }; Definition.prototype._reset = function reset() { this[kExec].delete('executionId'); this._deactivateRunConsumers(); this.broker.purgeQueue('run-q'); this.broker.purgeQueue('execution-q'); }; Definition.prototype._debug = function debug(msg) { this.logger.debug(`<${this.id}> ${msg}`); }; function addConsumerCallbacks(definition, callback) { const broker = definition.broker; clearConsumers(); broker.subscribeOnce('event', 'definition.stop', cbLeave, { consumerTag: '_definition-callback-stop' }); broker.subscribeOnce('event', 'definition.leave', cbLeave, { consumerTag: '_definition-callback-leave' }); broker.subscribeOnce('event', 'definition.error', cbError, { consumerTag: '_definition-callback-error' }); function cbLeave(_, message) { clearConsumers(); return callback(null, definition.getApi(message)); } function cbError(_, message) { clearConsumers(); definition._reset(); return callback(makeErrorFromMessage(message)); } function clearConsumers() { broker.cancel('_definition-callback-stop'); broker.cancel('_definition-callback-leave'); broker.cancel('_definition-callback-error'); } }