"use strict";
// Copyright 2021-2024 The Connect Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
Object.defineProperty(exports, "__esModule", { value: true });
exports.createNodeHttpClient = createNodeHttpClient;
const http = require("http");
const https = require("https");
const connect_1 = require("@connectrpc/connect");
const node_universal_header_js_1 = require("./node-universal-header.js");
const node_error_js_1 = require("./node-error.js");
const protocol_1 = require("@connectrpc/connect/protocol");
const http2_session_manager_js_1 = require("./http2-session-manager.js");
/**
 * Create a universal client function, a minimal abstraction of an HTTP client,
 * using the Node.js `http`, `https`, or `http2` module.
 *
 * @private Internal code, does not follow semantic versioning.
 */
function createNodeHttpClient(options) {
    var _a;
    if (options.httpVersion == "1.1") {
        return createNodeHttp1Client(options.nodeOptions);
    }
    const sessionProvider = (_a = options.sessionProvider) !== null && _a !== void 0 ? _a : ((url) => new http2_session_manager_js_1.Http2SessionManager(url));
    return createNodeHttp2Client(sessionProvider);
}
/**
 * Create an HTTP client using the Node.js `http` or `https` package.
 *
 * The HTTP client is a simple function conforming to the type UniversalClientFn.
 * It takes an UniversalClientRequest as an argument, and returns a promise for
 * an UniversalClientResponse.
 */
function createNodeHttp1Client(httpOptions) {
    return async function request(req) {
        const sentinel = createSentinel(req.signal);
        return new Promise((resolve, reject) => {
            sentinel.catch((e) => {
                reject(e);
            });
            h1Request(sentinel, req.url, Object.assign(Object.assign({}, httpOptions), { headers: (0, node_universal_header_js_1.webHeaderToNodeHeaders)(req.header, httpOptions === null || httpOptions === void 0 ? void 0 : httpOptions.headers), method: req.method }), (request) => {
                void sinkRequest(req, request, sentinel);
                request.on("response", (response) => {
                    var _a;
                    response.on("error", sentinel.reject);
                    sentinel.catch((reason) => response.destroy((0, node_error_js_1.connectErrorFromNodeReason)(reason)));
                    const trailer = new Headers();
                    resolve({
                        status: (_a = response.statusCode) !== null && _a !== void 0 ? _a : 0,
                        header: (0, node_universal_header_js_1.nodeHeaderToWebHeader)(response.headers),
                        body: h1ResponseIterable(sentinel, response, trailer),
                        trailer,
                    });
                });
            });
        });
    };
}
/**
 * Create an HTTP client using the Node.js `http2` package.
 *
 * The HTTP client is a simple function conforming to the type UniversalClientFn.
 * It takes an UniversalClientRequest as an argument, and returns a promise for
 * an UniversalClientResponse.
 */
function createNodeHttp2Client(sessionProvider) {
    return function request(req) {
        const sentinel = createSentinel(req.signal);
        const sessionManager = sessionProvider(req.url);
        return new Promise((resolve, reject) => {
            sentinel.catch((e) => {
                reject(e);
            });
            h2Request(sentinel, sessionManager, req.url, req.method, (0, node_universal_header_js_1.webHeaderToNodeHeaders)(req.header), {}, (stream) => {
                void sinkRequest(req, stream, sentinel);
                stream.on("response", (headers) => {
                    var _a;
                    const response = {
                        status: (_a = headers[":status"]) !== null && _a !== void 0 ? _a : 0,
                        header: (0, node_universal_header_js_1.nodeHeaderToWebHeader)(headers),
                        body: h2ResponseIterable(sentinel, stream, sessionManager),
                        trailer: h2ResponseTrailer(stream),
                    };
                    resolve(response);
                });
            });
        });
    };
}
function h1Request(sentinel, url, options, onRequest) {
    let request;
    if (new URL(url).protocol.startsWith("https")) {
        request = https.request(url, options);
    }
    else {
        request = http.request(url, options);
    }
    sentinel.catch((reason) => request.destroy((0, node_error_js_1.connectErrorFromNodeReason)(reason)));
    // Node.js will only send headers with the first request body byte by default.
    // We force it to send headers right away for consistent behavior between
    // HTTP/1.1 and HTTP/2.2 clients.
    request.flushHeaders();
    request.on("error", sentinel.reject);
    request.on("socket", function onRequestSocket(socket) {
        function onSocketConnect() {
            socket.off("connect", onSocketConnect);
            onRequest(request);
        }
        // If readyState is open, then socket is already open due to keepAlive, so
        // the 'connect' event will never fire so call onRequest explicitly
        if (socket.readyState === "open") {
            onRequest(request);
        }
        else {
            socket.on("connect", onSocketConnect);
        }
    });
}
function h1ResponseIterable(sentinel, response, trailer) {
    const inner = response[Symbol.asyncIterator]();
    return {
        [Symbol.asyncIterator]() {
            return {
                async next() {
                    const r = await sentinel.race(inner.next());
                    if (r.done === true) {
                        (0, node_universal_header_js_1.nodeHeaderToWebHeader)(response.trailers).forEach((value, key) => {
                            trailer.set(key, value);
                        });
                        sentinel.resolve();
                        await sentinel;
                    }
                    return r;
                },
                throw(e) {
                    sentinel.reject(e);
                    throw e;
                },
            };
        },
    };
}
function h2Request(sentinel, sm, url, method, headers, options, onStream) {
    const requestUrl = new URL(url);
    if (requestUrl.origin !== sm.authority) {
        const message = `cannot make a request to ${requestUrl.origin}: the http2 session is connected to ${sm.authority}`;
        sentinel.reject(new connect_1.ConnectError(message, connect_1.Code.Internal));
        return;
    }
    sm.request(method, requestUrl.pathname + requestUrl.search, headers, {}).then((stream) => {
        sentinel.catch((reason) => {
            if (stream.closed) {
                return;
            }
            // Node.js http2 streams that are aborted via an AbortSignal close with
            // an RST_STREAM with code INTERNAL_ERROR.
            // To comply with the mapping between gRPC and HTTP/2 codes, we need to
            // close with code CANCEL.
            // See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#errors
            // See https://www.rfc-editor.org/rfc/rfc7540#section-7
            const rstCode = reason instanceof connect_1.ConnectError && reason.code == connect_1.Code.Canceled
                ? node_error_js_1.H2Code.CANCEL
                : node_error_js_1.H2Code.INTERNAL_ERROR;
            return new Promise((resolve) => stream.close(rstCode, resolve));
        });
        stream.on("error", function h2StreamError(e) {
            if (stream.writableEnded &&
                (0, node_error_js_1.unwrapNodeErrorChain)(e)
                    .map(node_error_js_1.getNodeErrorProps)
                    .some((p) => p.code == "ERR_STREAM_WRITE_AFTER_END")) {
                return;
            }
            sentinel.reject(e);
        });
        stream.on("close", function h2StreamClose() {
            const err = (0, node_error_js_1.connectErrorFromH2ResetCode)(stream.rstCode);
            if (err) {
                sentinel.reject(err);
            }
        });
        onStream(stream);
    }, (reason) => {
        sentinel.reject(reason);
    });
}
function h2ResponseTrailer(response) {
    const trailer = new Headers();
    response.on("trailers", (args) => {
        (0, node_universal_header_js_1.nodeHeaderToWebHeader)(args).forEach((value, key) => {
            trailer.set(key, value);
        });
    });
    return trailer;
}
function h2ResponseIterable(sentinel, response, sm) {
    const inner = response[Symbol.asyncIterator]();
    return {
        [Symbol.asyncIterator]() {
            return {
                async next() {
                    const r = await sentinel.race(inner.next());
                    if (r.done === true) {
                        sentinel.resolve();
                        await sentinel;
                    }
                    sm === null || sm === void 0 ? void 0 : sm.notifyResponseByteRead(response);
                    return r;
                },
                throw(e) {
                    sentinel.reject(e);
                    throw e;
                },
            };
        },
    };
}
async function sinkRequest(request, nodeRequest, sentinel) {
    if (request.body === undefined) {
        await new Promise((resolve) => nodeRequest.end(resolve));
        return;
    }
    const it = request.body[Symbol.asyncIterator]();
    return new Promise((resolve) => {
        writeNext();
        function writeNext() {
            if (sentinel.isRejected()) {
                return;
            }
            it.next().then((r) => {
                if (r.done === true) {
                    nodeRequest.end(resolve);
                    return;
                }
                nodeRequest.write(r.value, "binary", function (e) {
                    if (e === null || e === undefined) {
                        writeNext();
                        return;
                    }
                    if (it.throw !== undefined) {
                        it.throw((0, node_error_js_1.connectErrorFromNodeReason)(e)).catch(() => {
                            //
                        });
                    }
                    // If the server responds and closes the connection before the client has written the entire response
                    // body, we get an ERR_STREAM_WRITE_AFTER_END error code from Node.js here.
                    // We do want to notify the iterable of the error condition, but we do not want to reject our sentinel,
                    // because that would also affect the reading side.
                    if (nodeRequest.writableEnded &&
                        (0, node_error_js_1.unwrapNodeErrorChain)(e)
                            .map(node_error_js_1.getNodeErrorProps)
                            .some((p) => p.code == "ERR_STREAM_WRITE_AFTER_END")) {
                        return;
                    }
                    sentinel.reject(e);
                });
            }, (e) => {
                sentinel.reject(e);
            });
        }
    });
}
function createSentinel(signal) {
    let res;
    let rej;
    let resolved = false;
    let rejected = false;
    const p = new Promise((resolve, reject) => {
        res = resolve;
        rej = reject;
    });
    const c = {
        resolve() {
            if (!resolved && !rejected) {
                resolved = true;
                res === null || res === void 0 ? void 0 : res();
            }
        },
        isResolved() {
            return resolved;
        },
        reject(reason) {
            if (!resolved && !rejected) {
                rejected = true;
                rej === null || rej === void 0 ? void 0 : rej((0, node_error_js_1.connectErrorFromNodeReason)(reason));
            }
        },
        isRejected() {
            return rejected;
        },
        async race(promise) {
            const r = await Promise.race([promise, p]);
            if (r === undefined && resolved) {
                throw new connect_1.ConnectError("sentinel completed early", connect_1.Code.Internal);
            }
            return r;
        },
    };
    const s = Object.assign(p, c);
    function onSignalAbort() {
        c.reject((0, protocol_1.getAbortSignalReason)(this));
    }
    if (signal) {
        if (signal.aborted) {
            c.reject((0, protocol_1.getAbortSignalReason)(signal));
        }
        else {
            signal.addEventListener("abort", onSignalAbort);
        }
        p.finally(() => signal.removeEventListener("abort", onSignalAbort)).catch(() => {
            // We intentionally swallow sentinel rejection - errors must
            // propagate through the request or response iterables.
        });
    }
    return s;
}
