"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Worker = void 0;
const base_js_1 = require("./base.js");
const proc_js_1 = require("./proc.js");
const node_util_1 = require("node:util");
const node_worker_threads_1 = require("node:worker_threads");
const throw_to_parser_js_1 = require("./throw-to-parser.js");
/**
 * Class representing a TAP generating node worker thread
 *
 * Instantiated by `t.worker()`, typically.
 *
 * @internal
 */
class Worker extends base_js_1.Base {
    eval;
    filename;
    cb;
    worker = null;
    #childId;
    env;
    // doesn't have to be cryptographically secure, just a gut check
    #childKey = String(Math.random());
    #timedOut;
    #workerEnded = false;
    constructor(options) {
        const { filename } = options;
        if (!filename) {
            throw new TypeError('no filename provided for t.worker()');
        }
        options.name =
            options.name || Worker.procName(filename, !!options.eval);
        super(options);
        this.#childId = String(options.childId || proc_js_1.env.TAP_CHILD_ID || '1');
        this.filename = filename;
        this.eval = !!options.eval;
        this.env = {
            ...(options.env || proc_js_1.env),
            TAP: '1',
            TAP_CHILD_ID: this.#childId,
            TAP_BAIL: this.bail ? '1' : '0',
            TAP_CHILD_KEY: this.#childKey,
        };
        this.bail = !!options.bail;
    }
    main(cb) {
        this.cb = cb;
        this.setTimeout(this.options.timeout || 0);
        this.parser.on('comment', c => {
            const tomatch = c.match(/# timeout=([0-9]+)\n$/);
            if (tomatch) {
                this.setTimeout(+tomatch[1]);
            }
        });
        this.parent?.emit('worker', this);
        const options = {
            ...this.options,
            eval: this.eval,
            stdout: true,
            env: this.env,
        };
        this.emit('preprocess', options);
        this.worker = new node_worker_threads_1.Worker(this.filename, options);
        this.worker.stdout.pipe(this.parser);
        this.worker.on('error', e => this.threw(e));
        this.worker.on('exit', () => this.#onworkerexit());
        this.worker.on('message', msg => {
            const m = msg;
            if (m && m.key === this.#childKey && m.child === this.#childId) {
                this.setTimeout(m.setTimeout);
                return;
            }
            this.comment(...(Array.isArray(msg) ? msg : [msg]));
        });
        this.emit('process', this.worker);
    }
    threw(er, extra) {
        return (0, throw_to_parser_js_1.throwToParser)(this.parser, super.threw(er, extra));
    }
    #onworkerexit() {
        this.#workerEnded = true;
        if (this.results)
            this.#callCb();
    }
    timeout(options = { expired: this.name }) {
        this.#timedOut = options;
        // try to send the timeout signal.  If the child test process is
        // using node-tap as the test runner, and not caught in a busy
        // loop, it will trigger a dump of outstanding handles and refs.
        // If that doesn't do the job, then we fall back to thread termination.
        const worker = this.worker;
        if (worker) {
            try {
                worker.postMessage({
                    tapAbort: 'timeout',
                    key: this.#childKey,
                    child: this.childId,
                });
                /* c8 ignore start */
            }
            catch { }
            // need to ignore this bit because there's no way (by design) to
            // ignore the timeout signal, but it's theoretically possible that
            // it could be dropped or some busy-wait process prevents it from
            // being processed.
            const t = setTimeout(() => {
                // try to give it a chance to note the timeout and report handles
                try {
                    worker.terminate();
                }
                catch (er) { }
            }, 500);
            if (t.unref)
                t.unref();
            /* c8 ignore stop */
        }
    }
    oncomplete(results) {
        this.results = results;
        if (this.#workerEnded)
            this.#callCb();
    }
    #callCb() {
        if (this.#timedOut)
            super.timeout(this.#timedOut);
        const { results } = this;
        /* c8 ignore start */
        if (!results) {
            throw new Error('worker calling cb before parser ended??');
        }
        /* c8 ignore stop */
        // worker closing with no tests is treated as a skip.
        if (results.plan && results.plan.skipAll) {
            this.options.skip = results.plan.skipReason || true;
        }
        super.oncomplete(results);
        const { cb } = this;
        /* c8 ignore start */
        if (!cb) {
            throw new Error('tap worker finished before receiving cb??');
        }
        /* c8 ignore stop */
        cb();
    }
    comment(...args) {
        const body = (0, node_util_1.format)(...args);
        const message = ('# ' + body.split(/\r?\n/).join('\n# ')).trim() + '\n';
        // it's almost impossible to send a message that will arrive
        // AFTER the stdout closes, as this only happens when the worker
        // thread closes, but it is theoretically possible, since messages
        // are asynchronous.
        /* c8 ignore start */
        if (this.parser.results) {
            if (this.parent && !this.parent.results) {
                this.parent.parser.write(message);
            }
            else {
                console.log(message.trimEnd());
            }
        }
        else {
            /* c8 ignore stop */
            this.parser.write(message);
        }
    }
    endAll() {
        if (this.worker) {
            this.parser.abort('test unfinished');
            this.worker.terminate();
        }
    }
    static procName(filename, ev) {
        const pref = '<worker> node';
        if (ev)
            return `${pref} -e <inline code>`;
        if (filename.indexOf(proc_js_1.cwd) === 0) {
            filename = './' + filename.substring(proc_js_1.cwd.length + 1);
        }
        filename = filename.replace(/\\/g, '/');
        return `${pref} ${filename}`;
    }
}
exports.Worker = Worker;
//# sourceMappingURL=worker.js.map