"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const chalk_1 = __importDefault(require("chalk"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const promise_retry_1 = __importDefault(require("promise-retry"));
const url_1 = require("url");
const uuid_1 = require("uuid");
const lib_1 = require("../lib");
const GRPCClient_1 = require("../lib/GRPCClient");
const utils_1 = require("../lib/utils");
const ZBWorker_1 = require("./ZBWorker");
const DEFAULT_GATEWAY_PORT = '26500';
const idColors = [
    chalk_1.default.yellow,
    chalk_1.default.green,
    chalk_1.default.cyan,
    chalk_1.default.magenta,
    chalk_1.default.blue,
];
class ZBClient {
    constructor(gatewayAddress, options = {}) {
        this.closing = false;
        this.workerCount = 0;
        this.workers = [];
        this.maxRetries = 50;
        this.maxRetryTimeout = 5000;
        if (!gatewayAddress) {
            throw new Error('Must provide a gateway address string to constructor');
        }
        this.options = options || {};
        this.options.loglevel =
            process.env.ZB_NODE_LOG_LEVEL ||
                options.loglevel ||
                'INFO';
        const includesProtocol = gatewayAddress.includes('://');
        if (!includesProtocol) {
            gatewayAddress = `grpc://${gatewayAddress}`;
        }
        const url = url_1.parse(gatewayAddress);
        url.port = url.port || DEFAULT_GATEWAY_PORT;
        url.hostname = url.hostname || url.path;
        this.gatewayAddress = `${url.hostname}:${url.port}`;
        this.gRPCClient = new GRPCClient_1.GRPCClient(path.join(__dirname, '../../proto/zeebe.proto'), 'gateway_protocol', 'Gateway', this.gatewayAddress, {}, options.tls);
        this.retry = options.retry !== false;
        this.maxRetries = options.maxRetries || this.maxRetries;
        this.maxRetryTimeout = options.maxRetryTimeout || this.maxRetryTimeout;
    }
    /**
     *
     * @param id - A unique identifier for this worker.
     * @param taskType - The BPMN Zeebe task type that this worker services.
     * @param taskHandler - A handler for activated jobs.
     * @param options - Configuration options for the worker.
     */
    createWorker(id, taskType, taskHandler, options = {}, onConnectionError) {
        if (this.closing) {
            throw new Error('Client is closing. No worker creation allowed!');
        }
        const idColor = idColors[this.workerCount++ % idColors.length];
        const worker = new ZBWorker_1.ZBWorker({
            gRPCClient: this.gRPCClient,
            id,
            idColor,
            onConnectionError,
            options: { ...this.options, ...options },
            taskHandler,
            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
     */
    close() {
        if (this.closePromise) {
            return this.closePromise;
        }
        // Prevent the creation of more workers
        this.closing = true;
        this.closePromise = Promise.all(this.workers.map(w => w.close()));
        return this.closePromise;
    }
    /**
     * Return the broker cluster topology
     */
    topology() {
        return this.executeOperation(this.gRPCClient.topologySync);
    }
    /**
     *
     * @param workflow - A path or array of paths to .bpmn files.
     * @param {redeploy?: boolean} - Redeploy workflow. Defaults to true.
     * If set false, will not redeploy a workflow that exists.
     */
    async deployWorkflow(workflow) {
        const workflows = Array.isArray(workflow) ? workflow : [workflow];
        const readFile = (filename) => {
            if (fs.existsSync(filename)) {
                return fs.readFileSync(filename);
            }
            const name = `${filename}.bpmn`;
            if (fs.existsSync(name)) {
                return fs.readFileSync(name);
            }
            throw new Error(`${filename} not found.`);
        };
        const workFlowRequests = workflows.map(wf => ({
            definition: readFile(wf),
            name: path.basename(wf),
            type: 1,
        }));
        if (workFlowRequests.length > 0) {
            return this.executeOperation(() => this.gRPCClient.deployWorkflowSync({
                workflows: workFlowRequests,
            }));
        }
        else {
            return {
                key: '-1',
                workflows: [],
            };
        }
    }
    /**
     * Return an array of task-types specified in a BPMN file.
     * @param file - Path to bpmn file.
     */
    getServiceTypesFromBpmn(files) {
        if (typeof files === 'string') {
            files = [files];
        }
        return lib_1.BpmnParser.getTaskTypes(lib_1.BpmnParser.parseBpmn(files));
    }
    /**
     * Publish a message to the broker for correlation with a workflow instance.
     * @param publishMessageRequest - The message to publish.
     */
    publishMessage(publishMessageRequest) {
        return this.executeOperation(() => this.gRPCClient.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(() => this.gRPCClient.publishMessageSync(lib_1.stringifyVariables(publishMessageRequest)));
    }
    updateJobRetries(updateJobRetriesRequest) {
        return this.executeOperation(() => this.gRPCClient.updateJobRetriesSync(updateJobRetriesRequest));
    }
    failJob(failJobRequest) {
        return this.executeOperation(() => this.gRPCClient.failJobSync(failJobRequest));
    }
    /**
     *
     * Create and start execution of a workflow instance.
     * @param {string} bpmnProcessId
     * @param {Variables} variables - payload to pass in to the workflow
     * @param {number} [version] - version of the workflow to run. Optional: defaults to latest if not present
     * @returns {Promise<CreateWorkflowInstanceResponse>}
     * @memberof ZBClient
     */
    createWorkflowInstance(bpmnProcessId, variables, version) {
        version = version || -1;
        const createWorkflowInstanceRequest = {
            bpmnProcessId,
            variables: variables,
            version,
        };
        return this.executeOperation(() => this.gRPCClient.createWorkflowInstanceSync(lib_1.stringifyVariables(createWorkflowInstanceRequest)));
    }
    async cancelWorkflowInstance(workflowInstanceKey) {
        utils_1.Utils.validateNumber(workflowInstanceKey, 'workflowInstanceKey');
        return this.executeOperation(() => this.gRPCClient.cancelWorkflowInstanceSync({
            workflowInstanceKey,
        }));
    }
    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(() => this.gRPCClient.setVariablesSync(request));
    }
    resolveIncident(incidentKey) {
        return this.executeOperation(() => this.gRPCClient.resolveIncidentSync(incidentKey));
    }
    /**
     * 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(operation) {
        return this.retry ? this.retryOnFailure(operation) : operation();
    }
    /**
     * 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(operation) {
        const c = console;
        return promise_retry_1.default((retry, n) => {
            if (this.closing) {
                return Promise.resolve();
            }
            if (n > 1) {
                c.error(`gRPC connection is in failed state. Attempt ${n}. Retrying in 5s...`);
            }
            return operation().catch(err => {
                // This could be DNS resolution, or the gRPC gateway is not reachable yet
                const isNetworkError = err.message.indexOf('14') === 0;
                if (isNetworkError) {
                    c.error(`${err.message}`);
                    retry(err);
                }
                throw err;
            });
        }, {
            maxTimeout: this.maxRetryTimeout,
            retries: this.maxRetries,
        });
    }
}
exports.ZBClient = ZBClient;
//# sourceMappingURL=ZBClient.js.map