UNPKG

camunda-external-task-client-js

Version:

Implement your [BPMN Service Task](https://docs.camunda.org/manual/latest/user-guide/process-engine/external-tasks/) in NodeJS.

387 lines (331 loc) 10.2 kB
/* * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH * under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright * ownership. Camunda licenses this file to you under the Apache License, * Version 2.0; you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import events from "events"; import EngineService from "./__internal/EngineService.js"; import TaskService from "./TaskService.js"; import Variables from "./Variables.js"; import { MISSING_BASE_URL, ALREADY_REGISTERED, MISSING_HANDLER, WRONG_INTERCEPTOR, WRONG_MIDDLEWARES, WRONG_SORTING, WRONG_SORTING_SORT_ORDER, WRONG_SORTING_SORT_BY, } from "./__internal/errors.js"; import { isFunction, isArrayOfFunctions, isUndefinedOrNull, } from "./__internal/utils.js"; const defaultOptions = { workerId: "some-random-id", maxTasks: 10, usePriority: true, interval: 300, lockDuration: 50000, autoPoll: true, }; /** * @throws Error * @param customOptions * @param customOptions.baseUrl: baseUrl to api | REQUIRED * @param customOptions.workerId * @param customOptions.maxTasks * @param customOptions.interval * @param customOptions.lockDuration * @param customOptions.autoPoll * @param customOptions.asyncResponseTimeout * @param customOptions.interceptors * @param customOptions.usePriority * @param customOptions.maxParallelExecutions * @param customOptions.sorting * @constructor */ class Client extends events { static SortBy = Object.freeze({ CreateTime: "createTime", }); static SortOrder = Object.freeze({ ASC: "asc", DESC: "desc", }); constructor(customOptions) { super(); /** * Bind member methods */ this.sanitizeOptions = this.sanitizeOptions.bind(this); this.start = this.start.bind(this); this.poll = this.poll.bind(this); this.subscribe = this.subscribe.bind(this); this.executeTask = this.executeTask.bind(this); this.stop = this.stop.bind(this); /** * Initialize data */ this.activeTasksCount = 0; this.sanitizeOptions(customOptions); this.topicSubscriptions = {}; // Map which contains all subscribed topics this.engineService = new EngineService(this.options); this.taskService = new TaskService(this, this.engineService); if (this.options.use) { this.options.use.forEach((f) => f(this)); } if (this.options.autoPoll) { this.start(); } } /** * @throws Error * @param customOptions */ sanitizeOptions(customOptions) { this.options = { ...defaultOptions, ...customOptions }; if ( !customOptions || !customOptions.baseUrl || !customOptions.baseUrl.length ) { throw new Error(MISSING_BASE_URL); } const { interceptors, use } = customOptions; // sanitize request interceptors if ( interceptors && !isFunction(interceptors) && !isArrayOfFunctions(interceptors) ) { throw new Error(WRONG_INTERCEPTOR); } if (isFunction(interceptors)) { this.options.interceptors = [interceptors]; } // sanitize middlewares if (use && !isFunction(use) && !isArrayOfFunctions(use)) { throw new Error(WRONG_MIDDLEWARES); } if (isFunction(use)) { this.options.use = [use]; } if (this.options.sorting) { // sanitize sorting if (!Array.isArray(this.options.sorting)) { throw new Error(WRONG_SORTING); } // sanitize sorting.sortBy const sortByPredicate = (sorting) => !sorting.sortBy || !Object.values(Client.SortBy).includes(sorting.sortBy); if (this.options.sorting.some(sortByPredicate)) { throw new Error(WRONG_SORTING_SORT_BY + Object.values(Client.SortBy)); } // sanitize sorting.sortOrder const sortOrderPredicate = (sorting) => !sorting.sortOrder || !Object.values(Client.SortOrder).includes(sorting.sortOrder); if (this.options.sorting.some(sortOrderPredicate)) { throw new Error( WRONG_SORTING_SORT_ORDER + Object.values(Client.SortOrder) ); } } } /** * Starts polling */ start() { this._isPollAllowed = true; this.poll(); } /** * Polls tasks from engine and executes them */ async poll() { if (!this._isPollAllowed) { return; } this.emit("poll:start"); const { topicSubscriptions, engineService, executeTask, poll } = this; const { maxTasks, usePriority, sorting, interval, asyncResponseTimeout, maxParallelExecutions, } = this.options; const requiredTasksCount = isUndefinedOrNull(maxParallelExecutions) ? maxTasks : Math.min(maxTasks, maxParallelExecutions - this.activeTasksCount); // If all previously received tasks are running now, reschedule polling if (requiredTasksCount <= 0) { return setTimeout(poll, interval); } // setup polling options let pollingOptions = { maxTasks: requiredTasksCount, usePriority: usePriority, }; if (asyncResponseTimeout) { pollingOptions = { ...pollingOptions, asyncResponseTimeout }; } if (sorting) { pollingOptions = { ...pollingOptions, sorting }; } // if there are no topic subscriptions, reschedule polling if (!Object.keys(topicSubscriptions).length) { return setTimeout(poll, interval); } // collect topics that have subscriptions const topics = Object.entries(topicSubscriptions).map( ([ topicName, { businessKey, lockDuration, variables, processDefinitionId, processDefinitionIdIn, processDefinitionKey, processDefinitionKeyIn, processDefinitionVersionTag, processVariables, tenantIdIn, withoutTenantId, localVariables, includeExtensionProperties, }, ]) => { let topic = { topicName, lockDuration }; if (!isUndefinedOrNull(variables)) { topic.variables = variables; } if (!isUndefinedOrNull(businessKey)) { topic.businessKey = businessKey; } if (!isUndefinedOrNull(processDefinitionId)) { topic.processDefinitionId = processDefinitionId; } if (!isUndefinedOrNull(processDefinitionIdIn)) { topic.processDefinitionIdIn = processDefinitionIdIn; } if (!isUndefinedOrNull(processDefinitionKey)) { topic.processDefinitionKey = processDefinitionKey; } if (!isUndefinedOrNull(processDefinitionKeyIn)) { topic.processDefinitionKeyIn = processDefinitionKeyIn; } if (!isUndefinedOrNull(processDefinitionVersionTag)) { topic.processDefinitionVersionTag = processDefinitionVersionTag; } if (!isUndefinedOrNull(processVariables)) { topic.processVariables = processVariables; } if (!isUndefinedOrNull(tenantIdIn)) { topic.tenantIdIn = tenantIdIn; } if (!isUndefinedOrNull(withoutTenantId)) { topic.withoutTenantId = withoutTenantId; } if (!isUndefinedOrNull(localVariables)) { topic.localVariables = localVariables; } if (!isUndefinedOrNull(includeExtensionProperties)) { topic.includeExtensionProperties = includeExtensionProperties; } return topic; } ); const requestBody = { ...pollingOptions, topics }; try { const tasks = await engineService.fetchAndLock(requestBody); this.emit("poll:success", tasks); tasks.forEach(executeTask); } catch (e) { this.emit("poll:error", e); } setTimeout(poll, interval); } /** * Subscribes a handler by adding a topic subscription * @param topic * @param customOptions * @param customOptions.lockDuration * @param handler */ subscribe(topic, customOptions, handler) { const topicSubscriptions = this.topicSubscriptions; const options = this.options; const lockDuration = options.lockDuration; if (topicSubscriptions[topic]) { throw new Error(ALREADY_REGISTERED); } // handles the case if there is no options being if (typeof customOptions === "function") { handler = customOptions; customOptions = null; } if (!handler) { throw new Error(MISSING_HANDLER); } const unsubscribe = () => { delete topicSubscriptions[topic]; this.emit("unsubscribe"); }; const topicSubscription = { handler, unsubscribe, lockDuration, ...customOptions, }; topicSubscriptions[topic] = topicSubscription; this.emit("subscribe", topic, topicSubscription); return topicSubscription; } /** * Executes task using the worker registered to its topic */ async executeTask(task) { const taskService = this.taskService; const topicSubscription = this.topicSubscriptions[task.topicName]; const variables = new Variables(task.variables, { readOnly: true, processInstanceId: task.processInstanceId, engineService: this.engineService, }); this.activeTasksCount++; const newTask = { ...task, variables }; try { await topicSubscription.handler({ task: newTask, taskService }); this.emit("handler:success", task); } catch (e) { this.emit("handler:error", e); } finally { this.activeTasksCount--; } } /** * Stops polling */ stop() { this._isPollAllowed = false; this.emit("poll:stop"); } } export default Client;