@amqp-contract/client 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -133,19 +133,24 @@ var TypedAmqpClient = class TypedAmqpClient {
133
133
  /**
134
134
  * Demultiplex an RPC reply by `correlationId`, validate the body against the
135
135
  * call's response schema, and resolve the awaiting caller. Replies with no
136
- * matching pending call (e.g. arriving after the call timed out) are dropped
137
- * with a debug log.
136
+ * matching pending call (the call already timed out, was cancelled, or the
137
+ * correlationId is unknown) are logged at warn — a non-zero rate of these
138
+ * usually indicates a tuning problem (handler latency exceeds caller
139
+ * timeout). The `messaging.rpc.late_reply` counter lets dashboards alert on
140
+ * sustained drift without parsing logs.
138
141
  */
139
142
  handleRpcReply(msg) {
140
143
  if (!msg) return;
141
144
  const correlationId = msg.properties.correlationId;
142
145
  if (typeof correlationId !== "string") {
143
146
  this.logger?.warn("Received RPC reply without correlationId; dropping", { deliveryTag: msg.fields.deliveryTag });
147
+ (0, _amqp_contract_core.recordLateRpcReply)(this.telemetry, "missing-correlation-id");
144
148
  return;
145
149
  }
146
150
  const pending = this.pendingCalls.get(correlationId);
147
151
  if (!pending) {
148
- this.logger?.debug("Received RPC reply for unknown correlationId", { correlationId });
152
+ this.logger?.warn("Received RPC reply for unknown correlationId (caller already timed out or cancelled)", { correlationId });
153
+ (0, _amqp_contract_core.recordLateRpcReply)(this.telemetry, "unknown-correlation-id");
149
154
  return;
150
155
  }
151
156
  this.pendingCalls.delete(correlationId);
package/dist/index.d.cts CHANGED
@@ -141,11 +141,11 @@ type CreateClientOptions<TContract extends ContractDefinition> = {
141
141
  defaultPublishOptions?: PublishOptions | undefined;
142
142
  /**
143
143
  * Maximum time in ms to wait for the AMQP connection to become ready before
144
- * `create()` resolves to `Result.Error<TechnicalError>`. Without this option,
145
- * `create()` waits forever the underlying amqp-connection-manager retries
146
- * indefinitely.
144
+ * `create()` resolves to `Result.Error<TechnicalError>`. Defaults to 30s
145
+ * (the {@link AmqpClient}'s `DEFAULT_CONNECT_TIMEOUT_MS`). Pass `null` to
146
+ * disable the timeout and let amqp-connection-manager retry indefinitely.
147
147
  */
148
- connectTimeoutMs?: number | undefined;
148
+ connectTimeoutMs?: number | null | undefined;
149
149
  };
150
150
  /**
151
151
  * Per-call options for `client.call()`.
@@ -213,8 +213,11 @@ declare class TypedAmqpClient<TContract extends ContractDefinition> {
213
213
  /**
214
214
  * Demultiplex an RPC reply by `correlationId`, validate the body against the
215
215
  * call's response schema, and resolve the awaiting caller. Replies with no
216
- * matching pending call (e.g. arriving after the call timed out) are dropped
217
- * with a debug log.
216
+ * matching pending call (the call already timed out, was cancelled, or the
217
+ * correlationId is unknown) are logged at warn — a non-zero rate of these
218
+ * usually indicates a tuning problem (handler latency exceeds caller
219
+ * timeout). The `messaging.rpc.late_reply` counter lets dashboards alert on
220
+ * sustained drift without parsing logs.
218
221
  */
219
222
  private handleRpcReply;
220
223
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":["amqp","EventEmitter","TcpSocketConnectOpts","ConnectionOptions","ChannelWrapper","CreateChannelOpts","ConnectionUrl","Options","Connect","AmqpConnectionOptions","url","connectionOptions","ConnectListener","Connection","connection","arg","ConnectFailedListener","Error","err","Buffer","noDelay","timeout","keepAlive","keepAliveDelay","clientProperties","credentials","mechanism","username","password","response","AmqpConnectionManagerOptions","Promise","heartbeatIntervalInSeconds","reconnectTimeInSeconds","findServers","urls","callback","IAmqpConnectionManager","Function","ChannelModel","addListener","event","args","listener","reason","listeners","eventName","on","once","prependListener","prependOnceListener","removeListener","connect","options","reconnect","createChannel","close","isConnected","channelCount","AmqpConnectionManager","_channels","_currentUrl","_closed","_cancelRetriesHandler","_connectPromise","_currentConnection","_findServers","_urls","constructor","_connect","default"],"sources":["../../../node_modules/.pnpm/amqp-connection-manager@5.0.0_amqplib@1.0.3/node_modules/amqp-connection-manager/dist/types/AmqpConnectionManager.d.ts","../src/errors.ts","../src/types.ts","../src/client.ts"],"x_google_ignoreList":[0],"mappings":"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"}
1
+ {"version":3,"file":"index.d.cts","names":["amqp","EventEmitter","TcpSocketConnectOpts","ConnectionOptions","ChannelWrapper","CreateChannelOpts","ConnectionUrl","Options","Connect","AmqpConnectionOptions","url","connectionOptions","ConnectListener","Connection","connection","arg","ConnectFailedListener","Error","err","Buffer","noDelay","timeout","keepAlive","keepAliveDelay","clientProperties","credentials","mechanism","username","password","response","AmqpConnectionManagerOptions","Promise","heartbeatIntervalInSeconds","reconnectTimeInSeconds","findServers","urls","callback","IAmqpConnectionManager","Function","ChannelModel","addListener","event","args","listener","reason","listeners","eventName","on","once","prependListener","prependOnceListener","removeListener","connect","options","reconnect","createChannel","close","isConnected","channelCount","AmqpConnectionManager","_channels","_currentUrl","_closed","_cancelRetriesHandler","_connectPromise","_currentConnection","_findServers","_urls","constructor","_connect","default"],"sources":["../../../node_modules/.pnpm/amqp-connection-manager@5.0.0_amqplib@1.0.3/node_modules/amqp-connection-manager/dist/types/AmqpConnectionManager.d.ts","../src/errors.ts","../src/types.ts","../src/client.ts"],"x_google_ignoreList":[0],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -141,11 +141,11 @@ type CreateClientOptions<TContract extends ContractDefinition> = {
141
141
  defaultPublishOptions?: PublishOptions | undefined;
142
142
  /**
143
143
  * Maximum time in ms to wait for the AMQP connection to become ready before
144
- * `create()` resolves to `Result.Error<TechnicalError>`. Without this option,
145
- * `create()` waits forever the underlying amqp-connection-manager retries
146
- * indefinitely.
144
+ * `create()` resolves to `Result.Error<TechnicalError>`. Defaults to 30s
145
+ * (the {@link AmqpClient}'s `DEFAULT_CONNECT_TIMEOUT_MS`). Pass `null` to
146
+ * disable the timeout and let amqp-connection-manager retry indefinitely.
147
147
  */
148
- connectTimeoutMs?: number | undefined;
148
+ connectTimeoutMs?: number | null | undefined;
149
149
  };
150
150
  /**
151
151
  * Per-call options for `client.call()`.
@@ -213,8 +213,11 @@ declare class TypedAmqpClient<TContract extends ContractDefinition> {
213
213
  /**
214
214
  * Demultiplex an RPC reply by `correlationId`, validate the body against the
215
215
  * call's response schema, and resolve the awaiting caller. Replies with no
216
- * matching pending call (e.g. arriving after the call timed out) are dropped
217
- * with a debug log.
216
+ * matching pending call (the call already timed out, was cancelled, or the
217
+ * correlationId is unknown) are logged at warn — a non-zero rate of these
218
+ * usually indicates a tuning problem (handler latency exceeds caller
219
+ * timeout). The `messaging.rpc.late_reply` counter lets dashboards alert on
220
+ * sustained drift without parsing logs.
218
221
  */
219
222
  private handleRpcReply;
220
223
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":["amqp","EventEmitter","TcpSocketConnectOpts","ConnectionOptions","ChannelWrapper","CreateChannelOpts","ConnectionUrl","Options","Connect","AmqpConnectionOptions","url","connectionOptions","ConnectListener","Connection","connection","arg","ConnectFailedListener","Error","err","Buffer","noDelay","timeout","keepAlive","keepAliveDelay","clientProperties","credentials","mechanism","username","password","response","AmqpConnectionManagerOptions","Promise","heartbeatIntervalInSeconds","reconnectTimeInSeconds","findServers","urls","callback","IAmqpConnectionManager","Function","ChannelModel","addListener","event","args","listener","reason","listeners","eventName","on","once","prependListener","prependOnceListener","removeListener","connect","options","reconnect","createChannel","close","isConnected","channelCount","AmqpConnectionManager","_channels","_currentUrl","_closed","_cancelRetriesHandler","_connectPromise","_currentConnection","_findServers","_urls","constructor","_connect","default"],"sources":["../../../node_modules/.pnpm/amqp-connection-manager@5.0.0_amqplib@1.0.3/node_modules/amqp-connection-manager/dist/types/AmqpConnectionManager.d.ts","../src/errors.ts","../src/types.ts","../src/client.ts"],"x_google_ignoreList":[0],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":["amqp","EventEmitter","TcpSocketConnectOpts","ConnectionOptions","ChannelWrapper","CreateChannelOpts","ConnectionUrl","Options","Connect","AmqpConnectionOptions","url","connectionOptions","ConnectListener","Connection","connection","arg","ConnectFailedListener","Error","err","Buffer","noDelay","timeout","keepAlive","keepAliveDelay","clientProperties","credentials","mechanism","username","password","response","AmqpConnectionManagerOptions","Promise","heartbeatIntervalInSeconds","reconnectTimeInSeconds","findServers","urls","callback","IAmqpConnectionManager","Function","ChannelModel","addListener","event","args","listener","reason","listeners","eventName","on","once","prependListener","prependOnceListener","removeListener","connect","options","reconnect","createChannel","close","isConnected","channelCount","AmqpConnectionManager","_channels","_currentUrl","_closed","_cancelRetriesHandler","_connectPromise","_currentConnection","_findServers","_urls","constructor","_connect","default"],"sources":["../../../node_modules/.pnpm/amqp-connection-manager@5.0.0_amqplib@1.0.3/node_modules/amqp-connection-manager/dist/types/AmqpConnectionManager.d.ts","../src/errors.ts","../src/types.ts","../src/client.ts"],"x_google_ignoreList":[0],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { extractQueue } from "@amqp-contract/contract";
2
- import { AmqpClient, MessageValidationError, MessagingSemanticConventions, TechnicalError, defaultTelemetryProvider, endSpanError, endSpanSuccess, recordPublishMetric, startPublishSpan } from "@amqp-contract/core";
2
+ import { AmqpClient, MessageValidationError, MessagingSemanticConventions, TechnicalError, defaultTelemetryProvider, endSpanError, endSpanSuccess, recordLateRpcReply, recordPublishMetric, startPublishSpan } from "@amqp-contract/core";
3
3
  import { Future, Result } from "@swan-io/boxed";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { deflate, gzip } from "node:zlib";
@@ -132,19 +132,24 @@ var TypedAmqpClient = class TypedAmqpClient {
132
132
  /**
133
133
  * Demultiplex an RPC reply by `correlationId`, validate the body against the
134
134
  * call's response schema, and resolve the awaiting caller. Replies with no
135
- * matching pending call (e.g. arriving after the call timed out) are dropped
136
- * with a debug log.
135
+ * matching pending call (the call already timed out, was cancelled, or the
136
+ * correlationId is unknown) are logged at warn — a non-zero rate of these
137
+ * usually indicates a tuning problem (handler latency exceeds caller
138
+ * timeout). The `messaging.rpc.late_reply` counter lets dashboards alert on
139
+ * sustained drift without parsing logs.
137
140
  */
138
141
  handleRpcReply(msg) {
139
142
  if (!msg) return;
140
143
  const correlationId = msg.properties.correlationId;
141
144
  if (typeof correlationId !== "string") {
142
145
  this.logger?.warn("Received RPC reply without correlationId; dropping", { deliveryTag: msg.fields.deliveryTag });
146
+ recordLateRpcReply(this.telemetry, "missing-correlation-id");
143
147
  return;
144
148
  }
145
149
  const pending = this.pendingCalls.get(correlationId);
146
150
  if (!pending) {
147
- this.logger?.debug("Received RPC reply for unknown correlationId", { correlationId });
151
+ this.logger?.warn("Received RPC reply for unknown correlationId (caller already timed out or cancelled)", { correlationId });
152
+ recordLateRpcReply(this.telemetry, "unknown-correlation-id");
148
153
  return;
149
154
  }
150
155
  this.pendingCalls.delete(correlationId);
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/compression.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["import { Future, Result } from \"@swan-io/boxed\";\nimport { deflate, gzip } from \"node:zlib\";\nimport type { CompressionAlgorithm } from \"@amqp-contract/contract\";\nimport { TechnicalError } from \"@amqp-contract/core\";\nimport { match } from \"ts-pattern\";\nimport { promisify } from \"node:util\";\n\nconst gzipAsync = promisify(gzip);\nconst deflateAsync = promisify(deflate);\n\n/**\n * Compress a buffer using the specified compression algorithm.\n *\n * @param buffer - The buffer to compress\n * @param algorithm - The compression algorithm to use\n * @returns A Future with the compressed buffer or a TechnicalError\n *\n * @internal\n */\nexport function compressBuffer(\n buffer: Buffer,\n algorithm: CompressionAlgorithm,\n): Future<Result<Buffer, TechnicalError>> {\n return match(algorithm)\n .with(\"gzip\", () =>\n Future.fromPromise(gzipAsync(buffer)).mapError(\n (error) => new TechnicalError(\"Failed to compress with gzip\", error),\n ),\n )\n .with(\"deflate\", () =>\n Future.fromPromise(deflateAsync(buffer)).mapError(\n (error) => new TechnicalError(\"Failed to compress with deflate\", error),\n ),\n )\n .exhaustive();\n}\n","export { MessageValidationError } from \"@amqp-contract/core\";\n\n/**\n * Captured `Error.captureStackTrace` shim — only present on Node.js.\n */\nfunction captureStack(target: object, ctor: Function): void {\n const ErrorConstructor = Error as unknown as {\n captureStackTrace?: (target: object, constructor: Function) => void;\n };\n if (typeof ErrorConstructor.captureStackTrace === \"function\") {\n ErrorConstructor.captureStackTrace(target, ctor);\n }\n}\n\n/**\n * Returned from `TypedAmqpClient.call()` when the configured `timeoutMs` elapses\n * before the RPC server publishes a reply with the matching `correlationId`.\n *\n * The pending call is removed from the in-memory correlation map; if a reply\n * arrives after the timeout it is dropped (and a debug log is emitted by the\n * client if a logger is configured).\n */\nexport class RpcTimeoutError extends Error {\n constructor(\n public readonly rpcName: string,\n public readonly timeoutMs: number,\n ) {\n super(`RPC call to \"${rpcName}\" timed out after ${timeoutMs}ms with no reply received`);\n this.name = \"RpcTimeoutError\";\n captureStack(this, this.constructor);\n }\n}\n\n/**\n * Returned from any in-flight RPC call when the client is closed before the\n * reply is received. The correlation map is cleared on close and every pending\n * caller's promise resolves with `Result.Error(RpcCancelledError)`.\n */\nexport class RpcCancelledError extends Error {\n constructor(public readonly rpcName: string) {\n super(`RPC call to \"${rpcName}\" was cancelled because the client was closed`);\n this.name = \"RpcCancelledError\";\n captureStack(this, this.constructor);\n }\n}\n","import {\n extractQueue,\n type CompressionAlgorithm,\n type ContractDefinition,\n type InferPublisherNames,\n type InferRpcNames,\n} from \"@amqp-contract/contract\";\nimport {\n AmqpClient,\n PublishOptions as AmqpClientPublishOptions,\n type Logger,\n MessagingSemanticConventions,\n TechnicalError,\n type TelemetryProvider,\n defaultTelemetryProvider,\n endSpanError,\n endSpanSuccess,\n recordPublishMetric,\n startPublishSpan,\n} from \"@amqp-contract/core\";\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { Future, Result } from \"@swan-io/boxed\";\nimport type { AmqpConnectionManagerOptions, ConnectionUrl } from \"amqp-connection-manager\";\nimport { randomUUID } from \"node:crypto\";\nimport { compressBuffer } from \"./compression.js\";\nimport { MessageValidationError, RpcCancelledError, RpcTimeoutError } from \"./errors.js\";\nimport type {\n ClientInferPublisherInput,\n ClientInferRpcRequestInput,\n ClientInferRpcResponseOutput,\n} from \"./types.js\";\n\n/**\n * The RabbitMQ direct-reply-to pseudo-queue. Publishing with `replyTo` set to\n * this value tells the server to deliver the response back to the consumer\n * subscribed on this queue on the same channel — no real queue is created and\n * no setup is required beyond consuming from it once with `noAck: true`.\n *\n * @see https://www.rabbitmq.com/docs/direct-reply-to\n */\nconst DIRECT_REPLY_TO = \"amq.rabbitmq.reply-to\";\n\n/**\n * In-flight RPC call tracked by `TypedAmqpClient`. The reply consumer\n * looks up entries by `correlationId` when responses arrive.\n */\ntype PendingCall = {\n rpcName: string;\n responseSchema: StandardSchemaV1;\n resolve: (\n result: Result<\n unknown,\n TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError\n >,\n ) => void;\n timer: ReturnType<typeof setTimeout>;\n};\n\n/**\n * Publish options that extend amqp-client's PublishOptions with optional compression support.\n */\nexport type PublishOptions = AmqpClientPublishOptions & {\n /**\n * Optional compression algorithm to use for the message payload.\n * When specified, the message will be compressed using the chosen algorithm\n * and the contentEncoding header will be set automatically.\n */\n compression?: CompressionAlgorithm | undefined;\n};\n\n/**\n * Options for creating a client\n */\nexport type CreateClientOptions<TContract extends ContractDefinition> = {\n contract: TContract;\n urls: ConnectionUrl[];\n connectionOptions?: AmqpConnectionManagerOptions | undefined;\n logger?: Logger | undefined;\n /**\n * Optional telemetry provider for tracing and metrics.\n * If not provided, uses the default provider which attempts to load OpenTelemetry.\n * OpenTelemetry instrumentation is automatically enabled if @opentelemetry/api is installed.\n */\n telemetry?: TelemetryProvider | undefined;\n /**\n * Default publish options that will be applied to all publish operations.\n * These can be overridden by options passed to the publish method.\n * By default, persistent is set to true for message durability.\n */\n defaultPublishOptions?: PublishOptions | undefined;\n /**\n * Maximum time in ms to wait for the AMQP connection to become ready before\n * `create()` resolves to `Result.Error<TechnicalError>`. Without this option,\n * `create()` waits forever — the underlying amqp-connection-manager retries\n * indefinitely.\n */\n connectTimeoutMs?: number | undefined;\n};\n\n/**\n * Per-call options for `client.call()`.\n */\nexport type CallOptions = {\n /**\n * Maximum time in ms to wait for an RPC reply. If exceeded, the call resolves\n * to `Result.Error<RpcTimeoutError>` and the in-memory correlation entry is\n * cleared. A late reply arriving after the timeout is silently dropped.\n *\n * Required: RPC without a timeout is a footgun.\n */\n timeoutMs: number;\n\n /**\n * Optional AMQP message properties to merge into the request. `replyTo` and\n * `correlationId` are managed by the client and cannot be overridden.\n */\n publishOptions?: Omit<AmqpClientPublishOptions, \"replyTo\" | \"correlationId\">;\n};\n\n/**\n * Type-safe AMQP client for publishing messages\n */\nexport class TypedAmqpClient<TContract extends ContractDefinition> {\n /**\n * In-flight RPC calls keyed by `correlationId`. Cleared when a reply is\n * received, when the call times out, or when the client is closed.\n */\n private readonly pendingCalls = new Map<string, PendingCall>();\n\n /**\n * Consumer tag of the reply consumer subscribed on `amq.rabbitmq.reply-to`.\n * Set when the contract has at least one entry in `rpcs`; undefined otherwise.\n */\n private replyConsumerTag?: string;\n\n private constructor(\n private readonly contract: TContract,\n private readonly amqpClient: AmqpClient,\n private readonly defaultPublishOptions: PublishOptions,\n private readonly logger?: Logger,\n private readonly telemetry: TelemetryProvider = defaultTelemetryProvider,\n ) {}\n\n /**\n * Create a type-safe AMQP client from a contract.\n *\n * Connection management (including automatic reconnection) is handled internally\n * by amqp-connection-manager via the {@link AmqpClient}. The client establishes\n * infrastructure asynchronously in the background once the connection is ready.\n *\n * Connections are automatically shared across clients with the same URLs and\n * connection options, following RabbitMQ best practices.\n */\n static create<TContract extends ContractDefinition>({\n contract,\n urls,\n connectionOptions,\n defaultPublishOptions,\n logger,\n telemetry,\n connectTimeoutMs,\n }: CreateClientOptions<TContract>): Future<Result<TypedAmqpClient<TContract>, TechnicalError>> {\n const client = new TypedAmqpClient(\n contract,\n new AmqpClient(contract, { urls, connectionOptions, connectTimeoutMs }),\n { persistent: true, ...defaultPublishOptions },\n logger,\n telemetry ?? defaultTelemetryProvider,\n );\n\n return client\n .waitForConnectionReady()\n .flatMapOk(() => client.setupReplyConsumerIfNeeded())\n .flatMap((result) =>\n result.match({\n Ok: () => Future.value(Result.Ok<TypedAmqpClient<TContract>, TechnicalError>(client)),\n // Release the AmqpClient's connection ref-count so a failed create() does not leak.\n Error: (error) =>\n client\n .close()\n .tapError((closeError) => {\n logger?.warn(\"Failed to close client after connection failure\", {\n error: closeError,\n });\n })\n .map(() => Result.Error<TypedAmqpClient<TContract>, TechnicalError>(error)),\n }),\n );\n }\n\n /**\n * If the contract has any RPC entry, subscribe to `amq.rabbitmq.reply-to`\n * once. Replies for every in-flight call arrive on this single consumer and\n * are demultiplexed by `correlationId`.\n */\n private setupReplyConsumerIfNeeded(): Future<Result<void, TechnicalError>> {\n const rpcs = this.contract.rpcs ?? {};\n if (Object.keys(rpcs).length === 0) {\n return Future.value(Result.Ok(undefined));\n }\n\n return this.amqpClient\n .consume(DIRECT_REPLY_TO, (msg) => this.handleRpcReply(msg), { noAck: true })\n .tapOk((tag) => {\n this.replyConsumerTag = tag;\n })\n .mapOk(() => undefined);\n }\n\n /**\n * Demultiplex an RPC reply by `correlationId`, validate the body against the\n * call's response schema, and resolve the awaiting caller. Replies with no\n * matching pending call (e.g. arriving after the call timed out) are dropped\n * with a debug log.\n */\n private handleRpcReply(msg: Parameters<Parameters<AmqpClient[\"consume\"]>[1]>[0]): void {\n if (!msg) return;\n const correlationId = msg.properties.correlationId;\n if (typeof correlationId !== \"string\") {\n this.logger?.warn(\"Received RPC reply without correlationId; dropping\", {\n deliveryTag: msg.fields.deliveryTag,\n });\n return;\n }\n const pending = this.pendingCalls.get(correlationId);\n if (!pending) {\n this.logger?.debug(\"Received RPC reply for unknown correlationId\", { correlationId });\n return;\n }\n this.pendingCalls.delete(correlationId);\n clearTimeout(pending.timer);\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(msg.content.toString());\n } catch (error: unknown) {\n pending.resolve(\n Result.Error(\n new TechnicalError(`Failed to parse RPC reply JSON for \"${pending.rpcName}\"`, error),\n ),\n );\n return;\n }\n\n // Wrap the validate call itself — a Standard Schema implementation may\n // throw synchronously, and the throw would otherwise escape the consume\n // callback and could crash the reply consumer.\n let rawValidation: ReturnType<StandardSchemaV1[\"~standard\"][\"validate\"]>;\n try {\n rawValidation = pending.responseSchema[\"~standard\"].validate(parsed);\n } catch (error: unknown) {\n pending.resolve(\n Result.Error(\n new TechnicalError(`RPC reply validation threw for \"${pending.rpcName}\"`, error),\n ),\n );\n return;\n }\n const validationPromise =\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);\n\n validationPromise.then(\n (validation) => {\n if (validation.issues) {\n pending.resolve(\n Result.Error(new MessageValidationError(pending.rpcName, validation.issues)),\n );\n return;\n }\n pending.resolve(Result.Ok(validation.value));\n },\n (error: unknown) => {\n pending.resolve(\n Result.Error(\n new TechnicalError(`RPC reply validation threw for \"${pending.rpcName}\"`, error),\n ),\n );\n },\n );\n }\n\n /**\n * Publish a message using a defined publisher\n *\n * @param publisherName - The name of the publisher to use\n * @param message - The message to publish\n * @param options - Optional publish options including compression, headers, priority, etc.\n *\n * @remarks\n * If `options.compression` is specified, the message will be compressed before publishing\n * and the `contentEncoding` property will be set automatically. Any `contentEncoding`\n * value already in options will be overwritten by the compression algorithm.\n *\n * @returns Result.Ok(void) on success, or Result.Error with specific error on failure\n */\n /**\n * Publish a message using a defined publisher.\n * TypeScript guarantees publisher exists for valid publisher names.\n */\n publish<TName extends InferPublisherNames<TContract>>(\n publisherName: TName,\n message: ClientInferPublisherInput<TContract, TName>,\n options?: PublishOptions,\n ): Future<Result<void, TechnicalError | MessageValidationError>> {\n const startTime = Date.now();\n // Non-null assertions safe: TypeScript guarantees these exist for valid TName\n const publisher = this.contract.publishers![publisherName as string]!;\n const { exchange, routingKey } = publisher;\n\n // Start telemetry span\n const span = startPublishSpan(this.telemetry, exchange.name, routingKey, {\n [MessagingSemanticConventions.AMQP_PUBLISHER_NAME]: String(publisherName),\n });\n\n const validateMessage = () => {\n const validationResult = publisher.message.payload[\"~standard\"].validate(message);\n return Future.fromPromise(\n validationResult instanceof Promise ? validationResult : Promise.resolve(validationResult),\n )\n .mapError((error) => new TechnicalError(`Validation failed`, error))\n .mapOkToResult((validation) => {\n if (validation.issues) {\n return Result.Error(\n new MessageValidationError(String(publisherName), validation.issues),\n );\n }\n\n return Result.Ok(validation.value);\n });\n };\n\n const publishMessage = (validatedMessage: unknown): Future<Result<void, TechnicalError>> => {\n // Merge default options with provided options\n const mergedOptions = { ...this.defaultPublishOptions, ...options };\n\n // Extract compression from merged options and create publish options without it\n const { compression, ...restOptions } = mergedOptions;\n const publishOptions: AmqpClientPublishOptions = { ...restOptions };\n\n // Prepare payload and options based on compression configuration\n const preparePayload = (): Future<Result<Buffer | unknown, TechnicalError>> => {\n if (compression) {\n // Compress the message payload\n const messageBuffer = Buffer.from(JSON.stringify(validatedMessage));\n publishOptions.contentEncoding = compression;\n\n return compressBuffer(messageBuffer, compression);\n }\n\n // No compression: use the channel's built-in JSON serialization\n return Future.value(Result.Ok(validatedMessage));\n };\n\n // Publish the prepared payload\n return preparePayload().flatMapOk((payload) =>\n this.amqpClient\n .publish(publisher.exchange.name, publisher.routingKey ?? \"\", payload, publishOptions)\n .mapOkToResult((published) => {\n if (!published) {\n return Result.Error(\n new TechnicalError(\n `Failed to publish message for publisher \"${String(publisherName)}\": Channel rejected the message (buffer full or other channel issue)`,\n ),\n );\n }\n\n this.logger?.info(\"Message published successfully\", {\n publisherName: String(publisherName),\n exchange: publisher.exchange.name,\n routingKey: publisher.routingKey,\n compressed: !!compression,\n });\n\n return Result.Ok(undefined);\n }),\n );\n };\n\n // Validate message using schema\n return validateMessage()\n .flatMapOk((validatedMessage) => publishMessage(validatedMessage))\n .tapOk(() => {\n const durationMs = Date.now() - startTime;\n endSpanSuccess(span);\n recordPublishMetric(this.telemetry, exchange.name, routingKey, true, durationMs);\n })\n .tapError((error) => {\n const durationMs = Date.now() - startTime;\n endSpanError(span, error);\n recordPublishMetric(this.telemetry, exchange.name, routingKey, false, durationMs);\n });\n }\n\n /**\n * Invoke an RPC defined via `defineRpc` and await the typed response.\n *\n * The request payload is validated against the RPC's request schema, then\n * published to the AMQP default exchange with the server's queue name as\n * routing key, `replyTo` set to `amq.rabbitmq.reply-to`, and a fresh UUID\n * `correlationId`. The returned Future resolves once a matching reply\n * arrives and validates against the response schema, or once `timeoutMs`\n * elapses (whichever comes first).\n *\n * @typeParam TName - An RPC name from `contract.rpcs`.\n * @param rpcName - The RPC name from the contract.\n * @param request - The request payload, validated against the request schema.\n * @param options - Per-call options. `timeoutMs` is required.\n *\n * @returns `Result.Ok(response)` on a successful round-trip; `Result.Error`\n * on validation, transport, timeout, or cancel.\n *\n * @example\n * ```typescript\n * const result = await client\n * .call('calculate', { a: 1, b: 2 }, { timeoutMs: 5_000 })\n * .toPromise();\n * if (result.isOk()) console.log(result.value.sum); // 3\n * ```\n */\n call<TName extends InferRpcNames<TContract>>(\n rpcName: TName,\n request: ClientInferRpcRequestInput<TContract, TName>,\n options: CallOptions,\n ): Future<\n Result<\n ClientInferRpcResponseOutput<TContract, TName>,\n TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError\n >\n > {\n type CallResult = Result<\n ClientInferRpcResponseOutput<TContract, TName>,\n TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError\n >;\n\n // setTimeout truncates fractional ms and clamps anything outside the\n // 32-bit signed integer range (~24.8 days) to 1ms, so reject those up\n // front as user errors rather than producing surprising behavior.\n const TIMEOUT_MAX_MS = 2_147_483_647;\n if (\n typeof options.timeoutMs !== \"number\" ||\n !Number.isFinite(options.timeoutMs) ||\n options.timeoutMs <= 0 ||\n options.timeoutMs > TIMEOUT_MAX_MS\n ) {\n return Future.value(\n Result.Error(\n new TechnicalError(\n `Invalid timeoutMs for RPC call to \"${String(rpcName)}\": expected a finite positive number ≤ ${TIMEOUT_MAX_MS}, got ${String(options.timeoutMs)}`,\n ),\n ) as CallResult,\n );\n }\n\n const startTime = Date.now();\n // Non-null assertion safe: TName is constrained to RPC names in the contract.\n const rpc = this.contract.rpcs![rpcName as string]!;\n const requestSchema = rpc.request.payload;\n const responseSchema = rpc.response.payload;\n const queueName = extractQueue(rpc.queue).name;\n\n // RPC publishes to the default exchange with the queue name as routing key.\n const span = startPublishSpan(this.telemetry, \"\", queueName, {\n [MessagingSemanticConventions.AMQP_PUBLISHER_NAME]: String(rpcName),\n });\n\n const correlationId = randomUUID();\n const callFuture = Future.make<CallResult>((resolve) => {\n const timer = setTimeout(() => {\n const pending = this.pendingCalls.get(correlationId);\n if (!pending) return;\n this.pendingCalls.delete(correlationId);\n resolve(Result.Error(new RpcTimeoutError(String(rpcName), options.timeoutMs)));\n }, options.timeoutMs);\n\n this.pendingCalls.set(correlationId, {\n rpcName: String(rpcName),\n responseSchema,\n resolve: resolve as PendingCall[\"resolve\"],\n timer,\n });\n });\n\n const validateRequest = (): Future<\n Result<unknown, TechnicalError | MessageValidationError>\n > => {\n // Wrap the validate call — a Standard Schema implementation may throw\n // synchronously, and that throw would otherwise escape the Future chain\n // and leave the pending-call entry/timer dangling until timeout.\n let rawValidation: ReturnType<StandardSchemaV1[\"~standard\"][\"validate\"]>;\n try {\n rawValidation = requestSchema[\"~standard\"].validate(request);\n } catch (error: unknown) {\n return Future.value(\n Result.Error<unknown, TechnicalError | MessageValidationError>(\n new TechnicalError(\"RPC request validation threw\", error),\n ),\n );\n }\n const validationPromise =\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);\n return Future.fromPromise(validationPromise)\n .mapError((error) => new TechnicalError(\"RPC request validation threw\", error))\n .mapOkToResult((validation) =>\n validation.issues\n ? Result.Error<unknown, TechnicalError | MessageValidationError>(\n new MessageValidationError(String(rpcName), validation.issues),\n )\n : Result.Ok<unknown, TechnicalError | MessageValidationError>(validation.value),\n );\n };\n\n const publishRequest = (validatedRequest: unknown): Future<Result<void, TechnicalError>> => {\n // Merge `defaultPublishOptions` (persistent, priority, headers, …) with\n // the per-call options, then layer the RPC-managed fields on top so they\n // cannot be overridden. `compression` is intentionally dropped: RPC v1\n // does not implement reply-side decompression, so request-side\n // compression would break the round-trip.\n const { compression: _ignoredCompression, ...defaultsWithoutCompression } =\n this.defaultPublishOptions;\n const publishOptions: AmqpClientPublishOptions = {\n ...defaultsWithoutCompression,\n ...options.publishOptions,\n replyTo: DIRECT_REPLY_TO,\n correlationId,\n contentType: \"application/json\",\n };\n return this.amqpClient\n .publish(\"\", queueName, validatedRequest, publishOptions)\n .mapOkToResult((published) =>\n published\n ? Result.Ok<void, TechnicalError>(undefined)\n : Result.Error<void, TechnicalError>(\n new TechnicalError(\n `Failed to publish RPC request for \"${String(rpcName)}\": channel buffer full`,\n ),\n ),\n );\n };\n\n // Validate the request, publish it, and await the reply (or timeout).\n return validateRequest()\n .flatMapOk((validated) => publishRequest(validated))\n .flatMap((preflight) => {\n if (preflight.isError()) {\n // Publish/validation failed before the request hit the broker — clean\n // up the pending entry so the timer never fires.\n const pending = this.pendingCalls.get(correlationId);\n if (pending) {\n clearTimeout(pending.timer);\n this.pendingCalls.delete(correlationId);\n }\n return Future.value(Result.Error(preflight.error) as CallResult);\n }\n return callFuture;\n })\n .tapOk(() => {\n const durationMs = Date.now() - startTime;\n endSpanSuccess(span);\n recordPublishMetric(this.telemetry, \"\", queueName, true, durationMs);\n })\n .tapError((error) => {\n const durationMs = Date.now() - startTime;\n endSpanError(span, error);\n recordPublishMetric(this.telemetry, \"\", queueName, false, durationMs);\n });\n }\n\n /**\n * Close the channel and connection. Cancels the reply consumer (if any) and\n * rejects every in-flight RPC call with `RpcCancelledError`.\n */\n close(): Future<Result<void, TechnicalError>> {\n // Reject pending calls first — once close() runs, no reply will arrive.\n for (const [, pending] of this.pendingCalls) {\n clearTimeout(pending.timer);\n pending.resolve(Result.Error(new RpcCancelledError(pending.rpcName)));\n }\n this.pendingCalls.clear();\n\n const cancelReply = this.replyConsumerTag\n ? this.amqpClient.cancel(this.replyConsumerTag).tapError((error) => {\n this.logger?.warn(\"Failed to cancel RPC reply consumer during close\", { error });\n })\n : Future.value(Result.Ok<void, TechnicalError>(undefined));\n\n return cancelReply.flatMap(() => this.amqpClient.close()).mapOk(() => undefined);\n }\n\n private waitForConnectionReady(): Future<Result<void, TechnicalError>> {\n return this.amqpClient.waitForConnect();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/compression.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["import { Future, Result } from \"@swan-io/boxed\";\nimport { deflate, gzip } from \"node:zlib\";\nimport type { CompressionAlgorithm } from \"@amqp-contract/contract\";\nimport { TechnicalError } from \"@amqp-contract/core\";\nimport { match } from \"ts-pattern\";\nimport { promisify } from \"node:util\";\n\nconst gzipAsync = promisify(gzip);\nconst deflateAsync = promisify(deflate);\n\n/**\n * Compress a buffer using the specified compression algorithm.\n *\n * @param buffer - The buffer to compress\n * @param algorithm - The compression algorithm to use\n * @returns A Future with the compressed buffer or a TechnicalError\n *\n * @internal\n */\nexport function compressBuffer(\n buffer: Buffer,\n algorithm: CompressionAlgorithm,\n): Future<Result<Buffer, TechnicalError>> {\n return match(algorithm)\n .with(\"gzip\", () =>\n Future.fromPromise(gzipAsync(buffer)).mapError(\n (error) => new TechnicalError(\"Failed to compress with gzip\", error),\n ),\n )\n .with(\"deflate\", () =>\n Future.fromPromise(deflateAsync(buffer)).mapError(\n (error) => new TechnicalError(\"Failed to compress with deflate\", error),\n ),\n )\n .exhaustive();\n}\n","export { MessageValidationError } from \"@amqp-contract/core\";\n\n/**\n * Captured `Error.captureStackTrace` shim — only present on Node.js.\n */\nfunction captureStack(target: object, ctor: Function): void {\n const ErrorConstructor = Error as unknown as {\n captureStackTrace?: (target: object, constructor: Function) => void;\n };\n if (typeof ErrorConstructor.captureStackTrace === \"function\") {\n ErrorConstructor.captureStackTrace(target, ctor);\n }\n}\n\n/**\n * Returned from `TypedAmqpClient.call()` when the configured `timeoutMs` elapses\n * before the RPC server publishes a reply with the matching `correlationId`.\n *\n * The pending call is removed from the in-memory correlation map; if a reply\n * arrives after the timeout it is dropped (and a debug log is emitted by the\n * client if a logger is configured).\n */\nexport class RpcTimeoutError extends Error {\n constructor(\n public readonly rpcName: string,\n public readonly timeoutMs: number,\n ) {\n super(`RPC call to \"${rpcName}\" timed out after ${timeoutMs}ms with no reply received`);\n this.name = \"RpcTimeoutError\";\n captureStack(this, this.constructor);\n }\n}\n\n/**\n * Returned from any in-flight RPC call when the client is closed before the\n * reply is received. The correlation map is cleared on close and every pending\n * caller's promise resolves with `Result.Error(RpcCancelledError)`.\n */\nexport class RpcCancelledError extends Error {\n constructor(public readonly rpcName: string) {\n super(`RPC call to \"${rpcName}\" was cancelled because the client was closed`);\n this.name = \"RpcCancelledError\";\n captureStack(this, this.constructor);\n }\n}\n","import {\n extractQueue,\n type CompressionAlgorithm,\n type ContractDefinition,\n type InferPublisherNames,\n type InferRpcNames,\n} from \"@amqp-contract/contract\";\nimport {\n AmqpClient,\n PublishOptions as AmqpClientPublishOptions,\n type Logger,\n MessagingSemanticConventions,\n TechnicalError,\n type TelemetryProvider,\n defaultTelemetryProvider,\n endSpanError,\n endSpanSuccess,\n recordLateRpcReply,\n recordPublishMetric,\n startPublishSpan,\n} from \"@amqp-contract/core\";\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { Future, Result } from \"@swan-io/boxed\";\nimport type { AmqpConnectionManagerOptions, ConnectionUrl } from \"amqp-connection-manager\";\nimport { randomUUID } from \"node:crypto\";\nimport { compressBuffer } from \"./compression.js\";\nimport { MessageValidationError, RpcCancelledError, RpcTimeoutError } from \"./errors.js\";\nimport type {\n ClientInferPublisherInput,\n ClientInferRpcRequestInput,\n ClientInferRpcResponseOutput,\n} from \"./types.js\";\n\n/**\n * The RabbitMQ direct-reply-to pseudo-queue. Publishing with `replyTo` set to\n * this value tells the server to deliver the response back to the consumer\n * subscribed on this queue on the same channel — no real queue is created and\n * no setup is required beyond consuming from it once with `noAck: true`.\n *\n * @see https://www.rabbitmq.com/docs/direct-reply-to\n */\nconst DIRECT_REPLY_TO = \"amq.rabbitmq.reply-to\";\n\n/**\n * In-flight RPC call tracked by `TypedAmqpClient`. The reply consumer\n * looks up entries by `correlationId` when responses arrive.\n */\ntype PendingCall = {\n rpcName: string;\n responseSchema: StandardSchemaV1;\n resolve: (\n result: Result<\n unknown,\n TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError\n >,\n ) => void;\n timer: ReturnType<typeof setTimeout>;\n};\n\n/**\n * Publish options that extend amqp-client's PublishOptions with optional compression support.\n */\nexport type PublishOptions = AmqpClientPublishOptions & {\n /**\n * Optional compression algorithm to use for the message payload.\n * When specified, the message will be compressed using the chosen algorithm\n * and the contentEncoding header will be set automatically.\n */\n compression?: CompressionAlgorithm | undefined;\n};\n\n/**\n * Options for creating a client\n */\nexport type CreateClientOptions<TContract extends ContractDefinition> = {\n contract: TContract;\n urls: ConnectionUrl[];\n connectionOptions?: AmqpConnectionManagerOptions | undefined;\n logger?: Logger | undefined;\n /**\n * Optional telemetry provider for tracing and metrics.\n * If not provided, uses the default provider which attempts to load OpenTelemetry.\n * OpenTelemetry instrumentation is automatically enabled if @opentelemetry/api is installed.\n */\n telemetry?: TelemetryProvider | undefined;\n /**\n * Default publish options that will be applied to all publish operations.\n * These can be overridden by options passed to the publish method.\n * By default, persistent is set to true for message durability.\n */\n defaultPublishOptions?: PublishOptions | undefined;\n /**\n * Maximum time in ms to wait for the AMQP connection to become ready before\n * `create()` resolves to `Result.Error<TechnicalError>`. Defaults to 30s\n * (the {@link AmqpClient}'s `DEFAULT_CONNECT_TIMEOUT_MS`). Pass `null` to\n * disable the timeout and let amqp-connection-manager retry indefinitely.\n */\n connectTimeoutMs?: number | null | undefined;\n};\n\n/**\n * Per-call options for `client.call()`.\n */\nexport type CallOptions = {\n /**\n * Maximum time in ms to wait for an RPC reply. If exceeded, the call resolves\n * to `Result.Error<RpcTimeoutError>` and the in-memory correlation entry is\n * cleared. A late reply arriving after the timeout is silently dropped.\n *\n * Required: RPC without a timeout is a footgun.\n */\n timeoutMs: number;\n\n /**\n * Optional AMQP message properties to merge into the request. `replyTo` and\n * `correlationId` are managed by the client and cannot be overridden.\n */\n publishOptions?: Omit<AmqpClientPublishOptions, \"replyTo\" | \"correlationId\">;\n};\n\n/**\n * Type-safe AMQP client for publishing messages\n */\nexport class TypedAmqpClient<TContract extends ContractDefinition> {\n /**\n * In-flight RPC calls keyed by `correlationId`. Cleared when a reply is\n * received, when the call times out, or when the client is closed.\n */\n private readonly pendingCalls = new Map<string, PendingCall>();\n\n /**\n * Consumer tag of the reply consumer subscribed on `amq.rabbitmq.reply-to`.\n * Set when the contract has at least one entry in `rpcs`; undefined otherwise.\n */\n private replyConsumerTag?: string;\n\n private constructor(\n private readonly contract: TContract,\n private readonly amqpClient: AmqpClient,\n private readonly defaultPublishOptions: PublishOptions,\n private readonly logger?: Logger,\n private readonly telemetry: TelemetryProvider = defaultTelemetryProvider,\n ) {}\n\n /**\n * Create a type-safe AMQP client from a contract.\n *\n * Connection management (including automatic reconnection) is handled internally\n * by amqp-connection-manager via the {@link AmqpClient}. The client establishes\n * infrastructure asynchronously in the background once the connection is ready.\n *\n * Connections are automatically shared across clients with the same URLs and\n * connection options, following RabbitMQ best practices.\n */\n static create<TContract extends ContractDefinition>({\n contract,\n urls,\n connectionOptions,\n defaultPublishOptions,\n logger,\n telemetry,\n connectTimeoutMs,\n }: CreateClientOptions<TContract>): Future<Result<TypedAmqpClient<TContract>, TechnicalError>> {\n const client = new TypedAmqpClient(\n contract,\n new AmqpClient(contract, { urls, connectionOptions, connectTimeoutMs }),\n { persistent: true, ...defaultPublishOptions },\n logger,\n telemetry ?? defaultTelemetryProvider,\n );\n\n return client\n .waitForConnectionReady()\n .flatMapOk(() => client.setupReplyConsumerIfNeeded())\n .flatMap((result) =>\n result.match({\n Ok: () => Future.value(Result.Ok<TypedAmqpClient<TContract>, TechnicalError>(client)),\n // Release the AmqpClient's connection ref-count so a failed create() does not leak.\n Error: (error) =>\n client\n .close()\n .tapError((closeError) => {\n logger?.warn(\"Failed to close client after connection failure\", {\n error: closeError,\n });\n })\n .map(() => Result.Error<TypedAmqpClient<TContract>, TechnicalError>(error)),\n }),\n );\n }\n\n /**\n * If the contract has any RPC entry, subscribe to `amq.rabbitmq.reply-to`\n * once. Replies for every in-flight call arrive on this single consumer and\n * are demultiplexed by `correlationId`.\n */\n private setupReplyConsumerIfNeeded(): Future<Result<void, TechnicalError>> {\n const rpcs = this.contract.rpcs ?? {};\n if (Object.keys(rpcs).length === 0) {\n return Future.value(Result.Ok(undefined));\n }\n\n return this.amqpClient\n .consume(DIRECT_REPLY_TO, (msg) => this.handleRpcReply(msg), { noAck: true })\n .tapOk((tag) => {\n this.replyConsumerTag = tag;\n })\n .mapOk(() => undefined);\n }\n\n /**\n * Demultiplex an RPC reply by `correlationId`, validate the body against the\n * call's response schema, and resolve the awaiting caller. Replies with no\n * matching pending call (the call already timed out, was cancelled, or the\n * correlationId is unknown) are logged at warn — a non-zero rate of these\n * usually indicates a tuning problem (handler latency exceeds caller\n * timeout). The `messaging.rpc.late_reply` counter lets dashboards alert on\n * sustained drift without parsing logs.\n */\n private handleRpcReply(msg: Parameters<Parameters<AmqpClient[\"consume\"]>[1]>[0]): void {\n if (!msg) return;\n const correlationId = msg.properties.correlationId;\n if (typeof correlationId !== \"string\") {\n this.logger?.warn(\"Received RPC reply without correlationId; dropping\", {\n deliveryTag: msg.fields.deliveryTag,\n });\n recordLateRpcReply(this.telemetry, \"missing-correlation-id\");\n return;\n }\n const pending = this.pendingCalls.get(correlationId);\n if (!pending) {\n this.logger?.warn(\n \"Received RPC reply for unknown correlationId (caller already timed out or cancelled)\",\n { correlationId },\n );\n recordLateRpcReply(this.telemetry, \"unknown-correlation-id\");\n return;\n }\n this.pendingCalls.delete(correlationId);\n clearTimeout(pending.timer);\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(msg.content.toString());\n } catch (error: unknown) {\n pending.resolve(\n Result.Error(\n new TechnicalError(`Failed to parse RPC reply JSON for \"${pending.rpcName}\"`, error),\n ),\n );\n return;\n }\n\n // Wrap the validate call itself — a Standard Schema implementation may\n // throw synchronously, and the throw would otherwise escape the consume\n // callback and could crash the reply consumer.\n let rawValidation: ReturnType<StandardSchemaV1[\"~standard\"][\"validate\"]>;\n try {\n rawValidation = pending.responseSchema[\"~standard\"].validate(parsed);\n } catch (error: unknown) {\n pending.resolve(\n Result.Error(\n new TechnicalError(`RPC reply validation threw for \"${pending.rpcName}\"`, error),\n ),\n );\n return;\n }\n const validationPromise =\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);\n\n validationPromise.then(\n (validation) => {\n if (validation.issues) {\n pending.resolve(\n Result.Error(new MessageValidationError(pending.rpcName, validation.issues)),\n );\n return;\n }\n pending.resolve(Result.Ok(validation.value));\n },\n (error: unknown) => {\n pending.resolve(\n Result.Error(\n new TechnicalError(`RPC reply validation threw for \"${pending.rpcName}\"`, error),\n ),\n );\n },\n );\n }\n\n /**\n * Publish a message using a defined publisher\n *\n * @param publisherName - The name of the publisher to use\n * @param message - The message to publish\n * @param options - Optional publish options including compression, headers, priority, etc.\n *\n * @remarks\n * If `options.compression` is specified, the message will be compressed before publishing\n * and the `contentEncoding` property will be set automatically. Any `contentEncoding`\n * value already in options will be overwritten by the compression algorithm.\n *\n * @returns Result.Ok(void) on success, or Result.Error with specific error on failure\n */\n /**\n * Publish a message using a defined publisher.\n * TypeScript guarantees publisher exists for valid publisher names.\n */\n publish<TName extends InferPublisherNames<TContract>>(\n publisherName: TName,\n message: ClientInferPublisherInput<TContract, TName>,\n options?: PublishOptions,\n ): Future<Result<void, TechnicalError | MessageValidationError>> {\n const startTime = Date.now();\n // Non-null assertions safe: TypeScript guarantees these exist for valid TName\n const publisher = this.contract.publishers![publisherName as string]!;\n const { exchange, routingKey } = publisher;\n\n // Start telemetry span\n const span = startPublishSpan(this.telemetry, exchange.name, routingKey, {\n [MessagingSemanticConventions.AMQP_PUBLISHER_NAME]: String(publisherName),\n });\n\n const validateMessage = () => {\n const validationResult = publisher.message.payload[\"~standard\"].validate(message);\n return Future.fromPromise(\n validationResult instanceof Promise ? validationResult : Promise.resolve(validationResult),\n )\n .mapError((error) => new TechnicalError(`Validation failed`, error))\n .mapOkToResult((validation) => {\n if (validation.issues) {\n return Result.Error(\n new MessageValidationError(String(publisherName), validation.issues),\n );\n }\n\n return Result.Ok(validation.value);\n });\n };\n\n const publishMessage = (validatedMessage: unknown): Future<Result<void, TechnicalError>> => {\n // Merge default options with provided options\n const mergedOptions = { ...this.defaultPublishOptions, ...options };\n\n // Extract compression from merged options and create publish options without it\n const { compression, ...restOptions } = mergedOptions;\n const publishOptions: AmqpClientPublishOptions = { ...restOptions };\n\n // Prepare payload and options based on compression configuration\n const preparePayload = (): Future<Result<Buffer | unknown, TechnicalError>> => {\n if (compression) {\n // Compress the message payload\n const messageBuffer = Buffer.from(JSON.stringify(validatedMessage));\n publishOptions.contentEncoding = compression;\n\n return compressBuffer(messageBuffer, compression);\n }\n\n // No compression: use the channel's built-in JSON serialization\n return Future.value(Result.Ok(validatedMessage));\n };\n\n // Publish the prepared payload\n return preparePayload().flatMapOk((payload) =>\n this.amqpClient\n .publish(publisher.exchange.name, publisher.routingKey ?? \"\", payload, publishOptions)\n .mapOkToResult((published) => {\n if (!published) {\n return Result.Error(\n new TechnicalError(\n `Failed to publish message for publisher \"${String(publisherName)}\": Channel rejected the message (buffer full or other channel issue)`,\n ),\n );\n }\n\n this.logger?.info(\"Message published successfully\", {\n publisherName: String(publisherName),\n exchange: publisher.exchange.name,\n routingKey: publisher.routingKey,\n compressed: !!compression,\n });\n\n return Result.Ok(undefined);\n }),\n );\n };\n\n // Validate message using schema\n return validateMessage()\n .flatMapOk((validatedMessage) => publishMessage(validatedMessage))\n .tapOk(() => {\n const durationMs = Date.now() - startTime;\n endSpanSuccess(span);\n recordPublishMetric(this.telemetry, exchange.name, routingKey, true, durationMs);\n })\n .tapError((error) => {\n const durationMs = Date.now() - startTime;\n endSpanError(span, error);\n recordPublishMetric(this.telemetry, exchange.name, routingKey, false, durationMs);\n });\n }\n\n /**\n * Invoke an RPC defined via `defineRpc` and await the typed response.\n *\n * The request payload is validated against the RPC's request schema, then\n * published to the AMQP default exchange with the server's queue name as\n * routing key, `replyTo` set to `amq.rabbitmq.reply-to`, and a fresh UUID\n * `correlationId`. The returned Future resolves once a matching reply\n * arrives and validates against the response schema, or once `timeoutMs`\n * elapses (whichever comes first).\n *\n * @typeParam TName - An RPC name from `contract.rpcs`.\n * @param rpcName - The RPC name from the contract.\n * @param request - The request payload, validated against the request schema.\n * @param options - Per-call options. `timeoutMs` is required.\n *\n * @returns `Result.Ok(response)` on a successful round-trip; `Result.Error`\n * on validation, transport, timeout, or cancel.\n *\n * @example\n * ```typescript\n * const result = await client\n * .call('calculate', { a: 1, b: 2 }, { timeoutMs: 5_000 })\n * .toPromise();\n * if (result.isOk()) console.log(result.value.sum); // 3\n * ```\n */\n call<TName extends InferRpcNames<TContract>>(\n rpcName: TName,\n request: ClientInferRpcRequestInput<TContract, TName>,\n options: CallOptions,\n ): Future<\n Result<\n ClientInferRpcResponseOutput<TContract, TName>,\n TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError\n >\n > {\n type CallResult = Result<\n ClientInferRpcResponseOutput<TContract, TName>,\n TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError\n >;\n\n // setTimeout truncates fractional ms and clamps anything outside the\n // 32-bit signed integer range (~24.8 days) to 1ms, so reject those up\n // front as user errors rather than producing surprising behavior.\n const TIMEOUT_MAX_MS = 2_147_483_647;\n if (\n typeof options.timeoutMs !== \"number\" ||\n !Number.isFinite(options.timeoutMs) ||\n options.timeoutMs <= 0 ||\n options.timeoutMs > TIMEOUT_MAX_MS\n ) {\n return Future.value(\n Result.Error(\n new TechnicalError(\n `Invalid timeoutMs for RPC call to \"${String(rpcName)}\": expected a finite positive number ≤ ${TIMEOUT_MAX_MS}, got ${String(options.timeoutMs)}`,\n ),\n ) as CallResult,\n );\n }\n\n const startTime = Date.now();\n // Non-null assertion safe: TName is constrained to RPC names in the contract.\n const rpc = this.contract.rpcs![rpcName as string]!;\n const requestSchema = rpc.request.payload;\n const responseSchema = rpc.response.payload;\n const queueName = extractQueue(rpc.queue).name;\n\n // RPC publishes to the default exchange with the queue name as routing key.\n const span = startPublishSpan(this.telemetry, \"\", queueName, {\n [MessagingSemanticConventions.AMQP_PUBLISHER_NAME]: String(rpcName),\n });\n\n const correlationId = randomUUID();\n const callFuture = Future.make<CallResult>((resolve) => {\n const timer = setTimeout(() => {\n const pending = this.pendingCalls.get(correlationId);\n if (!pending) return;\n this.pendingCalls.delete(correlationId);\n resolve(Result.Error(new RpcTimeoutError(String(rpcName), options.timeoutMs)));\n }, options.timeoutMs);\n\n this.pendingCalls.set(correlationId, {\n rpcName: String(rpcName),\n responseSchema,\n resolve: resolve as PendingCall[\"resolve\"],\n timer,\n });\n });\n\n const validateRequest = (): Future<\n Result<unknown, TechnicalError | MessageValidationError>\n > => {\n // Wrap the validate call — a Standard Schema implementation may throw\n // synchronously, and that throw would otherwise escape the Future chain\n // and leave the pending-call entry/timer dangling until timeout.\n let rawValidation: ReturnType<StandardSchemaV1[\"~standard\"][\"validate\"]>;\n try {\n rawValidation = requestSchema[\"~standard\"].validate(request);\n } catch (error: unknown) {\n return Future.value(\n Result.Error<unknown, TechnicalError | MessageValidationError>(\n new TechnicalError(\"RPC request validation threw\", error),\n ),\n );\n }\n const validationPromise =\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);\n return Future.fromPromise(validationPromise)\n .mapError((error) => new TechnicalError(\"RPC request validation threw\", error))\n .mapOkToResult((validation) =>\n validation.issues\n ? Result.Error<unknown, TechnicalError | MessageValidationError>(\n new MessageValidationError(String(rpcName), validation.issues),\n )\n : Result.Ok<unknown, TechnicalError | MessageValidationError>(validation.value),\n );\n };\n\n const publishRequest = (validatedRequest: unknown): Future<Result<void, TechnicalError>> => {\n // Merge `defaultPublishOptions` (persistent, priority, headers, …) with\n // the per-call options, then layer the RPC-managed fields on top so they\n // cannot be overridden. `compression` is intentionally dropped: RPC v1\n // does not implement reply-side decompression, so request-side\n // compression would break the round-trip.\n const { compression: _ignoredCompression, ...defaultsWithoutCompression } =\n this.defaultPublishOptions;\n const publishOptions: AmqpClientPublishOptions = {\n ...defaultsWithoutCompression,\n ...options.publishOptions,\n replyTo: DIRECT_REPLY_TO,\n correlationId,\n contentType: \"application/json\",\n };\n return this.amqpClient\n .publish(\"\", queueName, validatedRequest, publishOptions)\n .mapOkToResult((published) =>\n published\n ? Result.Ok<void, TechnicalError>(undefined)\n : Result.Error<void, TechnicalError>(\n new TechnicalError(\n `Failed to publish RPC request for \"${String(rpcName)}\": channel buffer full`,\n ),\n ),\n );\n };\n\n // Validate the request, publish it, and await the reply (or timeout).\n return validateRequest()\n .flatMapOk((validated) => publishRequest(validated))\n .flatMap((preflight) => {\n if (preflight.isError()) {\n // Publish/validation failed before the request hit the broker — clean\n // up the pending entry so the timer never fires.\n const pending = this.pendingCalls.get(correlationId);\n if (pending) {\n clearTimeout(pending.timer);\n this.pendingCalls.delete(correlationId);\n }\n return Future.value(Result.Error(preflight.error) as CallResult);\n }\n return callFuture;\n })\n .tapOk(() => {\n const durationMs = Date.now() - startTime;\n endSpanSuccess(span);\n recordPublishMetric(this.telemetry, \"\", queueName, true, durationMs);\n })\n .tapError((error) => {\n const durationMs = Date.now() - startTime;\n endSpanError(span, error);\n recordPublishMetric(this.telemetry, \"\", queueName, false, durationMs);\n });\n }\n\n /**\n * Close the channel and connection. Cancels the reply consumer (if any) and\n * rejects every in-flight RPC call with `RpcCancelledError`.\n */\n close(): Future<Result<void, TechnicalError>> {\n // Reject pending calls first — once close() runs, no reply will arrive.\n for (const [, pending] of this.pendingCalls) {\n clearTimeout(pending.timer);\n pending.resolve(Result.Error(new RpcCancelledError(pending.rpcName)));\n }\n this.pendingCalls.clear();\n\n const cancelReply = this.replyConsumerTag\n ? this.amqpClient.cancel(this.replyConsumerTag).tapError((error) => {\n this.logger?.warn(\"Failed to cancel RPC reply consumer during close\", { error });\n })\n : Future.value(Result.Ok<void, TechnicalError>(undefined));\n\n return cancelReply.flatMap(() => this.amqpClient.close()).mapOk(() => undefined);\n }\n\n private waitForConnectionReady(): Future<Result<void, TechnicalError>> {\n return this.amqpClient.waitForConnect();\n }\n}\n"],"mappings":"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"}
package/docs/index.md CHANGED
@@ -170,7 +170,7 @@ Error.prepareStackTrace
170
170
 
171
171
  ### RpcCancelledError
172
172
 
173
- Defined in: [packages/client/src/errors.ts:39](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/errors.ts#L39)
173
+ Defined in: [packages/client/src/errors.ts:39](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/errors.ts#L39)
174
174
 
175
175
  Returned from any in-flight RPC call when the client is closed before the
176
176
  reply is received. The correlation map is cleared on close and every pending
@@ -188,7 +188,7 @@ caller's promise resolves with `Result.Error(RpcCancelledError)`.
188
188
  new RpcCancelledError(rpcName): RpcCancelledError;
189
189
  ```
190
190
 
191
- Defined in: [packages/client/src/errors.ts:40](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/errors.ts#L40)
191
+ Defined in: [packages/client/src/errors.ts:40](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/errors.ts#L40)
192
192
 
193
193
  ###### Parameters
194
194
 
@@ -213,7 +213,7 @@ Error.constructor
213
213
  | <a id="cause-1"></a> `cause?` | `public` | `unknown` | - | `Error.cause` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es2022.error.d.ts:24 |
214
214
  | <a id="message-1"></a> `message` | `public` | `string` | - | `Error.message` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es5.d.ts:1075 |
215
215
  | <a id="name-1"></a> `name` | `public` | `string` | - | `Error.name` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es5.d.ts:1074 |
216
- | <a id="rpcname"></a> `rpcName` | `readonly` | `string` | - | - | [packages/client/src/errors.ts:40](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/errors.ts#L40) |
216
+ | <a id="rpcname"></a> `rpcName` | `readonly` | `string` | - | - | [packages/client/src/errors.ts:40](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/errors.ts#L40) |
217
217
  | <a id="stack-1"></a> `stack?` | `public` | `string` | - | `Error.stack` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es5.d.ts:1076 |
218
218
  | <a id="stacktracelimit-1"></a> `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured _after_ the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | `Error.stackTraceLimit` | node\_modules/.pnpm/@types+node@24.12.2/node\_modules/@types/node/globals.d.ts:68 |
219
219
 
@@ -321,7 +321,7 @@ Error.prepareStackTrace
321
321
 
322
322
  ### RpcTimeoutError
323
323
 
324
- Defined in: [packages/client/src/errors.ts:23](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/errors.ts#L23)
324
+ Defined in: [packages/client/src/errors.ts:23](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/errors.ts#L23)
325
325
 
326
326
  Returned from `TypedAmqpClient.call()` when the configured `timeoutMs` elapses
327
327
  before the RPC server publishes a reply with the matching `correlationId`.
@@ -342,7 +342,7 @@ client if a logger is configured).
342
342
  new RpcTimeoutError(rpcName, timeoutMs): RpcTimeoutError;
343
343
  ```
344
344
 
345
- Defined in: [packages/client/src/errors.ts:24](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/errors.ts#L24)
345
+ Defined in: [packages/client/src/errors.ts:24](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/errors.ts#L24)
346
346
 
347
347
  ###### Parameters
348
348
 
@@ -368,9 +368,9 @@ Error.constructor
368
368
  | <a id="cause-2"></a> `cause?` | `public` | `unknown` | - | `Error.cause` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es2022.error.d.ts:24 |
369
369
  | <a id="message-2"></a> `message` | `public` | `string` | - | `Error.message` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es5.d.ts:1075 |
370
370
  | <a id="name-2"></a> `name` | `public` | `string` | - | `Error.name` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es5.d.ts:1074 |
371
- | <a id="rpcname-1"></a> `rpcName` | `readonly` | `string` | - | - | [packages/client/src/errors.ts:25](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/errors.ts#L25) |
371
+ | <a id="rpcname-1"></a> `rpcName` | `readonly` | `string` | - | - | [packages/client/src/errors.ts:25](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/errors.ts#L25) |
372
372
  | <a id="stack-2"></a> `stack?` | `public` | `string` | - | `Error.stack` | node\_modules/.pnpm/typescript@6.0.3/node\_modules/typescript/lib/lib.es5.d.ts:1076 |
373
- | <a id="timeoutms"></a> `timeoutMs` | `readonly` | `number` | - | - | [packages/client/src/errors.ts:26](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/errors.ts#L26) |
373
+ | <a id="timeoutms"></a> `timeoutMs` | `readonly` | `number` | - | - | [packages/client/src/errors.ts:26](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/errors.ts#L26) |
374
374
  | <a id="stacktracelimit-2"></a> `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured _after_ the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | `Error.stackTraceLimit` | node\_modules/.pnpm/@types+node@24.12.2/node\_modules/@types/node/globals.d.ts:68 |
375
375
 
376
376
  #### Methods
@@ -477,7 +477,7 @@ Error.prepareStackTrace
477
477
 
478
478
  ### TypedAmqpClient
479
479
 
480
- Defined in: [packages/client/src/client.ts:123](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L123)
480
+ Defined in: [packages/client/src/client.ts:124](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L124)
481
481
 
482
482
  Type-safe AMQP client for publishing messages
483
483
 
@@ -502,7 +502,7 @@ call<TName>(
502
502
  | RpcCancelledError>>;
503
503
  ```
504
504
 
505
- Defined in: [packages/client/src/client.ts:420](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L420)
505
+ Defined in: [packages/client/src/client.ts:429](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L429)
506
506
 
507
507
  Invoke an RPC defined via `defineRpc` and await the typed response.
508
508
 
@@ -553,7 +553,7 @@ if (result.isOk()) console.log(result.value.sum); // 3
553
553
  close(): Future<Result<void, TechnicalError>>;
554
554
  ```
555
555
 
556
- Defined in: [packages/client/src/client.ts:572](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L572)
556
+ Defined in: [packages/client/src/client.ts:581](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L581)
557
557
 
558
558
  Close the channel and connection. Cancels the reply consumer (if any) and
559
559
  rejects every in-flight RPC call with `RpcCancelledError`.
@@ -571,7 +571,7 @@ publish<TName>(
571
571
  options?): Future<Result<void, TechnicalError | MessageValidationError>>;
572
572
  ```
573
573
 
574
- Defined in: [packages/client/src/client.ts:300](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L300)
574
+ Defined in: [packages/client/src/client.ts:309](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L309)
575
575
 
576
576
  Publish a message using a defined publisher.
577
577
  TypeScript guarantees publisher exists for valid publisher names.
@@ -600,7 +600,7 @@ TypeScript guarantees publisher exists for valid publisher names.
600
600
  static create<TContract>(__namedParameters): Future<Result<TypedAmqpClient<TContract>, TechnicalError>>;
601
601
  ```
602
602
 
603
- Defined in: [packages/client/src/client.ts:154](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L154)
603
+ Defined in: [packages/client/src/client.ts:155](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L155)
604
604
 
605
605
  Create a type-safe AMQP client from a contract.
606
606
 
@@ -635,7 +635,7 @@ connection options, following RabbitMQ best practices.
635
635
  type CallOptions = object;
636
636
  ```
637
637
 
638
- Defined in: [packages/client/src/client.ts:103](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L103)
638
+ Defined in: [packages/client/src/client.ts:104](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L104)
639
639
 
640
640
  Per-call options for `client.call()`.
641
641
 
@@ -643,8 +643,8 @@ Per-call options for `client.call()`.
643
643
 
644
644
  | Property | Type | Description | Defined in |
645
645
  | ------ | ------ | ------ | ------ |
646
- | <a id="publishoptions"></a> `publishOptions?` | `Omit`&lt;`AmqpClientPublishOptions`, `"replyTo"` \| `"correlationId"`&gt; | Optional AMQP message properties to merge into the request. `replyTo` and `correlationId` are managed by the client and cannot be overridden. | [packages/client/src/client.ts:117](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L117) |
647
- | <a id="timeoutms-1"></a> `timeoutMs` | `number` | Maximum time in ms to wait for an RPC reply. If exceeded, the call resolves to `Result.Error<RpcTimeoutError>` and the in-memory correlation entry is cleared. A late reply arriving after the timeout is silently dropped. Required: RPC without a timeout is a footgun. | [packages/client/src/client.ts:111](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L111) |
646
+ | <a id="publishoptions"></a> `publishOptions?` | `Omit`&lt;`AmqpClientPublishOptions`, `"replyTo"` \| `"correlationId"`&gt; | Optional AMQP message properties to merge into the request. `replyTo` and `correlationId` are managed by the client and cannot be overridden. | [packages/client/src/client.ts:118](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L118) |
647
+ | <a id="timeoutms-1"></a> `timeoutMs` | `number` | Maximum time in ms to wait for an RPC reply. If exceeded, the call resolves to `Result.Error<RpcTimeoutError>` and the in-memory correlation entry is cleared. A late reply arriving after the timeout is silently dropped. Required: RPC without a timeout is a footgun. | [packages/client/src/client.ts:112](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L112) |
648
648
 
649
649
  ***
650
650
 
@@ -654,7 +654,7 @@ Per-call options for `client.call()`.
654
654
  type ClientInferPublisherInput<TContract, TName> = PublisherInferInput<InferPublisher<TContract, TName>>;
655
655
  ```
656
656
 
657
- Defined in: [packages/client/src/types.ts:43](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/types.ts#L43)
657
+ Defined in: [packages/client/src/types.ts:43](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/types.ts#L43)
658
658
 
659
659
  Input type accepted by `client.publish(name, ...)` for a specific publisher.
660
660
 
@@ -673,7 +673,7 @@ Input type accepted by `client.publish(name, ...)` for a specific publisher.
673
673
  type ClientInferRpcRequestInput<TContract, TName> = InferRpc<TContract, TName> extends RpcDefinition<infer TRequest, MessageDefinition> ? TRequest extends MessageDefinition ? InferSchemaInput<TRequest["payload"]> : never : never;
674
674
  ```
675
675
 
676
- Defined in: [packages/client/src/types.ts:61](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/types.ts#L61)
676
+ Defined in: [packages/client/src/types.ts:61](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/types.ts#L61)
677
677
 
678
678
  Input type accepted by `client.call(name, request, ...)`.
679
679
 
@@ -692,7 +692,7 @@ Input type accepted by `client.call(name, request, ...)`.
692
692
  type ClientInferRpcResponseOutput<TContract, TName> = InferRpc<TContract, TName> extends RpcDefinition<MessageDefinition, infer TResponse> ? TResponse extends MessageDefinition ? InferSchemaOutput<TResponse["payload"]> : never : never;
693
693
  ```
694
694
 
695
- Defined in: [packages/client/src/types.ts:74](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/types.ts#L74)
695
+ Defined in: [packages/client/src/types.ts:74](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/types.ts#L74)
696
696
 
697
697
  Output (validated) response type returned by `client.call(name, ...)`.
698
698
 
@@ -711,7 +711,7 @@ Output (validated) response type returned by `client.call(name, ...)`.
711
711
  type CreateClientOptions<TContract> = object;
712
712
  ```
713
713
 
714
- Defined in: [packages/client/src/client.ts:74](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L74)
714
+ Defined in: [packages/client/src/client.ts:75](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L75)
715
715
 
716
716
  Options for creating a client
717
717
 
@@ -725,13 +725,13 @@ Options for creating a client
725
725
 
726
726
  | Property | Type | Description | Defined in |
727
727
  | ------ | ------ | ------ | ------ |
728
- | <a id="connectionoptions"></a> `connectionOptions?` | `AmqpConnectionManagerOptions` | - | [packages/client/src/client.ts:77](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L77) |
729
- | <a id="connecttimeoutms"></a> `connectTimeoutMs?` | `number` | Maximum time in ms to wait for the AMQP connection to become ready before `create()` resolves to `Result.Error<TechnicalError>`. Without this option, `create()` waits forever the underlying amqp-connection-manager retries indefinitely. | [packages/client/src/client.ts:97](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L97) |
730
- | <a id="contract"></a> `contract` | `TContract` | - | [packages/client/src/client.ts:75](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L75) |
731
- | <a id="defaultpublishoptions"></a> `defaultPublishOptions?` | [`PublishOptions`](#publishoptions-1) | Default publish options that will be applied to all publish operations. These can be overridden by options passed to the publish method. By default, persistent is set to true for message durability. | [packages/client/src/client.ts:90](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L90) |
732
- | <a id="logger"></a> `logger?` | `Logger` | - | [packages/client/src/client.ts:78](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L78) |
733
- | <a id="telemetry"></a> `telemetry?` | `TelemetryProvider` | Optional telemetry provider for tracing and metrics. If not provided, uses the default provider which attempts to load OpenTelemetry. OpenTelemetry instrumentation is automatically enabled if @opentelemetry/api is installed. | [packages/client/src/client.ts:84](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L84) |
734
- | <a id="urls"></a> `urls` | `ConnectionUrl`[] | - | [packages/client/src/client.ts:76](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L76) |
728
+ | <a id="connectionoptions"></a> `connectionOptions?` | `AmqpConnectionManagerOptions` | - | [packages/client/src/client.ts:78](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L78) |
729
+ | <a id="connecttimeoutms"></a> `connectTimeoutMs?` | `number` \| `null` | Maximum time in ms to wait for the AMQP connection to become ready before `create()` resolves to `Result.Error<TechnicalError>`. Defaults to 30s (the [AmqpClient](https://btravers.github.io/amqp-contract/api/core#amqpclient)'s `DEFAULT_CONNECT_TIMEOUT_MS`). Pass `null` to disable the timeout and let amqp-connection-manager retry indefinitely. | [packages/client/src/client.ts:98](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L98) |
730
+ | <a id="contract"></a> `contract` | `TContract` | - | [packages/client/src/client.ts:76](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L76) |
731
+ | <a id="defaultpublishoptions"></a> `defaultPublishOptions?` | [`PublishOptions`](#publishoptions-1) | Default publish options that will be applied to all publish operations. These can be overridden by options passed to the publish method. By default, persistent is set to true for message durability. | [packages/client/src/client.ts:91](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L91) |
732
+ | <a id="logger"></a> `logger?` | `Logger` | - | [packages/client/src/client.ts:79](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L79) |
733
+ | <a id="telemetry"></a> `telemetry?` | `TelemetryProvider` | Optional telemetry provider for tracing and metrics. If not provided, uses the default provider which attempts to load OpenTelemetry. OpenTelemetry instrumentation is automatically enabled if @opentelemetry/api is installed. | [packages/client/src/client.ts:85](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L85) |
734
+ | <a id="urls"></a> `urls` | `ConnectionUrl`[] | - | [packages/client/src/client.ts:77](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L77) |
735
735
 
736
736
  ***
737
737
 
@@ -741,7 +741,7 @@ Options for creating a client
741
741
  type PublishOptions = AmqpClientPublishOptions & object;
742
742
  ```
743
743
 
744
- Defined in: [packages/client/src/client.ts:62](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L62)
744
+ Defined in: [packages/client/src/client.ts:63](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L63)
745
745
 
746
746
  Publish options that extend amqp-client's PublishOptions with optional compression support.
747
747
 
@@ -749,4 +749,4 @@ Publish options that extend amqp-client's PublishOptions with optional compressi
749
749
 
750
750
  | Name | Type | Description | Defined in |
751
751
  | ------ | ------ | ------ | ------ |
752
- | `compression?` | `CompressionAlgorithm` | Optional compression algorithm to use for the message payload. When specified, the message will be compressed using the chosen algorithm and the contentEncoding header will be set automatically. | [packages/client/src/client.ts:68](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/client/src/client.ts#L68) |
752
+ | `compression?` | `CompressionAlgorithm` | Optional compression algorithm to use for the message payload. When specified, the message will be compressed using the chosen algorithm and the contentEncoding header will be set automatically. | [packages/client/src/client.ts:69](https://github.com/btravers/amqp-contract/blob/cb947e699b512bf57d7ddfcc5ae6e2531e8dfa43/packages/client/src/client.ts#L69) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amqp-contract/client",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Client utilities for publishing messages using amqp-contract",
5
5
  "keywords": [
6
6
  "amqp",
@@ -53,8 +53,8 @@
53
53
  "@standard-schema/spec": "1.1.0",
54
54
  "@swan-io/boxed": "3.2.1",
55
55
  "ts-pattern": "5.9.0",
56
- "@amqp-contract/contract": "0.22.0",
57
- "@amqp-contract/core": "0.22.0"
56
+ "@amqp-contract/contract": "0.23.0",
57
+ "@amqp-contract/core": "0.23.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/amqplib": "0.10.8",
@@ -68,7 +68,7 @@
68
68
  "typescript": "6.0.3",
69
69
  "vitest": "4.1.5",
70
70
  "zod": "4.3.6",
71
- "@amqp-contract/testing": "0.22.0",
71
+ "@amqp-contract/testing": "0.23.0",
72
72
  "@amqp-contract/tsconfig": "0.1.0",
73
73
  "@amqp-contract/typedoc": "0.1.0"
74
74
  },