@linagora/rabbitmq-client 0.4.0 → 0.5.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.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/logger.ts","../src/semaphore.ts"],"sourcesContent":["import amqp from 'amqplib'\nimport type {\n ILogger,\n PublishOptions,\n RabbitMQClientOptions,\n RabbitMQHooks,\n RabbitMQMessage,\n RabbitMQMessageHandler,\n RabbitMQMessageProperties,\n RabbitMQSubscription,\n SubscribeOptions,\n} from './types.js'\nimport { defaultLogger, forLibraryCalls } from './logger.js'\nimport { Semaphore } from './semaphore.js'\n\nconst DEFAULT_PREFETCH = 10\nconst MAX_PUBLISH_RETRY_DELAY_MS = 60_000\n\nconst DEFAULTS = {\n maxRetries: 3,\n retryDelay: 1000,\n connectionRetryDelay: 5000,\n initMaxAttempts: 5,\n publishMaxAttempts: 5,\n prefetch: DEFAULT_PREFETCH,\n closeTimeout: 5000,\n} as const\n\n/** A `mandatory` publish the broker could route to no queue. */\nexport class UnroutableMessageError extends Error {\n constructor(\n readonly exchange: string,\n readonly routingKey: string,\n ) {\n super(`No queue is bound to receive ${exchange || '(default exchange)'}/${routingKey}`)\n this.name = 'UnroutableMessageError'\n }\n}\n\n/**\n * Production-grade RabbitMQ client with confirm channels, automatic DLQ\n * infrastructure, reconnection with subscription restoration, and\n * exponential-backoff publishing.\n *\n * @example\n * ```ts\n * const client = new RabbitMQClient({ url: 'amqp://localhost' })\n * await client.init()\n * await client.publish('events', 'user.created', { userId: '123' })\n * await client.subscribe('events', 'user.created', 'user-service', handler)\n * ```\n */\nexport class RabbitMQClient {\n private connection: amqp.ChannelModel | null = null\n private channel: amqp.ConfirmChannel | null = null\n private connected = false\n private subscriptions: RabbitMQSubscription[] = []\n private readonly options: Required<Omit<RabbitMQClientOptions, 'logger' | 'hooks'>>\n private readonly logger: ILogger\n private readonly hooks: RabbitMQHooks\n private initializationPromise: Promise<void> | null = null\n private reconnectionPromise: Promise<void> | null = null\n private assertedExchanges = new Set<string>()\n // One per channel, so a late return on a replaced channel reaches no waiter.\n private returnWaiters = new Set<(msg: amqp.Message) => void>()\n private consumerTags = new Map<string, string>()\n // Per-queue concurrency limiter, keyed by queue name. Persisted across\n // reconnects (unlike consumerTags) so handlers still running when a\n // connection drops keep holding their permits, and the new consumer cannot\n // exceed the ceiling while the old pipeline drains. `null` = no limit.\n private subscriptionSemaphores = new Map<string, Semaphore | null>()\n private inflightCount = 0\n private drainResolve: (() => void) | null = null\n\n constructor(options: RabbitMQClientOptions) {\n this.options = {\n url: options.url,\n maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,\n retryDelay: options.retryDelay ?? DEFAULTS.retryDelay,\n connectionRetryDelay: options.connectionRetryDelay ?? DEFAULTS.connectionRetryDelay,\n initMaxAttempts: options.initMaxAttempts ?? DEFAULTS.initMaxAttempts,\n publishMaxAttempts: options.publishMaxAttempts ?? DEFAULTS.publishMaxAttempts,\n prefetch: options.prefetch ?? DEFAULTS.prefetch,\n // Default concurrency tracks prefetch, preserving the previous behaviour\n // (handlers ran fire-and-forget, so up to `prefetch` ran at once) while\n // making the ceiling explicit and independently tunable. A value <= 0\n // (including an unlimited `prefetch: 0`) means \"no concurrency limit\".\n concurrency: options.concurrency ?? options.prefetch ?? DEFAULTS.prefetch,\n closeTimeout: options.closeTimeout ?? DEFAULTS.closeTimeout,\n }\n this.logger = forLibraryCalls(options.logger ?? defaultLogger)\n this.hooks = options.hooks ?? {}\n }\n\n /**\n * Opens a connection and creates a confirm channel. Retries up to\n * `initMaxAttempts` times, then throws. Idempotent and safe to call\n * concurrently — duplicate calls share the same in-flight promise.\n */\n async init(): Promise<void> {\n if (this.connection && this.connected) {\n return\n }\n if (this.initializationPromise) {\n return this.initializationPromise\n }\n // Attach a no-op catch to prevent Node from briefly treating this as an\n // unhandled rejection before the await below registers its own handler.\n this.initializationPromise = this.doConnect(this.options.initMaxAttempts)\n this.initializationPromise.catch(() => undefined)\n try {\n await this.initializationPromise\n } finally {\n this.initializationPromise = null\n }\n }\n\n private async doConnect(maxAttempts?: number): Promise<void> {\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n let attempts = 0\n while (!this.connected) {\n try {\n this.connection = await amqp.connect(this.options.url)\n this.logger.info('Connected to server')\n this.channel = await this.connection.createConfirmChannel()\n const returnWaiters = new Set<(msg: amqp.Message) => void>()\n this.returnWaiters = returnWaiters\n this.channel.on('return', (msg: amqp.Message) => {\n for (const waiter of returnWaiters) waiter(msg)\n })\n this.logger.info('Confirm channel created')\n await this.channel.prefetch(this.options.prefetch)\n this.logger.info('Channel prefetch set', { prefetch: this.options.prefetch })\n\n this.connection.on('error', (error: Error) => {\n this.connected = false\n this.logger.error('Connection error', { error })\n })\n this.connection.on('close', () => {\n this.connected = false\n this.logger.warn('Connection closed')\n this.reconnectWithRetry()\n })\n this.channel.on('error', (error: Error) => {\n this.logger.error('Channel error', { error })\n this.handleChannelFailure()\n })\n this.channel.on('close', () => {\n this.logger.warn('Channel closed')\n this.handleChannelFailure()\n })\n\n this.connected = true\n this.logger.info('Client initialized successfully')\n } catch (error) {\n attempts++\n this.connected = false\n if (maxAttempts !== undefined && attempts >= maxAttempts) {\n this.logger.error('Connection failed after maximum attempts', { error, attempts, maxAttempts })\n throw new Error(\n `Failed to connect to RabbitMQ after ${attempts} attempts. ` +\n 'Check RABBITMQ_URL configuration and RabbitMQ server availability.',\n )\n }\n this.logger.warn('Connection attempt failed, retrying...', {\n error,\n attempt: attempts,\n maxAttempts: maxAttempts ?? 'unlimited',\n retryDelayMs: this.options.connectionRetryDelay,\n })\n await this.sleep(this.options.connectionRetryDelay)\n }\n }\n }\n\n private handleChannelFailure(): void {\n if (this.connected) {\n this.connected = false\n this.reconnectWithRetry()\n }\n }\n\n /**\n * Returns whether the client currently has an active connection and channel.\n */\n isConnected(): boolean {\n return this.connected\n }\n\n /**\n * Publishes a JSON message to a topic exchange with publisher confirms, or\n * straight to a queue through the default exchange (`''`, routing key = queue name).\n * Retries with exponential backoff (capped at 60 s), forcing a reconnect\n * on each failure. Throws after `publishMaxAttempts` exhausted.\n */\n async publish(\n exchange: string,\n routingKey: string,\n message: RabbitMQMessage,\n options?: PublishOptions,\n ): Promise<void> {\n let attempts = 0\n const maxAttempts = options?.maxAttempts ?? this.options.publishMaxAttempts\n const baseDelay = this.options.connectionRetryDelay\n const content = Buffer.from(JSON.stringify(message))\n // Taken once, not per attempt: a retried publish must not look newer than\n // a message published after it succeeded.\n const timestamp = Math.floor(Date.now() / 1000)\n\n while (attempts < maxAttempts) {\n try {\n if (!this.connected) {\n await this.reconnectWithRetry()\n }\n\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n\n // The default exchange ('') always exists and the broker refuses to\n // declare it, so publishing straight to a queue skips the assertion.\n if (exchange !== '' && !this.assertedExchanges.has(exchange)) {\n await this.channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n const channel = this.channel\n const returnWaiters = this.returnWaiters\n // The broker sends basic.return before the confirm of the same message,\n // so the flag is settled by the time waitForConfirms resolves. A return\n // carries no delivery tag, so it is matched on where it was sent plus the\n // message id, or the content when there is none.\n let returned = false\n const messageId = options?.messageId\n const onReturn = (msg: amqp.Message) => {\n if (msg.fields.exchange !== exchange || msg.fields.routingKey !== routingKey) return\n if (messageId !== undefined ? msg.properties.messageId === messageId : msg.content.equals(content)) {\n returned = true\n }\n }\n if (options?.mandatory) returnWaiters.add(onReturn)\n\n try {\n channel.publish(exchange, routingKey, content, {\n persistent: true,\n timestamp,\n headers: options?.headers,\n correlationId: options?.correlationId,\n messageId: options?.messageId,\n expiration: options?.expiration,\n mandatory: options?.mandatory,\n })\n\n await channel.waitForConfirms()\n } finally {\n returnWaiters.delete(onReturn)\n }\n\n if (returned) {\n throw new UnroutableMessageError(exchange, routingKey)\n }\n\n if (attempts > 0) {\n this.logger.info('Published message after retries', { exchange, routingKey, messageSize: content.length, attempts })\n } else {\n this.logger.info('Published message', { exchange, routingKey, messageSize: content.length })\n }\n this.logger.debug('Published message payload', { exchange, routingKey, payload: message })\n\n this.callHook(this.hooks.onPublish, { exchange, routingKey, attempts: attempts + 1 })\n\n return\n } catch (error) {\n // The broker answered, and the same routing would fail the same way.\n if (error instanceof UnroutableMessageError) {\n this.logger.warn('Published message was unroutable', { exchange, routingKey })\n throw error\n }\n\n attempts++\n this.connected = false\n\n if (attempts >= maxAttempts) {\n this.logger.error('Publish failed after max attempts', { error, exchange, routingKey, attempts, maxAttempts })\n throw new Error(\n `Failed to publish to ${exchange || '(default exchange)'}/${routingKey} after ${attempts} attempts: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n\n const retryDelay = Math.min(\n baseDelay * Math.pow(2, attempts - 1),\n MAX_PUBLISH_RETRY_DELAY_MS,\n )\n\n this.logger.warn('Publish attempt failed, retrying', { error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay })\n\n await this.sleep(retryDelay)\n }\n }\n }\n\n /**\n * Gracefully shuts down the client. Waits up to `closeTimeout` ms for\n * in-flight message handlers to finish before closing the channel and\n * connection. Pass `false` to preserve subscriptions for a later\n * `init()` / reconnect cycle.\n */\n async close(clearSubscriptions = true): Promise<void> {\n try {\n if (this.inflightCount > 0) {\n this.logger.info('Waiting for in-flight messages to drain', { inflightCount: this.inflightCount })\n await this.waitForDrain(this.options.closeTimeout)\n }\n await this.channel?.close()\n await this.connection?.close()\n this.connection = null\n this.channel = null\n this.connected = false\n this.initializationPromise = null\n this.reconnectionPromise = null\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n if (clearSubscriptions) {\n this.subscriptions = []\n this.subscriptionSemaphores.clear()\n }\n this.logger.info('Connection closed')\n } catch (error) {\n this.logger.error('Error closing connection', { error })\n throw error\n }\n }\n\n private async reconnectWithRetry(): Promise<void> {\n if (this.reconnectionPromise) {\n return this.reconnectionPromise\n }\n this.connected = false\n this.logger.info('Starting reconnection...')\n this.reconnectionPromise = this.doReconnect()\n this.reconnectionPromise.catch(() => undefined)\n try {\n await this.reconnectionPromise\n } finally {\n this.reconnectionPromise = null\n }\n }\n\n private async doReconnect(): Promise<void> {\n await this.doConnect()\n await this.resubscribeAll()\n }\n\n private async resubscribeAll(): Promise<void> {\n if (this.subscriptions.length === 0) {\n return\n }\n this.logger.info('Re-establishing subscriptions', { count: this.subscriptions.length })\n const subs = [...this.subscriptions]\n const results = await Promise.allSettled(\n subs.map(async (sub) => {\n await this.setupSubscription(sub)\n return sub.queue\n }),\n )\n const succeeded: string[] = []\n const failed: string[] = []\n results.forEach((result, index) => {\n if (result.status === 'fulfilled') {\n succeeded.push(result.value)\n } else {\n const queue = subs[index].queue\n failed.push(queue)\n this.logger.error('Failed to re-subscribe to queue', { error: result.reason, queue })\n }\n })\n if (succeeded.length > 0) {\n this.logger.info('Successfully re-subscribed to queues', { count: succeeded.length, queues: succeeded })\n }\n if (failed.length > 0) {\n this.logger.warn('Some subscriptions failed to restore', { count: failed.length, queues: failed })\n }\n this.callHook(this.hooks.onReconnect, { subscriptionsRestored: succeeded.length, subscriptionsFailed: failed.length })\n }\n\n /**\n * Subscribes to a queue with automatic DLQ infrastructure setup.\n *\n * Pass `options.queueArguments` to override the default quorum-queue\n * arguments (merged with the DLQ wiring defaults).\n */\n async subscribe(\n exchange: string,\n routingKey: string,\n queue: string,\n handler: RabbitMQMessageHandler,\n options?: SubscribeOptions,\n ): Promise<void> {\n if (!this.connected || !this.channel) {\n throw new Error('RabbitMQ client not connected. Call init() first.')\n }\n const sub: RabbitMQSubscription = { exchange, routingKey, queue, handler, options }\n const existingIndex = this.subscriptions.findIndex((s) => s.queue === queue)\n if (existingIndex === -1) {\n this.subscriptions.push(sub)\n } else {\n this.subscriptions[existingIndex] = sub\n // Re-subscribing may change concurrency; drop the old limiter so\n // setupSubscription rebuilds it from the new options. (A reconnect goes\n // through setupSubscription directly and keeps the existing limiter.)\n this.subscriptionSemaphores.delete(queue)\n }\n await this.setupSubscription(sub)\n }\n\n /**\n * Cancels a queue subscription and removes it from the restoration list.\n */\n async unsubscribe(queue: string): Promise<void> {\n const tag = this.consumerTags.get(queue)\n if (tag && this.channel) {\n await this.channel.cancel(tag)\n }\n this.consumerTags.delete(queue)\n this.subscriptionSemaphores.delete(queue)\n this.subscriptions = this.subscriptions.filter((s) => s.queue !== queue)\n this.logger.info('Unsubscribed from queue', { queue })\n }\n\n private async setupSubscription(sub: RabbitMQSubscription): Promise<void> {\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n // Bind this consumer to the exact channel that will deliver its messages,\n // so ack/nack always target that channel even after a reconnect swaps\n // `this.channel`.\n const channel = this.channel\n const { exchange, routingKey, queue, handler, options } = sub\n const dlxExchange = `${exchange}.dlx`\n const dlqQueue = `${queue}.dlq`\n const dlqRoutingKey = `${routingKey}.dead`\n\n await channel.assertExchange(dlxExchange, 'topic', { durable: true })\n await channel.assertQueue(dlqQueue, { durable: true })\n await channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey)\n\n if (!this.assertedExchanges.has(exchange)) {\n await channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n const queueType = options?.queueArguments?.['x-queue-type'] ?? 'quorum'\n const queueArgs: Record<string, unknown> = {\n 'x-queue-type': queueType,\n 'x-overflow': 'reject-publish',\n }\n // at-least-once DLQ strategy is only supported by quorum queues\n if (queueType === 'quorum') {\n queueArgs['x-dead-letter-strategy'] = 'at-least-once'\n }\n\n await channel.assertQueue(queue, {\n durable: true,\n deadLetterExchange: dlxExchange,\n deadLetterRoutingKey: dlqRoutingKey,\n arguments: { ...queueArgs, ...options?.queueArguments },\n })\n\n await channel.bindQueue(queue, exchange, routingKey)\n // Reuse the queue's existing limiter across reconnects; only build a new\n // one the first time (or after subscribe()/unsubscribe() cleared it).\n // A concurrency <= 0 means \"no limit\" (null), matching an unlimited prefetch.\n if (!this.subscriptionSemaphores.has(queue)) {\n const concurrency = options?.concurrency ?? this.options.concurrency\n this.subscriptionSemaphores.set(queue, concurrency > 0 ? new Semaphore(concurrency) : null)\n }\n const semaphore = this.subscriptionSemaphores.get(queue) ?? null\n const { consumerTag } = await channel.consume(\n queue,\n (message) => {\n if (message) {\n this.dispatch(message, handler, semaphore, channel)\n }\n },\n { noAck: false },\n )\n this.consumerTags.set(queue, consumerTag)\n this.logger.info('Subscribed to queue', { exchange, routingKey, queue })\n }\n\n /**\n * Gates a delivered message on the subscription's concurrency semaphore, then\n * processes it. A message counts as in-flight from delivery until its handler\n * settles (including time spent waiting for a permit), so graceful `close()`\n * drains queued messages as well as actively-processing ones.\n */\n private dispatch(\n message: amqp.ConsumeMessage,\n handler: RabbitMQMessageHandler,\n semaphore: Semaphore | null,\n channel: amqp.ConfirmChannel,\n ): void {\n this.inflightCount++\n const acquire = semaphore ? semaphore.acquire() : Promise.resolve()\n acquire\n .then(() => this.handleWithRetry(message, handler, channel))\n .catch((error) => {\n this.logger.error('Unhandled error in message handler', { error })\n })\n .finally(() => {\n semaphore?.release()\n this.inflightCount--\n if (this.inflightCount === 0 && this.drainResolve) {\n this.drainResolve()\n }\n })\n }\n\n private async handleWithRetry(\n message: amqp.ConsumeMessage,\n handler: RabbitMQMessageHandler,\n channel: amqp.ConfirmChannel,\n ): Promise<void> {\n // `channel` is the one that delivered this message. If a reconnect has\n // since replaced it, this message was never acked and the broker will\n // redeliver it on the new channel, so drop this stale attempt rather than\n // run the handler again or ack a tag the new channel does not know.\n if (channel !== this.channel) {\n this.logger.warn('Skipping message from a superseded channel; it will be redelivered')\n return\n }\n const startTime = Date.now()\n let attempts = 0\n const routingKey = message.fields.routingKey\n const exchange = message.fields.exchange\n\n let content: RabbitMQMessage\n try {\n content = JSON.parse(message.content.toString())\n } catch (parseError) {\n const rawContent = message.content.toString()\n const rawPreview = rawContent.substring(0, 100)\n this.logger.error('Failed to parse message JSON, sending to DLQ', {\n error: parseError,\n exchange,\n routingKey,\n rawContentPreview: rawPreview + (rawContent.length > 100 ? '...' : ''),\n })\n if (this.settle(channel, message, 'nack')) {\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: 'invalid_json' })\n }\n return\n }\n\n this.logger.debug('Message received, processing', { exchange, routingKey, payload: content })\n\n const properties: RabbitMQMessageProperties = {\n headers: message.properties.headers ?? {},\n timestamp: message.properties.timestamp,\n messageId: message.properties.messageId,\n correlationId: message.properties.correlationId,\n }\n\n while (attempts < this.options.maxRetries) {\n // Only the handler call belongs in this try. Acking inside it would make\n // a dead channel look like a failed handler and re-run its side effects.\n try {\n await handler(content, properties)\n } catch (error) {\n attempts++\n this.logger.error('Handler failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n exchange,\n routingKey,\n attempt: attempts,\n maxRetries: this.options.maxRetries,\n })\n if (attempts < this.options.maxRetries) {\n await this.sleep(this.options.retryDelay)\n }\n continue\n }\n const duration = Date.now() - startTime\n this.logger.info('Message processed successfully', { exchange, routingKey, duration, attempts: attempts + 1 })\n if (this.settle(channel, message, 'ack')) {\n this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration, attempts: attempts + 1 })\n }\n return\n }\n\n const duration = Date.now() - startTime\n this.logger.error('Message failed after max retries, sending to DLQ', { exchange, routingKey, maxRetries: this.options.maxRetries, duration })\n if (this.settle(channel, message, 'nack')) {\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: 'max_retries_exhausted' })\n }\n }\n\n /**\n * Acks or nacks a delivery, reporting whether the broker accepted it.\n *\n * A settle failure is a transport error, never a handler failure: by this\n * point the handler has already run, so it must not be retried just because\n * the channel went away. The delivery stays unacked and the broker redelivers\n * it once the channel closes.\n */\n private settle(\n channel: amqp.ConfirmChannel,\n message: amqp.ConsumeMessage,\n action: 'ack' | 'nack',\n ): boolean {\n // A handler can run for minutes, so the channel that delivered this\n // message may have been replaced by a reconnect since the entry check.\n // Settling on it would throw, and replaying the delivery tag on the\n // current channel would settle an unrelated message, since tags are scoped\n // to the channel that issued them. The broker requeues a closed channel's\n // unacked deliveries, so let it be redelivered instead.\n if (channel !== this.channel) {\n this.logger.warn('Skipping message from a superseded channel; it will be redelivered')\n return false\n }\n try {\n if (action === 'ack') {\n channel.ack(message)\n } else {\n channel.nack(message, false, false)\n }\n return true\n } catch (error) {\n this.logger.error('Failed to settle message; leaving it unacked for redelivery', {\n error,\n action,\n exchange: message.fields.exchange,\n routingKey: message.fields.routingKey,\n })\n return false\n }\n }\n\n /**\n * Lightweight liveness probe: creates and immediately deletes a temporary\n * exclusive queue. Returns `false` when disconnected or on broker error.\n */\n async checkHealth(): Promise<boolean> {\n if (!this.connected || !this.channel) {\n return false\n }\n try {\n const { queue } = await this.channel.assertQueue('', {\n exclusive: true,\n autoDelete: true,\n })\n await this.channel.deleteQueue(queue)\n return true\n } catch (error) {\n this.logger.warn('Health check failed', { error })\n return false\n }\n }\n\n private waitForDrain(timeout: number): Promise<void> {\n if (this.inflightCount === 0) return Promise.resolve()\n return new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n this.logger.warn('Close timeout reached with messages still in flight', { inflightCount: this.inflightCount })\n this.drainResolve = null\n resolve()\n }, timeout)\n this.drainResolve = () => {\n clearTimeout(timer)\n this.drainResolve = null\n resolve()\n }\n })\n }\n\n /** Invokes a hook callback, swallowing errors so hooks never break message flow. */\n private callHook<T>(hook: ((info: T) => void) | undefined, info: T): void {\n try { hook?.(info) } catch (error) { this.logger.debug('Observability hook error', { error }) }\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n }\n}\n","import type { ILogger } from './types.js'\n\n/**\n * Default logger used when the consumer does not inject one.\n *\n * Forwards `warn` and `error` to the console so connection drops, channel\n * errors, and failed handlers stay visible out of the box, while staying\n * silent for `info`/`debug` so routine operation never pollutes the\n * consumer's output. Pass `silentLogger` for zero output, or any `ILogger`\n * (console, Winston, pino, Bunyan) to take full control.\n */\nexport const defaultLogger: ILogger = {\n debug: () => {},\n info: () => {},\n warn: (message, ...args) => console.warn('[rabbitmq-client]', message, ...args),\n error: (message, ...args) => console.error('[rabbitmq-client]', message, ...args),\n}\n\ntype Level = keyof ILogger\ntype ObjectFirst = (context: object, message: string) => void\n\n/**\n * pino takes `(context, message)` and drops a context passed after the\n * message, and only serializes an Error under `err`. It is the one logger\n * with a `bindings()` method, so it is recognised by that and called its way.\n */\nexport const forLibraryCalls = (logger: ILogger): ILogger => {\n if (typeof (logger as { bindings?: unknown }).bindings !== 'function') return logger\n const call =\n (level: Level) =>\n (message: string, context?: unknown): void => {\n const log = (logger[level] as unknown as ObjectFirst).bind(logger)\n if (typeof context !== 'object' || context === null) return log({}, message)\n const { error, ...rest } = context as { error?: unknown }\n log(error === undefined ? rest : { ...rest, err: error }, message)\n }\n return { debug: call('debug'), info: call('info'), warn: call('warn'), error: call('error') }\n}\n\n/**\n * No-op logger that discards every message. Use this to silence the client\n * entirely, including the `warn`/`error` output the default logger emits.\n */\nexport const silentLogger: ILogger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n}\n","/**\n * Minimal FIFO counting semaphore used to cap how many message handlers run\n * concurrently, independently of channel prefetch.\n *\n * Prefetch bounds how many messages the broker delivers into consumer memory;\n * the semaphore bounds how many of those are actively being processed at once.\n * Keeping the two separate lets a consumer buffer a large prefetch window while\n * still protecting a slow downstream (a database, an HTTP API) from a burst.\n */\nexport class Semaphore {\n private permits: number\n private readonly waiters: Array<() => void> = []\n\n constructor(permits: number) {\n if (!Number.isInteger(permits) || permits < 1) {\n throw new Error(`Semaphore requires a positive integer of permits, got ${permits}`)\n }\n this.permits = permits\n }\n\n /**\n * Acquires a permit, resolving immediately when one is free or queueing\n * (FIFO) until another holder releases. Always pair with `release()` in a\n * `finally` so a throwing handler never leaks a permit.\n */\n acquire(): Promise<void> {\n if (this.permits > 0) {\n this.permits--\n return Promise.resolve()\n }\n return new Promise<void>((resolve) => {\n this.waiters.push(resolve)\n })\n }\n\n /**\n * Releases a permit. Hands it directly to the next waiter if any are queued,\n * otherwise returns it to the pool.\n */\n release(): void {\n const next = this.waiters.shift()\n if (next) {\n next()\n } else {\n this.permits++\n }\n }\n\n /** Permits currently available. Exposed for tests and introspection. */\n get available(): number {\n return this.permits\n }\n\n /** Number of callers currently queued waiting for a permit. */\n get waiting(): number {\n return this.waiters.length\n }\n}\n"],"mappings":"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","names":["duration"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/logger.ts","../src/semaphore.ts"],"sourcesContent":["import amqp from 'amqplib'\nimport type {\n ILogger,\n PublishOptions,\n RabbitMQClientOptions,\n RabbitMQHooks,\n RabbitMQMessage,\n RabbitMQMessageHandler,\n RabbitMQMessageProperties,\n RabbitMQSubscription,\n SubscribeOptions,\n} from './types.js'\nimport { defaultLogger, forLibraryCalls } from './logger.js'\nimport { Semaphore } from './semaphore.js'\n\nconst DEFAULT_PREFETCH = 10\nconst MAX_PUBLISH_RETRY_DELAY_MS = 60_000\n\nconst DEFAULTS = {\n maxRetries: 3,\n retryDelay: 1000,\n connectionRetryDelay: 5000,\n initMaxAttempts: 5,\n publishMaxAttempts: 5,\n prefetch: DEFAULT_PREFETCH,\n closeTimeout: 5000,\n} as const\n\nfunction backoff(baseDelay: number, attempt: number, maxDelay: number): number {\n return Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay)\n}\n\n/** A `mandatory` publish the broker could route to no queue. */\nexport class UnroutableMessageError extends Error {\n constructor(\n readonly exchange: string,\n readonly routingKey: string,\n ) {\n super(`No queue is bound to receive ${exchange || '(default exchange)'}/${routingKey}`)\n this.name = 'UnroutableMessageError'\n }\n}\n\n/** Thrown by a handler to send its message to the DLQ at once, without retrying. */\nexport class DeadLetterError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options)\n this.name = 'DeadLetterError'\n }\n}\n\n/**\n * Production-grade RabbitMQ client with confirm channels, automatic DLQ\n * infrastructure, reconnection with subscription restoration, and\n * exponential-backoff publishing.\n *\n * @example\n * ```ts\n * const client = new RabbitMQClient({ url: 'amqp://localhost' })\n * await client.init()\n * await client.publish('events', 'user.created', { userId: '123' })\n * await client.subscribe('events', 'user.created', 'user-service', handler)\n * ```\n */\nexport class RabbitMQClient {\n private connection: amqp.ChannelModel | null = null\n private channel: amqp.ConfirmChannel | null = null\n private connected = false\n private closing = false\n private subscriptions: RabbitMQSubscription[] = []\n private readonly options: Required<Omit<RabbitMQClientOptions, 'logger' | 'hooks'>>\n private readonly logger: ILogger\n private readonly hooks: RabbitMQHooks\n private initializationPromise: Promise<void> | null = null\n private reconnectionPromise: Promise<void> | null = null\n private assertedExchanges = new Set<string>()\n // One per channel, so a late return on a replaced channel reaches no waiter.\n private returnWaiters = new Set<(msg: amqp.Message) => void>()\n private consumerTags = new Map<string, string>()\n // Per-queue concurrency limiter, keyed by queue name. Persisted across\n // reconnects (unlike consumerTags) so handlers still running when a\n // connection drops keep holding their permits, and the new consumer cannot\n // exceed the ceiling while the old pipeline drains. `null` = no limit.\n private subscriptionSemaphores = new Map<string, Semaphore | null>()\n private retryWakers = new Set<() => void>()\n private inflightCount = 0\n private drainResolve: (() => void) | null = null\n\n constructor(options: RabbitMQClientOptions) {\n this.options = {\n url: options.url,\n maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,\n retryDelay: options.retryDelay ?? DEFAULTS.retryDelay,\n connectionRetryDelay: options.connectionRetryDelay ?? DEFAULTS.connectionRetryDelay,\n initMaxAttempts: options.initMaxAttempts ?? DEFAULTS.initMaxAttempts,\n publishMaxAttempts: options.publishMaxAttempts ?? DEFAULTS.publishMaxAttempts,\n prefetch: options.prefetch ?? DEFAULTS.prefetch,\n // Default concurrency tracks prefetch, preserving the previous behaviour\n // (handlers ran fire-and-forget, so up to `prefetch` ran at once) while\n // making the ceiling explicit and independently tunable. A value <= 0\n // (including an unlimited `prefetch: 0`) means \"no concurrency limit\".\n concurrency: options.concurrency ?? options.prefetch ?? DEFAULTS.prefetch,\n closeTimeout: options.closeTimeout ?? DEFAULTS.closeTimeout,\n }\n this.logger = forLibraryCalls(options.logger ?? defaultLogger)\n this.hooks = options.hooks ?? {}\n }\n\n /**\n * Opens a connection and creates a confirm channel. Retries up to\n * `initMaxAttempts` times, then throws. Idempotent and safe to call\n * concurrently — duplicate calls share the same in-flight promise.\n */\n async init(): Promise<void> {\n if (this.connection && this.connected) {\n return\n }\n if (this.initializationPromise) {\n return this.initializationPromise\n }\n // Attach a no-op catch to prevent Node from briefly treating this as an\n // unhandled rejection before the await below registers its own handler.\n this.initializationPromise = this.doConnect(this.options.initMaxAttempts)\n this.initializationPromise.catch(() => undefined)\n try {\n await this.initializationPromise\n } finally {\n this.initializationPromise = null\n }\n }\n\n private async doConnect(maxAttempts?: number): Promise<void> {\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n // A channel failure leaves its connection open; drop it so it does not\n // outlive the client and keep the process alive.\n this.connection?.close().catch(() => undefined)\n this.connection = null\n let attempts = 0\n while (!this.connected && !this.closing) {\n try {\n const connection = await amqp.connect(this.options.url)\n this.connection = connection\n this.logger.info('Connected to server')\n const channel = await connection.createConfirmChannel()\n this.channel = channel\n // Retries sleeping on the replaced channel give up; the broker redelivers their messages.\n this.wakeRetries()\n const returnWaiters = new Set<(msg: amqp.Message) => void>()\n this.returnWaiters = returnWaiters\n channel.on('return', (msg: amqp.Message) => {\n for (const waiter of returnWaiters) waiter(msg)\n })\n this.logger.info('Confirm channel created')\n await channel.prefetch(this.options.prefetch)\n this.logger.info('Channel prefetch set', { prefetch: this.options.prefetch })\n\n // Events from a connection or channel a reconnect has replaced must not\n // tear down the current one.\n connection.on('error', (error: Error) => {\n if (connection !== this.connection) return\n this.connected = false\n this.logger.error('Connection error', { error })\n })\n connection.on('close', () => {\n if (connection !== this.connection) return\n this.connected = false\n this.logger.warn('Connection closed')\n this.reconnectWithRetry()\n })\n channel.on('error', (error: Error) => {\n if (channel !== this.channel) return\n this.logger.error('Channel error', { error })\n this.handleChannelFailure()\n })\n channel.on('close', () => {\n if (channel !== this.channel) return\n this.logger.warn('Channel closed')\n this.handleChannelFailure()\n })\n\n this.connected = true\n this.logger.info('Client initialized successfully')\n } catch (error) {\n attempts++\n this.connected = false\n if (maxAttempts !== undefined && attempts >= maxAttempts) {\n this.logger.error('Connection failed after maximum attempts', { error, attempts, maxAttempts })\n throw new Error(\n `Failed to connect to RabbitMQ after ${attempts} attempts. ` +\n 'Check RABBITMQ_URL configuration and RabbitMQ server availability.',\n )\n }\n this.logger.warn('Connection attempt failed, retrying...', {\n error,\n attempt: attempts,\n maxAttempts: maxAttempts ?? 'unlimited',\n retryDelayMs: this.options.connectionRetryDelay,\n })\n await this.sleep(this.options.connectionRetryDelay)\n }\n }\n if (!this.connected) {\n throw new Error('RabbitMQ client closed while connecting')\n }\n }\n\n private handleChannelFailure(): void {\n if (this.connected) {\n this.connected = false\n this.reconnectWithRetry()\n }\n }\n\n /**\n * Returns whether the client currently has an active connection and channel.\n */\n isConnected(): boolean {\n return this.connected\n }\n\n /**\n * Publishes a JSON message to a topic exchange with publisher confirms, or\n * straight to a queue through the default exchange (`''`, routing key = queue name).\n * Retries with exponential backoff (capped at 60 s), forcing a reconnect\n * on each failure. Throws after `publishMaxAttempts` exhausted.\n */\n async publish(\n exchange: string,\n routingKey: string,\n message: RabbitMQMessage,\n options?: PublishOptions,\n ): Promise<void> {\n let attempts = 0\n const maxAttempts = options?.maxAttempts ?? this.options.publishMaxAttempts\n const baseDelay = this.options.connectionRetryDelay\n const content = Buffer.from(JSON.stringify(message))\n // Taken once, not per attempt: a retried publish must not look newer than\n // a message published after it succeeded.\n const timestamp = Math.floor(Date.now() / 1000)\n\n while (attempts < maxAttempts) {\n try {\n if (!this.connected) {\n await this.reconnectWithRetry()\n }\n\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n\n // The default exchange ('') always exists and the broker refuses to\n // declare it, so publishing straight to a queue skips the assertion.\n if (exchange !== '' && !this.assertedExchanges.has(exchange)) {\n await this.channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n const channel = this.channel\n const returnWaiters = this.returnWaiters\n // The broker sends basic.return before the confirm of the same message,\n // so the flag is settled by the time waitForConfirms resolves. A return\n // carries no delivery tag, so it is matched on where it was sent plus the\n // message id, or the content when there is none.\n let returned = false\n const messageId = options?.messageId\n const onReturn = (msg: amqp.Message) => {\n if (msg.fields.exchange !== exchange || msg.fields.routingKey !== routingKey) return\n if (messageId !== undefined ? msg.properties.messageId === messageId : msg.content.equals(content)) {\n returned = true\n }\n }\n if (options?.mandatory) returnWaiters.add(onReturn)\n\n try {\n channel.publish(exchange, routingKey, content, {\n persistent: true,\n timestamp,\n headers: options?.headers,\n correlationId: options?.correlationId,\n messageId: options?.messageId,\n expiration: options?.expiration,\n mandatory: options?.mandatory,\n })\n\n await channel.waitForConfirms()\n } finally {\n returnWaiters.delete(onReturn)\n }\n\n if (returned) {\n throw new UnroutableMessageError(exchange, routingKey)\n }\n\n if (attempts > 0) {\n this.logger.info('Published message after retries', { exchange, routingKey, messageSize: content.length, attempts })\n } else {\n this.logger.info('Published message', { exchange, routingKey, messageSize: content.length })\n }\n this.logger.debug('Published message payload', { exchange, routingKey, payload: message })\n\n this.callHook(this.hooks.onPublish, { exchange, routingKey, attempts: attempts + 1 })\n\n return\n } catch (error) {\n // The broker answered, and the same routing would fail the same way.\n if (error instanceof UnroutableMessageError) {\n this.logger.warn('Published message was unroutable', { exchange, routingKey })\n throw error\n }\n\n attempts++\n this.connected = false\n\n if (attempts >= maxAttempts) {\n this.logger.error('Publish failed after max attempts', { error, exchange, routingKey, attempts, maxAttempts })\n throw new Error(\n `Failed to publish to ${exchange || '(default exchange)'}/${routingKey} after ${attempts} attempts: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n\n const retryDelay = backoff(baseDelay, attempts, MAX_PUBLISH_RETRY_DELAY_MS)\n\n this.logger.warn('Publish attempt failed, retrying', { error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay })\n\n await this.sleep(retryDelay)\n }\n }\n }\n\n /**\n * Gracefully shuts down the client. Waits up to `closeTimeout` ms for\n * in-flight message handlers to finish before closing the channel and\n * connection. Pass `false` to preserve subscriptions for a later\n * `init()` / reconnect cycle.\n */\n async close(clearSubscriptions = true): Promise<void> {\n // Closing the channel fires its 'close' handler, which would otherwise\n // reconnect and leave a connection behind that keeps the process alive.\n this.closing = true\n try {\n // A connect already running gives up after its current attempt; whatever\n // it opened is closed below.\n await Promise.allSettled([this.initializationPromise, this.reconnectionPromise])\n this.wakeRetries()\n if (this.inflightCount > 0) {\n this.logger.info('Waiting for in-flight messages to drain', { inflightCount: this.inflightCount })\n await this.waitForDrain(this.options.closeTimeout)\n }\n try {\n await this.channel?.close()\n } finally {\n await this.connection?.close()\n }\n this.connection = null\n this.channel = null\n this.connected = false\n this.initializationPromise = null\n this.reconnectionPromise = null\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n if (clearSubscriptions) {\n this.subscriptions = []\n this.subscriptionSemaphores.clear()\n }\n this.logger.info('Connection closed')\n } catch (error) {\n this.logger.error('Error closing connection', { error })\n throw error\n } finally {\n this.closing = false\n }\n }\n\n private async reconnectWithRetry(): Promise<void> {\n if (this.closing) return\n if (this.reconnectionPromise) {\n return this.reconnectionPromise\n }\n this.connected = false\n this.logger.info('Starting reconnection...')\n this.reconnectionPromise = this.doReconnect()\n this.reconnectionPromise.catch(() => undefined)\n try {\n await this.reconnectionPromise\n } catch (error) {\n // The event handlers call this without awaiting it, so a reconnect that\n // close() interrupted must not surface as an unhandled rejection.\n if (!this.closing) throw error\n } finally {\n this.reconnectionPromise = null\n }\n }\n\n private async doReconnect(): Promise<void> {\n await this.doConnect()\n await this.resubscribeAll()\n }\n\n private async resubscribeAll(): Promise<void> {\n if (this.subscriptions.length === 0) {\n return\n }\n this.logger.info('Re-establishing subscriptions', { count: this.subscriptions.length })\n const subs = [...this.subscriptions]\n const results = await Promise.allSettled(\n subs.map(async (sub) => {\n await this.setupSubscription(sub)\n return sub.queue\n }),\n )\n const succeeded: string[] = []\n const failed: string[] = []\n results.forEach((result, index) => {\n if (result.status === 'fulfilled') {\n succeeded.push(result.value)\n } else {\n const queue = subs[index].queue\n failed.push(queue)\n this.logger.error('Failed to re-subscribe to queue', { error: result.reason, queue })\n }\n })\n if (succeeded.length > 0) {\n this.logger.info('Successfully re-subscribed to queues', { count: succeeded.length, queues: succeeded })\n }\n if (failed.length > 0) {\n this.logger.warn('Some subscriptions failed to restore', { count: failed.length, queues: failed })\n }\n this.callHook(this.hooks.onReconnect, { subscriptionsRestored: succeeded.length, subscriptionsFailed: failed.length })\n }\n\n /**\n * Subscribes to a queue with automatic DLQ infrastructure setup.\n *\n * Pass `options.queueArguments` to override the default quorum-queue\n * arguments (merged with the DLQ wiring defaults).\n */\n async subscribe(\n exchange: string,\n routingKey: string,\n queue: string,\n handler: RabbitMQMessageHandler,\n options?: SubscribeOptions,\n ): Promise<void> {\n if (!this.connected || !this.channel) {\n throw new Error('RabbitMQ client not connected. Call init() first.')\n }\n const sub: RabbitMQSubscription = { exchange, routingKey, queue, handler, options }\n const existingIndex = this.subscriptions.findIndex((s) => s.queue === queue)\n const previous = existingIndex === -1 ? undefined : this.subscriptions[existingIndex]\n if (existingIndex === -1) {\n this.subscriptions.push(sub)\n } else {\n this.subscriptions[existingIndex] = sub\n // Re-subscribing may change concurrency; drop the old limiter so\n // setupSubscription rebuilds it from the new options. (A reconnect goes\n // through setupSubscription directly and keeps the existing limiter.)\n this.subscriptionSemaphores.delete(queue)\n }\n try {\n await this.setupSubscription(sub)\n } catch (error) {\n // A subscription that cannot be set up must not be retried on every reconnect.\n this.subscriptions = previous\n ? this.subscriptions.map((s) => (s === sub ? previous : s))\n : this.subscriptions.filter((s) => s !== sub)\n throw error\n }\n }\n\n /**\n * Cancels a queue subscription and removes it from the restoration list.\n */\n async unsubscribe(queue: string): Promise<void> {\n const tag = this.consumerTags.get(queue)\n if (tag && this.channel) {\n await this.channel.cancel(tag)\n }\n this.consumerTags.delete(queue)\n this.subscriptionSemaphores.delete(queue)\n this.subscriptions = this.subscriptions.filter((s) => s.queue !== queue)\n this.logger.info('Unsubscribed from queue', { queue })\n }\n\n private async setupSubscription(sub: RabbitMQSubscription): Promise<void> {\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n // Bind this consumer to the exact channel that will deliver its messages,\n // so ack/nack always target that channel even after a reconnect swaps\n // `this.channel`.\n const channel = this.channel\n const { exchange, routingKey, queue, handler, options } = sub\n const dlxExchange = options?.deadLetterExchange ?? `${exchange}.dlx`\n const dlqQueue = `${queue}.dlq`\n // Set once on the queue, so every binding's dead letters reach the DLQ.\n const dlqRoutingKey = `${routingKey}.dead`\n const bindings = [{ exchange, routingKey }, ...(options?.bindings ?? [])]\n\n await channel.assertExchange(dlxExchange, 'topic', { durable: true })\n await channel.assertQueue(dlqQueue, { durable: true })\n await channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey)\n\n const missing = bindings.map((b) => b.exchange).filter((name) => !this.assertedExchanges.has(name))\n if (options?.passiveExchanges && missing.length > 0) {\n await this.checkExchanges(missing)\n } else {\n for (const name of missing) {\n await channel.assertExchange(name, 'topic', { durable: true })\n }\n }\n for (const name of missing) this.assertedExchanges.add(name)\n\n const queueType = options?.queueArguments?.['x-queue-type'] ?? 'quorum'\n const queueArgs: Record<string, unknown> = {\n 'x-queue-type': queueType,\n 'x-overflow': 'reject-publish',\n }\n // at-least-once DLQ strategy is only supported by quorum queues\n if (queueType === 'quorum') {\n queueArgs['x-dead-letter-strategy'] = 'at-least-once'\n }\n\n await channel.assertQueue(queue, {\n durable: true,\n deadLetterExchange: dlxExchange,\n deadLetterRoutingKey: dlqRoutingKey,\n arguments: { ...queueArgs, ...options?.queueArguments },\n })\n\n for (const binding of bindings) {\n await channel.bindQueue(queue, binding.exchange, binding.routingKey)\n }\n // Reuse the queue's existing limiter across reconnects; only build a new\n // one the first time (or after subscribe()/unsubscribe() cleared it).\n // A concurrency <= 0 means \"no limit\" (null), matching an unlimited prefetch.\n if (!this.subscriptionSemaphores.has(queue)) {\n const concurrency = options?.concurrency ?? this.options.concurrency\n this.subscriptionSemaphores.set(queue, concurrency > 0 ? new Semaphore(concurrency) : null)\n }\n const semaphore = this.subscriptionSemaphores.get(queue) ?? null\n const { consumerTag } = await channel.consume(\n queue,\n (message) => {\n if (message) {\n this.dispatch(message, handler, semaphore, channel, options)\n }\n },\n { noAck: false },\n )\n this.consumerTags.set(queue, consumerTag)\n this.logger.info('Subscribed to queue', { queue, bindings })\n }\n\n // A failed check closes the channel it ran on, so it runs on a throwaway one\n // rather than take down the shared channel and every consumer on it.\n private async checkExchanges(exchanges: string[]): Promise<void> {\n if (!this.connection) {\n throw new Error('Connection not available')\n }\n const probe = await this.connection.createChannel()\n probe.on('error', () => undefined)\n try {\n for (const name of exchanges) {\n await probe.checkExchange(name)\n }\n } finally {\n await probe.close().catch(() => undefined)\n }\n }\n\n /**\n * Gates a delivered message on the subscription's concurrency semaphore, then\n * processes it. A message counts as in-flight from delivery until its handler\n * settles (including time spent waiting for a permit), so graceful `close()`\n * drains queued messages as well as actively-processing ones.\n */\n private dispatch(\n message: amqp.ConsumeMessage,\n handler: RabbitMQMessageHandler,\n semaphore: Semaphore | null,\n channel: amqp.ConfirmChannel,\n options: SubscribeOptions | undefined,\n ): void {\n this.inflightCount++\n const acquire = semaphore ? semaphore.acquire() : Promise.resolve()\n acquire\n .then(() => this.handleWithRetry(message, handler, channel, options))\n .catch((error) => {\n this.logger.error('Unhandled error in message handler', { error })\n })\n .finally(() => {\n semaphore?.release()\n this.inflightCount--\n if (this.inflightCount === 0 && this.drainResolve) {\n this.drainResolve()\n }\n })\n }\n\n private async handleWithRetry(\n message: amqp.ConsumeMessage,\n handler: RabbitMQMessageHandler,\n channel: amqp.ConfirmChannel,\n options: SubscribeOptions | undefined,\n ): Promise<void> {\n // `channel` is the one that delivered this message. If a reconnect has\n // since replaced it, this message was never acked and the broker will\n // redeliver it on the new channel, so drop this stale attempt rather than\n // run the handler again or ack a tag the new channel does not know.\n if (channel !== this.channel) {\n this.logger.warn('Skipping message from a superseded channel; it will be redelivered')\n return\n }\n const startTime = Date.now()\n let attempts = 0\n const routingKey = message.fields.routingKey\n const exchange = message.fields.exchange\n\n let content: RabbitMQMessage\n try {\n content = JSON.parse(message.content.toString())\n } catch (parseError) {\n const rawContent = message.content.toString()\n const rawPreview = rawContent.substring(0, 100)\n this.logger.error('Failed to parse message JSON, sending to DLQ', {\n error: parseError,\n exchange,\n routingKey,\n rawContentPreview: rawPreview + (rawContent.length > 100 ? '...' : ''),\n })\n if (this.settle(channel, message, 'nack')) {\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: 'invalid_json' })\n }\n return\n }\n\n this.logger.debug('Message received, processing', { exchange, routingKey, payload: content })\n\n const properties: RabbitMQMessageProperties = {\n exchange,\n routingKey,\n headers: message.properties.headers ?? {},\n timestamp: message.properties.timestamp,\n messageId: message.properties.messageId,\n correlationId: message.properties.correlationId,\n }\n\n const maxRetries = options?.maxRetries ?? this.options.maxRetries\n while (attempts < maxRetries) {\n // Only the handler call belongs in this try. Acking inside it would make\n // a dead channel look like a failed handler and re-run its side effects.\n try {\n await handler(content, properties)\n } catch (error) {\n attempts++\n // By name, not instanceof: the ESM and CJS builds each define their own class.\n if (error instanceof Error && error.name === 'DeadLetterError') {\n const duration = Date.now() - startTime\n this.logger.warn('Handler dead-lettered the message', { error: error.message, exchange, routingKey })\n if (this.settle(channel, message, 'nack')) {\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: 'dead_letter_error' })\n }\n return\n }\n this.logger.error('Handler failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n exchange,\n routingKey,\n attempt: attempts,\n maxRetries,\n })\n if (attempts < maxRetries) {\n await this.sleepUntilClose(this.retryDelayFor(attempts, options))\n if (this.closing || channel !== this.channel) {\n this.logger.warn('Stopped retrying a message on close or reconnect; it will be redelivered', { exchange, routingKey })\n return\n }\n }\n continue\n }\n const duration = Date.now() - startTime\n this.logger.info('Message processed successfully', { exchange, routingKey, duration, attempts: attempts + 1 })\n if (this.settle(channel, message, 'ack')) {\n this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration, attempts: attempts + 1 })\n }\n return\n }\n\n const duration = Date.now() - startTime\n this.logger.error('Message failed after max retries, sending to DLQ', { exchange, routingKey, maxRetries, duration })\n if (this.settle(channel, message, 'nack')) {\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: 'max_retries_exhausted' })\n }\n }\n\n /**\n * Acks or nacks a delivery, reporting whether the broker accepted it.\n *\n * A settle failure is a transport error, never a handler failure: by this\n * point the handler has already run, so it must not be retried just because\n * the channel went away. The delivery stays unacked and the broker redelivers\n * it once the channel closes.\n */\n private settle(\n channel: amqp.ConfirmChannel,\n message: amqp.ConsumeMessage,\n action: 'ack' | 'nack',\n ): boolean {\n // A handler can run for minutes, so the channel that delivered this\n // message may have been replaced by a reconnect since the entry check.\n // Settling on it would throw, and replaying the delivery tag on the\n // current channel would settle an unrelated message, since tags are scoped\n // to the channel that issued them. The broker requeues a closed channel's\n // unacked deliveries, so let it be redelivered instead.\n if (channel !== this.channel) {\n this.logger.warn('Skipping message from a superseded channel; it will be redelivered')\n return false\n }\n try {\n if (action === 'ack') {\n channel.ack(message)\n } else {\n channel.nack(message, false, false)\n }\n return true\n } catch (error) {\n this.logger.error('Failed to settle message; leaving it unacked for redelivery', {\n error,\n action,\n exchange: message.fields.exchange,\n routingKey: message.fields.routingKey,\n })\n return false\n }\n }\n\n /**\n * Lightweight liveness probe: creates and immediately deletes a temporary\n * exclusive queue. Returns `false` when disconnected or on broker error.\n */\n async checkHealth(): Promise<boolean> {\n if (!this.connected || !this.channel) {\n return false\n }\n try {\n const { queue } = await this.channel.assertQueue('', {\n exclusive: true,\n autoDelete: true,\n })\n await this.channel.deleteQueue(queue)\n return true\n } catch (error) {\n this.logger.warn('Health check failed', { error })\n return false\n }\n }\n\n private waitForDrain(timeout: number): Promise<void> {\n if (this.inflightCount === 0) return Promise.resolve()\n return new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n this.logger.warn('Close timeout reached with messages still in flight', { inflightCount: this.inflightCount })\n this.drainResolve = null\n resolve()\n }, timeout)\n this.drainResolve = () => {\n clearTimeout(timer)\n this.drainResolve = null\n resolve()\n }\n })\n }\n\n /** Invokes a hook callback, swallowing errors so hooks never break message flow. */\n private callHook<T>(hook: ((info: T) => void) | undefined, info: T): void {\n try { hook?.(info) } catch (error) { this.logger.debug('Observability hook error', { error }) }\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n }\n\n private retryDelayFor(attempts: number, options: SubscribeOptions | undefined): number {\n const { retryDelay } = this.options\n if (options?.maxRetryDelay === undefined) return retryDelay\n return backoff(retryDelay, attempts, options.maxRetryDelay)\n }\n\n private wakeRetries(): void {\n for (const wake of [...this.retryWakers]) wake()\n }\n\n /** Also cut short by a reconnect, via wakeRetries(). */\n private sleepUntilClose(ms: number): Promise<void> {\n if (this.closing) return Promise.resolve()\n return new Promise((resolve) => {\n const wake = () => {\n clearTimeout(timer)\n this.retryWakers.delete(wake)\n resolve()\n }\n const timer = setTimeout(wake, ms)\n this.retryWakers.add(wake)\n })\n }\n}\n","import type { ILogger } from './types.js'\n\n/**\n * Default logger used when the consumer does not inject one.\n *\n * Forwards `warn` and `error` to the console so connection drops, channel\n * errors, and failed handlers stay visible out of the box, while staying\n * silent for `info`/`debug` so routine operation never pollutes the\n * consumer's output. Pass `silentLogger` for zero output, or any `ILogger`\n * (console, Winston, pino, Bunyan) to take full control.\n */\nexport const defaultLogger: ILogger = {\n debug: () => {},\n info: () => {},\n warn: (message, ...args) => console.warn('[rabbitmq-client]', message, ...args),\n error: (message, ...args) => console.error('[rabbitmq-client]', message, ...args),\n}\n\ntype Level = keyof ILogger\ntype ObjectFirst = (context: object, message: string) => void\n\n/**\n * pino takes `(context, message)` and drops a context passed after the\n * message, and only serializes an Error under `err`. It is the one logger\n * with a `bindings()` method, so it is recognised by that and called its way.\n */\nexport const forLibraryCalls = (logger: ILogger): ILogger => {\n if (typeof (logger as { bindings?: unknown }).bindings !== 'function') return logger\n const call =\n (level: Level) =>\n (message: string, context?: unknown): void => {\n const log = (logger[level] as unknown as ObjectFirst).bind(logger)\n if (typeof context !== 'object' || context === null) return log({}, message)\n const { error, ...rest } = context as { error?: unknown }\n log(error === undefined ? rest : { ...rest, err: error }, message)\n }\n return { debug: call('debug'), info: call('info'), warn: call('warn'), error: call('error') }\n}\n\n/**\n * No-op logger that discards every message. Use this to silence the client\n * entirely, including the `warn`/`error` output the default logger emits.\n */\nexport const silentLogger: ILogger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n}\n","/**\n * Minimal FIFO counting semaphore used to cap how many message handlers run\n * concurrently, independently of channel prefetch.\n *\n * Prefetch bounds how many messages the broker delivers into consumer memory;\n * the semaphore bounds how many of those are actively being processed at once.\n * Keeping the two separate lets a consumer buffer a large prefetch window while\n * still protecting a slow downstream (a database, an HTTP API) from a burst.\n */\nexport class Semaphore {\n private permits: number\n private readonly waiters: Array<() => void> = []\n\n constructor(permits: number) {\n if (!Number.isInteger(permits) || permits < 1) {\n throw new Error(`Semaphore requires a positive integer of permits, got ${permits}`)\n }\n this.permits = permits\n }\n\n /**\n * Acquires a permit, resolving immediately when one is free or queueing\n * (FIFO) until another holder releases. Always pair with `release()` in a\n * `finally` so a throwing handler never leaks a permit.\n */\n acquire(): Promise<void> {\n if (this.permits > 0) {\n this.permits--\n return Promise.resolve()\n }\n return new Promise<void>((resolve) => {\n this.waiters.push(resolve)\n })\n }\n\n /**\n * Releases a permit. Hands it directly to the next waiter if any are queued,\n * otherwise returns it to the pool.\n */\n release(): void {\n const next = this.waiters.shift()\n if (next) {\n next()\n } else {\n this.permits++\n }\n }\n\n /** Permits currently available. Exposed for tests and introspection. */\n get available(): number {\n return this.permits\n }\n\n /** Number of callers currently queued waiting for a permit. */\n get waiting(): number {\n return this.waiters.length\n }\n}\n"],"mappings":"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","names":["duration"]}
@@ -50,6 +50,7 @@ var MockRabbitMQChannel = class extends import_events.EventEmitter {
50
50
  isClosed = false;
51
51
  // Stubs
52
52
  assertExchange = mockFn().mockResolvedValue({});
53
+ checkExchange = mockFn().mockResolvedValue({});
53
54
  assertQueue = mockFn().mockResolvedValue({ queue: "" });
54
55
  bindQueue = mockFn().mockResolvedValue({});
55
56
  ack = mockFn();
@@ -100,10 +101,11 @@ var MockRabbitMQChannel = class extends import_events.EventEmitter {
100
101
  }
101
102
  _deliver(content, properties = {}) {
102
103
  if (!this._consumeCallback) return;
104
+ const { exchange = "", routingKey = "", ...amqpProperties } = properties;
103
105
  this._consumeCallback({
104
106
  content,
105
- fields: { deliveryTag: 1, redelivered: false, exchange: "", routingKey: "", consumerTag: "mock-consumer" },
106
- properties: { headers: {}, ...properties }
107
+ fields: { deliveryTag: 1, redelivered: false, exchange, routingKey, consumerTag: "mock-consumer" },
108
+ properties: { headers: {}, ...amqpProperties }
107
109
  });
108
110
  }
109
111
  /** Emits a close event and marks the channel as closed. */
@@ -163,6 +165,10 @@ var MockRabbitMQConnection = class extends import_events.EventEmitter {
163
165
  this._channels.push(ch);
164
166
  return Promise.resolve(ch);
165
167
  }
168
+ /** Creates an untracked plain channel, as used for passive exchange checks. */
169
+ createChannel() {
170
+ return Promise.resolve(new MockRabbitMQChannel());
171
+ }
166
172
  close() {
167
173
  this.isConnected = false;
168
174
  return Promise.resolve();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/testing/index.ts"],"sourcesContent":["import { EventEmitter } from 'events'\nimport type { RabbitMQMessageProperties } from '../types.js'\n\n// ---------------------------------------------------------------------------\n// Framework-agnostic mock function factory\n// ---------------------------------------------------------------------------\n\ninterface MockFn {\n (...args: unknown[]): unknown\n mock: { calls: unknown[][] }\n mockResolvedValue: (val: unknown) => MockFn\n mockImplementation: (fn: (...args: unknown[]) => unknown) => MockFn\n}\n\nfunction mockFn(): MockFn {\n let impl: ((...args: unknown[]) => unknown) | null = null\n const calls: unknown[][] = []\n\n const fn = ((...args: unknown[]) => {\n calls.push(args)\n return impl ? impl(...args) : undefined\n }) as MockFn\n\n fn.mock = { calls }\n\n fn.mockResolvedValue = (val: unknown) => {\n impl = () => Promise.resolve(val)\n return fn\n }\n\n fn.mockImplementation = (newImpl: (...args: unknown[]) => unknown) => {\n impl = newImpl\n return fn\n }\n\n return fn\n}\n\n// ---------------------------------------------------------------------------\n// Published message record\n// ---------------------------------------------------------------------------\n\nexport interface PublishedMessage {\n exchange: string\n routingKey: string\n message: unknown\n options: unknown\n}\n\n// ---------------------------------------------------------------------------\n// MockRabbitMQChannel\n// ---------------------------------------------------------------------------\n\nexport class MockRabbitMQChannel extends EventEmitter {\n private _publishedMessages: PublishedMessage[] = []\n private _consumeCallback: ((msg: unknown) => void) | null = null\n\n isClosed = false\n\n // Stubs\n readonly assertExchange = mockFn().mockResolvedValue({})\n readonly assertQueue = mockFn().mockResolvedValue({ queue: '' })\n readonly bindQueue = mockFn().mockResolvedValue({})\n readonly ack = mockFn()\n readonly nack = mockFn()\n readonly close = mockFn().mockResolvedValue(undefined)\n readonly waitForConfirms = mockFn().mockResolvedValue(undefined)\n readonly prefetch = mockFn().mockResolvedValue(undefined)\n readonly deleteQueue = mockFn().mockResolvedValue({})\n readonly cancel = mockFn().mockResolvedValue(undefined)\n\n /**\n * Tracks all published messages and stores them for later inspection.\n */\n publish(exchange: string, routingKey: string, content: Buffer, options?: unknown): boolean {\n let message: unknown\n try {\n message = JSON.parse(content.toString())\n } catch {\n message = content.toString()\n }\n this._publishedMessages.push({ exchange, routingKey, message, options })\n return true\n }\n\n private _consumerCounter = 0\n\n /**\n * Stores the consumer callback for use with simulateMessage.\n * Returns a unique consumer tag per call.\n */\n consume(_queue: string, callback: (msg: unknown) => void): Promise<{ consumerTag: string }> {\n this._consumeCallback = callback\n const consumerTag = `mock-consumer-${++this._consumerCounter}`\n return Promise.resolve({ consumerTag })\n }\n\n // ---------------------------------------------------------------------------\n // Simulation helpers\n // ---------------------------------------------------------------------------\n\n /**\n * Delivers a valid JSON message to the registered consume callback, with\n * optional AMQP properties such as `headers` or `timestamp`.\n */\n simulateMessage(content: unknown, properties: Partial<RabbitMQMessageProperties> = {}): void {\n this._deliver(Buffer.from(JSON.stringify(content)), properties)\n }\n\n /**\n * Delivers a message with content that is not valid JSON.\n */\n simulateInvalidJsonMessage(): void {\n this._deliver(Buffer.from('not valid json {'))\n }\n\n private _deliver(content: Buffer, properties: Partial<RabbitMQMessageProperties> = {}): void {\n if (!this._consumeCallback) return\n this._consumeCallback({\n content,\n fields: { deliveryTag: 1, redelivered: false, exchange: '', routingKey: '', consumerTag: 'mock-consumer' },\n properties: { headers: {}, ...properties },\n })\n }\n\n /** Emits a close event and marks the channel as closed. */\n simulateClose(): void {\n this.isClosed = true\n this.emit('close')\n }\n\n /** Emits an error event. */\n simulateError(error: Error): void {\n this.emit('error', error)\n }\n\n // ---------------------------------------------------------------------------\n // Inspection helpers\n // ---------------------------------------------------------------------------\n\n getPublishedMessages(): PublishedMessage[] {\n return [...this._publishedMessages]\n }\n\n clearMessages(): void {\n this._publishedMessages = []\n }\n\n /** Resets all state back to initial values. */\n reset(): void {\n this._publishedMessages = []\n this._consumeCallback = null\n this._consumerCounter = 0\n this.isClosed = false\n this.removeAllListeners()\n }\n}\n\n// ---------------------------------------------------------------------------\n// MockRabbitMQConnection\n// ---------------------------------------------------------------------------\n\nexport class MockRabbitMQConnection extends EventEmitter {\n private _channels: MockRabbitMQChannel[] = []\n\n isConnected = true\n\n constructor() {\n super()\n // Create the initial channel eagerly so `conn.channel` is defined\n // before the first createConfirmChannel call.\n this._channels.push(new MockRabbitMQChannel())\n }\n\n /**\n * Returns the most recently created channel.\n */\n get channel(): MockRabbitMQChannel {\n return this._channels[this._channels.length - 1]\n }\n\n /**\n * Returns all channels created so far (including the initial one).\n */\n get channels(): MockRabbitMQChannel[] {\n return [...this._channels]\n }\n\n get channelCount(): number {\n return this._channels.length\n }\n\n /**\n * Creates a new channel (simulates a reconnection / new confirm-channel).\n */\n createConfirmChannel(): Promise<MockRabbitMQChannel> {\n const ch = new MockRabbitMQChannel()\n this._channels.push(ch)\n return Promise.resolve(ch)\n }\n\n close(): Promise<void> {\n this.isConnected = false\n return Promise.resolve()\n }\n\n /** Emits a close event and marks the connection as disconnected. */\n simulateClose(): void {\n this.isConnected = false\n this.emit('close')\n }\n\n /** Emits an error event. */\n simulateError(error: Error): void {\n this.emit('error', error)\n }\n\n /** Resets to the initial connected state with a single fresh channel. */\n reset(): void {\n this._channels = [new MockRabbitMQChannel()]\n this.isConnected = true\n this.removeAllListeners()\n }\n}\n\n// ---------------------------------------------------------------------------\n// createMockAmqplib\n// ---------------------------------------------------------------------------\n\nexport interface AmqpMock {\n connect: MockFn\n}\n\nexport interface MockAmqplibResult {\n mockConnection: MockRabbitMQConnection\n amqpMock: AmqpMock\n}\n\n/**\n * Returns a pre-wired { mockConnection, amqpMock } pair that can be used as\n * a drop-in replacement for amqplib in tests (e.g. via vi.mock or jest.mock).\n */\nexport function createMockAmqplib(): MockAmqplibResult {\n const mockConnection = new MockRabbitMQConnection()\n const amqpMock: AmqpMock = {\n connect: mockFn().mockResolvedValue(mockConnection),\n }\n return { mockConnection, amqpMock }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAA6B;AAc7B,SAAS,SAAiB;AACxB,MAAI,OAAiD;AACrD,QAAM,QAAqB,CAAC;AAE5B,QAAM,MAAM,IAAI,SAAoB;AAClC,UAAM,KAAK,IAAI;AACf,WAAO,OAAO,KAAK,GAAG,IAAI,IAAI;AAAA,EAChC;AAEA,KAAG,OAAO,EAAE,MAAM;AAElB,KAAG,oBAAoB,CAAC,QAAiB;AACvC,WAAO,MAAM,QAAQ,QAAQ,GAAG;AAChC,WAAO;AAAA,EACT;AAEA,KAAG,qBAAqB,CAAC,YAA6C;AACpE,WAAO;AACP,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAiBO,IAAM,sBAAN,cAAkC,2BAAa;AAAA,EAC5C,qBAAyC,CAAC;AAAA,EAC1C,mBAAoD;AAAA,EAE5D,WAAW;AAAA;AAAA,EAGF,iBAAiB,OAAO,EAAE,kBAAkB,CAAC,CAAC;AAAA,EAC9C,cAAc,OAAO,EAAE,kBAAkB,EAAE,OAAO,GAAG,CAAC;AAAA,EACtD,YAAY,OAAO,EAAE,kBAAkB,CAAC,CAAC;AAAA,EACzC,MAAM,OAAO;AAAA,EACb,OAAO,OAAO;AAAA,EACd,QAAQ,OAAO,EAAE,kBAAkB,MAAS;AAAA,EAC5C,kBAAkB,OAAO,EAAE,kBAAkB,MAAS;AAAA,EACtD,WAAW,OAAO,EAAE,kBAAkB,MAAS;AAAA,EAC/C,cAAc,OAAO,EAAE,kBAAkB,CAAC,CAAC;AAAA,EAC3C,SAAS,OAAO,EAAE,kBAAkB,MAAS;AAAA;AAAA;AAAA;AAAA,EAKtD,QAAQ,UAAkB,YAAoB,SAAiB,SAA4B;AACzF,QAAI;AACJ,QAAI;AACF,gBAAU,KAAK,MAAM,QAAQ,SAAS,CAAC;AAAA,IACzC,QAAQ;AACN,gBAAU,QAAQ,SAAS;AAAA,IAC7B;AACA,SAAK,mBAAmB,KAAK,EAAE,UAAU,YAAY,SAAS,QAAQ,CAAC;AACvE,WAAO;AAAA,EACT;AAAA,EAEQ,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA,EAM3B,QAAQ,QAAgB,UAAoE;AAC1F,SAAK,mBAAmB;AACxB,UAAM,cAAc,iBAAiB,EAAE,KAAK,gBAAgB;AAC5D,WAAO,QAAQ,QAAQ,EAAE,YAAY,CAAC;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,gBAAgB,SAAkB,aAAiD,CAAC,GAAS;AAC3F,SAAK,SAAS,OAAO,KAAK,KAAK,UAAU,OAAO,CAAC,GAAG,UAAU;AAAA,EAChE;AAAA;AAAA;AAAA;AAAA,EAKA,6BAAmC;AACjC,SAAK,SAAS,OAAO,KAAK,kBAAkB,CAAC;AAAA,EAC/C;AAAA,EAEQ,SAAS,SAAiB,aAAiD,CAAC,GAAS;AAC3F,QAAI,CAAC,KAAK,iBAAkB;AAC5B,SAAK,iBAAiB;AAAA,MACpB;AAAA,MACA,QAAQ,EAAE,aAAa,GAAG,aAAa,OAAO,UAAU,IAAI,YAAY,IAAI,aAAa,gBAAgB;AAAA,MACzG,YAAY,EAAE,SAAS,CAAC,GAAG,GAAG,WAAW;AAAA,IAC3C,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,gBAAsB;AACpB,SAAK,WAAW;AAChB,SAAK,KAAK,OAAO;AAAA,EACnB;AAAA;AAAA,EAGA,cAAc,OAAoB;AAChC,SAAK,KAAK,SAAS,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAMA,uBAA2C;AACzC,WAAO,CAAC,GAAG,KAAK,kBAAkB;AAAA,EACpC;AAAA,EAEA,gBAAsB;AACpB,SAAK,qBAAqB,CAAC;AAAA,EAC7B;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,qBAAqB,CAAC;AAC3B,SAAK,mBAAmB;AACxB,SAAK,mBAAmB;AACxB,SAAK,WAAW;AAChB,SAAK,mBAAmB;AAAA,EAC1B;AACF;AAMO,IAAM,yBAAN,cAAqC,2BAAa;AAAA,EAC/C,YAAmC,CAAC;AAAA,EAE5C,cAAc;AAAA,EAEd,cAAc;AACZ,UAAM;AAGN,SAAK,UAAU,KAAK,IAAI,oBAAoB,CAAC;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,UAA+B;AACjC,WAAO,KAAK,UAAU,KAAK,UAAU,SAAS,CAAC;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,WAAkC;AACpC,WAAO,CAAC,GAAG,KAAK,SAAS;AAAA,EAC3B;AAAA,EAEA,IAAI,eAAuB;AACzB,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,uBAAqD;AACnD,UAAM,KAAK,IAAI,oBAAoB;AACnC,SAAK,UAAU,KAAK,EAAE;AACtB,WAAO,QAAQ,QAAQ,EAAE;AAAA,EAC3B;AAAA,EAEA,QAAuB;AACrB,SAAK,cAAc;AACnB,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA;AAAA,EAGA,gBAAsB;AACpB,SAAK,cAAc;AACnB,SAAK,KAAK,OAAO;AAAA,EACnB;AAAA;AAAA,EAGA,cAAc,OAAoB;AAChC,SAAK,KAAK,SAAS,KAAK;AAAA,EAC1B;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,YAAY,CAAC,IAAI,oBAAoB,CAAC;AAC3C,SAAK,cAAc;AACnB,SAAK,mBAAmB;AAAA,EAC1B;AACF;AAmBO,SAAS,oBAAuC;AACrD,QAAM,iBAAiB,IAAI,uBAAuB;AAClD,QAAM,WAAqB;AAAA,IACzB,SAAS,OAAO,EAAE,kBAAkB,cAAc;AAAA,EACpD;AACA,SAAO,EAAE,gBAAgB,SAAS;AACpC;","names":[]}
1
+ {"version":3,"sources":["../../src/testing/index.ts"],"sourcesContent":["import { EventEmitter } from 'events'\nimport type { RabbitMQMessageProperties } from '../types.js'\n\n// ---------------------------------------------------------------------------\n// Framework-agnostic mock function factory\n// ---------------------------------------------------------------------------\n\ninterface MockFn {\n (...args: unknown[]): unknown\n mock: { calls: unknown[][] }\n mockResolvedValue: (val: unknown) => MockFn\n mockImplementation: (fn: (...args: unknown[]) => unknown) => MockFn\n}\n\nfunction mockFn(): MockFn {\n let impl: ((...args: unknown[]) => unknown) | null = null\n const calls: unknown[][] = []\n\n const fn = ((...args: unknown[]) => {\n calls.push(args)\n return impl ? impl(...args) : undefined\n }) as MockFn\n\n fn.mock = { calls }\n\n fn.mockResolvedValue = (val: unknown) => {\n impl = () => Promise.resolve(val)\n return fn\n }\n\n fn.mockImplementation = (newImpl: (...args: unknown[]) => unknown) => {\n impl = newImpl\n return fn\n }\n\n return fn\n}\n\n// ---------------------------------------------------------------------------\n// Published message record\n// ---------------------------------------------------------------------------\n\nexport interface PublishedMessage {\n exchange: string\n routingKey: string\n message: unknown\n options: unknown\n}\n\n// ---------------------------------------------------------------------------\n// MockRabbitMQChannel\n// ---------------------------------------------------------------------------\n\nexport class MockRabbitMQChannel extends EventEmitter {\n private _publishedMessages: PublishedMessage[] = []\n private _consumeCallback: ((msg: unknown) => void) | null = null\n\n isClosed = false\n\n // Stubs\n readonly assertExchange = mockFn().mockResolvedValue({})\n readonly checkExchange = mockFn().mockResolvedValue({})\n readonly assertQueue = mockFn().mockResolvedValue({ queue: '' })\n readonly bindQueue = mockFn().mockResolvedValue({})\n readonly ack = mockFn()\n readonly nack = mockFn()\n readonly close = mockFn().mockResolvedValue(undefined)\n readonly waitForConfirms = mockFn().mockResolvedValue(undefined)\n readonly prefetch = mockFn().mockResolvedValue(undefined)\n readonly deleteQueue = mockFn().mockResolvedValue({})\n readonly cancel = mockFn().mockResolvedValue(undefined)\n\n /**\n * Tracks all published messages and stores them for later inspection.\n */\n publish(exchange: string, routingKey: string, content: Buffer, options?: unknown): boolean {\n let message: unknown\n try {\n message = JSON.parse(content.toString())\n } catch {\n message = content.toString()\n }\n this._publishedMessages.push({ exchange, routingKey, message, options })\n return true\n }\n\n private _consumerCounter = 0\n\n /**\n * Stores the consumer callback for use with simulateMessage.\n * Returns a unique consumer tag per call.\n */\n consume(_queue: string, callback: (msg: unknown) => void): Promise<{ consumerTag: string }> {\n this._consumeCallback = callback\n const consumerTag = `mock-consumer-${++this._consumerCounter}`\n return Promise.resolve({ consumerTag })\n }\n\n // ---------------------------------------------------------------------------\n // Simulation helpers\n // ---------------------------------------------------------------------------\n\n /**\n * Delivers a valid JSON message to the registered consume callback, with\n * optional AMQP properties such as `headers` or `timestamp`.\n */\n simulateMessage(content: unknown, properties: Partial<RabbitMQMessageProperties> = {}): void {\n this._deliver(Buffer.from(JSON.stringify(content)), properties)\n }\n\n /**\n * Delivers a message with content that is not valid JSON.\n */\n simulateInvalidJsonMessage(): void {\n this._deliver(Buffer.from('not valid json {'))\n }\n\n private _deliver(content: Buffer, properties: Partial<RabbitMQMessageProperties> = {}): void {\n if (!this._consumeCallback) return\n const { exchange = '', routingKey = '', ...amqpProperties } = properties\n this._consumeCallback({\n content,\n fields: { deliveryTag: 1, redelivered: false, exchange, routingKey, consumerTag: 'mock-consumer' },\n properties: { headers: {}, ...amqpProperties },\n })\n }\n\n /** Emits a close event and marks the channel as closed. */\n simulateClose(): void {\n this.isClosed = true\n this.emit('close')\n }\n\n /** Emits an error event. */\n simulateError(error: Error): void {\n this.emit('error', error)\n }\n\n // ---------------------------------------------------------------------------\n // Inspection helpers\n // ---------------------------------------------------------------------------\n\n getPublishedMessages(): PublishedMessage[] {\n return [...this._publishedMessages]\n }\n\n clearMessages(): void {\n this._publishedMessages = []\n }\n\n /** Resets all state back to initial values. */\n reset(): void {\n this._publishedMessages = []\n this._consumeCallback = null\n this._consumerCounter = 0\n this.isClosed = false\n this.removeAllListeners()\n }\n}\n\n// ---------------------------------------------------------------------------\n// MockRabbitMQConnection\n// ---------------------------------------------------------------------------\n\nexport class MockRabbitMQConnection extends EventEmitter {\n private _channels: MockRabbitMQChannel[] = []\n\n isConnected = true\n\n constructor() {\n super()\n // Create the initial channel eagerly so `conn.channel` is defined\n // before the first createConfirmChannel call.\n this._channels.push(new MockRabbitMQChannel())\n }\n\n /**\n * Returns the most recently created channel.\n */\n get channel(): MockRabbitMQChannel {\n return this._channels[this._channels.length - 1]\n }\n\n /**\n * Returns all channels created so far (including the initial one).\n */\n get channels(): MockRabbitMQChannel[] {\n return [...this._channels]\n }\n\n get channelCount(): number {\n return this._channels.length\n }\n\n /**\n * Creates a new channel (simulates a reconnection / new confirm-channel).\n */\n createConfirmChannel(): Promise<MockRabbitMQChannel> {\n const ch = new MockRabbitMQChannel()\n this._channels.push(ch)\n return Promise.resolve(ch)\n }\n\n /** Creates an untracked plain channel, as used for passive exchange checks. */\n createChannel(): Promise<MockRabbitMQChannel> {\n return Promise.resolve(new MockRabbitMQChannel())\n }\n\n close(): Promise<void> {\n this.isConnected = false\n return Promise.resolve()\n }\n\n /** Emits a close event and marks the connection as disconnected. */\n simulateClose(): void {\n this.isConnected = false\n this.emit('close')\n }\n\n /** Emits an error event. */\n simulateError(error: Error): void {\n this.emit('error', error)\n }\n\n /** Resets to the initial connected state with a single fresh channel. */\n reset(): void {\n this._channels = [new MockRabbitMQChannel()]\n this.isConnected = true\n this.removeAllListeners()\n }\n}\n\n// ---------------------------------------------------------------------------\n// createMockAmqplib\n// ---------------------------------------------------------------------------\n\nexport interface AmqpMock {\n connect: MockFn\n}\n\nexport interface MockAmqplibResult {\n mockConnection: MockRabbitMQConnection\n amqpMock: AmqpMock\n}\n\n/**\n * Returns a pre-wired { mockConnection, amqpMock } pair that can be used as\n * a drop-in replacement for amqplib in tests (e.g. via vi.mock or jest.mock).\n */\nexport function createMockAmqplib(): MockAmqplibResult {\n const mockConnection = new MockRabbitMQConnection()\n const amqpMock: AmqpMock = {\n connect: mockFn().mockResolvedValue(mockConnection),\n }\n return { mockConnection, amqpMock }\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from 'events';
2
- import { d as RabbitMQMessageProperties } from '../types-D29GyoZX.cjs';
2
+ import { e as RabbitMQMessageProperties } from '../types-Cjg-65Ef.cjs';
3
3
 
4
4
  interface MockFn {
5
5
  (...args: unknown[]): unknown;
@@ -20,6 +20,7 @@ declare class MockRabbitMQChannel extends EventEmitter {
20
20
  private _consumeCallback;
21
21
  isClosed: boolean;
22
22
  readonly assertExchange: MockFn;
23
+ readonly checkExchange: MockFn;
23
24
  readonly assertQueue: MockFn;
24
25
  readonly bindQueue: MockFn;
25
26
  readonly ack: MockFn;
@@ -77,6 +78,8 @@ declare class MockRabbitMQConnection extends EventEmitter {
77
78
  * Creates a new channel (simulates a reconnection / new confirm-channel).
78
79
  */
79
80
  createConfirmChannel(): Promise<MockRabbitMQChannel>;
81
+ /** Creates an untracked plain channel, as used for passive exchange checks. */
82
+ createChannel(): Promise<MockRabbitMQChannel>;
80
83
  close(): Promise<void>;
81
84
  /** Emits a close event and marks the connection as disconnected. */
82
85
  simulateClose(): void;
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from 'events';
2
- import { d as RabbitMQMessageProperties } from '../types-D29GyoZX.js';
2
+ import { e as RabbitMQMessageProperties } from '../types-Cjg-65Ef.js';
3
3
 
4
4
  interface MockFn {
5
5
  (...args: unknown[]): unknown;
@@ -20,6 +20,7 @@ declare class MockRabbitMQChannel extends EventEmitter {
20
20
  private _consumeCallback;
21
21
  isClosed: boolean;
22
22
  readonly assertExchange: MockFn;
23
+ readonly checkExchange: MockFn;
23
24
  readonly assertQueue: MockFn;
24
25
  readonly bindQueue: MockFn;
25
26
  readonly ack: MockFn;
@@ -77,6 +78,8 @@ declare class MockRabbitMQConnection extends EventEmitter {
77
78
  * Creates a new channel (simulates a reconnection / new confirm-channel).
78
79
  */
79
80
  createConfirmChannel(): Promise<MockRabbitMQChannel>;
81
+ /** Creates an untracked plain channel, as used for passive exchange checks. */
82
+ createChannel(): Promise<MockRabbitMQChannel>;
80
83
  close(): Promise<void>;
81
84
  /** Emits a close event and marks the connection as disconnected. */
82
85
  simulateClose(): void;
@@ -24,6 +24,7 @@ var MockRabbitMQChannel = class extends EventEmitter {
24
24
  isClosed = false;
25
25
  // Stubs
26
26
  assertExchange = mockFn().mockResolvedValue({});
27
+ checkExchange = mockFn().mockResolvedValue({});
27
28
  assertQueue = mockFn().mockResolvedValue({ queue: "" });
28
29
  bindQueue = mockFn().mockResolvedValue({});
29
30
  ack = mockFn();
@@ -74,10 +75,11 @@ var MockRabbitMQChannel = class extends EventEmitter {
74
75
  }
75
76
  _deliver(content, properties = {}) {
76
77
  if (!this._consumeCallback) return;
78
+ const { exchange = "", routingKey = "", ...amqpProperties } = properties;
77
79
  this._consumeCallback({
78
80
  content,
79
- fields: { deliveryTag: 1, redelivered: false, exchange: "", routingKey: "", consumerTag: "mock-consumer" },
80
- properties: { headers: {}, ...properties }
81
+ fields: { deliveryTag: 1, redelivered: false, exchange, routingKey, consumerTag: "mock-consumer" },
82
+ properties: { headers: {}, ...amqpProperties }
81
83
  });
82
84
  }
83
85
  /** Emits a close event and marks the channel as closed. */
@@ -137,6 +139,10 @@ var MockRabbitMQConnection = class extends EventEmitter {
137
139
  this._channels.push(ch);
138
140
  return Promise.resolve(ch);
139
141
  }
142
+ /** Creates an untracked plain channel, as used for passive exchange checks. */
143
+ createChannel() {
144
+ return Promise.resolve(new MockRabbitMQChannel());
145
+ }
140
146
  close() {
141
147
  this.isConnected = false;
142
148
  return Promise.resolve();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/testing/index.ts"],"sourcesContent":["import { EventEmitter } from 'events'\nimport type { RabbitMQMessageProperties } from '../types.js'\n\n// ---------------------------------------------------------------------------\n// Framework-agnostic mock function factory\n// ---------------------------------------------------------------------------\n\ninterface MockFn {\n (...args: unknown[]): unknown\n mock: { calls: unknown[][] }\n mockResolvedValue: (val: unknown) => MockFn\n mockImplementation: (fn: (...args: unknown[]) => unknown) => MockFn\n}\n\nfunction mockFn(): MockFn {\n let impl: ((...args: unknown[]) => unknown) | null = null\n const calls: unknown[][] = []\n\n const fn = ((...args: unknown[]) => {\n calls.push(args)\n return impl ? impl(...args) : undefined\n }) as MockFn\n\n fn.mock = { calls }\n\n fn.mockResolvedValue = (val: unknown) => {\n impl = () => Promise.resolve(val)\n return fn\n }\n\n fn.mockImplementation = (newImpl: (...args: unknown[]) => unknown) => {\n impl = newImpl\n return fn\n }\n\n return fn\n}\n\n// ---------------------------------------------------------------------------\n// Published message record\n// ---------------------------------------------------------------------------\n\nexport interface PublishedMessage {\n exchange: string\n routingKey: string\n message: unknown\n options: unknown\n}\n\n// ---------------------------------------------------------------------------\n// MockRabbitMQChannel\n// ---------------------------------------------------------------------------\n\nexport class MockRabbitMQChannel extends EventEmitter {\n private _publishedMessages: PublishedMessage[] = []\n private _consumeCallback: ((msg: unknown) => void) | null = null\n\n isClosed = false\n\n // Stubs\n readonly assertExchange = mockFn().mockResolvedValue({})\n readonly assertQueue = mockFn().mockResolvedValue({ queue: '' })\n readonly bindQueue = mockFn().mockResolvedValue({})\n readonly ack = mockFn()\n readonly nack = mockFn()\n readonly close = mockFn().mockResolvedValue(undefined)\n readonly waitForConfirms = mockFn().mockResolvedValue(undefined)\n readonly prefetch = mockFn().mockResolvedValue(undefined)\n readonly deleteQueue = mockFn().mockResolvedValue({})\n readonly cancel = mockFn().mockResolvedValue(undefined)\n\n /**\n * Tracks all published messages and stores them for later inspection.\n */\n publish(exchange: string, routingKey: string, content: Buffer, options?: unknown): boolean {\n let message: unknown\n try {\n message = JSON.parse(content.toString())\n } catch {\n message = content.toString()\n }\n this._publishedMessages.push({ exchange, routingKey, message, options })\n return true\n }\n\n private _consumerCounter = 0\n\n /**\n * Stores the consumer callback for use with simulateMessage.\n * Returns a unique consumer tag per call.\n */\n consume(_queue: string, callback: (msg: unknown) => void): Promise<{ consumerTag: string }> {\n this._consumeCallback = callback\n const consumerTag = `mock-consumer-${++this._consumerCounter}`\n return Promise.resolve({ consumerTag })\n }\n\n // ---------------------------------------------------------------------------\n // Simulation helpers\n // ---------------------------------------------------------------------------\n\n /**\n * Delivers a valid JSON message to the registered consume callback, with\n * optional AMQP properties such as `headers` or `timestamp`.\n */\n simulateMessage(content: unknown, properties: Partial<RabbitMQMessageProperties> = {}): void {\n this._deliver(Buffer.from(JSON.stringify(content)), properties)\n }\n\n /**\n * Delivers a message with content that is not valid JSON.\n */\n simulateInvalidJsonMessage(): void {\n this._deliver(Buffer.from('not valid json {'))\n }\n\n private _deliver(content: Buffer, properties: Partial<RabbitMQMessageProperties> = {}): void {\n if (!this._consumeCallback) return\n this._consumeCallback({\n content,\n fields: { deliveryTag: 1, redelivered: false, exchange: '', routingKey: '', consumerTag: 'mock-consumer' },\n properties: { headers: {}, ...properties },\n })\n }\n\n /** Emits a close event and marks the channel as closed. */\n simulateClose(): void {\n this.isClosed = true\n this.emit('close')\n }\n\n /** Emits an error event. */\n simulateError(error: Error): void {\n this.emit('error', error)\n }\n\n // ---------------------------------------------------------------------------\n // Inspection helpers\n // ---------------------------------------------------------------------------\n\n getPublishedMessages(): PublishedMessage[] {\n return [...this._publishedMessages]\n }\n\n clearMessages(): void {\n this._publishedMessages = []\n }\n\n /** Resets all state back to initial values. */\n reset(): void {\n this._publishedMessages = []\n this._consumeCallback = null\n this._consumerCounter = 0\n this.isClosed = false\n this.removeAllListeners()\n }\n}\n\n// ---------------------------------------------------------------------------\n// MockRabbitMQConnection\n// ---------------------------------------------------------------------------\n\nexport class MockRabbitMQConnection extends EventEmitter {\n private _channels: MockRabbitMQChannel[] = []\n\n isConnected = true\n\n constructor() {\n super()\n // Create the initial channel eagerly so `conn.channel` is defined\n // before the first createConfirmChannel call.\n this._channels.push(new MockRabbitMQChannel())\n }\n\n /**\n * Returns the most recently created channel.\n */\n get channel(): MockRabbitMQChannel {\n return this._channels[this._channels.length - 1]\n }\n\n /**\n * Returns all channels created so far (including the initial one).\n */\n get channels(): MockRabbitMQChannel[] {\n return [...this._channels]\n }\n\n get channelCount(): number {\n return this._channels.length\n }\n\n /**\n * Creates a new channel (simulates a reconnection / new confirm-channel).\n */\n createConfirmChannel(): Promise<MockRabbitMQChannel> {\n const ch = new MockRabbitMQChannel()\n this._channels.push(ch)\n return Promise.resolve(ch)\n }\n\n close(): Promise<void> {\n this.isConnected = false\n return Promise.resolve()\n }\n\n /** Emits a close event and marks the connection as disconnected. */\n simulateClose(): void {\n this.isConnected = false\n this.emit('close')\n }\n\n /** Emits an error event. */\n simulateError(error: Error): void {\n this.emit('error', error)\n }\n\n /** Resets to the initial connected state with a single fresh channel. */\n reset(): void {\n this._channels = [new MockRabbitMQChannel()]\n this.isConnected = true\n this.removeAllListeners()\n }\n}\n\n// ---------------------------------------------------------------------------\n// createMockAmqplib\n// ---------------------------------------------------------------------------\n\nexport interface AmqpMock {\n connect: MockFn\n}\n\nexport interface MockAmqplibResult {\n mockConnection: MockRabbitMQConnection\n amqpMock: AmqpMock\n}\n\n/**\n * Returns a pre-wired { mockConnection, amqpMock } pair that can be used as\n * a drop-in replacement for amqplib in tests (e.g. via vi.mock or jest.mock).\n */\nexport function createMockAmqplib(): MockAmqplibResult {\n const mockConnection = new MockRabbitMQConnection()\n const amqpMock: AmqpMock = {\n connect: mockFn().mockResolvedValue(mockConnection),\n }\n return { mockConnection, amqpMock }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/testing/index.ts"],"sourcesContent":["import { EventEmitter } from 'events'\nimport type { RabbitMQMessageProperties } from '../types.js'\n\n// ---------------------------------------------------------------------------\n// Framework-agnostic mock function factory\n// ---------------------------------------------------------------------------\n\ninterface MockFn {\n (...args: unknown[]): unknown\n mock: { calls: unknown[][] }\n mockResolvedValue: (val: unknown) => MockFn\n mockImplementation: (fn: (...args: unknown[]) => unknown) => MockFn\n}\n\nfunction mockFn(): MockFn {\n let impl: ((...args: unknown[]) => unknown) | null = null\n const calls: unknown[][] = []\n\n const fn = ((...args: unknown[]) => {\n calls.push(args)\n return impl ? impl(...args) : undefined\n }) as MockFn\n\n fn.mock = { calls }\n\n fn.mockResolvedValue = (val: unknown) => {\n impl = () => Promise.resolve(val)\n return fn\n }\n\n fn.mockImplementation = (newImpl: (...args: unknown[]) => unknown) => {\n impl = newImpl\n return fn\n }\n\n return fn\n}\n\n// ---------------------------------------------------------------------------\n// Published message record\n// ---------------------------------------------------------------------------\n\nexport interface PublishedMessage {\n exchange: string\n routingKey: string\n message: unknown\n options: unknown\n}\n\n// ---------------------------------------------------------------------------\n// MockRabbitMQChannel\n// ---------------------------------------------------------------------------\n\nexport class MockRabbitMQChannel extends EventEmitter {\n private _publishedMessages: PublishedMessage[] = []\n private _consumeCallback: ((msg: unknown) => void) | null = null\n\n isClosed = false\n\n // Stubs\n readonly assertExchange = mockFn().mockResolvedValue({})\n readonly checkExchange = mockFn().mockResolvedValue({})\n readonly assertQueue = mockFn().mockResolvedValue({ queue: '' })\n readonly bindQueue = mockFn().mockResolvedValue({})\n readonly ack = mockFn()\n readonly nack = mockFn()\n readonly close = mockFn().mockResolvedValue(undefined)\n readonly waitForConfirms = mockFn().mockResolvedValue(undefined)\n readonly prefetch = mockFn().mockResolvedValue(undefined)\n readonly deleteQueue = mockFn().mockResolvedValue({})\n readonly cancel = mockFn().mockResolvedValue(undefined)\n\n /**\n * Tracks all published messages and stores them for later inspection.\n */\n publish(exchange: string, routingKey: string, content: Buffer, options?: unknown): boolean {\n let message: unknown\n try {\n message = JSON.parse(content.toString())\n } catch {\n message = content.toString()\n }\n this._publishedMessages.push({ exchange, routingKey, message, options })\n return true\n }\n\n private _consumerCounter = 0\n\n /**\n * Stores the consumer callback for use with simulateMessage.\n * Returns a unique consumer tag per call.\n */\n consume(_queue: string, callback: (msg: unknown) => void): Promise<{ consumerTag: string }> {\n this._consumeCallback = callback\n const consumerTag = `mock-consumer-${++this._consumerCounter}`\n return Promise.resolve({ consumerTag })\n }\n\n // ---------------------------------------------------------------------------\n // Simulation helpers\n // ---------------------------------------------------------------------------\n\n /**\n * Delivers a valid JSON message to the registered consume callback, with\n * optional AMQP properties such as `headers` or `timestamp`.\n */\n simulateMessage(content: unknown, properties: Partial<RabbitMQMessageProperties> = {}): void {\n this._deliver(Buffer.from(JSON.stringify(content)), properties)\n }\n\n /**\n * Delivers a message with content that is not valid JSON.\n */\n simulateInvalidJsonMessage(): void {\n this._deliver(Buffer.from('not valid json {'))\n }\n\n private _deliver(content: Buffer, properties: Partial<RabbitMQMessageProperties> = {}): void {\n if (!this._consumeCallback) return\n const { exchange = '', routingKey = '', ...amqpProperties } = properties\n this._consumeCallback({\n content,\n fields: { deliveryTag: 1, redelivered: false, exchange, routingKey, consumerTag: 'mock-consumer' },\n properties: { headers: {}, ...amqpProperties },\n })\n }\n\n /** Emits a close event and marks the channel as closed. */\n simulateClose(): void {\n this.isClosed = true\n this.emit('close')\n }\n\n /** Emits an error event. */\n simulateError(error: Error): void {\n this.emit('error', error)\n }\n\n // ---------------------------------------------------------------------------\n // Inspection helpers\n // ---------------------------------------------------------------------------\n\n getPublishedMessages(): PublishedMessage[] {\n return [...this._publishedMessages]\n }\n\n clearMessages(): void {\n this._publishedMessages = []\n }\n\n /** Resets all state back to initial values. */\n reset(): void {\n this._publishedMessages = []\n this._consumeCallback = null\n this._consumerCounter = 0\n this.isClosed = false\n this.removeAllListeners()\n }\n}\n\n// ---------------------------------------------------------------------------\n// MockRabbitMQConnection\n// ---------------------------------------------------------------------------\n\nexport class MockRabbitMQConnection extends EventEmitter {\n private _channels: MockRabbitMQChannel[] = []\n\n isConnected = true\n\n constructor() {\n super()\n // Create the initial channel eagerly so `conn.channel` is defined\n // before the first createConfirmChannel call.\n this._channels.push(new MockRabbitMQChannel())\n }\n\n /**\n * Returns the most recently created channel.\n */\n get channel(): MockRabbitMQChannel {\n return this._channels[this._channels.length - 1]\n }\n\n /**\n * Returns all channels created so far (including the initial one).\n */\n get channels(): MockRabbitMQChannel[] {\n return [...this._channels]\n }\n\n get channelCount(): number {\n return this._channels.length\n }\n\n /**\n * Creates a new channel (simulates a reconnection / new confirm-channel).\n */\n createConfirmChannel(): Promise<MockRabbitMQChannel> {\n const ch = new MockRabbitMQChannel()\n this._channels.push(ch)\n return Promise.resolve(ch)\n }\n\n /** Creates an untracked plain channel, as used for passive exchange checks. */\n createChannel(): Promise<MockRabbitMQChannel> {\n return Promise.resolve(new MockRabbitMQChannel())\n }\n\n close(): Promise<void> {\n this.isConnected = false\n return Promise.resolve()\n }\n\n /** Emits a close event and marks the connection as disconnected. */\n simulateClose(): void {\n this.isConnected = false\n this.emit('close')\n }\n\n /** Emits an error event. */\n simulateError(error: Error): void {\n this.emit('error', error)\n }\n\n /** Resets to the initial connected state with a single fresh channel. */\n reset(): void {\n this._channels = [new MockRabbitMQChannel()]\n this.isConnected = true\n this.removeAllListeners()\n }\n}\n\n// ---------------------------------------------------------------------------\n// createMockAmqplib\n// ---------------------------------------------------------------------------\n\nexport interface AmqpMock {\n connect: MockFn\n}\n\nexport interface MockAmqplibResult {\n mockConnection: MockRabbitMQConnection\n amqpMock: AmqpMock\n}\n\n/**\n * Returns a pre-wired { mockConnection, amqpMock } pair that can be used as\n * a drop-in replacement for amqplib in tests (e.g. via vi.mock or jest.mock).\n */\nexport function createMockAmqplib(): MockAmqplibResult {\n const mockConnection = new MockRabbitMQConnection()\n const amqpMock: AmqpMock = {\n connect: mockFn().mockResolvedValue(mockConnection),\n }\n return { mockConnection, amqpMock }\n}\n"],"mappings":"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","names":[]}
@@ -39,7 +39,7 @@ interface RabbitMQHooks {
39
39
  exchange: string;
40
40
  routingKey: string;
41
41
  duration: number;
42
- reason: 'invalid_json' | 'max_retries_exhausted';
42
+ reason: 'invalid_json' | 'max_retries_exhausted' | 'dead_letter_error';
43
43
  }) => void;
44
44
  /** Called after reconnection completes and subscriptions are re-established */
45
45
  onReconnect?: (info: {
@@ -60,6 +60,27 @@ interface SubscribeOptions {
60
60
  * at once, and only throttles when set below `prefetch`. `0` means no limit.
61
61
  */
62
62
  concurrency?: number;
63
+ /** More exchanges and routing keys to bind the same queue to, consumed by the same handler */
64
+ bindings?: RabbitMQBinding[];
65
+ /** Dead-letter exchange name (default: `<exchange>.dlx`) */
66
+ deadLetterExchange?: string;
67
+ /**
68
+ * Check that the source exchanges exist instead of declaring them, for
69
+ * exchanges another service owns. Subscribing fails when one is missing.
70
+ */
71
+ passiveExchanges?: boolean;
72
+ /** Max handler attempts before the DLQ for this queue (overrides the client default); `Infinity` retries until the handler succeeds */
73
+ maxRetries?: number;
74
+ /**
75
+ * Double the delay between handler attempts, from `retryDelay` up to this
76
+ * many ms. Without it the delay stays at `retryDelay`.
77
+ */
78
+ maxRetryDelay?: number;
79
+ }
80
+ /** An exchange and routing key a queue is bound to */
81
+ interface RabbitMQBinding {
82
+ exchange: string;
83
+ routingKey: string;
63
84
  }
64
85
  /**
65
86
  * Configuration options for the RabbitMQ client.
@@ -119,6 +140,10 @@ interface PublishOptions {
119
140
  type RabbitMQMessage = Record<string, unknown>;
120
141
  /** What the publisher and the broker attached to a consumed message */
121
142
  interface RabbitMQMessageProperties {
143
+ /** Exchange the message was published to */
144
+ exchange: string;
145
+ /** Routing key the message was published with */
146
+ routingKey: string;
122
147
  /** AMQP headers, including broker-added ones such as `x-death` on a dead-lettered message */
123
148
  headers: Record<string, unknown>;
124
149
  /** AMQP timestamp in seconds, if the publisher set one */
@@ -139,4 +164,4 @@ interface RabbitMQSubscription {
139
164
  options?: SubscribeOptions;
140
165
  }
141
166
 
142
- export type { ILogger as I, PublishOptions as P, RabbitMQClientOptions as R, SubscribeOptions as S, RabbitMQMessage as a, RabbitMQMessageHandler as b, RabbitMQHooks as c, RabbitMQMessageProperties as d, RabbitMQSubscription as e };
167
+ export type { ILogger as I, PublishOptions as P, RabbitMQClientOptions as R, SubscribeOptions as S, RabbitMQMessage as a, RabbitMQMessageHandler as b, RabbitMQBinding as c, RabbitMQHooks as d, RabbitMQMessageProperties as e, RabbitMQSubscription as f };
@@ -39,7 +39,7 @@ interface RabbitMQHooks {
39
39
  exchange: string;
40
40
  routingKey: string;
41
41
  duration: number;
42
- reason: 'invalid_json' | 'max_retries_exhausted';
42
+ reason: 'invalid_json' | 'max_retries_exhausted' | 'dead_letter_error';
43
43
  }) => void;
44
44
  /** Called after reconnection completes and subscriptions are re-established */
45
45
  onReconnect?: (info: {
@@ -60,6 +60,27 @@ interface SubscribeOptions {
60
60
  * at once, and only throttles when set below `prefetch`. `0` means no limit.
61
61
  */
62
62
  concurrency?: number;
63
+ /** More exchanges and routing keys to bind the same queue to, consumed by the same handler */
64
+ bindings?: RabbitMQBinding[];
65
+ /** Dead-letter exchange name (default: `<exchange>.dlx`) */
66
+ deadLetterExchange?: string;
67
+ /**
68
+ * Check that the source exchanges exist instead of declaring them, for
69
+ * exchanges another service owns. Subscribing fails when one is missing.
70
+ */
71
+ passiveExchanges?: boolean;
72
+ /** Max handler attempts before the DLQ for this queue (overrides the client default); `Infinity` retries until the handler succeeds */
73
+ maxRetries?: number;
74
+ /**
75
+ * Double the delay between handler attempts, from `retryDelay` up to this
76
+ * many ms. Without it the delay stays at `retryDelay`.
77
+ */
78
+ maxRetryDelay?: number;
79
+ }
80
+ /** An exchange and routing key a queue is bound to */
81
+ interface RabbitMQBinding {
82
+ exchange: string;
83
+ routingKey: string;
63
84
  }
64
85
  /**
65
86
  * Configuration options for the RabbitMQ client.
@@ -119,6 +140,10 @@ interface PublishOptions {
119
140
  type RabbitMQMessage = Record<string, unknown>;
120
141
  /** What the publisher and the broker attached to a consumed message */
121
142
  interface RabbitMQMessageProperties {
143
+ /** Exchange the message was published to */
144
+ exchange: string;
145
+ /** Routing key the message was published with */
146
+ routingKey: string;
122
147
  /** AMQP headers, including broker-added ones such as `x-death` on a dead-lettered message */
123
148
  headers: Record<string, unknown>;
124
149
  /** AMQP timestamp in seconds, if the publisher set one */
@@ -139,4 +164,4 @@ interface RabbitMQSubscription {
139
164
  options?: SubscribeOptions;
140
165
  }
141
166
 
142
- export type { ILogger as I, PublishOptions as P, RabbitMQClientOptions as R, SubscribeOptions as S, RabbitMQMessage as a, RabbitMQMessageHandler as b, RabbitMQHooks as c, RabbitMQMessageProperties as d, RabbitMQSubscription as e };
167
+ export type { ILogger as I, PublishOptions as P, RabbitMQClientOptions as R, SubscribeOptions as S, RabbitMQMessage as a, RabbitMQMessageHandler as b, RabbitMQBinding as c, RabbitMQHooks as d, RabbitMQMessageProperties as e, RabbitMQSubscription as f };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linagora/rabbitmq-client",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Production-grade RabbitMQ client with confirm channels, DLQ infrastructure, auto-reconnection, exponential backoff, graceful shutdown, and observability hooks",
5
5
  "type": "module",
6
6
  "exports": {