export class JsonRpcRequestError extends Error {
    code;
    constructor(method, code, message) {
        super(`${method}: ${message} (code ${code})`);
        this.name = "JsonRpcRequestError";
        this.code = code;
    }
}
const HEADER_SEPARATOR = "\r\n\r\n";
/**
 * JSON-RPC connection speaking Content-Length framed messages.
 *
 * Server→client REQUESTS are auto-replied with safe defaults so language
 * servers never stall waiting on us: `workspace/configuration` gets one null
 * per requested item; everything else (`client/registerCapability`,
 * `window/workDoneProgress/create`, …) gets a plain null result.
 */
export class JsonRpcConnection {
    output;
    trace;
    buffer = Buffer.alloc(0);
    nextId = 1;
    pending = new Map();
    notificationHandlers = new Map();
    disposed = false;
    constructor(input, output, 
    /**
     * Optional wire tracer. Diagnosing a server that accepts a document and
     * then never publishes requires seeing whether our notification actually
     * went out and whether anything came back — indistinguishable from outside,
     * because LSP failures are silent by design. Off unless a caller passes
     * one, so the normal path is byte-identical.
     */
    trace) {
        this.output = output;
        this.trace = trace;
        input.on("data", (chunk) => this.onData(chunk));
        // Writing to a dead server's stdin must never crash the host process.
        output.on("error", () => { });
    }
    onNotification(method, handler) {
        const handlers = this.notificationHandlers.get(method) ?? [];
        handlers.push(handler);
        this.notificationHandlers.set(method, handlers);
    }
    request(method, params, timeoutMs) {
        if (this.disposed) {
            return Promise.reject(new JsonRpcRequestError(method, -32099, "connection disposed"));
        }
        const id = this.nextId++;
        return new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                this.pending.delete(id);
                reject(new JsonRpcRequestError(method, -32803, `timed out after ${timeoutMs}ms`));
            }, timeoutMs);
            timer.unref();
            this.pending.set(id, (msg) => {
                clearTimeout(timer);
                if (msg.error) {
                    reject(new JsonRpcRequestError(method, msg.error.code, msg.error.message));
                }
                else {
                    resolve(msg.result);
                }
            });
            this.send({ jsonrpc: "2.0", id, method, params });
        });
    }
    notify(method, params) {
        this.send({ jsonrpc: "2.0", method, params });
    }
    /** Reject all in-flight requests and stop accepting new work. */
    dispose() {
        if (this.disposed)
            return;
        this.disposed = true;
        for (const [id, settle] of this.pending) {
            this.pending.delete(id);
            settle({ error: { code: -32099, message: "connection disposed" } });
        }
    }
    send(msg) {
        if (this.disposed)
            return;
        this.trace?.("out", msg);
        const body = JSON.stringify(msg);
        try {
            this.output.write(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
        }
        catch {
            // Server stdin already closed — degradation is handled by the caller.
        }
    }
    onData(chunk) {
        this.buffer = Buffer.concat([this.buffer, chunk]);
        for (;;) {
            const headerEnd = this.buffer.indexOf(HEADER_SEPARATOR);
            if (headerEnd === -1)
                return;
            const header = this.buffer.subarray(0, headerEnd).toString();
            const match = /Content-Length: (\d+)/i.exec(header);
            if (!match) {
                this.buffer = this.buffer.subarray(headerEnd + HEADER_SEPARATOR.length);
                continue;
            }
            const length = Number(match[1]);
            const bodyStart = headerEnd + HEADER_SEPARATOR.length;
            if (this.buffer.length < bodyStart + length)
                return;
            const body = this.buffer.subarray(bodyStart, bodyStart + length).toString();
            this.buffer = this.buffer.subarray(bodyStart + length);
            let msg;
            try {
                msg = JSON.parse(body);
            }
            catch {
                continue;
            }
            this.onMessage(msg);
        }
    }
    onMessage(msg) {
        this.trace?.("in", msg);
        // Response to one of our requests.
        if (msg.id !== undefined && msg.method === undefined) {
            const settle = typeof msg.id === "number" ? this.pending.get(msg.id) : undefined;
            if (settle && typeof msg.id === "number") {
                this.pending.delete(msg.id);
                settle(msg);
            }
            return;
        }
        // Server→client request: auto-reply with safe defaults.
        if (msg.id !== undefined && msg.method !== undefined) {
            let result = null;
            if (msg.method === "workspace/configuration") {
                const items = msg.params?.items ?? [];
                result = items.map(() => null);
            }
            this.send({ jsonrpc: "2.0", id: msg.id, result });
            return;
        }
        // Notification.
        if (msg.method !== undefined) {
            const handlers = this.notificationHandlers.get(msg.method);
            if (handlers) {
                for (const handler of handlers)
                    handler(msg.params);
            }
        }
    }
}
//# sourceMappingURL=jsonrpc.js.map