"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SrpcClient = exports.__ΩSrpcClientOptions = void 0;
const tslib_1 = require("tslib");
const __ΩError = ['name', 'message', 'stack', 'Error', 'P&4!&4"&4#8Mw$y'];
const __ΩNonNullable = ['T', 'NonNullable', 'b!Pe"!PMKw"y'];
function __assignType(fn, args) {
    fn.__type = args;
    return fn;
}
/*@ts-ignore*/
var { __ΩLoggerInterface } = require('@deepkit/logger');
const crypto_1 = require("crypto");
const lodash_1 = require("lodash");
const ws_1 = tslib_1.__importDefault(require("ws"));
const resolver_1 = require("../app/resolver");
const helpers_1 = require("../helpers");
const services_1 = require("../services");
const telemetry_1 = require("../telemetry");
const SrpcByteStream_1 = require("./SrpcByteStream");
/*@ts-ignore*/
var { __ΩRequestKeys, __ΩIQueuedRequest, __ΩBaseMessage, __ΩSrpcMessageFns, __ΩSrpcMeta, __ΩHandlerRequestData, __ΩResponseData, __ΩRequestData } = require('./types');
const __ΩSrpcClientOptions = ['enableReconnect', 'SrpcClientOptions', 'P)4!8Mw"y'];
exports.__ΩSrpcClientOptions = __ΩSrpcClientOptions;
class SrpcClient {
    constructor(logger, uri, 
    /** ts-proto generated ClientMessage type with encode/decode */
    clientMessage, 
    /** ts-proto generated ServerMessage type with encode/decode */
    serverMessage, clientId, clientMeta, clientSecret, clientOptions) {
        this.logger = logger;
        this.uri = uri;
        this.clientId = clientId;
        this.clientMeta = clientMeta;
        this.clientSecret = clientSecret;
        this.streamConnectionHandlers = (Set.Ω = [['', 'P$/!']], new Set());
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        this.streamMessageHandlers = (Map.Ω = [[() => __ΩRequestKeys, '!o!"'], ['resultType', 'data', '', 'handler', 'P&4!P"2""`/#4$M']], new Map());
        this.streamDisconnectionHandlers = (Set.Ω = [['', 'P$/!']], new Set());
        this.requestQueue = (Map.Ω = [['&'], [() => __ΩIQueuedRequest, 'n!']], new Map());
        this.intentionalDisconnect = false;
        this.streamId = '';
        this.isConnected = false;
        ////////////////////////////////////////
        // Byte Stream Support
        this.byteStream = {
            parentStreamId: '',
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            write: __assignType((streamId, chunk) => {
                return this.writeMessage({ byteStreamOperation: { streamId, write: { chunk } } });
            }, ['streamId', 'chunk', '', 'P\'2!"2""/#']),
            finish: __assignType((streamId) => {
                this.writeMessage({ byteStreamOperation: { streamId, finish: {} } });
            }, ['streamId', '', 'P\'2!"/"']),
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            destroy: __assignType((streamId, err) => {
                this.writeMessage({ byteStreamOperation: { streamId, destroy: { error: err ? String(err) : undefined } } });
            }, ['streamId', 'err', '', 'P\'2!"2"8"/#']),
            attachDisconnectHandler: __assignType((handler) => {
                this.ws?.on('close', handler);
            }, ['', 'handler', 'PP$/!2""/!']),
            detachDisconnectHandler: __assignType((handler) => {
                this.ws?.off('close', handler);
            }, ['', 'handler', 'PP$/!2""/!']),
            getBufferedAmount: () => {
                return this.ws?.bufferedAmount ?? 0;
            }
        };
        this.enableReconnect = clientOptions?.enableReconnect !== false;
        this.outboundType = clientMessage;
        this.inboundType = serverMessage;
    }
    ////////////////////////////////////////
    // Connection Management
    connect() {
        if (this.ws) {
            this.intentionalDisconnect = true;
            this.ws.close();
            this.ws = undefined;
        }
        this.intentionalDisconnect = false;
        this.logger.info('Connecting...');
        this.streamId = (0, helpers_1.uuid7)();
        this.byteStream.parentStreamId = this.streamId;
        SrpcByteStream_1.SrpcByteStream.init(this, { startId: 1, step: 2 });
        const wsUrl = this.generateWsUrl();
        const ws = new ws_1.default(wsUrl);
        ws.binaryType = 'nodebuffer';
        const connectTimeout = setTimeout(() => {
            this.logger.warn('Connection timeout');
            ws.close();
            this.queueReconnect();
        }, 10_000);
        const clearConnectTimeout = (0, lodash_1.once)(() => clearTimeout(connectTimeout));
        ws.once('open', () => {
            if (this.ws !== ws)
                return;
            this.logger.debug('WebSocket connection opened, waiting for server ping');
        });
        ws.once('message', __assignType((data) => this.handleInitialHandshake(ws, data, clearConnectTimeout), ['data', '', 'P!2!"/"']));
        ws.on('close', __assignType((code, reason) => this.handleClose(ws, code, reason, clearConnectTimeout), ['code', 'reason', '', 'P"2!"2""/#']));
        ws.on('error', __assignType((err) => this.handleError(ws, err, clearConnectTimeout), [() => __ΩError, 'err', '', 'Pn!2""/#']));
        this.ws = ws;
    }
    disconnect() {
        this.enableReconnect = false;
        this.intentionalDisconnect = true;
        if (this.reconnectionTimeout) {
            clearTimeout(this.reconnectionTimeout);
            this.reconnectionTimeout = undefined;
        }
        if (this.pingInterval) {
            clearInterval(this.pingInterval);
            this.pingInterval = undefined;
        }
        if (this.ws) {
            this.ws.close(1000, 'Client disconnect');
        }
    }
    /**
     * Trigger an immediate connection health check.
     * Sends a ping and expects a pong before the next scheduled ping interval,
     * otherwise the connection will be reset due to pong timeout.
     */
    triggerConnectionCheck() {
        if (!this.isConnected)
            return;
        this.logger.debug('Triggering connection check');
        this.lastPongMs = Date.now() - 20_000;
        this.writeMessage({ pingPong: {} });
    }
    ////////////////////////////////////////
    // Connection Handlers
    handleInitialHandshake(ws, data, clearConnectTimeout) {
        if (this.ws !== ws)
            return;
        clearConnectTimeout();
        const message = this.decodeMessage(data);
        if (!message) {
            this.logger.warn('Failed to decode initial message');
            ws.close();
            return;
        }
        if (!message.pingPong) {
            this.logger.warn('Expected pingPong as first message, got:', message);
            ws.close();
            return;
        }
        this.lastPongMs = Date.now();
        this.writeMessage({ pingPong: {} });
        this.logger.info('Stream established');
        this.isConnected = true;
        this.pingInterval = setInterval(() => this.doPingPong(), 55_000);
        this.streamConnectionHandlers.forEach(__assignType(handler => handler(), ['handler', '', 'P"2!"/"']));
        ws.on('message', __assignType(async (msgData) => this.handleMessage(msgData), ['msgData', '', 'P!2!"/"']));
    }
    handleClose(ws, code, reason, clearConnectTimeout) {
        if (ws !== this.ws)
            return;
        clearConnectTimeout();
        if (this.intentionalDisconnect) {
            this.logger.debug('WebSocket closed by client');
        }
        else {
            this.logger.info('Stream ended', { code, reason: String(reason) });
        }
        this.handleDisconnect();
    }
    handleError(ws, err, clearConnectTimeout) {
        if (ws !== this.ws)
            return;
        clearConnectTimeout();
        if (!this.intentionalDisconnect) {
            this.logger.warn('WebSocket error', err);
        }
        ws.terminate();
        this.handleDisconnect();
    }
    handleDisconnect() {
        if (this.reconnectionTimeout) {
            return;
        }
        if (this.enableReconnect) {
            this.queueReconnect();
        }
        clearInterval(this.pingInterval);
        this.pingInterval = undefined;
        const wasConnected = this.isConnected;
        this.isConnected = false;
        this.ws = undefined;
        if (wasConnected) {
            this.streamDisconnectionHandlers.forEach(__assignType(handler => handler(), ['handler', '', 'P"2!"/"']));
        }
        for (const queueItem of this.requestQueue.values()) {
            queueItem.reject(new Error('Disconnected'));
        }
        this.requestQueue.clear();
    }
    queueReconnect() {
        if (!this.enableReconnect) {
            return;
        }
        this.reconnectionTimeout = setTimeout(() => {
            this.reconnectionTimeout = undefined;
            this.connect();
        }, 1000);
    }
    doPingPong() {
        if ((this.lastPongMs ?? 0) < Date.now() - 75_000) {
            this.logger.warn('Pong timeout');
            this.ws?.close(4001, 'Pong timeout');
            return;
        }
        this.writeMessage({ pingPong: {} });
    }
    ////////////////////////////////////////
    // Message Handling
    async handleMessage(data) {
        const message = this.decodeMessage(data);
        if (!message) {
            this.logger.warn('Failed to decode message');
            return;
        }
        if (message.pingPong) {
            this.lastPongMs = Date.now();
            return;
        }
        if (message.byteStreamOperation) {
            return this.handleByteStreamOperation(message.byteStreamOperation);
        }
        const { requestId, reply } = message;
        this.logger.debug('Server message received', { requestId, reply, error: !!message.error });
        if (!requestId) {
            this.logger.warn('Protocol error: missing request ID, terminating connection');
            this.ws?.close(4002, 'Invalid request ID');
            return;
        }
        if (reply) {
            return this.handleReply(requestId, message);
        }
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        await this.handleServerRequest(requestId, message);
    }
    handleByteStreamOperation(op) {
        if (op.write) {
            return SrpcByteStream_1.SrpcByteStream.writeReceiver(this, op.streamId, op.write.chunk);
        }
        if (op.finish) {
            return SrpcByteStream_1.SrpcByteStream.finishReceiver(this, op.streamId);
        }
        if (op.destroy) {
            return SrpcByteStream_1.SrpcByteStream.destroySubstream(this, op.streamId, op.destroy.error);
        }
    }
    handleReply(requestId, message) {
        const queueItem = this.requestQueue.get(requestId);
        if (!queueItem) {
            this.logger.warn('Protocol error: unknown request ID for reply, terminating connection', { requestId });
            this.ws?.close(4003, 'Unknown request ID');
            return;
        }
        this.requestQueue.delete(requestId);
        if (message.error) {
            return queueItem.reject(message.error);
        }
        return queueItem.resolve(message);
    }
    async handleServerRequest(requestId, message) {
        for (const key of this.streamMessageHandlers.keys()) {
            if (message[key]) {
                const logMeta = { requestId, requestType: key, traceId: message.trace?.traceId };
                const response = await (0, telemetry_1.withRemoteSpan)('srpc:handleServerRequest', message.trace, { requestId, requestType: key }, () => (0, services_1.withLoggerContext)({ srpc: logMeta }, async () => {
                    try {
                        this.logger.info('Server request received');
                        const handlerMeta = this.streamMessageHandlers.get(key);
                        const result = await handlerMeta.handler(message[key]);
                        this.logger.info('Server request processed');
                        return {
                            [handlerMeta.resultType]: result
                        };
                    }
                    catch (err) {
                        this.logger.warn('Server request failed', err);
                        throw err;
                    }
                }));
                this.writeMessage({ requestId, reply: true, ...response });
                return;
            }
        }
        this.logger.error('Unhandled message type', { requestId });
        this.writeMessage({ requestId, reply: true, error: 'Unhandled message type' });
    }
    ////////////////////////////////////////
    // Protocol Helpers
    generateWsUrl() {
        const authv = 1;
        const appv = '0.0.0'; // todo: autodetect
        const ts = Date.now();
        const cid = this.clientId;
        const signable = `${authv}\n${appv}\n${ts}\n${this.streamId}\n${cid}\n`;
        const secret = this.clientSecret ?? (0, resolver_1.getAppConfig)().SRPC_AUTH_SECRET;
        if (!secret)
            throw new Error('SRPC_AUTH_SECRET is not configured.');
        const signature = (0, crypto_1.createHmac)('sha256', secret).update(signable).digest('hex');
        const params = new URLSearchParams({
            authv: String(authv),
            appv,
            ts: String(ts),
            id: this.streamId,
            cid,
            signature
        });
        if (this.clientMeta) {
            for (const [key, value] of Object.entries(this.clientMeta)) {
                params.set(`m--${key}`, String(value));
            }
        }
        const baseUri = this.uri.startsWith('ws://') || this.uri.startsWith('wss://') ? this.uri : `ws://${this.uri}`;
        const url = new URL(baseUri);
        url.search = params.toString();
        return url.toString();
    }
    decodeMessage(data) {
        try {
            return this.inboundType.decode(data);
        }
        catch (err) {
            this.logger.error('Failed to decode message', { err: String(err) });
            return null;
        }
    }
    writeMessage(message) {
        if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
            return false;
        }
        try {
            const encoded = this.outboundType.encode(message).finish();
            this.ws.send(encoded);
            return true;
        }
        catch (err) {
            this.logger.error('Failed to encode/send message', { err: String(err) });
            return false;
        }
    }
    ////////////////////////////////////////
    // Public API
    registerConnectionHandler(handler) {
        this.streamConnectionHandlers.add(handler);
    }
    /**
     * Register a handler for server requests (downstream/server-initiated).
     * @example client.registerMessageHandler('dNotify', async (data) => ({ acknowledged: true }))
     */
    registerMessageHandler(prefix, handler) {
        const actionType = `${prefix}Request`;
        const resultType = `${prefix}Response`;
        this.streamMessageHandlers.set(actionType, {
            resultType,
            handler
        });
    }
    registerDisconnectHandler(handler) {
        this.streamDisconnectionHandlers.add(handler);
    }
    /**
     * Invoke a server method.
     * @example client.invoke('uEcho', { message: 'hello' })
     */
    invoke(prefix, data, timeoutMs = 30_000) {
        const requestType = `${prefix}Request`;
        const resultType = `${prefix}Response`;
        const requestId = (0, helpers_1.uuid7)();
        const traceContext = (0, telemetry_1.getTraceContext)();
        const trace = traceContext ? { traceId: traceContext.traceId, spanId: traceContext.spanId, traceFlags: traceContext.traceFlags } : undefined;
        return (0, telemetry_1.withSpan)('srpc:invokeServer', { requestType }, () => (0, services_1.withLoggerContext)({ srpc: { requestId, requestType, traceId: trace?.traceId } }, async () => {
            this.logger.info('Requesting server invocation');
            let timeoutHandle;
            try {
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                const responsePromise = (Promise.Ω = [['"']], new Promise(__assignType((resolve, reject) => {
                    this.requestQueue.set(requestId, {
                        exp: Date.now() + timeoutMs,
                        resolve,
                        reject
                    });
                    const sent = this.writeMessage({ requestId, trace, [requestType]: data });
                    if (!sent) {
                        this.requestQueue.delete(requestId);
                        reject(new Error('Failed to send request: not connected'));
                    }
                }, ['resolve', 'reject', '', 'P"2!"2""/#'])));
                const timeoutPromise = (Promise.Ω = [['!']], new Promise(__assignType((_, reject) => {
                    timeoutHandle = setTimeout(() => {
                        if (this.requestQueue.has(requestId)) {
                            this.requestQueue.delete(requestId);
                            reject(new Error(`Request timeout after ${timeoutMs}ms`));
                        }
                    }, timeoutMs);
                }, ['_', 'reject', '', 'P"2!"2""/#'])));
                const result = await Promise.race([responsePromise, timeoutPromise]);
                clearTimeout(timeoutHandle);
                // Use null check instead of `in` for consistency with ts-proto field materialization
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                if (result[resultType] == null) {
                    throw new Error('Invalid response from server');
                }
                this.logger.info('Server invocation completed');
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                return result[resultType];
            }
            catch (err) {
                clearTimeout(timeoutHandle);
                this.logger.warn('Server invocation failed', err);
                throw err;
            }
        }));
    }
}
exports.SrpcClient = SrpcClient;
SrpcClient.__type = [() => __ΩBaseMessage, 'TClientInput', () => __ΩBaseMessage, 'TServerOutput', () => ws_1.default, 'ws', () => __ΩSrpcMessageFns, 'outboundType', () => __ΩSrpcMessageFns, 'inboundType', 'streamConnectionHandlers', function () { return (Set.Ω = [['', 'P$/!']], new Set()); }, 'streamMessageHandlers', function () { return (Map.Ω = [[() => __ΩRequestKeys, '!o!"'], ['resultType', 'data', '', 'handler', 'P&4!P"2""`/#4$M']], new Map()); }, 'streamDisconnectionHandlers', function () { return (Set.Ω = [['', 'P$/!']], new Set()); }, 'reconnectionTimeout', 'pingInterval', 'lastPongMs', 'requestQueue', function () { return (Map.Ω = [['&'], [() => __ΩIQueuedRequest, 'n!']], new Map()); }, 'enableReconnect', 'intentionalDisconnect', function () { return false; }, 'streamId', function () { return ''; }, 'isConnected', function () { return false; }, () => __ΩLoggerInterface, 'logger', 'uri', () => __ΩSrpcMessageFns, 'clientMessage', () => __ΩSrpcMessageFns, 'serverMessage', 'clientId', () => __ΩSrpcMeta, 'clientMeta', 'clientSecret', () => __ΩSrpcClientOptions, 'clientOptions', 'constructor', 'connect', 'disconnect', 'triggerConnectionCheck', () => ws_1.default, 'data', '', 'clearConnectTimeout', 'handleInitialHandshake', () => ws_1.default, 'code', 'reason', 'handleClose', () => ws_1.default, () => __ΩError, 'err', 'handleError', 'handleDisconnect', 'queueReconnect', 'doPingPong', 'handleMessage', () => __ΩNonNullable, "byteStreamOperation", 'op', 'handleByteStreamOperation', 'requestId', () => __ΩBaseMessage, 'message', 'handleReply', () => __ΩBaseMessage, 'handleServerRequest', 'generateWsUrl', () => __ΩBaseMessage, 'decodeMessage', () => __ΩBaseMessage, 'writeMessage', 'byteStream', function () { return {
        parentStreamId: '',
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        write: __assignType((streamId, chunk) => {
            return this.writeMessage({ byteStreamOperation: { streamId, write: { chunk } } });
        }, ['streamId', 'chunk', '', 'P\'2!"2""/#']),
        finish: __assignType((streamId) => {
            this.writeMessage({ byteStreamOperation: { streamId, finish: {} } });
        }, ['streamId', '', 'P\'2!"/"']),
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        destroy: __assignType((streamId, err) => {
            this.writeMessage({ byteStreamOperation: { streamId, destroy: { error: err ? String(err) : undefined } } });
        }, ['streamId', 'err', '', 'P\'2!"2"8"/#']),
        attachDisconnectHandler: __assignType((handler) => {
            this.ws?.on('close', handler);
        }, ['', 'handler', 'PP$/!2""/!']),
        detachDisconnectHandler: __assignType((handler) => {
            this.ws?.off('close', handler);
        }, ['', 'handler', 'PP$/!2""/!']),
        getBufferedAmount: () => {
            return this.ws?.bufferedAmount ?? 0;
        }
    }; }, 'handler', 'registerConnectionHandler', 'prefix', () => __ΩHandlerRequestData, () => __ΩResponseData, 'registerMessageHandler', 'registerDisconnectHandler', () => __ΩRequestData, 'timeoutMs', () => __ΩResponseData, 'invoke', 'SrpcClient', 'n!c"n#c$P7%3&8;e!!o\'"3(;e!"o)"3*;!3+;>,!3-;>.!3/;>0!318;!328;\'338;!34;>5)36;)37;>8&39;>:)3;><Pn=2>;&2?;e"!o@"2Ae""oB"2C&2D;nE2F8;&2G8;nH2I8"0JP"0KP"0LP"0MPP7N2&!2OP$/P2Q"0R;PP7S2&\'2TP!&J2UP$/P2Q"0V;PP7W2&nX2YP$/P2Q"0Z;P"0[;P"0\\;P"0];P!2O"0^;Pe"".`fo_"2a"0b;P&2cPe#"ndK2e"0f;P&2cPe#"ngK2e$`0h;P&0i;P!2OPPe$"njK,J0k;Pnl2e)0m;!3n>oPP$/P2p"0qP"2rPe#""os#2Oe#!"ot#`/P2p"0uPP$/P2p"0vP"2re"!"ow#2O"2xe"""oy#`0z5w{'];
//# sourceMappingURL=SrpcClient.js.map