UNPKG

@vzeta/zeebe-node-test

Version:

The Node.js client library for the Zeebe Workflow Automation Engine.

634 lines 28.4 kB
"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