UNPKG

bpmn-elements

Version:

Executable workflow elements based on BPMN 2.0

706 lines (705 loc) 23.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = DefinitionExecution; var _getPropertyValue = _interopRequireDefault(require("../getPropertyValue.js")); var _Api = require("../Api.js"); var _shared = require("../shared.js"); var _messageHelper = require("../messageHelper.js"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } const kActivated = Symbol.for('activated'); const kProcessesQ = Symbol.for('processesQ'); const kCompleted = Symbol.for('completed'); const kExecuteMessage = Symbol.for('executeMessage'); const kMessageHandlers = Symbol.for('messageHandlers'); const kParent = Symbol.for('definition'); const kProcesses = Symbol.for('processes'); const kStatus = Symbol.for('status'); const kStopped = Symbol.for('stopped'); function DefinitionExecution(definition, context) { const broker = definition.broker; this[kParent] = definition; this.id = definition.id; this.type = definition.type; this.broker = broker; const environment = this.environment = definition.environment; this.context = context; const processes = context.getProcesses(); const ids = new Set(); const executable = new Set(); for (const bp of processes) { bp.environment.assignVariables(environment.variables); bp.environment.assignSettings(environment.settings); ids.add(bp.id); if (bp.isExecutable) executable.add(bp); } this[kProcesses] = { processes, ids, executable, running: new Set(), postponed: new Set() }; broker.assertExchange('execution', 'topic', { autoDelete: false, durable: true }); this.executionId = undefined; this[kCompleted] = false; this[kStopped] = false; this[kActivated] = false; this[kStatus] = 'init'; this[kProcessesQ] = undefined; this[kMessageHandlers] = { onApiMessage: this._onApiMessage.bind(this), onCallActivity: this._onCallActivity.bind(this), onCancelCallActivity: this._onCancelCallActivity.bind(this), onChildEvent: this._onChildEvent.bind(this), onDelegateMessage: this._onDelegateMessage.bind(this), onMessageOutbound: this._onMessageOutbound.bind(this), onProcessMessage: this._onProcessMessage.bind(this) }; } Object.defineProperties(DefinitionExecution.prototype, { stopped: { get() { return this[kStopped]; } }, completed: { get() { return this[kCompleted]; } }, status: { get() { return this[kStatus]; } }, processes: { get() { return [...this[kProcesses].running]; } }, postponedCount: { get() { return this[kProcesses].postponed.size; } }, isRunning: { get() { return this[kActivated]; } }, activityStatus: { get() { let status = 'idle'; const running = this[kProcesses].running; if (!running.size) return status; for (const bp of running) { const bpStatus = bp.activityStatus; switch (bp.activityStatus) { case 'idle': break; case 'executing': return bpStatus; case 'timer': status = bpStatus; break; case 'wait': if (status === 'idle') status = bpStatus; break; } } return status; } } }); DefinitionExecution.prototype.execute = function execute(executeMessage) { if (!executeMessage) throw new Error('Definition execution requires message'); const content = executeMessage.content; const executionId = this.executionId = content.executionId; if (!executionId) throw new Error('Definition execution requires execution id'); this[kExecuteMessage] = (0, _messageHelper.cloneMessage)(executeMessage, { executionId, state: 'start' }); this[kStopped] = false; this[kProcessesQ] = this.broker.assertQueue(`execute-${executionId}-q`, { durable: true, autoDelete: false }); if (executeMessage.fields.redelivered) { return this.resume(); } const { running, executable } = this[kProcesses]; if (content.processId) { const startWithProcess = this.getProcessById(content.processId); if (startWithProcess) { executable.clear(); executable.add(startWithProcess); } } this._debug('execute definition'); for (const bp of executable) { running.add(bp); } this._activate(executable); this._start(); return true; }; DefinitionExecution.prototype.resume = function resume() { this._debug(`resume ${this[kStatus]} definition execution`); if (this[kCompleted]) return this._complete('completed'); const { running, postponed } = this[kProcesses]; this._activate(running); postponed.clear(); this[kProcessesQ].consume(this[kMessageHandlers].onProcessMessage, { prefetch: 1000, consumerTag: `_definition-activity-${this.executionId}` }); if (this[kCompleted]) return; for (const bp of running) bp.resume(); }; DefinitionExecution.prototype.recover = function recover(state) { if (!state) return this; this.executionId = state.executionId; this[kStopped] = state.stopped; this[kCompleted] = state.completed; this[kStatus] = state.status; this._debug(`recover ${this[kStatus]} definition execution`); const running = this[kProcesses].running; running.clear(); const ids = new Set(); for (const bpState of state.processes) { const bpid = bpState.id; let bp; if (ids.has(bpid)) { bp = this.context.getNewProcessById(bpid); } else { bp = this.getProcessById(bpid); } if (!bp) continue; ids.add(bpid); bp.recover(bpState); running.add(bp); } return this; }; DefinitionExecution.prototype.stop = function stop() { this.getApi().stop(); }; DefinitionExecution.prototype.getProcesses = function getProcesses() { const { running, processes } = this[kProcesses]; const result = [...running]; for (const bp of processes) { if (!result.find(runningBp => bp.id === runningBp.id)) result.push(bp); } return result; }; DefinitionExecution.prototype.getProcessById = function getProcessById(processId) { return this.getProcesses().find(bp => bp.id === processId); }; DefinitionExecution.prototype.getProcessesById = function getProcessesById(processId) { return this.getProcesses().filter(bp => bp.id === processId); }; DefinitionExecution.prototype.getProcessByExecutionId = function getProcessByExecutionId(processExecutionId) { for (const bp of this[kProcesses].running) { if (bp.executionId === processExecutionId) return bp; } }; DefinitionExecution.prototype.getRunningProcesses = function getRunningProcesses() { return [...this[kProcesses].running].filter(bp => bp.executionId); }; DefinitionExecution.prototype.getExecutableProcesses = function getExecutableProcesses() { return [...this[kProcesses].executable]; }; DefinitionExecution.prototype.getState = function getState() { const processes = []; for (const bp of this[kProcesses].running) { processes.push(bp.getState()); } return { executionId: this.executionId, stopped: this[kStopped], completed: this[kCompleted], status: this[kStatus], processes }; }; DefinitionExecution.prototype.getApi = function getApi(apiMessage) { if (!apiMessage) apiMessage = this[kExecuteMessage] || { content: this._createMessage() }; const content = apiMessage.content; if (content.executionId !== this.executionId) { return this._getProcessApi(apiMessage); } const api = (0, _Api.DefinitionApi)(this.broker, apiMessage); const postponed = this[kProcesses].postponed; const self = this; api.getExecuting = function getExecuting() { const apis = []; for (const msg of postponed) { const bpApi = self._getProcessApi(msg); if (bpApi) apis.push(bpApi); } return apis; }; return api; }; DefinitionExecution.prototype.getPostponed = function getPostponed(...args) { let result = []; for (const bp of this[kProcesses].running) { result = result.concat(bp.getPostponed(...args)); } return result; }; DefinitionExecution.prototype._start = function start() { const { ids, executable, postponed } = this[kProcesses]; if (!ids.size) { return this._complete('completed'); } if (!executable.size) { return this._complete('error', { error: new Error('No executable process') }); } this[kStatus] = 'start'; for (const bp of executable) bp.init(); for (const bp of executable) bp.run(); postponed.clear(); this[kProcessesQ].assertConsumer(this[kMessageHandlers].onProcessMessage, { prefetch: 1000, consumerTag: `_definition-activity-${this.executionId}` }); }; DefinitionExecution.prototype._activate = function activate(processList) { this.broker.subscribeTmp('api', '#', this[kMessageHandlers].onApiMessage, { noAck: true, consumerTag: '_definition-api-consumer' }); for (const bp of processList) this._activateProcess(bp); this[kActivated] = true; }; DefinitionExecution.prototype._activateProcess = function activateProcess(bp) { const handlers = this[kMessageHandlers]; const broker = bp.broker; broker.subscribeTmp('message', 'message.outbound', handlers.onMessageOutbound, { noAck: true, consumerTag: '_definition-outbound-message-consumer' }); const delegateEventQ = broker.assertQueue('_delegate-event-q', { autoDelete: false, durable: false }); delegateEventQ.consume(handlers.onDelegateMessage, { noAck: true, consumerTag: '_definition-signal-consumer' }); broker.bindQueue('_delegate-event-q', 'event', 'activity.signal', { priority: 200 }); broker.bindQueue('_delegate-event-q', 'event', 'activity.message', { priority: 200 }); broker.subscribeTmp('event', 'activity.call', handlers.onCallActivity, { noAck: true, consumerTag: '_definition-call-consumer', priority: 200 }); broker.subscribeTmp('event', 'activity.call.cancel', handlers.onCancelCallActivity, { noAck: true, consumerTag: '_definition-call-cancel-consumer', priority: 200 }); broker.subscribeTmp('event', '#', handlers.onChildEvent, { noAck: true, consumerTag: '_definition-activity-consumer', priority: 100 }); }; DefinitionExecution.prototype._onChildEvent = function onChildEvent(routingKey, originalMessage) { const message = (0, _messageHelper.cloneMessage)(originalMessage); const content = message.content; const parent = content.parent = content.parent || {}; const isDirectChild = this[kProcesses].ids.has(content.id); if (isDirectChild) { parent.executionId = this.executionId; } else { content.parent = (0, _messageHelper.pushParent)(parent, this); } this.broker.publish('event', routingKey, content, { ...message.properties, mandatory: false }); if (!isDirectChild) return; this[kProcessesQ].queueMessage(message.fields, (0, _messageHelper.cloneContent)(content), message.properties); }; DefinitionExecution.prototype._deactivate = function deactivate() { this.broker.cancel('_definition-api-consumer'); this.broker.cancel(`_definition-activity-${this.executionId}`); for (const bp of this[kProcesses].running) this._deactivateProcess(bp); this[kActivated] = false; }; DefinitionExecution.prototype._deactivateProcess = function deactivateProcess(bp) { bp.broker.cancel('_definition-outbound-message-consumer'); bp.broker.cancel('_definition-activity-consumer'); bp.broker.cancel('_definition-signal-consumer'); bp.broker.cancel('_definition-call-consumer'); bp.broker.cancel('_definition-call-cancel-consumer'); }; DefinitionExecution.prototype._onProcessMessage = function onProcessMessage(routingKey, message) { const content = message.content; const isRedelivered = message.fields.redelivered; const { id: childId, inbound } = content; if (isRedelivered && message.properties.persistent === false) return; switch (routingKey) { case 'execution.stop': { message.ack(); return this._onStopped(message); } case 'process.leave': { return this._onProcessCompleted(message); } } this._stateChangeMessage(message, true); switch (routingKey) { case 'process.enter': this[kStatus] = 'executing'; break; case 'process.discarded': { if (inbound?.length) { const calledFrom = inbound[0]; this._getProcessApi({ content: calledFrom }).cancel({ executionId: calledFrom.executionId }); } break; } case 'process.end': { if (inbound?.length) { const calledFrom = inbound[0]; this._getProcessApi({ content: calledFrom }).signal({ executionId: calledFrom.executionId, output: { ...content.output } }); } else { Object.assign(this.environment.output, content.output); } break; } case 'process.error': { if (inbound?.length) { const calledFrom = inbound[0]; this._getProcessApi({ content: calledFrom }).sendApiMessage('error', { executionId: calledFrom.executionId, error: content.error }, { mandatory: true, type: 'error' }); } else { for (const bp of new Set(this[kProcesses].running)) { if (bp.id !== childId) bp.stop(); } Object.assign(this.environment.output, content.output); this._complete('error', { error: content.error }); } break; } } }; DefinitionExecution.prototype._stateChangeMessage = function stateChangeMessage(message, postponeMessage) { let previousMsg; const postponed = this[kProcesses].postponed; for (const msg of postponed) { if (msg.content.executionId === message.content.executionId) { previousMsg = msg; postponed.delete(msg); break; } } if (previousMsg) previousMsg.ack(); if (postponeMessage) postponed.add(message); }; DefinitionExecution.prototype._onProcessCompleted = function onProcessCompleted(message) { this._stateChangeMessage(message, false); if (message.fields.redelivered) return message.ack(); const { id, executionId, type, inbound } = message.content; message.ack(); this._debug(`left <${executionId} (${id})> (${type}), pending runs ${this.postponedCount}`); if (inbound?.length) { const bp = this._removeProcessByExecutionId(executionId); this._deactivateProcess(bp); } if (!this.postponedCount) { this._complete('completed'); } }; DefinitionExecution.prototype._onStopped = function onStopped(message) { const running = this[kProcesses].running; this._debug(`stop definition execution (stop process executions ${running.size})`); this[kProcessesQ].close(); for (const bp of new Set(running)) bp.stop(); this._deactivate(); this[kStopped] = true; return this.broker.publish('execution', `execution.stopped.${this.executionId}`, (0, _messageHelper.cloneContent)(this[kExecuteMessage].content, { ...message.content }), { type: 'stopped', persistent: false }); }; DefinitionExecution.prototype._onApiMessage = function onApiMessage(routingKey, message) { const messageType = message.properties.type; const delegate = message.properties.delegate; if (delegate && this.id === message.content.id) { const referenceId = (0, _getPropertyValue.default)(message, 'content.message.id'); this._startProcessesByMessage({ referenceId, referenceType: messageType }); } if (delegate) { for (const bp of new Set(this[kProcesses].running)) { bp.broker.publish('api', routingKey, (0, _messageHelper.cloneContent)(message.content), message.properties); } } if (this.executionId !== message.content.executionId) return; if (messageType === 'stop') { this[kProcessesQ].queueMessage({ routingKey: 'execution.stop' }, (0, _messageHelper.cloneContent)(message.content), { persistent: false }); } }; DefinitionExecution.prototype._startProcessesByMessage = function startProcessesByMessage(reference) { const { processes: bps, running } = this[kProcesses]; if (bps.length < 2) return; for (const bp of bps) { if (bp.isExecutable) continue; if (!bp.getStartActivities(reference).length) continue; if (!bp.executionId) { this._debug(`start <${bp.id}> by <${reference.referenceId}> (${reference.referenceType})`); this._activateProcess(bp); running.add(bp); bp.init(); bp.run(); if (reference.referenceType === 'message') return; continue; } this._debug(`start new <${bp.id}> by <${reference.referenceId}> (${reference.referenceType})`); const targetProcess = this.context.getNewProcessById(bp.id); this._activateProcess(targetProcess); running.add(targetProcess); targetProcess.init(); targetProcess.run(); if (reference.referenceType === 'message') return; } }; DefinitionExecution.prototype._onMessageOutbound = function onMessageOutbound(routingKey, message) { const content = message.content; const { target, source } = content; this._debug(`conveying message from <${source.processId}.${source.id}> to`, target.id ? `<${target.processId}.${target.id}>` : `<${target.processId}>`); const targetProcesses = this.getProcessesById(target.processId); if (!targetProcesses.length) return; let targetProcess, found; for (const bp of targetProcesses) { if (!bp.executionId) { targetProcess = bp; continue; } bp.sendMessage(message); found = true; } if (found) return; targetProcess = targetProcess || this.context.getNewProcessById(target.processId); this._activateProcess(targetProcess); this[kProcesses].running.add(targetProcess); targetProcess.init(); targetProcess.run(); targetProcess.sendMessage(message); }; DefinitionExecution.prototype._onCallActivity = function onCallActivity(routingKey, message) { const content = message.content; const { calledElement, id: fromId, executionId: fromExecutionId, name: fromName, parent: fromParent } = content; if (!calledElement) return; const bpExecutionId = `${(0, _shared.brokerSafeId)(calledElement)}_${fromExecutionId}`; if (content.isRecovered) { if (this.getProcessByExecutionId(bpExecutionId)) return; } const targetProcess = this.context.getNewProcessById(calledElement, { settings: { calledFrom: (0, _messageHelper.cloneContent)({ id: fromId, name: fromName, executionId: content.executionId, parent: content.parent }) } }); if (!targetProcess) return; this._debug(`call from <${fromParent.id}.${fromId}> to <${calledElement}>`); this._activateProcess(targetProcess); this[kProcesses].running.add(targetProcess); targetProcess.init(bpExecutionId); targetProcess.run({ inbound: [(0, _messageHelper.cloneContent)(content)] }); }; DefinitionExecution.prototype._onCancelCallActivity = function onCancelCallActivity(routingKey, message) { const { calledElement, id: fromId, executionId: fromExecutionId, parent: fromParent } = message.content; if (!calledElement) return; const bpExecutionId = `${(0, _shared.brokerSafeId)(calledElement)}_${fromExecutionId}`; const targetProcess = this.getProcessByExecutionId(bpExecutionId); if (!targetProcess) return; this._debug(`cancel call from <${fromParent.id}.${fromId}> to <${calledElement}>`); if (!targetProcess.isRunning) { targetProcess.getApi({ content: { id: targetProcess.id, executionId: targetProcess.executionId } }).discard(); } else { targetProcess.getApi().discard(); } }; DefinitionExecution.prototype._onDelegateMessage = function onDelegateMessage(routingKey, executeMessage) { const content = executeMessage.content; const messageType = executeMessage.properties.type; const delegateMessage = executeMessage.content.message; const reference = this.context.getActivityById(delegateMessage.id); const message = reference?.resolve(executeMessage); this._debug(`<${reference ? `${messageType} ${delegateMessage.id}>` : `anonymous ${messageType}`} event received from <${content.parent.id}.${content.id}>. Delegating.`); this.getApi().sendApiMessage(messageType, { source: { id: content.id, executionId: content.executionId, type: content.type, parent: (0, _messageHelper.cloneParent)(content.parent) }, message, originalMessage: content.message }, { delegate: true, type: messageType }); this.broker.publish('event', `definition.${messageType}`, this._createMessage({ message: message && (0, _messageHelper.cloneContent)(message) }), { type: messageType }); }; DefinitionExecution.prototype._removeProcessByExecutionId = function removeProcessByExecutionId(processExecutionId) { const bp = this.getProcessByExecutionId(processExecutionId); if (bp) this[kProcesses].running.delete(bp); return bp; }; DefinitionExecution.prototype._complete = function complete(completionType, content, options) { this._deactivate(); const stateMessage = this[kExecuteMessage]; this._debug(`definition execution ${completionType} in ${Date.now() - stateMessage.properties.timestamp}ms`); if (!content) content = this._createMessage(); this[kCompleted] = true; this[kStatus] = completionType; this.broker.deleteQueue(this[kProcessesQ].name); return this.broker.publish('execution', `execution.${completionType}.${this.executionId}`, { ...stateMessage.content, output: { ...this.environment.output }, ...content, state: completionType }, { type: completionType, mandatory: completionType === 'error', ...options }); }; DefinitionExecution.prototype._createMessage = function createMessage(content) { return { id: this.id, type: this.type, executionId: this.executionId, status: this[kStatus], ...content }; }; DefinitionExecution.prototype._getProcessApi = function getProcessApi(message) { const content = message.content; let api = this._getProcessApiByExecutionId(content.executionId, message); if (api) return api; if (!content.parent) return; api = this._getProcessApiByExecutionId(content.parent.executionId, message); if (api) return api; if (!content.parent.path) return; for (const pp of content.parent.path) { api = this._getProcessApiByExecutionId(pp.executionId, message); if (api) return api; } }; DefinitionExecution.prototype._getProcessApiByExecutionId = function getProcessApiByExecutionId(parentExecutionId, message) { const processInstance = this.getProcessByExecutionId(parentExecutionId); if (!processInstance) return; return processInstance.getApi(message); }; DefinitionExecution.prototype._debug = function debug(logMessage) { this[kParent].logger.debug(`<${this.executionId} (${this.id})> ${logMessage}`); };