bpmn-elements
Version:
Executable workflow elements based on BPMN 2.0
322 lines (321 loc) • 12.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = LoopCharacteristics;
var _Errors = require("../error/Errors.js");
var _messageHelper = require("../messageHelper.js");
function LoopCharacteristics(activity, loopCharacteristics) {
this.activity = activity;
this.loopCharacteristics = loopCharacteristics;
const {
type = 'LoopCharacteristics',
behaviour = {}
} = loopCharacteristics;
this.type = type;
const {
isSequential = false,
collection
} = behaviour;
this.isSequential = isSequential;
this.collection = collection;
let completionCondition, startCondition, loopCardinality;
if ('loopCardinality' in behaviour) loopCardinality = behaviour.loopCardinality;else if ('loopMaximum' in behaviour) loopCardinality = behaviour.loopMaximum;
this.loopCardinality = loopCardinality;
if (behaviour.loopCondition) {
if (behaviour.testBefore) startCondition = behaviour.loopCondition;else completionCondition = behaviour.loopCondition;
}
if (behaviour.completionCondition) {
completionCondition = behaviour.completionCondition;
}
if (collection) {
this.loopType = 'collection';
this.elementVariable = behaviour.elementVariable || 'item';
} else if (completionCondition) this.loopType = 'complete condition';else if (startCondition) this.loopType = 'start condition';else if (loopCardinality) this.loopType = 'cardinality';
this.characteristics = null;
this.execution = null;
}
LoopCharacteristics.prototype.execute = function execute(executeMessage) {
if (!executeMessage) throw new TypeError('LoopCharacteristics execution requires message');
const chr = this.characteristics = this.characteristics || new Characteristics(this.activity, this.loopCharacteristics, executeMessage);
if (chr.cardinality === 0) return chr.complete();
const execution = this.isSequential ? new SequentialLoopCharacteristics(this.activity, chr) : new ParallelLoopCharacteristics(this.activity, chr);
return execution.execute(executeMessage);
};
function SequentialLoopCharacteristics(activity, characteristics) {
this.activity = activity;
this.id = activity.id;
this.characteristics = characteristics;
}
SequentialLoopCharacteristics.prototype.execute = function execute(executeMessage) {
const {
routingKey: executeRoutingKey,
redelivered: isRedelivered
} = executeMessage.fields || {};
const chr = this.characteristics;
if (!chr.cardinality && !chr.startCondition && !chr.completionCondition) {
throw new _Errors.RunError(`<${this.id}> cardinality, collection, or condition is required in sequential loops`, executeMessage);
}
let startIndex = 0;
if (isRedelivered && executeRoutingKey === 'execute.iteration.next') {
startIndex = executeMessage.content.index;
}
chr.subscribe(this._onCompleteMessage.bind(this));
return this._startNext(startIndex, isRedelivered);
};
SequentialLoopCharacteristics.prototype._startNext = function startNext(index, ignoreIfExecuting) {
const chr = this.characteristics;
const content = chr.next(index);
if (!content) return;
if (chr.isStartConditionMet({
content
})) {
chr.debug('start condition met');
return;
}
chr.debug(`${ignoreIfExecuting ? 'resume' : 'start'} sequential iteration index ${content.index}`);
const broker = this.activity.broker;
broker.publish('execution', 'execute.iteration.next', {
...content,
...chr.getContent(),
index,
preventComplete: true,
output: chr.output.slice(),
state: 'iteration.next'
});
broker.publish('execution', 'execute.start', {
...content,
ignoreIfExecuting
});
return content;
};
SequentialLoopCharacteristics.prototype._onCompleteMessage = function onCompleteMessage(_, message) {
const {
content
} = message;
const chr = this.characteristics;
const loopOutput = chr.output;
if (content.output !== undefined) loopOutput[content.index] = content.output;
this.activity.broker.publish('execution', 'execute.iteration.completed', {
...message.content,
...chr.getContent(),
preventComplete: true,
output: loopOutput.slice(),
state: 'iteration.completed'
});
if (chr.isCompletionConditionMet(message, loopOutput)) {
chr.debug('complete condition met');
} else if (this._startNext(content.index + 1)) return;
chr.debug('sequential loop completed');
return chr.complete(content);
};
function ParallelLoopCharacteristics(activity, characteristics) {
this.activity = activity;
this.id = activity.id;
this.characteristics = characteristics;
this.running = 0;
this.index = 0;
this.discarded = 0;
}
ParallelLoopCharacteristics.prototype.execute = function execute(executeMessage) {
const chr = this.characteristics;
if (!chr.cardinality) throw new _Errors.RunError(`<${this.id}> cardinality or collection is required in parallel loops`, executeMessage);
const isRedelivered = executeMessage.fields.redelivered;
if (isRedelivered) {
const {
index,
running,
discarded
} = executeMessage.content;
if (!isNaN(index)) this.index = index;
if (!isNaN(running)) this.running = running;
if (!isNaN(discarded)) this.discarded = discarded;
}
chr.subscribe(this._onCompleteMessage.bind(this));
if (isRedelivered) return;
return this._startBatch();
};
ParallelLoopCharacteristics.prototype._startBatch = function startBatch() {
const chr = this.characteristics;
const cardinality = chr.cardinality;
const batch = new Set();
let startContent = chr.next(this.index);
do {
chr.debug(`start parallel iteration index ${this.index}`);
batch.add(startContent);
this.running++;
this.index++;
if (this.index >= cardinality || this.running >= chr.batchSize) {
break;
}
} while (startContent = chr.next(this.index));
const broker = this.activity.broker;
broker.publish('execution', 'execute.iteration.batch', {
...chr.getContent(),
index: this.index,
running: this.running,
discarded: this.discarded,
output: chr.output,
preventComplete: true
});
for (const content of batch) {
broker.publish('execution', 'execute.start', content);
}
};
ParallelLoopCharacteristics.prototype._onCompleteMessage = function onCompleteMessage(routingKey, message) {
const chr = this.characteristics;
const {
content
} = message;
if (content.output !== undefined) chr.output[content.index] = content.output;
if (routingKey === 'execute.discard') {
this.discarded++;
}
this.running--;
this.activity.broker.publish('execution', 'execute.iteration.completed', {
...content,
...chr.getContent(),
index: this.index,
running: this.running,
discarded: this.discarded,
output: chr.output,
state: 'iteration.completed',
preventComplete: true
});
if (this.running <= 0 && !chr.next(this.index)) {
return chr.complete(content, this.discarded === this.index);
}
if (chr.isCompletionConditionMet(message)) {
return chr.complete(content, this.discarded === this.index);
}
if (this.running <= 0) {
this.running = 0;
this._startBatch();
}
};
function Characteristics(activity, loopCharacteristics, executeMessage) {
this.activity = activity;
const behaviour = this.behaviour = loopCharacteristics.behaviour || {};
this.message = executeMessage;
const type = this.type = loopCharacteristics.type || 'LoopCharacteristics';
this.id = activity.id;
this.broker = activity.broker;
this.parentExecutionId = executeMessage.content.executionId;
this.isSequential = behaviour.isSequential || false;
this.output = executeMessage.content.output || [];
this.parent = (0, _messageHelper.unshiftParent)(executeMessage.content.parent, executeMessage.content);
if ('loopCardinality' in behaviour) this.loopCardinality = behaviour.loopCardinality;else if ('loopMaximum' in behaviour) this.loopCardinality = behaviour.loopMaximum;
if (behaviour.loopCondition) {
if (behaviour.testBefore) this.startCondition = behaviour.loopCondition;else this.completionCondition = behaviour.loopCondition;
}
if (behaviour.completionCondition) {
this.completionCondition = behaviour.completionCondition;
}
const collection = this.collection = this.getCollection();
if (collection) {
this.elementVariable = behaviour.elementVariable || 'item';
}
this.cardinality = this.getCardinality(collection);
this.onApiMessage = this.onApiMessage.bind(this);
const environment = activity.environment;
this.logger = environment.Logger(type.toLowerCase());
this.batchSize = environment.settings.batchSize || 50;
}
Characteristics.prototype.getContent = function getContent() {
return {
...(0, _messageHelper.cloneContent)(this.message.content),
loopCardinality: this.cardinality,
isSequential: this.isSequential,
output: undefined
};
};
Characteristics.prototype.next = function next(index) {
const cardinality = this.cardinality;
if (cardinality > 0 && index >= cardinality) return;
const collection = this.collection;
if (collection && index >= collection.length) return;
const content = {
...this.getContent(),
isRootScope: undefined,
executionId: `${this.parentExecutionId}_${index}`,
isMultiInstance: true,
parent: (0, _messageHelper.cloneParent)(this.parent),
index
};
if (collection) {
content[this.elementVariable] = collection[index];
}
return content;
};
Characteristics.prototype.getCardinality = function getCardinality(collection) {
const collectionLen = this.collection && Array.isArray(collection) ? collection.length : undefined;
if (!this.loopCardinality) {
return collectionLen;
}
const value = this.activity.environment.resolveExpression(this.loopCardinality, this.message);
if (value !== undefined && isNaN(value) || value < 0) {
throw new _Errors.RunError(`<${this.id}> invalid loop cardinality >${value}<`, this.message);
}
if (value === undefined) return collectionLen;
return Number(value);
};
Characteristics.prototype.getCollection = function getCollection() {
const collectionExpression = this.behaviour.collection;
if (!collectionExpression) return;
return this.activity.environment.resolveExpression(collectionExpression, this.message);
};
Characteristics.prototype.isStartConditionMet = function isStartConditionMet(message) {
if (!this.startCondition) return false;
return this.activity.environment.resolveExpression(this.startCondition, (0, _messageHelper.cloneMessage)(message));
};
Characteristics.prototype.isCompletionConditionMet = function isCompletionConditionMet(message) {
if (!this.completionCondition) return false;
return this.activity.environment.resolveExpression(this.completionCondition, (0, _messageHelper.cloneMessage)(message, {
loopOutput: this.output
}));
};
Characteristics.prototype.complete = function complete(content, allDiscarded) {
this.stop();
return this.broker.publish('execution', 'execute.' + (allDiscarded ? 'discard' : 'completed'), {
...content,
...this.getContent(),
output: this.output
});
};
Characteristics.prototype.subscribe = function subscribe(onIterationCompleteMessage) {
this.broker.subscribeTmp('api', `activity.*.${this.parentExecutionId}`, this.onApiMessage, {
noAck: true,
consumerTag: '_api-multi-instance-tag'
}, {
priority: 400
});
this.broker.subscribeTmp('execution', 'execute.*', onComplete, {
noAck: true,
consumerTag: '_execute-q-multi-instance-tag',
priority: 300
});
function onComplete(routingKey, message, ...args) {
if (!message.content.isMultiInstance) return;
switch (routingKey) {
case 'execute.discard':
case 'execute.cancel':
case 'execute.completed':
return onIterationCompleteMessage(routingKey, message, ...args);
}
}
};
Characteristics.prototype.onApiMessage = function onApiMessage(_, message) {
switch (message.properties.type) {
case 'stop':
case 'discard':
this.stop();
break;
}
};
Characteristics.prototype.stop = function stop() {
this.broker.cancel('_execute-q-multi-instance-tag');
this.broker.cancel('_api-multi-instance-tag');
};
Characteristics.prototype.debug = function debug(msg) {
this.logger.debug(`<${this.parentExecutionId} (${this.id})> ${msg}`);
};