@amqp-contract/client 0.23.1 → 0.24.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.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { extractQueue } from "@amqp-contract/contract";
2
2
  import { AmqpClient, MessageValidationError, MessagingSemanticConventions, TechnicalError, defaultTelemetryProvider, endSpanError, endSpanSuccess, recordLateRpcReply, recordPublishMetric, startPublishSpan } from "@amqp-contract/core";
3
- import { Future, Result } from "@swan-io/boxed";
3
+ import { ResultAsync, err, errAsync, ok, okAsync } from "neverthrow";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { deflate, gzip } from "node:zlib";
6
- import { match } from "ts-pattern";
7
6
  import { promisify } from "node:util";
7
+ import { match } from "ts-pattern";
8
8
  //#region src/compression.ts
9
9
  const gzipAsync = promisify(gzip);
10
10
  const deflateAsync = promisify(deflate);
@@ -13,12 +13,12 @@ const deflateAsync = promisify(deflate);
13
13
  *
14
14
  * @param buffer - The buffer to compress
15
15
  * @param algorithm - The compression algorithm to use
16
- * @returns A Future with the compressed buffer or a TechnicalError
16
+ * @returns A ResultAsync resolving to the compressed buffer or a TechnicalError
17
17
  *
18
18
  * @internal
19
19
  */
20
20
  function compressBuffer(buffer, algorithm) {
21
- return match(algorithm).with("gzip", () => Future.fromPromise(gzipAsync(buffer)).mapError((error) => new TechnicalError("Failed to compress with gzip", error))).with("deflate", () => Future.fromPromise(deflateAsync(buffer)).mapError((error) => new TechnicalError("Failed to compress with deflate", error))).exhaustive();
21
+ return match(algorithm).with("gzip", () => ResultAsync.fromPromise(gzipAsync(buffer), (error) => new TechnicalError("Failed to compress with gzip", error))).with("deflate", () => ResultAsync.fromPromise(deflateAsync(buffer), (error) => new TechnicalError("Failed to compress with deflate", error))).exhaustive();
22
22
  }
23
23
  //#endregion
24
24
  //#region src/errors.ts
@@ -49,7 +49,7 @@ var RpcTimeoutError = class extends Error {
49
49
  /**
50
50
  * Returned from any in-flight RPC call when the client is closed before the
51
51
  * reply is received. The correlation map is cleared on close and every pending
52
- * caller's promise resolves with `Result.Error(RpcCancelledError)`.
52
+ * caller's promise resolves with `err(RpcCancelledError)`.
53
53
  */
54
54
  var RpcCancelledError = class extends Error {
55
55
  constructor(rpcName) {
@@ -110,12 +110,14 @@ var TypedAmqpClient = class TypedAmqpClient {
110
110
  persistent: true,
111
111
  ...defaultPublishOptions
112
112
  }, logger, telemetry ?? defaultTelemetryProvider);
113
- return client.waitForConnectionReady().flatMapOk(() => client.setupReplyConsumerIfNeeded()).flatMap((result) => result.match({
114
- Ok: () => Future.value(Result.Ok(client)),
115
- Error: (error) => client.close().tapError((closeError) => {
116
- logger?.warn("Failed to close client after connection failure", { error: closeError });
117
- }).map(() => Result.Error(error))
118
- }));
113
+ const setup = client.waitForConnectionReady().andThen(() => client.setupReplyConsumerIfNeeded());
114
+ return new ResultAsync((async () => {
115
+ const setupResult = await setup;
116
+ if (setupResult.isOk()) return ok(client);
117
+ const closeResult = await client.close();
118
+ if (closeResult.isErr()) logger?.warn("Failed to close client after connection failure", { error: closeResult.error });
119
+ return err(setupResult.error);
120
+ })());
119
121
  }
120
122
  /**
121
123
  * If the contract has any RPC entry, subscribe to `amq.rabbitmq.reply-to`
@@ -124,10 +126,10 @@ var TypedAmqpClient = class TypedAmqpClient {
124
126
  */
125
127
  setupReplyConsumerIfNeeded() {
126
128
  const rpcs = this.contract.rpcs ?? {};
127
- if (Object.keys(rpcs).length === 0) return Future.value(Result.Ok(void 0));
128
- return this.amqpClient.consume(DIRECT_REPLY_TO, (msg) => this.handleRpcReply(msg), { noAck: true }).tapOk((tag) => {
129
+ if (Object.keys(rpcs).length === 0) return okAsync(void 0);
130
+ return this.amqpClient.consume(DIRECT_REPLY_TO, (msg) => this.handleRpcReply(msg), { noAck: true }).andTee((tag) => {
129
131
  this.replyConsumerTag = tag;
130
- }).mapOk(() => void 0);
132
+ }).map(() => void 0);
131
133
  }
132
134
  /**
133
135
  * Demultiplex an RPC reply by `correlationId`, validate the body against the
@@ -158,28 +160,28 @@ var TypedAmqpClient = class TypedAmqpClient {
158
160
  try {
159
161
  parsed = JSON.parse(msg.content.toString());
160
162
  } catch (error) {
161
- pending.resolve(Result.Error(new TechnicalError(`Failed to parse RPC reply JSON for "${pending.rpcName}"`, error)));
163
+ pending.resolve(err(new TechnicalError(`Failed to parse RPC reply JSON for "${pending.rpcName}"`, error)));
162
164
  return;
163
165
  }
164
166
  let rawValidation;
165
167
  try {
166
168
  rawValidation = pending.responseSchema["~standard"].validate(parsed);
167
169
  } catch (error) {
168
- pending.resolve(Result.Error(new TechnicalError(`RPC reply validation threw for "${pending.rpcName}"`, error)));
170
+ pending.resolve(err(new TechnicalError(`RPC reply validation threw for "${pending.rpcName}"`, error)));
169
171
  return;
170
172
  }
171
173
  (rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation)).then((validation) => {
172
174
  if (validation.issues) {
173
- pending.resolve(Result.Error(new MessageValidationError(pending.rpcName, validation.issues)));
175
+ pending.resolve(err(new MessageValidationError(pending.rpcName, validation.issues)));
174
176
  return;
175
177
  }
176
- pending.resolve(Result.Ok(validation.value));
178
+ pending.resolve(ok(validation.value));
177
179
  }, (error) => {
178
- pending.resolve(Result.Error(new TechnicalError(`RPC reply validation threw for "${pending.rpcName}"`, error)));
180
+ pending.resolve(err(new TechnicalError(`RPC reply validation threw for "${pending.rpcName}"`, error)));
179
181
  });
180
182
  }
181
183
  /**
182
- * Publish a message using a defined publisher
184
+ * Publish a message using a defined publisher.
183
185
  *
184
186
  * @param publisherName - The name of the publisher to use
185
187
  * @param message - The message to publish
@@ -189,12 +191,6 @@ var TypedAmqpClient = class TypedAmqpClient {
189
191
  * If `options.compression` is specified, the message will be compressed before publishing
190
192
  * and the `contentEncoding` property will be set automatically. Any `contentEncoding`
191
193
  * value already in options will be overwritten by the compression algorithm.
192
- *
193
- * @returns Result.Ok(void) on success, or Result.Error with specific error on failure
194
- */
195
- /**
196
- * Publish a message using a defined publisher.
197
- * TypeScript guarantees publisher exists for valid publisher names.
198
194
  */
199
195
  publish(publisherName, message, options) {
200
196
  const startTime = Date.now();
@@ -203,9 +199,10 @@ var TypedAmqpClient = class TypedAmqpClient {
203
199
  const span = startPublishSpan(this.telemetry, exchange.name, routingKey, { [MessagingSemanticConventions.AMQP_PUBLISHER_NAME]: String(publisherName) });
204
200
  const validateMessage = () => {
205
201
  const validationResult = publisher.message.payload["~standard"].validate(message);
206
- return Future.fromPromise(validationResult instanceof Promise ? validationResult : Promise.resolve(validationResult)).mapError((error) => new TechnicalError(`Validation failed`, error)).mapOkToResult((validation) => {
207
- if (validation.issues) return Result.Error(new MessageValidationError(String(publisherName), validation.issues));
208
- return Result.Ok(validation.value);
202
+ const promise = validationResult instanceof Promise ? validationResult : Promise.resolve(validationResult);
203
+ return ResultAsync.fromPromise(promise, (error) => new TechnicalError("Validation failed", error)).andThen((validation) => {
204
+ if (validation.issues) return err(new MessageValidationError(String(publisherName), validation.issues));
205
+ return ok(validation.value);
209
206
  });
210
207
  };
211
208
  const publishMessage = (validatedMessage) => {
@@ -220,24 +217,24 @@ var TypedAmqpClient = class TypedAmqpClient {
220
217
  publishOptions.contentEncoding = compression;
221
218
  return compressBuffer(messageBuffer, compression);
222
219
  }
223
- return Future.value(Result.Ok(validatedMessage));
220
+ return okAsync(validatedMessage);
224
221
  };
225
- return preparePayload().flatMapOk((payload) => this.amqpClient.publish(publisher.exchange.name, publisher.routingKey ?? "", payload, publishOptions).mapOkToResult((published) => {
226
- if (!published) return Result.Error(new TechnicalError(`Failed to publish message for publisher "${String(publisherName)}": Channel rejected the message (buffer full or other channel issue)`));
222
+ return preparePayload().andThen((payload) => this.amqpClient.publish(publisher.exchange.name, publisher.routingKey ?? "", payload, publishOptions).andThen((published) => {
223
+ if (!published) return err(new TechnicalError(`Failed to publish message for publisher "${String(publisherName)}": Channel rejected the message (buffer full or other channel issue)`));
227
224
  this.logger?.info("Message published successfully", {
228
225
  publisherName: String(publisherName),
229
226
  exchange: publisher.exchange.name,
230
227
  routingKey: publisher.routingKey,
231
228
  compressed: !!compression
232
229
  });
233
- return Result.Ok(void 0);
230
+ return ok(void 0);
234
231
  }));
235
232
  };
236
- return validateMessage().flatMapOk((validatedMessage) => publishMessage(validatedMessage)).tapOk(() => {
233
+ return validateMessage().andThen((validatedMessage) => publishMessage(validatedMessage)).andTee(() => {
237
234
  const durationMs = Date.now() - startTime;
238
235
  endSpanSuccess(span);
239
236
  recordPublishMetric(this.telemetry, exchange.name, routingKey, true, durationMs);
240
- }).tapError((error) => {
237
+ }).orTee((error) => {
241
238
  const durationMs = Date.now() - startTime;
242
239
  endSpanError(span, error);
243
240
  recordPublishMetric(this.telemetry, exchange.name, routingKey, false, durationMs);
@@ -249,29 +246,19 @@ var TypedAmqpClient = class TypedAmqpClient {
249
246
  * The request payload is validated against the RPC's request schema, then
250
247
  * published to the AMQP default exchange with the server's queue name as
251
248
  * routing key, `replyTo` set to `amq.rabbitmq.reply-to`, and a fresh UUID
252
- * `correlationId`. The returned Future resolves once a matching reply
249
+ * `correlationId`. The returned ResultAsync resolves once a matching reply
253
250
  * arrives and validates against the response schema, or once `timeoutMs`
254
251
  * elapses (whichever comes first).
255
252
  *
256
- * @typeParam TName - An RPC name from `contract.rpcs`.
257
- * @param rpcName - The RPC name from the contract.
258
- * @param request - The request payload, validated against the request schema.
259
- * @param options - Per-call options. `timeoutMs` is required.
260
- *
261
- * @returns `Result.Ok(response)` on a successful round-trip; `Result.Error`
262
- * on validation, transport, timeout, or cancel.
263
- *
264
253
  * @example
265
254
  * ```typescript
266
- * const result = await client
267
- * .call('calculate', { a: 1, b: 2 }, { timeoutMs: 5_000 })
268
- * .toPromise();
255
+ * const result = await client.call('calculate', { a: 1, b: 2 }, { timeoutMs: 5_000 });
269
256
  * if (result.isOk()) console.log(result.value.sum); // 3
270
257
  * ```
271
258
  */
272
259
  call(rpcName, request, options) {
273
260
  const TIMEOUT_MAX_MS = 2147483647;
274
- if (typeof options.timeoutMs !== "number" || !Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > TIMEOUT_MAX_MS) return Future.value(Result.Error(new TechnicalError(`Invalid timeoutMs for RPC call to "${String(rpcName)}": expected a finite positive number ≤ ${TIMEOUT_MAX_MS}, got ${String(options.timeoutMs)}`)));
261
+ if (typeof options.timeoutMs !== "number" || !Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > TIMEOUT_MAX_MS) return errAsync(new TechnicalError(`Invalid timeoutMs for RPC call to "${String(rpcName)}": expected a finite positive number ≤ ${TIMEOUT_MAX_MS}, got ${String(options.timeoutMs)}`));
275
262
  const startTime = Date.now();
276
263
  const rpc = this.contract.rpcs[rpcName];
277
264
  const requestSchema = rpc.request.payload;
@@ -279,28 +266,30 @@ var TypedAmqpClient = class TypedAmqpClient {
279
266
  const queueName = extractQueue(rpc.queue).name;
280
267
  const span = startPublishSpan(this.telemetry, "", queueName, { [MessagingSemanticConventions.AMQP_PUBLISHER_NAME]: String(rpcName) });
281
268
  const correlationId = randomUUID();
282
- const callFuture = Future.make((resolve) => {
283
- const timer = setTimeout(() => {
284
- if (!this.pendingCalls.get(correlationId)) return;
285
- this.pendingCalls.delete(correlationId);
286
- resolve(Result.Error(new RpcTimeoutError(String(rpcName), options.timeoutMs)));
287
- }, options.timeoutMs);
288
- this.pendingCalls.set(correlationId, {
289
- rpcName: String(rpcName),
290
- responseSchema,
291
- resolve,
292
- timer
293
- });
269
+ let resolveCall;
270
+ const callResultAsync = new ResultAsync(new Promise((res) => {
271
+ resolveCall = res;
272
+ }));
273
+ const timer = setTimeout(() => {
274
+ if (!this.pendingCalls.has(correlationId)) return;
275
+ this.pendingCalls.delete(correlationId);
276
+ resolveCall(err(new RpcTimeoutError(String(rpcName), options.timeoutMs)));
277
+ }, options.timeoutMs);
278
+ this.pendingCalls.set(correlationId, {
279
+ rpcName: String(rpcName),
280
+ responseSchema,
281
+ resolve: resolveCall,
282
+ timer
294
283
  });
295
284
  const validateRequest = () => {
296
285
  let rawValidation;
297
286
  try {
298
287
  rawValidation = requestSchema["~standard"].validate(request);
299
288
  } catch (error) {
300
- return Future.value(Result.Error(new TechnicalError("RPC request validation threw", error)));
289
+ return errAsync(new TechnicalError("RPC request validation threw", error));
301
290
  }
302
291
  const validationPromise = rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);
303
- return Future.fromPromise(validationPromise).mapError((error) => new TechnicalError("RPC request validation threw", error)).mapOkToResult((validation) => validation.issues ? Result.Error(new MessageValidationError(String(rpcName), validation.issues)) : Result.Ok(validation.value));
292
+ return ResultAsync.fromPromise(validationPromise, (error) => new TechnicalError("RPC request validation threw", error)).andThen((validation) => validation.issues ? err(new MessageValidationError(String(rpcName), validation.issues)) : ok(validation.value));
304
293
  };
305
294
  const publishRequest = (validatedRequest) => {
306
295
  const { compression: _ignoredCompression, ...defaultsWithoutCompression } = this.defaultPublishOptions;
@@ -311,23 +300,19 @@ var TypedAmqpClient = class TypedAmqpClient {
311
300
  correlationId,
312
301
  contentType: "application/json"
313
302
  };
314
- return this.amqpClient.publish("", queueName, validatedRequest, publishOptions).mapOkToResult((published) => published ? Result.Ok(void 0) : Result.Error(new TechnicalError(`Failed to publish RPC request for "${String(rpcName)}": channel buffer full`)));
303
+ return this.amqpClient.publish("", queueName, validatedRequest, publishOptions).andThen((published) => published ? ok(void 0) : err(new TechnicalError(`Failed to publish RPC request for "${String(rpcName)}": channel buffer full`)));
315
304
  };
316
- return validateRequest().flatMapOk((validated) => publishRequest(validated)).flatMap((preflight) => {
317
- if (preflight.isError()) {
318
- const pending = this.pendingCalls.get(correlationId);
319
- if (pending) {
320
- clearTimeout(pending.timer);
321
- this.pendingCalls.delete(correlationId);
322
- }
323
- return Future.value(Result.Error(preflight.error));
305
+ return validateRequest().andThen((validated) => publishRequest(validated)).andThen(() => callResultAsync).orElse((error) => {
306
+ if (this.pendingCalls.has(correlationId)) {
307
+ clearTimeout(timer);
308
+ this.pendingCalls.delete(correlationId);
324
309
  }
325
- return callFuture;
326
- }).tapOk(() => {
310
+ return errAsync(error);
311
+ }).andTee(() => {
327
312
  const durationMs = Date.now() - startTime;
328
313
  endSpanSuccess(span);
329
314
  recordPublishMetric(this.telemetry, "", queueName, true, durationMs);
330
- }).tapError((error) => {
315
+ }).orTee((error) => {
331
316
  const durationMs = Date.now() - startTime;
332
317
  endSpanError(span, error);
333
318
  recordPublishMetric(this.telemetry, "", queueName, false, durationMs);
@@ -340,12 +325,13 @@ var TypedAmqpClient = class TypedAmqpClient {
340
325
  close() {
341
326
  for (const [, pending] of this.pendingCalls) {
342
327
  clearTimeout(pending.timer);
343
- pending.resolve(Result.Error(new RpcCancelledError(pending.rpcName)));
328
+ pending.resolve(err(new RpcCancelledError(pending.rpcName)));
344
329
  }
345
330
  this.pendingCalls.clear();
346
- return (this.replyConsumerTag ? this.amqpClient.cancel(this.replyConsumerTag).tapError((error) => {
331
+ return (this.replyConsumerTag ? this.amqpClient.cancel(this.replyConsumerTag).orElse((error) => {
347
332
  this.logger?.warn("Failed to cancel RPC reply consumer during close", { error });
348
- }) : Future.value(Result.Ok(void 0))).flatMap(() => this.amqpClient.close()).mapOk(() => void 0);
333
+ return ok(void 0);
334
+ }) : okAsync(void 0)).andThen(() => this.amqpClient.close());
349
335
  }
350
336
  waitForConnectionReady() {
351
337
  return this.amqpClient.waitForConnect();
@@ -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 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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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/compression.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["import type { CompressionAlgorithm } from \"@amqp-contract/contract\";\nimport { TechnicalError } from \"@amqp-contract/core\";\nimport { ResultAsync } from \"neverthrow\";\nimport { deflate, gzip } from \"node:zlib\";\nimport { promisify } from \"node:util\";\nimport { match } from \"ts-pattern\";\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 ResultAsync resolving to the compressed buffer or a TechnicalError\n *\n * @internal\n */\nexport function compressBuffer(\n buffer: Buffer,\n algorithm: CompressionAlgorithm,\n): ResultAsync<Buffer, TechnicalError> {\n return match(algorithm)\n .with(\"gzip\", () =>\n ResultAsync.fromPromise(\n gzipAsync(buffer),\n (error) => new TechnicalError(\"Failed to compress with gzip\", error),\n ),\n )\n .with(\"deflate\", () =>\n ResultAsync.fromPromise(\n deflateAsync(buffer),\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 `err(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 type { AmqpConnectionManagerOptions, ConnectionUrl } from \"amqp-connection-manager\";\nimport { err, errAsync, ok, okAsync, Result, ResultAsync } from \"neverthrow\";\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 an `err(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 `err(RpcTimeoutError)` and the in-memory correlation entry is cleared.\n * 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>): ResultAsync<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 const setup = client\n .waitForConnectionReady()\n .andThen(() => client.setupReplyConsumerIfNeeded());\n\n return new ResultAsync<TypedAmqpClient<TContract>, TechnicalError>(\n (async () => {\n const setupResult = await setup;\n if (setupResult.isOk()) {\n return ok(client);\n }\n const closeResult = await client.close();\n if (closeResult.isErr()) {\n logger?.warn(\"Failed to close client after connection failure\", {\n error: closeResult.error,\n });\n }\n return err(setupResult.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(): ResultAsync<void, TechnicalError> {\n const rpcs = this.contract.rpcs ?? {};\n if (Object.keys(rpcs).length === 0) {\n return okAsync(undefined);\n }\n\n return this.amqpClient\n .consume(DIRECT_REPLY_TO, (msg) => this.handleRpcReply(msg), { noAck: true })\n .andTee((tag) => {\n this.replyConsumerTag = tag;\n })\n .map(() => 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 err(new TechnicalError(`Failed to parse RPC reply JSON for \"${pending.rpcName}\"`, error)),\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 err(new TechnicalError(`RPC reply validation threw for \"${pending.rpcName}\"`, error)),\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(err(new MessageValidationError(pending.rpcName, validation.issues)));\n return;\n }\n pending.resolve(ok(validation.value));\n },\n (error: unknown) => {\n pending.resolve(\n err(new TechnicalError(`RPC reply validation threw for \"${pending.rpcName}\"`, error)),\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 publish<TName extends InferPublisherNames<TContract>>(\n publisherName: TName,\n message: ClientInferPublisherInput<TContract, TName>,\n options?: PublishOptions,\n ): ResultAsync<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 = (): ResultAsync<unknown, TechnicalError | MessageValidationError> => {\n const validationResult = publisher.message.payload[\"~standard\"].validate(message);\n const promise =\n validationResult instanceof Promise ? validationResult : Promise.resolve(validationResult);\n return ResultAsync.fromPromise(\n promise,\n (error): TechnicalError | MessageValidationError =>\n new TechnicalError(\"Validation failed\", error),\n ).andThen((validation) => {\n if (validation.issues) {\n return err<unknown, TechnicalError | MessageValidationError>(\n new MessageValidationError(String(publisherName), validation.issues),\n );\n }\n return ok<unknown, TechnicalError | MessageValidationError>(validation.value);\n });\n };\n\n const publishMessage = (validatedMessage: unknown): ResultAsync<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 = (): ResultAsync<Buffer | unknown, TechnicalError> => {\n if (compression) {\n // Compress the message payload\n const messageBuffer = Buffer.from(JSON.stringify(validatedMessage));\n publishOptions.contentEncoding = compression;\n return compressBuffer(messageBuffer, compression);\n }\n\n // No compression: use the channel's built-in JSON serialization\n return okAsync(validatedMessage);\n };\n\n return preparePayload().andThen((payload) =>\n this.amqpClient\n .publish(publisher.exchange.name, publisher.routingKey ?? \"\", payload, publishOptions)\n .andThen((published) => {\n if (!published) {\n return err<void, TechnicalError>(\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 ok<void, TechnicalError>(undefined);\n }),\n );\n };\n\n return validateMessage()\n .andThen((validatedMessage) => publishMessage(validatedMessage))\n .andTee(() => {\n const durationMs = Date.now() - startTime;\n endSpanSuccess(span);\n recordPublishMetric(this.telemetry, exchange.name, routingKey, true, durationMs);\n })\n .orTee((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 ResultAsync resolves once a matching reply\n * arrives and validates against the response schema, or once `timeoutMs`\n * elapses (whichever comes first).\n *\n * @example\n * ```typescript\n * const result = await client.call('calculate', { a: 1, b: 2 }, { timeoutMs: 5_000 });\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 ): ResultAsync<\n ClientInferRpcResponseOutput<TContract, TName>,\n TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError\n > {\n type ResponseType = ClientInferRpcResponseOutput<TContract, TName>;\n type CallError = TechnicalError | MessageValidationError | RpcTimeoutError | RpcCancelledError;\n type CallResult = Result<ResponseType, CallError>;\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 errAsync<ResponseType, CallError>(\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 );\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\n // Set up the reply future + pending entry up front so a reply that arrives\n // racing the publish round-trip can find a slot. Cleanup on preflight\n // failure happens in the `.orElse` below.\n let resolveCall!: (result: CallResult) => void;\n const callPromise = new Promise<CallResult>((res) => {\n resolveCall = res;\n });\n const callResultAsync = new ResultAsync<ResponseType, CallError>(callPromise);\n\n const timer = setTimeout(() => {\n if (!this.pendingCalls.has(correlationId)) return;\n this.pendingCalls.delete(correlationId);\n resolveCall(err(new RpcTimeoutError(String(rpcName), options.timeoutMs)));\n }, options.timeoutMs);\n\n this.pendingCalls.set(correlationId, {\n rpcName: String(rpcName),\n responseSchema,\n resolve: resolveCall as PendingCall[\"resolve\"],\n timer,\n });\n\n const validateRequest = (): ResultAsync<unknown, TechnicalError | MessageValidationError> => {\n // Wrap the validate call — a Standard Schema implementation may throw\n // synchronously, and that throw would otherwise escape the chain and\n // 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 errAsync<unknown, TechnicalError | MessageValidationError>(\n new TechnicalError(\"RPC request validation threw\", error),\n );\n }\n const validationPromise =\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);\n return ResultAsync.fromPromise(\n validationPromise,\n (error): TechnicalError | MessageValidationError =>\n new TechnicalError(\"RPC request validation threw\", error),\n ).andThen((validation) =>\n validation.issues\n ? err<unknown, TechnicalError | MessageValidationError>(\n new MessageValidationError(String(rpcName), validation.issues),\n )\n : ok<unknown, TechnicalError | MessageValidationError>(validation.value),\n );\n };\n\n const publishRequest = (validatedRequest: unknown): ResultAsync<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 .andThen((published) =>\n published\n ? ok<void, TechnicalError>(undefined)\n : err<void, TechnicalError>(\n new TechnicalError(\n `Failed to publish RPC request for \"${String(rpcName)}\": channel buffer full`,\n ),\n ),\n );\n };\n\n return validateRequest()\n .andThen((validated) => publishRequest(validated))\n .andThen(() => callResultAsync)\n .orElse((error: CallError) => {\n // If preflight failed (validate or publish), the pending entry still\n // exists and the timer is alive. Clean both up so the call doesn't\n // leak. Timer-fired errors and reply-resolved errors have already\n // cleaned the entry, so the .has() check guards against double cleanup.\n if (this.pendingCalls.has(correlationId)) {\n clearTimeout(timer);\n this.pendingCalls.delete(correlationId);\n }\n return errAsync<ResponseType, CallError>(error);\n })\n .andTee(() => {\n const durationMs = Date.now() - startTime;\n endSpanSuccess(span);\n recordPublishMetric(this.telemetry, \"\", queueName, true, durationMs);\n })\n .orTee((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(): ResultAsync<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(err(new RpcCancelledError(pending.rpcName)));\n }\n this.pendingCalls.clear();\n\n const cancelReply: ResultAsync<void, TechnicalError> = this.replyConsumerTag\n ? this.amqpClient.cancel(this.replyConsumerTag).orElse((error) => {\n this.logger?.warn(\"Failed to cancel RPC reply consumer during close\", { error });\n return ok<void, TechnicalError>(undefined);\n })\n : okAsync<void, TechnicalError>(undefined);\n\n return cancelReply.andThen(() => this.amqpClient.close());\n }\n\n private waitForConnectionReady(): ResultAsync<void, TechnicalError> {\n return this.amqpClient.waitForConnect();\n }\n}\n"],"mappings":"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"}