bpmn-elements
Version:
Executable workflow elements based on BPMN 2.0
546 lines (455 loc) • 15.1 kB
JavaScript
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');
}
}