@vzeta/zeebe-node-test
Version:
The Node.js client library for the Zeebe Workflow Automation Engine.
634 lines • 28.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZBClient = exports.ConnectionStatusEvent = void 0;
const chalk_1 = __importDefault(require("chalk"));
const fp_ts_1 = require("fp-ts");
const pipeable_1 = require("fp-ts/lib/pipeable");
const path = __importStar(require("path"));
const promise_retry_1 = __importDefault(require("promise-retry"));
const typed_duration_1 = require("typed-duration");
const uuid_1 = require("uuid");
const lib_1 = require("../lib");
const ConfigurationHydrator_1 = require("../lib/ConfigurationHydrator");
const ConnectionFactory_1 = require("../lib/ConnectionFactory");
const impure_1 = require("../lib/deployWorkflow/impure");
const pure_1 = require("../lib/deployWorkflow/pure");
const OAuthProvider_1 = require("../lib/OAuthProvider");
const SimpleLogger_1 = require("../lib/SimpleLogger");
const TypedEmitter_1 = require("../lib/TypedEmitter");
const utils_1 = require("../lib/utils");
const ZBJsonLogger_1 = require("../lib/ZBJsonLogger");
const ZBWorkerSignature_1 = require("../lib/ZBWorkerSignature");
const ZBBatchWorker_1 = require("./ZBBatchWorker");
const ZBWorker_1 = require("./ZBWorker");
const fs_1 = require("fs");
const idColors = [
chalk_1.default.yellow,
chalk_1.default.green,
chalk_1.default.cyan,
chalk_1.default.magenta,
chalk_1.default.blue,
];
exports.ConnectionStatusEvent = {
close: 'close',
connectionError: 'connectionError',
ready: 'ready',
unknown: 'unknown',
};
class ZBClient extends TypedEmitter_1.TypedEmitter {
constructor(gatewayAddress, options) {
var _a, _b, _c;
super();
this.connectionTolerance = process.env
.ZEEBE_CONNECTION_TOLERANCE
? parseInt(process.env.ZEEBE_CONNECTION_TOLERANCE, 10)
: ZBClient.DEFAULT_CONNECTION_TOLERANCE;
this.connected = undefined;
this.readied = false;
this.closing = false;
this.workerCount = 0;
this.workers = [];
this.maxRetries = process.env.ZEEBE_CLIENT_MAX_RETRIES
? parseInt(process.env.ZEEBE_CLIENT_MAX_RETRIES, 10)
: ZBClient.DEFAULT_MAX_RETRIES;
this.maxRetryTimeout = process.env
.ZEEBE_CLIENT_MAX_RETRY_TIMEOUT
? parseInt(process.env.ZEEBE_CLIENT_MAX_RETRY_TIMEOUT, 10)
: ZBClient.DEFAULT_MAX_RETRY_TIMEOUT;
if (typeof gatewayAddress === 'object') {
options = gatewayAddress;
gatewayAddress = undefined;
}
const constructorOptionsWithDefaults = {
longPoll: ZBClient.DEFAULT_LONGPOLL_PERIOD,
pollInterval: ZBClient.DEFAULT_POLL_INTERVAL,
...(options ? options : {}),
retry: (options === null || options === void 0 ? void 0 : options.retry) !== false,
};
constructorOptionsWithDefaults.loglevel =
process.env.ZEEBE_NODE_LOG_LEVEL ||
constructorOptionsWithDefaults.loglevel ||
'INFO';
this.loglevel = constructorOptionsWithDefaults.loglevel;
const logTypeFromEnvironment = () => {
var _a;
return ({
JSON: ZBJsonLogger_1.ZBJsonLogger,
SIMPLE: SimpleLogger_1.ZBSimpleLogger,
}[(_a = process.env.ZEEBE_NODE_LOG_TYPE) !== null && _a !== void 0 ? _a : 'NONE']);
};
constructorOptionsWithDefaults.stdout =
(_b = (_a = constructorOptionsWithDefaults.stdout) !== null && _a !== void 0 ? _a : logTypeFromEnvironment()) !== null && _b !== void 0 ? _b : SimpleLogger_1.ZBSimpleLogger;
this.stdout = constructorOptionsWithDefaults.stdout;
this.options = ConfigurationHydrator_1.ConfigurationHydrator.configure(gatewayAddress, constructorOptionsWithDefaults);
this.gatewayAddress = `${this.options.hostname}:${this.options.port}`;
this.oAuth = this.options.oAuth
? new OAuthProvider_1.OAuthProvider(this.options.oAuth)
: undefined;
this.useTLS =
this.options.useTLS === true ||
(!!this.options.oAuth && this.options.useTLS !== false);
this.customSSL = this.options.customSSL;
this.basicAuth = this.options.basicAuth;
this.connectionTolerance = typed_duration_1.Duration.milliseconds.from(this.options.connectionTolerance || this.connectionTolerance);
this.onConnectionError = this.options.onConnectionError;
this.onReady = this.options.onReady;
const { grpcClient, log } = this.constructGrpcClient({
grpcConfig: {
namespace: this.options.logNamespace || 'ZBClient',
},
logConfig: {
_tag: 'ZBCLIENT',
loglevel: this.loglevel,
longPoll: this.options.longPoll
? typed_duration_1.Duration.milliseconds.from(this.options.longPoll)
: undefined,
namespace: this.options.logNamespace || 'ZBClient',
pollInterval: this.options.pollInterval
? typed_duration_1.Duration.milliseconds.from(this.options.pollInterval)
: undefined,
stdout: this.stdout,
},
});
grpcClient.on(exports.ConnectionStatusEvent.connectionError, () => {
var _a;
if (this.connected !== false) {
(_a = this.onConnectionError) === null || _a === void 0 ? void 0 : _a.call(this);
this.emit(exports.ConnectionStatusEvent.connectionError);
}
this.connected = false;
this.readied = false;
});
grpcClient.on(exports.ConnectionStatusEvent.ready, () => {
var _a;
if (!this.readied) {
(_a = this.onReady) === null || _a === void 0 ? void 0 : _a.call(this);
this.emit(exports.ConnectionStatusEvent.ready);
}
this.connected = true;
this.readied = true;
});
this.grpc = grpcClient;
this.logger = log;
this.retry = this.options.retry;
this.maxRetries =
this.options.maxRetries || ZBClient.DEFAULT_MAX_RETRIES;
this.maxRetryTimeout =
this.options.maxRetryTimeout || ZBClient.DEFAULT_MAX_RETRY_TIMEOUT;
// Send command to broker to eagerly fail / prove connection.
// This is useful for, for example: the Node-Red client, which wants to
// display the connection status.
if ((_c = this.options.eagerConnection) !== null && _c !== void 0 ? _c : false) {
this.topology()
.then(res => {
this.logger.logDirect(chalk_1.default.blueBright('Zeebe cluster topology:'));
this.logger.logDirect(res.brokers);
})
.catch(e => {
// Swallow exception to avoid throwing if retries are off
if (e.thisWillNeverHappenYo) {
this.emit(exports.ConnectionStatusEvent.unknown);
}
});
}
}
activateJobs(request) {
return new Promise(async (resolve, reject) => {
try {
const stream = await this.grpc.activateJobsStream(request);
stream.on('data', (res) => {
const jobs = res.jobs.map(job => lib_1.parseVariablesAndCustomHeadersToJSON(job));
resolve(jobs);
});
}
catch (e) {
reject(e);
}
});
}
/**
* @deprecated use cancelProcessInstance instead
*/
async cancelWorkflowInstance(workflowInstanceKey) {
utils_1.Utils.validateNumber(workflowInstanceKey, 'workflowInstanceKey');
return this.cancelProcessInstance(workflowInstanceKey);
}
async cancelProcessInstance(processInstanceKey) {
utils_1.Utils.validateNumber(processInstanceKey, 'processInstanceKey');
return this.executeOperation('cancelProcessInstance', () => this.grpc.cancelProcessInstanceSync({
processInstanceKey,
}));
}
createBatchWorker(conf) {
var _a;
if (this.closing) {
throw new Error('Client is closing. No worker creation allowed!');
}
const config = ZBWorkerSignature_1.decodeCreateZBWorkerSig({
idOrTaskTypeOrConfig: conf,
});
// Merge parent client options with worker override
const options = {
...this.options,
loglevel: this.loglevel,
onConnectionError: undefined,
onReady: undefined,
...config.options,
};
const idColor = idColors[this.workerCount++ % idColors.length];
// Give worker its own gRPC connection
const { grpcClient: workerGRPCClient, log } = this.constructGrpcClient({
grpcConfig: {
namespace: 'ZBWorker',
tasktype: config.taskType,
},
logConfig: {
_tag: 'ZBWORKER',
colorise: true,
id: (_a = config.id) !== null && _a !== void 0 ? _a : uuid_1.v4(),
loglevel: options.loglevel,
namespace: ['ZBWorker', options.logNamespace].join(' ').trim(),
pollInterval: options.longPoll || ZBClient.DEFAULT_LONGPOLL_PERIOD,
stdout: options.stdout,
taskType: `${config.taskType} (batch)`,
},
});
const worker = new ZBBatchWorker_1.ZBBatchWorker({
grpcClient: workerGRPCClient,
id: config.id || null,
idColor,
log,
options: { ...this.options, ...options },
taskHandler: config.taskHandler,
taskType: config.taskType,
zbClient: this,
});
this.workers.push(worker);
return worker;
}
createWorker(idOrTaskTypeOrConfig, taskTypeOrTaskHandler, taskHandlerOrOptions, optionsOrOnConnectionError, onConnectionError) {
if (this.closing) {
throw new Error('Client is closing. No worker creation allowed!');
}
const idColor = idColors[this.workerCount++ % idColors.length];
const config = ZBWorkerSignature_1.decodeCreateZBWorkerSig({
idOrTaskTypeOrConfig,
onConnectionError,
optionsOrOnConnectionError,
taskHandlerOrOptions,
taskTypeOrTaskHandler,
});
// Merge parent client options with worker override
const options = {
...this.options,
loglevel: this.loglevel,
onConnectionError: undefined,
onReady: undefined,
...config.options,
};
// Give worker its own gRPC connection
const { grpcClient: workerGRPCClient, log } = this.constructGrpcClient({
grpcConfig: {
namespace: 'ZBWorker',
tasktype: config.taskType,
},
logConfig: {
_tag: 'ZBWORKER',
colorise: true,
id: config.id,
loglevel: options.loglevel,
namespace: ['ZBWorker', options.logNamespace].join(' ').trim(),
pollInterval: options.longPoll || ZBClient.DEFAULT_LONGPOLL_PERIOD,
stdout: options.stdout,
taskType: config.taskType,
},
});
const worker = new ZBWorker_1.ZBWorker({
grpcClient: workerGRPCClient,
id: config.id || null,
idColor,
log,
options: { ...this.options, ...options },
taskHandler: config.taskHandler,
taskType: config.taskType,
zbClient: this,
});
this.workers.push(worker);
return worker;
}
/**
* Gracefully shut down all workers, draining existing tasks, and return when it is safe to exit.
* @returns Promise
* @memberof ZBClient
*/
async close(timeout) {
this.closePromise =
this.closePromise ||
new Promise(async (resolve) => {
// Prevent the creation of more workers
this.closing = true;
await Promise.all(this.workers.map(w => w.close(timeout)));
await this.grpc.close(timeout); // close the client GRPC channel
this.emit(exports.ConnectionStatusEvent.close);
this.grpc.removeAllListeners();
this.removeAllListeners();
resolve(null);
});
return this.closePromise;
}
completeJob(completeJobRequest) {
const withStringifiedVariables = lib_1.stringifyVariables(completeJobRequest);
this.logger.logDebug(withStringifiedVariables);
return this.executeOperation('completeJob', () => this.grpc.completeJobSync(withStringifiedVariables).catch(e => {
if (e.code === 5) {
e.details +=
'. The process may have been cancelled, the job cancelled by an interrupting event, or the job already completed.' +
' For more detail, see: https://forum.zeebe.io/t/command-rejected-with-code-complete/908/17';
}
throw e;
}));
}
createWorkflowInstance(configOrbpmnProcessId, variables) {
return this.createProcessInstance(lib_1.transformAPI0ReqToAPI1(configOrbpmnProcessId), lib_1.transformAPI0ReqToAPI1(variables)).then(res => lib_1.makeAPI1ResAPI0Compatible(res));
}
createProcessInstance(configOrbpmnProcessId, variables) {
const isConfigObject = (conf) => typeof conf === 'object';
const request = isConfigObject(configOrbpmnProcessId)
? {
bpmnProcessId: configOrbpmnProcessId.bpmnProcessId,
variables: configOrbpmnProcessId.variables,
version: configOrbpmnProcessId.version || -1,
}
: {
bpmnProcessId: configOrbpmnProcessId,
variables,
version: -1,
};
const createProcessInstanceRequest = {
bpmnProcessId: request.bpmnProcessId,
variables: request.variables,
version: request.version,
};
return this.executeOperation('createProcessInstance', () => this.grpc.createProcessInstanceSync(lib_1.stringifyVariables(createProcessInstanceRequest)));
}
/**
* @deprecated use createProcessInstanceWithResult instead
*
*/
createWorkflowInstanceWithResult(configOrBpmnProcessId, variables) {
return this.createProcessInstanceWithResult(lib_1.transformAPI0ReqToAPI1(configOrBpmnProcessId), lib_1.transformAPI0ReqToAPI1(variables)).then(res => lib_1.makeAPI1ResAPI0Compatible(res));
}
createProcessInstanceWithResult(configOrBpmnProcessId, variables) {
const isConfigObject = (config) => typeof config === 'object';
const request = isConfigObject(configOrBpmnProcessId)
? {
bpmnProcessId: configOrBpmnProcessId.bpmnProcessId,
fetchVariables: configOrBpmnProcessId.fetchVariables,
requestTimeout: configOrBpmnProcessId.requestTimeout || 0,
variables: configOrBpmnProcessId.variables,
version: configOrBpmnProcessId.version || -1,
}
: {
bpmnProcessId: configOrBpmnProcessId,
fetchVariables: undefined,
requestTimeout: 0,
variables,
version: -1,
};
const createProcessInstanceRequest = lib_1.stringifyVariables({
bpmnProcessId: request.bpmnProcessId,
variables: request.variables,
version: request.version,
});
return this.executeOperation('createProcessInstanceWithResult', () => this.grpc.createProcessInstanceWithResultSync({
fetchVariables: request.fetchVariables,
request: createProcessInstanceRequest,
requestTimeout: request.requestTimeout,
})).then(res => lib_1.parseVariables(res));
}
/**
*
* @param workflow - A path or array of paths to .bpmn files or an object describing the workflow
* @deprecated use deployProcess instead
*/
async deployWorkflow(workflow) {
return this.deployProcess(workflow).then(res => lib_1.makeAPI1ResAPI0Compatible(res));
}
async deployResource(resource) {
const isProcess = (maybeProcess) => !!maybeProcess.process;
const isProcessFilename = (maybeProcessFilename) => !!maybeProcessFilename.processFilename;
const isDecision = (maybeDecision) => !!maybeDecision.decision;
if (isProcessFilename(resource)) {
const filename = resource.processFilename;
const process = fs_1.readFileSync(filename);
return this.executeOperation('deployResource', () => this.grpc.deployResourceSync({
resources: [
{
name: filename,
content: process,
},
],
}));
}
else if (isProcess(resource)) {
return this.executeOperation('deployResource', () => this.grpc.deployResourceSync({
resources: [
{
name: resource.name,
content: resource.process,
},
],
}));
}
else if (isDecision(resource)) {
return this.executeOperation('deployResource', () => this.grpc.deployResourceSync({
resources: [
{
name: resource.name,
content: resource.decision,
},
],
}));
}
else {
const filename = resource.decisionFilename;
const decision = fs_1.readFileSync(filename);
return this.executeOperation('deployResource', () => this.grpc.deployResourceSync({
resources: [
{
name: filename,
content: decision,
},
],
}));
}
}
async deployProcess(process) {
const deploy = (processes) => this.executeOperation('deployWorkflow', () => this.grpc.deployProcessSync({
processes,
}));
const error = (e) => Promise.reject(`Deployment failed. The following files were not found: ${e.join(', ')}.`);
return pipeable_1.pipe(pure_1.bufferOrFiles(process), fp_ts_1.either.fold(deploy, files => pipeable_1.pipe(pure_1.mapThese(files, impure_1.readDefinitionFromFile), fp_ts_1.either.fold(error, deploy))));
}
failJob(failJobRequest) {
return this.executeOperation('failJob', () => this.grpc.failJobSync(failJobRequest));
}
/**
* Return an array of task-types specified in a BPMN file.
* @param file - Path to bpmn file.
*/
getServiceTypesFromBpmn(files) {
const fileArray = typeof files === 'string' ? [files] : files;
return lib_1.BpmnParser.getTaskTypes(lib_1.BpmnParser.parseBpmn(fileArray));
}
/**
* Publish a message to the broker for correlation with a workflow instance.
* @param publishMessageRequest - The message to publish.
*/
publishMessage(publishMessageRequest) {
return this.executeOperation('publishMessage', () => this.grpc.publishMessageSync(lib_1.stringifyVariables(publishMessageRequest)));
}
/**
* Publish a message to the broker for correlation with a workflow message start event.
* @param publishStartMessageRequest - The message to publish.
*/
publishStartMessage(publishStartMessageRequest) {
/**
* The hash of the correlationKey is used to determine the partition where this workflow will start.
* So we assign a random uuid to balance workflow instances created via start message across partitions.
*
* We make the correlationKey optional, because the caller can specify a correlationKey + messageId
* to guarantee an idempotent message.
*
* Multiple messages with the same correlationKey + messageId combination will only start a workflow once.
* See: https://github.com/zeebe-io/zeebe/issues/1012 and https://github.com/zeebe-io/zeebe/issues/1022
*/
const publishMessageRequest = {
correlationKey: uuid_1.v4(),
...publishStartMessageRequest,
};
return this.executeOperation('publishStartMessage', () => this.grpc.publishMessageSync(lib_1.stringifyVariables(publishMessageRequest)));
}
resolveIncident(resolveIncidentRequest) {
return this.executeOperation('resolveIncident', () => this.grpc.resolveIncidentSync(resolveIncidentRequest));
}
setVariables(request) {
/*
We allow developers to interact with variables as a native JS object, but the Zeebe server needs it as a JSON document
So we stringify it here.
*/
if (typeof request.variables === 'object') {
request.variables = JSON.stringify(request.variables);
}
return this.executeOperation('setVariables', () => this.grpc.setVariablesSync(request));
}
/**
*
* Report a business error (i.e. non-technical) that occurs while processing a job.
* The error is handled in the workflow by an error catch event.
* If there is no error catch event with the specified errorCode then an incident will be raised instead.
*/
throwError(throwErrorRequest) {
return this.executeOperation('throwError', () => this.grpc.throwErrorSync(throwErrorRequest));
}
/**
* Return the broker cluster topology
*/
topology() {
return this.executeOperation('topology', this.grpc.topologySync);
}
updateJobRetries(updateJobRetriesRequest) {
return this.executeOperation('updateJobRetries', () => this.grpc.updateJobRetriesSync(updateJobRetriesRequest));
}
constructGrpcClient({ grpcConfig, logConfig, }) {
const { grpcClient, log } = ConnectionFactory_1.ConnectionFactory.getGrpcClient({
grpcConfig: {
basicAuth: this.basicAuth,
connectionTolerance: typed_duration_1.Duration.milliseconds.from(this.connectionTolerance),
customSSL: this.customSSL,
host: this.gatewayAddress,
loglevel: this.loglevel,
namespace: grpcConfig.namespace,
oAuth: this.oAuth,
options: {
longPoll: this.options.longPoll
? typed_duration_1.Duration.milliseconds.from(this.options.longPoll)
: undefined,
},
packageName: 'gateway_protocol',
protoPath: path.join(__dirname, '../../proto/zeebe.proto'),
service: 'Gateway',
stdout: this.stdout,
tasktype: grpcConfig.tasktype,
useTLS: this.useTLS,
},
logConfig,
});
if (grpcConfig.onConnectionError) {
grpcClient.on(exports.ConnectionStatusEvent.connectionError, grpcConfig.onConnectionError);
}
if (grpcConfig.onReady) {
grpcClient.on(exports.ConnectionStatusEvent.ready, grpcConfig.onReady);
}
return { grpcClient: grpcClient, log };
}
/**
* If this.retry is set true, the operation will be wrapped in an configurable retry on exceptions
* of gRPC error code 14 - Transient Network Failure.
* See: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md
* If this.retry is false, it will be executed with no retry, and the application should handle the exception.
* @param operation A gRPC command operation
*/
async executeOperation(operationName, operation, retries) {
return this.retry
? this.retryOnFailure(operationName, operation, retries)
: operation();
}
_onConnectionError() {
var _a;
if (!this.connected) {
return;
}
this.connected = false;
// const debounce =
// this.lastConnectionError &&
// new Date().valueOf() - this.lastConnectionError.valueOf() >
// this.connectionTolerance / 2
// if (!debounce) {
(_a = this.onConnectionError) === null || _a === void 0 ? void 0 : _a.call(this);
this.emit(exports.ConnectionStatusEvent.connectionError);
// }
// this.lastConnectionError = new Date()
}
/**
* This function takes a gRPC operation that returns a Promise as a function, and invokes it.
* If the operation throws gRPC error 14, this function will continue to try it until it succeeds
* or retries are exhausted.
* @param operation A gRPC command operation that may fail if the broker is not available
*/
async retryOnFailure(operationName, operation, retries = this.maxRetries) {
let connectionErrorCount = 0;
return promise_retry_1.default((retry, n) => {
if (this.closing || this.grpc.channelClosed) {
return Promise.resolve();
}
if (n > 1) {
this.logger.logError(`[${operationName}]: Attempt ${n} (max: ${this.maxRetries}).`);
}
return operation().catch(err => {
// This could be DNS resolution, or the gRPC gateway is not reachable yet, or Backpressure
const isNetworkError = err.message.indexOf('14') === 0 ||
err.message.indexOf('Stream removed') !== -1;
const isBackpressure = err.message.indexOf('8') === 0 || err.code === 8;
if (isNetworkError) {
if (connectionErrorCount < 0) {
this._onConnectionError();
}
connectionErrorCount++;
}
if (isNetworkError || isBackpressure) {
this.logger.logError(`[${operationName}]: ${err.message}`);
retry(err);
}
// The gRPC channel will be closed if close has been called
if (this.grpc.channelClosed) {
return Promise.resolve();
}
throw err;
});
}, {
forever: retries === -1,
maxTimeout: typed_duration_1.Duration.milliseconds.from(this.maxRetryTimeout),
retries: retries === -1 ? undefined : retries,
});
}
}
exports.ZBClient = ZBClient;
ZBClient.DEFAULT_CONNECTION_TOLERANCE = typed_duration_1.Duration.milliseconds.of(3000);
ZBClient.DEFAULT_MAX_RETRIES = -1; // Infinite retry
ZBClient.DEFAULT_MAX_RETRY_TIMEOUT = typed_duration_1.Duration.seconds.of(5);
ZBClient.DEFAULT_LONGPOLL_PERIOD = typed_duration_1.Duration.seconds.of(30);
ZBClient.DEFAULT_POLL_INTERVAL = typed_duration_1.Duration.milliseconds.of(300);
//# sourceMappingURL=ZBClient.js.map