@bitspacerlabs/rabbit-relay 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.
@@ -0,0 +1,62 @@
1
+ import { Options } from "amqplib";
2
+ import { EventEnvelope } from "./eventFactories";
3
+ export interface ExchangeConfig {
4
+ exchangeType?: "topic" | "direct" | "fanout";
5
+ routingKey?: string;
6
+ durable?: boolean;
7
+ publisherConfirms?: boolean;
8
+ queueArgs?: Options.AssertQueue["arguments"];
9
+ /**
10
+ * If true, do NOT declare the queue; only check it exists.
11
+ * Use this when a separate setup step has already created the queue with specific args.
12
+ */
13
+ passiveQueue?: boolean;
14
+ }
15
+ export interface ConsumeOptions {
16
+ /** Max unacked messages this consumer can hold. Also default concurrency. */
17
+ prefetch?: number;
18
+ /** Parallel handler executions. Defaults to prefetch (or 1). */
19
+ concurrency?: number;
20
+ /** If true, nack+requeue on handler error; else ack even on error. (back-compat) */
21
+ requeueOnError?: boolean;
22
+ /** What to do when the handler throws. Default "ack". */
23
+ onError?: "ack" | "requeue" | "dead-letter";
24
+ }
25
+ /**
26
+ * Generic Broker Interface:
27
+ * TEvents maps event name keys -> EventEnvelope types.
28
+ */
29
+ export interface BrokerInterface<TEvents extends Record<string, EventEnvelope>> {
30
+ handle<K extends keyof TEvents>(eventName: K | "*", handler: (id: string | number, event: TEvents[K]) => Promise<unknown>): BrokerInterface<TEvents>;
31
+ consume(opts?: ConsumeOptions): Promise<{
32
+ stop(): Promise<void>;
33
+ }>;
34
+ produce<K extends keyof TEvents>(...events: TEvents[K][]): Promise<void | unknown>;
35
+ produceMany<K extends keyof TEvents>(...events: TEvents[K][]): Promise<void>;
36
+ with<U extends Record<string, (...args: any[]) => EventEnvelope>>(events: U): BrokerInterface<{
37
+ [K in keyof U]: ReturnType<U[K]>;
38
+ }> & {
39
+ [K in keyof U]: (...args: Parameters<U[K]>) => ReturnType<U[K]>;
40
+ };
41
+ }
42
+ export declare class RabbitMQBroker {
43
+ private peerName;
44
+ private defaultCfg;
45
+ /** The current live channel promise (replaced after reconnect). */
46
+ private channelPromise;
47
+ /** Reconnect state */
48
+ private reconnecting;
49
+ private backoffMs;
50
+ private readonly maxBackoffMs;
51
+ /** Callbacks to run after a successful reconnect (like re-assert topology, resume consume). */
52
+ private onReconnectCbs;
53
+ constructor(peerName: string, config?: ExchangeConfig);
54
+ private initChannel;
55
+ private scheduleReconnect;
56
+ private getChannel;
57
+ private onReconnect;
58
+ queue(queueName: string): {
59
+ exchange: <TEvents extends Record<string, EventEnvelope>>(exchangeName: string, exchangeConfig?: ExchangeConfig) => Promise<BrokerInterface<TEvents>>;
60
+ };
61
+ private exchange;
62
+ }
@@ -0,0 +1,403 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RabbitMQBroker = void 0;
4
+ const config_1 = require("./config");
5
+ const pluginManager_1 = require("./pluginManager");
6
+ function generateUuid() {
7
+ return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
8
+ }
9
+ class RabbitMQBroker {
10
+ constructor(peerName, config = {}) {
11
+ var _a, _b, _c, _d, _e;
12
+ /** Reconnect state */
13
+ this.reconnecting = false;
14
+ this.backoffMs = 500;
15
+ this.maxBackoffMs = 20000;
16
+ /** Callbacks to run after a successful reconnect (like re-assert topology, resume consume). */
17
+ this.onReconnectCbs = [];
18
+ this.peerName = peerName;
19
+ this.defaultCfg = {
20
+ exchangeType: (_a = config.exchangeType) !== null && _a !== void 0 ? _a : "topic",
21
+ routingKey: (_b = config.routingKey) !== null && _b !== void 0 ? _b : "#",
22
+ durable: (_c = config.durable) !== null && _c !== void 0 ? _c : true,
23
+ publisherConfirms: (_d = config.publisherConfirms) !== null && _d !== void 0 ? _d : false,
24
+ queueArgs: config.queueArgs,
25
+ passiveQueue: (_e = config.passiveQueue) !== null && _e !== void 0 ? _e : false,
26
+ };
27
+ this.initChannel();
28
+ }
29
+ async initChannel() {
30
+ var _a, _b, _c, _d;
31
+ this.channelPromise = (0, config_1.getRabbitMQChannel)();
32
+ const ch = await this.channelPromise;
33
+ this.backoffMs = 500;
34
+ const onClose = () => this.scheduleReconnect("channel.close");
35
+ const onError = () => this.scheduleReconnect("channel.error");
36
+ (_b = (_a = ch).on) === null || _b === void 0 ? void 0 : _b.call(_a, "close", onClose);
37
+ (_d = (_c = ch).on) === null || _d === void 0 ? void 0 : _d.call(_c, "error", onError);
38
+ }
39
+ async scheduleReconnect(reason) {
40
+ if (this.reconnecting)
41
+ return;
42
+ this.reconnecting = true;
43
+ // eslint-disable-next-line no-constant-condition
44
+ while (true) {
45
+ try {
46
+ const jitter = Math.floor(Math.random() * 250);
47
+ await new Promise((r) => setTimeout(r, this.backoffMs + jitter));
48
+ await this.initChannel();
49
+ const ch = await this.channelPromise;
50
+ this.backoffMs = 500;
51
+ this.reconnecting = false;
52
+ for (const cb of this.onReconnectCbs) {
53
+ try {
54
+ await cb(ch);
55
+ }
56
+ catch (e) {
57
+ console.error("[broker] onReconnect callback failed:", e);
58
+ }
59
+ }
60
+ return;
61
+ }
62
+ catch {
63
+ this.backoffMs = Math.min(this.maxBackoffMs, Math.floor(this.backoffMs * 1.7 + Math.random() * 100));
64
+ console.error(`[broker] reconnect failed (${reason}), retrying in ~${this.backoffMs}ms`);
65
+ }
66
+ }
67
+ }
68
+ async getChannel() {
69
+ return this.channelPromise;
70
+ }
71
+ onReconnect(cb) {
72
+ this.onReconnectCbs.push(cb);
73
+ }
74
+ queue(queueName) {
75
+ return {
76
+ exchange: async (exchangeName, exchangeConfig = {}) => {
77
+ return this.exchange(exchangeName, queueName, exchangeConfig);
78
+ },
79
+ };
80
+ }
81
+ async exchange(exchangeName, queueName, exchangeConfig = {}) {
82
+ const assertTopology = async (channel) => {
83
+ var _a, _b, _c, _d, _e, _f;
84
+ const cfg = {
85
+ exchangeType: (_a = exchangeConfig.exchangeType) !== null && _a !== void 0 ? _a : this.defaultCfg.exchangeType,
86
+ routingKey: (_b = exchangeConfig.routingKey) !== null && _b !== void 0 ? _b : this.defaultCfg.routingKey,
87
+ durable: (_c = exchangeConfig.durable) !== null && _c !== void 0 ? _c : this.defaultCfg.durable,
88
+ publisherConfirms: (_d = exchangeConfig.publisherConfirms) !== null && _d !== void 0 ? _d : this.defaultCfg.publisherConfirms,
89
+ queueArgs: (_e = exchangeConfig.queueArgs) !== null && _e !== void 0 ? _e : this.defaultCfg.queueArgs,
90
+ passiveQueue: (_f = exchangeConfig.passiveQueue) !== null && _f !== void 0 ? _f : this.defaultCfg.passiveQueue,
91
+ };
92
+ await channel.assertExchange(exchangeName, cfg.exchangeType, { durable: cfg.durable });
93
+ if (cfg.passiveQueue) {
94
+ if (cfg.queueArgs) {
95
+ console.warn(`[broker] passiveQueue=true: ignoring queueArgs for '${queueName}' (not declaring).`);
96
+ }
97
+ try {
98
+ await channel.checkQueue(queueName);
99
+ }
100
+ catch (err) {
101
+ const code = err === null || err === void 0 ? void 0 : err.code;
102
+ if (code === 404) {
103
+ throw new Error(`[broker] passiveQueue check failed: queue '${queueName}' does not exist. ` +
104
+ `Either create it in your setup step with the desired arguments, ` +
105
+ `or call with passiveQueue:false and queueArgs to auto-declare.`);
106
+ }
107
+ throw err;
108
+ }
109
+ }
110
+ else {
111
+ try {
112
+ const qOpts = {
113
+ durable: cfg.durable,
114
+ ...(cfg.queueArgs ? { arguments: cfg.queueArgs } : {}),
115
+ };
116
+ await channel.assertQueue(queueName, qOpts);
117
+ }
118
+ catch (err) {
119
+ if ((err === null || err === void 0 ? void 0 : err.code) === 406) {
120
+ throw new Error(`[broker] QueueDeclare PRECONDITION_FAILED for '${queueName}'. ` +
121
+ `Existing queue has different arguments. ` +
122
+ `Fix: delete the queue or switch to { passiveQueue: true } if you're using a setup step.`);
123
+ }
124
+ throw err;
125
+ }
126
+ }
127
+ // (Re)bind is idempotent - safe to call even if binding already exists
128
+ await channel.bindQueue(queueName, exchangeName, cfg.routingKey);
129
+ };
130
+ const channel = await this.getChannel();
131
+ await assertTopology(channel);
132
+ const handlers = new Map();
133
+ let consumerTag;
134
+ let isConsuming = false;
135
+ let consumeCh = null;
136
+ let prefetchCount = 1;
137
+ let concurrency = 1;
138
+ let onError = "ack";
139
+ this.onReconnect(async (ch) => {
140
+ await assertTopology(ch);
141
+ if (isConsuming) {
142
+ if (prefetchCount > 0)
143
+ await ch.prefetch(prefetchCount, false);
144
+ consumeCh = ch; // pin
145
+ const ok = await ch.consume(queueName, onMessage);
146
+ consumerTag = ok.consumerTag;
147
+ }
148
+ });
149
+ const handle = (eventName, handler) => {
150
+ handlers.set(eventName, handler);
151
+ return brokerInterface;
152
+ };
153
+ // Backpressure-aware publish helper
154
+ const waitForDrain = (ch) => new Promise((resolve) => {
155
+ const anyCh = ch;
156
+ if (typeof anyCh.once === "function")
157
+ anyCh.once("drain", resolve);
158
+ else
159
+ resolve(); // if not supported, resolve immediately
160
+ });
161
+ const publishWithBackpressure = async (ch, exchange, routingKey, content, options) => {
162
+ const ok = ch.publish(exchange, routingKey, content, options);
163
+ if (!ok) {
164
+ console.warn(`[amqp] publish backpressure: waiting for 'drain' (exchange=${exchange}, key=${routingKey}, size=${content.length})`);
165
+ const t0 = Date.now();
166
+ await waitForDrain(ch);
167
+ const dt = Date.now() - t0;
168
+ if (dt >= 1) {
169
+ console.warn(`[amqp] drain resolved after ${dt}ms (exchange=${exchange}, key=${routingKey})`);
170
+ }
171
+ }
172
+ };
173
+ const getPubChannel = async () => {
174
+ var _a;
175
+ if ((_a = exchangeConfig.publisherConfirms) !== null && _a !== void 0 ? _a : this.defaultCfg.publisherConfirms) {
176
+ return (0, config_1.getRabbitMQConfirmChannel)();
177
+ }
178
+ return this.getChannel();
179
+ };
180
+ const maybeWaitForConfirms = async (ch) => {
181
+ const anyCh = ch;
182
+ if (typeof anyCh.waitForConfirms === "function") {
183
+ await anyCh.waitForConfirms();
184
+ }
185
+ };
186
+ const onMessage = async (msg) => {
187
+ if (!msg)
188
+ return;
189
+ const ch = consumeCh;
190
+ if (!ch)
191
+ return;
192
+ const id = msg.fields.deliveryTag;
193
+ const payload = JSON.parse(msg.content.toString());
194
+ const handler = handlers.get(payload.name) || handlers.get("*");
195
+ let result = null;
196
+ let errored = false;
197
+ try {
198
+ await pluginManager_1.pluginManager.executeHook("beforeProcess", id, payload);
199
+ if (handler) {
200
+ // concurrency is enforced by prefetch limiting in-flight
201
+ result = await handler(id, payload);
202
+ }
203
+ await pluginManager_1.pluginManager.executeHook("afterProcess", id, payload, result);
204
+ }
205
+ catch (err) {
206
+ errored = true;
207
+ console.error("Handler error:", err);
208
+ }
209
+ // RPC reply path (even if handler errored, you might still want a reply)
210
+ if (msg.properties.replyTo) {
211
+ try {
212
+ await publishWithBackpressure(ch, "", msg.properties.replyTo, Buffer.from(JSON.stringify({ reply: errored ? null : result })), { correlationId: msg.properties.correlationId });
213
+ }
214
+ catch (e) {
215
+ console.error("Reply publish failed:", e);
216
+ }
217
+ }
218
+ // Ack/Nack decision
219
+ try {
220
+ if (errored) {
221
+ // derive behavior from onError (Backward compatibility: requeueOnError -> "requeue" handled in consume())
222
+ if (onError === "requeue") {
223
+ ch.nack(msg, false, true); // requeue back to SAME queue
224
+ }
225
+ else if (onError === "dead-letter") {
226
+ ch.nack(msg, false, false); // route to DLX (if queue is DLX-configured)
227
+ }
228
+ else {
229
+ ch.ack(msg); // swallow the error
230
+ }
231
+ }
232
+ else {
233
+ ch.ack(msg);
234
+ }
235
+ }
236
+ catch (e) {
237
+ console.error("Ack/Nack failed:", e);
238
+ }
239
+ };
240
+ const consume = async (opts) => {
241
+ var _a, _b, _c, _d;
242
+ prefetchCount = (_b = (_a = opts === null || opts === void 0 ? void 0 : opts.prefetch) !== null && _a !== void 0 ? _a : opts === null || opts === void 0 ? void 0 : opts.concurrency) !== null && _b !== void 0 ? _b : 1;
243
+ concurrency = (_c = opts === null || opts === void 0 ? void 0 : opts.concurrency) !== null && _c !== void 0 ? _c : prefetchCount;
244
+ // Back-compat: if requeueOnError is set and onError not explicitly provided, use "requeue"
245
+ onError = (_d = opts === null || opts === void 0 ? void 0 : opts.onError) !== null && _d !== void 0 ? _d : ((opts === null || opts === void 0 ? void 0 : opts.requeueOnError) ? "requeue" : "ack");
246
+ const ch = await this.getChannel();
247
+ consumeCh = ch;
248
+ if (prefetchCount > 0)
249
+ await ch.prefetch(prefetchCount, false);
250
+ const ok = await ch.consume(queueName, onMessage);
251
+ consumerTag = ok.consumerTag;
252
+ isConsuming = true;
253
+ return {
254
+ stop: async () => {
255
+ isConsuming = false;
256
+ try {
257
+ const c = consumeCh;
258
+ if (consumerTag && c)
259
+ await c.cancel(consumerTag);
260
+ }
261
+ catch {
262
+ // channel may be closed; ignore
263
+ }
264
+ },
265
+ };
266
+ };
267
+ const safePublish = async (publish) => {
268
+ try {
269
+ const ch = await getPubChannel();
270
+ await publish(ch);
271
+ await maybeWaitForConfirms(ch);
272
+ }
273
+ catch {
274
+ // Broker is likely reconnecting. Briefly wait, then retry once.
275
+ const delay = Math.min(this.backoffMs * 2, 2000);
276
+ await new Promise(r => setTimeout(r, delay));
277
+ // try once more after reconnect
278
+ const ch2 = await getPubChannel();
279
+ await publish(ch2);
280
+ await maybeWaitForConfirms(ch2);
281
+ }
282
+ };
283
+ const produceMany = async (...events) => {
284
+ for (const evt of events) {
285
+ await pluginManager_1.pluginManager.executeHook("beforeProduce", evt);
286
+ await safePublish((ch) => {
287
+ var _a, _b, _c;
288
+ const e = evt;
289
+ const props = {
290
+ messageId: e.id, // idempotency key
291
+ type: e.name, // event name
292
+ timestamp: Math.floor(((_a = e.time) !== null && _a !== void 0 ? _a : Date.now()) / 1000),
293
+ correlationId: (_b = e.meta) === null || _b === void 0 ? void 0 : _b.corrId,
294
+ headers: (_c = e.meta) === null || _c === void 0 ? void 0 : _c.headers,
295
+ };
296
+ return publishWithBackpressure(ch, exchangeName, e.name, Buffer.from(JSON.stringify(e)), props);
297
+ });
298
+ await pluginManager_1.pluginManager.executeHook("afterProduce", evt, null);
299
+ }
300
+ };
301
+ const produce = async (...events) => {
302
+ var _a, _b, _c, _d, _e;
303
+ // Back-compat: upgrade legacy `wait` (if present) to meta fields
304
+ if (events.length === 1 && ((_a = events[0]) === null || _a === void 0 ? void 0 : _a.wait)) {
305
+ const first = events[0];
306
+ const w = first.wait;
307
+ first.meta = first.meta || {};
308
+ if (first.meta.expectsReply !== true)
309
+ first.meta.expectsReply = true;
310
+ if ((w === null || w === void 0 ? void 0 : w.timeout) != null && first.meta.timeoutMs == null)
311
+ first.meta.timeoutMs = w.timeout;
312
+ if (w === null || w === void 0 ? void 0 : w.source) {
313
+ first.meta.headers = { ...(first.meta.headers || {}), source: w.source };
314
+ }
315
+ }
316
+ // RPC request path
317
+ if (events.length === 1 && ((_c = (_b = events[0]) === null || _b === void 0 ? void 0 : _b.meta) === null || _c === void 0 ? void 0 : _c.expectsReply) === true) {
318
+ const evt = events[0];
319
+ const correlationId = generateUuid();
320
+ const rpcCh = await this.getChannel(); // pin for reply consumer/ack
321
+ const temp = await rpcCh.assertQueue("", { exclusive: true, autoDelete: true });
322
+ await pluginManager_1.pluginManager.executeHook("beforeProduce", evt);
323
+ await safePublish(async () => {
324
+ var _a, _b;
325
+ // use (confirm) pub channel for the request publish
326
+ const pubCh = await getPubChannel();
327
+ const props = {
328
+ messageId: evt.id,
329
+ type: evt.name,
330
+ timestamp: Math.floor(((_a = evt.time) !== null && _a !== void 0 ? _a : Date.now()) / 1000),
331
+ correlationId,
332
+ headers: (_b = evt.meta) === null || _b === void 0 ? void 0 : _b.headers,
333
+ replyTo: temp.queue,
334
+ };
335
+ await publishWithBackpressure(pubCh, exchangeName, evt.name, Buffer.from(JSON.stringify(evt)), props);
336
+ });
337
+ const timeoutMs = (_e = (_d = evt.meta) === null || _d === void 0 ? void 0 : _d.timeoutMs) !== null && _e !== void 0 ? _e : 5000;
338
+ return await new Promise((resolve, reject) => {
339
+ let ctag;
340
+ const timer = setTimeout(async () => {
341
+ try {
342
+ if (ctag)
343
+ await rpcCh.cancel(ctag);
344
+ }
345
+ catch { }
346
+ try {
347
+ await rpcCh.deleteQueue(temp.queue);
348
+ }
349
+ catch { }
350
+ reject(new Error("Timeout waiting for reply"));
351
+ }, timeoutMs);
352
+ rpcCh
353
+ .consume(temp.queue, (msg) => {
354
+ if (!msg)
355
+ return;
356
+ if (msg.properties.correlationId !== correlationId)
357
+ return;
358
+ clearTimeout(timer);
359
+ try {
360
+ const reply = JSON.parse(msg.content.toString()).reply;
361
+ pluginManager_1.pluginManager.executeHook("afterProduce", evt, reply);
362
+ resolve(reply);
363
+ }
364
+ finally {
365
+ Promise.resolve()
366
+ .then(async () => {
367
+ try {
368
+ if (ctag)
369
+ await rpcCh.cancel(ctag);
370
+ }
371
+ catch { }
372
+ try {
373
+ await rpcCh.deleteQueue(temp.queue);
374
+ }
375
+ catch { }
376
+ })
377
+ .catch(() => undefined);
378
+ }
379
+ }, { noAck: true })
380
+ .then((ok) => { ctag = ok.consumerTag; })
381
+ .catch((err) => {
382
+ clearTimeout(timer);
383
+ reject(err);
384
+ });
385
+ });
386
+ }
387
+ return produceMany(...events);
388
+ };
389
+ const brokerInterface = {
390
+ handle,
391
+ consume,
392
+ produce,
393
+ produceMany,
394
+ with: (events) => {
395
+ const { augmentEvents } = require("./eventFactories");
396
+ const augmented = augmentEvents(events, brokerInterface);
397
+ return augmented;
398
+ },
399
+ };
400
+ return brokerInterface;
401
+ }
402
+ }
403
+ exports.RabbitMQBroker = RabbitMQBroker;
@@ -0,0 +1,12 @@
1
+ export type KeyOf = (e: any) => string | undefined;
2
+ export interface DedupeOpts {
3
+ ttlMs?: number;
4
+ maxKeys?: number;
5
+ keyOf?: KeyOf;
6
+ }
7
+ export interface Dedupe {
8
+ seen(id: string): boolean;
9
+ checkAndRemember(e: any): boolean;
10
+ size(): number;
11
+ }
12
+ export declare function makeMemoryDedupe(opts?: DedupeOpts): Dedupe;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeMemoryDedupe = makeMemoryDedupe;
4
+ function makeMemoryDedupe(opts = {}) {
5
+ var _a, _b, _c, _d, _e;
6
+ const ttl = Number((_b = (_a = process.env.DEDUPE_TTL_MS) !== null && _a !== void 0 ? _a : opts.ttlMs) !== null && _b !== void 0 ? _b : 10 * 60 * 1000);
7
+ const max = Number((_d = (_c = process.env.DEDUPE_MAX_KEYS) !== null && _c !== void 0 ? _c : opts.maxKeys) !== null && _d !== void 0 ? _d : 100000);
8
+ const keyOf = (_e = opts.keyOf) !== null && _e !== void 0 ? _e : ((e) => {
9
+ var _a, _b, _c, _d, _e, _f, _g;
10
+ return (_f = (_c = (_a = e === null || e === void 0 ? void 0 : e.id) !== null && _a !== void 0 ? _a : (_b = e === null || e === void 0 ? void 0 : e.meta) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : (_e = (_d = e === null || e === void 0 ? void 0 : e.meta) === null || _d === void 0 ? void 0 : _d.headers) === null || _e === void 0 ? void 0 : _e.messageId) !== null && _f !== void 0 ? _f : (_g = e === null || e === void 0 ? void 0 : e.meta) === null || _g === void 0 ? void 0 : _g.corrId;
11
+ });
12
+ const map = new Map();
13
+ function gc(now = Date.now()) {
14
+ // TTL cleanup
15
+ for (const [k, exp] of map) {
16
+ if (exp <= now)
17
+ map.delete(k);
18
+ }
19
+ // Size guard (simple LRU-ish by expiration)
20
+ if (map.size > max) {
21
+ const arr = [...map.entries()].sort((a, b) => a[1] - b[1]); // means oldest first
22
+ const toRemove = map.size - max;
23
+ for (let i = 0; i < toRemove; i++)
24
+ map.delete(arr[i][0]);
25
+ }
26
+ }
27
+ function remember(id) {
28
+ const now = Date.now();
29
+ gc(now);
30
+ map.set(id, now + ttl);
31
+ }
32
+ function seen(id) {
33
+ const exp = map.get(id);
34
+ const now = Date.now();
35
+ if (exp && exp > now)
36
+ return true;
37
+ if (exp)
38
+ map.delete(id);
39
+ return false;
40
+ }
41
+ return {
42
+ seen,
43
+ checkAndRemember(e) {
44
+ const id = typeof e === "string" ? e : keyOf(e);
45
+ if (!id)
46
+ return true; // nothing to de-dupe on -> treat as new one
47
+ if (seen(id))
48
+ return false;
49
+ remember(id);
50
+ return true;
51
+ },
52
+ size() {
53
+ return map.size;
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,5 @@
1
+ import { Channel, ConfirmChannel, ChannelModel } from "amqplib";
2
+ export declare const rabbitMQUrl: string;
3
+ export declare function getRabbitMQConnection(): Promise<ChannelModel>;
4
+ export declare function getRabbitMQChannel(): Promise<Channel>;
5
+ export declare function getRabbitMQConfirmChannel(): Promise<ConfirmChannel>;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ var _a;
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.rabbitMQUrl = void 0;
8
+ exports.getRabbitMQConnection = getRabbitMQConnection;
9
+ exports.getRabbitMQChannel = getRabbitMQChannel;
10
+ exports.getRabbitMQConfirmChannel = getRabbitMQConfirmChannel;
11
+ const amqplib_1 = require("amqplib");
12
+ const node_os_1 = __importDefault(require("node:os"));
13
+ exports.rabbitMQUrl = (_a = process.env.RABBITMQ_URL) !== null && _a !== void 0 ? _a : "amqp://user:password@localhost";
14
+ let conn = null;
15
+ let ch = null;
16
+ let opening = null;
17
+ let cch = null;
18
+ let cOpening = null;
19
+ function attachConnHandlers(c) {
20
+ c.on("blocked", (reason) => console.warn("[amqp] connection blocked:", reason));
21
+ c.on("unblocked", () => console.log("[amqp] connection unblocked"));
22
+ c.on("close", () => {
23
+ console.error("[amqp] connection closed");
24
+ conn = null;
25
+ ch = null;
26
+ opening = null;
27
+ cch = null;
28
+ cOpening = null;
29
+ });
30
+ c.on("error", (err) => {
31
+ console.error("[amqp] connection error:", err);
32
+ // 'close' will clear caches
33
+ });
34
+ }
35
+ function attachChannelHandlers(channel, kind) {
36
+ channel.on("close", () => {
37
+ console.error(`[amqp] ${kind === "confirm" ? "confirm channel" : "channel"} closed`);
38
+ if (kind === "ch") {
39
+ ch = null;
40
+ opening = null;
41
+ }
42
+ else {
43
+ cch = null;
44
+ cOpening = null;
45
+ }
46
+ });
47
+ channel.on("error", (err) => {
48
+ console.error(`[amqp] ${kind === "confirm" ? "confirm channel" : "channel"} error:`, err);
49
+ });
50
+ }
51
+ async function getConn() {
52
+ if (conn)
53
+ return conn;
54
+ const c = await (0, amqplib_1.connect)(exports.rabbitMQUrl, {
55
+ clientProperties: {
56
+ connection_name: process.env.AMQP_CONN_NAME ||
57
+ `app:${process.title || "node"}@${node_os_1.default.hostname()}#${process.pid}`,
58
+ },
59
+ });
60
+ attachConnHandlers(c);
61
+ conn = c;
62
+ return c;
63
+ }
64
+ /** Always return a live channel */
65
+ async function openChannelFresh() {
66
+ if (ch)
67
+ return ch;
68
+ if (opening)
69
+ return opening;
70
+ opening = (async () => {
71
+ try {
72
+ const c = await getConn();
73
+ const channel = await c.createChannel();
74
+ attachChannelHandlers(channel, "ch");
75
+ ch = channel;
76
+ return channel;
77
+ }
78
+ catch (err) {
79
+ opening = null;
80
+ ch = null;
81
+ conn = null;
82
+ throw err;
83
+ }
84
+ })();
85
+ return opening;
86
+ }
87
+ async function openConfirmChannelFresh() {
88
+ if (cch)
89
+ return cch;
90
+ if (cOpening)
91
+ return cOpening;
92
+ cOpening = (async () => {
93
+ try {
94
+ const c = await getConn();
95
+ const channel = await c.createConfirmChannel();
96
+ attachChannelHandlers(channel, "confirm");
97
+ cch = channel;
98
+ return channel;
99
+ }
100
+ catch (err) {
101
+ cOpening = null;
102
+ cch = null;
103
+ conn = null;
104
+ throw err;
105
+ }
106
+ })();
107
+ return cOpening;
108
+ }
109
+ async function getRabbitMQConnection() {
110
+ if (conn)
111
+ return conn;
112
+ await openChannelFresh();
113
+ return conn;
114
+ }
115
+ async function getRabbitMQChannel() {
116
+ return openChannelFresh();
117
+ }
118
+ async function getRabbitMQConfirmChannel() {
119
+ return openConfirmChannelFresh();
120
+ }