UNPKG

zeebe-node

Version:

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

1,168 lines 46.5 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 debug = require('debug')('client'); 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 GrpcError_1 = require("../lib/GrpcError"); 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', }; /** * @description A client for interacting with a Zeebe broker. With the connection credentials set in the environment, you can use a "zero-conf" constructor with no arguments. * @example * ``` * const zbc = new ZBClient() * zbc.topology().then(info => * console.log(JSON.stringify(info, null, 2)) * ) * ``` */ 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.tenantId = this.options.tenantId; this.gatewayAddress = `${this.options.hostname}:${this.options.port}`; 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; this.oAuth = this.options.oAuth ? new OAuthProvider_1.OAuthProvider(this.options.oAuth) : undefined; 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, (err) => { var _a; if (this.connected !== false) { (_a = this.onConnectionError) === null || _a === void 0 ? void 0 : _a.call(this, err); 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); } }); } } /** * @description activateJobs allows you to manually activate jobs, effectively building a worker; rather than using the ZBWorker class. * @example * ``` * const zbc = new ZBClient() * zbc.activateJobs({ * maxJobsToActivate: 5, * requestTimeout: 6000, * timeout: 5 * 60 * 1000, * type: 'process-payment', * worker: 'my-worker-uuid' * }).then(jobs => * jobs.forEach(job => * // business logic * zbc.completeJob({ * jobKey: job.key, * variables: {} * )) * ) * }) * ``` */ 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 => (0, lib_1.parseVariablesAndCustomHeadersToJSON)(job)); resolve(jobs); }); } catch (e) { reject(e); } }); } /** * * @description Broadcast a Signal * @example * ``` * const zbc = new ZBClient() * * zbc.broadcastSignal({ * signalName: 'my-signal', * variables: { reasonCode: 3 } * }) */ async broadcastSignal(req) { var _a; const request = { signalName: req.signalName, variables: JSON.stringify((_a = req.variables) !== null && _a !== void 0 ? _a : {}) }; return this.executeOperation('broadcastSignal', () => this.grpc.broadcastSignalSync(request)); } /** * * @description Cancel a process instance by process instance key. * @example * ``` * const zbc = new ZBClient() * * zbc.cancelProcessInstance(processInstanceId) * .catch( * (e: any) => console.log(`Error cancelling instance: ${e.message}`) * ) * ``` */ async cancelProcessInstance(processInstanceKey) { utils_1.Utils.validateNumber(processInstanceKey, 'processInstanceKey'); return this.executeOperation('cancelProcessInstance', () => this.grpc.cancelProcessInstanceSync({ processInstanceKey, })); } /** * * @description Create a new Batch Worker. This is useful when you need to rate limit access to an external resource. * @example * ``` * const zbc = new ZBClient() * // Helper function to find a job by its key * const findJobByKey = jobs => key => jobs.filter(job => job.jobKey === id)?.[0] ?? [] * * const handler = async (jobs: BatchedJob[]) => { * console.log("Let's do this!") * const {jobKey, variables} = job * // Construct some hypothetical payload with correlation ids and requests * const req = jobs.map(job => ({id: jobKey, data: variables.request})) * // An uncaught exception will not be managed by the library * try { * // Our API wrapper turns that into a request, and returns * // an array of results with ids * const outcomes = await API.post(req) * // Construct a find function for these jobs * const getJob = findJobByKey(jobs) * // Iterate over the results and call the succeed method on the corresponding job, * // passing in the correlated outcome of the API call * outcomes.forEach(res => getJob(res.id)?.complete(res.data)) * } catch (e) { * jobs.forEach(job => job.fail(e.message)) * } * } * * const batchWorker = zbc.createBatchWorker({ * taskType: 'get-data-from-external-api', * taskHandler: handler, * jobBatchMinSize: 10, // at least 10 at a time * jobBatchMaxTime: 60, // or every 60 seconds, whichever comes first * timeout: Duration.seconds.of(80) // 80 second timeout means we have 20 seconds to process at least * }) * ``` */ createBatchWorker(conf) { var _a; if (this.closing) { throw new Error('Client is closing. No worker creation allowed!'); } const config = (0, 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 : (0, 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; } /** * * @description Create a worker that polls the gateway for jobs and executes a job handler when units of work are available. * @example * ``` * const zbc = new ZB.ZBClient() * * const zbWorker = zbc.createWorker({ * taskType: 'demo-service', * taskHandler: myTaskHandler, * }) * * // A job handler must return one of job.complete, job.fail, job.error, or job.forward * // Note: unhandled exceptions in the job handler cause the library to call job.fail * async function myTaskHandler(job) { * zbWorker.log('Task variables', job.variables) * * // Task worker business logic goes here * const updateToBrokerVariables = { * updatedProperty: 'newValue', * } * * const res = await callExternalSystem(job.variables) * * if (res.code === 'SUCCESS') { * return job.complete({ * ...updateToBrokerVariables, * ...res.values * }) * } * if (res.code === 'BUSINESS_ERROR') { * return job.error({ * code: res.errorCode, * message: res.message * }) * } * if (res.code === 'ERROR') { * return job.fail({ * errorMessage: res.message, * retryBackOff: 2000 * }) * } * } * ``` */ createWorker(config) { debug(`Creating worker for task type ${config.taskType}`); if (this.closing) { throw new Error('Client is closing. No worker creation allowed!'); } const idColor = idColors[this.workerCount++ % idColors.length]; // Merge parent client options with worker override const options = { ...this.options, loglevel: this.loglevel, onConnectionError: undefined, onReady: undefined, ...config, }; // 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; } /** * @description Gracefully shut down all workers, draining existing tasks, and return when it is safe to exit. * * @example * ``` * const zbc = new ZBClient() * * zbc.createWorker({ * taskType: * }) * * setTimeout(async () => { * await zbc.close() * console.log('All work completed.') * }), * 5 * 60 * 1000 // 5 mins * ) * ``` */ async close(timeout) { this.closePromise = this.closePromise || new Promise(async (resolve) => { var _a; // Prevent the creation of more workers this.closing = true; await Promise.all(this.workers.map(w => w.close(timeout))); (_a = this.oAuth) === null || _a === void 0 ? void 0 : _a.stopExpiryTimer(); 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; } /** * * @description Explicitly complete a job. The method is useful for manually constructing a worker. * @example * ``` * const zbc = new ZBClient() * zbc.activateJobs({ * maxJobsToActivate: 5, * requestTimeout: 6000, * timeout: 5 * 60 * 1000, * type: 'process-payment', * worker: 'my-worker-uuid' * }).then(jobs => * jobs.forEach(job => * // business logic * zbc.completeJob({ * jobKey: job.key, * variables: {} * )) * ) * }) * ``` */ completeJob(completeJobRequest) { const withStringifiedVariables = (0, lib_1.stringifyVariables)(completeJobRequest); this.logger.logDebug(withStringifiedVariables); return this.executeOperation('completeJob', () => this.grpc.completeJobSync(withStringifiedVariables).catch(e => { if (e.code === GrpcError_1.GrpcError.NOT_FOUND) { 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; })); } // tslint:disable: no-object-literal-type-assertion /** * * @description Create a new process instance. Asynchronously returns a process instance id. * @example * ``` * const zbc = new ZBClient() * * zbc.createProcessInstance({ * bpmnProcessId: 'onboarding-process', * variables: { * customerId: 'uuid-3455' * }, * version: 5 // optional, will use latest by default * }).then(res => console.log(JSON.stringify(res, null, 2))) * * zbc.createProcessInstance({ * bpmnProcessId: 'SkipFirstTask', * variables: { id: random }, * startInstructions: [{elementId: 'second_service_task'}] * }).then(res => (id = res.processInstanceKey)) * ``` */ createProcessInstance(config) { var _a; const request = { bpmnProcessId: config.bpmnProcessId, variables: config.variables, version: config.version || -1, startInstructions: config.startInstructions || [], }; const createProcessInstanceRequest = (0, lib_1.stringifyVariables)({ ...request, startInstructions: request.startInstructions, tenantId: (_a = config.tenantId) !== null && _a !== void 0 ? _a : this.tenantId }); return this.executeOperation('createProcessInstance', () => this.grpc.createProcessInstanceSync(createProcessInstanceRequest)); } /** * * @description Create a process instance, and return a Promise that returns the outcome of the process. * @example * ``` * const zbc = new ZBClient() * * zbc.createProcessInstanceWithResult({ * bpmnProcessId: 'order-process', * variables: { * customerId: 123, * invoiceId: 567 * } * }) * .then(console.log) * ``` */ createProcessInstanceWithResult(config) { var _a, _b; const request = { bpmnProcessId: config.bpmnProcessId, fetchVariables: config.fetchVariables, requestTimeout: config.requestTimeout || 0, variables: config.variables, version: config.version || -1, tenantId: (_a = config.tenantId) !== null && _a !== void 0 ? _a : this.tenantId }; const createProcessInstanceRequest = (0, lib_1.stringifyVariables)({ bpmnProcessId: request.bpmnProcessId, variables: request.variables, version: request.version, tenantId: (_b = request.tenantId) !== null && _b !== void 0 ? _b : this.tenantId }); return this.executeOperation('createProcessInstanceWithResult', () => this.grpc.createProcessInstanceWithResultSync({ fetchVariables: request.fetchVariables, request: createProcessInstanceRequest, requestTimeout: request.requestTimeout, })).then(res => (0, lib_1.parseVariables)(res)); } async deployResource(resource) { const isProcess = (maybeProcess) => !!maybeProcess.process; const isProcessFilename = (maybeProcessFilename) => !!maybeProcessFilename.processFilename; const isDecision = (maybeDecision) => !!maybeDecision.decision; const isDecisionFilename = (maybeDecisionFilename) => !!maybeDecisionFilename.decisionFilename; // default fall-through /* const isForm = ( maybeForm: any ): maybeForm is { form: Buffer; name: string } => !!maybeForm.form */ const isFormFilename = (maybeFormFilename) => !!maybeFormFilename.formFilename; if (isProcessFilename(resource)) { const filename = resource.processFilename; const process = (0, fs_1.readFileSync)(filename); return this.executeOperation('deployResource', () => { var _a; return this.grpc.deployResourceSync({ resources: [ { name: filename, content: process, }, ], tenantId: (_a = resource.tenantId) !== null && _a !== void 0 ? _a : this.tenantId }); }); } else if (isProcess(resource)) { return this.executeOperation('deployResource', () => { var _a; return this.grpc.deployResourceSync({ resources: [ { name: resource.name, content: resource.process, }, ], tenantId: (_a = resource.tenantId) !== null && _a !== void 0 ? _a : this.tenantId }); }); } else if (isDecisionFilename(resource)) { const filename = resource.decisionFilename; const decision = (0, fs_1.readFileSync)(filename); return this.executeOperation('deployResource', () => { var _a; return this.grpc.deployResourceSync({ resources: [ { name: filename, content: decision, }, ], tenantId: (_a = resource.tenantId) !== null && _a !== void 0 ? _a : this.tenantId }); }); } else if (isDecision(resource)) { return this.executeOperation('deployResource', () => { var _a; return this.grpc.deployResourceSync({ resources: [ { name: resource.name, content: resource.decision, }, ], tenantId: (_a = resource.tenantId) !== null && _a !== void 0 ? _a : this.tenantId }); }); } else if (isFormFilename(resource)) { const filename = resource.formFilename; const form = (0, fs_1.readFileSync)(filename); return this.executeOperation('deployResource', () => { var _a; return this.grpc.deployResourceSync({ resources: [ { name: filename, content: form, }, ], tenantId: (_a = resource.tenantId) !== null && _a !== void 0 ? _a : this.tenantId }); }); } else /* if (isForm(resource)) */ { // default fall-through return this.executeOperation('deployResource', () => { var _a; return this.grpc.deployResourceSync({ resources: [ { name: resource.name, content: resource.form } ], tenantId: (_a = resource.tenantId) !== null && _a !== void 0 ? _a : this.tenantId }); }); } } /** * * @description Deploy a process model. * @example * ``` * import { readFileSync } from 'fs' * import { join } from 'path' * * const zbc = new ZBClient() * const bpmnFilePath = join(process.cwd(), 'bpmn', 'onboarding.bpmn') * * // Loading the process model allows you to perform modifications or analysis * const bpmn = readFileSync(bpmnFilePath, 'utf8') * * zbc.deployProcess({ * definition: bpmn, * name: 'onboarding.bpmn' * }) * * // If you don't need access to model contents, simply pass a file path * zbc.deployProcess(bpmnFilePath) * ``` */ 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 (0, pipeable_1.pipe)((0, pure_1.bufferOrFiles)(process), fp_ts_1.either.fold(deploy, files => (0, pipeable_1.pipe)((0, pure_1.mapThese)(files, impure_1.readDefinitionFromFile), fp_ts_1.either.fold(error, deploy)))); } /** * * @description Evaluates a decision. The decision to evaluate can be specified either by using its unique key (as returned by DeployResource), or using the decision ID. When using the decision ID, the latest deployed version of the decision is used. * @example * ``` * const zbc = new ZBClient() * zbc.evaluateDecision({ * decisionId: 'my-decision', * variables: { season: "Fall" } * }).then(res => console.log(JSON.stringify(res, null, 2))) */ evaluateDecision(evaluateDecisionRequest) { // the gRPC API call needs a JSON string, but we accept a JSON object, so we transform it here const variables = JSON.stringify(evaluateDecisionRequest.variables); return this.executeOperation('evaluateDecision', () => { var _a; return this.grpc.evaluateDecisionSync({ ...evaluateDecisionRequest, variables, tenantId: (_a = evaluateDecisionRequest.tenantId) !== null && _a !== void 0 ? _a : this.tenantId }); }); } /** * * @description Fail a job. This is useful if you are using the decoupled completion pattern or building your own worker. * For the retry count, the current count is available in the job metadata. * * @example * ``` * const zbc = new ZBClient() * zbc.failJob( { * jobKey: '345424343451', * retries: 3, * errorMessage: 'Could not get a response from the order invoicing API', * retryBackOff: 30 * 1000 // optional, otherwise available for reactivation immediately * }) * ``` */ failJob(failJobRequest) { return this.executeOperation('failJob', () => this.grpc.failJobSync(failJobRequest)); } /** * @description Return an array of task types contained in a BPMN file or array of BPMN files. This can be useful, for example, to do * @example * ``` * const zbc = new ZBClient() * zbc.getServiceTypesFromBpmn(['bpmn/onboarding.bpmn', 'bpmn/process-sale.bpmn']) * .then(tasktypes => console.log('The task types are:', tasktypes)) * * ``` */ getServiceTypesFromBpmn(files) { const fileArray = typeof files === 'string' ? [files] : files; return lib_1.BpmnParser.getTaskTypes(lib_1.BpmnParser.parseBpmn(fileArray)); } /** * * @description Modify a running process instance. This allows you to move the execution tokens, and change the variables. Added in 8.1. * See the [gRPC protocol documentation](https://docs.camunda.io/docs/apis-clients/grpc/#modifyprocessinstance-rpc). * @example * ``` * zbc.createProcessInstance('SkipFirstTask', {}).then(res => * zbc.modifyProcessInstance({ * processInstanceKey: res.processInstanceKey, * activateInstructions: [{ * elementId: 'second_service_task', * ancestorElementInstanceKey: "-1", * variableInstructions: [{ * scopeId: '', * variables: { second: 1} * }] * }] * }) * ) * ``` */ modifyProcessInstance(modifyProcessInstanceRequest) { return this.executeOperation('modifyProcessInstance', () => { var _a; // We accept JSONDoc for the variableInstructions, but the actual gRPC call needs stringified JSON, so transform it with a mutation (_a = modifyProcessInstanceRequest === null || modifyProcessInstanceRequest === void 0 ? void 0 : modifyProcessInstanceRequest.activateInstructions) === null || _a === void 0 ? void 0 : _a.forEach(a => a.variableInstructions.forEach(v => (v.variables = JSON.stringify(v.variables)))); return this.grpc.modifyProcessInstanceSync({ ...modifyProcessInstanceRequest, }); }); } /** * @description Publish a message to the broker for correlation with a workflow instance. See [this tutorial](https://docs.camunda.io/docs/guides/message-correlation/) for a detailed description of message correlation. * @example * ``` * const zbc = new ZBClient() * * zbc.publishMessage({ * // Should match the "Message Name" in a BPMN Message Catch * name: 'order_status', * correlationKey: 'uuid-124-532-5432', * variables: { * event: 'PROCESSED' * } * }) * ``` */ publishMessage(publishMessageRequest) { return this.executeOperation('publishMessage', () => { var _a; return this.grpc.publishMessageSync((0, lib_1.stringifyVariables)({ ...publishMessageRequest, variables: publishMessageRequest.variables, tenantId: (_a = publishMessageRequest.tenantId) !== null && _a !== void 0 ? _a : this.tenantId })); }); } /** * @description Publish a message to the broker for correlation with a workflow message start event. * For a message targeting a start event, the correlation key is not needed to target a specific running process instance. * However, 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 * @example * ``` * const zbc = new ZBClient() * zbc.publishStartMessage({ * name: 'Start_New_Onboarding_Flow', * variables: { * customerId: 'uuid-348-234-8908' * } * }) * * // To do the same in an idempotent fashion - note: only idempotent during the lifetime of the created instance. * zbc.publishStartMessage({ * name: 'Start_New_Onboarding_Flow', * messageId: 'uuid-348-234-8908', // use customerId to make process idempotent per customer * variables: { * customerId: 'uuid-348-234-8908' * } * }) * ``` */ 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 */ var _a; const publishMessageRequest = { correlationKey: (0, uuid_1.v4)(), ...publishStartMessageRequest, tenantId: (_a = publishStartMessageRequest.tenantId) !== null && _a !== void 0 ? _a : this.tenantId }; return this.executeOperation('publishStartMessage', () => this.grpc.publishMessageSync((0, lib_1.stringifyVariables)({ ...publishMessageRequest, variables: publishMessageRequest.variables || {} }))); } /** * * @description Resolve an incident by incident key. * @example * ``` * type JSONObject = {[key: string]: string | number | boolean | JSONObject} * * const zbc = new ZBClient() * * async updateAndResolveIncident({ * processInstanceId, * incidentKey, * variables * } : { * processInstanceId: string, * incidentKey: string, * variables: JSONObject * }) { * await zbc.setVariables({ * elementInstanceKey: processInstanceId, * variables * }) * await zbc.updateRetries() * zbc.resolveIncident({ * incidentKey * }) * zbc.resolveIncident(incidentKey) * } * * ``` */ resolveIncident(resolveIncidentRequest) { return this.executeOperation('resolveIncident', () => this.grpc.resolveIncidentSync(resolveIncidentRequest)); } /** * * @description Directly modify the variables is a process instance. This can be used with `resolveIncident` to update the process and resolve an incident. * @example * ``` * type JSONObject = {[key: string]: string | number | boolean | JSONObject} * * const zbc = new ZBClient() * * async function updateAndResolveIncident({ * incidentKey, * processInstanceKey, * jobKey, * variableUpdate * } : { * incidentKey: string * processInstanceKey: string * jobKey: string * variableUpdate: JSONObject * }) { * await zbc.setVariables({ * elementInstanceKey: processInstanceKey, * variables: variableUpdate * }) * await zbc.updateJobRetries({ * jobKey, * retries: 1 * }) * return zbc.resolveIncident({ * incidentKey * }) * } * ``` */ 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)); } /** * * @description Fail a job by throwing 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. * This method is useful when building a worker, for example for the decoupled completion pattern. * @example * ``` * type JSONObject = {[key: string]: string | number | boolean | JSONObject} * * interface errorResult { * resultType: 'ERROR' as 'ERROR' * errorCode: string * errorMessage: string * } * * interface successResult { * resultType: 'SUCCESS' as 'SUCCESS' * variableUpdate: JSONObject * } * * type Result = errorResult | successResult * * const zbc = new ZBClient() * * * // This could be a listener on a return queue from an external system * async function handleJob(jobKey: string, result: Result) { * if (resultType === 'ERROR') { * const { errorMessage, errorCode } = result * zbc.throwError({ * jobKey, * errorCode, * errorMessage * }) * } else { * zbc.completeJob({ * jobKey, * variables: result.variableUpdate * }) * } * } * ``` */ throwError(throwErrorRequest) { var _a; const req = (0, lib_1.stringifyVariables)({ ...throwErrorRequest, variables: (_a = throwErrorRequest.variables) !== null && _a !== void 0 ? _a : {} }); return this.executeOperation('throwError', () => this.grpc.throwErrorSync(req)); } /** * @description Return the broker cluster topology. * @example * ``` * const zbc = new ZBClient() * * zbc.topology().then(res => console.res(JSON.stringify(res, null, 2))) * ``` */ topology() { return this.executeOperation('topology', this.grpc.topologySync); } /** * * @description Update the number of retries for a Job. This is useful if a job has zero remaining retries and fails, raising an incident. * @example * ``` * type JSONObject = {[key: string]: string | number | boolean | JSONObject} * * const zbc = new ZBClient() * * async function updateAndResolveIncident({ * incidentKey, * processInstanceKey, * jobKey, * variableUpdate * } : { * incidentKey: string * processInstanceKey: string * jobKey: string * variableUpdate: JSONObject * }) { * await zbc.setVariables({ * elementInstanceKey: processInstanceKey, * variables: variableUpdate * }) * await zbc.updateJobRetries({ * jobKey, * retries: 1 * }) * return zbc.resolveIncident({ * incidentKey * }) * } * ``` */ 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(err) { 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, err); 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 (0, 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(err); } 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