"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.startServer = exports.startWebSocketProxy = void 0;
const http_1 = require("http");
const dispatch_1 = require("./dispatch");
const logo_1 = __importDefault(require("../utils/logo"));
const chalk_1 = __importDefault(require("chalk"));
const ws_1 = require("ws");
const ports_1 = require("../utils/ports");
const log_1 = __importDefault(require("../log"));
const RuntimeContext_1 = require("./RuntimeContext");
const environment_1 = require("../environment");
/**
 * Creates a WebSocket handler attached to the existing server instance
 * that proxies all socket connections and messages to the target host and port and back.
 * @param server The server instance
 * @param targetPort The target port to which to proxy the messages
 * @param targetHost The target host to which to proxy the messages
 * @returns The websocket server instance
 */
const startWebSocketProxy = (server, targetPort = ports_1.jsPort, targetHost = ports_1.localhost) => {
    const proxySocketServer = new ws_1.WebSocketServer({ server });
    proxySocketServer.on('connection', async (proxySocket, request) => {
        var _a;
        try {
            const path = (_a = request.url) !== null && _a !== void 0 ? _a : '/';
            const targetSocketUrl = `ws://${targetHost}:${targetPort}${path}`;
            // The target socket may have a protocol header that we need to pass to the target host.
            // NOTE: Valid protocols are also undefined, [] but not [''] or ''.
            const targetSocketProtocolHeader = request.headers['sec-websocket-protocol'];
            const targetSocketProtocols = targetSocketProtocolHeader
                ? targetSocketProtocolHeader.split(',').map(p => p.trim())
                : [];
            // Create a new WebSocket connection to the target host
            // using same path and headers as the incoming upgrade request
            const targetSocket = new ws_1.WebSocket(targetSocketUrl, targetSocketProtocols, {
                headers: request.headers,
            });
            // Wait for the target socket to be opened
            // and ready before we start proxying messages
            const targetSocketReadyPromise = new Promise((resolve, reject) => {
                targetSocket.on('open', resolve);
                targetSocket.on('error', reject);
            }).catch(e => {
                // WebSocket errors are not fatal. Backend app may not support WebSocket connections
                // because we often have SW from different project on the same port or cached site in the browser.
                // Let's log them just for debugging purposes to not make too much noise in the console.
                log_1.default.debug(`[WebSocket proxy]: Error - Failed to establish connection to target host '${targetSocketUrl}'. The target host may not support WebSocket connections.\r\n`);
                log_1.default.debug(`[WebSocket proxy]: Error - ${e}\r\n`);
            });
            // Handle WebSocket protocol negotiation
            // For example: sec-websocket-protocol: vite-hmr, graphql-ws
            proxySocket.on('handleProtocols', (protocols, callback) => {
                targetSocket.emit('handleProtocols', protocols, callback);
            });
            targetSocket.on('handleProtocols', (protocols, callback) => {
                proxySocket.emit('handleProtocols', protocols, callback);
            });
            // Close both sockets when one of them is closed
            const closeSockets = () => {
                targetSocket.close();
                proxySocket.close();
            };
            targetSocket.on('close', closeSockets);
            proxySocket.on('close', closeSockets);
            // Log errors and close the sockets when an error occurs
            const handleError = (err) => {
                log_1.default.debug(`[WebSocket proxy]: ${err}`);
                closeSockets();
            };
            targetSocket.on('error', handleError);
            proxySocket.on('error', handleError);
            // Proxy messages from target to server and from server to target.
            // If the target socket is not ready yet, we'll hold the message
            // and send it as soon as the target socket is ready.
            // NOTE: The messages are received as Buffer, so we need to convert them to string and re-send them to the other socket
            const proxyMessage = async (receivedMessage, receiver) => {
                await targetSocketReadyPromise;
                receiver.send(receivedMessage.toString());
            };
            targetSocket.on('message', message => proxyMessage(message, proxySocket));
            proxySocket.on('message', message => proxyMessage(message, targetSocket));
        }
        catch (e) {
            log_1.default.error(`[WebSocket proxy]: Fatal Error - ${e}\r\n`);
        }
    });
    return proxySocketServer;
};
exports.startWebSocketProxy = startWebSocketProxy;
/**
 * Initializes the runtime, server
 * and immediately starts listening on the given port and host.
 * @param port The port on which to listen
 * @param host The host on which to listen
 * @returns The server instance
 */
const startServer = (port = ports_1.port, host = ports_1.localhost) => {
    const server = (0, http_1.createServer)(dispatch_1.dispatch);
    // Warm-up the runtime before first request comes in
    // and wait for it to be ready before we print the ready message.
    // This ensures that the user's app is started immediately with the server.
    // NOTE: It's important to do it asynchronously before we start listening on the server,
    // so we don't wait for server to start listen on cold starts.
    const runtime = (0, RuntimeContext_1.getRuntime)();
    runtime.waitForRuntime().catch(e => {
        // Locally, log the error and exit the process.
        log_1.default.error(`[Edgio]: Fatal Error - ${e}\r\n`);
        log_1.default.error(e.stack);
        // Stop the server and exit the process
        // on localhost. In lambda, we keep the server running,
        // so the error can be serialized and sent to the client in dispatch.ts.
        if ((0, environment_1.isLocal)())
            process.exit(1);
    });
    // If we are in dev mode, we also start the WebSocket proxy
    // attached to the server instance, that proxies everything from server to app and back.
    // This is needed so the features such as HMR (Hot Module Replacement)
    // work correctly in the development mode and the user can see the
    // changes in the browser immediately after the code is saved.
    if (runtime.isDevMode)
        (0, exports.startWebSocketProxy)(server);
    server.listen(port, host, async () => {
        // The actual port that the server is listening on
        // can be different from the requested port if '0' was used.
        const { port: usedPort } = server.address();
        // Print the ready message after the runtime is ready.
        // This important not only for the user, but also for the handler.ts,
        // as it waits for the ready message before it resolves server start promise.
        console.log(`> ${logo_1.default} ` +
            chalk_1.default.green(`ready on ${chalk_1.default.bold(chalk_1.default.underline(`http://${host}:${usedPort}\n`))}`));
    });
    return server;
};
exports.startServer = startServer;
