@bitspacerlabs/rabbit-relay 0.5.2 → 0.6.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.
Files changed (45) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +101 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  3. package/.github/dependabot.yml +11 -0
  4. package/CODE_OF_CONDUCT.md +128 -0
  5. package/CONTRIBUTING.md +145 -0
  6. package/README.md +89 -33
  7. package/SECURITY.md +8 -0
  8. package/dist/cjs/backpressure.d.ts +4 -0
  9. package/dist/cjs/backpressure.js +31 -0
  10. package/dist/cjs/consumer.d.ts +19 -0
  11. package/dist/cjs/consumer.js +111 -0
  12. package/dist/cjs/index.d.ts +1 -0
  13. package/dist/cjs/index.js +1 -0
  14. package/dist/cjs/publisher.d.ts +13 -0
  15. package/dist/cjs/publisher.js +141 -0
  16. package/dist/cjs/rabbitmqBroker.d.ts +2 -50
  17. package/dist/cjs/rabbitmqBroker.js +31 -345
  18. package/dist/cjs/reconnect.d.ts +17 -0
  19. package/dist/cjs/reconnect.js +64 -0
  20. package/dist/cjs/topology.d.ts +9 -0
  21. package/dist/cjs/topology.js +58 -0
  22. package/dist/cjs/types.d.ts +49 -0
  23. package/dist/cjs/types.js +2 -0
  24. package/dist/cjs/uuid.d.ts +1 -0
  25. package/dist/cjs/uuid.js +6 -0
  26. package/dist/esm/backpressure.d.ts +4 -0
  27. package/dist/esm/backpressure.js +31 -0
  28. package/dist/esm/consumer.d.ts +19 -0
  29. package/dist/esm/consumer.js +111 -0
  30. package/dist/esm/index.d.ts +1 -0
  31. package/dist/esm/index.js +1 -0
  32. package/dist/esm/publisher.d.ts +13 -0
  33. package/dist/esm/publisher.js +141 -0
  34. package/dist/esm/rabbitmqBroker.d.ts +2 -50
  35. package/dist/esm/rabbitmqBroker.js +31 -345
  36. package/dist/esm/reconnect.d.ts +17 -0
  37. package/dist/esm/reconnect.js +64 -0
  38. package/dist/esm/topology.d.ts +9 -0
  39. package/dist/esm/topology.js +58 -0
  40. package/dist/esm/types.d.ts +49 -0
  41. package/dist/esm/types.js +2 -0
  42. package/dist/esm/uuid.d.ts +1 -0
  43. package/dist/esm/uuid.js +6 -0
  44. package/package.json +1 -1
  45. /package/assets/{logo.svg → rabbit-relay.svg} +0 -0
@@ -1,20 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
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
- }
4
+ const reconnect_1 = require("./reconnect");
5
+ const topology_1 = require("./topology");
6
+ const consumer_1 = require("./consumer");
7
+ const publisher_1 = require("./publisher");
9
8
  class RabbitMQBroker {
10
9
  constructor(peerName, config = {}) {
11
10
  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
11
  this.peerName = peerName;
19
12
  this.defaultCfg = {
20
13
  exchangeType: (_a = config.exchangeType) !== null && _a !== void 0 ? _a : "topic",
@@ -24,52 +17,14 @@ class RabbitMQBroker {
24
17
  queueArgs: config.queueArgs,
25
18
  passiveQueue: (_e = config.passiveQueue) !== null && _e !== void 0 ? _e : false,
26
19
  };
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
- }
20
+ this.reconnect = new reconnect_1.ReconnectController();
21
+ void this.reconnect.initChannel();
67
22
  }
68
23
  async getChannel() {
69
- return this.channelPromise;
24
+ return this.reconnect.getChannel();
70
25
  }
71
26
  onReconnect(cb) {
72
- this.onReconnectCbs.push(cb);
27
+ this.reconnect.onReconnect(cb);
73
28
  }
74
29
  queue(queueName) {
75
30
  return {
@@ -79,312 +34,42 @@ class RabbitMQBroker {
79
34
  };
80
35
  }
81
36
  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
- };
37
+ const assertTopology = (0, topology_1.createAssertTopology)({
38
+ exchangeName,
39
+ queueName,
40
+ defaultCfg: this.defaultCfg,
41
+ exchangeConfig,
42
+ });
130
43
  const channel = await this.getChannel();
131
44
  await assertTopology(channel);
132
45
  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";
46
+ const consumer = (0, consumer_1.createConsumer)({
47
+ queueName,
48
+ handlers,
49
+ });
50
+ const publisher = (0, publisher_1.createPublisher)({
51
+ exchangeName,
52
+ exchangeConfig,
53
+ defaultCfg: this.defaultCfg,
54
+ getChannel: () => this.getChannel(),
55
+ getBackoffMs: () => this.reconnect.getBackoffMs(),
56
+ });
139
57
  this.onReconnect(async (ch) => {
140
58
  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
- }
59
+ await consumer.resumeOnReconnect(ch);
148
60
  });
149
61
  const handle = (eventName, handler) => {
150
62
  handlers.set(eventName, handler);
151
63
  return brokerInterface;
152
64
  };
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
65
  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
- }
66
+ return consumer.startConsume(() => this.getChannel(), opts);
282
67
  };
283
68
  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
- }
69
+ return publisher.produceMany(...events);
300
70
  };
301
71
  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);
72
+ return publisher.produce(...events);
388
73
  };
389
74
  const brokerInterface = {
390
75
  handle,
@@ -392,7 +77,8 @@ class RabbitMQBroker {
392
77
  produce,
393
78
  produceMany,
394
79
  with: (events) => {
395
- const { augmentEvents } = require("./eventFactories");
80
+ // keep original behavior (dynamic require) to avoid import cycles
81
+ const { augmentEvents } = require("../eventFactories");
396
82
  const augmented = augmentEvents(events, brokerInterface);
397
83
  return augmented;
398
84
  },
@@ -0,0 +1,17 @@
1
+ import { Channel } from "amqplib";
2
+ export declare class ReconnectController {
3
+ /** The current live channel promise (replaced after reconnect). */
4
+ private channelPromise;
5
+ /** Reconnect state */
6
+ private reconnecting;
7
+ private backoffMs;
8
+ private readonly maxBackoffMs;
9
+ /** Callbacks to run after a successful reconnect (like re-assert topology, resume consume). */
10
+ private onReconnectCbs;
11
+ constructor();
12
+ initChannel(): Promise<void>;
13
+ getBackoffMs(): number;
14
+ onReconnect(cb: (ch: Channel) => void | Promise<void>): void;
15
+ getChannel(): Promise<Channel>;
16
+ private scheduleReconnect;
17
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReconnectController = void 0;
4
+ const config_1 = require("./config");
5
+ class ReconnectController {
6
+ constructor() {
7
+ /** Reconnect state */
8
+ this.reconnecting = false;
9
+ this.backoffMs = 500;
10
+ this.maxBackoffMs = 20000;
11
+ /** Callbacks to run after a successful reconnect (like re-assert topology, resume consume). */
12
+ this.onReconnectCbs = [];
13
+ // no-op
14
+ }
15
+ async initChannel() {
16
+ var _a, _b, _c, _d;
17
+ this.channelPromise = (0, config_1.getRabbitMQChannel)();
18
+ const ch = await this.channelPromise;
19
+ this.backoffMs = 500;
20
+ const onClose = () => this.scheduleReconnect("channel.close");
21
+ const onError = () => this.scheduleReconnect("channel.error");
22
+ (_b = (_a = ch).on) === null || _b === void 0 ? void 0 : _b.call(_a, "close", onClose);
23
+ (_d = (_c = ch).on) === null || _d === void 0 ? void 0 : _d.call(_c, "error", onError);
24
+ }
25
+ getBackoffMs() {
26
+ return this.backoffMs;
27
+ }
28
+ onReconnect(cb) {
29
+ this.onReconnectCbs.push(cb);
30
+ }
31
+ async getChannel() {
32
+ return this.channelPromise;
33
+ }
34
+ async scheduleReconnect(reason) {
35
+ if (this.reconnecting)
36
+ return;
37
+ this.reconnecting = true;
38
+ // eslint-disable-next-line no-constant-condition
39
+ while (true) {
40
+ try {
41
+ const jitter = Math.floor(Math.random() * 250);
42
+ await new Promise((r) => setTimeout(r, this.backoffMs + jitter));
43
+ await this.initChannel();
44
+ const ch = await this.channelPromise;
45
+ this.backoffMs = 500;
46
+ this.reconnecting = false;
47
+ for (const cb of this.onReconnectCbs) {
48
+ try {
49
+ await cb(ch);
50
+ }
51
+ catch (e) {
52
+ console.error("[broker] onReconnect callback failed:", e);
53
+ }
54
+ }
55
+ return;
56
+ }
57
+ catch {
58
+ this.backoffMs = Math.min(this.maxBackoffMs, Math.floor(this.backoffMs * 1.7 + Math.random() * 100));
59
+ console.error(`[broker] reconnect failed (${reason}), retrying in ~${this.backoffMs}ms`);
60
+ }
61
+ }
62
+ }
63
+ }
64
+ exports.ReconnectController = ReconnectController;
@@ -0,0 +1,9 @@
1
+ import { Channel } from "amqplib";
2
+ import { ExchangeConfig, InternalCfg } from "./types";
3
+ export declare function mergeInternalCfg(defaultCfg: InternalCfg, exchangeConfig: ExchangeConfig): InternalCfg;
4
+ export declare function createAssertTopology(params: {
5
+ exchangeName: string;
6
+ queueName: string;
7
+ defaultCfg: InternalCfg;
8
+ exchangeConfig: ExchangeConfig;
9
+ }): (channel: Channel) => Promise<void>;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeInternalCfg = mergeInternalCfg;
4
+ exports.createAssertTopology = createAssertTopology;
5
+ function mergeInternalCfg(defaultCfg, exchangeConfig) {
6
+ var _a, _b, _c, _d, _e, _f;
7
+ return {
8
+ exchangeType: (_a = exchangeConfig.exchangeType) !== null && _a !== void 0 ? _a : defaultCfg.exchangeType,
9
+ routingKey: (_b = exchangeConfig.routingKey) !== null && _b !== void 0 ? _b : defaultCfg.routingKey,
10
+ durable: (_c = exchangeConfig.durable) !== null && _c !== void 0 ? _c : defaultCfg.durable,
11
+ publisherConfirms: (_d = exchangeConfig.publisherConfirms) !== null && _d !== void 0 ? _d : defaultCfg.publisherConfirms,
12
+ queueArgs: (_e = exchangeConfig.queueArgs) !== null && _e !== void 0 ? _e : defaultCfg.queueArgs,
13
+ passiveQueue: (_f = exchangeConfig.passiveQueue) !== null && _f !== void 0 ? _f : defaultCfg.passiveQueue,
14
+ };
15
+ }
16
+ function createAssertTopology(params) {
17
+ const { exchangeName, queueName, defaultCfg, exchangeConfig } = params;
18
+ return async function assertTopology(channel) {
19
+ const cfg = mergeInternalCfg(defaultCfg, exchangeConfig);
20
+ await channel.assertExchange(exchangeName, cfg.exchangeType, { durable: cfg.durable });
21
+ if (cfg.passiveQueue) {
22
+ if (cfg.queueArgs) {
23
+ console.warn(`[broker] passiveQueue=true: ignoring queueArgs for '${queueName}' (not declaring).`);
24
+ }
25
+ try {
26
+ await channel.checkQueue(queueName);
27
+ }
28
+ catch (err) {
29
+ const code = err === null || err === void 0 ? void 0 : err.code;
30
+ if (code === 404) {
31
+ throw new Error(`[broker] passiveQueue check failed: queue '${queueName}' does not exist. ` +
32
+ `Either create it in your setup step with the desired arguments, ` +
33
+ `or call with passiveQueue:false and queueArgs to auto-declare.`);
34
+ }
35
+ throw err;
36
+ }
37
+ }
38
+ else {
39
+ try {
40
+ const qOpts = {
41
+ durable: cfg.durable,
42
+ ...(cfg.queueArgs ? { arguments: cfg.queueArgs } : {}),
43
+ };
44
+ await channel.assertQueue(queueName, qOpts);
45
+ }
46
+ catch (err) {
47
+ if ((err === null || err === void 0 ? void 0 : err.code) === 406) {
48
+ throw new Error(`[broker] QueueDeclare PRECONDITION_FAILED for '${queueName}'. ` +
49
+ `Existing queue has different arguments. ` +
50
+ `Fix: delete the queue or switch to { passiveQueue: true } if you're using a setup step.`);
51
+ }
52
+ throw err;
53
+ }
54
+ }
55
+ // (Re)bind is idempotent - safe to call even if binding already exists
56
+ await channel.bindQueue(queueName, exchangeName, cfg.routingKey);
57
+ };
58
+ }
@@ -0,0 +1,49 @@
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 type InternalCfg = {
43
+ exchangeType: "topic" | "direct" | "fanout";
44
+ routingKey: string;
45
+ durable: boolean;
46
+ publisherConfirms: boolean;
47
+ queueArgs?: Options.AssertQueue["arguments"];
48
+ passiveQueue: boolean;
49
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export declare function generateUuid(): string;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateUuid = generateUuid;
4
+ function generateUuid() {
5
+ return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
6
+ }
@@ -0,0 +1,4 @@
1
+ import { Channel, Options } from "amqplib";
2
+ export declare const waitForDrain: (ch: Channel) => Promise<void>;
3
+ export declare const publishWithBackpressure: (ch: Channel, exchange: string, routingKey: string, content: Buffer, options?: Options.Publish) => Promise<void>;
4
+ export declare const maybeWaitForConfirms: (ch: Channel) => Promise<void>;