"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
    for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BatchCluster = exports.ExpectedTerminationReasons = exports.Task = exports.pidExists = exports.kill = exports.SimpleParser = exports.findStreamFlushMillis = exports.Deferred = exports.BatchProcess = exports.BatchClusterOptions = void 0;
const node_events_1 = __importDefault(require("node:events"));
const node_process_1 = __importDefault(require("node:process"));
const node_timers_1 = __importDefault(require("node:timers"));
const BatchClusterEventCoordinator_1 = require("./BatchClusterEventCoordinator");
const Deferred_1 = require("./Deferred");
const OptionsVerifier_1 = require("./OptionsVerifier");
const Pids_1 = require("./Pids");
const ProcessPoolManager_1 = require("./ProcessPoolManager");
const TaskQueueManager_1 = require("./TaskQueueManager");
var BatchClusterOptions_1 = require("./BatchClusterOptions");
Object.defineProperty(exports, "BatchClusterOptions", { enumerable: true, get: function () { return BatchClusterOptions_1.BatchClusterOptions; } });
var BatchProcess_1 = require("./BatchProcess");
Object.defineProperty(exports, "BatchProcess", { enumerable: true, get: function () { return BatchProcess_1.BatchProcess; } });
var Deferred_2 = require("./Deferred");
Object.defineProperty(exports, "Deferred", { enumerable: true, get: function () { return Deferred_2.Deferred; } });
var FindFlushThresholds_1 = require("./FindFlushThresholds");
Object.defineProperty(exports, "findStreamFlushMillis", { enumerable: true, get: function () { return FindFlushThresholds_1.findStreamFlushMillis; } });
__exportStar(require("./Logger"), exports);
var Parser_1 = require("./Parser");
Object.defineProperty(exports, "SimpleParser", { enumerable: true, get: function () { return Parser_1.SimpleParser; } });
var Pids_2 = require("./Pids");
Object.defineProperty(exports, "kill", { enumerable: true, get: function () { return Pids_2.kill; } });
Object.defineProperty(exports, "pidExists", { enumerable: true, get: function () { return Pids_2.pidExists; } });
var Task_1 = require("./Task");
Object.defineProperty(exports, "Task", { enumerable: true, get: function () { return Task_1.Task; } });
var WhyNotHealthy_1 = require("./WhyNotHealthy");
Object.defineProperty(exports, "ExpectedTerminationReasons", { enumerable: true, get: function () { return WhyNotHealthy_1.ExpectedTerminationReasons; } });
/**
 * BatchCluster instances manage 0 or more homogeneous child processes, and
 * provide the main interface for enqueuing `Task`s via `enqueueTask`.
 *
 * Given the large number of configuration options, the constructor
 * receives a single options hash. The most important of these are the
 * `ChildProcessFactory`, which specifies the factory that creates
 * ChildProcess instances, and `BatchProcessOptions`, which specifies how
 * child tasks can be verified and shut down.
 */
class BatchCluster {
    #logger;
    options;
    #processPool;
    #taskQueue;
    #eventCoordinator;
    #onIdleRequested = false;
    #onIdleInterval;
    #endPromise;
    emitter = new node_events_1.default.EventEmitter();
    constructor(opts) {
        this.options = (0, OptionsVerifier_1.verifyOptions)({ ...opts, observer: this.emitter });
        this.#logger = this.options.logger;
        // Initialize the managers
        this.#processPool = new ProcessPoolManager_1.ProcessPoolManager(this.options, this.emitter, () => this.#onIdleLater());
        this.#taskQueue = new TaskQueueManager_1.TaskQueueManager(this.#logger, this.emitter);
        // Initialize event coordinator to handle all event processing
        this.#eventCoordinator = new BatchClusterEventCoordinator_1.BatchClusterEventCoordinator(this.emitter, {
            streamFlushMillis: this.options.streamFlushMillis,
            logger: this.#logger,
        }, () => this.#onIdleLater());
        if (this.options.onIdleIntervalMillis > 0) {
            this.#onIdleInterval = node_timers_1.default.setInterval(() => this.#onIdleLater(), this.options.onIdleIntervalMillis);
            this.#onIdleInterval.unref(); // < don't prevent node from exiting
        }
        if (this.options.cleanupChildProcsOnExit) {
            node_process_1.default.once("beforeExit", this.#beforeExitListener);
            node_process_1.default.once("exit", this.#exitListener);
        }
    }
    /**
     * @see BatchClusterEvents
     */
    on = this.emitter.on.bind(this.emitter);
    /**
     * @see BatchClusterEvents
     * @since v9.0.0
     */
    off = this.emitter.off.bind(this.emitter);
    // void (not return) because event listeners ignore returned promises.
    // The async work keeps the process alive until complete regardless.
    #beforeExitListener = () => {
        void this.end(true);
    };
    /**
     * Synchronously kill all child processes on exit.
     *
     * The `exit` event only allows synchronous operations - the event loop is
     * about to terminate, so any async work (like `this.end()`) would be
     * discarded and never execute. We must force-kill immediately.
     */
    #exitListener = () => {
        for (const pid of this.#processPool.pids()) {
            (0, Pids_1.kill)(pid, true);
        }
    };
    get ended() {
        return this.#endPromise != null;
    }
    /**
     * Shut down this instance, and all child processes.
     * @param gracefully should an attempt be made to finish in-flight tasks, or
     * should we force-kill child PIDs.
     */
    // NOT ASYNC so state transition happens immediately
    end(gracefully = true) {
        this.#logger().info("BatchCluster.end()", { gracefully });
        if (this.#endPromise == null) {
            this.emitter.emit("beforeEnd");
            if (this.#onIdleInterval != null)
                node_timers_1.default.clearInterval(this.#onIdleInterval);
            this.#onIdleInterval = undefined;
            let removeBackstop;
            if (this.options.cleanupChildProcsOnExit) {
                // Remove only beforeExit to prevent re-calling end().
                node_process_1.default.removeListener("beforeExit", this.#beforeExitListener);
                // Snapshot live PIDs BEFORE closeChildProcesses empties #procs.
                const livePids = this.#processPool.pids();
                // Swap the pool-based listener for a snapshot-based backstop.
                node_process_1.default.removeListener("exit", this.#exitListener);
                const backstop = () => {
                    for (const pid of livePids)
                        (0, Pids_1.kill)(pid, true);
                };
                node_process_1.default.once("exit", backstop);
                removeBackstop = () => node_process_1.default.removeListener("exit", backstop);
            }
            this.#endPromise = new Deferred_1.Deferred().observe(this.closeChildProcesses(gracefully).then(() => {
                // Async cleanup done — backstop is no longer needed.
                removeBackstop?.();
                this.emitter.emit("end");
            }));
        }
        return this.#endPromise;
    }
    /**
     * Submits `task` for processing by a `BatchProcess` instance
     *
     * @return a Promise that is resolved or rejected once the task has been
     * attempted on an idle BatchProcess
     */
    enqueueTask(task) {
        if (this.ended) {
            task.reject(new Error("BatchCluster has ended, cannot enqueue " + task.command));
            return task.promise;
        }
        this.#taskQueue.enqueue(task);
        // Run #onIdle now (not later), to make sure the task gets enqueued asap if
        // possible
        this.#onIdleLater();
        // (BatchProcess will call our #onIdleLater when tasks settle or when they
        // exit)
        return task.promise;
    }
    /**
     * @return true if all previously-enqueued tasks have settled
     */
    get isIdle() {
        return this.pendingTaskCount === 0 && this.busyProcCount === 0;
    }
    /**
     * @return the number of pending tasks
     */
    get pendingTaskCount() {
        return this.#taskQueue.pendingTaskCount;
    }
    /**
     * @returns {number} the mean number of tasks completed by child processes
     */
    get meanTasksPerProc() {
        return this.#eventCoordinator.meanTasksPerProc;
    }
    /**
     * @return the total number of child processes created by this instance
     */
    get spawnedProcCount() {
        return this.#processPool.spawnedProcCount;
    }
    /**
     * @return the current number of spawned child processes. Some (or all) may be idle.
     */
    get procCount() {
        return this.#processPool.processCount;
    }
    /**
     * @return the current number of child processes currently servicing tasks
     */
    get busyProcCount() {
        return this.#processPool.busyProcCount;
    }
    get startingProcCount() {
        return this.#processPool.startingProcCount;
    }
    /**
     * @return the current pending Tasks (mostly for testing)
     */
    get pendingTasks() {
        return this.#taskQueue.pendingTasks;
    }
    /**
     * @return the current running Tasks (mostly for testing)
     */
    get currentTasks() {
        return this.#processPool.currentTasks();
    }
    /**
     * For integration tests:
     */
    get internalErrorCount() {
        return this.#eventCoordinator.internalErrorCount;
    }
    /**
     * Verify that each BatchProcess PID is actually alive.
     *
     * @return the spawned PIDs that are still in the process table.
     */
    pids() {
        return this.#processPool.pids();
    }
    /**
     * For diagnostics. Contents may change.
     */
    stats() {
        return {
            pendingTaskCount: this.pendingTaskCount,
            currentProcCount: this.procCount,
            readyProcCount: this.#processPool.readyProcCount,
            maxProcCount: this.options.maxProcs,
            internalErrorCount: this.#eventCoordinator.internalErrorCount,
            msBeforeNextSpawn: this.#processPool.msBeforeNextSpawn,
            spawnedProcCount: this.spawnedProcCount,
            childEndCounts: this.childEndCounts,
            ending: this.#endPromise != null,
            ended: false === this.#endPromise?.pending,
        };
    }
    /**
     * Get ended process counts (used for tests)
     */
    countEndedChildProcs(why) {
        return this.#eventCoordinator.countEndedChildProcs(why);
    }
    get childEndCounts() {
        return this.#eventCoordinator.childEndCounts;
    }
    /**
     * Shut down any currently-running child processes. New child processes will
     * be started automatically to handle new tasks.
     */
    async closeChildProcesses(gracefully = true) {
        return this.#processPool.closeChildProcesses(gracefully);
    }
    /**
     * Reset the maximum number of active child processes to `maxProcs`. Note that
     * this is handled gracefully: child processes are only reduced as tasks are
     * completed.
     */
    setMaxProcs(maxProcs) {
        this.#processPool.setMaxProcs(maxProcs);
        // we may now be able to handle an enqueued task. Vacuum pids and see:
        this.#onIdleLater();
    }
    #onIdleLater = () => {
        if (!this.#onIdleRequested) {
            this.#onIdleRequested = true;
            node_timers_1.default.setTimeout(() => this.#onIdle(), 1);
        }
    };
    // NOT ASYNC: updates internal state:
    #onIdle() {
        this.#onIdleRequested = false;
        void this.vacuumProcs();
        while (this.#execNextTask()) {
            //
        }
        void this.#maybeSpawnProcs();
    }
    /**
     * Run maintenance on currently spawned child processes. This method is
     * normally invoked automatically as tasks are enqueued and processed.
     *
     * Only public for tests.
     */
    // NOT ASYNC: updates internal state. only exported for tests.
    vacuumProcs() {
        return this.#processPool.vacuumProcs();
    }
    /**
     * NOT ASYNC: updates internal state.
     * @return true iff a task was submitted to a child process
     */
    #execNextTask() {
        if (this.ended)
            return false;
        const readyProc = this.#processPool.findReadyProcess();
        return this.#taskQueue.tryAssignNextTask(readyProc);
    }
    async #maybeSpawnProcs() {
        return this.#processPool.maybeSpawnProcs(this.#taskQueue.pendingTaskCount, this.ended);
    }
}
exports.BatchCluster = BatchCluster;
//# sourceMappingURL=BatchCluster.js.map