/**
 * Pure JS replacement for @typescript/libsyncrpc.
 *
 * Spawns a child process and communicates with it synchronously over
 * stdin/stdout pipes using the same MessagePack-based tuple protocol:
 *   [MessageType (u8), method (bin), payload (bin)]
 *
 * Synchronous I/O is achieved by calling fs.readSync / fs.writeSync
 * directly on the pipe file descriptors obtained from the spawned
 * ChildProcess.
 */
import { spawn, } from "node:child_process";
import { closeSync, openSync, readSync, writeSync, } from "node:fs";
import { binHeaderSize, MSGPACK_BIN16, MSGPACK_BIN32, MSGPACK_BIN8, MSGPACK_FIXARRAY3, MSGPACK_UINT8, writeBinHeader, } from "./node/msgpack.js";
// ── MessageType constants ────────────────────────────────────────────
// Sent by channel (parent → child)
const MSG_REQUEST = 1;
const MSG_CALL_RESPONSE = 2;
const MSG_CALL_ERROR = 3;
// Sent by child (child → parent)
const MSG_RESPONSE = 4;
const MSG_ERROR = 5;
const MSG_CALL = 6;
// Pre-allocated buffer used by Atomics.wait for tiny sleeps when a
// non-blocking fd returns EAGAIN.
const sleepBuf = new Int32Array(new SharedArrayBuffer(4));
// Shared empty buffer – avoids allocating Buffer.alloc(0) on every
// zero-length bin field.
const EMPTY_BUF = Buffer.alloc(0);
// ── Global cleanup tracking ─────────────────────────────────────────
// Track all live child processes so they can be killed on process exit.
// This mimics the auto-cleanup behavior of the native libsyncrpc module,
// whose Rust/C++ destructors would kill children automatically.
const liveChildren = new Set();
process.on("exit", () => {
    for (const child of liveChildren) {
        try {
            child.kill();
        }
        catch {
            // swallow – process may already be dead
        }
    }
    liveChildren.clear();
});
/**
 * SyncRpcChannel – drop-in replacement for the native libsyncrpc class.
 *
 * API surface intentionally matches the original:
 *   - constructor(exe, args)
 *   - requestSync(method, payload): string
 *   - requestBinarySync(method, payload): Uint8Array
 *   - registerCallback(name, cb)
 *   - close()
 *
 * The protocol is unversioned; both sides (this JS channel and the Go
 * child process) must be built from the same tree.
 *
 * This class is **not** thread-safe. All calls must originate from a
 * single thread — do not share an instance across worker threads.
 */
export class SyncRpcChannel {
    child;
    readFd;
    writeFd;
    pipeFd;
    callbacks = new Map();
    methodBufCache = new Map();
    // When true, the payload byte lengths of each request/response are recorded
    // and exposed via the `last*` fields below. These count only the JSON/binary
    // payload bytes, not the MessagePack tuple framing (message type, method
    // name, and length headers).
    collectTiming;
    // Per-request payload byte measurements for the most recently completed
    // request. Only meaningful when `collectTiming` is true. Callers read these
    // immediately after a request returns (the channel is strictly serial).
    lastBytesSent = 0;
    lastBytesReceived = 0;
    _msgType = 0;
    _msgName = EMPTY_BUF;
    _msgPayload = EMPTY_BUF;
    headerBuf = Buffer.allocUnsafe(4);
    // Read-ahead buffer – reduces readSync syscalls by buffering data from the pipe.
    readBuf = Buffer.allocUnsafe(65536);
    readBufPos = 0;
    readBufLen = 0;
    // Write buffer – assembles entire tuples for a single writeSync.
    writeBuf = Buffer.allocUnsafe(65536);
    constructor(exe, args, collectTiming = false) {
        this.collectTiming = collectTiming;
        const isWindows = process.platform === "win32";
        if (isWindows) {
            // On Windows, libuv pipe handles don't expose POSIX fds, so
            // readSync/writeSync can't be used on stdio pipes. Instead,
            // we create a Windows named pipe path, pass it to the child
            // via --pipe, and open it with fs.openSync which returns a
            // real C-runtime fd backed by a proper HANDLE.
            const pipePath = `\\\\.\\pipe\\tsgo-sync-${process.pid}-${Date.now()}`;
            this.child = spawn(exe, [...args, "--pipe", pipePath], {
                stdio: ["ignore", "ignore", "ignore"],
            });
            // Retry openSync until the child creates the named pipe.
            let fd;
            for (let i = 0; i < 500; i++) {
                try {
                    fd = openSync(pipePath, "r+");
                    break;
                }
                catch {
                    if (this.child.exitCode !== null) {
                        throw new Error(`Child process exited with code ${this.child.exitCode} before pipe was ready`);
                    }
                    Atomics.wait(sleepBuf, 0, 0, 10);
                }
            }
            if (fd === undefined) {
                this.child.kill();
                throw new Error("SyncRpcChannel: timed out connecting to named pipe");
            }
            this.readFd = fd;
            this.writeFd = fd;
            this.pipeFd = fd;
        }
        else {
            // POSIX: use stdio pipe file descriptors directly.
            // stderr must NOT inherit the parent's fd — sharing a TTY/pipe with
            // the parent lets Node's SignalExit/ResetStdio storm fcntl on the
            // shared fd when the embedded/sibling Go runtime is present.
            this.child = spawn(exe, args, {
                stdio: ["pipe", "pipe", "pipe"],
            });
            const stdout = this.child.stdout;
            const stdin = this.child.stdin;
            const stderr = this.child.stderr;
            this.readFd = stdout._handle.fd;
            this.writeFd = stdin._handle.fd;
            if (typeof this.readFd !== "number" || this.readFd < 0 || typeof this.writeFd !== "number" || this.writeFd < 0) {
                stdout.destroy();
                stdin.destroy();
                stderr?.destroy();
                this.child.kill();
                throw new Error("SyncRpcChannel: could not obtain pipe file descriptors.");
            }
            // Set the pipe handles to blocking mode. Under node --test's
            // process isolation, pipes are created in non-blocking mode
            // (for the IPC channel). This causes readSync/writeSync to get
            // EAGAIN, requiring costly 1ms sleeps per retry. Setting
            // blocking mode ensures readSync blocks properly until data
            // arrives, matching the behavior of the native libsyncrpc.
            stdout._handle.setBlocking?.(true);
            stdin._handle.setBlocking?.(true);
            // Prevent Node's event-loop from reading stdout/stderr or keeping
            // the process alive – we will use fs.readSync exclusively.
            stdout.pause();
            stdout.unref();
            stdin.unref();
            stderr?.pause();
            stderr?.unref();
        }
        // Track for auto-cleanup on process exit.
        liveChildren.add(this.child);
        this.child.unref();
    }
    // ── Public API ──────────────────────────────────────────────────
    /**
     * Send a request and synchronously wait for the response (string).
     * Handles Call (callback) messages from the child inline.
     */
    requestSync(method, payload) {
        this.ensureOpen();
        const result = this.requestBytesSync(method, payload);
        return result.toString("utf-8");
    }
    /**
     * Send a request and synchronously wait for the response (binary).
     * Handles Call (callback) messages from the child inline.
     */
    requestBinarySync(method, payload) {
        this.ensureOpen();
        return this.requestBytesSync(method, payload);
    }
    /** Register a string→string callback that the child may invoke. */
    registerCallback(name, callback) {
        this.callbacks.set(name, callback);
    }
    /** Kill the child process and release resources. */
    close() {
        try {
            liveChildren.delete(this.child);
            if (this.pipeFd !== undefined) {
                closeSync(this.pipeFd);
                this.pipeFd = undefined;
            }
            // Destroy the stdio streams so that their pipe handles are closed
            // and no longer prevent the event loop from draining.
            this.child.stdout?.destroy();
            this.child.stdin?.destroy();
            this.child.stderr?.destroy();
            this.child.kill();
            this.readFd = -1;
            this.writeFd = -1;
        }
        catch {
            // swallow – process may already be dead
        }
    }
    // ── Core request loop ───────────────────────────────────────────
    ensureOpen() {
        if (this.readFd < 0) {
            throw new Error("SyncRpcChannel is closed");
        }
    }
    getMethodBuf(method) {
        let buf = this.methodBufCache.get(method);
        if (buf === undefined) {
            buf = Buffer.from(method, "utf-8");
            this.methodBufCache.set(method, buf);
        }
        return buf;
    }
    requestBytesSync(method, payload) {
        const methodBuf = this.getMethodBuf(method);
        if (this.collectTiming) {
            this.lastBytesSent = typeof payload === "string"
                ? Buffer.byteLength(payload, "utf-8")
                : payload.length;
            this.lastBytesReceived = 0;
        }
        this.writeTuple(MSG_REQUEST, methodBuf, payload);
        for (;;) {
            this.readTuple();
            switch (this._msgType) {
                case MSG_RESPONSE: {
                    // Compare raw bytes instead of decoding to string.
                    if (!methodBuf.equals(this._msgName)) {
                        throw new Error(`name mismatch for response: expected \`${method}\`, got \`${this._msgName.toString("utf-8")}\``);
                    }
                    if (this.collectTiming) {
                        this.lastBytesReceived = this._msgPayload.length;
                    }
                    return this._msgPayload;
                }
                case MSG_ERROR: {
                    if (methodBuf.equals(this._msgName)) {
                        throw new Error(this._msgPayload.toString("utf-8"));
                    }
                    throw new Error(`name mismatch for response: expected \`${method}\`, got \`${this._msgName.toString("utf-8")}\``);
                }
                case MSG_CALL: {
                    this.handleCall(this._msgName.toString("utf-8"), this._msgPayload);
                    break;
                }
                default:
                    throw new Error(`Invalid message type from child: ${this._msgType}`);
            }
        }
    }
    // ── Callback handling ───────────────────────────────────────────
    /**
     * Handle an incoming MSG_CALL from the child process.
     *
     * After sending the error response back to the child, this method
     * intentionally re-throws to abort the caller's request loop.
     * A failed callback is treated as unrecoverable to match the
     * behavior of the native libsyncrpc addon.
     */
    handleCall(name, payload) {
        const cb = this.callbacks.get(name);
        if (!cb) {
            const errMsg = `unknown callback: \`${name}\`. Please make sure to register it on the JavaScript side before invoking it.`;
            this.writeTuple(MSG_CALL_ERROR, Buffer.from(name, "utf-8"), Buffer.from(errMsg, "utf-8"));
            throw new Error(`no callback named \`${name}\` found`);
        }
        try {
            const result = cb(name, payload.toString("utf-8"));
            this.writeTuple(MSG_CALL_RESPONSE, Buffer.from(name, "utf-8"), Buffer.from(result, "utf-8"));
        }
        catch (e) {
            const errMsg = String(e instanceof Error ? e.message : e).trim();
            this.writeTuple(MSG_CALL_ERROR, Buffer.from(name, "utf-8"), Buffer.from(errMsg, "utf-8"));
            throw new Error(`Error calling callback \`${name}\`: ${errMsg}`);
        }
    }
    // ── MessagePack tuple write ─────────────────────────────────────
    /**
     * Write a complete [type, name, payload] tuple in as few writeSync
     * calls as possible.  For messages that fit in the pre-allocated
     * write buffer (64 KB), everything is assembled and sent in a single
     * syscall.  Larger messages use two syscalls: one for the header
     * portion and one for the payload data.
     */
    writeTuple(type, name, payload) {
        const nameLen = name.length;
        const payloadIsString = typeof payload === "string";
        const payloadLen = payloadIsString ? Buffer.byteLength(payload, "utf-8") : payload.length;
        const nameHdrSize = binHeaderSize(nameLen);
        const payloadHdrSize = binHeaderSize(payloadLen);
        const headerSize = 2 + nameHdrSize + nameLen + payloadHdrSize;
        const totalSize = headerSize + payloadLen;
        if (totalSize <= this.writeBuf.length) {
            // Small message: assemble into write buffer, one syscall
            let off = 0;
            this.writeBuf[off++] = MSGPACK_FIXARRAY3;
            this.writeBuf[off++] = type;
            off = writeBinHeader(this.writeBuf, off, nameLen);
            name.copy(this.writeBuf, off);
            off += nameLen;
            off = writeBinHeader(this.writeBuf, off, payloadLen);
            if (payloadLen > 0) {
                if (payloadIsString) {
                    // Encode string directly into write buffer — avoids
                    // Buffer.from(string, 'utf-8') allocation entirely.
                    this.writeBuf.write(payload, off, payloadLen, "utf-8");
                }
                else if (payload instanceof Buffer) {
                    payload.copy(this.writeBuf, off);
                }
                else {
                    this.writeBuf.set(payload, off);
                }
            }
            this.writeAllBuf(this.writeBuf, totalSize);
        }
        else {
            // Large message: header + name in one call, payload in another
            let off = 0;
            this.writeBuf[off++] = MSGPACK_FIXARRAY3;
            this.writeBuf[off++] = type;
            off = writeBinHeader(this.writeBuf, off, nameLen);
            name.copy(this.writeBuf, off);
            off += nameLen;
            off = writeBinHeader(this.writeBuf, off, payloadLen);
            this.writeAllBuf(this.writeBuf, off);
            if (payloadLen > 0) {
                if (payloadIsString) {
                    // Large string: must allocate (can't stream-encode
                    // across multiple writeSync calls).
                    this.writeAllBuf(Buffer.from(payload, "utf-8"));
                }
                else {
                    this.writeAllBuf(payload);
                }
            }
        }
    }
    // ── MessagePack tuple read ──────────────────────────────────────
    /**
     * Read a [type, name, payload] tuple into instance fields
     * (_msgType, _msgName, _msgPayload) to avoid allocating a
     * short-lived 3-element array on every call.
     */
    readTuple() {
        // Fixed 3-element array marker
        const marker = this.readByte();
        if (marker !== MSGPACK_FIXARRAY3) {
            throw new Error(`Expected fixed 3-element array (0x93), received: 0x${marker.toString(16)}`);
        }
        // Message type – positive fixint or uint8
        const tb = this.readByte();
        if (tb <= 0x7f) {
            this._msgType = tb;
        }
        else if (tb === MSGPACK_UINT8) {
            this._msgType = this.readByte();
        }
        else {
            throw new Error(`Expected positive fixint or uint8 marker, received: 0x${tb.toString(16)}`);
        }
        this._msgName = this.readBin();
        this._msgPayload = this.readBin();
    }
    /**
     * Read a MessagePack bin field.
     */
    readBin() {
        const marker = this.readByte();
        let size;
        switch (marker) {
            case MSGPACK_BIN8:
                size = this.readByte();
                break;
            case MSGPACK_BIN16:
                this.readExactInto(this.headerBuf, 2);
                size = (this.headerBuf[0] << 8) | this.headerBuf[1];
                break;
            case MSGPACK_BIN32:
                this.readExactInto(this.headerBuf, 4);
                size = this.headerBuf.readUInt32BE(0);
                break;
            default:
                throw new Error(`Expected binary data (0xc4-0xc6), received: 0x${marker.toString(16)}`);
        }
        if (size === 0)
            return EMPTY_BUF;
        return this.readExact(size);
    }
    // ── Low-level synchronous I/O ───────────────────────────────────
    /** Build an EOF error with the child's exit code/signal if available. */
    eofError() {
        const code = this.child.exitCode;
        const signal = this.child.signalCode;
        const detail = signal ? `killed by signal ${signal}` : code !== null ? `exited with code ${code}` : "unknown reason";
        return new Error(`Unexpected EOF while reading from child process (${detail})`);
    }
    /** Read a single byte from the buffered read-ahead. */
    readByte() {
        if (this.readBufPos >= this.readBufLen) {
            this.fillReadBuffer();
        }
        return this.readBuf[this.readBufPos++];
    }
    readExact(length) {
        // Use allocUnsafeSlow (not allocUnsafe) so the buffer has its own
        // backing ArrayBuffer at byteOffset 0.  This is critical because
        // callers such as RemoteSourceFile create DataView/Uint8Array over
        // buffer.buffer with absolute offsets.  Buffer.allocUnsafe returns
        // slices of a shared pool whose byteOffset is non-zero, corrupting
        // those downstream views.
        //
        // allocUnsafeSlow (vs alloc) skips zero-fill, which matters for
        // large transfers — e.g. checker.ts at ~57 MB saves ~5 ms of
        // unnecessary memset.  The buffer is immediately filled by
        // readExactInto so uninitialized memory is never exposed.
        const buf = Buffer.allocUnsafeSlow(length);
        this.readExactInto(buf, length);
        return buf;
    }
    /**
     * Fill the internal read-ahead buffer from the pipe fd.
     * Retries on EAGAIN for non-blocking mode compatibility.
     */
    fillReadBuffer() {
        this.readBufPos = 0;
        this.readBufLen = 0;
        for (;;) {
            try {
                const n = readSync(this.readFd, this.readBuf, 0, this.readBuf.length, null);
                if (n === 0) {
                    throw this.eofError();
                }
                this.readBufLen = n;
                return;
            }
            catch (e) {
                if (e instanceof Error && ("code" in e) && (e.code === "EAGAIN" || e.code === "EWOULDBLOCK")) {
                    Atomics.wait(sleepBuf, 0, 0, 1);
                    continue;
                }
                throw e;
            }
        }
    }
    /**
     * Synchronously read exactly `length` bytes into `buffer`.
     * Serves from the internal read-ahead buffer first; for large reads
     * that exceed the buffer size, reads directly from the fd to avoid
     * an extra copy.
     */
    readExactInto(buffer, length) {
        let pos = 0;
        while (pos < length) {
            const avail = this.readBufLen - this.readBufPos;
            if (avail > 0) {
                // Serve from read-ahead buffer
                const toCopy = Math.min(avail, length - pos);
                this.readBuf.copy(buffer, pos, this.readBufPos, this.readBufPos + toCopy);
                this.readBufPos += toCopy;
                pos += toCopy;
            }
            else if (length - pos >= this.readBuf.length) {
                // Remaining data is larger than read buffer; read directly
                // into the target to avoid unnecessary copying.
                try {
                    const n = readSync(this.readFd, buffer, pos, length - pos, null);
                    if (n === 0) {
                        throw this.eofError();
                    }
                    pos += n;
                }
                catch (e) {
                    if (e instanceof Error && ("code" in e) && (e.code === "EAGAIN" || e.code === "EWOULDBLOCK")) {
                        Atomics.wait(sleepBuf, 0, 0, 1);
                        continue;
                    }
                    throw e;
                }
            }
            else {
                // Refill the read-ahead buffer
                this.fillReadBuffer();
            }
        }
    }
    /**
     * Synchronously write all bytes from `data` (up to `length`).
     * Retries on EAGAIN.
     */
    writeAllBuf(data, length) {
        const total = length ?? data.length;
        let pos = 0;
        while (pos < total) {
            try {
                const n = writeSync(this.writeFd, data, pos, total - pos);
                pos += n;
            }
            catch (e) {
                if (e instanceof Error && ("code" in e) && (e.code === "EAGAIN" || e.code === "EWOULDBLOCK")) {
                    Atomics.wait(sleepBuf, 0, 0, 1);
                    continue;
                }
                throw e;
            }
        }
    }
}
//# sourceMappingURL=syncChannel.js.map