@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import amqp from "amqplib";
|
|
3
|
+
|
|
4
|
+
// src/logger.ts
|
|
5
|
+
import pino from "pino";
|
|
6
|
+
var createLogger = (parent) => {
|
|
7
|
+
if (parent) {
|
|
8
|
+
return parent.child({ service: "rabbitmq-client" });
|
|
9
|
+
}
|
|
10
|
+
return pino({ name: "rabbitmq-client" });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/client.ts
|
|
14
|
+
var DEFAULT_PREFETCH = 10;
|
|
15
|
+
var MAX_PUBLISH_RETRY_DELAY_MS = 6e4;
|
|
16
|
+
var DEFAULTS = {
|
|
17
|
+
maxRetries: 3,
|
|
18
|
+
retryDelay: 1e3,
|
|
19
|
+
connectionRetryDelay: 5e3,
|
|
20
|
+
initMaxAttempts: 5,
|
|
21
|
+
publishMaxAttempts: 5,
|
|
22
|
+
prefetch: DEFAULT_PREFETCH,
|
|
23
|
+
closeTimeout: 5e3
|
|
24
|
+
};
|
|
25
|
+
var RabbitMQClient = class {
|
|
26
|
+
connection = null;
|
|
27
|
+
channel = null;
|
|
28
|
+
connected = false;
|
|
29
|
+
subscriptions = [];
|
|
30
|
+
options;
|
|
31
|
+
logger;
|
|
32
|
+
hooks;
|
|
33
|
+
initializationPromise = null;
|
|
34
|
+
reconnectionPromise = null;
|
|
35
|
+
assertedExchanges = /* @__PURE__ */ new Set();
|
|
36
|
+
consumerTags = /* @__PURE__ */ new Map();
|
|
37
|
+
inflightCount = 0;
|
|
38
|
+
drainResolve = null;
|
|
39
|
+
constructor(options) {
|
|
40
|
+
this.options = {
|
|
41
|
+
url: options.url,
|
|
42
|
+
maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,
|
|
43
|
+
retryDelay: options.retryDelay ?? DEFAULTS.retryDelay,
|
|
44
|
+
connectionRetryDelay: options.connectionRetryDelay ?? DEFAULTS.connectionRetryDelay,
|
|
45
|
+
initMaxAttempts: options.initMaxAttempts ?? DEFAULTS.initMaxAttempts,
|
|
46
|
+
publishMaxAttempts: options.publishMaxAttempts ?? DEFAULTS.publishMaxAttempts,
|
|
47
|
+
prefetch: options.prefetch ?? DEFAULTS.prefetch,
|
|
48
|
+
closeTimeout: options.closeTimeout ?? DEFAULTS.closeTimeout
|
|
49
|
+
};
|
|
50
|
+
this.logger = createLogger(options.logger);
|
|
51
|
+
this.hooks = options.hooks ?? {};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Opens a connection and creates a confirm channel. Retries up to
|
|
55
|
+
* `initMaxAttempts` times, then throws. Idempotent and safe to call
|
|
56
|
+
* concurrently — duplicate calls share the same in-flight promise.
|
|
57
|
+
*/
|
|
58
|
+
async init() {
|
|
59
|
+
if (this.connection && this.connected) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (this.initializationPromise) {
|
|
63
|
+
return this.initializationPromise;
|
|
64
|
+
}
|
|
65
|
+
this.initializationPromise = this.doConnect(this.options.initMaxAttempts);
|
|
66
|
+
this.initializationPromise.catch(() => void 0);
|
|
67
|
+
try {
|
|
68
|
+
await this.initializationPromise;
|
|
69
|
+
} finally {
|
|
70
|
+
this.initializationPromise = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async doConnect(maxAttempts) {
|
|
74
|
+
this.assertedExchanges.clear();
|
|
75
|
+
this.consumerTags.clear();
|
|
76
|
+
let attempts = 0;
|
|
77
|
+
while (!this.connected) {
|
|
78
|
+
try {
|
|
79
|
+
this.connection = await amqp.connect(this.options.url);
|
|
80
|
+
this.logger.info("Connected to server");
|
|
81
|
+
this.channel = await this.connection.createConfirmChannel();
|
|
82
|
+
this.logger.info("Confirm channel created");
|
|
83
|
+
await this.channel.prefetch(this.options.prefetch);
|
|
84
|
+
this.logger.info({ prefetch: this.options.prefetch }, "Channel prefetch set");
|
|
85
|
+
this.connection.on("error", (error) => {
|
|
86
|
+
this.connected = false;
|
|
87
|
+
this.logger.error({ error }, "Connection error");
|
|
88
|
+
});
|
|
89
|
+
this.connection.on("close", () => {
|
|
90
|
+
this.connected = false;
|
|
91
|
+
this.logger.warn("Connection closed");
|
|
92
|
+
this.reconnectWithRetry();
|
|
93
|
+
});
|
|
94
|
+
this.channel.on("error", (error) => {
|
|
95
|
+
this.logger.error({ error }, "Channel error");
|
|
96
|
+
this.handleChannelFailure();
|
|
97
|
+
});
|
|
98
|
+
this.channel.on("close", () => {
|
|
99
|
+
this.logger.warn("Channel closed");
|
|
100
|
+
this.handleChannelFailure();
|
|
101
|
+
});
|
|
102
|
+
this.connected = true;
|
|
103
|
+
this.logger.info("Client initialized successfully");
|
|
104
|
+
} catch (error) {
|
|
105
|
+
attempts++;
|
|
106
|
+
this.connected = false;
|
|
107
|
+
if (maxAttempts !== void 0 && attempts >= maxAttempts) {
|
|
108
|
+
this.logger.error({ error, attempts, maxAttempts }, "Connection failed after maximum attempts");
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Failed to connect to RabbitMQ after ${attempts} attempts. Check RABBITMQ_URL configuration and RabbitMQ server availability.`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
this.logger.warn(
|
|
114
|
+
{
|
|
115
|
+
error,
|
|
116
|
+
attempt: attempts,
|
|
117
|
+
maxAttempts: maxAttempts ?? "unlimited",
|
|
118
|
+
retryDelayMs: this.options.connectionRetryDelay
|
|
119
|
+
},
|
|
120
|
+
"Connection attempt failed, retrying..."
|
|
121
|
+
);
|
|
122
|
+
await this.sleep(this.options.connectionRetryDelay);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
handleChannelFailure() {
|
|
127
|
+
if (this.connected) {
|
|
128
|
+
this.connected = false;
|
|
129
|
+
this.reconnectWithRetry();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Returns whether the client currently has an active connection and channel.
|
|
134
|
+
*/
|
|
135
|
+
isConnected() {
|
|
136
|
+
return this.connected;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Publishes a JSON message to a topic exchange with publisher confirms.
|
|
140
|
+
* Retries with exponential backoff (capped at 60 s), forcing a reconnect
|
|
141
|
+
* on each failure. Throws after `publishMaxAttempts` exhausted.
|
|
142
|
+
*/
|
|
143
|
+
async publish(exchange, routingKey, message, options) {
|
|
144
|
+
let attempts = 0;
|
|
145
|
+
const maxAttempts = options?.maxAttempts ?? this.options.publishMaxAttempts;
|
|
146
|
+
const baseDelay = this.options.connectionRetryDelay;
|
|
147
|
+
const content = Buffer.from(JSON.stringify(message));
|
|
148
|
+
while (attempts < maxAttempts) {
|
|
149
|
+
try {
|
|
150
|
+
if (!this.connected) {
|
|
151
|
+
await this.reconnectWithRetry();
|
|
152
|
+
}
|
|
153
|
+
if (!this.channel) {
|
|
154
|
+
throw new Error("Channel not available");
|
|
155
|
+
}
|
|
156
|
+
if (!this.assertedExchanges.has(exchange)) {
|
|
157
|
+
await this.channel.assertExchange(exchange, "topic", { durable: true });
|
|
158
|
+
this.assertedExchanges.add(exchange);
|
|
159
|
+
}
|
|
160
|
+
this.channel.publish(exchange, routingKey, content, {
|
|
161
|
+
persistent: true
|
|
162
|
+
});
|
|
163
|
+
await this.channel.waitForConfirms();
|
|
164
|
+
if (attempts > 0) {
|
|
165
|
+
this.logger.info({ exchange, routingKey, messageSize: content.length, attempts }, "Published message after retries");
|
|
166
|
+
} else {
|
|
167
|
+
this.logger.info({ exchange, routingKey, messageSize: content.length }, "Published message");
|
|
168
|
+
}
|
|
169
|
+
this.logger.debug({ exchange, routingKey, payload: message }, "Published message payload");
|
|
170
|
+
this.callHook(this.hooks.onPublish, { exchange, routingKey, attempts: attempts + 1 });
|
|
171
|
+
return;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
attempts++;
|
|
174
|
+
this.connected = false;
|
|
175
|
+
if (attempts >= maxAttempts) {
|
|
176
|
+
this.logger.error(
|
|
177
|
+
{ error, exchange, routingKey, attempts, maxAttempts },
|
|
178
|
+
"Publish failed after max attempts"
|
|
179
|
+
);
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Failed to publish to ${exchange}/${routingKey} after ${attempts} attempts: ${error instanceof Error ? error.message : String(error)}`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const retryDelay = Math.min(
|
|
185
|
+
baseDelay * Math.pow(2, attempts - 1),
|
|
186
|
+
MAX_PUBLISH_RETRY_DELAY_MS
|
|
187
|
+
);
|
|
188
|
+
this.logger.warn(
|
|
189
|
+
{ error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay },
|
|
190
|
+
"Publish attempt failed, retrying"
|
|
191
|
+
);
|
|
192
|
+
await this.sleep(retryDelay);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Gracefully shuts down the client. Waits up to `closeTimeout` ms for
|
|
198
|
+
* in-flight message handlers to finish before closing the channel and
|
|
199
|
+
* connection. Pass `false` to preserve subscriptions for a later
|
|
200
|
+
* `init()` / reconnect cycle.
|
|
201
|
+
*/
|
|
202
|
+
async close(clearSubscriptions = true) {
|
|
203
|
+
try {
|
|
204
|
+
if (this.inflightCount > 0) {
|
|
205
|
+
this.logger.info({ inflightCount: this.inflightCount }, "Waiting for in-flight messages to drain");
|
|
206
|
+
await this.waitForDrain(this.options.closeTimeout);
|
|
207
|
+
}
|
|
208
|
+
await this.channel?.close();
|
|
209
|
+
await this.connection?.close();
|
|
210
|
+
this.connection = null;
|
|
211
|
+
this.channel = null;
|
|
212
|
+
this.connected = false;
|
|
213
|
+
this.initializationPromise = null;
|
|
214
|
+
this.reconnectionPromise = null;
|
|
215
|
+
this.assertedExchanges.clear();
|
|
216
|
+
this.consumerTags.clear();
|
|
217
|
+
if (clearSubscriptions) {
|
|
218
|
+
this.subscriptions = [];
|
|
219
|
+
}
|
|
220
|
+
this.logger.info("Connection closed");
|
|
221
|
+
} catch (error) {
|
|
222
|
+
this.logger.error({ error }, "Error closing connection");
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async reconnectWithRetry() {
|
|
227
|
+
if (this.reconnectionPromise) {
|
|
228
|
+
return this.reconnectionPromise;
|
|
229
|
+
}
|
|
230
|
+
this.connected = false;
|
|
231
|
+
this.logger.info("Starting reconnection...");
|
|
232
|
+
this.reconnectionPromise = this.doReconnect();
|
|
233
|
+
this.reconnectionPromise.catch(() => void 0);
|
|
234
|
+
try {
|
|
235
|
+
await this.reconnectionPromise;
|
|
236
|
+
} finally {
|
|
237
|
+
this.reconnectionPromise = null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async doReconnect() {
|
|
241
|
+
await this.doConnect();
|
|
242
|
+
await this.resubscribeAll();
|
|
243
|
+
}
|
|
244
|
+
async resubscribeAll() {
|
|
245
|
+
if (this.subscriptions.length === 0) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
this.logger.info({ count: this.subscriptions.length }, "Re-establishing subscriptions");
|
|
249
|
+
const subs = [...this.subscriptions];
|
|
250
|
+
const results = await Promise.allSettled(
|
|
251
|
+
subs.map(async (sub) => {
|
|
252
|
+
await this.setupSubscription(sub);
|
|
253
|
+
return sub.queue;
|
|
254
|
+
})
|
|
255
|
+
);
|
|
256
|
+
const succeeded = [];
|
|
257
|
+
const failed = [];
|
|
258
|
+
results.forEach((result, index) => {
|
|
259
|
+
if (result.status === "fulfilled") {
|
|
260
|
+
succeeded.push(result.value);
|
|
261
|
+
} else {
|
|
262
|
+
const queue = subs[index].queue;
|
|
263
|
+
failed.push(queue);
|
|
264
|
+
this.logger.error({ error: result.reason, queue }, "Failed to re-subscribe to queue");
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
if (succeeded.length > 0) {
|
|
268
|
+
this.logger.info({ count: succeeded.length, queues: succeeded }, "Successfully re-subscribed to queues");
|
|
269
|
+
}
|
|
270
|
+
if (failed.length > 0) {
|
|
271
|
+
this.logger.warn({ count: failed.length, queues: failed }, "Some subscriptions failed to restore");
|
|
272
|
+
}
|
|
273
|
+
this.callHook(this.hooks.onReconnect, { subscriptionsRestored: succeeded.length, subscriptionsFailed: failed.length });
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Subscribes to a queue with automatic DLQ infrastructure setup.
|
|
277
|
+
*
|
|
278
|
+
* Pass `options.queueArguments` to override the default quorum-queue
|
|
279
|
+
* arguments (merged with the DLQ wiring defaults).
|
|
280
|
+
*/
|
|
281
|
+
async subscribe(exchange, routingKey, queue, handler, options) {
|
|
282
|
+
if (!this.connected || !this.channel) {
|
|
283
|
+
throw new Error("RabbitMQ client not connected. Call init() first.");
|
|
284
|
+
}
|
|
285
|
+
const sub = { exchange, routingKey, queue, handler, options };
|
|
286
|
+
const existingIndex = this.subscriptions.findIndex((s) => s.queue === queue);
|
|
287
|
+
if (existingIndex === -1) {
|
|
288
|
+
this.subscriptions.push(sub);
|
|
289
|
+
} else {
|
|
290
|
+
this.subscriptions[existingIndex] = sub;
|
|
291
|
+
}
|
|
292
|
+
await this.setupSubscription(sub);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Cancels a queue subscription and removes it from the restoration list.
|
|
296
|
+
*/
|
|
297
|
+
async unsubscribe(queue) {
|
|
298
|
+
const tag = this.consumerTags.get(queue);
|
|
299
|
+
if (tag && this.channel) {
|
|
300
|
+
await this.channel.cancel(tag);
|
|
301
|
+
}
|
|
302
|
+
this.consumerTags.delete(queue);
|
|
303
|
+
this.subscriptions = this.subscriptions.filter((s) => s.queue !== queue);
|
|
304
|
+
this.logger.info({ queue }, "Unsubscribed from queue");
|
|
305
|
+
}
|
|
306
|
+
async setupSubscription(sub) {
|
|
307
|
+
if (!this.channel) {
|
|
308
|
+
throw new Error("Channel not available");
|
|
309
|
+
}
|
|
310
|
+
const { exchange, routingKey, queue, handler, options } = sub;
|
|
311
|
+
const dlxExchange = `${exchange}.dlx`;
|
|
312
|
+
const dlqQueue = `${queue}.dlq`;
|
|
313
|
+
const dlqRoutingKey = `${routingKey}.dead`;
|
|
314
|
+
await this.channel.assertExchange(dlxExchange, "topic", { durable: true });
|
|
315
|
+
await this.channel.assertQueue(dlqQueue, { durable: true });
|
|
316
|
+
await this.channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey);
|
|
317
|
+
if (!this.assertedExchanges.has(exchange)) {
|
|
318
|
+
await this.channel.assertExchange(exchange, "topic", { durable: true });
|
|
319
|
+
this.assertedExchanges.add(exchange);
|
|
320
|
+
}
|
|
321
|
+
await this.channel.assertQueue(queue, {
|
|
322
|
+
durable: true,
|
|
323
|
+
deadLetterExchange: dlxExchange,
|
|
324
|
+
deadLetterRoutingKey: dlqRoutingKey,
|
|
325
|
+
arguments: {
|
|
326
|
+
"x-dead-letter-strategy": "at-least-once",
|
|
327
|
+
"x-queue-type": "quorum",
|
|
328
|
+
"x-overflow": "reject-publish",
|
|
329
|
+
...options?.queueArguments
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
await this.channel.bindQueue(queue, exchange, routingKey);
|
|
333
|
+
const { consumerTag } = await this.channel.consume(
|
|
334
|
+
queue,
|
|
335
|
+
(message) => {
|
|
336
|
+
if (message) {
|
|
337
|
+
this.handleWithRetry(message, handler);
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
{ noAck: false }
|
|
341
|
+
);
|
|
342
|
+
this.consumerTags.set(queue, consumerTag);
|
|
343
|
+
this.logger.info({ exchange, routingKey, queue }, "Subscribed to queue");
|
|
344
|
+
}
|
|
345
|
+
async handleWithRetry(message, handler) {
|
|
346
|
+
const channel = this.channel;
|
|
347
|
+
if (!channel) {
|
|
348
|
+
this.logger.warn("Cannot process message -- channel unavailable");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
this.inflightCount++;
|
|
352
|
+
try {
|
|
353
|
+
const startTime = Date.now();
|
|
354
|
+
let attempts = 0;
|
|
355
|
+
const routingKey = message.fields.routingKey;
|
|
356
|
+
const exchange = message.fields.exchange;
|
|
357
|
+
let content;
|
|
358
|
+
try {
|
|
359
|
+
content = JSON.parse(message.content.toString());
|
|
360
|
+
} catch (parseError) {
|
|
361
|
+
const rawContent = message.content.toString();
|
|
362
|
+
const rawPreview = rawContent.substring(0, 100);
|
|
363
|
+
this.logger.error(
|
|
364
|
+
{
|
|
365
|
+
error: parseError,
|
|
366
|
+
exchange,
|
|
367
|
+
routingKey,
|
|
368
|
+
rawContentPreview: rawPreview + (rawContent.length > 100 ? "..." : "")
|
|
369
|
+
},
|
|
370
|
+
"Failed to parse message JSON, sending to DLQ"
|
|
371
|
+
);
|
|
372
|
+
channel.nack(message, false, false);
|
|
373
|
+
this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: "invalid_json" });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
this.logger.debug({ exchange, routingKey, payload: content }, "Message received, processing");
|
|
377
|
+
while (attempts < this.options.maxRetries) {
|
|
378
|
+
try {
|
|
379
|
+
await handler(content);
|
|
380
|
+
const duration2 = Date.now() - startTime;
|
|
381
|
+
this.logger.info(
|
|
382
|
+
{ exchange, routingKey, duration: duration2, attempts: attempts + 1 },
|
|
383
|
+
"Message processed successfully"
|
|
384
|
+
);
|
|
385
|
+
channel.ack(message);
|
|
386
|
+
this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration: duration2, attempts: attempts + 1 });
|
|
387
|
+
return;
|
|
388
|
+
} catch (error) {
|
|
389
|
+
attempts++;
|
|
390
|
+
this.logger.error(
|
|
391
|
+
{
|
|
392
|
+
error: error instanceof Error ? error.message : error,
|
|
393
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
394
|
+
exchange,
|
|
395
|
+
routingKey,
|
|
396
|
+
attempt: attempts,
|
|
397
|
+
maxRetries: this.options.maxRetries
|
|
398
|
+
},
|
|
399
|
+
"Handler failed"
|
|
400
|
+
);
|
|
401
|
+
if (attempts < this.options.maxRetries) {
|
|
402
|
+
await this.sleep(this.options.retryDelay);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const duration = Date.now() - startTime;
|
|
407
|
+
this.logger.error(
|
|
408
|
+
{ exchange, routingKey, maxRetries: this.options.maxRetries, duration },
|
|
409
|
+
"Message failed after max retries, sending to DLQ"
|
|
410
|
+
);
|
|
411
|
+
channel.nack(message, false, false);
|
|
412
|
+
this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: "max_retries_exhausted" });
|
|
413
|
+
} finally {
|
|
414
|
+
this.inflightCount--;
|
|
415
|
+
if (this.inflightCount === 0 && this.drainResolve) {
|
|
416
|
+
this.drainResolve();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Lightweight liveness probe: creates and immediately deletes a temporary
|
|
422
|
+
* exclusive queue. Returns `false` when disconnected or on broker error.
|
|
423
|
+
*/
|
|
424
|
+
async checkHealth() {
|
|
425
|
+
if (!this.connected || !this.channel) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
const { queue } = await this.channel.assertQueue("", {
|
|
430
|
+
exclusive: true,
|
|
431
|
+
autoDelete: true
|
|
432
|
+
});
|
|
433
|
+
await this.channel.deleteQueue(queue);
|
|
434
|
+
return true;
|
|
435
|
+
} catch (error) {
|
|
436
|
+
this.logger.warn({ error }, "Health check failed");
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
waitForDrain(timeout) {
|
|
441
|
+
if (this.inflightCount === 0) return Promise.resolve();
|
|
442
|
+
return new Promise((resolve) => {
|
|
443
|
+
const timer = setTimeout(() => {
|
|
444
|
+
this.logger.warn(
|
|
445
|
+
{ inflightCount: this.inflightCount },
|
|
446
|
+
"Close timeout reached with messages still in flight"
|
|
447
|
+
);
|
|
448
|
+
this.drainResolve = null;
|
|
449
|
+
resolve();
|
|
450
|
+
}, timeout);
|
|
451
|
+
this.drainResolve = () => {
|
|
452
|
+
clearTimeout(timer);
|
|
453
|
+
this.drainResolve = null;
|
|
454
|
+
resolve();
|
|
455
|
+
};
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
/** Invokes a hook callback, swallowing errors so hooks never break message flow. */
|
|
459
|
+
callHook(hook, info) {
|
|
460
|
+
try {
|
|
461
|
+
hook?.(info);
|
|
462
|
+
} catch {
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
sleep(ms) {
|
|
466
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
export {
|
|
470
|
+
RabbitMQClient
|
|
471
|
+
};
|
|
472
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/logger.ts"],"sourcesContent":["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":";AAAA,OAAO,UAAU;;;ACAjB,OAAO,UAA2B;AAS3B,IAAM,eAAe,CAAC,WAA4B;AACvD,MAAI,QAAQ;AACV,WAAO,OAAO,MAAM,EAAE,SAAS,kBAAkB,CAAC;AAAA,EACpD;AACA,SAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC;;;ADFA,IAAM,mBAAmB;AACzB,IAAM,6BAA6B;AAEnC,IAAM,WAAW;AAAA,EACf,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,sBAAsB;AAAA,EACtB,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,UAAU;AAAA,EACV,cAAc;AAChB;AAeO,IAAM,iBAAN,MAAqB;AAAA,EAClB,aAAuC;AAAA,EACvC,UAAsC;AAAA,EACtC,YAAY;AAAA,EACZ,gBAAwC,CAAC;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACT,wBAA8C;AAAA,EAC9C,sBAA4C;AAAA,EAC5C,oBAAoB,oBAAI,IAAY;AAAA,EACpC,eAAe,oBAAI,IAAoB;AAAA,EACvC,gBAAgB;AAAA,EAChB,eAAoC;AAAA,EAE5C,YAAY,SAAgC;AAC1C,SAAK,UAAU;AAAA,MACb,KAAK,QAAQ;AAAA,MACb,YAAY,QAAQ,cAAc,SAAS;AAAA,MAC3C,YAAY,QAAQ,cAAc,SAAS;AAAA,MAC3C,sBAAsB,QAAQ,wBAAwB,SAAS;AAAA,MAC/D,iBAAiB,QAAQ,mBAAmB,SAAS;AAAA,MACrD,oBAAoB,QAAQ,sBAAsB,SAAS;AAAA,MAC3D,UAAU,QAAQ,YAAY,SAAS;AAAA,MACvC,cAAc,QAAQ,gBAAgB,SAAS;AAAA,IACjD;AACA,SAAK,SAAS,aAAa,QAAQ,MAAM;AACzC,SAAK,QAAQ,QAAQ,SAAS,CAAC;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,OAAsB;AAC1B,QAAI,KAAK,cAAc,KAAK,WAAW;AACrC;AAAA,IACF;AACA,QAAI,KAAK,uBAAuB;AAC9B,aAAO,KAAK;AAAA,IACd;AAGA,SAAK,wBAAwB,KAAK,UAAU,KAAK,QAAQ,eAAe;AACxE,SAAK,sBAAsB,MAAM,MAAM,MAAS;AAChD,QAAI;AACF,YAAM,KAAK;AAAA,IACb,UAAE;AACA,WAAK,wBAAwB;AAAA,IAC/B;AAAA,EACF;AAAA,EAEA,MAAc,UAAU,aAAqC;AAC3D,SAAK,kBAAkB,MAAM;AAC7B,SAAK,aAAa,MAAM;AACxB,QAAI,WAAW;AACf,WAAO,CAAC,KAAK,WAAW;AACtB,UAAI;AACF,aAAK,aAAa,MAAM,KAAK,QAAQ,KAAK,QAAQ,GAAG;AACrD,aAAK,OAAO,KAAK,qBAAqB;AACtC,aAAK,UAAU,MAAM,KAAK,WAAW,qBAAqB;AAC1D,aAAK,OAAO,KAAK,yBAAyB;AAC1C,cAAM,KAAK,QAAQ,SAAS,KAAK,QAAQ,QAAQ;AACjD,aAAK,OAAO,KAAK,EAAE,UAAU,KAAK,QAAQ,SAAS,GAAG,sBAAsB;AAE5E,aAAK,WAAW,GAAG,SAAS,CAAC,UAAiB;AAC5C,eAAK,YAAY;AACjB,eAAK,OAAO,MAAM,EAAE,MAAM,GAAG,kBAAkB;AAAA,QACjD,CAAC;AACD,aAAK,WAAW,GAAG,SAAS,MAAM;AAChC,eAAK,YAAY;AACjB,eAAK,OAAO,KAAK,mBAAmB;AACpC,eAAK,mBAAmB;AAAA,QAC1B,CAAC;AACD,aAAK,QAAQ,GAAG,SAAS,CAAC,UAAiB;AACzC,eAAK,OAAO,MAAM,EAAE,MAAM,GAAG,eAAe;AAC5C,eAAK,qBAAqB;AAAA,QAC5B,CAAC;AACD,aAAK,QAAQ,GAAG,SAAS,MAAM;AAC7B,eAAK,OAAO,KAAK,gBAAgB;AACjC,eAAK,qBAAqB;AAAA,QAC5B,CAAC;AAED,aAAK,YAAY;AACjB,aAAK,OAAO,KAAK,iCAAiC;AAAA,MACpD,SAAS,OAAO;AACd;AACA,aAAK,YAAY;AACjB,YAAI,gBAAgB,UAAa,YAAY,aAAa;AACxD,eAAK,OAAO,MAAM,EAAE,OAAO,UAAU,YAAY,GAAG,0CAA0C;AAC9F,gBAAM,IAAI;AAAA,YACR,uCAAuC,QAAQ;AAAA,UAEjD;AAAA,QACF;AACA,aAAK,OAAO;AAAA,UACV;AAAA,YACE;AAAA,YACA,SAAS;AAAA,YACT,aAAa,eAAe;AAAA,YAC5B,cAAc,KAAK,QAAQ;AAAA,UAC7B;AAAA,UACA;AAAA,QACF;AACA,cAAM,KAAK,MAAM,KAAK,QAAQ,oBAAoB;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,uBAA6B;AACnC,QAAI,KAAK,WAAW;AAClB,WAAK,YAAY;AACjB,WAAK,mBAAmB;AAAA,IAC1B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,cAAuB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,QACJ,UACA,YACA,SACA,SACe;AACf,QAAI,WAAW;AACf,UAAM,cAAc,SAAS,eAAe,KAAK,QAAQ;AACzD,UAAM,YAAY,KAAK,QAAQ;AAC/B,UAAM,UAAU,OAAO,KAAK,KAAK,UAAU,OAAO,CAAC;AAEnD,WAAO,WAAW,aAAa;AAC7B,UAAI;AACF,YAAI,CAAC,KAAK,WAAW;AACnB,gBAAM,KAAK,mBAAmB;AAAA,QAChC;AAEA,YAAI,CAAC,KAAK,SAAS;AACjB,gBAAM,IAAI,MAAM,uBAAuB;AAAA,QACzC;AAEA,YAAI,CAAC,KAAK,kBAAkB,IAAI,QAAQ,GAAG;AACzC,gBAAM,KAAK,QAAQ,eAAe,UAAU,SAAS,EAAE,SAAS,KAAK,CAAC;AACtE,eAAK,kBAAkB,IAAI,QAAQ;AAAA,QACrC;AAEA,aAAK,QAAQ,QAAQ,UAAU,YAAY,SAAS;AAAA,UAClD,YAAY;AAAA,QACd,CAAC;AAED,cAAM,KAAK,QAAQ,gBAAgB;AAEnC,YAAI,WAAW,GAAG;AAChB,eAAK,OAAO,KAAK,EAAE,UAAU,YAAY,aAAa,QAAQ,QAAQ,SAAS,GAAG,iCAAiC;AAAA,QACrH,OAAO;AACL,eAAK,OAAO,KAAK,EAAE,UAAU,YAAY,aAAa,QAAQ,OAAO,GAAG,mBAAmB;AAAA,QAC7F;AACA,aAAK,OAAO,MAAM,EAAE,UAAU,YAAY,SAAS,QAAQ,GAAG,2BAA2B;AAEzF,aAAK,SAAS,KAAK,MAAM,WAAW,EAAE,UAAU,YAAY,UAAU,WAAW,EAAE,CAAC;AAEpF;AAAA,MACF,SAAS,OAAO;AACd;AACA,aAAK,YAAY;AAEjB,YAAI,YAAY,aAAa;AAC3B,eAAK,OAAO;AAAA,YACV,EAAE,OAAO,UAAU,YAAY,UAAU,YAAY;AAAA,YACrD;AAAA,UACF;AACA,gBAAM,IAAI;AAAA,YACR,wBAAwB,QAAQ,IAAI,UAAU,UAAU,QAAQ,cAAc,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,UACtI;AAAA,QACF;AAEA,cAAM,aAAa,KAAK;AAAA,UACtB,YAAY,KAAK,IAAI,GAAG,WAAW,CAAC;AAAA,UACpC;AAAA,QACF;AAEA,aAAK,OAAO;AAAA,UACV,EAAE,OAAO,UAAU,YAAY,SAAS,UAAU,aAAa,cAAc,WAAW;AAAA,UACxF;AAAA,QACF;AAEA,cAAM,KAAK,MAAM,UAAU;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,MAAM,qBAAqB,MAAqB;AACpD,QAAI;AACF,UAAI,KAAK,gBAAgB,GAAG;AAC1B,aAAK,OAAO,KAAK,EAAE,eAAe,KAAK,cAAc,GAAG,yCAAyC;AACjG,cAAM,KAAK,aAAa,KAAK,QAAQ,YAAY;AAAA,MACnD;AACA,YAAM,KAAK,SAAS,MAAM;AAC1B,YAAM,KAAK,YAAY,MAAM;AAC7B,WAAK,aAAa;AAClB,WAAK,UAAU;AACf,WAAK,YAAY;AACjB,WAAK,wBAAwB;AAC7B,WAAK,sBAAsB;AAC3B,WAAK,kBAAkB,MAAM;AAC7B,WAAK,aAAa,MAAM;AACxB,UAAI,oBAAoB;AACtB,aAAK,gBAAgB,CAAC;AAAA,MACxB;AACA,WAAK,OAAO,KAAK,mBAAmB;AAAA,IACtC,SAAS,OAAO;AACd,WAAK,OAAO,MAAM,EAAE,MAAM,GAAG,0BAA0B;AACvD,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAc,qBAAoC;AAChD,QAAI,KAAK,qBAAqB;AAC5B,aAAO,KAAK;AAAA,IACd;AACA,SAAK,YAAY;AACjB,SAAK,OAAO,KAAK,0BAA0B;AAC3C,SAAK,sBAAsB,KAAK,YAAY;AAC5C,SAAK,oBAAoB,MAAM,MAAM,MAAS;AAC9C,QAAI;AACF,YAAM,KAAK;AAAA,IACb,UAAE;AACA,WAAK,sBAAsB;AAAA,IAC7B;AAAA,EACF;AAAA,EAEA,MAAc,cAA6B;AACzC,UAAM,KAAK,UAAU;AACrB,UAAM,KAAK,eAAe;AAAA,EAC5B;AAAA,EAEA,MAAc,iBAAgC;AAC5C,QAAI,KAAK,cAAc,WAAW,GAAG;AACnC;AAAA,IACF;AACA,SAAK,OAAO,KAAK,EAAE,OAAO,KAAK,cAAc,OAAO,GAAG,+BAA+B;AACtF,UAAM,OAAO,CAAC,GAAG,KAAK,aAAa;AACnC,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC5B,KAAK,IAAI,OAAO,QAAQ;AACtB,cAAM,KAAK,kBAAkB,GAAG;AAChC,eAAO,IAAI;AAAA,MACb,CAAC;AAAA,IACH;AACA,UAAM,YAAsB,CAAC;AAC7B,UAAM,SAAmB,CAAC;AAC1B,YAAQ,QAAQ,CAAC,QAAQ,UAAU;AACjC,UAAI,OAAO,WAAW,aAAa;AACjC,kBAAU,KAAK,OAAO,KAAK;AAAA,MAC7B,OAAO;AACL,cAAM,QAAQ,KAAK,KAAK,EAAE;AAC1B,eAAO,KAAK,KAAK;AACjB,aAAK,OAAO,MAAM,EAAE,OAAO,OAAO,QAAQ,MAAM,GAAG,iCAAiC;AAAA,MACtF;AAAA,IACF,CAAC;AACD,QAAI,UAAU,SAAS,GAAG;AACxB,WAAK,OAAO,KAAK,EAAE,OAAO,UAAU,QAAQ,QAAQ,UAAU,GAAG,sCAAsC;AAAA,IACzG;AACA,QAAI,OAAO,SAAS,GAAG;AACrB,WAAK,OAAO,KAAK,EAAE,OAAO,OAAO,QAAQ,QAAQ,OAAO,GAAG,sCAAsC;AAAA,IACnG;AACA,SAAK,SAAS,KAAK,MAAM,aAAa,EAAE,uBAAuB,UAAU,QAAQ,qBAAqB,OAAO,OAAO,CAAC;AAAA,EACvH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,UACJ,UACA,YACA,OACA,SACA,SACe;AACf,QAAI,CAAC,KAAK,aAAa,CAAC,KAAK,SAAS;AACpC,YAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AACA,UAAM,MAA4B,EAAE,UAAU,YAAY,OAAO,SAAS,QAAQ;AAClF,UAAM,gBAAgB,KAAK,cAAc,UAAU,CAAC,MAAM,EAAE,UAAU,KAAK;AAC3E,QAAI,kBAAkB,IAAI;AACxB,WAAK,cAAc,KAAK,GAAG;AAAA,IAC7B,OAAO;AACL,WAAK,cAAc,aAAa,IAAI;AAAA,IACtC;AACA,UAAM,KAAK,kBAAkB,GAAG;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,OAA8B;AAC9C,UAAM,MAAM,KAAK,aAAa,IAAI,KAAK;AACvC,QAAI,OAAO,KAAK,SAAS;AACvB,YAAM,KAAK,QAAQ,OAAO,GAAG;AAAA,IAC/B;AACA,SAAK,aAAa,OAAO,KAAK;AAC9B,SAAK,gBAAgB,KAAK,cAAc,OAAO,CAAC,MAAM,EAAE,UAAU,KAAK;AACvE,SAAK,OAAO,KAAK,EAAE,MAAM,GAAG,yBAAyB;AAAA,EACvD;AAAA,EAEA,MAAc,kBAAkB,KAA0C;AACxE,QAAI,CAAC,KAAK,SAAS;AACjB,YAAM,IAAI,MAAM,uBAAuB;AAAA,IACzC;AACA,UAAM,EAAE,UAAU,YAAY,OAAO,SAAS,QAAQ,IAAI;AAC1D,UAAM,cAAc,GAAG,QAAQ;AAC/B,UAAM,WAAW,GAAG,KAAK;AACzB,UAAM,gBAAgB,GAAG,UAAU;AAEnC,UAAM,KAAK,QAAQ,eAAe,aAAa,SAAS,EAAE,SAAS,KAAK,CAAC;AACzE,UAAM,KAAK,QAAQ,YAAY,UAAU,EAAE,SAAS,KAAK,CAAC;AAC1D,UAAM,KAAK,QAAQ,UAAU,UAAU,aAAa,aAAa;AAEjE,QAAI,CAAC,KAAK,kBAAkB,IAAI,QAAQ,GAAG;AACzC,YAAM,KAAK,QAAQ,eAAe,UAAU,SAAS,EAAE,SAAS,KAAK,CAAC;AACtE,WAAK,kBAAkB,IAAI,QAAQ;AAAA,IACrC;AAEA,UAAM,KAAK,QAAQ,YAAY,OAAO;AAAA,MACpC,SAAS;AAAA,MACT,oBAAoB;AAAA,MACpB,sBAAsB;AAAA,MACtB,WAAW;AAAA,QACT,0BAA0B;AAAA,QAC1B,gBAAgB;AAAA,QAChB,cAAc;AAAA,QACd,GAAG,SAAS;AAAA,MACd;AAAA,IACF,CAAC;AAED,UAAM,KAAK,QAAQ,UAAU,OAAO,UAAU,UAAU;AACxD,UAAM,EAAE,YAAY,IAAI,MAAM,KAAK,QAAQ;AAAA,MACzC;AAAA,MACA,CAAC,YAAY;AACX,YAAI,SAAS;AACX,eAAK,gBAAgB,SAAS,OAAO;AAAA,QACvC;AAAA,MACF;AAAA,MACA,EAAE,OAAO,MAAM;AAAA,IACjB;AACA,SAAK,aAAa,IAAI,OAAO,WAAW;AACxC,SAAK,OAAO,KAAK,EAAE,UAAU,YAAY,MAAM,GAAG,qBAAqB;AAAA,EACzE;AAAA,EAEA,MAAc,gBACZ,SACA,SACe;AAGf,UAAM,UAAU,KAAK;AACrB,QAAI,CAAC,SAAS;AACZ,WAAK,OAAO,KAAK,+CAA+C;AAChE;AAAA,IACF;AACA,SAAK;AACL,QAAI;AACF,YAAM,YAAY,KAAK,IAAI;AAC3B,UAAI,WAAW;AACf,YAAM,aAAa,QAAQ,OAAO;AAClC,YAAM,WAAW,QAAQ,OAAO;AAEhC,UAAI;AACJ,UAAI;AACF,kBAAU,KAAK,MAAM,QAAQ,QAAQ,SAAS,CAAC;AAAA,MACjD,SAAS,YAAY;AACnB,cAAM,aAAa,QAAQ,QAAQ,SAAS;AAC5C,cAAM,aAAa,WAAW,UAAU,GAAG,GAAG;AAC9C,aAAK,OAAO;AAAA,UACV;AAAA,YACE,OAAO;AAAA,YACP;AAAA,YACA;AAAA,YACA,mBAAmB,cAAc,WAAW,SAAS,MAAM,QAAQ;AAAA,UACrE;AAAA,UACA;AAAA,QACF;AACA,gBAAQ,KAAK,SAAS,OAAO,KAAK;AAClC,aAAK,SAAS,KAAK,MAAM,cAAc,EAAE,UAAU,YAAY,UAAU,GAAG,QAAQ,eAAe,CAAC;AACpG;AAAA,MACF;AAEA,WAAK,OAAO,MAAM,EAAE,UAAU,YAAY,SAAS,QAAQ,GAAG,8BAA8B;AAE5F,aAAO,WAAW,KAAK,QAAQ,YAAY;AACzC,YAAI;AACF,gBAAM,QAAQ,OAAO;AACrB,gBAAMA,YAAW,KAAK,IAAI,IAAI;AAC9B,eAAK,OAAO;AAAA,YACV,EAAE,UAAU,YAAY,UAAAA,WAAU,UAAU,WAAW,EAAE;AAAA,YACzD;AAAA,UACF;AACA,kBAAQ,IAAI,OAAO;AACnB,eAAK,SAAS,KAAK,MAAM,oBAAoB,EAAE,UAAU,YAAY,UAAAA,WAAU,UAAU,WAAW,EAAE,CAAC;AACvG;AAAA,QACF,SAAS,OAAO;AACd;AACA,eAAK,OAAO;AAAA,YACV;AAAA,cACE,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,cAChD,OAAO,iBAAiB,QAAQ,MAAM,QAAQ;AAAA,cAC9C;AAAA,cACA;AAAA,cACA,SAAS;AAAA,cACT,YAAY,KAAK,QAAQ;AAAA,YAC3B;AAAA,YACA;AAAA,UACF;AACA,cAAI,WAAW,KAAK,QAAQ,YAAY;AACtC,kBAAM,KAAK,MAAM,KAAK,QAAQ,UAAU;AAAA,UAC1C;AAAA,QACF;AAAA,MACF;AAEA,YAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,WAAK,OAAO;AAAA,QACV,EAAE,UAAU,YAAY,YAAY,KAAK,QAAQ,YAAY,SAAS;AAAA,QACtE;AAAA,MACF;AACA,cAAQ,KAAK,SAAS,OAAO,KAAK;AAClC,WAAK,SAAS,KAAK,MAAM,cAAc,EAAE,UAAU,YAAY,UAAU,QAAQ,wBAAwB,CAAC;AAAA,IAC5G,UAAE;AACA,WAAK;AACL,UAAI,KAAK,kBAAkB,KAAK,KAAK,cAAc;AACjD,aAAK,aAAa;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAgC;AACpC,QAAI,CAAC,KAAK,aAAa,CAAC,KAAK,SAAS;AACpC,aAAO;AAAA,IACT;AACA,QAAI;AACF,YAAM,EAAE,MAAM,IAAI,MAAM,KAAK,QAAQ,YAAY,IAAI;AAAA,QACnD,WAAW;AAAA,QACX,YAAY;AAAA,MACd,CAAC;AACD,YAAM,KAAK,QAAQ,YAAY,KAAK;AACpC,aAAO;AAAA,IACT,SAAS,OAAO;AACd,WAAK,OAAO,KAAK,EAAE,MAAM,GAAG,qBAAqB;AACjD,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEQ,aAAa,SAAgC;AACnD,QAAI,KAAK,kBAAkB,EAAG,QAAO,QAAQ,QAAQ;AACrD,WAAO,IAAI,QAAc,CAAC,YAAY;AACpC,YAAM,QAAQ,WAAW,MAAM;AAC7B,aAAK,OAAO;AAAA,UACV,EAAE,eAAe,KAAK,cAAc;AAAA,UACpC;AAAA,QACF;AACA,aAAK,eAAe;AACpB,gBAAQ;AAAA,MACV,GAAG,OAAO;AACV,WAAK,eAAe,MAAM;AACxB,qBAAa,KAAK;AAClB,aAAK,eAAe;AACpB,gBAAQ;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA,EAGQ,SAAY,MAAuC,MAAe;AACxE,QAAI;AAAE,aAAO,IAAI;AAAA,IAAE,QAAQ;AAAA,IAAkB;AAAA,EAC/C;AAAA,EAEQ,MAAM,IAA2B;AACvC,WAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,EACzD;AACF;","names":["duration"]}
|