"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 uuid = __importStar(require("uuid"));
const lib_1 = require("../lib");
const ZBLogger_1 = require("../lib/ZBLogger");
class ZBWorker {
    constructor({ gRPCClient, id, idColor, onConnectionError, options, taskHandler, taskType, zbClient, }) {
        this.activeJobs = 0;
        this.closing = false;
        this.closed = false;
        this.errored = false;
        this.id = uuid.v4();
        this.cancelWorkflowOnException = false;
        this.work = () => {
            this.logger.log(`Ready for ${this.taskType}...`);
            this.activateJobs();
            this.pollHandle = setInterval(() => this.activateJobs(), this.pollInterval);
        };
        this.internalLog = (ns) => (msg) => 
        // tslint:disable-next-line:no-console
        console.log(`${ns}:`, msg);
        this.handleGrpcError = (err) => {
            if (!this.errored) {
                if (this.onConnectionErrorHandler) {
                    this.onConnectionErrorHandler(err);
                    this.errored = true;
                }
                else {
                    this.internalLog(chalk_1.default.red(`ERROR: `) +
                        chalk_1.default.yellow(`${this.id} - ${this.taskType}`))(chalk_1.default.red(err.details));
                    this.errored = true;
                }
            }
        };
        options = options || {};
        if (!taskType) {
            throw new Error('Missing taskType');
        }
        if (!taskHandler) {
            throw new Error('Missing taskHandler');
        }
        this.taskHandler = taskHandler;
        this.taskType = taskType;
        this.maxActiveJobs = options.maxJobsToActivate || 32;
        this.timeout = options.timeout || 1000;
        this.pollInterval = options.pollInterval || 100;
        this.id = id || uuid.v4();
        this.gRPCClient = gRPCClient;
        this.onConnectionErrorHandler = onConnectionError;
        const loglevel = options.loglevel || 'INFO';
        this.cancelWorkflowOnException =
            options.failWorkflowOnException || false;
        this.zbClient = zbClient;
        this.logger = new ZBLogger_1.ZBLogger({
            color: idColor,
            id: this.id,
            loglevel,
            namespace: 'ZBWorker',
            stdout: options.stdout || console,
            taskType: this.taskType,
        });
        this.work();
    }
    /**
     * Returns a promise that the worker has stopped accepting tasks and
     * has drained all current active tasks. Will reject if you try to call it more than once.
     */
    close() {
        if (this.closePromise) {
            return this.closePromise;
        }
        this.closePromise = new Promise(resolve => {
            // this.closing prevents the worker from starting work on any new tasks
            this.closing = true;
            if (this.pollHandle) {
                // Stop polling for jobs
                clearInterval(this.pollHandle);
            }
            // We will resolve the Promise in any case at two seconds over the worker timeout period.
            // This deals with phantom tasks, which will have timed out on the server.
            const closeTimeout = setTimeout(resolve, this.timeout + 2000);
            // If we have no active tasks right now, resolve immediately.
            // There could be a race condition here if we just polled the server and it is about to return jobs.
            // In any case, we do not start working on those jobs, so they will time out on the server.
            if (this.activeJobs <= 0) {
                resolve();
            }
            // When this.activeJobs reaches 0, this will resolve the promise to close. Called in this.drainOne().
            this.closeCallback = () => {
                clearTimeout(closeTimeout);
                this.closed = true;
                resolve();
            };
        });
        return this.closePromise;
    }
    completeJob(completeJobRequest) {
        const withStringifiedVariables = lib_1.stringifyVariables(completeJobRequest);
        this.logger.debug(withStringifiedVariables);
        return this.gRPCClient.completeJobSync(withStringifiedVariables);
    }
    onConnectionError(handler) {
        this.onConnectionErrorHandler = handler;
    }
    log(msg) {
        this.logger.log(msg);
    }
    getNewLogger(options) {
        return new ZBLogger_1.ZBLogger({
            ...options,
            id: this.id,
            taskType: this.taskType,
        });
    }
    activateJobs() {
        /**
         * It would be good to use a mutex to prevent multiple overlapping polling invocations.
         * However, the stream.on("data") is only called when there are jobs, so it there isn't an obvious (to me)
         * way to release the mutex.
         */
        let stream;
        if (this.activeJobs >= this.maxActiveJobs) {
            this.logger.log(`Polling cancelled - ${this.taskType} has ${this.activeJobs} and a capacity of ${this.maxActiveJobs}.`);
            return;
        }
        const amount = this.maxActiveJobs - this.activeJobs;
        const activateJobsRequest = {
            maxJobsToActivate: amount,
            timeout: this.timeout,
            type: this.taskType,
            worker: this.id,
        };
        try {
            stream = this.gRPCClient.activateJobsStream(activateJobsRequest);
        }
        catch (err) {
            return this.handleGrpcError(err);
        }
        const taskHandler = this.taskHandler;
        stream.on('data', (res) => {
            // If we are closing, don't start working on these jobs. They will have to be timed out by the server.
            if (this.closing) {
                return;
            }
            const parsedVariables = res.jobs.map(lib_1.parseVariables);
            this.activeJobs += parsedVariables.length;
            // Call task handler for each new job
            parsedVariables.forEach(async (job) => {
                const customHeaders = JSON.parse(job.customHeaders || '{}');
                /**
                 * Client-side timeout handler - removes jobs from the activeJobs count if timed out,
                 * prevents diminished capacity of this worker due to handler misbehaviour.
                 */
                let taskTimedout = false;
                const taskId = uuid.v4();
                this.logger.debug(`Setting ${this.taskType} task timeout for ${taskId} to ${this.timeout}`);
                const timeoutCancel = setTimeout(() => {
                    taskTimedout = true;
                    this.drainOne();
                    this.logger.log(`Timed out task ${taskId} for ${this.taskType}`);
                }, this.timeout);
                // Any unhandled exception thrown by the user-supplied code will bubble up and throw here.
                // The task timeout handler above will deal with it.
                try {
                    /**
                     * Construct the backward compatible worker callback
                     * See https://stackoverflow.com/questions/12766528/build-a-function-object-with-properties-in-typescript
                     * for an explanation of the pattern used.
                     *
                     * It is backward compatible because the old success handler is available as the function:
                     *  complete(variables?: object).
                     *
                     * The new API is available as
                     * complete.success(variables?: object) and complete.failure(errorMessage: string, retries?: number)
                     *
                     * To halt execution of the business process and raise an incident in Operate, call
                     * complete(errorMessage, 0)
                     */
                    const workerCallback = (() => {
                        const shadowWorkerCallback = (completedVariables = {}) => {
                            this.completeJob({
                                jobKey: job.key,
                                variables: completedVariables,
                            });
                            clearInterval(timeoutCancel);
                            if (!taskTimedout) {
                                this.drainOne();
                                this.logger.debug(`Completed task ${taskId} for ${this.taskType}`);
                                return true;
                            }
                            else {
                                this.logger.debug(`Completed task ${taskId} for ${this.taskType}, however it had timed out.`);
                                return false;
                            }
                        };
                        shadowWorkerCallback.success = shadowWorkerCallback;
                        shadowWorkerCallback.failure = (errorMessage, retries = Math.max(0, job.retries - 1)) => {
                            this.zbClient.failJob({
                                errorMessage,
                                jobKey: job.key,
                                retries,
                            });
                            this.logger.debug(`Failed job ${job.key} - ${errorMessage}`);
                            clearInterval(timeoutCancel);
                        };
                        return shadowWorkerCallback;
                    })();
                    await taskHandler({ ...job, customHeaders: { ...customHeaders } }, workerCallback, this);
                }
                catch (e) {
                    this.logger.error('Caught an unhandled exception in a task handler:');
                    this.logger.error(e);
                    this.logger.info(`Failing job ${job.key}`);
                    const retries = job.retries - 1;
                    this.zbClient.failJob({
                        errorMessage: `Unhandled exception in task handler ${e}`,
                        jobKey: job.key,
                        retries,
                    });
                    if (this.cancelWorkflowOnException) {
                        const workflowInstanceKey = job.workflowInstanceKey;
                        this.logger.debug(`Cancelling workflow ${workflowInstanceKey}`);
                        this.drainOne();
                        this.zbClient.cancelWorkflowInstance(workflowInstanceKey);
                    }
                    else {
                        if (retries > 0) {
                            this.logger.debug(`The Zeebe engine will handle the retry. Retries left: ${retries}`);
                        }
                        else {
                            this.logger.debug('No retries left for this task');
                        }
                    }
                }
            });
        });
        stream.on('error', (err) => this.handleGrpcError(err));
    }
    drainOne() {
        this.activeJobs--;
        // If we are closing and hit zero active jobs, resolve the closing promise.
        if (this.activeJobs <= 0 && this.closing) {
            if (this.closeCallback && !this.closed) {
                this.closeCallback();
            }
        }
    }
}
exports.ZBWorker = ZBWorker;
//# sourceMappingURL=ZBWorker.js.map