@linagora/rabbitmq-client 0.1.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/README.md +161 -0
- package/dist/index.cjs +509 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +161 -0
- package/dist/index.d.ts +161 -0
- package/dist/index.js +472 -0
- package/dist/index.js.map +1 -0
- package/dist/testing/index.cjs +198 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +99 -0
- package/dist/testing/index.d.ts +99 -0
- package/dist/testing/index.js +171 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/logger.ts"],"sourcesContent":["export { RabbitMQClient } from './client.js'\nexport type {\n RabbitMQClientOptions,\n RabbitMQHooks,\n RabbitMQMessage,\n RabbitMQMessageHandler,\n RabbitMQSubscription,\n SubscribeOptions,\n} from './types.js'\n","import amqp from 'amqplib'\nimport type {\n RabbitMQClientOptions,\n RabbitMQHooks,\n RabbitMQMessage,\n RabbitMQMessageHandler,\n RabbitMQSubscription,\n SubscribeOptions,\n} from './types.js'\nimport { createLogger } from './logger.js'\nimport type { Logger } from 'pino'\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/**\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: Logger\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 private consumerTags = new Map<string, string>()\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 closeTimeout: options.closeTimeout ?? DEFAULTS.closeTimeout,\n }\n this.logger = createLogger(options.logger)\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 this.logger.info('Confirm channel created')\n await this.channel.prefetch(this.options.prefetch)\n this.logger.info({ prefetch: this.options.prefetch }, 'Channel prefetch set')\n\n this.connection.on('error', (error: Error) => {\n this.connected = false\n this.logger.error({ error }, 'Connection 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({ error }, 'Channel 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({ error, attempts, maxAttempts }, 'Connection failed after maximum attempts')\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(\n {\n error,\n attempt: attempts,\n maxAttempts: maxAttempts ?? 'unlimited',\n retryDelayMs: this.options.connectionRetryDelay,\n },\n 'Connection attempt failed, retrying...',\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.\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?: { maxAttempts?: number },\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\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 if (!this.assertedExchanges.has(exchange)) {\n await this.channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n this.channel.publish(exchange, routingKey, content, {\n persistent: true,\n })\n\n await this.channel.waitForConfirms()\n\n if (attempts > 0) {\n this.logger.info({ exchange, routingKey, messageSize: content.length, attempts }, 'Published message after retries')\n } else {\n this.logger.info({ exchange, routingKey, messageSize: content.length }, 'Published message')\n }\n this.logger.debug({ exchange, routingKey, payload: message }, 'Published message payload')\n\n this.callHook(this.hooks.onPublish, { exchange, routingKey, attempts: attempts + 1 })\n\n return\n } catch (error) {\n attempts++\n this.connected = false\n\n if (attempts >= maxAttempts) {\n this.logger.error(\n { error, exchange, routingKey, attempts, maxAttempts },\n 'Publish failed after max attempts',\n )\n throw new Error(\n `Failed to publish to ${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(\n { error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay },\n 'Publish attempt failed, retrying',\n )\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({ inflightCount: this.inflightCount }, 'Waiting for in-flight messages to drain')\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 }\n this.logger.info('Connection closed')\n } catch (error) {\n this.logger.error({ error }, 'Error closing connection')\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({ count: this.subscriptions.length }, 'Re-establishing subscriptions')\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({ error: result.reason, queue }, 'Failed to re-subscribe to queue')\n }\n })\n if (succeeded.length > 0) {\n this.logger.info({ count: succeeded.length, queues: succeeded }, 'Successfully re-subscribed to queues')\n }\n if (failed.length > 0) {\n this.logger.warn({ count: failed.length, queues: failed }, 'Some subscriptions failed to restore')\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 }\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.subscriptions = this.subscriptions.filter((s) => s.queue !== queue)\n this.logger.info({ queue }, 'Unsubscribed from queue')\n }\n\n private async setupSubscription(sub: RabbitMQSubscription): Promise<void> {\n if (!this.channel) {\n throw new Error('Channel not available')\n }\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 this.channel.assertExchange(dlxExchange, 'topic', { durable: true })\n await this.channel.assertQueue(dlqQueue, { durable: true })\n await this.channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey)\n\n if (!this.assertedExchanges.has(exchange)) {\n await this.channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n await this.channel.assertQueue(queue, {\n durable: true,\n deadLetterExchange: dlxExchange,\n deadLetterRoutingKey: dlqRoutingKey,\n arguments: {\n 'x-dead-letter-strategy': 'at-least-once',\n 'x-queue-type': 'quorum',\n 'x-overflow': 'reject-publish',\n ...options?.queueArguments,\n },\n })\n\n await this.channel.bindQueue(queue, exchange, routingKey)\n const { consumerTag } = await this.channel.consume(\n queue,\n (message) => {\n if (message) {\n this.handleWithRetry(message, handler)\n }\n },\n { noAck: false },\n )\n this.consumerTags.set(queue, consumerTag)\n this.logger.info({ exchange, routingKey, queue }, 'Subscribed to queue')\n }\n\n private async handleWithRetry(\n message: amqp.ConsumeMessage,\n handler: RabbitMQMessageHandler,\n ): Promise<void> {\n // Capture the channel reference so ack/nack always targets the channel\n // that delivered this message, even if a reconnection swaps this.channel.\n const channel = this.channel\n if (!channel) {\n this.logger.warn('Cannot process message -- channel unavailable')\n return\n }\n this.inflightCount++\n try {\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(\n {\n error: parseError,\n exchange,\n routingKey,\n rawContentPreview: rawPreview + (rawContent.length > 100 ? '...' : ''),\n },\n 'Failed to parse message JSON, sending to DLQ',\n )\n channel.nack(message, false, false)\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: 'invalid_json' })\n return\n }\n\n this.logger.debug({ exchange, routingKey, payload: content }, 'Message received, processing')\n\n while (attempts < this.options.maxRetries) {\n try {\n await handler(content)\n const duration = Date.now() - startTime\n this.logger.info(\n { exchange, routingKey, duration, attempts: attempts + 1 },\n 'Message processed successfully',\n )\n channel.ack(message)\n this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration, attempts: attempts + 1 })\n return\n } catch (error) {\n attempts++\n this.logger.error(\n {\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 'Handler failed',\n )\n if (attempts < this.options.maxRetries) {\n await this.sleep(this.options.retryDelay)\n }\n }\n }\n\n const duration = Date.now() - startTime\n this.logger.error(\n { exchange, routingKey, maxRetries: this.options.maxRetries, duration },\n 'Message failed after max retries, sending to DLQ',\n )\n channel.nack(message, false, false)\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: 'max_retries_exhausted' })\n } finally {\n this.inflightCount--\n if (this.inflightCount === 0 && this.drainResolve) {\n this.drainResolve()\n }\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({ error }, 'Health check failed')\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(\n { inflightCount: this.inflightCount },\n 'Close timeout reached with messages still in flight',\n )\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 { /* swallowed */ }\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n }\n}\n","import pino, { type Logger } from 'pino'\n\n/**\n * Creates a child pino logger for the RabbitMQ client.\n *\n * If a parent logger is provided, the child inherits its configuration\n * (level, transports, etc.) for unified log output. Otherwise, a new\n * root logger is created.\n */\nexport const createLogger = (parent?: Logger): Logger => {\n if (parent) {\n return parent.child({ service: 'rabbitmq-client' })\n }\n return pino({ name: 'rabbitmq-client' })\n}\n"],"mappings":"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","names":["pino","amqp","duration"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Logger } from 'pino';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hooks for observability — optional callbacks invoked on key client events.
|
|
5
|
+
* Use these to wire metrics, tracing, or custom logging.
|
|
6
|
+
*/
|
|
7
|
+
interface RabbitMQHooks {
|
|
8
|
+
/** Called after a message is successfully published */
|
|
9
|
+
onPublish?: (info: {
|
|
10
|
+
exchange: string;
|
|
11
|
+
routingKey: string;
|
|
12
|
+
attempts: number;
|
|
13
|
+
}) => void;
|
|
14
|
+
/** Called after a message handler completes successfully */
|
|
15
|
+
onMessageProcessed?: (info: {
|
|
16
|
+
exchange: string;
|
|
17
|
+
routingKey: string;
|
|
18
|
+
duration: number;
|
|
19
|
+
attempts: number;
|
|
20
|
+
}) => void;
|
|
21
|
+
/** Called when a message is sent to the dead-letter queue */
|
|
22
|
+
onMessageDlq?: (info: {
|
|
23
|
+
exchange: string;
|
|
24
|
+
routingKey: string;
|
|
25
|
+
duration: number;
|
|
26
|
+
reason: 'invalid_json' | 'max_retries_exhausted';
|
|
27
|
+
}) => void;
|
|
28
|
+
/** Called after a successful reconnection */
|
|
29
|
+
onReconnect?: (info: {
|
|
30
|
+
subscriptionsRestored: number;
|
|
31
|
+
subscriptionsFailed: number;
|
|
32
|
+
}) => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Options for queue subscription.
|
|
36
|
+
*/
|
|
37
|
+
interface SubscribeOptions {
|
|
38
|
+
/** Override default AMQP queue arguments (merged with DLQ wiring defaults) */
|
|
39
|
+
queueArguments?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Configuration options for the RabbitMQ client.
|
|
43
|
+
*/
|
|
44
|
+
interface RabbitMQClientOptions {
|
|
45
|
+
/** AMQP connection URL (e.g., 'amqp://user:pass@host:5672') */
|
|
46
|
+
url: string;
|
|
47
|
+
/** Max handler retries before sending to DLQ (default: 3) */
|
|
48
|
+
maxRetries?: number;
|
|
49
|
+
/** Delay in ms between handler retries (default: 1000) */
|
|
50
|
+
retryDelay?: number;
|
|
51
|
+
/** Delay in ms between reconnection attempts (default: 5000) */
|
|
52
|
+
connectionRetryDelay?: number;
|
|
53
|
+
/** Max connection attempts during init; undefined = unlimited (default: 5) */
|
|
54
|
+
initMaxAttempts?: number;
|
|
55
|
+
/** Max publish attempts with exponential backoff (default: 5) */
|
|
56
|
+
publishMaxAttempts?: number;
|
|
57
|
+
/** Channel prefetch count (default: 10) */
|
|
58
|
+
prefetch?: number;
|
|
59
|
+
/** Parent pino logger instance; library creates its own if omitted */
|
|
60
|
+
logger?: Logger;
|
|
61
|
+
/** Timeout in ms to wait for in-flight messages during close (default: 5000) */
|
|
62
|
+
closeTimeout?: number;
|
|
63
|
+
/** Observability hooks for metrics and monitoring */
|
|
64
|
+
hooks?: RabbitMQHooks;
|
|
65
|
+
}
|
|
66
|
+
/** JSON-serializable message payload */
|
|
67
|
+
type RabbitMQMessage = Record<string, unknown>;
|
|
68
|
+
/** Async handler function for consumed messages */
|
|
69
|
+
type RabbitMQMessageHandler = (message: RabbitMQMessage) => Promise<void>;
|
|
70
|
+
/** Stored subscription metadata for restoration after reconnection */
|
|
71
|
+
interface RabbitMQSubscription {
|
|
72
|
+
exchange: string;
|
|
73
|
+
routingKey: string;
|
|
74
|
+
queue: string;
|
|
75
|
+
handler: RabbitMQMessageHandler;
|
|
76
|
+
options?: SubscribeOptions;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Production-grade RabbitMQ client with confirm channels, automatic DLQ
|
|
81
|
+
* infrastructure, reconnection with subscription restoration, and
|
|
82
|
+
* exponential-backoff publishing.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* const client = new RabbitMQClient({ url: 'amqp://localhost' })
|
|
87
|
+
* await client.init()
|
|
88
|
+
* await client.publish('events', 'user.created', { userId: '123' })
|
|
89
|
+
* await client.subscribe('events', 'user.created', 'user-service', handler)
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
declare class RabbitMQClient {
|
|
93
|
+
private connection;
|
|
94
|
+
private channel;
|
|
95
|
+
private connected;
|
|
96
|
+
private subscriptions;
|
|
97
|
+
private readonly options;
|
|
98
|
+
private readonly logger;
|
|
99
|
+
private readonly hooks;
|
|
100
|
+
private initializationPromise;
|
|
101
|
+
private reconnectionPromise;
|
|
102
|
+
private assertedExchanges;
|
|
103
|
+
private consumerTags;
|
|
104
|
+
private inflightCount;
|
|
105
|
+
private drainResolve;
|
|
106
|
+
constructor(options: RabbitMQClientOptions);
|
|
107
|
+
/**
|
|
108
|
+
* Opens a connection and creates a confirm channel. Retries up to
|
|
109
|
+
* `initMaxAttempts` times, then throws. Idempotent and safe to call
|
|
110
|
+
* concurrently — duplicate calls share the same in-flight promise.
|
|
111
|
+
*/
|
|
112
|
+
init(): Promise<void>;
|
|
113
|
+
private doConnect;
|
|
114
|
+
private handleChannelFailure;
|
|
115
|
+
/**
|
|
116
|
+
* Returns whether the client currently has an active connection and channel.
|
|
117
|
+
*/
|
|
118
|
+
isConnected(): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Publishes a JSON message to a topic exchange with publisher confirms.
|
|
121
|
+
* Retries with exponential backoff (capped at 60 s), forcing a reconnect
|
|
122
|
+
* on each failure. Throws after `publishMaxAttempts` exhausted.
|
|
123
|
+
*/
|
|
124
|
+
publish(exchange: string, routingKey: string, message: RabbitMQMessage, options?: {
|
|
125
|
+
maxAttempts?: number;
|
|
126
|
+
}): Promise<void>;
|
|
127
|
+
/**
|
|
128
|
+
* Gracefully shuts down the client. Waits up to `closeTimeout` ms for
|
|
129
|
+
* in-flight message handlers to finish before closing the channel and
|
|
130
|
+
* connection. Pass `false` to preserve subscriptions for a later
|
|
131
|
+
* `init()` / reconnect cycle.
|
|
132
|
+
*/
|
|
133
|
+
close(clearSubscriptions?: boolean): Promise<void>;
|
|
134
|
+
private reconnectWithRetry;
|
|
135
|
+
private doReconnect;
|
|
136
|
+
private resubscribeAll;
|
|
137
|
+
/**
|
|
138
|
+
* Subscribes to a queue with automatic DLQ infrastructure setup.
|
|
139
|
+
*
|
|
140
|
+
* Pass `options.queueArguments` to override the default quorum-queue
|
|
141
|
+
* arguments (merged with the DLQ wiring defaults).
|
|
142
|
+
*/
|
|
143
|
+
subscribe(exchange: string, routingKey: string, queue: string, handler: RabbitMQMessageHandler, options?: SubscribeOptions): Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* Cancels a queue subscription and removes it from the restoration list.
|
|
146
|
+
*/
|
|
147
|
+
unsubscribe(queue: string): Promise<void>;
|
|
148
|
+
private setupSubscription;
|
|
149
|
+
private handleWithRetry;
|
|
150
|
+
/**
|
|
151
|
+
* Lightweight liveness probe: creates and immediately deletes a temporary
|
|
152
|
+
* exclusive queue. Returns `false` when disconnected or on broker error.
|
|
153
|
+
*/
|
|
154
|
+
checkHealth(): Promise<boolean>;
|
|
155
|
+
private waitForDrain;
|
|
156
|
+
/** Invokes a hook callback, swallowing errors so hooks never break message flow. */
|
|
157
|
+
private callHook;
|
|
158
|
+
private sleep;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export { RabbitMQClient, type RabbitMQClientOptions, type RabbitMQHooks, type RabbitMQMessage, type RabbitMQMessageHandler, type RabbitMQSubscription, type SubscribeOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Logger } from 'pino';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hooks for observability — optional callbacks invoked on key client events.
|
|
5
|
+
* Use these to wire metrics, tracing, or custom logging.
|
|
6
|
+
*/
|
|
7
|
+
interface RabbitMQHooks {
|
|
8
|
+
/** Called after a message is successfully published */
|
|
9
|
+
onPublish?: (info: {
|
|
10
|
+
exchange: string;
|
|
11
|
+
routingKey: string;
|
|
12
|
+
attempts: number;
|
|
13
|
+
}) => void;
|
|
14
|
+
/** Called after a message handler completes successfully */
|
|
15
|
+
onMessageProcessed?: (info: {
|
|
16
|
+
exchange: string;
|
|
17
|
+
routingKey: string;
|
|
18
|
+
duration: number;
|
|
19
|
+
attempts: number;
|
|
20
|
+
}) => void;
|
|
21
|
+
/** Called when a message is sent to the dead-letter queue */
|
|
22
|
+
onMessageDlq?: (info: {
|
|
23
|
+
exchange: string;
|
|
24
|
+
routingKey: string;
|
|
25
|
+
duration: number;
|
|
26
|
+
reason: 'invalid_json' | 'max_retries_exhausted';
|
|
27
|
+
}) => void;
|
|
28
|
+
/** Called after a successful reconnection */
|
|
29
|
+
onReconnect?: (info: {
|
|
30
|
+
subscriptionsRestored: number;
|
|
31
|
+
subscriptionsFailed: number;
|
|
32
|
+
}) => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Options for queue subscription.
|
|
36
|
+
*/
|
|
37
|
+
interface SubscribeOptions {
|
|
38
|
+
/** Override default AMQP queue arguments (merged with DLQ wiring defaults) */
|
|
39
|
+
queueArguments?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Configuration options for the RabbitMQ client.
|
|
43
|
+
*/
|
|
44
|
+
interface RabbitMQClientOptions {
|
|
45
|
+
/** AMQP connection URL (e.g., 'amqp://user:pass@host:5672') */
|
|
46
|
+
url: string;
|
|
47
|
+
/** Max handler retries before sending to DLQ (default: 3) */
|
|
48
|
+
maxRetries?: number;
|
|
49
|
+
/** Delay in ms between handler retries (default: 1000) */
|
|
50
|
+
retryDelay?: number;
|
|
51
|
+
/** Delay in ms between reconnection attempts (default: 5000) */
|
|
52
|
+
connectionRetryDelay?: number;
|
|
53
|
+
/** Max connection attempts during init; undefined = unlimited (default: 5) */
|
|
54
|
+
initMaxAttempts?: number;
|
|
55
|
+
/** Max publish attempts with exponential backoff (default: 5) */
|
|
56
|
+
publishMaxAttempts?: number;
|
|
57
|
+
/** Channel prefetch count (default: 10) */
|
|
58
|
+
prefetch?: number;
|
|
59
|
+
/** Parent pino logger instance; library creates its own if omitted */
|
|
60
|
+
logger?: Logger;
|
|
61
|
+
/** Timeout in ms to wait for in-flight messages during close (default: 5000) */
|
|
62
|
+
closeTimeout?: number;
|
|
63
|
+
/** Observability hooks for metrics and monitoring */
|
|
64
|
+
hooks?: RabbitMQHooks;
|
|
65
|
+
}
|
|
66
|
+
/** JSON-serializable message payload */
|
|
67
|
+
type RabbitMQMessage = Record<string, unknown>;
|
|
68
|
+
/** Async handler function for consumed messages */
|
|
69
|
+
type RabbitMQMessageHandler = (message: RabbitMQMessage) => Promise<void>;
|
|
70
|
+
/** Stored subscription metadata for restoration after reconnection */
|
|
71
|
+
interface RabbitMQSubscription {
|
|
72
|
+
exchange: string;
|
|
73
|
+
routingKey: string;
|
|
74
|
+
queue: string;
|
|
75
|
+
handler: RabbitMQMessageHandler;
|
|
76
|
+
options?: SubscribeOptions;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Production-grade RabbitMQ client with confirm channels, automatic DLQ
|
|
81
|
+
* infrastructure, reconnection with subscription restoration, and
|
|
82
|
+
* exponential-backoff publishing.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* const client = new RabbitMQClient({ url: 'amqp://localhost' })
|
|
87
|
+
* await client.init()
|
|
88
|
+
* await client.publish('events', 'user.created', { userId: '123' })
|
|
89
|
+
* await client.subscribe('events', 'user.created', 'user-service', handler)
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
declare class RabbitMQClient {
|
|
93
|
+
private connection;
|
|
94
|
+
private channel;
|
|
95
|
+
private connected;
|
|
96
|
+
private subscriptions;
|
|
97
|
+
private readonly options;
|
|
98
|
+
private readonly logger;
|
|
99
|
+
private readonly hooks;
|
|
100
|
+
private initializationPromise;
|
|
101
|
+
private reconnectionPromise;
|
|
102
|
+
private assertedExchanges;
|
|
103
|
+
private consumerTags;
|
|
104
|
+
private inflightCount;
|
|
105
|
+
private drainResolve;
|
|
106
|
+
constructor(options: RabbitMQClientOptions);
|
|
107
|
+
/**
|
|
108
|
+
* Opens a connection and creates a confirm channel. Retries up to
|
|
109
|
+
* `initMaxAttempts` times, then throws. Idempotent and safe to call
|
|
110
|
+
* concurrently — duplicate calls share the same in-flight promise.
|
|
111
|
+
*/
|
|
112
|
+
init(): Promise<void>;
|
|
113
|
+
private doConnect;
|
|
114
|
+
private handleChannelFailure;
|
|
115
|
+
/**
|
|
116
|
+
* Returns whether the client currently has an active connection and channel.
|
|
117
|
+
*/
|
|
118
|
+
isConnected(): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Publishes a JSON message to a topic exchange with publisher confirms.
|
|
121
|
+
* Retries with exponential backoff (capped at 60 s), forcing a reconnect
|
|
122
|
+
* on each failure. Throws after `publishMaxAttempts` exhausted.
|
|
123
|
+
*/
|
|
124
|
+
publish(exchange: string, routingKey: string, message: RabbitMQMessage, options?: {
|
|
125
|
+
maxAttempts?: number;
|
|
126
|
+
}): Promise<void>;
|
|
127
|
+
/**
|
|
128
|
+
* Gracefully shuts down the client. Waits up to `closeTimeout` ms for
|
|
129
|
+
* in-flight message handlers to finish before closing the channel and
|
|
130
|
+
* connection. Pass `false` to preserve subscriptions for a later
|
|
131
|
+
* `init()` / reconnect cycle.
|
|
132
|
+
*/
|
|
133
|
+
close(clearSubscriptions?: boolean): Promise<void>;
|
|
134
|
+
private reconnectWithRetry;
|
|
135
|
+
private doReconnect;
|
|
136
|
+
private resubscribeAll;
|
|
137
|
+
/**
|
|
138
|
+
* Subscribes to a queue with automatic DLQ infrastructure setup.
|
|
139
|
+
*
|
|
140
|
+
* Pass `options.queueArguments` to override the default quorum-queue
|
|
141
|
+
* arguments (merged with the DLQ wiring defaults).
|
|
142
|
+
*/
|
|
143
|
+
subscribe(exchange: string, routingKey: string, queue: string, handler: RabbitMQMessageHandler, options?: SubscribeOptions): Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* Cancels a queue subscription and removes it from the restoration list.
|
|
146
|
+
*/
|
|
147
|
+
unsubscribe(queue: string): Promise<void>;
|
|
148
|
+
private setupSubscription;
|
|
149
|
+
private handleWithRetry;
|
|
150
|
+
/**
|
|
151
|
+
* Lightweight liveness probe: creates and immediately deletes a temporary
|
|
152
|
+
* exclusive queue. Returns `false` when disconnected or on broker error.
|
|
153
|
+
*/
|
|
154
|
+
checkHealth(): Promise<boolean>;
|
|
155
|
+
private waitForDrain;
|
|
156
|
+
/** Invokes a hook callback, swallowing errors so hooks never break message flow. */
|
|
157
|
+
private callHook;
|
|
158
|
+
private sleep;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export { RabbitMQClient, type RabbitMQClientOptions, type RabbitMQHooks, type RabbitMQMessage, type RabbitMQMessageHandler, type RabbitMQSubscription, type SubscribeOptions };
|