import { Channel, PrivateChannel } from './channel.js'; import { ChunkingManager } from './chunking.js'; import { RPCService } from './service.js'; import type { Msg, MsgHdrs, NatsConnection, Status } from '@nats-io/nats-core'; import type { OrphanProbeOptions, Promisify, PullCallbackCallOptions, RPCAuthOptions, RPCCallTiming, RPCClient as RPCClientImpl, RPCClientOptions, ProxyCallOptions, ProxyTimingReporter } from './types.js'; export declare function createRPCClient(options: RPCClientOptions): RPCClientImpl; /** A proxy call's method, checked against rpc..__methods after its first no-responder status. */ interface MethodProbe { probeNamespace?: string; probeMethod?: string; onMethods?: (methods: string[]) => void; } export declare class RPCClient implements RPCClientImpl { options: RPCClientOptions; readonly service: RPCService; chunkingManager: ChunkingManager; /** Teardowns of every handler session this client serves. */ private sessionTeardowns; /** The handler registrations served on this connection. */ private responders; private nc?; /** The connection suspend(), disconnect() or abortClose() closes: its close * event is not a connection loss. */ private closingNc?; private subscriptionSeq; private subscriptionEntries; private pullIteratorSettles; /** First dot-separated segment of every id this client generates. Equals * the connId when one is configured (browser clients: the firewall * allowlists `rpc.reply..>`), otherwise a local random prefix. * All reply subjects derived from those ids therefore fall under one * wildcard: `rpc.reply..>` — the muxed reply inbox. */ private readonly replyPrefix; /** The single persistent reply-mux subscription entry (wildcard * `rpc.reply..>`). Lives in subscriptionEntries so * suspend()/connect() restore it like any other subscription. */ private muxEntry?; /** Chunked replies on the mux, separate from handler traffic so foreign * headers on handler subjects cannot use up the reply transfers. */ private muxChunking; /** Bytes the buffers of all chunked transfers into this client may hold * together, shared by every manager. */ private chunkBudget; /** Chunk scopes of the subscriptions, so the transfer cap applies per * subscription instead of per client. */ private chunkScopes; private chunkScopeSeq; /** Chunk managers of the service endpoints on this connection, one per endpoint. */ private endpointChunking; /** 503/no-responder handlers keyed by reply-subject suffix (the part after * `rpc.reply.`). Used by pull-iterator/stream paths whose per-message * `reply` subject only ever carries no-responder statuses. One-shot: * the mux dispatcher removes an entry when it fires (max:1 semantics). */ private statusHandlers; private _maxPayloadSize; /** The payload limit of the connection while it is below MIN_PAYLOAD_LIMIT: nothing is chunked then. */ private unchunkableLimit?; private connectionPromise?; private closed; private isolatedClients; /** Replies answered inline during the current socket read, reset by a microtask. */ private syncReplies; /** Set while nats.js reconnects: requests wait here in order instead of * going into an outbound buffer the next dial attempt discards. */ private linkWaiters?; private pendingRequests; private streamHandlers; /** * Check connection status */ get isConnected(): boolean; /** * True when the connection is stale by wall-clock (heartbeat deadline passed * while the timer was throttled/frozen in the background). Detects a dead * socket on resume without waiting for the ping timeout to fire. */ get isStale(): boolean; get isClosed(): boolean; /** * Get the maximum payload size */ get maxPayloadSize(): number; /** * Access the underlying NATS connection status events. * Yields events like 'reconnect', 'disconnect', 'reconnecting'. */ status(): AsyncIterable | undefined; /** * Active liveness probe via NATS PING/PONG round-trip. Resolves when a PONG * arrives, rejects on timeout. Caller treats timeout as "connection is dead" * — the underlying socket may still report OPEN in that case (silent-dead * TCP). Use a tight timeout (a few seconds) so a stale connection is * detected promptly. */ flush(timeoutMs?: number): Promise; constructor(options: RPCClientOptions); /** * Create a new isolated RPC client * @param options - Options for the isolated client */ createIsolatedClient(options: RPCClientOptions): RPCClient; /** * Connect to NATS server. Accepts an AbortSignal so callers can cancel * an in-flight handshake when another candidate has won. * The underlying nats-js connect() does not itself accept a signal, * so we wrap it: on abort we synchronously close any late-arriving connection * via the fork's abortClose() and reject with AbortError. */ connect(options?: { signal?: AbortSignal; }): Promise; /** * Disconnect from NATS server * Cleans up resources without rejecting pending operations */ disconnect(): Promise; /** * Update connection options between suspend() and connect(). Used for token * rotation / endpoint switching: subscription metadata is preserved by * suspend(), reconfigure() points the next connect() at the new server, and * connect() re-subscribes everything on the fresh transport. */ reconfigure(overrides: { servers?: string[]; auth?: RPCAuthOptions; }): void; /** * Hot-swap the server pool without dropping the live connection. * Use this when only the URL needs to change but the existing connection * is still authenticated and serving traffic. The new pool kicks in on the next auto-reconnect. * * For a forced switch (e.g. endpoint host changed), use suspend() + * reconfigure() + connect() instead. * * Limitation: has no effect on a connect() that is still dialing its FIRST * connection (waitOnFirstConnect) — the dial loop captured the server list * at connect() time. Tear the client down (abortClose) and build a fresh * one to redirect an initial dial. */ setServers(servers: string[]): void; /** * Force the current dial loop (if any) to immediately retry with the latest * server pool — including from a stuck mid-handshake or sleeping-on-delay * state. Falls back to standard `reconnect()` on transports that don't ship * the fork's `forceReconnect()` (e.g. server-side TCP via npm `@nats-io/nats-core`). */ forceReconnect(): Promise; /** * Synchronous force-tear-down without awaiting the transport's close * handshake. Use during network changes when the WS is half-open and a * normal `close()` would hang for 2s+ on the dead socket. Falls back to * fire-and-forget `close()` on transports without the fork patch. */ abortClose(err?: Error): void; /** * Suspend the connection without clearing subscription metadata. * After calling suspend(), connect() will restore previous subscriptions, * also those of the isolated connections, which suspend and reconnect with it. */ suspend(): Promise; /** * Public publish method */ publish(subject: string, data: TMessage, opts?: { headers?: MsgHdrs; reply?: string; }): Promise; /** * Publish without a promise when the encoded payload fits one message. A * payload above maxPayloadSize is chunked from the bytes already encoded: * returns undefined once the message is queued, otherwise the promise of * the chunked publish. Throws synchronously when not connected or when the * data cannot be encoded. Wire format is identical to publish(). * * With `zeroCopy` a large frame is written without copying its binaries * (see publishFrameZeroCopy); only call requests pass it. * * Internal: the handler reply paths use it, so an oversized result is * encoded once. Not part of the public RPCClient interface. */ publishOrChunk(subject: string, data: TMessage, opts?: { headers?: MsgHdrs; reply?: string; }, zeroCopy?: boolean): Promise | undefined; /** * Synchronous publish fast path for small payloads. * * Encodes and publishes in one synchronous step — no promise allocation, * no microtask hop. Returns false when the encoded payload exceeds * maxPayloadSize and needs chunking; the caller must then fall back to * the async publish(). Wire format is identical to publish(). * * Internal: used by hot oneway paths (callback proxy, pull-iterator * responses). Not part of the public RPCClient interface. */ tryPublishSync(subject: string, data: TMessage, opts?: { headers?: MsgHdrs; reply?: string; }): boolean; /** * Chunk reassembly for one service endpoint: a transfer cap of its own on * the budget of this client, like a subscription. suspend() and close drop * its transfers. * * Internal: RPCService uses it. Not part of the public RPCClient interface. */ createEndpointChunking(): ChunkingManager; /** * Drop the transfers of a manager from createEndpointChunking() once its * endpoint stops. * * Internal: RPCService uses it. Not part of the public RPCClient interface. */ releaseEndpointChunking(manager: ChunkingManager): void; /** * Register the teardown of a handler session, so disconnect(), suspend() * and a lost connection end it too. Returns the function that removes it. * * Internal: the session handlers use it. Not part of the public RPCClient interface. */ trackSession(teardown: () => Promise): () => void; /** * A reply subject on this client's reply inbox. When the server answers a * publish that carries it with a no-responder status, `onNoResponders` runs * once. `forget` drops it unused. Connection loss, suspend() and * disconnect() drop it too. * * Internal: the session probes use it. Not part of the public RPCClient interface. */ statusReply(onNoResponders: () => void): { reply: string; forget: () => void; }; /** * Subscribe with a handler that gets the NATS message as it arrived, with * subject, reply and headers and the payload not decoded. No chunk * reassembly and no dispatcher. connect() restores it like every * subscription; with `restore` false it ends with the connection, like a * service endpoint. * * Internal: onRequest() and services use it. Not part of the public RPCClient interface. */ subscribeMessages(pattern: string, handler: (msg: Msg) => void, restore?: boolean): () => void; /** * Public subscribe method */ subscribe(pattern: string, handler: (data: TResponse) => void | Promise, options?: { queue?: string; }): Promise<() => void>; /** * Internal subscribe that also exposes the SubscriptionEntry, so callers * (e.g. the pull-callback iterator) can flush the per-subscription handler * chain deterministically. */ private subscribeEntry; /** subscribeEntry without the promise, so a registration queues all its SUBs in one pass. */ private addSubscription; /** * Create the NATS subscription for an entry. Called from subscribe() and * again from connect() when restoring entries after a suspend cycle. */ private natsSubscribe; /** * Muxed reply inbox: register (and by default subscribe) the single * persistent wildcard subscription `rpc.reply..>` that * receives every RPC reply of this client — real responses, chunked * responses and 503/no-responder statuses. Idempotent. * * connect() passes subscribeNow=false: it only registers the entry and * lets its restore loop create the actual subscription alongside all * other entries (also after suspend cycles). */ private ensureMuxSubscription; /** * Dispatcher for the muxed reply inbox. Routes by message kind: * - 503/no-responder status (empty payload + code 503): the call/iterator * is identified by the SUBJECT (`rpc.reply.`) — the server echoes * the request's reply subject, there is no payload to decode. * - chunked transfer header/chunk: reassemble via muxChunking while the * call is pending, then route the assembled response by envelope id. A * reply that cannot be assembled rejects the call at once. * - regular response: decode and route by envelope id. */ private handleMuxMessage; /** * Settle the pending request a (possibly reassembled) RPC response belongs * to. Unknown ids are dropped silently — late replies after a timeout, or * traffic of another client sharing the same connId prefix. */ private routeMuxResponse; /** * Reject a call whose chunked reply cannot be assembled (refused header, * bad chunk, stalled or undecodable transfer) right away instead of at its * timeout. */ private failChunkedReply; /** * Force-settle every client-side pull iterator parked in next(). Runs on * disconnect/suspend/abortClose so consumers' `for await` loops terminate * with a connection error instead of hanging forever. */ private settlePullIterators; /** * The synchronous part of every session end (suspend, disconnect, * abortClose, connection closed for good): handler sessions are torn down, * pending calls reject with CONNECTION_CLOSED, client streams end, pull * iterators settle, entries lose their subscription but stay, and every * chunk manager drops its transfers. Returns the session cleanups still * running. A per-call reply entry of the service path is dropped by its * pending cleanup, so it is not restored as a dead subscription. * * With `closedRejects` the rejections of pending calls are collected there * instead, for rejectAfterQueuedWrites: a zero-copy request can still sit * in the socket queue. */ private dropConnectionState; /** * Run the rejections dropConnectionState collected once the socket is done * with the requests queued so far: when the server answers a PING sent * behind them, at the latest when the returned function runs after the * close. It does not wait for the session teardowns, which may wait for * these calls. */ private rejectAfterQueuedWrites; /** * Start the teardown of every session this client serves and forget them. * Each one stops publishing right here, before any await. */ private endHandlerSessions; /** * The drain of disconnect() and suspend(): every registration of this * client and of its isolated clients at once. Sessions end right away; * their teardowns are returned, not awaited, since one may wait for a * call that only the close rejects. */ private drainAll; /** * Stop the intake of these registrations and give the requests they * received until drainTimeoutMs to finish. The subscriptions are drained: * the UNSUB reaches the server first, requests already on their way are * still dispatched. Requests unanswered at the deadline get one * CONNECTION_CLOSED reply and do not start any more. */ private drainResponders; private drainResponder; /** Answer a request that will not run with CONNECTION_CLOSED, a stream request on its stream subject. */ private replyClosed; /** * Answer a request for a method a namespace subscription does not have with * METHOD_NOT_FOUND and the method list, a stream request on its stream subject. */ private replyMissingMethod; private drainTimeoutMs; /** * The current connection closed for good without suspend(), disconnect() * or abortClose(): tear down at once, the closed flag stays false, so the * next call or connect() dials again and restores every entry. */ private onConnectionLost; /** * Follow the link state of a connection: a 'disconnect' or * 'staleConnection' status holds new requests, 'reconnect' releases them. * nats.js keeps a publish made while the link is down in a buffer that its * next dial attempt discards. */ private watchLink; /** * nats-server refused a publish of this connection to `subject`: the calls * and stream starts waiting there would only run into their timeout, so * they fail with FORBIDDEN now. Nothing retries a refused subject. */ private failRefusedPublish; /** * Undefined while the link is up. Otherwise waits while nats.js * reconnects, bounded by the call timeout of the client, and rejects with * TIMEOUT when the link stays down that long. */ private holdForLink; /** Let the held requests go, in the order they arrived. */ private releaseLinkWaiters; /** * Undefined while the link is up. Otherwise resolves true once nats.js has * reconnected or the connection ended, false when `ms` passed first. */ private waitForLink; /** * One PING/PONG round trip before close: the Node TCP transport destroys * the socket on close() and drops whatever is still queued for writing. * Bounded to 1 s (or disconnectTimeout when lower), a dead socket only * delays the close. */ private flushBeforeClose; /** * One PING/PONG round trip after SUBs were queued: once the server answered, * it routes to them, also calls from other connections. Bounded and never * rejects, a slow or lost connection only delays the caller. */ private awaitSubscriptions; /** * Ensure the connection is truly closed. With waitOnFirstConnect: false the * underlying WebSocket transport may still be mid-handshake when close() is * called. nc.closed() resolves once the NATS protocol is fully shut down, * preventing zombie connections that survive a disconnect/reconnect cycle. */ private waitClosed; /** * Encode `data` for publishing. A frame with out-of-band binaries of at * least PUBLISH_FRAME_MIN_SIZE that fits one message and carries no headers * is published right here as a single buffer (see publishFrame), with * `zeroCopy` without copying its binaries where possible; then nothing is * returned. */ private encodeOrPublishFrame; /** Publish the request of a call, without copying large binaries when zeroCopyBinaries is set. */ private publishRequest; private publishChunks; private noteSyncReply; private disposeChunking; private newChunkingManager; private chunkManager; private joinChunkScope; private leaveChunkScope; /** * Native NATS request/reply * @param subject - The subject to send the request to * @param data - The request data * @param options - Request options including timeout and per-call retry override; `sender` * (internal, for channels) marks the request with the channel end that sends it */ request(subject: string, data: TRequest, options?: { timeout?: number; headers?: MsgHdrs; noResponderRetry?: { maxRetries?: number; delays?: number[]; }; sender?: string; }): Promise; private _requestOnce; /** * Retry helper for no-responder errors this client observed itself (a 503 * status for its own request). An error reply from a responder is never * retried, whatever its code. The per-call `override` lets a request that * targets a known-flaky responder (e.g. a child process that may be * restarting) extend the wait window without affecting the client-wide * default. */ private withNoResponderRetry; /** * Retries and delays for a no-responder retry, from the call's override * field by field, else the client option. A caller that sets neither gets * DEFAULT_RETRY_DELAYS with one retry per delay, one that sets only one of * them gets 3 retries or the delays 500, 1000 and 2000 ms for the other. */ private retryPlan; /** * Ask rpc..__methods for the method of a proxy call, started * with the first retry delay; undefined without a probe. */ private probeMethod; /** * Sleep one retry delay, or until `signal` aborts. With the answer of a * method probe pending or in, true as soon as it says the namespace is * served without the method, so the caller fails at once. Anything else * waits out the delay. */ private retryWait; /** * One call to rpc..__methods without retry. True when the * namespace answers and does not have `method`. A namespace without * responders, a responder without __methods (older libs), an error or no * answer within `timeoutMs` give false. */ private isMethodMissing; /** * Make an RPC call */ call(subject: string, ...args: any[]): Promise; /** * Make an RPC call and additionally return the response's __methods * metadata as a side channel. The result object is passed through * untouched — __methods is never spliced into it. Used by createProxy for * method discovery; plain call() discards the metadata. * * With `discover: true` the request envelope carries `__discover: true`, * asking the responder to attach its method list to the response. Proxies * request this only while their method cache is empty. * * With `probeNamespace` and `probeMethod` the first no-responder status is * checked against rpc..__methods while the first retry delay * runs: when the namespace is served but the method is missing, the * no-responder error is thrown at once instead of after every retry. The * method list that comes back goes to `onMethods`. * * Internal: not part of the public RPCClient interface. */ callWithMeta(subject: string, args?: any[], opts?: { discover?: boolean; timing?: boolean; probeNamespace?: string; probeMethod?: string; onMethods?: (methods: string[]) => void; } & ProxyCallOptions): Promise<{ result: TResponse; methods?: string[]; timing?: RPCCallTiming; }>; /** * Make an RPC call (single attempt) */ private _callOnce; /** * A plain call to a service endpoint. A request too large for one message * goes to `direct`, the subject of the one instance the proxy picked, when * the service has one: a queue group would hand its chunks to different * instances. * * Internal: the service proxy uses it. Not part of the public RPCClient interface. */ callService(subject: string, args: any[], direct?: string): Promise; /** * Legacy single-attempt call for service subjects (reply pattern * `.reply.`): per-call reply subscription + one-shot * no-responder inbox. The rpc.* path is muxed — see _callOnce. */ private _callOnceService; /** * Make a streaming RPC call */ callStream(subject: string, ...args: any[]): AsyncGenerator; /** * callStream with the method probe of callWithMeta: when the first * no-responder status comes from a namespace that is served but does not * have the method, the stream fails at once instead of after every retry. * * Internal: createProxy uses it. Not part of the public RPCClient interface. */ callStreamWithProbe(subject: string, args: any[], probe: MethodProbe): AsyncGenerator; /** * The stream of callStream(): a no-responder status before the first item * starts the stream again after the retry delay. A hand-written iterator, * so return() ends a stream parked between items or in a retry wait at * once instead of after its next step. */ private streamWithRetry; /** * Make a streaming RPC call (single attempt). * * Manual iterator — see callPullIteratorWithCallback for rationale. * The push-based stream still parks the client at `await resolver` * while waiting for the next stream message; if the server stops * sending without an `end` frame, the generator would hang on * iter.return(). Force-settling the pending resolver lets return() * run cleanup cleanly. */ private _callStreamOnce; /** * Make a pull-based iterator RPC call */ callPullIterator(subject: string, ...args: any[]): AsyncGenerator; /** * Pull-iterator-with-callbacks call. * * Combines a client-driven pull iterator (1 RTT per batch) with a oneway * callback channel (fire-and-forget server → client) for low-latency * frame-level data delivery with coarse-grained backpressure. * * The returned async generator yields `undefined` for each batch boundary * the server produces. Meaningful data is dispatched through the provided * callback object. return() ends it without waiting for callbacks, so a * callback may call it: one that runs finishes, the queued ones are dropped. * * In place of the plain onewayMethods array, a PullCallbackCallOptions * object can be passed (direct client API only — the proxy path always * passes the array form). `prefetch: true` sends the next `next` request * as soon as a batch boundary arrives, before the batch's callbacks are * drained — hides one RTT per batch at the cost of strict backpressure. */ callPullIteratorWithCallback(subject: string, callbacks: Record any>, onewayMethods: string[] | PullCallbackCallOptions, ...args: any[]): AsyncGenerator; /** * Make an RPC call with a callback subscription. * Returns an unsubscribe function. */ callWithCallback(subject: string, args: any[], callback: (value: TResponse) => void | Promise): Promise<() => void>; /** * Register RPC handlers * * Calls of one method run one after another by default. `concurrency` lets * chosen methods run up to the given number of calls at once, keyed by * method name (`{ detect: 4 }`); values below 2 keep the serial default. * Calls still start in arrival order, and stream, pull-iterator and * callback setups of such a method stay serial. * * `namespaceSubscription: true` serves the namespace with one subscription * on rpc..> instead of one per method, which saves setup time and * server interest on namespaces with many methods. Each method keeps its * own lane. A call for a method the registration does not have gets * METHOD_NOT_FOUND at once instead of no responders, and every subject below * the namespace reaches this registration, also one another process serves. * A namespace whose first segment is `reply` or `cb` cannot take it. * * `dropExpired` (true for every method, or a list of method names) skips a * request without an answer once it waited in the method's queue longer * than its caller waits for it (__ttl of the request plus 5 ms). Only for * reads whose answer is worthless once the caller gave up, never for a * method whose side effect must happen. * * Stream, pull-iterator and pull-callback producers end with their session * and can watch sessionSignal() to stop while they wait. A session whose * consumer is gone ends after the grace of `orphanProbe` (client option by * default). On the returned cleanup, disconnect() and suspend() the * registration first stops taking requests, then the calls it already * received get drainTimeoutMs to finish; unanswered ones get * CONNECTION_CLOSED. A plain call that runs when the connection is lost * finishes, its reply is lost. */ registerHandler(namespace: string, handlers: object, options?: { isolatedConnection?: boolean; withoutDecorators?: boolean; queue?: string; concurrency?: Record; orphanProbe?: OrphanProbeOptions | false; namespaceSubscription?: boolean; dropExpired?: boolean | string[]; }): Promise<() => Promise>; /** * Setup a request handler (responder). connect() restores it after suspend(). * @param pattern - The subject pattern to listen for requests * @param handler - The handler function that receives data and subject * @param options - `ignoreSender` (internal, for channels) leaves the * requests a request() with this sender sent to the other subscribers */ onRequest(pattern: string, handler: (data: TRequest) => Promise | TResponse, options?: { ignoreSender?: string; }): Promise<() => void>; /** * Create or join a bidirectional channel * @param channelId - Unique channel identifier * @param options - Optional configuration */ channel(channelId: string, options?: { isolatedConnection?: boolean; }): Promise; /** * Create a private 1:1 channel * @param channelId - Unique channel identifier * @param targetClientId - Target client to connect to * @param options - Optional configuration */ privateChannel(channelId: string, targetClientId: string, options?: { isolatedConnection?: boolean; }): Promise; private isolatedChannelClient; /** Set up a channel; an isolated client of its own goes away with it, also when the setup fails. */ private openChannel; /** Drop an isolated client from the list suspend(), connect() and disconnect() go through. */ private forgetIsolated; /** * Create proxy for type-safe RPC calls * @param namespace - The namespace for RPC calls * @param options - Optional configuration. `noResponderRetry` and `timeout` * apply to every plain call of this proxy and its nested proxies. */ createProxy(namespace: string): Promisify; createProxy(namespace: string, options: { isolatedConnection?: false; onTiming?: ProxyTimingReporter; } & ProxyCallOptions): Promisify; createProxy(namespace: string, options: { isolatedConnection: true; onTiming?: ProxyTimingReporter; } & ProxyCallOptions): { proxy: Promisify; close: () => Promise; }; /** * Create a service client proxy with automatic service discovery */ createServiceProxy(serviceName: string): Promise>; createServiceProxy(serviceName: string, options: { preferredId?: string; timeout?: number; isolatedConnection?: false; }): Promise>; createServiceProxy(serviceName: string, options: { preferredId?: string; timeout?: number; isolatedConnection?: true; }): Promise<{ proxy: Promisify; close: () => Promise; }>; } export {};