"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.__tsfTypeAliases = exports.HttpServerRuntime = void 0;
exports.writeNodeResponse = writeNodeResponse;
exports.writeUnhandledNodeError = writeUnhandledNodeError;
const tslib_1 = require("tslib");
const _a = tslib_1.__importStar(require("@zyno-io/ts-server-foundation/type-metadata-runtime"));
const node_http_1 = require("node:http");
const helpers_1 = require("../helpers");
const cors_1 = require("./cors");
const context_1 = require("./context");
const request_1 = require("./request");
const response_1 = require("./response");
const request_logging_1 = require("./request-logging");
const startup_logging_1 = require("./startup-logging");
const static_files_1 = require("./static-files");
const upgrade_1 = require("./upgrade");
const __tsf_metadata_type = _a.createCompactMetadataRegistryV1("[1,[{\"kind\":7},{\"kind\":6},{\"kind\":18,\"typeName\":\"HttpServerListenHooks\",\"types\":[{\"kind\":20,\"name\":\"start\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2,\"typeName\":\"() =\\u003e Promise\\u003cvoid\\u003e\"},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"afterListen\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2,\"typeName\":\"(server: Server) =\\u003e Promise\\u003cvoid\\u003e\"},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"onListenError\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2,\"typeName\":\"(error: unknown) =\\u003e Promise\\u003cvoid\\u003e\"},{\"kind\":4}]},\"optional\":true}]},{\"kind\":2,\"typeName\":\"(request: IncomingMessage, socket: Socket, head: Buffer) =\\u003e void\",\"typeName\":\"HttpUpgradeHandler\"},{\"kind\":2,\"typeName\":\"() =\\u003e void\"},{\"kind\":2,\"typeName\":\"(entry: HttpRequestObservation) =\\u003e void\",\"typeName\":\"HttpRequestObserver\"},{\"kind\":22,\"type\":{\"kind\":3}}]]", []);
let _HttpServerRuntimeMetadata_1;
class HttpServerRuntime {
    options;
    corsOptions;
    staticFiles;
    appLogger;
    requestLogger;
    upgradeHandlers = new Set();
    observers = new Set();
    server;
    constructor(options) {
        this.options = options;
        this.corsOptions = (0, cors_1.resolveCorsOptions)(options.config, options.cors);
        this.staticFiles = (0, static_files_1.resolveStaticFilesOptions)(options.staticFiles);
        this.appLogger = options.logger.scoped('app');
        this.requestLogger = new request_logging_1.HttpRequestLogger(options.config, options.logger.scoped('http'), options.requestLogging);
    }
    async request(request, response = new response_1.MemoryHttpResponse()) {
        request.trustProxyHeaders = this.options.config.USE_REAL_IP_HEADER === true;
        request.setBodyLimits({
            maxBodyBytes: this.options.config.HTTP_MAX_REQUEST_BODY_BYTES,
            maxCompressedBodyBytes: this.options.config.HTTP_MAX_REQUEST_COMPRESSED_BODY_BYTES
        });
        const activeHttpContext = getActiveHttpContext();
        const hasActiveRequestContext = activeHttpContext === request.context;
        const context = hasActiveRequestContext ? activeHttpContext : (0, context_1.applyHttpContext)(request);
        const startedAt = Date.now();
        let observedResponse = response;
        let observedError;
        const runRequest = async () => {
            if ((0, cors_1.handleCorsPreflight)(request, response, this.corsOptions))
                return response;
            (0, cors_1.prepareCorsResponseHeaders)(request, response, this.corsOptions);
            (0, cors_1.applyCorsResponseHeaders)(request, response, this.corsOptions);
            if (this.staticFiles && request.method === 'GET' && !this.options.router.hasRoute(request)) {
                const staticResponse = await (0, static_files_1.serveStaticFile)(request, response, this.staticFiles);
                if (staticResponse) {
                    (0, cors_1.applyCorsResponseHeaders)(request, staticResponse, this.corsOptions);
                    return staticResponse;
                }
            }
            const routed = await this.options.router.handle(request, response);
            (0, cors_1.applyCorsResponseHeaders)(request, routed, this.corsOptions);
            return routed;
        };
        try {
            observedResponse = hasActiveRequestContext ? await runRequest() : await (0, helpers_1.withContextData)({ http: context }, runRequest);
            if (request.method === 'HEAD')
                observedResponse.discardBody();
            return observedResponse;
        }
        catch (error) {
            observedError = error;
            throw error;
        }
        finally {
            const error = observedError ?? request.store['$ControllerError'];
            if (error && !(observedResponse instanceof response_1.NodeHttpResponse))
                this.requestLogger.errorForRequest(request, observedResponse, startedAt, error);
            this.notifyObservers({
                request,
                response: observedResponse,
                startedAt,
                durationMs: Date.now() - startedAt,
                error
            });
        }
    }
    async listen(port, host, hooks = {}) {
        const listenHooks = composeListenHooks(resolveListenHooks(this.options.listenHooks), hooks);
        await listenHooks.start?.();
        if (this.server)
            return this.server;
        const server = (0, node_http_1.createServer)((request, response) => {
            this.handleNodeRequest(request, response).catch(error => {
                writeUnhandledNodeError(response, error);
            });
        });
        this.server = server;
        (0, upgrade_1.installUpgradeClaimHandling)(server);
        for (const handler of this.upgradeHandlers)
            server.prependListener('upgrade', handler);
        const shouldLogStartup = this.shouldLogStartupDetails();
        if (shouldLogStartup) {
            (0, startup_logging_1.logStartupDetails)(this.appLogger, this.options.packageName ?? (0, helpers_1.getPackageName)() ?? 'app', this.options.router.listRoutes());
        }
        try {
            const listenPort = this.getPort(port);
            await listenServer(server, listenPort, host);
            await listenHooks.afterListen?.(server);
            if (shouldLogStartup) {
                (0, startup_logging_1.logServerListening)(this.appLogger, server, listenPort, host);
                this.appLogger.info('Server started');
                if (this.options.devConsoleEnabled)
                    (0, startup_logging_1.logDevConsoleAvailable)(this.appLogger, server, listenPort);
            }
            return server;
        }
        catch (error) {
            const cleanupErrors = [];
            try {
                // Keep the server reachable while app-level rollback runs so
                // app.stop() can close a socket that already bound successfully.
                await listenHooks.onListenError?.(error);
            }
            catch (cleanupError) {
                cleanupErrors.push(cleanupError);
            }
            if (this.server === server)
                this.server = undefined;
            try {
                await closeServer(server);
            }
            catch (cleanupError) {
                cleanupErrors.push(cleanupError);
            }
            if (cleanupErrors.length) {
                const rollbackError = cleanupErrors.length === 1 ? cleanupErrors[0] : new AggregateError(cleanupErrors, 'HTTP listen rollback failed');
                if (error instanceof Error && error.cause === undefined)
                    error.cause = rollbackError;
                else
                    throw new AggregateError([error, rollbackError], 'HTTP listen and rollback failed');
            }
            throw error;
        }
    }
    registerUpgradeHandler(handler) {
        this.upgradeHandlers.add(handler);
        if (this.server) {
            (0, upgrade_1.installUpgradeClaimHandling)(this.server);
            this.server.prependListener('upgrade', handler);
        }
        return () => {
            this.upgradeHandlers.delete(handler);
            this.server?.off('upgrade', handler);
        };
    }
    registerObserver(observer) {
        this.observers.add(observer);
        return () => this.observers.delete(observer);
    }
    async close() {
        if (!this.server)
            return;
        const server = this.server;
        this.server = undefined;
        await closeServer(server);
    }
    notifyObservers(entry) {
        for (const observer of this.observers) {
            try {
                observer(entry);
            }
            catch {
                // Observers must never affect request handling.
            }
        }
    }
    getPort(explicitPort) {
        if (explicitPort !== undefined)
            return explicitPort;
        if (this.options.config.APP_ENV === 'test') {
            const configured = this.options.frameworkConfig?.port ?? this.options.serverConfig?.port;
            return configured === undefined ? 3000 : typeof configured === 'number' ? configured : Number(configured);
        }
        const configured = this.options.frameworkConfig?.port ?? this.options.serverConfig?.port ?? this.options.config.PORT ?? 3000;
        return typeof configured === 'number' ? configured : Number(configured);
    }
    shouldLogStartupDetails() {
        return this.options.config.APP_ENV !== 'test';
    }
    async handleNodeRequest(incoming, outgoing) {
        const headers = normalizeIncomingHeaders(incoming.headers);
        const method = normalizeMethod(incoming.method);
        const request = new request_1.HttpRequest(method, incoming.url ?? '/', headers, undefined, incoming);
        request.remoteAddress = incoming.socket.remoteAddress ?? '127.0.0.1';
        request.socket = incoming.socket;
        request.trustProxyHeaders = this.options.config.USE_REAL_IP_HEADER === true;
        const context = (0, context_1.applyHttpContext)(request);
        const logging = this.requestLogger.createState(request, outgoing);
        await (0, helpers_1.withContextData)({ http: context }, async () => {
            this.requestLogger.start(logging);
            const response = new response_1.NodeHttpResponse(outgoing);
            try {
                await this.request(request, response);
                response.flush();
                this.requestLogger.error(logging, request.store['$ControllerError'], response);
                this.requestLogger.finish(logging, response);
            }
            catch (error) {
                writeUnhandledNodeError(outgoing, error);
                this.requestLogger.error(logging, error, outgoing);
                this.requestLogger.finish(logging, outgoing);
            }
            finally {
                this.requestLogger.dispose(logging, outgoing);
            }
        });
    }
    static __tsfType = _HttpServerRuntimeMetadata_1 = _a.decodeCompactMetadataV1("[1,{\"kind\":16,\"name\":\"HttpServerRuntime\",\"typeName\":\"HttpServerRuntime\",\"classType\":{\"$tsf\":0},\"properties\":[],\"methods\":[{\"name\":\"request\",\"parameters\":[{\"name\":\"request\",\"type\":{\"kind\":16,\"typeName\":\"HttpRequest\",\"classType\":{\"$tsfImport\":[1,\"./request\",\"HttpRequest\"]}},\"optional\":false,\"default\":false},{\"name\":\"response\",\"type\":{\"kind\":16,\"typeName\":\"HttpResponse\",\"classType\":{\"$tsfImport\":[1,\"./response\",\"HttpResponse\"]}},\"optional\":true,\"default\":true}],\"returnType\":{\"kind\":22,\"type\":{\"kind\":16,\"typeName\":\"HttpResponse\",\"classType\":{\"$tsfImport\":[1,\"./response\",\"HttpResponse\"]}}}},{\"name\":\"listen\",\"parameters\":[{\"name\":\"port\",\"type\":{\"$tsfType\":0},\"optional\":true,\"default\":false},{\"name\":\"host\",\"type\":{\"$tsfType\":1},\"optional\":true,\"default\":false},{\"name\":\"hooks\",\"type\":{\"$tsfType\":2},\"optional\":true,\"default\":true}],\"returnType\":{\"kind\":22,\"type\":{\"$tsfAlias\":[1,\"node:http\",\"Server\",\"Server\"]}}},{\"name\":\"registerUpgradeHandler\",\"parameters\":[{\"name\":\"handler\",\"type\":{\"$tsfType\":3},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":4}},{\"name\":\"registerObserver\",\"parameters\":[{\"name\":\"observer\",\"type\":{\"$tsfType\":5},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":4}},{\"name\":\"close\",\"parameters\":[],\"returnType\":{\"$tsfType\":6}},{\"name\":\"getPort\",\"parameters\":[{\"name\":\"explicitPort\",\"type\":{\"$tsfType\":0},\"optional\":true,\"default\":false}],\"returnType\":{\"$tsfType\":0}}],\"hasConstructor\":true,\"constructorParameters\":[{\"name\":\"options\",\"type\":{\"kind\":16,\"typeName\":\"HttpServerRuntimeOptions\",\"classType\":{\"$tsf\":2},\"typeArguments\":[{\"kind\":2,\"typeName\":\"C\"}]},\"optional\":false,\"default\":false}]}]", [() => HttpServerRuntime, require, () => (typeof HttpServerRuntimeOptions !== "undefined" ? HttpServerRuntimeOptions : (typeof exports !== "undefined" ? exports.HttpServerRuntimeOptions : undefined))], __tsf_metadata_type);
}
exports.HttpServerRuntime = HttpServerRuntime;
_HttpServerRuntimeMetadata_1.classType = HttpServerRuntime;
HttpServerRuntime.__tsfType = _HttpServerRuntimeMetadata_1;
function resolveListenHooks(hooks) {
    return typeof hooks === 'function' ? hooks() : (hooks ?? {});
}
function composeListenHooks(first, second) {
    return {
        start: first.start || second.start
            ? async () => {
                await first.start?.();
                await second.start?.();
            }
            : undefined,
        afterListen: first.afterListen || second.afterListen
            ? async (server) => {
                await first.afterListen?.(server);
                await second.afterListen?.(server);
            }
            : undefined,
        onListenError: first.onListenError || second.onListenError
            ? async (error) => {
                const failures = [];
                for (const hook of [first.onListenError, second.onListenError]) {
                    try {
                        await hook?.(error);
                    }
                    catch (hookError) {
                        failures.push(hookError);
                    }
                }
                if (failures.length === 1)
                    throw failures[0];
                if (failures.length > 1)
                    throw new AggregateError(failures, 'HTTP listen rollback hooks failed');
            }
            : undefined
    };
}
function getActiveHttpContext() {
    const context = (0, helpers_1.getContext)();
    return isRecord(context?.http) ? context.http : undefined;
}
function isRecord(value) {
    return !!value && typeof value === 'object';
}
function writeNodeResponse(outgoing, response) {
    outgoing.statusCode = response.statusCode;
    for (const [name, value] of Object.entries(response.headers))
        outgoing.setHeader(name, value);
    outgoing.end(response.body);
}
function writeUnhandledNodeError(outgoing, error) {
    void error;
    if (!outgoing.headersSent) {
        outgoing.writeHead(500, { 'content-type': 'application/json' });
        if (!outgoing.writableEnded)
            outgoing.end(JSON.stringify({ error: 'Internal Server Error' }));
        return;
    }
    if (!outgoing.writableEnded)
        outgoing.end();
}
function normalizeMethod(method) {
    return (method ?? 'GET').toUpperCase();
}
function normalizeIncomingHeaders(headers) {
    const result = {};
    for (const [name, value] of Object.entries(headers)) {
        if (value !== undefined)
            result[name] = value;
    }
    return result;
}
function listenServer(server, port, host) {
    return new Promise((resolve, reject) => {
        const onError = (error) => {
            server.off('listening', onListening);
            reject(error);
        };
        const onListening = () => {
            server.off('error', onError);
            resolve();
        };
        server.once('error', onError);
        try {
            server.listen(port, host, onListening);
        }
        catch (error) {
            server.off('error', onError);
            reject(error);
        }
    });
}
function closeServer(server) {
    if (!server.listening)
        return Promise.resolve();
    return new Promise((resolve, reject) => {
        server.close(error => {
            if (error)
                reject(error);
            else
                resolve();
        });
    });
}
exports.__tsfTypeAliases = _a.decodeCompactMetadataV1("[1,{\"HttpRequestObservation\":{\"kind\":18,\"typeName\":\"HttpRequestObservation\",\"types\":[{\"kind\":20,\"name\":\"request\",\"type\":{\"kind\":16,\"typeName\":\"HttpRequest\",\"classType\":{\"$tsfImport\":[0,\"./request\",\"HttpRequest\"]}},\"optional\":false},{\"kind\":20,\"name\":\"response\",\"type\":{\"kind\":16,\"typeName\":\"HttpResponse\",\"classType\":{\"$tsfImport\":[0,\"./response\",\"HttpResponse\"]}},\"optional\":false},{\"kind\":20,\"name\":\"startedAt\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"durationMs\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"error\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2},{\"kind\":4}]},\"optional\":true}]},\"HttpRequestObserver\":{\"kind\":2,\"typeName\":\"(entry: HttpRequestObservation) =\\u003e void\",\"typeName\":\"HttpRequestObserver\"},\"HttpServerListenHooks\":{\"kind\":18,\"typeName\":\"HttpServerListenHooks\",\"types\":[{\"kind\":20,\"name\":\"start\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2,\"typeName\":\"() =\\u003e Promise\\u003cvoid\\u003e\"},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"afterListen\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2,\"typeName\":\"(server: Server) =\\u003e Promise\\u003cvoid\\u003e\"},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"onListenError\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2,\"typeName\":\"(error: unknown) =\\u003e Promise\\u003cvoid\\u003e\"},{\"kind\":4}]},\"optional\":true}]},\"HttpServerRuntimeOptions\":{\"kind\":18,\"typeName\":\"HttpServerRuntimeOptions\",\"types\":[{\"kind\":20,\"name\":\"config\",\"type\":{\"kind\":2,\"typeName\":\"C\"},\"optional\":false},{\"kind\":20,\"name\":\"router\",\"type\":{\"kind\":16,\"typeName\":\"HttpRouter\",\"classType\":{\"$tsfImport\":[0,\"./router\",\"HttpRouter\"]}},\"optional\":false},{\"kind\":20,\"name\":\"logger\",\"type\":{\"kind\":16,\"typeName\":\"ScopedLogger\",\"classType\":{\"$tsfImport\":[0,\"../services/index\",\"ScopedLogger\"]}},\"optional\":false},{\"kind\":20,\"name\":\"frameworkConfig\",\"type\":{\"kind\":12,\"types\":[{\"kind\":18,\"typeName\":\"Record\",\"utilityType\":\"Record\",\"typeArguments\":[{\"kind\":6},{\"kind\":2}],\"index\":{\"kind\":2},\"types\":[]},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"serverConfig\",\"type\":{\"kind\":12,\"types\":[{\"kind\":18,\"typeName\":\"Record\",\"utilityType\":\"Record\",\"typeArguments\":[{\"kind\":6},{\"kind\":2}],\"index\":{\"kind\":2},\"types\":[]},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"cors\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2,\"typeName\":\"HttpCorsConfig\",\"typeArguments\":[{\"kind\":2,\"typeName\":\"C\"}]},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"staticFiles\",\"type\":{\"kind\":12,\"types\":[{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":18,\"typeName\":\"StaticFilesOptions\",\"types\":[{\"kind\":20,\"name\":\"directory\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"index\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"spaFallback\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true}]}]},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"requestLogging\",\"type\":{\"kind\":12,\"types\":[{\"kind\":18,\"typeName\":\"HttpRequestLoggingOptions\",\"types\":[{\"kind\":20,\"name\":\"excludePaths\",\"type\":{\"kind\":12,\"types\":[{\"kind\":14,\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":16,\"typeName\":\"RegExp\",\"classType\":{\"$tsf\":1}}]}},{\"kind\":4}]},\"optional\":true}]},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"packageName\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"devConsoleEnabled\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"listenHooks\",\"type\":{\"kind\":12,\"types\":[{\"kind\":12,\"types\":[{\"kind\":18,\"typeName\":\"HttpServerListenHooks\",\"types\":[{\"kind\":20,\"name\":\"start\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2,\"typeName\":\"() =\\u003e Promise\\u003cvoid\\u003e\"},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"afterListen\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2,\"typeName\":\"(server: Server) =\\u003e Promise\\u003cvoid\\u003e\"},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"onListenError\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2,\"typeName\":\"(error: unknown) =\\u003e Promise\\u003cvoid\\u003e\"},{\"kind\":4}]},\"optional\":true}]},{\"kind\":2,\"typeName\":\"() =\\u003e HttpServerListenHooks\"}]},{\"kind\":4}]},\"optional\":true}]}}]", [require, () => (typeof RegExp !== "undefined" ? RegExp : (typeof exports !== "undefined" ? exports.RegExp : undefined))], __tsf_metadata_type);
//# sourceMappingURL=base.js.map