"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.__tsfTypeAliases = exports.SrpcClient = exports.SrpcConflictError = void 0;
const tslib_1 = require("tslib");
const _a = tslib_1.__importStar(require("@zyno-io/ts-server-foundation/type-metadata-runtime"));
const node_crypto_1 = require("node:crypto");
const ws_1 = tslib_1.__importDefault(require("ws"));
const app_1 = require("../app");
const helpers_1 = require("../helpers");
const SrpcByteStream_1 = require("./SrpcByteStream");
const types_1 = require("./types");
const __tsf_metadata_type = _a.createCompactMetadataRegistryV1("[1,[{\"kind\":2},{\"kind\":18,\"types\":[{\"kind\":20,\"name\":\"supersede\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true}]},{\"kind\":22,\"type\":{\"kind\":3}},{\"kind\":3},{\"kind\":2,\"typeName\":\"() =\\u003e void\"},{\"kind\":2,\"typeName\":\"P\"},{\"kind\":2,\"typeName\":\"(data: HandlerRequestData\\u003cTServerOutput, P\\u003e) =\\u003e Promise\\u003cResponseData\\u003cTClientInput, P\\u003e\\u003e | ResponseData\\u003cTClientInput, P\\u003e\"},{\"kind\":2,\"typeName\":\"(cause: SrpcDisconnectCause) =\\u003e void\"},{\"kind\":0},{\"kind\":18,\"typeName\":\"ResponseData\\u003cTServerOutput, P\\u003e\",\"types\":[{\"kind\":20,\"name\":\"\u00FE@toStringTag@64\",\"type\":{\"kind\":6},\"optional\":false}]},{\"kind\":18,\"typeName\":\"ISrpcLogger\",\"types\":[]},{\"kind\":6},{\"kind\":17,\"typeName\":\"SrpcMeta\"},{\"kind\":18,\"typeName\":\"SrpcClientOptions\",\"types\":[{\"kind\":20,\"name\":\"enableReconnect\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"protocolVersion\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true}]}]]", []);
let _SrpcConflictErrorMetadata_1;
class SrpcConflictError extends Error {
    constructor() {
        super('Client ID is already connected');
        this.name = 'SrpcConflictError';
    }
    static __tsfType = _SrpcConflictErrorMetadata_1 = _a.decodeCompactMetadataV1("[1,{\"kind\":16,\"name\":\"SrpcConflictError\",\"typeName\":\"SrpcConflictError\",\"classType\":{\"$tsf\":0},\"properties\":[],\"methods\":[],\"hasConstructor\":true,\"constructorParameters\":[]}]", [() => SrpcConflictError], __tsf_metadata_type);
}
exports.SrpcConflictError = SrpcConflictError;
_SrpcConflictErrorMetadata_1.classType = SrpcConflictError;
SrpcConflictError.__tsfType = _SrpcConflictErrorMetadata_1;
let _SrpcClientMetadata_1;
class SrpcClient {
    logger;
    uri;
    clientMessage;
    serverMessage;
    clientId;
    clientMeta;
    clientSecret;
    ws;
    streamConnectionHandlers = new Set();
    streamDisconnectionHandlers = new Set();
    streamMessageHandlers = new Map();
    requestQueue = new Map();
    connectResolve;
    connectReject;
    reconnectionTimeout;
    pingInterval;
    lastPongMs = 0;
    intentionalDisconnect = false;
    supersede = false;
    streamId = '';
    enableReconnect;
    protocolVersion;
    isConnected = false;
    constructor(logger, uri, clientMessage, serverMessage, clientId, clientMeta, clientSecret, clientOptions) {
        this.logger = logger;
        this.uri = uri;
        this.clientMessage = clientMessage;
        this.serverMessage = serverMessage;
        this.clientId = clientId;
        this.clientMeta = clientMeta;
        this.clientSecret = clientSecret;
        this.enableReconnect = clientOptions?.enableReconnect !== false;
        this.protocolVersion = clientOptions?.protocolVersion ?? 2;
    }
    connect(options) {
        if (this.reconnectionTimeout) {
            clearTimeout(this.reconnectionTimeout);
            this.reconnectionTimeout = undefined;
        }
        this.connectReject?.(new Error('Connection superseded by new connect() call'));
        this.connectResolve = undefined;
        this.connectReject = undefined;
        if (this.ws) {
            this.intentionalDisconnect = true;
            this.ws.close();
            this.ws = undefined;
        }
        this.intentionalDisconnect = false;
        this.supersede = options?.supersede ?? false;
        this.streamId = (0, helpers_1.uuid7)();
        this.byteStream.parentStreamId = this.streamId;
        SrpcByteStream_1.SrpcByteStream.init(this, { startId: 1, step: 2 });
        const ws = new ws_1.default(this.generateWsUrl());
        ws.binaryType = 'nodebuffer';
        this.ws = ws;
        const connectTimeout = setTimeout(() => {
            this.connectReject?.(new Error('Connection failed: timeout'));
            this.connectResolve = undefined;
            this.connectReject = undefined;
            ws.close();
            this.queueReconnect();
        }, 10000);
        const clearConnectTimeout = () => clearTimeout(connectTimeout);
        ws.once('message', data => this.handleInitialHandshake(ws, data, clearConnectTimeout));
        ws.on('close', (code, reason) => this.handleClose(ws, code, reason, clearConnectTimeout));
        ws.on('error', error => this.handleError(ws, error, clearConnectTimeout));
        const promise = new Promise((resolve, reject) => {
            this.connectResolve = resolve;
            this.connectReject = reject;
        });
        promise.catch(() => { });
        return promise;
    }
    disconnect() {
        this.enableReconnect = false;
        this.intentionalDisconnect = true;
        if (this.reconnectionTimeout)
            clearTimeout(this.reconnectionTimeout);
        if (this.pingInterval)
            clearInterval(this.pingInterval);
        this.ws?.close(1000, 'Client disconnect');
    }
    triggerConnectionCheck() {
        if (!this.isConnected)
            return;
        this.lastPongMs = Date.now() - 20000;
        this.writeMessage({ pingPong: {} });
    }
    handleInitialHandshake(ws, data, clearConnectTimeout) {
        if (this.ws !== ws)
            return;
        clearConnectTimeout();
        const message = this.decodeMessage(data);
        if (!message?.pingPong) {
            ws.close(4000, 'Expected handshake ping');
            return;
        }
        this.lastPongMs = Date.now();
        this.writeMessage({ pingPong: {} });
        this.isConnected = true;
        this.pingInterval = setInterval(() => this.doPingPong(), 55000);
        this.pingInterval.unref?.();
        this.connectResolve?.();
        this.connectResolve = undefined;
        this.connectReject = undefined;
        for (const handler of this.streamConnectionHandlers)
            handler();
        ws.on('message', msgData => this.handleMessage(msgData));
    }
    handleClose(ws, code, _reason, clearConnectTimeout) {
        if (ws !== this.ws)
            return;
        clearConnectTimeout();
        const cause = parseDisconnectCause(code);
        if (cause === 'conflict') {
            this.connectReject?.(new SrpcConflictError());
            this.connectResolve = undefined;
            this.connectReject = undefined;
            this.handleDisconnect(cause, true);
            return;
        }
        this.handleDisconnect(cause);
    }
    handleError(ws, error, clearConnectTimeout) {
        if (ws !== this.ws)
            return;
        clearConnectTimeout();
        if (!this.intentionalDisconnect)
            this.logger.warn('SRPC WebSocket error', error);
        ws.terminate();
        this.handleDisconnect('disconnect');
    }
    handleDisconnect(cause = 'disconnect', suppressReconnect = false) {
        if (this.reconnectionTimeout)
            return;
        if (this.enableReconnect && !suppressReconnect && !this.intentionalDisconnect)
            this.queueReconnect();
        if (this.pingInterval)
            clearInterval(this.pingInterval);
        this.pingInterval = undefined;
        this.connectReject?.(new Error(`Connection failed: ${cause}`));
        this.connectResolve = undefined;
        this.connectReject = undefined;
        const wasConnected = this.isConnected;
        this.isConnected = false;
        this.ws = undefined;
        if (wasConnected) {
            for (const handler of this.streamDisconnectionHandlers)
                handler(cause);
        }
        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().catch(() => { });
        }, 1000);
        this.reconnectionTimeout.unref?.();
    }
    doPingPong() {
        if (this.lastPongMs < Date.now() - 75000) {
            this.ws?.close(4003, 'Pong timeout');
            return;
        }
        this.writeMessage({ pingPong: {} });
    }
    async handleMessage(data) {
        const message = this.decodeMessage(data);
        if (!message)
            return;
        if (message.pingPong) {
            this.lastPongMs = Date.now();
            return;
        }
        if (message.byteStreamOperation) {
            this.handleByteStreamOperation(message.byteStreamOperation);
            return;
        }
        if (!message.requestId) {
            this.ws?.close(4000, 'Invalid request ID');
            return;
        }
        if (message.reply) {
            this.handleReply(message.requestId, message);
            return;
        }
        await this.handleServerRequest(message.requestId, message);
    }
    handleByteStreamOperation(op) {
        if (op.write)
            SrpcByteStream_1.SrpcByteStream.writeReceiver(this, op.streamId, op.write.chunk);
        else if (op.finish)
            SrpcByteStream_1.SrpcByteStream.finishReceiver(this, op.streamId);
        else if (op.destroy)
            SrpcByteStream_1.SrpcByteStream.destroySubstream(this, op.streamId, op.destroy.error);
    }
    handleReply(requestId, message) {
        const queueItem = this.requestQueue.get(requestId);
        if (!queueItem) {
            this.ws?.close(4000, 'Unknown request ID');
            return;
        }
        this.requestQueue.delete(requestId);
        if (message.error)
            queueItem.reject(new Error(message.error));
        else
            queueItem.resolve(message);
    }
    async handleServerRequest(requestId, message) {
        for (const [key, handlerMeta] of this.streamMessageHandlers) {
            const requestData = message[key];
            if (requestData == null)
                continue;
            try {
                const result = await handlerMeta.handler(requestData);
                this.writeMessage({
                    requestId,
                    reply: true,
                    [handlerMeta.resultType]: result
                });
            }
            catch (error) {
                this.writeMessage({ requestId, reply: true, error: String(error) });
            }
            return;
        }
        this.writeMessage({ requestId, reply: true, error: 'Unhandled message type' });
    }
    generateWsUrl() {
        const authv = 1;
        const appv = '0.0.0';
        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, app_1.getCurrentApp)().config.SRPC_AUTH_SECRET;
        if (!secret)
            throw new Error('SRPC_AUTH_SECRET is not configured.');
        const signature = (0, node_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,
            _v: String(this.protocolVersion)
        });
        if (this.supersede) {
            params.set('_supersede', '1');
            this.supersede = false;
        }
        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.serverMessage.decode(toBuffer(data));
        }
        catch (error) {
            this.logger.error('Failed to decode SRPC message', error);
            return null;
        }
    }
    writeMessage(message) {
        if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
            return false;
        this.ws.send((0, types_1.encodeSrpcMessage)(this.clientMessage, message));
        return true;
    }
    writeMessageAsync(message) {
        const ws = this.ws;
        if (!ws || ws.readyState !== ws_1.default.OPEN)
            return Promise.reject(new Error('Failed to send SRPC message: not connected'));
        const encoded = (0, types_1.encodeSrpcMessage)(this.clientMessage, message);
        return new Promise((resolve, reject) => {
            ws.send(encoded, error => {
                if (error)
                    reject(error);
                else
                    resolve();
            });
        });
    }
    byteStream = {
        parentStreamId: '',
        write: (streamId, chunk) => this.writeMessageAsync({
            byteStreamOperation: { streamId, write: { chunk: chunk } }
        }),
        finish: (streamId) => {
            this.writeMessage({ byteStreamOperation: { streamId, finish: {} } });
        },
        destroy: (streamId, error) => {
            this.writeMessage({
                byteStreamOperation: { streamId, destroy: { error: error ? String(error) : undefined } }
            });
        },
        attachDisconnectHandler: (handler) => {
            this.ws?.on('close', handler);
        },
        detachDisconnectHandler: (handler) => {
            this.ws?.off('close', handler);
        },
        getBufferedAmount: () => this.ws?.bufferedAmount ?? 0
    };
    registerConnectionHandler(handler) {
        this.streamConnectionHandlers.add(handler);
    }
    registerMessageHandler(prefix, handler) {
        const actionType = `${prefix}Request`;
        this.streamMessageHandlers.set(actionType, {
            resultType: `${prefix}Response`,
            handler: handler
        });
    }
    registerDisconnectHandler(handler) {
        this.streamDisconnectionHandlers.add(handler);
    }
    invoke(prefix, data, timeoutMs = 30000) {
        const requestType = `${prefix}Request`;
        const resultType = `${prefix}Response`;
        const requestId = (0, helpers_1.uuid7)();
        return new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                this.requestQueue.delete(requestId);
                reject(new Error(`Request timeout after ${timeoutMs}ms`));
            }, timeoutMs);
            this.requestQueue.set(requestId, {
                exp: Date.now() + timeoutMs,
                resolve: response => {
                    clearTimeout(timeout);
                    const result = response[resultType];
                    if (result == null)
                        reject(new Error('Invalid response from server'));
                    else
                        resolve(result);
                },
                reject: error => {
                    clearTimeout(timeout);
                    reject(error);
                }
            });
            const sent = this.writeMessage({ requestId, [requestType]: data });
            if (!sent) {
                this.requestQueue.delete(requestId);
                clearTimeout(timeout);
                reject(new Error('Failed to send request: not connected'));
            }
        });
    }
    static __tsfType = _SrpcClientMetadata_1 = _a.decodeCompactMetadataV1("[1,{\"kind\":16,\"name\":\"SrpcClient\",\"typeName\":\"SrpcClient\",\"classType\":{\"$tsf\":0},\"properties\":[{\"name\":\"isConnected\",\"type\":{\"$tsfType\":0},\"optional\":false},{\"name\":\"byteStream\",\"type\":{\"$tsfType\":0},\"optional\":false}],\"methods\":[{\"name\":\"connect\",\"parameters\":[{\"name\":\"options\",\"type\":{\"$tsfType\":1},\"optional\":true,\"default\":false}],\"returnType\":{\"$tsfType\":2}},{\"name\":\"disconnect\",\"parameters\":[],\"returnType\":{\"$tsfType\":3}},{\"name\":\"triggerConnectionCheck\",\"parameters\":[],\"returnType\":{\"$tsfType\":3}},{\"name\":\"registerConnectionHandler\",\"parameters\":[{\"name\":\"handler\",\"type\":{\"$tsfType\":4},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":3}},{\"name\":\"registerMessageHandler\",\"parameters\":[{\"name\":\"prefix\",\"type\":{\"$tsfType\":5},\"optional\":false,\"default\":false},{\"name\":\"handler\",\"type\":{\"$tsfType\":6},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":3}},{\"name\":\"registerDisconnectHandler\",\"parameters\":[{\"name\":\"handler\",\"type\":{\"$tsfType\":7},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":3}},{\"name\":\"invoke\",\"parameters\":[{\"name\":\"prefix\",\"type\":{\"$tsfType\":5},\"optional\":false,\"default\":false},{\"name\":\"data\",\"type\":{\"$tsfType\":8},\"optional\":false,\"default\":false},{\"name\":\"timeoutMs\",\"type\":{\"$tsfType\":0},\"optional\":true,\"default\":true}],\"returnType\":{\"$tsfType\":9}}],\"hasConstructor\":true,\"constructorParameters\":[{\"name\":\"logger\",\"type\":{\"$tsfType\":10},\"optional\":false,\"default\":false},{\"name\":\"uri\",\"type\":{\"$tsfType\":11},\"optional\":false,\"default\":false},{\"name\":\"clientMessage\",\"type\":{\"kind\":16,\"typeName\":\"SrpcMessageFns\",\"classType\":{\"$tsfImport\":[1,\"./types\",\"SrpcMessageFns\"]},\"typeArguments\":[{\"kind\":16,\"typeName\":\"TClientInput\",\"classType\":{\"$tsf\":2}}]},\"optional\":false,\"default\":false},{\"name\":\"serverMessage\",\"type\":{\"kind\":16,\"typeName\":\"SrpcMessageFns\",\"classType\":{\"$tsfImport\":[1,\"./types\",\"SrpcMessageFns\"]},\"typeArguments\":[{\"kind\":16,\"typeName\":\"TServerOutput\",\"classType\":{\"$tsf\":3}}]},\"optional\":false,\"default\":false},{\"name\":\"clientId\",\"type\":{\"$tsfType\":11},\"optional\":false,\"default\":false},{\"name\":\"clientMeta\",\"type\":{\"$tsfType\":12},\"optional\":true,\"default\":false},{\"name\":\"clientSecret\",\"type\":{\"$tsfType\":11},\"optional\":true,\"default\":false},{\"name\":\"clientOptions\",\"type\":{\"$tsfType\":13},\"optional\":true,\"default\":false}]}]", [() => SrpcClient, require, () => (typeof TClientInput !== "undefined" ? TClientInput : (typeof exports !== "undefined" ? exports.TClientInput : undefined)), () => (typeof TServerOutput !== "undefined" ? TServerOutput : (typeof exports !== "undefined" ? exports.TServerOutput : undefined))], __tsf_metadata_type);
}
exports.SrpcClient = SrpcClient;
_SrpcClientMetadata_1.classType = SrpcClient;
SrpcClient.__tsfType = _SrpcClientMetadata_1;
function parseDisconnectCause(code) {
    if (code === 4000)
        return 'badArg';
    if (code === 4001)
        return 'conflict';
    if (code === 4002)
        return 'supersede';
    if (code === 4003)
        return 'timeout';
    return 'disconnect';
}
function toBuffer(data) {
    if (Buffer.isBuffer(data))
        return data;
    if (Array.isArray(data))
        return Buffer.concat(data);
    return Buffer.from(data);
}
exports.__tsfTypeAliases = _a.decodeCompactMetadataV1("[1,{\"SrpcClientOptions\":{\"kind\":18,\"typeName\":\"SrpcClientOptions\",\"types\":[{\"kind\":20,\"name\":\"enableReconnect\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"protocolVersion\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true}]}}]", [], __tsf_metadata_type);
//# sourceMappingURL=SrpcClient.js.map