@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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +101 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/dependabot.yml +11 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +145 -0
- package/README.md +89 -33
- package/SECURITY.md +8 -0
- package/dist/cjs/backpressure.d.ts +4 -0
- package/dist/cjs/backpressure.js +31 -0
- package/dist/cjs/consumer.d.ts +19 -0
- package/dist/cjs/consumer.js +111 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/publisher.d.ts +13 -0
- package/dist/cjs/publisher.js +141 -0
- package/dist/cjs/rabbitmqBroker.d.ts +2 -50
- package/dist/cjs/rabbitmqBroker.js +31 -345
- package/dist/cjs/reconnect.d.ts +17 -0
- package/dist/cjs/reconnect.js +64 -0
- package/dist/cjs/topology.d.ts +9 -0
- package/dist/cjs/topology.js +58 -0
- package/dist/cjs/types.d.ts +49 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/uuid.d.ts +1 -0
- package/dist/cjs/uuid.js +6 -0
- package/dist/esm/backpressure.d.ts +4 -0
- package/dist/esm/backpressure.js +31 -0
- package/dist/esm/consumer.d.ts +19 -0
- package/dist/esm/consumer.js +111 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/publisher.d.ts +13 -0
- package/dist/esm/publisher.js +141 -0
- package/dist/esm/rabbitmqBroker.d.ts +2 -50
- package/dist/esm/rabbitmqBroker.js +31 -345
- package/dist/esm/reconnect.d.ts +17 -0
- package/dist/esm/reconnect.js +64 -0
- package/dist/esm/topology.d.ts +9 -0
- package/dist/esm/topology.js +58 -0
- package/dist/esm/types.d.ts +49 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/uuid.d.ts +1 -0
- package/dist/esm/uuid.js +6 -0
- package/package.json +1 -1
- /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
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
|
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.
|
|
24
|
+
return this.reconnect.getChannel();
|
|
70
25
|
}
|
|
71
26
|
onReconnect(cb) {
|
|
72
|
-
this.
|
|
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 =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 @@
|
|
|
1
|
+
export declare function generateUuid(): string;
|
package/dist/cjs/uuid.js
ADDED
|
@@ -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>;
|