"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 node_async_hooks_1 = require("node:async_hooks");
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\":18,\"typeName\":\"IByteStream\",\"types\":[{\"kind\":20,\"name\":\"parentStreamId\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"remoteSenderIdParity\",\"type\":{\"kind\":12,\"types\":[{\"kind\":12,\"types\":[{\"kind\":10,\"literal\":0},{\"kind\":10,\"literal\":1}]},{\"kind\":4}]},\"optional\":true}]},{\"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\":18,\"typeName\":\"SrpcMessageFns\",\"types\":[]},{\"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\":\"authAudience\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"senderAnnouncements\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"maxPendingRequests\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"maxPendingRequestBytes\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"maxInFlightServerRequests\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"maxInFlightServerRequestBytes\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"maxBufferedBytes\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"maxMessageBytes\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"connectTimeoutMs\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"lateReplyTombstoneTtlMs\",\"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;
const DefaultMaxRequests = 128;
const DefaultMaxBytes = 8 * 1024 * 1024;
const DefaultMaxMessageBytes = 8 * 1024 * 1024;
const DefaultConnectTimeoutMs = 60000;
const DefaultLateReplyTombstoneTtlMs = 60000;
const MaxLateReplyTombstones = 256;
const MaxByteStreamId = 0x7fffffff;
const MaxBackpressuredByteStreams = 1024;
let _SrpcClientMetadata_1;
class SrpcClient {
    logger;
    uri;
    clientMessage;
    serverMessage;
    clientId;
    clientMeta;
    clientSecret;
    clientOptions;
    handlerRequestId = new node_async_hooks_1.AsyncLocalStorage();
    ws;
    streamConnectionHandlers = new Set();
    streamDisconnectionHandlers = new Set();
    streamMessageHandlers = new Map();
    requestQueue = new Map();
    requestBytes = new Map();
    lateReplyTombstones = new Map();
    connectResolve;
    connectReject;
    reconnectionTimeout;
    pingInterval;
    lastPongMs = 0;
    intentionalDisconnect = false;
    supersede = false;
    hasEstablishedGeneration = false;
    streamId = '';
    enableReconnect;
    senderAnnouncements;
    generation = 0;
    handlerPressureByGeneration = new Map();
    byteStreamsByGeneration = new Map();
    byteStreamPressureByGeneration = new Map();
    byteStreamDisconnectHandlersByGeneration = new Map();
    awaitingActivation;
    clearConnectTimeout;
    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.clientOptions = clientOptions;
        validateClientResourceOptions(clientOptions);
        this.enableReconnect = clientOptions?.enableReconnect !== false;
        this.senderAnnouncements = clientOptions?.senderAnnouncements !== false;
    }
    connect(options) {
        if (this.reconnectionTimeout) {
            clearTimeout(this.reconnectionTimeout);
            this.reconnectionTimeout = undefined;
        }
        const wasConnected = this.isConnected;
        this.clearConnectTimeout?.();
        this.connectReject?.(new Error('Connection superseded by new connect() call'));
        this.rejectAllRequests(new types_1.SrpcIndeterminateDeliveryError(this.clientId, new Error('Connection superseded by new connect() call')));
        this.connectResolve = undefined;
        this.connectReject = undefined;
        this.isConnected = false;
        this.awaitingActivation = undefined;
        if (this.pingInterval)
            clearInterval(this.pingInterval);
        this.pingInterval = undefined;
        if (wasConnected) {
            for (const handler of this.streamDisconnectionHandlers) {
                try {
                    handler('supersede');
                }
                catch (error) {
                    this.logger.error('SRPC disconnect handler failed', error);
                }
            }
        }
        const previousGeneration = this.generation;
        const generation = ++this.generation;
        this.handlerPressureByGeneration.delete(previousGeneration);
        this.revokeByteStreamGeneration(previousGeneration);
        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.currentByteStream = this.createByteStream(generation);
        this.byteStreamsByGeneration.set(generation, this.currentByteStream);
        this.currentByteStream.parentStreamId = this.streamId;
        SrpcByteStream_1.SrpcByteStream.init({ byteStream: this.currentByteStream }, { startId: 1, step: 2 });
        const ws = new ws_1.default(this.generateWsUrl(), { maxPayload: this.maxMessageBytes });
        ws.binaryType = 'nodebuffer';
        this.ws = ws;
        let connectTimeout;
        const clearConnectTimeout = () => {
            clearTimeout(connectTimeout);
            if (this.clearConnectTimeout === clearConnectTimeout)
                this.clearConnectTimeout = undefined;
        };
        this.clearConnectTimeout = clearConnectTimeout;
        connectTimeout = setTimeout(() => {
            if (!this.isCurrent(ws, generation))
                return;
            this.revokeGeneration(ws, generation, 'timeout', false, new Error('Connection failed: timeout'));
            ws.close();
        }, this.connectTimeoutMs);
        ws.once('message', data => this.handleInitialHandshake(ws, generation, data, clearConnectTimeout));
        ws.on('close', (code, reason) => this.handleClose(ws, generation, code, reason, clearConnectTimeout));
        ws.on('error', error => this.handleError(ws, generation, error, clearConnectTimeout));
        const promise = new Promise((resolve, reject) => {
            this.connectResolve = resolve;
            this.connectReject = reject;
        });
        promise.catch(() => { });
        return promise;
    }
    disconnect() {
        const ws = this.ws;
        const generation = this.generation;
        this.enableReconnect = false;
        this.intentionalDisconnect = true;
        this.clearConnectTimeout?.();
        if (this.reconnectionTimeout)
            clearTimeout(this.reconnectionTimeout);
        this.reconnectionTimeout = undefined;
        if (this.pingInterval)
            clearInterval(this.pingInterval);
        this.pingInterval = undefined;
        if (ws && this.isCurrent(ws, generation)) {
            this.revokeGeneration(ws, generation, 'disconnect', true, new Error('Connection closed by client'));
        }
        ws?.close(1000, 'Client disconnect');
    }
    triggerConnectionCheck() {
        if (!this.isConnected)
            return;
        this.lastPongMs = Date.now() - 20000;
        this.writeMessage({ pingPong: {} });
    }
    handleInitialHandshake(ws, generation, data, clearConnectTimeout) {
        if (!this.isCurrent(ws, generation))
            return;
        const message = this.decodeMessage(data);
        if (!message?.pingPong) {
            this.closeGenerationWithError(ws, generation, 'badArg', 'Expected handshake ping');
            return;
        }
        this.lastPongMs = Date.now();
        if (!this.writeMessage({ pingPong: {} }, generation) || !this.isCurrent(ws, generation))
            return;
        this.awaitingActivation = { ws, generation, clearConnectTimeout };
        ws.on('message', msgData => {
            void this.handleMessage(ws, generation, msgData).catch(error => {
                this.logger.warn('Failed to dispatch SRPC message', error);
                this.closeGenerationWithError(ws, generation, 'badArg', 'Invalid message format');
            });
        });
    }
    completeConnection(ws, generation) {
        if (!this.isCurrent(ws, generation) || this.isConnected)
            return;
        this.awaitingActivation?.clearConnectTimeout();
        this.awaitingActivation = undefined;
        this.isConnected = true;
        this.hasEstablishedGeneration = true;
        this.pingInterval = setInterval(() => this.doPingPong(), 55000);
        this.pingInterval.unref?.();
        this.connectResolve?.();
        this.connectResolve = undefined;
        this.connectReject = undefined;
        for (const handler of this.streamConnectionHandlers) {
            try {
                handler();
            }
            catch (error) {
                this.logger.error('SRPC connection handler failed', error);
            }
        }
    }
    handleClose(ws, generation, code, _reason, clearConnectTimeout) {
        if (!this.isCurrent(ws, generation))
            return;
        clearConnectTimeout();
        const cause = parseDisconnectCause(code);
        if (cause === 'conflict') {
            this.revokeGeneration(ws, generation, cause, true, new SrpcConflictError());
            return;
        }
        this.revokeGeneration(ws, generation, cause);
    }
    handleError(ws, generation, error, clearConnectTimeout) {
        if (!this.isCurrent(ws, generation))
            return;
        clearConnectTimeout();
        if (!this.intentionalDisconnect)
            this.logger.warn('SRPC WebSocket error', error);
        this.revokeGeneration(ws, generation, 'disconnect');
        ws.terminate();
    }
    revokeGeneration(ws, generation, cause, suppressReconnect = false, connectError = new Error(`Connection failed: ${cause}`)) {
        if (!this.isCurrent(ws, generation))
            return false;
        this.clearConnectTimeout?.();
        if (this.pingInterval)
            clearInterval(this.pingInterval);
        this.pingInterval = undefined;
        this.connectReject?.(connectError);
        this.connectResolve = undefined;
        this.connectReject = undefined;
        const wasConnected = this.isConnected;
        this.isConnected = false;
        this.awaitingActivation = undefined;
        this.ws = undefined;
        this.handlerPressureByGeneration?.delete(generation);
        this.revokeByteStreamGeneration(generation);
        this.rejectAllRequests(new types_1.SrpcIndeterminateDeliveryError(this.clientId ?? '', new Error('Disconnected')));
        if (wasConnected) {
            for (const handler of this.streamDisconnectionHandlers ?? []) {
                try {
                    handler(cause);
                }
                catch (error) {
                    this.logger.error('SRPC disconnect handler failed', error);
                }
            }
        }
        if (this.enableReconnect && !suppressReconnect && !this.intentionalDisconnect && !this.reconnectionTimeout)
            this.queueReconnect();
        return true;
    }
    closeGenerationWithError(ws, generation, cause, message) {
        if (!this.revokeGeneration(ws, generation, cause))
            return;
        ws.close(closeCodeForCause(cause), message.slice(0, 123));
    }
    queueReconnect() {
        if (!this.enableReconnect)
            return;
        this.reconnectionTimeout = setTimeout(() => {
            this.reconnectionTimeout = undefined;
            this.connect({ supersede: this.hasEstablishedGeneration }).catch(() => { });
        }, 1000);
        this.reconnectionTimeout.unref?.();
    }
    doPingPong() {
        if (this.lastPongMs < Date.now() - 75000) {
            const ws = this.ws;
            if (ws)
                this.closeGenerationWithError(ws, this.generation, 'timeout', 'Pong timeout');
            return;
        }
        this.writeMessage({ pingPong: {} });
    }
    async handleMessage(ws, generation, data) {
        if (!this.isCurrent(ws, generation))
            return;
        const frameBytes = toBuffer(data).byteLength;
        if (frameBytes > this.maxMessageBytes) {
            this.closeGenerationWithError(ws, generation, 'badArg', 'SRPC message too large');
            return;
        }
        const message = this.decodeMessage(data);
        if (!message) {
            this.closeGenerationWithError(ws, generation, 'badArg', 'Invalid message format');
            return;
        }
        if (message.pingPong) {
            this.lastPongMs = Date.now();
            if (this.awaitingActivation?.ws === ws && this.awaitingActivation.generation === generation)
                this.completeConnection(ws, generation);
            return;
        }
        const activation = this.awaitingActivation;
        const isActivating = activation?.ws === ws && activation.generation === generation;
        if (!this.isConnected && !isActivating)
            return;
        if (message.byteStreamOperation) {
            if (!this.validRemoteByteStreamOperation(message.byteStreamOperation)) {
                this.closeGenerationWithError(ws, generation, 'badArg', 'Invalid byte stream ID');
                return;
            }
            this.handleByteStreamOperation(generation, message.byteStreamOperation);
            return;
        }
        if (!message.requestId) {
            this.closeGenerationWithError(ws, generation, 'badArg', 'Invalid request ID');
            return;
        }
        if (message.reply) {
            this.handleReply(ws, generation, message.requestId, message);
            return;
        }
        const bytes = frameBytes;
        const pressure = this.handlerPressureByGeneration.get(generation) ?? { requests: 0, bytes: 0 };
        if (pressure.requests >= this.maxInFlightServerRequests || pressure.bytes + bytes > this.maxInFlightServerRequestBytes) {
            this.closeGenerationWithError(ws, generation, 'badArg', 'Too many in-flight server requests');
            return;
        }
        pressure.requests++;
        pressure.bytes += bytes;
        this.handlerPressureByGeneration.set(generation, pressure);
        try {
            await this.handleServerRequest(generation, message.requestId, message);
        }
        finally {
            if (this.handlerPressureByGeneration.get(generation) === pressure) {
                pressure.requests = Math.max(0, pressure.requests - 1);
                pressure.bytes = Math.max(0, pressure.bytes - bytes);
                if (!pressure.requests)
                    this.handlerPressureByGeneration.delete(generation);
            }
        }
    }
    handleByteStreamOperation(generation, op) {
        if (op.destroy != null && SrpcByteStream_1.SrpcByteStream.consumeTerminalSender(this, op.streamId))
            return;
        if (op.write != null) {
            const state = this.getByteStreamPressureState(generation);
            const wasBackpressured = state.backpressured.has(op.streamId);
            const accepted = SrpcByteStream_1.SrpcByteStream.writeReceiver(this, op.streamId, op.write.chunk);
            this.updateByteStreamBufferedBytes(generation, op.streamId, SrpcByteStream_1.SrpcByteStream.getReceiverBufferedBytes(this, op.streamId));
            if (!accepted && wasBackpressured) {
                SrpcByteStream_1.SrpcByteStream.abortReceiver(this, op.streamId, new Error(`SRPC byte stream ${op.streamId} receiver is not draining`));
                this.clearByteStreamPressure(generation, op.streamId);
                return;
            }
            if (!accepted) {
                if (state.backpressured.size >= MaxBackpressuredByteStreams) {
                    SrpcByteStream_1.SrpcByteStream.abortReceiver(this, op.streamId, new Error('Too many backpressured SRPC byte streams'));
                    this.clearByteStreamPressure(generation, op.streamId);
                    return;
                }
                state.backpressured.add(op.streamId);
            }
            else if (wasBackpressured) {
                state.backpressured.delete(op.streamId);
            }
            if (this.totalByteStreamBufferedBytes(state) > this.maxBufferedBytes) {
                SrpcByteStream_1.SrpcByteStream.abortReceiver(this, op.streamId, new Error('Too many buffered SRPC byte stream bytes'));
                this.clearByteStreamPressure(generation, op.streamId);
            }
        }
        else if (op.finish != null) {
            this.getByteStreamPressureState(generation).backpressured.delete(op.streamId);
            SrpcByteStream_1.SrpcByteStream.finishReceiver(this, op.streamId);
        }
        else if (op.destroy != null) {
            this.clearByteStreamPressure(generation, op.streamId);
            SrpcByteStream_1.SrpcByteStream.destroySubstream(this, op.streamId, op.destroy.error);
        }
    }
    handleReply(ws, generation, requestId, message) {
        const queueItem = this.requestQueue.get(requestId);
        if (!queueItem) {
            if (this.isLateReply(requestId))
                return;
            this.closeGenerationWithError(ws, generation, 'badArg', 'Unknown request ID');
            return;
        }
        this.requestQueue.delete(requestId);
        this.requestBytes.delete(requestId);
        if (message.error !== undefined)
            queueItem.reject(new types_1.SrpcError(message.error, message.userError));
        else
            queueItem.resolve(message);
    }
    async handleServerRequest(generation, requestId, message) {
        for (const [key, handlerMeta] of this.streamMessageHandlers) {
            const requestData = message[key];
            if (requestData == null)
                continue;
            try {
                const result = await this.handlerRequestId.run({ requestId, generation }, () => handlerMeta.handler(requestData));
                this.writeMessage({
                    requestId,
                    reply: true,
                    [handlerMeta.resultType]: result
                }, generation);
            }
            catch (error) {
                this.writeMessage({
                    requestId,
                    reply: true,
                    ...(0, types_1.serializeSrpcError)(error)
                }, generation);
            }
            return;
        }
        this.writeMessage({ requestId, reply: true, error: 'Unhandled message type' }, generation);
    }
    generateWsUrl() {
        const authv = 2;
        const appv = '0.0.0';
        const ts = Date.now();
        const cid = this.clientId;
        const secret = this.clientSecret ?? (0, app_1.getCurrentApp)().config.SRPC_AUTH_SECRET;
        if (!secret)
            throw new Error('SRPC_AUTH_SECRET is not configured.');
        const baseUri = this.uri.startsWith('ws://') || this.uri.startsWith('wss://') ? this.uri : `ws://${this.uri}`;
        const url = new URL(baseUri);
        const nonce = (0, helpers_1.uuid7)();
        const features = normalizeFeatures(this.senderAnnouncements ? ['sender-announcements'] : []);
        const metadata = normalizeMetadata(this.clientMeta);
        const supersede = this.supersede ? '1' : '0';
        const audience = this.clientOptions?.authAudience ?? url.pathname;
        const signable = canonicalAuthV2({
            path: url.pathname,
            audience,
            appv,
            ts: String(ts),
            nonce,
            id: this.streamId,
            cid,
            protocol: '2',
            supersede,
            features,
            metadata
        });
        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: '2'
        });
        params.set('nonce', nonce);
        params.set('aud', audience);
        if (features)
            params.set('_f', features);
        if (this.supersede) {
            params.set('_supersede', '1');
            this.supersede = false;
        }
        for (const [key, value] of Object.entries(metadata))
            params.set(`m--${key}`, value);
        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, generation = this.generation) {
        let encoded;
        try {
            encoded = (0, types_1.encodeSrpcMessage)(this.clientMessage, message);
        }
        catch (error) {
            this.logger.error('Failed to encode SRPC message', error);
            return false;
        }
        return this.writeEncodedMessage(encoded, generation);
    }
    writeEncodedMessage(encoded, generation = this.generation) {
        const ws = this.ws;
        if (!ws || !this.isCurrent(ws, generation) || ws.readyState !== ws_1.default.OPEN)
            return false;
        if (encoded.byteLength > this.maxMessageBytes || (ws.bufferedAmount ?? 0) + encoded.byteLength > this.maxBufferedBytes) {
            this.closeGenerationWithError(ws, generation, 'badArg', 'sRPC stream outgoing buffer limit exceeded');
            return false;
        }
        try {
            ws.send(encoded);
            return true;
        }
        catch (error) {
            this.logger.warn('Failed to send SRPC message', error);
            this.closeGenerationWithError(ws, generation, 'badArg', 'Failed to send SRPC message');
            return false;
        }
    }
    writeMessageAsync(message, generation = this.generation) {
        const ws = this.ws;
        if (!ws || !this.isCurrent(ws, generation) || 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);
        if (encoded.byteLength > this.maxMessageBytes || (ws.bufferedAmount ?? 0) + encoded.byteLength > this.maxBufferedBytes) {
            this.closeGenerationWithError(ws, generation, 'badArg', 'sRPC stream outgoing buffer limit exceeded');
            return Promise.reject(new types_1.SrpcBackpressureError('sRPC stream outgoing buffer limit exceeded'));
        }
        return new Promise((resolve, reject) => {
            try {
                ws.send(encoded, error => {
                    if (error) {
                        this.closeGenerationWithError(ws, generation, 'badArg', 'Failed to send SRPC message');
                        reject(error);
                    }
                    else if (!this.isCurrent(ws, generation))
                        reject(new Error('Failed to send SRPC message: generation revoked'));
                    else
                        resolve();
                });
            }
            catch (error) {
                this.closeGenerationWithError(ws, generation, 'badArg', 'Failed to send SRPC message');
                reject(error instanceof Error ? error : new Error(String(error)));
            }
        });
    }
    currentByteStream = this.createByteStream(0);
    get byteStream() {
        return this.resolveByteStream();
    }
    createByteStream(generation) {
        return {
            parentStreamId: '',
            remoteSenderIdParity: 0,
            receiverBufferChanged: (streamId, bufferedBytes) => this.updateByteStreamBufferedBytes(generation, streamId, bufferedBytes),
            write: (streamId, chunk) => this.writeMessageAsync({ byteStreamOperation: { streamId, write: { chunk: new Uint8Array(chunk) } } }, generation),
            finish: streamId => this.writeMessage({ byteStreamOperation: { streamId, finish: {} } }, generation),
            destroy: (streamId, error) => this.writeMessage({ byteStreamOperation: { streamId, destroy: { error: (0, SrpcByteStream_1.byteStreamDestroyReason)(error) } } }, generation),
            announceSender: streamId => {
                const context = this.handlerRequestId.getStore();
                if (!this.senderAnnouncements || !context || context.generation !== generation)
                    return;
                this.writeMessage({ requestId: context.requestId, byteStreamOperation: { streamId, write: { chunk: new Uint8Array() } } }, generation);
            },
            attachDisconnectHandler: handler => {
                let handlers = this.byteStreamDisconnectHandlersByGeneration.get(generation);
                if (!handlers) {
                    handlers = new Set();
                    this.byteStreamDisconnectHandlersByGeneration.set(generation, handlers);
                }
                handlers.add(handler);
            },
            detachDisconnectHandler: handler => this.byteStreamDisconnectHandlersByGeneration.get(generation)?.delete(handler),
            getBufferedAmount: () => (this.generation === generation ? (this.ws?.bufferedAmount ?? 0) : 0)
        };
    }
    resolveByteStream() {
        const context = this.handlerRequestId.getStore();
        const generation = context?.generation ?? this.generation;
        const activation = this.awaitingActivation;
        const isCurrentActivation = activation?.generation === generation && this.isCurrent(activation.ws, generation);
        if (generation !== this.generation || (!this.isConnected && !isCurrentActivation)) {
            throw new Error('Cannot create an SRPC byte stream from a stale handler generation');
        }
        const transport = this.byteStreamsByGeneration.get(generation);
        if (!transport)
            throw new Error('SRPC handler transport is unavailable');
        return transport;
    }
    revokeByteStreamGeneration(generation) {
        const handlers = this.byteStreamDisconnectHandlersByGeneration?.get(generation);
        this.byteStreamDisconnectHandlersByGeneration?.delete(generation);
        if (handlers) {
            for (const handler of handlers) {
                try {
                    handler();
                }
                catch (error) {
                    this.logger.error('SRPC byte stream disconnect handler failed', error);
                }
            }
        }
        this.byteStreamsByGeneration?.delete(generation);
        this.byteStreamPressureByGeneration?.delete(generation);
    }
    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) {
        assertTimeout(timeoutMs);
        const requestType = `${prefix}Request`;
        const resultType = `${prefix}Response`;
        const requestId = (0, helpers_1.uuid7)();
        let encoded;
        try {
            encoded = (0, types_1.encodeSrpcMessage)(this.clientMessage, { requestId, [requestType]: data });
        }
        catch (error) {
            return Promise.reject(error);
        }
        const encodedBytes = encoded.byteLength;
        if (encodedBytes > this.maxMessageBytes)
            return Promise.reject(new types_1.SrpcBackpressureError('sRPC request exceeds message limit'));
        if (this.requestQueue.size >= this.maxPendingRequests || this.pendingRequestBytes + encodedBytes > this.maxPendingRequestBytes) {
            return Promise.reject(new types_1.SrpcBackpressureError('Too many pending SRPC requests'));
        }
        return new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                if (!this.requestQueue.delete(requestId))
                    return;
                this.requestBytes.delete(requestId);
                this.addLateReplyTombstone(requestId);
                reject(new types_1.SrpcIndeterminateDeliveryError(this.clientId, new Error(`Request timeout after ${timeoutMs}ms`)));
            }, timeoutMs);
            this.requestQueue.set(requestId, {
                exp: Date.now() + timeoutMs,
                resolve: response => {
                    clearTimeout(timeout);
                    this.requestBytes.delete(requestId);
                    const result = response[resultType];
                    if (result == null)
                        reject(new Error('Invalid response from server'));
                    else
                        resolve(result);
                },
                reject: error => {
                    clearTimeout(timeout);
                    this.requestBytes.delete(requestId);
                    reject(error);
                }
            });
            this.requestBytes.set(requestId, encodedBytes);
            const sent = this.writeEncodedMessage(encoded, this.generation);
            if (!sent) {
                this.requestQueue.delete(requestId);
                this.requestBytes.delete(requestId);
                clearTimeout(timeout);
                reject(new Error('Failed to send request: not connected'));
            }
        });
    }
    isCurrent(ws, generation) {
        return this.ws === ws && this.generation === generation;
    }
    rejectAllRequests(error) {
        for (const [requestId, queueItem] of this.requestQueue?.entries() ?? []) {
            this.addLateReplyTombstone(requestId);
            queueItem.reject(error);
        }
        this.requestQueue?.clear();
        this.requestBytes?.clear();
    }
    validRemoteByteStreamOperation(op) {
        const id = op.streamId;
        if (typeof id !== 'number' || !Number.isSafeInteger(id) || id <= 0 || id > MaxByteStreamId)
            return false;
        const hasWrite = op.write != null;
        const hasFinish = op.finish != null;
        const hasDestroy = op.destroy != null;
        const operationCount = Number(hasWrite) + Number(hasFinish) + Number(hasDestroy);
        if (operationCount !== 1)
            return false;
        if (hasWrite && (!isPlainObject(op.write) || !(op.write.chunk instanceof Uint8Array)))
            return false;
        if (hasFinish && !isPlainObject(op.finish))
            return false;
        if (hasDestroy && (!isPlainObject(op.destroy) || (op.destroy.error != null && typeof op.destroy.error !== 'string')))
            return false;
        if (hasDestroy)
            return id % 2 === 0 || SrpcByteStream_1.SrpcByteStream.hasSender(this, id) || SrpcByteStream_1.SrpcByteStream.hasTerminalSender(this, id);
        return id % 2 === 0;
    }
    getByteStreamPressureState(generation) {
        const existing = this.byteStreamPressureByGeneration.get(generation);
        if (existing)
            return existing;
        const state = { backpressured: new Set(), bufferedBytes: new Map() };
        this.byteStreamPressureByGeneration.set(generation, state);
        return state;
    }
    updateByteStreamBufferedBytes(generation, streamId, bufferedBytes) {
        if (generation !== this.generation)
            return;
        const state = this.getByteStreamPressureState(generation);
        const previous = state.bufferedBytes.get(streamId) ?? 0;
        if (bufferedBytes < previous)
            state.backpressured.delete(streamId);
        if (bufferedBytes > 0)
            state.bufferedBytes.set(streamId, bufferedBytes);
        else
            state.bufferedBytes.delete(streamId);
    }
    clearByteStreamPressure(generation, streamId) {
        const state = this.byteStreamPressureByGeneration.get(generation);
        state?.backpressured.delete(streamId);
        state?.bufferedBytes.delete(streamId);
    }
    totalByteStreamBufferedBytes(state) {
        let total = 0;
        for (const bytes of state.bufferedBytes.values())
            total += bytes;
        return total;
    }
    addLateReplyTombstone(requestId) {
        const now = Date.now();
        this.pruneLateReplyTombstones(now);
        this.lateReplyTombstones.set(requestId, now + this.lateReplyTombstoneTtlMs);
        while (this.lateReplyTombstones.size > MaxLateReplyTombstones) {
            const oldestRequestId = this.lateReplyTombstones.keys().next().value;
            if (oldestRequestId === undefined)
                break;
            this.lateReplyTombstones.delete(oldestRequestId);
        }
    }
    isLateReply(requestId) {
        const now = Date.now();
        this.pruneLateReplyTombstones(now);
        return (this.lateReplyTombstones.get(requestId) ?? 0) > now;
    }
    pruneLateReplyTombstones(now) {
        for (const [requestId, expiresAt] of this.lateReplyTombstones) {
            if (expiresAt <= now)
                this.lateReplyTombstones.delete(requestId);
        }
    }
    get lateReplyTombstoneTtlMs() {
        return configuredPositiveInteger(this.clientOptions?.lateReplyTombstoneTtlMs, DefaultLateReplyTombstoneTtlMs);
    }
    get pendingRequestBytes() {
        let total = 0;
        for (const bytes of this.requestBytes.values())
            total += bytes;
        return total;
    }
    get maxPendingRequests() {
        return configuredPositiveInteger(this.clientOptions?.maxPendingRequests, DefaultMaxRequests);
    }
    get maxPendingRequestBytes() {
        return configuredPositiveInteger(this.clientOptions?.maxPendingRequestBytes, DefaultMaxBytes);
    }
    get maxInFlightServerRequests() {
        return configuredPositiveInteger(this.clientOptions?.maxInFlightServerRequests, DefaultMaxRequests);
    }
    get maxInFlightServerRequestBytes() {
        return configuredPositiveInteger(this.clientOptions?.maxInFlightServerRequestBytes, DefaultMaxBytes);
    }
    get maxBufferedBytes() {
        return configuredPositiveInteger(this.clientOptions?.maxBufferedBytes, DefaultMaxBytes);
    }
    get maxMessageBytes() {
        return configuredPositiveInteger(this.clientOptions?.maxMessageBytes, DefaultMaxMessageBytes);
    }
    get connectTimeoutMs() {
        const value = this.clientOptions?.connectTimeoutMs;
        return value != null && Number.isSafeInteger(value) && value > 0 && value <= helpers_1.MAX_SAFE_TIMER_MS ? value : DefaultConnectTimeoutMs;
    }
    static __tsfType = _SrpcClientMetadata_1 = _a.decodeCompactMetadataV1("[1,{\"kind\":16,\"name\":\"SrpcClient\",\"typeName\":\"SrpcClient\",\"classType\":{\"$tsf\":0},\"properties\":[{\"name\":\"isConnected\",\"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\":\"resolveByteStream\",\"parameters\":[],\"returnType\":{\"$tsfType\":4}},{\"name\":\"registerConnectionHandler\",\"parameters\":[{\"name\":\"handler\",\"type\":{\"$tsfType\":5},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":3}},{\"name\":\"registerMessageHandler\",\"parameters\":[{\"name\":\"prefix\",\"type\":{\"$tsfType\":6},\"optional\":false,\"default\":false},{\"name\":\"handler\",\"type\":{\"$tsfType\":7},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":3}},{\"name\":\"registerDisconnectHandler\",\"parameters\":[{\"name\":\"handler\",\"type\":{\"$tsfType\":8},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":3}},{\"name\":\"invoke\",\"parameters\":[{\"name\":\"prefix\",\"type\":{\"$tsfType\":6},\"optional\":false,\"default\":false},{\"name\":\"data\",\"type\":{\"$tsfType\":9},\"optional\":false,\"default\":false},{\"name\":\"timeoutMs\",\"type\":{\"$tsfType\":0},\"optional\":true,\"default\":true}],\"returnType\":{\"$tsfType\":10}}],\"hasConstructor\":true,\"constructorParameters\":[{\"name\":\"logger\",\"type\":{\"$tsfType\":11},\"optional\":false,\"default\":false},{\"name\":\"uri\",\"type\":{\"$tsfType\":12},\"optional\":false,\"default\":false},{\"name\":\"clientMessage\",\"type\":{\"$tsfType\":13},\"optional\":false,\"default\":false},{\"name\":\"serverMessage\",\"type\":{\"$tsfType\":13},\"optional\":false,\"default\":false},{\"name\":\"clientId\",\"type\":{\"$tsfType\":12},\"optional\":false,\"default\":false},{\"name\":\"clientMeta\",\"type\":{\"$tsfType\":14},\"optional\":true,\"default\":false},{\"name\":\"clientSecret\",\"type\":{\"$tsfType\":12},\"optional\":true,\"default\":false},{\"name\":\"clientOptions\",\"type\":{\"$tsfType\":15},\"optional\":true,\"default\":false}]}]", [() => SrpcClient], __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 closeCodeForCause(cause) {
    return { disconnect: 1000, badArg: 4000, conflict: 4001, supersede: 4002, timeout: 4003 }[cause];
}
function toBuffer(data) {
    if (Buffer.isBuffer(data))
        return data;
    if (Array.isArray(data))
        return Buffer.concat(data);
    return Buffer.from(data);
}
function configuredPositiveInteger(value, fallback) {
    return value != null && Number.isSafeInteger(value) && value > 0 ? value : fallback;
}
function validateClientResourceOptions(options) {
    if (!options)
        return;
    for (const [name, value] of [
        ['maxPendingRequests', options.maxPendingRequests],
        ['maxPendingRequestBytes', options.maxPendingRequestBytes],
        ['maxInFlightServerRequests', options.maxInFlightServerRequests],
        ['maxInFlightServerRequestBytes', options.maxInFlightServerRequestBytes],
        ['maxBufferedBytes', options.maxBufferedBytes],
        ['maxMessageBytes', options.maxMessageBytes]
    ]) {
        if (value !== undefined && (!Number.isSafeInteger(value) || value < 1)) {
            throw new Error(`sRPC client ${name} must be a positive integer`);
        }
    }
    const timer = options.lateReplyTombstoneTtlMs;
    if (timer !== undefined && (!Number.isSafeInteger(timer) || timer < 1 || timer > helpers_1.MAX_SAFE_TIMER_MS)) {
        throw new Error(`sRPC client lateReplyTombstoneTtlMs must be a safe positive integer between 1 and ${helpers_1.MAX_SAFE_TIMER_MS}`);
    }
    const connectTimeout = options.connectTimeoutMs;
    if (connectTimeout !== undefined && (!Number.isSafeInteger(connectTimeout) || connectTimeout < 1 || connectTimeout > helpers_1.MAX_SAFE_TIMER_MS)) {
        throw new Error(`sRPC client connectTimeoutMs must be a safe positive integer between 1 and ${helpers_1.MAX_SAFE_TIMER_MS}`);
    }
}
function assertTimeout(value) {
    (0, helpers_1.assertSafeTimerMs)(value, 'Request timeout');
}
function normalizeFeatures(features) {
    return [...new Set(features)].sort().join(',');
}
function normalizeMetadata(meta) {
    return Object.fromEntries(Object.entries(meta ?? {})
        .map(([key, value]) => [key, String(value)])
        .sort(([a], [b]) => compareCodeUnits(a, b)));
}
function canonicalAuthV2(fields) {
    return JSON.stringify({ version: 2, ...fields });
}
function compareCodeUnits(a, b) {
    return a < b ? -1 : a > b ? 1 : 0;
}
function isPlainObject(value) {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
}
exports.__tsfTypeAliases = JSON.parse("[1,{\"SrpcClientOptions\":{\"kind\":18,\"typeName\":\"SrpcClientOptions\",\"types\":[{\"kind\":20,\"name\":\"enableReconnect\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"authAudience\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"senderAnnouncements\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"maxPendingRequests\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"maxPendingRequestBytes\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"maxInFlightServerRequests\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"maxInFlightServerRequestBytes\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"maxBufferedBytes\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"maxMessageBytes\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"connectTimeoutMs\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"lateReplyTombstoneTtlMs\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true}]}}]")[1];
//# sourceMappingURL=SrpcClient.js.map