@znight/messaging 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +622 -2
- package/dist/async-CFh_bvex.mjs +216 -0
- package/dist/async-CFh_bvex.mjs.map +1 -0
- package/dist/codec-Dku-NXXU.mjs +83 -0
- package/dist/codec-Dku-NXXU.mjs.map +1 -0
- package/dist/compiler.d.mts +75 -0
- package/dist/compiler.mjs +1023 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/driver-ZxLqy_Ap.d.mts +216 -0
- package/dist/index.d.mts +459 -0
- package/dist/index.mjs +870 -0
- package/dist/index.mjs.map +1 -0
- package/dist/kafka.d.mts +51 -0
- package/dist/kafka.mjs +517 -0
- package/dist/kafka.mjs.map +1 -0
- package/dist/memory.d.mts +124 -0
- package/dist/memory.mjs +429 -0
- package/dist/memory.mjs.map +1 -0
- package/dist/rabbitmq.d.mts +65 -0
- package/dist/rabbitmq.mjs +686 -0
- package/dist/rabbitmq.mjs.map +1 -0
- package/dist/shared-BJBpLS8V.mjs +52 -0
- package/dist/shared-BJBpLS8V.mjs.map +1 -0
- package/dist/sqs.d.mts +38 -0
- package/dist/sqs.mjs +421 -0
- package/dist/sqs.mjs.map +1 -0
- package/package.json +123 -4
- package/src/broker.ts +156 -0
- package/src/codec.ts +98 -0
- package/src/compiler/analyze.ts +800 -0
- package/src/compiler/constants.ts +73 -0
- package/src/compiler/data.ts +75 -0
- package/src/compiler/generate.ts +131 -0
- package/src/compiler/index.ts +52 -0
- package/src/compiler/patterns.ts +54 -0
- package/src/compiler/retry.ts +136 -0
- package/src/compiler/validate.ts +57 -0
- package/src/compiler/values.ts +76 -0
- package/src/consumers.ts +260 -0
- package/src/decorators.ts +131 -0
- package/src/driver.ts +142 -0
- package/src/drivers/kafka.ts +765 -0
- package/src/drivers/memory.ts +609 -0
- package/src/drivers/rabbitmq.ts +944 -0
- package/src/drivers/shared.ts +81 -0
- package/src/drivers/sqs.ts +583 -0
- package/src/errors.ts +95 -0
- package/src/headers.ts +70 -0
- package/src/hooks.ts +2 -0
- package/src/index.ts +89 -0
- package/src/internal/async.ts +115 -0
- package/src/message.ts +96 -0
- package/src/metadata.ts +65 -0
- package/src/plugin.ts +236 -0
- package/src/retry.ts +129 -0
- package/src/runtime.ts +424 -0
- package/src/standard-schema.ts +68 -0
- package/src/tokens.ts +14 -0
- package/src/validation.ts +61 -0
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
import { _ as consumerError, a as sleep, c as attemptFromHeaders, g as brokerUnavailableError, o as truncate, r as errorMessage, s as MessagingHeaders } from "./async-CFh_bvex.mjs";
|
|
2
|
+
import { a as toHeaderRecord, i as readHeader, r as durationOption, t as MAX_ERROR_HEADER_LENGTH } from "./shared-BJBpLS8V.mjs";
|
|
3
|
+
import { connect } from "amqplib";
|
|
4
|
+
// src/drivers/rabbitmq.ts
|
|
5
|
+
/** Largest TTL RabbitMQ accepts (2^32 - 1 ms). */
|
|
6
|
+
const MAX_TTL_MS = 4294967295;
|
|
7
|
+
const DEFAULT_IN_PROCESS_DELAY_MS = 5e3;
|
|
8
|
+
const DEFAULT_DELAY_LEVEL_PREFIX = "znight.delays";
|
|
9
|
+
/** Delay levels of delayed publications: level n waits 2^n ms, all of them together `MAX_TTL_MS`. */
|
|
10
|
+
const DELAY_LEVELS = 32;
|
|
11
|
+
/** First word of the routing key of a delayed publication, followed by one bit per level (highest first). */
|
|
12
|
+
const DELAY_ROUTING_WORD = "znight-delay";
|
|
13
|
+
/**
|
|
14
|
+
* Headers never copied to republished messages (retries, dead letters): the ones managed by the broker,
|
|
15
|
+
* and the sender-selected destinations (`CC`, `BCC`), which would route the copy to other queues.
|
|
16
|
+
*/
|
|
17
|
+
const UNCOPIED_HEADERS = /* @__PURE__ */ new Set([
|
|
18
|
+
"x-delivery-count",
|
|
19
|
+
"x-death",
|
|
20
|
+
"x-first-death-exchange",
|
|
21
|
+
"x-first-death-queue",
|
|
22
|
+
"x-first-death-reason",
|
|
23
|
+
"x-last-death-exchange",
|
|
24
|
+
"x-last-death-queue",
|
|
25
|
+
"x-last-death-reason",
|
|
26
|
+
"CC",
|
|
27
|
+
"BCC"
|
|
28
|
+
]);
|
|
29
|
+
/** Creates a {@link RabbitMQDriver}. */
|
|
30
|
+
function rabbitmq(options) {
|
|
31
|
+
return new RabbitMQDriver(options);
|
|
32
|
+
}
|
|
33
|
+
/** RabbitMQ {@link MessagingDriver}. See the module documentation. */
|
|
34
|
+
var RabbitMQDriver = class {
|
|
35
|
+
name = "rabbitmq";
|
|
36
|
+
capabilities;
|
|
37
|
+
#options;
|
|
38
|
+
#queueType;
|
|
39
|
+
#assertTopology;
|
|
40
|
+
#subscriptions = /* @__PURE__ */ new Set();
|
|
41
|
+
/** Retry delay queues already declared, by name. */
|
|
42
|
+
#delayQueues = /* @__PURE__ */ new Map();
|
|
43
|
+
/** Publications awaiting their confirmation, by destination and message id (to detect returned messages). */
|
|
44
|
+
#pending = /* @__PURE__ */ new Map();
|
|
45
|
+
#abort = new AbortController();
|
|
46
|
+
#logger = {
|
|
47
|
+
debug() {},
|
|
48
|
+
info() {},
|
|
49
|
+
warn() {},
|
|
50
|
+
error() {}
|
|
51
|
+
};
|
|
52
|
+
#model;
|
|
53
|
+
#publisher;
|
|
54
|
+
/** Declaration of the delay levels on the current connection. */
|
|
55
|
+
#delayLevels;
|
|
56
|
+
#closing = false;
|
|
57
|
+
/** Running recovery loop (reconnection and resubscriptions). */
|
|
58
|
+
#recovering;
|
|
59
|
+
constructor(options) {
|
|
60
|
+
if (options === null || typeof options !== "object" || options.url === void 0) throw new TypeError("[znight] rabbitmq(): the \"url\" option is required");
|
|
61
|
+
if (options.delayLevelPrefix !== void 0 && (typeof options.delayLevelPrefix !== "string" || options.delayLevelPrefix === "")) throw new TypeError("[znight] rabbitmq(): \"delayLevelPrefix\" must be a non-empty string");
|
|
62
|
+
this.#options = options;
|
|
63
|
+
this.#queueType = options.queueType ?? "quorum";
|
|
64
|
+
this.#assertTopology = options.assertTopology ?? true;
|
|
65
|
+
this.capabilities = Object.freeze({
|
|
66
|
+
nativeDelay: true,
|
|
67
|
+
maxNativeDelayMs: MAX_TTL_MS,
|
|
68
|
+
maxInProcessDelayMs: durationOption(options.maxInProcessDelayMs, DEFAULT_IN_PROCESS_DELAY_MS, "rabbitmq maxInProcessDelayMs"),
|
|
69
|
+
deliveryCount: this.#queueType === "quorum" ? "native" : "header",
|
|
70
|
+
maxPublishDelayMs: MAX_TTL_MS
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async connect(context) {
|
|
74
|
+
this.#logger = context.logger;
|
|
75
|
+
if (this.#model !== void 0) return;
|
|
76
|
+
if (this.#closing) throw brokerUnavailableError("RabbitMQ: the driver is closed, create a new one");
|
|
77
|
+
await this.#open();
|
|
78
|
+
}
|
|
79
|
+
async close() {
|
|
80
|
+
if (this.#closing) return;
|
|
81
|
+
this.#closing = true;
|
|
82
|
+
this.#abort.abort();
|
|
83
|
+
for (const subscription of this.#subscriptions) {
|
|
84
|
+
subscription.active = false;
|
|
85
|
+
const channel = subscription.channel;
|
|
86
|
+
subscription.channel = void 0;
|
|
87
|
+
if (channel !== void 0) await closeQuietly(channel);
|
|
88
|
+
}
|
|
89
|
+
this.#subscriptions.clear();
|
|
90
|
+
const publisher = this.#publisher;
|
|
91
|
+
this.#publisher = void 0;
|
|
92
|
+
if (publisher !== void 0) try {
|
|
93
|
+
await closeQuietly(await publisher);
|
|
94
|
+
} catch {}
|
|
95
|
+
const model = this.#model;
|
|
96
|
+
this.#model = void 0;
|
|
97
|
+
if (model !== void 0) try {
|
|
98
|
+
await model.close();
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
deadLetterName(queue) {
|
|
102
|
+
return this.#options.deadLetterName?.(queue) ?? `${queue}.dlq`;
|
|
103
|
+
}
|
|
104
|
+
async #open() {
|
|
105
|
+
const socketOptions = {
|
|
106
|
+
...this.#options.socketOptions,
|
|
107
|
+
clientProperties: {
|
|
108
|
+
connection_name: this.#options.connectionName ?? "znight",
|
|
109
|
+
...this.#options.socketOptions?.clientProperties
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const model = await connect(this.#options.url, socketOptions);
|
|
113
|
+
model.on("error", (error) => {
|
|
114
|
+
this.#logger.error("RabbitMQ connection error", { err: error });
|
|
115
|
+
});
|
|
116
|
+
model.on("close", (error) => this.#onConnectionClosed(model, error));
|
|
117
|
+
model.on("blocked", (reason) => {
|
|
118
|
+
this.#logger.warn(`RabbitMQ connection blocked: ${reason}`);
|
|
119
|
+
});
|
|
120
|
+
model.on("unblocked", () => {
|
|
121
|
+
this.#logger.info("RabbitMQ connection unblocked");
|
|
122
|
+
});
|
|
123
|
+
this.#model = model;
|
|
124
|
+
this.#publisher = void 0;
|
|
125
|
+
this.#delayQueues.clear();
|
|
126
|
+
this.#delayLevels = void 0;
|
|
127
|
+
}
|
|
128
|
+
#onConnectionClosed(model, error) {
|
|
129
|
+
if (this.#model !== model) return;
|
|
130
|
+
this.#model = void 0;
|
|
131
|
+
this.#publisher = void 0;
|
|
132
|
+
for (const subscription of this.#subscriptions) {
|
|
133
|
+
subscription.channel = void 0;
|
|
134
|
+
subscription.consumerTag = void 0;
|
|
135
|
+
}
|
|
136
|
+
if (this.#closing) return;
|
|
137
|
+
const reconnect = this.#options.reconnect;
|
|
138
|
+
this.#logger.error(reconnect === false ? "RabbitMQ connection lost" : "RabbitMQ connection lost, reconnecting", error === void 0 ? {} : { err: error });
|
|
139
|
+
if (reconnect !== false) this.#recover();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Starts the recovery loop unless it is running. The loop re-reads the whole state (connection,
|
|
143
|
+
* subscriptions without channel) before it ends, so a failure happening while it runs is never lost.
|
|
144
|
+
*/
|
|
145
|
+
#recover() {
|
|
146
|
+
if (this.#closing) return;
|
|
147
|
+
this.#recovering ??= this.#recoverLoop();
|
|
148
|
+
}
|
|
149
|
+
/** Reconnects if needed, then resumes every subscription, with backoff, until everything consumes. */
|
|
150
|
+
async #recoverLoop() {
|
|
151
|
+
const reconnect = this.#options.reconnect;
|
|
152
|
+
const settings = reconnect === false ? {} : reconnect ?? {};
|
|
153
|
+
const initialDelay = settings.initialDelayMs ?? 100;
|
|
154
|
+
const maxDelay = settings.maxDelayMs ?? 3e4;
|
|
155
|
+
let delay = initialDelay;
|
|
156
|
+
let reconnected = false;
|
|
157
|
+
try {
|
|
158
|
+
while (!this.#closing) {
|
|
159
|
+
try {
|
|
160
|
+
await sleep(delay, this.#abort.signal);
|
|
161
|
+
} catch {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (this.#model === void 0) {
|
|
165
|
+
if (reconnect === false) return;
|
|
166
|
+
try {
|
|
167
|
+
await this.#open();
|
|
168
|
+
reconnected = true;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
delay = Math.min(delay * 2, maxDelay);
|
|
171
|
+
this.#logger.warn(`RabbitMQ reconnection failed, next attempt in ${delay} ms`, { err: error });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const model = this.#model;
|
|
176
|
+
for (const subscription of this.#subscriptions) {
|
|
177
|
+
if (!subscription.active || subscription.channel !== void 0) continue;
|
|
178
|
+
try {
|
|
179
|
+
if (subscription.redeclare) await this.#redeclare(subscription);
|
|
180
|
+
await this.#ensureConsuming(subscription);
|
|
181
|
+
if (!reconnected) this.#logger.info(`RabbitMQ: consuming "${subscription.queue}" again`);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
this.#logger.error(`RabbitMQ: cannot consume "${subscription.queue}" again`, { err: error });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (this.#closing) return;
|
|
187
|
+
if (this.#model !== model) {
|
|
188
|
+
delay = Math.min(delay * 2, maxDelay);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const waiting = [...this.#subscriptions].filter((subscription) => subscription.active && subscription.channel === void 0);
|
|
192
|
+
if (waiting.length > 0) {
|
|
193
|
+
delay = Math.min(delay * 2, maxDelay);
|
|
194
|
+
this.#logger.warn(`RabbitMQ: ${waiting.map((subscription) => `"${subscription.queue}"`).join(", ")} not consumed yet, next attempt in ${delay} ms`);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (reconnected) this.#logger.info("RabbitMQ connection restored");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
this.#recovering = void 0;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
#connection(action) {
|
|
205
|
+
const model = this.#model;
|
|
206
|
+
if (model === void 0) throw brokerUnavailableError(this.#closing ? `RabbitMQ: cannot ${action}, the driver is closed` : `RabbitMQ: cannot ${action}, the connection is not available`);
|
|
207
|
+
return model;
|
|
208
|
+
}
|
|
209
|
+
/** Runs `fn` on a temporary channel (failed declarations close the channel they run on). */
|
|
210
|
+
async #withChannel(action, fn) {
|
|
211
|
+
const channel = await this.#connection(action).createChannel();
|
|
212
|
+
channel.on("error", () => {});
|
|
213
|
+
try {
|
|
214
|
+
return await fn(channel);
|
|
215
|
+
} finally {
|
|
216
|
+
await closeQuietly(channel);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async publish(queue, message) {
|
|
220
|
+
const headers = { ...message.headers };
|
|
221
|
+
const properties = {
|
|
222
|
+
messageId: message.id,
|
|
223
|
+
contentType: message.contentType,
|
|
224
|
+
timestamp: Math.floor(message.timestamp / 1e3),
|
|
225
|
+
headers,
|
|
226
|
+
persistent: this.#options.persistent ?? true,
|
|
227
|
+
...message.type === void 0 ? {} : { type: message.type }
|
|
228
|
+
};
|
|
229
|
+
const content = Buffer.from(message.body);
|
|
230
|
+
if (message.delayMs > 0) {
|
|
231
|
+
await this.#publishDelayed(queue, content, properties, message.delayMs);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
await this.#confirmPublish("", queue, content, properties, queue);
|
|
235
|
+
}
|
|
236
|
+
#publisherChannel() {
|
|
237
|
+
const existing = this.#publisher;
|
|
238
|
+
if (existing !== void 0) return existing;
|
|
239
|
+
const opening = this.#connection("publish").createConfirmChannel().then((channel) => {
|
|
240
|
+
channel.on("error", (error) => {
|
|
241
|
+
this.#logger.error("RabbitMQ publisher channel error", { err: error });
|
|
242
|
+
});
|
|
243
|
+
channel.on("close", () => {
|
|
244
|
+
if (this.#publisher === opening) this.#publisher = void 0;
|
|
245
|
+
});
|
|
246
|
+
channel.on("return", (returned) => {
|
|
247
|
+
const key = pendingKey(returned.fields.exchange, returned.fields.routingKey, returned.properties.messageId);
|
|
248
|
+
const pending = this.#pending.get(key)?.find((entry) => !entry.returned);
|
|
249
|
+
if (pending !== void 0) pending.returned = true;
|
|
250
|
+
});
|
|
251
|
+
return channel;
|
|
252
|
+
});
|
|
253
|
+
opening.catch(() => {
|
|
254
|
+
if (this.#publisher === opening) this.#publisher = void 0;
|
|
255
|
+
});
|
|
256
|
+
this.#publisher = opening;
|
|
257
|
+
return opening;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Publishes to `exchange` with `routingKey` and waits for the broker confirmation. `destination`
|
|
261
|
+
* names the target queue in errors.
|
|
262
|
+
*/
|
|
263
|
+
async #confirmPublish(exchange, routingKey, content, properties, destination) {
|
|
264
|
+
const channel = await this.#publisherChannel();
|
|
265
|
+
const mandatory = this.#options.mandatory ?? true;
|
|
266
|
+
const id = properties.messageId;
|
|
267
|
+
const key = pendingKey(exchange, routingKey, id);
|
|
268
|
+
const pending = { returned: false };
|
|
269
|
+
let waiting = this.#pending.get(key);
|
|
270
|
+
if (waiting === void 0) {
|
|
271
|
+
waiting = [];
|
|
272
|
+
this.#pending.set(key, waiting);
|
|
273
|
+
}
|
|
274
|
+
waiting.push(pending);
|
|
275
|
+
try {
|
|
276
|
+
await new Promise((resolve, reject) => {
|
|
277
|
+
try {
|
|
278
|
+
channel.publish(exchange, routingKey, content, {
|
|
279
|
+
...properties,
|
|
280
|
+
mandatory
|
|
281
|
+
}, (error) => {
|
|
282
|
+
if (error !== null && error !== void 0) reject(error instanceof Error ? error : new Error(String(error)));
|
|
283
|
+
else resolve();
|
|
284
|
+
});
|
|
285
|
+
} catch (error) {
|
|
286
|
+
reject(error);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
} catch (error) {
|
|
290
|
+
throw brokerUnavailableError(`RabbitMQ: publishing to "${destination}" was not confirmed: ${errorMessage(error)}`, error);
|
|
291
|
+
} finally {
|
|
292
|
+
const index = waiting.indexOf(pending);
|
|
293
|
+
if (index !== -1) waiting.splice(index, 1);
|
|
294
|
+
if (waiting.length === 0 && this.#pending.get(key) === waiting) this.#pending.delete(key);
|
|
295
|
+
}
|
|
296
|
+
if (pending.returned) throw brokerUnavailableError(exchange === "" ? `RabbitMQ: message ${id} is unroutable, queue "${destination}" does not exist` : `RabbitMQ: message ${id} to "${destination}" is unroutable through the exchange "${exchange}"`);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Publishes through the delay levels: the delay, rounded up to an odd number of milliseconds (so
|
|
300
|
+
* that every route ends with level 0, whose queue dead-letters to the default exchange), is written
|
|
301
|
+
* in binary in the routing key; the `CC` header routes the message to `queue` at the end.
|
|
302
|
+
*/
|
|
303
|
+
async #publishDelayed(queue, content, properties, delayMs) {
|
|
304
|
+
const rounded = Math.min(MAX_TTL_MS, Math.ceil(delayMs));
|
|
305
|
+
const delay = rounded % 2 === 0 ? rounded + 1 : rounded;
|
|
306
|
+
await this.#declareDelayLevels();
|
|
307
|
+
let top = 0;
|
|
308
|
+
while (top < 31 && 2 ** (top + 1) <= delay) top++;
|
|
309
|
+
try {
|
|
310
|
+
await this.#confirmPublish(this.#delayLevelName(top), delayRoutingKey(delay), content, {
|
|
311
|
+
...properties,
|
|
312
|
+
headers: {
|
|
313
|
+
...properties.headers,
|
|
314
|
+
CC: [queue]
|
|
315
|
+
}
|
|
316
|
+
}, queue);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
this.#delayLevels = void 0;
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
#delayLevelName(level) {
|
|
323
|
+
return `${this.#options.delayLevelPrefix ?? DEFAULT_DELAY_LEVEL_PREFIX}.${level}`;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Declares (once per connection) the delay levels: for each level n, a topic exchange routing the
|
|
327
|
+
* messages whose bit n is set to the queue of the level (TTL 2^n ms, dead-lettering to the level
|
|
328
|
+
* below), and the others straight to the level below.
|
|
329
|
+
*/
|
|
330
|
+
async #declareDelayLevels() {
|
|
331
|
+
let declared = this.#delayLevels;
|
|
332
|
+
if (declared === void 0) {
|
|
333
|
+
const levels = this.#withChannel("declare the delay levels", async (channel) => {
|
|
334
|
+
for (let level = 0; level < DELAY_LEVELS; level++) {
|
|
335
|
+
const name = this.#delayLevelName(level);
|
|
336
|
+
const below = level === 0 ? "" : this.#delayLevelName(level - 1);
|
|
337
|
+
await channel.assertExchange(name, "topic", { durable: true });
|
|
338
|
+
await channel.assertQueue(name, {
|
|
339
|
+
durable: true,
|
|
340
|
+
arguments: this.#delayQueueArguments(2 ** level, below)
|
|
341
|
+
});
|
|
342
|
+
await channel.bindQueue(name, name, delayBindingKey(level, "1"));
|
|
343
|
+
if (level > 0) await channel.bindExchange(below, name, delayBindingKey(level, "0"));
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
declared = levels;
|
|
347
|
+
this.#delayLevels = levels;
|
|
348
|
+
levels.catch(() => {
|
|
349
|
+
if (this.#delayLevels === levels) this.#delayLevels = void 0;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
await declared;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
const message = errorMessage(error);
|
|
356
|
+
throw consumerError(`RabbitMQ: cannot declare the delay levels "${this.#delayLevelName(0)}" to "${this.#delayLevelName(31)}": ${message}`, message.includes("PRECONDITION_FAILED") ? ["They exist with other arguments (another queueType?): set rabbitmq({ delayLevelPrefix }) to use other names"] : []);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/** Arguments of a delay queue: TTL, then dead-lettering (at-least-once on quorum queues). */
|
|
360
|
+
#delayQueueArguments(ttl, exchange, routingKey) {
|
|
361
|
+
const args = {
|
|
362
|
+
"x-queue-type": this.#queueType,
|
|
363
|
+
"x-message-ttl": ttl,
|
|
364
|
+
"x-dead-letter-exchange": exchange
|
|
365
|
+
};
|
|
366
|
+
if (routingKey !== void 0) args["x-dead-letter-routing-key"] = routingKey;
|
|
367
|
+
if (this.#queueType === "quorum") {
|
|
368
|
+
args["x-dead-letter-strategy"] = "at-least-once";
|
|
369
|
+
args["x-overflow"] = "reject-publish";
|
|
370
|
+
}
|
|
371
|
+
return args;
|
|
372
|
+
}
|
|
373
|
+
/** Declares (once per connection) the retry delay queue of `queue` for `delayMs` and returns its name. */
|
|
374
|
+
async #delayQueue(queue, delayMs) {
|
|
375
|
+
const name = this.#options.delayQueueName?.(queue, delayMs) ?? `${queue}.delay.${delayMs}`;
|
|
376
|
+
let declared = this.#delayQueues.get(name);
|
|
377
|
+
if (declared === void 0) {
|
|
378
|
+
const args = this.#delayQueueArguments(delayMs, "", queue);
|
|
379
|
+
declared = this.#withChannel(`declare the delay queue "${name}"`, async (channel) => {
|
|
380
|
+
await channel.assertQueue(name, {
|
|
381
|
+
durable: true,
|
|
382
|
+
arguments: args
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
this.#delayQueues.set(name, declared);
|
|
386
|
+
declared.catch(() => this.#delayQueues.delete(name));
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
await declared;
|
|
390
|
+
} catch (error) {
|
|
391
|
+
throw topologyError(name, error);
|
|
392
|
+
}
|
|
393
|
+
return name;
|
|
394
|
+
}
|
|
395
|
+
async subscribe(queue, handler, options) {
|
|
396
|
+
await this.#prepareTopology(queue, options);
|
|
397
|
+
const subscription = {
|
|
398
|
+
queue,
|
|
399
|
+
handler,
|
|
400
|
+
options,
|
|
401
|
+
channel: void 0,
|
|
402
|
+
consumerTag: void 0,
|
|
403
|
+
active: true,
|
|
404
|
+
consuming: void 0,
|
|
405
|
+
redeclare: false
|
|
406
|
+
};
|
|
407
|
+
this.#subscriptions.add(subscription);
|
|
408
|
+
try {
|
|
409
|
+
await this.#ensureConsuming(subscription);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
subscription.active = false;
|
|
412
|
+
this.#subscriptions.delete(subscription);
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
cancel: async () => {
|
|
417
|
+
subscription.active = false;
|
|
418
|
+
const { channel, consumerTag } = subscription;
|
|
419
|
+
if (channel !== void 0 && consumerTag !== void 0) {
|
|
420
|
+
subscription.consumerTag = void 0;
|
|
421
|
+
try {
|
|
422
|
+
await channel.cancel(consumerTag);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
this.#logger.warn(`RabbitMQ: failed to cancel the consumer of "${queue}"`, { err: error });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
close: async () => {
|
|
429
|
+
subscription.active = false;
|
|
430
|
+
this.#subscriptions.delete(subscription);
|
|
431
|
+
const channel = subscription.channel;
|
|
432
|
+
subscription.channel = void 0;
|
|
433
|
+
if (channel !== void 0) await closeQuietly(channel);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
async #prepareTopology(queue, options) {
|
|
438
|
+
const dlq = options.deadLetterQueue;
|
|
439
|
+
const names = dlq === void 0 ? [queue] : [queue, dlq];
|
|
440
|
+
if (this.#assertTopology) for (const name of names) try {
|
|
441
|
+
await this.#declareQueue(name);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
throw topologyError(name, error);
|
|
444
|
+
}
|
|
445
|
+
else for (const name of names) try {
|
|
446
|
+
await this.#withChannel(`check the queue "${name}"`, (channel) => channel.checkQueue(name));
|
|
447
|
+
} catch (error) {
|
|
448
|
+
throw consumerError(`RabbitMQ: queue "${name}" of consumer ${options.consumer} does not exist (${errorMessage(error)})`, ["Declare it, or set assertTopology: true to let Znight declare it"]);
|
|
449
|
+
}
|
|
450
|
+
for (const delay of options.retryDelays) await this.#delayQueue(queue, Math.round(delay));
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Declares a consumed or dead-letter queue. Quorum queues get `x-delivery-limit: -1` on RabbitMQ >= 4
|
|
454
|
+
* (whose default limit of 20 deliveries counts the releases); an existing queue declared without it
|
|
455
|
+
* cannot be changed by a declaration: it is declared as it is, with a warning.
|
|
456
|
+
*/
|
|
457
|
+
async #declareQueue(name) {
|
|
458
|
+
const args = {
|
|
459
|
+
"x-queue-type": this.#queueType,
|
|
460
|
+
...this.#options.queueArguments
|
|
461
|
+
};
|
|
462
|
+
const declare = (queueArgs) => this.#withChannel(`declare the queue "${name}"`, async (channel) => {
|
|
463
|
+
await channel.assertQueue(name, {
|
|
464
|
+
durable: true,
|
|
465
|
+
arguments: queueArgs
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
if (args["x-queue-type"] === "quorum" && !Object.hasOwn(args, "x-delivery-limit") && this.#serverMajor() >= 4) try {
|
|
469
|
+
await declare({
|
|
470
|
+
...args,
|
|
471
|
+
"x-delivery-limit": -1
|
|
472
|
+
});
|
|
473
|
+
return;
|
|
474
|
+
} catch (error) {
|
|
475
|
+
if (!errorMessage(error).includes("'x-delivery-limit'")) throw error;
|
|
476
|
+
this.#logger.warn(`RabbitMQ: queue "${name}" exists without "x-delivery-limit": unless a policy sets "delivery-limit" to -1, RabbitMQ drops (or dead-letters through a queue DLX) a message delivered 20 times, releases included (shutdowns, failed dead-lettering). Apply such a policy, or re-create the queue`);
|
|
477
|
+
}
|
|
478
|
+
await declare(args);
|
|
479
|
+
}
|
|
480
|
+
/** Major version of the connected RabbitMQ (0 when unknown). */
|
|
481
|
+
#serverMajor() {
|
|
482
|
+
const version = this.#model?.connection?.serverProperties?.version;
|
|
483
|
+
const major = typeof version === "string" ? Number.parseInt(version, 10) : NaN;
|
|
484
|
+
return Number.isNaN(major) ? 0 : major;
|
|
485
|
+
}
|
|
486
|
+
/** After a consumer cancellation, declares its queues again (`assertTopology`) before consuming. */
|
|
487
|
+
async #redeclare(subscription) {
|
|
488
|
+
if (this.#assertTopology) {
|
|
489
|
+
const dlq = subscription.options.deadLetterQueue;
|
|
490
|
+
for (const name of dlq === void 0 ? [subscription.queue] : [subscription.queue, dlq]) try {
|
|
491
|
+
await this.#declareQueue(name);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
throw topologyError(name, error);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
subscription.redeclare = false;
|
|
497
|
+
}
|
|
498
|
+
/** Opens the consumer channel of `subscription` unless it is open or opening. */
|
|
499
|
+
#ensureConsuming(subscription) {
|
|
500
|
+
if (!subscription.active || subscription.channel !== void 0) return Promise.resolve();
|
|
501
|
+
subscription.consuming ??= this.#consume(subscription).finally(() => {
|
|
502
|
+
subscription.consuming = void 0;
|
|
503
|
+
});
|
|
504
|
+
return subscription.consuming;
|
|
505
|
+
}
|
|
506
|
+
async #consume(subscription) {
|
|
507
|
+
const channel = await this.#connection(`consume "${subscription.queue}"`).createChannel();
|
|
508
|
+
channel.on("error", (error) => {
|
|
509
|
+
this.#logger.error(`RabbitMQ consumer channel of "${subscription.queue}" failed`, { err: error });
|
|
510
|
+
});
|
|
511
|
+
channel.on("close", () => {
|
|
512
|
+
if (subscription.channel !== channel) return;
|
|
513
|
+
subscription.channel = void 0;
|
|
514
|
+
subscription.consumerTag = void 0;
|
|
515
|
+
if (subscription.active) this.#recover();
|
|
516
|
+
});
|
|
517
|
+
try {
|
|
518
|
+
await channel.prefetch(subscription.options.concurrency);
|
|
519
|
+
subscription.channel = channel;
|
|
520
|
+
const { consumerTag } = await channel.consume(subscription.queue, (message) => {
|
|
521
|
+
if (message === null) {
|
|
522
|
+
this.#onConsumerCancelled(subscription, channel);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
this.#deliver(subscription, channel, message);
|
|
526
|
+
}, { noAck: false });
|
|
527
|
+
subscription.consumerTag = consumerTag;
|
|
528
|
+
} catch (error) {
|
|
529
|
+
subscription.channel = void 0;
|
|
530
|
+
await closeQuietly(channel);
|
|
531
|
+
throw brokerUnavailableError(`RabbitMQ: cannot consume "${subscription.queue}": ${errorMessage(error)}`, error);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* The broker cancelled the consumer (`basic.cancel`: queue deleted, node down...). The channel stays
|
|
536
|
+
* open, so nothing else would notice: close it (its unsettled messages go back to the queue, when it
|
|
537
|
+
* still exists) and resume the subscription.
|
|
538
|
+
*/
|
|
539
|
+
#onConsumerCancelled(subscription, channel) {
|
|
540
|
+
if (subscription.channel !== channel || !subscription.active) return;
|
|
541
|
+
this.#logger.error(`RabbitMQ cancelled the consumer of "${subscription.queue}" (queue deleted?), consuming again`);
|
|
542
|
+
subscription.channel = void 0;
|
|
543
|
+
subscription.consumerTag = void 0;
|
|
544
|
+
subscription.redeclare = true;
|
|
545
|
+
closeQuietly(channel).then(() => void 0, () => void 0);
|
|
546
|
+
this.#recover();
|
|
547
|
+
}
|
|
548
|
+
#deliver(subscription, channel, message) {
|
|
549
|
+
const { queue } = subscription;
|
|
550
|
+
const properties = message.properties;
|
|
551
|
+
const rawHeaders = { ...properties.headers ?? {} };
|
|
552
|
+
const headers = toHeaderRecord(Object.entries(rawHeaders));
|
|
553
|
+
const deliveryCount = Number(rawHeaders["x-delivery-count"]);
|
|
554
|
+
let redeliveries = message.fields.redelivered ? 1 : 0;
|
|
555
|
+
if (Number.isSafeInteger(deliveryCount) && deliveryCount > 0) redeliveries = deliveryCount;
|
|
556
|
+
const type = stringProperty(properties.type) ?? readHeader(headers, MessagingHeaders.Type) ?? (this.#options.typeFromRoutingKey === true && message.fields.exchange !== "" ? message.fields.routingKey : void 0);
|
|
557
|
+
const id = stringProperty(properties.messageId) ?? `${queue}:${message.fields.deliveryTag}`;
|
|
558
|
+
const timestamp = typeof properties.timestamp === "number" ? properties.timestamp * 1e3 : void 0;
|
|
559
|
+
let settled = false;
|
|
560
|
+
const settle = (action) => {
|
|
561
|
+
if (settled) throw new Error(`[rabbitmq] message ${id} was already settled (${action})`);
|
|
562
|
+
if (subscription.channel !== channel) throw brokerUnavailableError(`RabbitMQ: cannot ${action} message ${id}, its channel is closed (it will be redelivered)`);
|
|
563
|
+
};
|
|
564
|
+
/** Properties of a republished copy: the original ones, with the type that selected the handler. */
|
|
565
|
+
const copyProperties = (extraHeaders, removeAttempt) => {
|
|
566
|
+
const copied = {};
|
|
567
|
+
for (const [name, value] of Object.entries(rawHeaders)) {
|
|
568
|
+
if (UNCOPIED_HEADERS.has(name) || removeAttempt && name === MessagingHeaders.Attempt) continue;
|
|
569
|
+
copied[name] = value;
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
...definedProperties(properties),
|
|
573
|
+
...type === void 0 ? {} : { type },
|
|
574
|
+
messageId: id,
|
|
575
|
+
headers: {
|
|
576
|
+
...copied,
|
|
577
|
+
...extraHeaders
|
|
578
|
+
},
|
|
579
|
+
persistent: this.#options.persistent ?? true
|
|
580
|
+
};
|
|
581
|
+
};
|
|
582
|
+
const delivery = {
|
|
583
|
+
id,
|
|
584
|
+
body: message.content,
|
|
585
|
+
contentType: stringProperty(properties.contentType),
|
|
586
|
+
headers,
|
|
587
|
+
type,
|
|
588
|
+
key: void 0,
|
|
589
|
+
attempt: attemptFromHeaders(headers) + redeliveries,
|
|
590
|
+
redelivered: message.fields.redelivered,
|
|
591
|
+
timestamp,
|
|
592
|
+
raw: message,
|
|
593
|
+
ack: async () => {
|
|
594
|
+
await Promise.resolve();
|
|
595
|
+
settle("ack");
|
|
596
|
+
channel.ack(message);
|
|
597
|
+
settled = true;
|
|
598
|
+
},
|
|
599
|
+
release: async () => {
|
|
600
|
+
await Promise.resolve();
|
|
601
|
+
settle("release");
|
|
602
|
+
channel.nack(message, false, true);
|
|
603
|
+
settled = true;
|
|
604
|
+
},
|
|
605
|
+
retry: async (request) => {
|
|
606
|
+
settle("retry");
|
|
607
|
+
const target = request.delayMs > 0 ? await this.#delayQueue(queue, Math.round(request.delayMs)) : queue;
|
|
608
|
+
try {
|
|
609
|
+
await this.#confirmPublish("", target, message.content, copyProperties({ [MessagingHeaders.Attempt]: String(request.attempt) }, false), target);
|
|
610
|
+
} catch (error) {
|
|
611
|
+
if (target !== queue) this.#delayQueues.delete(target);
|
|
612
|
+
throw error;
|
|
613
|
+
}
|
|
614
|
+
settle("retry");
|
|
615
|
+
channel.ack(message);
|
|
616
|
+
settled = true;
|
|
617
|
+
},
|
|
618
|
+
deadLetter: async (request) => {
|
|
619
|
+
settle("dead-letter");
|
|
620
|
+
if (request.queue === void 0) {
|
|
621
|
+
channel.nack(message, false, false);
|
|
622
|
+
settled = true;
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
await this.#confirmPublish("", request.queue, message.content, copyProperties({
|
|
626
|
+
[MessagingHeaders.DeadLetterReason]: request.reason,
|
|
627
|
+
[MessagingHeaders.Error]: truncate(errorMessage(request.error), MAX_ERROR_HEADER_LENGTH),
|
|
628
|
+
[MessagingHeaders.OriginalQueue]: queue,
|
|
629
|
+
[MessagingHeaders.Attempts]: String(request.attempt)
|
|
630
|
+
}, true), request.queue);
|
|
631
|
+
settle("dead-letter");
|
|
632
|
+
channel.ack(message);
|
|
633
|
+
settled = true;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
subscription.handler(delivery).catch((error) => {
|
|
637
|
+
this.#logger.error(`RabbitMQ: unexpected failure handling message ${id}`, { err: error });
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
function stringProperty(value) {
|
|
642
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
643
|
+
}
|
|
644
|
+
/** Key of the publications awaiting their confirmation (a returned message carries the same fields). */
|
|
645
|
+
function pendingKey(exchange, routingKey, messageId) {
|
|
646
|
+
return `${exchange}\u0000${routingKey}\u0000${String(messageId)}`;
|
|
647
|
+
}
|
|
648
|
+
/** Routing key of a delayed publication: `znight-delay.<bit 31>.<bit 30>...<bit 0>`. */
|
|
649
|
+
function delayRoutingKey(delayMs) {
|
|
650
|
+
const words = [DELAY_ROUTING_WORD];
|
|
651
|
+
for (let level = 31; level >= 0; level--) words.push(Math.floor(delayMs / 2 ** level) % 2 === 1 ? "1" : "0");
|
|
652
|
+
return words.join(".");
|
|
653
|
+
}
|
|
654
|
+
/** Binding key matching the delayed publications whose bit `level` is `bit` (exact word count). */
|
|
655
|
+
function delayBindingKey(level, bit) {
|
|
656
|
+
const words = [DELAY_ROUTING_WORD];
|
|
657
|
+
for (let index = 31; index >= 0; index--) words.push(index === level ? bit : "*");
|
|
658
|
+
return words.join(".");
|
|
659
|
+
}
|
|
660
|
+
/** AMQP properties of a received message, reusable for a republication. */
|
|
661
|
+
function definedProperties(properties) {
|
|
662
|
+
const result = {};
|
|
663
|
+
if (properties.contentType !== void 0) result.contentType = properties.contentType;
|
|
664
|
+
if (properties.contentEncoding !== void 0) result.contentEncoding = properties.contentEncoding;
|
|
665
|
+
if (properties.correlationId !== void 0) result.correlationId = properties.correlationId;
|
|
666
|
+
if (properties.replyTo !== void 0) result.replyTo = properties.replyTo;
|
|
667
|
+
if (properties.timestamp !== void 0) result.timestamp = properties.timestamp;
|
|
668
|
+
if (properties.type !== void 0) result.type = properties.type;
|
|
669
|
+
if (properties.appId !== void 0) result.appId = properties.appId;
|
|
670
|
+
if (properties.priority !== void 0) result.priority = properties.priority;
|
|
671
|
+
if (properties.userId !== void 0) result.userId = properties.userId;
|
|
672
|
+
return result;
|
|
673
|
+
}
|
|
674
|
+
function topologyError(queue, error) {
|
|
675
|
+
const message = errorMessage(error);
|
|
676
|
+
const hints = message.includes("PRECONDITION_FAILED") ? ["The queue exists with different arguments: align queueType/queueArguments, or set assertTopology: false"] : [];
|
|
677
|
+
return consumerError(`RabbitMQ: cannot declare the queue "${queue}": ${message}`, hints);
|
|
678
|
+
}
|
|
679
|
+
async function closeQuietly(channel) {
|
|
680
|
+
try {
|
|
681
|
+
await channel.close();
|
|
682
|
+
} catch {}
|
|
683
|
+
}
|
|
684
|
+
export { RabbitMQDriver, rabbitmq };
|
|
685
|
+
|
|
686
|
+
//# sourceMappingURL=rabbitmq.mjs.map
|