@relaybus/rabbitmq 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -46
- package/dist/index.cjs +441 -224
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -44
- package/dist/index.d.ts +120 -44
- package/dist/index.js +429 -221
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -1,35 +1,48 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var core = require('@relaybus/core');
|
|
4
|
+
var internal = require('@relaybus/core/internal');
|
|
4
5
|
|
|
5
6
|
// src/errors.ts
|
|
6
7
|
var RabbitMqUnavailableError = class extends core.RelayBusError {
|
|
8
|
+
name = "RabbitMqUnavailableError";
|
|
9
|
+
code = "RELAYBUS_RABBITMQ_UNAVAILABLE";
|
|
7
10
|
};
|
|
8
11
|
var RabbitMqPublishRejectedError = class extends core.RelayBusError {
|
|
12
|
+
name = "RabbitMqPublishRejectedError";
|
|
13
|
+
code = "RELAYBUS_RABBITMQ_PUBLISH_REJECTED";
|
|
9
14
|
};
|
|
10
15
|
var UnroutableMessageError = class extends core.RelayBusError {
|
|
16
|
+
name = "UnroutableMessageError";
|
|
17
|
+
code = "RELAYBUS_RABBITMQ_UNROUTABLE_MESSAGE";
|
|
11
18
|
};
|
|
12
19
|
var MalformedMessageError = class extends core.RelayBusError {
|
|
20
|
+
name = "MalformedMessageError";
|
|
21
|
+
code = "RELAYBUS_RABBITMQ_MALFORMED_MESSAGE";
|
|
13
22
|
};
|
|
14
23
|
var UnserializablePayloadError = class extends core.NonRetryableError {
|
|
15
24
|
name = "UnserializablePayloadError";
|
|
25
|
+
code = "RELAYBUS_RABBITMQ_UNSERIALIZABLE_PAYLOAD";
|
|
16
26
|
};
|
|
17
27
|
var AmqpLimitExceededError = class extends core.NonRetryableError {
|
|
18
28
|
name = "AmqpLimitExceededError";
|
|
29
|
+
code = "RELAYBUS_RABBITMQ_AMQP_LIMIT_EXCEEDED";
|
|
30
|
+
};
|
|
31
|
+
var RabbitMqUnencodableEventError = class extends core.NonRetryableError {
|
|
32
|
+
name = "RabbitMqUnencodableEventError";
|
|
33
|
+
code = "RELAYBUS_RABBITMQ_UNENCODABLE_EVENT";
|
|
19
34
|
};
|
|
20
|
-
|
|
21
|
-
// src/abortable-sleep.ts
|
|
22
35
|
function abortableSleep(milliseconds, signal) {
|
|
23
36
|
if (signal.aborted) {
|
|
24
37
|
return Promise.resolve();
|
|
25
38
|
}
|
|
26
39
|
return new Promise((resolve) => {
|
|
27
40
|
const finish = () => {
|
|
28
|
-
|
|
41
|
+
cancel();
|
|
29
42
|
signal.removeEventListener("abort", finish);
|
|
30
43
|
resolve();
|
|
31
44
|
};
|
|
32
|
-
const
|
|
45
|
+
const cancel = internal.schedule(core.systemClock, { run: finish, delayMs: milliseconds });
|
|
33
46
|
signal.addEventListener("abort", finish, { once: true });
|
|
34
47
|
});
|
|
35
48
|
}
|
|
@@ -38,7 +51,17 @@ function abortableSleep(milliseconds, signal) {
|
|
|
38
51
|
var ConnectionSupervisor = class {
|
|
39
52
|
#settings;
|
|
40
53
|
#stop = new AbortController();
|
|
54
|
+
/** Aborted by `forceClose`: destroys a socket that is still connecting. */
|
|
55
|
+
#force = new AbortController();
|
|
56
|
+
/** Resolves on `forceClose`: closing stops waiting for the broker. */
|
|
57
|
+
#forcing = new Promise((resolve) => {
|
|
58
|
+
this.#force.signal.addEventListener("abort", () => resolve(), { once: true });
|
|
59
|
+
});
|
|
41
60
|
#current;
|
|
61
|
+
/** Connections being closed, until their closing handshake finishes. */
|
|
62
|
+
#closing = /* @__PURE__ */ new Set();
|
|
63
|
+
/** Set by `forceClose`: connections are cut instead of closed. */
|
|
64
|
+
#forced = false;
|
|
42
65
|
#ready = false;
|
|
43
66
|
#started = false;
|
|
44
67
|
#closed = false;
|
|
@@ -85,7 +108,7 @@ var ConnectionSupervisor = class {
|
|
|
85
108
|
return;
|
|
86
109
|
}
|
|
87
110
|
this.#handleLoss(connection, reason);
|
|
88
|
-
|
|
111
|
+
void this.#closeConnection(connection);
|
|
89
112
|
}
|
|
90
113
|
/**
|
|
91
114
|
* Stops reconnecting, runs `beforeDisconnect` while the connection is still
|
|
@@ -94,23 +117,54 @@ var ConnectionSupervisor = class {
|
|
|
94
117
|
async close(beforeDisconnect) {
|
|
95
118
|
this.#closed = true;
|
|
96
119
|
this.#stop.abort();
|
|
97
|
-
await this.#reconnecting;
|
|
120
|
+
await Promise.race([this.#reconnecting, this.#forcing]);
|
|
98
121
|
try {
|
|
99
|
-
await beforeDisconnect();
|
|
122
|
+
await Promise.race([beforeDisconnect(), this.#forcing]);
|
|
100
123
|
} finally {
|
|
101
124
|
const connection = this.#current;
|
|
102
125
|
this.#current = void 0;
|
|
103
126
|
this.#ready = false;
|
|
104
|
-
|
|
127
|
+
if (connection !== void 0) {
|
|
128
|
+
await this.#closeConnection(connection);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Stops reconnecting and cuts every connection, open or closing, without the closing handshake. A
|
|
134
|
+
* connection that finishes opening afterwards is cut at once.
|
|
135
|
+
*/
|
|
136
|
+
forceClose() {
|
|
137
|
+
this.#closed = true;
|
|
138
|
+
this.#forced = true;
|
|
139
|
+
this.#stop.abort();
|
|
140
|
+
this.#force.abort();
|
|
141
|
+
for (const connection of [this.#current, ...this.#closing]) {
|
|
142
|
+
if (connection !== void 0) {
|
|
143
|
+
destroySocket(connection);
|
|
144
|
+
}
|
|
105
145
|
}
|
|
106
146
|
}
|
|
147
|
+
/** Closes a connection, which `forceClose` can cut until the broker answered; cuts it once forced. */
|
|
148
|
+
async #closeConnection(connection) {
|
|
149
|
+
if (this.#forced) {
|
|
150
|
+
destroySocket(connection);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this.#closing.add(connection);
|
|
154
|
+
await connection.close().catch(() => void 0);
|
|
155
|
+
this.#closing.delete(connection);
|
|
156
|
+
}
|
|
107
157
|
async #establish() {
|
|
108
|
-
const connection = await this.#settings.connect();
|
|
158
|
+
const connection = await this.#settings.connect({ signal: this.#force.signal });
|
|
109
159
|
connection.on("error", (error) => {
|
|
110
160
|
this.#settings.logger.warn(
|
|
111
161
|
`[ConnectionSupervisor](establish) Connection error | error=${error.message}`
|
|
112
162
|
);
|
|
113
163
|
});
|
|
164
|
+
if (this.#closed) {
|
|
165
|
+
await this.#closeConnection(connection);
|
|
166
|
+
throw new RabbitMqUnavailableError("[ConnectionSupervisor](establish) Transport is closed");
|
|
167
|
+
}
|
|
114
168
|
connection.on("close", (error) => {
|
|
115
169
|
this.#handleLoss(connection, error?.message ?? "closed by peer");
|
|
116
170
|
});
|
|
@@ -133,7 +187,7 @@ var ConnectionSupervisor = class {
|
|
|
133
187
|
this.#current = void 0;
|
|
134
188
|
}
|
|
135
189
|
this.#settings.onDisconnected();
|
|
136
|
-
await connection
|
|
190
|
+
await this.#closeConnection(connection);
|
|
137
191
|
}
|
|
138
192
|
#handleLoss(connection, reason) {
|
|
139
193
|
if (connection !== this.#current) {
|
|
@@ -160,7 +214,7 @@ var ConnectionSupervisor = class {
|
|
|
160
214
|
for (let attempt = 1; !this.#closed; attempt += 1) {
|
|
161
215
|
const delayMs = core.computeRetryDelay(policy, attempt, this.#settings.random);
|
|
162
216
|
this.#settings.logger.log(
|
|
163
|
-
`[ConnectionSupervisor](
|
|
217
|
+
`[ConnectionSupervisor](reconnectUntilConnected) Reconnecting | attempt=${attempt} delayMs=${delayMs}`
|
|
164
218
|
);
|
|
165
219
|
await abortableSleep(delayMs, this.#stop.signal);
|
|
166
220
|
if (this.#closed) {
|
|
@@ -169,78 +223,105 @@ var ConnectionSupervisor = class {
|
|
|
169
223
|
try {
|
|
170
224
|
await this.#establish();
|
|
171
225
|
this.#settings.logger.log(
|
|
172
|
-
`[ConnectionSupervisor](
|
|
226
|
+
`[ConnectionSupervisor](reconnectUntilConnected) Reconnected | attempt=${attempt}`
|
|
173
227
|
);
|
|
174
228
|
return;
|
|
175
229
|
} catch (error) {
|
|
176
230
|
this.#settings.logger.warn(
|
|
177
|
-
`[ConnectionSupervisor](
|
|
231
|
+
`[ConnectionSupervisor](reconnectUntilConnected) Attempt failed | attempt=${attempt} error=${core.toErrorInfo(error).message}`
|
|
178
232
|
);
|
|
179
233
|
}
|
|
180
234
|
}
|
|
181
235
|
}
|
|
182
236
|
};
|
|
237
|
+
function destroySocket(connection) {
|
|
238
|
+
const model = connection;
|
|
239
|
+
model.connection?.stream?.destroy(
|
|
240
|
+
new RabbitMqUnavailableError("Connection cut: the shutdown deadline passed")
|
|
241
|
+
);
|
|
242
|
+
}
|
|
183
243
|
|
|
184
244
|
// src/json-payload.ts
|
|
185
245
|
function assertJsonPayload(payload, eventId) {
|
|
186
|
-
visit(payload, "$", /* @__PURE__ */ new Set(), eventId);
|
|
246
|
+
visit(payload, { path: "$", ancestors: /* @__PURE__ */ new Set(), eventId });
|
|
187
247
|
}
|
|
188
|
-
function visit(value,
|
|
248
|
+
function visit(value, {
|
|
249
|
+
path,
|
|
250
|
+
ancestors,
|
|
251
|
+
eventId
|
|
252
|
+
}) {
|
|
189
253
|
switch (typeof value) {
|
|
190
254
|
case "string":
|
|
191
255
|
case "boolean":
|
|
192
256
|
return;
|
|
193
257
|
case "number":
|
|
194
258
|
if (!Number.isFinite(value)) {
|
|
195
|
-
throw refuse(eventId, path, `is ${value}, which JSON turns into null`);
|
|
259
|
+
throw refuse(eventId, { path, reason: `is ${value}, which JSON turns into null` });
|
|
196
260
|
}
|
|
197
261
|
return;
|
|
198
262
|
case "object":
|
|
199
263
|
if (value !== null) {
|
|
200
|
-
visitObject(value, path, ancestors, eventId);
|
|
264
|
+
visitObject(value, { path, ancestors, eventId });
|
|
201
265
|
}
|
|
202
266
|
return;
|
|
203
267
|
default:
|
|
204
|
-
throw refuse(eventId, path, `is ${typeof value}, which JSON cannot represent`);
|
|
268
|
+
throw refuse(eventId, { path, reason: `is ${typeof value}, which JSON cannot represent` });
|
|
205
269
|
}
|
|
206
270
|
}
|
|
207
|
-
function visitObject(value,
|
|
271
|
+
function visitObject(value, {
|
|
272
|
+
path,
|
|
273
|
+
ancestors,
|
|
274
|
+
eventId
|
|
275
|
+
}) {
|
|
208
276
|
if (ancestors.has(value)) {
|
|
209
|
-
throw refuse(eventId, path, "is a circular reference");
|
|
277
|
+
throw refuse(eventId, { path, reason: "is a circular reference" });
|
|
210
278
|
}
|
|
211
279
|
ancestors.add(value);
|
|
212
280
|
if (Array.isArray(value)) {
|
|
213
281
|
for (let index = 0; index < value.length; index += 1) {
|
|
214
282
|
if (!(index in value)) {
|
|
215
|
-
throw refuse(eventId,
|
|
283
|
+
throw refuse(eventId, {
|
|
284
|
+
path: `${path}[${index}]`,
|
|
285
|
+
reason: "is an array hole, which JSON turns into null"
|
|
286
|
+
});
|
|
216
287
|
}
|
|
217
|
-
visit(value[index], `${path}[${index}]`, ancestors, eventId);
|
|
288
|
+
visit(value[index], { path: `${path}[${index}]`, ancestors, eventId });
|
|
218
289
|
}
|
|
219
290
|
} else {
|
|
220
291
|
const prototype = Object.getPrototypeOf(value);
|
|
221
292
|
if (prototype !== Object.prototype && prototype !== null) {
|
|
222
|
-
throw refuse(eventId,
|
|
293
|
+
throw refuse(eventId, {
|
|
294
|
+
path,
|
|
295
|
+
reason: "is not a plain object (class instance, Date, Map or Set)"
|
|
296
|
+
});
|
|
223
297
|
}
|
|
224
298
|
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
225
|
-
throw refuse(eventId, path, "has a symbol key, which JSON drops");
|
|
299
|
+
throw refuse(eventId, { path, reason: "has a symbol key, which JSON drops" });
|
|
226
300
|
}
|
|
227
301
|
for (const key of Object.keys(value)) {
|
|
228
|
-
visit(value[key],
|
|
302
|
+
visit(value[key], {
|
|
303
|
+
path: `${path}.${key}`,
|
|
304
|
+
ancestors,
|
|
305
|
+
eventId
|
|
306
|
+
});
|
|
229
307
|
}
|
|
230
308
|
}
|
|
231
309
|
ancestors.delete(value);
|
|
232
310
|
}
|
|
233
|
-
function refuse(eventId, path, reason) {
|
|
311
|
+
function refuse(eventId, { path, reason }) {
|
|
234
312
|
return new UnserializablePayloadError(
|
|
235
313
|
`Event ${eventId} has a payload JSON cannot represent faithfully: ${path} ${reason}`
|
|
236
314
|
);
|
|
237
315
|
}
|
|
238
316
|
|
|
239
317
|
// src/wire-format.ts
|
|
318
|
+
var MILLISECONDS_PER_SECOND = 1e3;
|
|
240
319
|
var ATTRIBUTE_PREFIX = "cloudEvents:";
|
|
241
320
|
var EVENT_HEADER_PREFIX = "relaybus:";
|
|
242
321
|
var SPEC_VERSION = "1.0";
|
|
243
322
|
var JSON_CONTENT_TYPE = "application/json";
|
|
323
|
+
var STRUCTURED_CONTENT_TYPE = "application/cloudevents+json";
|
|
324
|
+
var RESERVED_HEADER_PREFIXES = ["x-death", "x-first-death-", "x-last-death-", "x-relaybus-"];
|
|
244
325
|
var SHORT_STRING_LIMIT_BYTES = 255;
|
|
245
326
|
var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
|
|
246
327
|
function encodeEnvelope(envelope) {
|
|
@@ -276,7 +357,7 @@ function encodeEnvelope(envelope) {
|
|
|
276
357
|
contentType: JSON_CONTENT_TYPE,
|
|
277
358
|
persistent: true,
|
|
278
359
|
messageId: envelope.id,
|
|
279
|
-
timestamp: Math.floor(envelope.occurredAt.getTime() /
|
|
360
|
+
timestamp: Math.floor(envelope.occurredAt.getTime() / MILLISECONDS_PER_SECOND),
|
|
280
361
|
...mirror("type", envelope.type),
|
|
281
362
|
...mirror("appId", envelope.source),
|
|
282
363
|
...envelope.correlationId === null ? {} : mirror("correlationId", envelope.correlationId),
|
|
@@ -284,6 +365,43 @@ function encodeEnvelope(envelope) {
|
|
|
284
365
|
}
|
|
285
366
|
};
|
|
286
367
|
}
|
|
368
|
+
function encodeStructuredEnvelope(envelope) {
|
|
369
|
+
assertJsonPayload(envelope.payload, envelope.id);
|
|
370
|
+
assertShortString("Event id", envelope.id);
|
|
371
|
+
const document = internal.writeStructuredCloudEvent(envelope, {
|
|
372
|
+
time: envelope.occurredAt.toISOString(),
|
|
373
|
+
data: JSON.stringify(envelope.payload)
|
|
374
|
+
});
|
|
375
|
+
return {
|
|
376
|
+
content: Buffer.from(document, "utf8"),
|
|
377
|
+
options: {
|
|
378
|
+
...messageProperties(envelope),
|
|
379
|
+
contentType: STRUCTURED_CONTENT_TYPE,
|
|
380
|
+
headers: plainHeaders(envelope, envelope.headers)
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function messageProperties(envelope) {
|
|
385
|
+
return {
|
|
386
|
+
persistent: true,
|
|
387
|
+
messageId: envelope.id,
|
|
388
|
+
timestamp: Math.floor(envelope.occurredAt.getTime() / MILLISECONDS_PER_SECOND),
|
|
389
|
+
...mirror("type", envelope.type),
|
|
390
|
+
...mirror("appId", envelope.source),
|
|
391
|
+
...envelope.correlationId === null ? {} : mirror("correlationId", envelope.correlationId)
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function plainHeaders(envelope, headers) {
|
|
395
|
+
for (const name of Object.keys(headers)) {
|
|
396
|
+
if (RESERVED_HEADER_PREFIXES.some((prefix) => name.startsWith(prefix))) {
|
|
397
|
+
throw new RabbitMqUnencodableEventError(
|
|
398
|
+
`Event ${envelope.id} has a header '${name}', a name the broker or the transport reserves`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
assertShortString(`Header name '${name}'`, name);
|
|
402
|
+
}
|
|
403
|
+
return Object.fromEntries(Object.entries(headers));
|
|
404
|
+
}
|
|
287
405
|
function assertShortString(label, value) {
|
|
288
406
|
const bytes = Buffer.byteLength(value, "utf8");
|
|
289
407
|
if (bytes > SHORT_STRING_LIMIT_BYTES) {
|
|
@@ -341,7 +459,7 @@ function parsePayload(content) {
|
|
|
341
459
|
}
|
|
342
460
|
function parseTime(rawTime, timestampSeconds) {
|
|
343
461
|
if (rawTime === void 0) {
|
|
344
|
-
const fallback = typeof timestampSeconds === "number" ? new Date(timestampSeconds *
|
|
462
|
+
const fallback = typeof timestampSeconds === "number" ? new Date(timestampSeconds * MILLISECONDS_PER_SECOND) : void 0;
|
|
345
463
|
if (fallback === void 0 || Number.isNaN(fallback.getTime())) {
|
|
346
464
|
throw new MalformedMessageError("Message has no valid time (header cloudEvents:time)");
|
|
347
465
|
}
|
|
@@ -368,6 +486,8 @@ function required(attribute, value) {
|
|
|
368
486
|
}
|
|
369
487
|
|
|
370
488
|
// src/inbound-decoders.ts
|
|
489
|
+
var MILLISECONDS_PER_SECOND2 = 1e3;
|
|
490
|
+
var { optionalAttribute, requiredAttribute, assertValidType, assertJsonDataContentType } = internal.createCloudEventReaders((reason) => new MalformedMessageError(reason));
|
|
371
491
|
var STRUCTURED_MEDIA_TYPE = "application/cloudevents+json";
|
|
372
492
|
var strictUtf8 = new TextDecoder("utf-8", { fatal: true });
|
|
373
493
|
var SPEC_VERSION2 = "1.0";
|
|
@@ -388,11 +508,11 @@ var STRUCTURED_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
|
388
508
|
"causationid"
|
|
389
509
|
]);
|
|
390
510
|
function decodeCloudEvent(message) {
|
|
391
|
-
if (mediaTypeOf(message.properties.contentType) !== STRUCTURED_MEDIA_TYPE) {
|
|
511
|
+
if (internal.mediaTypeOf(message.properties.contentType) !== STRUCTURED_MEDIA_TYPE) {
|
|
392
512
|
return decodeEnvelope(message);
|
|
393
513
|
}
|
|
394
514
|
const body = parseJsonBody(message.content);
|
|
395
|
-
if (!isJsonObject(body)) {
|
|
515
|
+
if (!internal.isJsonObject(body)) {
|
|
396
516
|
throw new MalformedMessageError("A structured CloudEvent must be a JSON object");
|
|
397
517
|
}
|
|
398
518
|
if (body["specversion"] !== SPEC_VERSION2) {
|
|
@@ -425,7 +545,7 @@ function plainJsonDecoder(options) {
|
|
|
425
545
|
}
|
|
426
546
|
return (message) => {
|
|
427
547
|
const { properties } = message;
|
|
428
|
-
if (properties.headers?.[SPEC_VERSION_HEADER] !== void 0 || mediaTypeOf(properties.contentType) === STRUCTURED_MEDIA_TYPE) {
|
|
548
|
+
if (properties.headers?.[SPEC_VERSION_HEADER] !== void 0 || internal.mediaTypeOf(properties.contentType) === STRUCTURED_MEDIA_TYPE) {
|
|
429
549
|
return decodeCloudEvent(message);
|
|
430
550
|
}
|
|
431
551
|
const plain = {
|
|
@@ -434,12 +554,12 @@ function plainJsonDecoder(options) {
|
|
|
434
554
|
headers: readForeignHeaders(properties),
|
|
435
555
|
payload: parseJsonBody(message.content)
|
|
436
556
|
};
|
|
437
|
-
const type = resolveText(options.type, plain, "type");
|
|
557
|
+
const type = resolveText(options.type, { message: plain, name: "type" });
|
|
438
558
|
if (type === void 0) {
|
|
439
559
|
throw new MalformedMessageError("Message has no event type: the type option found none");
|
|
440
560
|
}
|
|
441
561
|
assertValidType(type, "The event type");
|
|
442
|
-
const id = resolveText(options.id, plain, "id") ?? nonEmptyText(properties.messageId);
|
|
562
|
+
const id = resolveText(options.id, { message: plain, name: "id" }) ?? nonEmptyText(properties.messageId);
|
|
443
563
|
if (id === void 0) {
|
|
444
564
|
throw new MalformedMessageError(
|
|
445
565
|
"Message has no id: set the messageId property on the producer or configure the id option"
|
|
@@ -448,8 +568,8 @@ function plainJsonDecoder(options) {
|
|
|
448
568
|
return {
|
|
449
569
|
id,
|
|
450
570
|
type,
|
|
451
|
-
source: resolveText(options.source, plain, "source") ?? defaultSource(message),
|
|
452
|
-
key: resolveText(options.key, plain, "key") ?? null,
|
|
571
|
+
source: resolveText(options.source, { message: plain, name: "source" }) ?? defaultSource(message),
|
|
572
|
+
key: resolveText(options.key, { message: plain, name: "key" }) ?? null,
|
|
453
573
|
payload: plain.payload,
|
|
454
574
|
headers: plain.headers,
|
|
455
575
|
correlationId: null,
|
|
@@ -492,47 +612,12 @@ function parseJsonBody(content) {
|
|
|
492
612
|
throw new MalformedMessageError("Message body is not valid JSON");
|
|
493
613
|
}
|
|
494
614
|
}
|
|
495
|
-
function mediaTypeOf(contentType) {
|
|
496
|
-
return typeof contentType === "string" ? contentType.split(";")[0].trim().toLowerCase() : void 0;
|
|
497
|
-
}
|
|
498
|
-
function isJsonObject(value) {
|
|
499
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
500
|
-
}
|
|
501
615
|
function nonEmptyText(value) {
|
|
502
616
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
503
617
|
}
|
|
504
618
|
function defaultSource(message) {
|
|
505
619
|
return nonEmptyText(message.properties.appId) ?? message.fields.routingKey;
|
|
506
620
|
}
|
|
507
|
-
function optionalAttribute(body, name) {
|
|
508
|
-
const value = body[name];
|
|
509
|
-
if (value !== void 0 && typeof value !== "string") {
|
|
510
|
-
throw new MalformedMessageError(`Attribute '${name}' must be a string`);
|
|
511
|
-
}
|
|
512
|
-
return value;
|
|
513
|
-
}
|
|
514
|
-
function requiredAttribute(body, name) {
|
|
515
|
-
const value = optionalAttribute(body, name);
|
|
516
|
-
if (value === void 0 || value.length === 0) {
|
|
517
|
-
throw new MalformedMessageError(`Attribute '${name}' is missing or empty`);
|
|
518
|
-
}
|
|
519
|
-
return value;
|
|
520
|
-
}
|
|
521
|
-
function assertValidType(type, label) {
|
|
522
|
-
try {
|
|
523
|
-
core.assertEventType(type);
|
|
524
|
-
} catch (error) {
|
|
525
|
-
throw new MalformedMessageError(
|
|
526
|
-
`${label} is not a valid event type: ${error.message}`
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
function assertJsonDataContentType(dataContentType) {
|
|
531
|
-
const mediaType = mediaTypeOf(dataContentType);
|
|
532
|
-
if (mediaType !== void 0 && mediaType !== "application/json" && mediaType !== "text/json" && !mediaType.endsWith("+json")) {
|
|
533
|
-
throw new MalformedMessageError(`datacontenttype must be JSON, received '${dataContentType}'`);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
621
|
function parseTime2(text2) {
|
|
537
622
|
const time = new Date(text2);
|
|
538
623
|
if (!RFC3339_PATTERN.test(text2) || Number.isNaN(time.getTime())) {
|
|
@@ -542,7 +627,7 @@ function parseTime2(text2) {
|
|
|
542
627
|
}
|
|
543
628
|
function propertyTimeOrNow(properties) {
|
|
544
629
|
const { timestamp } = properties;
|
|
545
|
-
return typeof timestamp === "number" && Number.isFinite(timestamp) ? new Date(timestamp *
|
|
630
|
+
return typeof timestamp === "number" && Number.isFinite(timestamp) ? new Date(timestamp * MILLISECONDS_PER_SECOND2) : /* @__PURE__ */ new Date();
|
|
546
631
|
}
|
|
547
632
|
function extensionAttributes(body) {
|
|
548
633
|
return Object.fromEntries(
|
|
@@ -558,7 +643,7 @@ function readForeignHeaders(properties) {
|
|
|
558
643
|
)
|
|
559
644
|
);
|
|
560
645
|
}
|
|
561
|
-
function resolveText(option, message, name) {
|
|
646
|
+
function resolveText(option, { message, name }) {
|
|
562
647
|
const value = typeof option === "function" ? option(message) : option;
|
|
563
648
|
if (value === void 0 || value === null) {
|
|
564
649
|
return void 0;
|
|
@@ -587,9 +672,62 @@ function observeRejection(value) {
|
|
|
587
672
|
Promise.resolve(value).catch(() => void 0);
|
|
588
673
|
}
|
|
589
674
|
}
|
|
675
|
+
|
|
676
|
+
// src/codecs.ts
|
|
677
|
+
var cloudEventsBinary = Object.freeze({
|
|
678
|
+
encode: encodeEnvelope,
|
|
679
|
+
decode: decodeEnvelope
|
|
680
|
+
});
|
|
681
|
+
var cloudEventsStructured = Object.freeze({
|
|
682
|
+
encode: encodeStructuredEnvelope,
|
|
683
|
+
decode: decodeCloudEvent
|
|
684
|
+
});
|
|
685
|
+
var MESSAGE_ID_PROPERTY = "message-id-property";
|
|
686
|
+
var DEFAULT_CONTENT_TYPE = "application/json";
|
|
687
|
+
function jsonEnvelope(mapping) {
|
|
688
|
+
const format = internal.createJsonEnvelopeFormat(mapping, {
|
|
689
|
+
transportIds: [MESSAGE_ID_PROPERTY],
|
|
690
|
+
failures: {
|
|
691
|
+
invalid: (reason) => new MalformedMessageError(reason),
|
|
692
|
+
unencodable: (reason) => new RabbitMqUnencodableEventError(reason)
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
const contentType = mapping.contentType ?? DEFAULT_CONTENT_TYPE;
|
|
696
|
+
return {
|
|
697
|
+
encode: (envelope) => {
|
|
698
|
+
assertJsonPayload(envelope.payload, envelope.id);
|
|
699
|
+
assertShortString("Event id", envelope.id);
|
|
700
|
+
const { body, headers } = format.write(envelope);
|
|
701
|
+
return {
|
|
702
|
+
content: Buffer.from(JSON.stringify(body), "utf8"),
|
|
703
|
+
options: {
|
|
704
|
+
...messageProperties(envelope),
|
|
705
|
+
contentType,
|
|
706
|
+
headers: plainHeaders(envelope, headers)
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
},
|
|
710
|
+
decode: (message) => {
|
|
711
|
+
const fields = format.read({
|
|
712
|
+
body: parseJsonBody(message.content),
|
|
713
|
+
headers: readForeignHeaders(message.properties)
|
|
714
|
+
});
|
|
715
|
+
return { ...fields, id: fields.id ?? requireMessageId(message) };
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function requireMessageId(message) {
|
|
720
|
+
const id = nonEmptyText(message.properties.messageId);
|
|
721
|
+
if (id === void 0) {
|
|
722
|
+
throw new MalformedMessageError("Message has no id: the messageId property is missing");
|
|
723
|
+
}
|
|
724
|
+
return id;
|
|
725
|
+
}
|
|
590
726
|
var REDELIVERIES_HEADER = "x-relaybus-redeliveries";
|
|
591
727
|
var FIRST_FAILURE_HEADER = "x-relaybus-first-failure-at";
|
|
592
728
|
var DEAD_LETTER_REASON_HEADER = "x-relaybus-dead-letter-reason";
|
|
729
|
+
var DEAD_LETTER_ERROR_NAME_HEADER = "x-relaybus-dead-letter-error-name";
|
|
730
|
+
var DEAD_LETTER_ERROR_CODE_HEADER = "x-relaybus-dead-letter-error-code";
|
|
593
731
|
var ORIGINAL_ROUTING_KEY_HEADER = "x-relaybus-routing-key";
|
|
594
732
|
var RELAYBUS_STATE_HEADERS = [REDELIVERIES_HEADER, FIRST_FAILURE_HEADER];
|
|
595
733
|
var BROKER_COUNTER_HEADERS = ["x-delivery-count", "x-acquired-count"];
|
|
@@ -614,7 +752,7 @@ function buildRetryLadder(policy) {
|
|
|
614
752
|
delay *= policy.factor;
|
|
615
753
|
}
|
|
616
754
|
}
|
|
617
|
-
function chooseRetryTier(ladder, delayMs, random) {
|
|
755
|
+
function chooseRetryTier(ladder, { delayMs, random }) {
|
|
618
756
|
const upperIndex = ladder.findIndex((tier) => tier >= delayMs);
|
|
619
757
|
if (upperIndex === -1) {
|
|
620
758
|
return ladder.at(-1);
|
|
@@ -637,6 +775,8 @@ function copyForRepublish(message, headerChanges) {
|
|
|
637
775
|
for (const name of [
|
|
638
776
|
...RELAYBUS_STATE_HEADERS,
|
|
639
777
|
DEAD_LETTER_REASON_HEADER,
|
|
778
|
+
DEAD_LETTER_ERROR_NAME_HEADER,
|
|
779
|
+
DEAD_LETTER_ERROR_CODE_HEADER,
|
|
640
780
|
...BROKER_COUNTER_HEADERS
|
|
641
781
|
]) {
|
|
642
782
|
delete headers[name];
|
|
@@ -666,6 +806,8 @@ function withOriginalRoutingKey(message) {
|
|
|
666
806
|
}
|
|
667
807
|
|
|
668
808
|
// src/options.ts
|
|
809
|
+
var DEFAULT_PREFETCH = 50;
|
|
810
|
+
var DEFAULT_DELIVERY_LIMIT = 10;
|
|
669
811
|
var DEFAULT_HANDLER_RETRY = {
|
|
670
812
|
attempts: 3,
|
|
671
813
|
initialDelayMs: 200,
|
|
@@ -692,6 +834,14 @@ var DEFAULT_RECONNECT = {
|
|
|
692
834
|
retryIf: void 0
|
|
693
835
|
};
|
|
694
836
|
function resolveSettings(options) {
|
|
837
|
+
const basics = resolveBasics(options);
|
|
838
|
+
return {
|
|
839
|
+
...resolveTopology(options, basics),
|
|
840
|
+
...resolveRetries(options, basics),
|
|
841
|
+
...resolveRuntime(options, basics)
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function resolveBasics(options) {
|
|
695
845
|
const exchange = nonEmpty(options.exchange ?? "relaybus", "exchange");
|
|
696
846
|
const failOnConnectError = options.failOnConnectError ?? true;
|
|
697
847
|
if (!failOnConnectError && options.reconnect === false) {
|
|
@@ -700,11 +850,17 @@ function resolveSettings(options) {
|
|
|
700
850
|
);
|
|
701
851
|
}
|
|
702
852
|
const ordered = options.ordered ?? false;
|
|
703
|
-
const prefetch =
|
|
853
|
+
const prefetch = internal.assertPositiveInteger(
|
|
854
|
+
options.prefetch ?? (ordered ? 1 : DEFAULT_PREFETCH),
|
|
855
|
+
"prefetch"
|
|
856
|
+
);
|
|
704
857
|
if (ordered && prefetch !== 1) {
|
|
705
858
|
throw new core.ConfigurationError(`ordered consumption needs prefetch 1, received ${prefetch}`);
|
|
706
859
|
}
|
|
707
860
|
const redelivery = core.createRetryPolicy(options.redelivery, DEFAULT_REDELIVERY);
|
|
861
|
+
return { exchange, failOnConnectError, ordered, prefetch, redelivery };
|
|
862
|
+
}
|
|
863
|
+
function resolveTopology(options, { exchange, prefetch }) {
|
|
708
864
|
return {
|
|
709
865
|
name: nonEmpty(options.name ?? "rabbitmq", "name"),
|
|
710
866
|
connect: resolveConnect(options),
|
|
@@ -718,7 +874,14 @@ function resolveSettings(options) {
|
|
|
718
874
|
"deadLetterExchange"
|
|
719
875
|
),
|
|
720
876
|
deadLetterQueue: optionalNonEmpty(options.deadLetterQueue, "deadLetterQueue"),
|
|
721
|
-
deliveryLimit:
|
|
877
|
+
deliveryLimit: internal.assertPositiveInteger(
|
|
878
|
+
options.deliveryLimit ?? DEFAULT_DELIVERY_LIMIT,
|
|
879
|
+
"deliveryLimit"
|
|
880
|
+
)
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
function resolveRetries(options, { ordered, redelivery }) {
|
|
884
|
+
return {
|
|
722
885
|
handlerRetry: core.createRetryPolicy(options.handlerRetry, DEFAULT_HANDLER_RETRY),
|
|
723
886
|
redelivery,
|
|
724
887
|
retryLadder: ordered ? [] : buildRetryLadder(redelivery),
|
|
@@ -726,13 +889,17 @@ function resolveSettings(options) {
|
|
|
726
889
|
options.redelivery?.maxAge ?? DEFAULT_REDELIVERY_MAX_AGE,
|
|
727
890
|
"redelivery.maxAge"
|
|
728
891
|
),
|
|
729
|
-
ordered
|
|
892
|
+
ordered
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
function resolveRuntime(options, { failOnConnectError }) {
|
|
896
|
+
return {
|
|
730
897
|
reconnect: resolveReconnect(options.reconnect),
|
|
731
898
|
failOnConnectError,
|
|
732
899
|
allowUnroutable: options.allowUnroutable ?? false,
|
|
733
900
|
publishTimeoutMs: positiveDuration(options.publishTimeout ?? "30s", "publishTimeout"),
|
|
734
901
|
shutdownTimeoutMs: core.toMilliseconds(options.shutdownTimeout ?? "30s", "shutdownTimeout"),
|
|
735
|
-
|
|
902
|
+
...resolveCodec(options),
|
|
736
903
|
logger: options.logger ?? console,
|
|
737
904
|
random: options.random ?? Math.random
|
|
738
905
|
};
|
|
@@ -748,11 +915,21 @@ function resolveQueueName(settings) {
|
|
|
748
915
|
}
|
|
749
916
|
return `${settings.queuePrefix}.${settings.consumerGroup}`;
|
|
750
917
|
}
|
|
751
|
-
function
|
|
752
|
-
|
|
753
|
-
|
|
918
|
+
function resolveCodec({
|
|
919
|
+
codec,
|
|
920
|
+
decode
|
|
921
|
+
}) {
|
|
922
|
+
if (decode !== void 0 && codec !== void 0) {
|
|
923
|
+
throw new core.ConfigurationError("Give 'codec' or the deprecated 'decode', not both");
|
|
924
|
+
}
|
|
925
|
+
const chosen = decode === void 0 ? codec ?? cloudEventsBinary : { encode: encodeEnvelope, decode };
|
|
926
|
+
if (chosen === cloudEventsBinary) {
|
|
927
|
+
return { encode: encodeEnvelope, decode: decodeEnvelope };
|
|
754
928
|
}
|
|
755
|
-
return
|
|
929
|
+
return {
|
|
930
|
+
encode: chosen.encode,
|
|
931
|
+
decode: (message) => completeDecodedEvent(chosen.decode(message), message)
|
|
932
|
+
};
|
|
756
933
|
}
|
|
757
934
|
function resolveConnect(options) {
|
|
758
935
|
const { url, connect } = options;
|
|
@@ -762,9 +939,10 @@ function resolveConnect(options) {
|
|
|
762
939
|
if (connect !== void 0) {
|
|
763
940
|
return connect;
|
|
764
941
|
}
|
|
765
|
-
return async () => {
|
|
942
|
+
return async ({ signal }) => {
|
|
766
943
|
const amqplib = await import('amqplib');
|
|
767
|
-
|
|
944
|
+
const socketOptions = { signal };
|
|
945
|
+
return amqplib.connect(url, socketOptions);
|
|
768
946
|
};
|
|
769
947
|
}
|
|
770
948
|
function resolveReconnect(reconnect) {
|
|
@@ -782,12 +960,6 @@ function nonEmpty(value, name) {
|
|
|
782
960
|
function optionalNonEmpty(value, name) {
|
|
783
961
|
return value === void 0 ? void 0 : nonEmpty(value, name);
|
|
784
962
|
}
|
|
785
|
-
function positiveInteger(value, name) {
|
|
786
|
-
if (!Number.isInteger(value) || value < 1) {
|
|
787
|
-
throw new core.ConfigurationError(`${name} must be a positive integer, received ${value}`);
|
|
788
|
-
}
|
|
789
|
-
return value;
|
|
790
|
-
}
|
|
791
963
|
function positiveDuration(value, name) {
|
|
792
964
|
const milliseconds = core.toMilliseconds(value, name);
|
|
793
965
|
if (milliseconds === 0) {
|
|
@@ -936,74 +1108,115 @@ var QueueConsumer = class {
|
|
|
936
1108
|
this.#inFlight.add(task);
|
|
937
1109
|
}
|
|
938
1110
|
async #process(channel, message) {
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
try {
|
|
942
|
-
envelope = this.#settings.decode(withOriginalRoutingKey(message));
|
|
943
|
-
} catch (error) {
|
|
944
|
-
this.#settings.logger.error(
|
|
945
|
-
`[QueueConsumer](process) Dead-lettering malformed message | queue=${queue} routingKey=${message.fields.routingKey} error=${core.toErrorInfo(error).message}`
|
|
946
|
-
);
|
|
947
|
-
await this.#deadLetter(channel, message, "malformed message");
|
|
1111
|
+
const decoded = await this.#decodeOrDeadLetter(channel, message);
|
|
1112
|
+
if (decoded === void 0) {
|
|
948
1113
|
return;
|
|
949
1114
|
}
|
|
950
|
-
|
|
951
|
-
|
|
1115
|
+
const { envelope } = decoded;
|
|
1116
|
+
const delivery = {
|
|
1117
|
+
channel,
|
|
1118
|
+
message,
|
|
1119
|
+
envelope,
|
|
1120
|
+
redeliveries: readCounterHeader(message, REDELIVERIES_HEADER) ?? 0,
|
|
1121
|
+
firstFailureAt: readCounterHeader(message, FIRST_FAILURE_HEADER)
|
|
1122
|
+
};
|
|
952
1123
|
for (let attempt = 1; ; attempt += 1) {
|
|
953
1124
|
try {
|
|
954
|
-
await this.#settings.onMessage(envelope);
|
|
955
|
-
this.#settle(channel, message, "ack");
|
|
1125
|
+
await this.#settings.onMessage(envelope, { destination: message.fields.routingKey });
|
|
1126
|
+
this.#settle(channel, { message, settlement: "ack" });
|
|
956
1127
|
return;
|
|
957
1128
|
} catch (error) {
|
|
958
|
-
|
|
959
|
-
if (!core.isRetryableError(handlerRetry, error)) {
|
|
960
|
-
this.#settings.logger.error(
|
|
961
|
-
`[QueueConsumer](process) Dead-lettering message the handler cannot process | ${failure}`
|
|
962
|
-
);
|
|
963
|
-
await this.#deadLetter(channel, message, "non-retryable handler error");
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
if (attempt < handlerRetry.attempts) {
|
|
967
|
-
const delayMs2 = core.computeRetryDelay(handlerRetry, attempt, this.#settings.random);
|
|
968
|
-
this.#settings.logger.warn(
|
|
969
|
-
`[QueueConsumer](process) Handler failed, retrying | ${failure} delayMs=${delayMs2}`
|
|
970
|
-
);
|
|
971
|
-
if (await this.#interrupted(delayMs2)) {
|
|
972
|
-
this.#settle(channel, message, "requeue");
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
continue;
|
|
976
|
-
}
|
|
977
|
-
redeliveries += 1;
|
|
978
|
-
const now = Date.now();
|
|
979
|
-
firstFailureAt ??= now;
|
|
980
|
-
const delayMs = this.#redeliveryDelay(redeliveries);
|
|
981
|
-
if (now + delayMs - firstFailureAt > this.#settings.redeliveryMaxAgeMs) {
|
|
982
|
-
this.#settings.logger.error(
|
|
983
|
-
`[QueueConsumer](process) Dead-lettering message that kept failing | ${failure} redeliveries=${redeliveries - 1} failingForMs=${now - firstFailureAt} maxAgeMs=${this.#settings.redeliveryMaxAgeMs}`
|
|
984
|
-
);
|
|
985
|
-
await this.#deadLetter(channel, message, "redelivery window exhausted");
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
this.#settings.logger.warn(
|
|
989
|
-
`[QueueConsumer](process) Handler attempts spent, retrying later | ${failure} redeliveries=${redeliveries} delayMs=${delayMs}`
|
|
990
|
-
);
|
|
991
|
-
if (!this.#settings.ordered) {
|
|
992
|
-
await this.#redeliverLater(channel, message, delayMs, redeliveries, firstFailureAt);
|
|
993
|
-
return;
|
|
994
|
-
}
|
|
995
|
-
if (await this.#interrupted(delayMs)) {
|
|
996
|
-
this.#settle(channel, message, "requeue");
|
|
1129
|
+
if (await this.#afterFailure(delivery, { attempt, error }) === "settled") {
|
|
997
1130
|
return;
|
|
998
1131
|
}
|
|
999
1132
|
}
|
|
1000
1133
|
}
|
|
1001
1134
|
}
|
|
1135
|
+
/** The event of `message`, or undefined after dead-lettering a message that is not one. */
|
|
1136
|
+
async #decodeOrDeadLetter(channel, message) {
|
|
1137
|
+
try {
|
|
1138
|
+
return { envelope: this.#settings.decode(withOriginalRoutingKey(message)) };
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
this.#settings.logger.error(
|
|
1141
|
+
`[QueueConsumer](decodeOrDeadLetter) Dead-lettering malformed message | queue=${this.#settings.queue} routingKey=${message.fields.routingKey} ${describeError(error)}`
|
|
1142
|
+
);
|
|
1143
|
+
this.#settings.onInvalidMessage?.({ destination: message.fields.routingKey, error });
|
|
1144
|
+
await this.#deadLetter(channel, { message, reason: "malformed message", error });
|
|
1145
|
+
return void 0;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Decides what follows a failed attempt: dead-letter a non-retryable error, retry on the same
|
|
1150
|
+
* delivery while local attempts remain, else redeliver later. `retry` runs the handler again.
|
|
1151
|
+
*/
|
|
1152
|
+
async #afterFailure(delivery, { attempt, error }) {
|
|
1153
|
+
const { channel, message, envelope } = delivery;
|
|
1154
|
+
const { queue, handlerRetry } = this.#settings;
|
|
1155
|
+
const failure = `queue=${queue} eventId=${envelope.id} attempt=${attempt} ${describeError(error)}`;
|
|
1156
|
+
if (!core.isRetryableError(handlerRetry, error)) {
|
|
1157
|
+
this.#settings.logger.error(
|
|
1158
|
+
`[QueueConsumer](afterFailure) Dead-lettering message the handler cannot process | ${failure}`
|
|
1159
|
+
);
|
|
1160
|
+
await this.#deadLetter(channel, {
|
|
1161
|
+
message,
|
|
1162
|
+
reason: "non-retryable handler error",
|
|
1163
|
+
error
|
|
1164
|
+
});
|
|
1165
|
+
return "settled";
|
|
1166
|
+
}
|
|
1167
|
+
if (attempt < handlerRetry.attempts) {
|
|
1168
|
+
const delayMs = core.computeRetryDelay(handlerRetry, attempt, this.#settings.random);
|
|
1169
|
+
this.#settings.logger.warn(
|
|
1170
|
+
`[QueueConsumer](afterFailure) Handler failed, retrying | ${failure} delayMs=${delayMs}`
|
|
1171
|
+
);
|
|
1172
|
+
return this.#waitOrRequeue(delivery, delayMs);
|
|
1173
|
+
}
|
|
1174
|
+
return this.#afterSpentAttempts(delivery, { failure, error });
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* The local attempts are spent: dead-letter a message that kept failing past `redeliveryMaxAgeMs`,
|
|
1178
|
+
* else park it in a retry queue, or, when ordered, wait and retry it in place.
|
|
1179
|
+
*/
|
|
1180
|
+
async #afterSpentAttempts(delivery, { failure, error }) {
|
|
1181
|
+
const { channel, message } = delivery;
|
|
1182
|
+
delivery.redeliveries += 1;
|
|
1183
|
+
const now = Date.now();
|
|
1184
|
+
delivery.firstFailureAt ??= now;
|
|
1185
|
+
const { redeliveries, firstFailureAt } = delivery;
|
|
1186
|
+
const delayMs = this.#redeliveryDelay(redeliveries);
|
|
1187
|
+
if (now + delayMs - firstFailureAt > this.#settings.redeliveryMaxAgeMs) {
|
|
1188
|
+
this.#settings.logger.error(
|
|
1189
|
+
`[QueueConsumer](afterSpentAttempts) Dead-lettering message that kept failing | ${failure} redeliveries=${redeliveries - 1} failingForMs=${now - firstFailureAt} maxAgeMs=${this.#settings.redeliveryMaxAgeMs}`
|
|
1190
|
+
);
|
|
1191
|
+
await this.#deadLetter(channel, {
|
|
1192
|
+
message,
|
|
1193
|
+
reason: "redelivery window exhausted",
|
|
1194
|
+
error
|
|
1195
|
+
});
|
|
1196
|
+
return "settled";
|
|
1197
|
+
}
|
|
1198
|
+
this.#settings.logger.warn(
|
|
1199
|
+
`[QueueConsumer](afterSpentAttempts) Handler attempts spent, retrying later | ${failure} redeliveries=${redeliveries} delayMs=${delayMs}`
|
|
1200
|
+
);
|
|
1201
|
+
if (!this.#settings.ordered) {
|
|
1202
|
+
await this.#redeliverLater(channel, { message, delayMs, redeliveries, firstFailureAt });
|
|
1203
|
+
return "settled";
|
|
1204
|
+
}
|
|
1205
|
+
return this.#waitOrRequeue(delivery, delayMs);
|
|
1206
|
+
}
|
|
1207
|
+
/** Waits `delayMs` before the next attempt; requeues instead when the consumer stops meanwhile. */
|
|
1208
|
+
async #waitOrRequeue(delivery, delayMs) {
|
|
1209
|
+
if (await this.#interrupted(delayMs)) {
|
|
1210
|
+
this.#settle(delivery.channel, { message: delivery.message, settlement: "requeue" });
|
|
1211
|
+
return "settled";
|
|
1212
|
+
}
|
|
1213
|
+
return "retry";
|
|
1214
|
+
}
|
|
1002
1215
|
/** The backoff before the next delayed redelivery: a retry queue's TTL, or an exact wait when ordered. */
|
|
1003
1216
|
#redeliveryDelay(redeliveries) {
|
|
1004
1217
|
const { redelivery, retryLadder, ordered, random } = this.#settings;
|
|
1005
1218
|
const delayMs = core.computeRetryDelay(redelivery, redeliveries, random);
|
|
1006
|
-
return ordered ? delayMs : chooseRetryTier(retryLadder, delayMs, random);
|
|
1219
|
+
return ordered ? delayMs : chooseRetryTier(retryLadder, { delayMs, random });
|
|
1007
1220
|
}
|
|
1008
1221
|
/** Sleeps; true when the consumer is stopping, so the caller must give the message back. */
|
|
1009
1222
|
async #interrupted(delayMs) {
|
|
@@ -1016,7 +1229,8 @@ var QueueConsumer = class {
|
|
|
1016
1229
|
* the original is requeued after the same delay, so it is neither lost nor
|
|
1017
1230
|
* hot-looped.
|
|
1018
1231
|
*/
|
|
1019
|
-
async #redeliverLater(channel,
|
|
1232
|
+
async #redeliverLater(channel, input) {
|
|
1233
|
+
const { message, delayMs, redeliveries, firstFailureAt } = input;
|
|
1020
1234
|
const { content, options } = copyForRepublish(message, {
|
|
1021
1235
|
[REDELIVERIES_HEADER]: String(redeliveries),
|
|
1022
1236
|
[FIRST_FAILURE_HEADER]: String(firstFailureAt),
|
|
@@ -1024,20 +1238,18 @@ var QueueConsumer = class {
|
|
|
1024
1238
|
});
|
|
1025
1239
|
try {
|
|
1026
1240
|
await this.#settings.republish(
|
|
1027
|
-
"",
|
|
1028
|
-
|
|
1029
|
-
content,
|
|
1030
|
-
options
|
|
1241
|
+
{ exchange: "", routingKey: retryQueueName(this.#settings.queue, delayMs) },
|
|
1242
|
+
{ content, options }
|
|
1031
1243
|
);
|
|
1032
1244
|
} catch (error) {
|
|
1033
1245
|
this.#settings.logger.warn(
|
|
1034
1246
|
`[QueueConsumer](redeliverLater) Could not park the message in the retry queue, requeueing it | queue=${this.#settings.queue} delayMs=${delayMs} error=${core.toErrorInfo(error).message}`
|
|
1035
1247
|
);
|
|
1036
1248
|
await this.#interrupted(delayMs);
|
|
1037
|
-
this.#settle(channel, message, "requeue");
|
|
1249
|
+
this.#settle(channel, { message, settlement: "requeue" });
|
|
1038
1250
|
return;
|
|
1039
1251
|
}
|
|
1040
|
-
this.#settle(channel, message, "ack");
|
|
1252
|
+
this.#settle(channel, { message, settlement: "ack" });
|
|
1041
1253
|
}
|
|
1042
1254
|
/**
|
|
1043
1255
|
* Moves the message to the dead-letter queue through a confirmed copy that
|
|
@@ -1045,27 +1257,29 @@ var QueueConsumer = class {
|
|
|
1045
1257
|
* message is rejected without requeue, which makes the broker dead-letter it
|
|
1046
1258
|
* (at-least-once), so it still ends up in the dead-letter queue.
|
|
1047
1259
|
*/
|
|
1048
|
-
async #deadLetter(channel,
|
|
1260
|
+
async #deadLetter(channel, input) {
|
|
1261
|
+
const { message, reason, error } = input;
|
|
1262
|
+
const { name, code } = core.toErrorInfo(error);
|
|
1049
1263
|
const { content, options } = copyForRepublish(message, {
|
|
1050
|
-
[DEAD_LETTER_REASON_HEADER]: reason
|
|
1264
|
+
[DEAD_LETTER_REASON_HEADER]: reason,
|
|
1265
|
+
[DEAD_LETTER_ERROR_NAME_HEADER]: name,
|
|
1266
|
+
...code === void 0 ? {} : { [DEAD_LETTER_ERROR_CODE_HEADER]: code }
|
|
1051
1267
|
});
|
|
1052
1268
|
try {
|
|
1053
1269
|
await this.#settings.republish(
|
|
1054
|
-
this.#settings.deadLetterExchange,
|
|
1055
|
-
|
|
1056
|
-
content,
|
|
1057
|
-
options
|
|
1270
|
+
{ exchange: this.#settings.deadLetterExchange, routingKey: this.#settings.queue },
|
|
1271
|
+
{ content, options }
|
|
1058
1272
|
);
|
|
1059
|
-
} catch (
|
|
1273
|
+
} catch (error2) {
|
|
1060
1274
|
this.#settings.logger.warn(
|
|
1061
|
-
`[QueueConsumer](deadLetter) Could not publish to the dead-letter exchange, leaving it to the broker | queue=${this.#settings.queue} error=${core.toErrorInfo(
|
|
1275
|
+
`[QueueConsumer](deadLetter) Could not publish to the dead-letter exchange, leaving it to the broker | queue=${this.#settings.queue} error=${core.toErrorInfo(error2).message}`
|
|
1062
1276
|
);
|
|
1063
|
-
this.#settle(channel, message, "dead-letter");
|
|
1277
|
+
this.#settle(channel, { message, settlement: "dead-letter" });
|
|
1064
1278
|
return;
|
|
1065
1279
|
}
|
|
1066
|
-
this.#settle(channel, message, "ack");
|
|
1280
|
+
this.#settle(channel, { message, settlement: "ack" });
|
|
1067
1281
|
}
|
|
1068
|
-
#settle(channel, message, settlement) {
|
|
1282
|
+
#settle(channel, { message, settlement }) {
|
|
1069
1283
|
try {
|
|
1070
1284
|
if (settlement === "ack") {
|
|
1071
1285
|
channel.ack(message);
|
|
@@ -1079,6 +1293,10 @@ var QueueConsumer = class {
|
|
|
1079
1293
|
}
|
|
1080
1294
|
}
|
|
1081
1295
|
};
|
|
1296
|
+
function describeError(error) {
|
|
1297
|
+
const { message, name, code } = core.toErrorInfo(error);
|
|
1298
|
+
return `error=${message} errorName=${name}${code === void 0 ? "" : ` code=${code}`}`;
|
|
1299
|
+
}
|
|
1082
1300
|
|
|
1083
1301
|
// src/serial-queue.ts
|
|
1084
1302
|
var SerialQueue = class {
|
|
@@ -1094,13 +1312,14 @@ var SerialQueue = class {
|
|
|
1094
1312
|
var NACK_MESSAGE = "message nacked";
|
|
1095
1313
|
var RabbitMqTransport = class {
|
|
1096
1314
|
name;
|
|
1315
|
+
system = "rabbitmq";
|
|
1097
1316
|
#settings;
|
|
1098
1317
|
#supervisor;
|
|
1099
1318
|
/** Serializes topology work so a subscription never races a reconnect. */
|
|
1100
1319
|
#topologyWork = new SerialQueue();
|
|
1101
1320
|
#live;
|
|
1102
1321
|
#subscription;
|
|
1103
|
-
#
|
|
1322
|
+
#closing;
|
|
1104
1323
|
constructor(options) {
|
|
1105
1324
|
this.#settings = resolveSettings(options);
|
|
1106
1325
|
this.name = this.#settings.name;
|
|
@@ -1140,19 +1359,21 @@ var RabbitMqTransport = class {
|
|
|
1140
1359
|
* @throws {RabbitMqUnavailableError} not connected, channel lost, or confirm timed out
|
|
1141
1360
|
* @throws {RabbitMqPublishRejectedError} the broker nacked the message
|
|
1142
1361
|
* @throws {UnroutableMessageError} no queue is bound to `destination` (unless `allowUnroutable`)
|
|
1362
|
+
* @throws whatever the `codec`'s `encode` throws, such as {@link RabbitMqUnencodableEventError}
|
|
1143
1363
|
*/
|
|
1144
1364
|
async publish(envelope, destination) {
|
|
1145
1365
|
assertShortString("Destination (routing key)", destination);
|
|
1146
|
-
const { content, options } =
|
|
1147
|
-
await this.#publishConfirmed(
|
|
1148
|
-
this.#settings.exchange,
|
|
1149
|
-
destination,
|
|
1366
|
+
const { content, options } = this.#settings.encode(envelope);
|
|
1367
|
+
await this.#publishConfirmed({
|
|
1368
|
+
exchange: this.#settings.exchange,
|
|
1369
|
+
routingKey: destination,
|
|
1150
1370
|
content,
|
|
1151
1371
|
options,
|
|
1152
|
-
!this.#settings.allowUnroutable
|
|
1153
|
-
);
|
|
1372
|
+
mandatory: !this.#settings.allowUnroutable
|
|
1373
|
+
});
|
|
1154
1374
|
}
|
|
1155
|
-
async #publishConfirmed(
|
|
1375
|
+
async #publishConfirmed(input) {
|
|
1376
|
+
const { exchange, routingKey, content, options, mandatory } = input;
|
|
1156
1377
|
const publisher = this.#live?.publisher;
|
|
1157
1378
|
const eventId = options.messageId ?? "unknown";
|
|
1158
1379
|
if (publisher === void 0) {
|
|
@@ -1162,18 +1383,21 @@ var RabbitMqTransport = class {
|
|
|
1162
1383
|
}
|
|
1163
1384
|
const returnKey = unroutableKey(options.messageId, routingKey);
|
|
1164
1385
|
await new Promise((resolve, reject) => {
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1386
|
+
const cancelTimeout = internal.schedule(core.systemClock, {
|
|
1387
|
+
run: () => {
|
|
1388
|
+
reject(
|
|
1389
|
+
new RabbitMqUnavailableError(
|
|
1390
|
+
`[RabbitMqTransport](publish) No broker confirm within ${this.#settings.publishTimeoutMs}ms | transport=${this.name} eventId=${eventId} destination=${routingKey}`
|
|
1391
|
+
)
|
|
1392
|
+
);
|
|
1393
|
+
},
|
|
1394
|
+
delayMs: this.#settings.publishTimeoutMs
|
|
1395
|
+
});
|
|
1172
1396
|
const onConfirm = (error) => {
|
|
1173
|
-
|
|
1397
|
+
cancelTimeout();
|
|
1174
1398
|
const returned = takeUnroutable(publisher.unroutable, returnKey);
|
|
1175
1399
|
if (error) {
|
|
1176
|
-
reject(this.#toPublishError(error, eventId, routingKey));
|
|
1400
|
+
reject(this.#toPublishError(error, { eventId, destination: routingKey }));
|
|
1177
1401
|
} else if (returned) {
|
|
1178
1402
|
reject(
|
|
1179
1403
|
new UnroutableMessageError(
|
|
@@ -1193,8 +1417,8 @@ var RabbitMqTransport = class {
|
|
|
1193
1417
|
onConfirm
|
|
1194
1418
|
);
|
|
1195
1419
|
} catch (error) {
|
|
1196
|
-
|
|
1197
|
-
reject(this.#toPublishError(error, eventId, routingKey));
|
|
1420
|
+
cancelTimeout();
|
|
1421
|
+
reject(this.#toPublishError(error, { eventId, destination: routingKey }));
|
|
1198
1422
|
}
|
|
1199
1423
|
});
|
|
1200
1424
|
}
|
|
@@ -1203,7 +1427,8 @@ var RabbitMqTransport = class {
|
|
|
1203
1427
|
* to `onMessage`. Call it once per transport; give each subscription its own
|
|
1204
1428
|
* transport instance, with its own `name` and `consumerGroup`.
|
|
1205
1429
|
*/
|
|
1206
|
-
|
|
1430
|
+
// eslint-disable-next-line relaybus/max-two-params -- the Transport.subscribe signature
|
|
1431
|
+
async subscribe(destinations, onMessage, hooks) {
|
|
1207
1432
|
if (this.#subscription !== void 0) {
|
|
1208
1433
|
throw new core.ConfigurationError(
|
|
1209
1434
|
`Transport '${this.name}' already has a subscription; create another transport for another consumer group`
|
|
@@ -1233,9 +1458,10 @@ var RabbitMqTransport = class {
|
|
|
1233
1458
|
redeliveryMaxAgeMs: this.#settings.redeliveryMaxAgeMs,
|
|
1234
1459
|
ordered: this.#settings.ordered,
|
|
1235
1460
|
deadLetterExchange: this.#settings.deadLetterExchange,
|
|
1236
|
-
republish: (exchange, routingKey, content, options) => this.#publishConfirmed(exchange, routingKey, content, options, true),
|
|
1461
|
+
republish: ({ exchange, routingKey }, { content, options }) => this.#publishConfirmed({ exchange, routingKey, content, options, mandatory: true }),
|
|
1237
1462
|
shutdownTimeoutMs: this.#settings.shutdownTimeoutMs,
|
|
1238
1463
|
onMessage,
|
|
1464
|
+
...hooks === void 0 ? {} : { onInvalidMessage: hooks.onInvalidMessage },
|
|
1239
1465
|
decode: this.#settings.decode,
|
|
1240
1466
|
onConsumerLost: (reason) => this.#supervisor.recover(reason),
|
|
1241
1467
|
logger: this.#settings.logger,
|
|
@@ -1252,21 +1478,29 @@ var RabbitMqTransport = class {
|
|
|
1252
1478
|
/**
|
|
1253
1479
|
* Stops consuming, waits up to `shutdownTimeout` for running handlers
|
|
1254
1480
|
* (messages still being retried go back to the queue), then closes the
|
|
1255
|
-
* channels and the connection.
|
|
1481
|
+
* channels and the connection. Once `options.signal` aborts (the bus shutdown deadline passed),
|
|
1482
|
+
* the connection's socket is destroyed instead of waiting for the broker.
|
|
1256
1483
|
*/
|
|
1257
|
-
async close() {
|
|
1258
|
-
|
|
1259
|
-
|
|
1484
|
+
async close(options = {}) {
|
|
1485
|
+
const { signal } = options;
|
|
1486
|
+
const force = () => this.#supervisor.forceClose();
|
|
1487
|
+
if (signal?.aborted === true) {
|
|
1488
|
+
force();
|
|
1489
|
+
}
|
|
1490
|
+
signal?.addEventListener("abort", force, { once: true });
|
|
1491
|
+
try {
|
|
1492
|
+
this.#closing ??= this.#supervisor.close(
|
|
1493
|
+
() => this.#topologyWork.run(async () => {
|
|
1494
|
+
const live = this.#live;
|
|
1495
|
+
this.#live = void 0;
|
|
1496
|
+
await this.#subscription?.consumer.stop();
|
|
1497
|
+
await live?.publisher.channel.close().catch(() => void 0);
|
|
1498
|
+
})
|
|
1499
|
+
);
|
|
1500
|
+
await this.#closing;
|
|
1501
|
+
} finally {
|
|
1502
|
+
signal?.removeEventListener("abort", force);
|
|
1260
1503
|
}
|
|
1261
|
-
this.#closed = true;
|
|
1262
|
-
await this.#supervisor.close(
|
|
1263
|
-
() => this.#topologyWork.run(async () => {
|
|
1264
|
-
const live = this.#live;
|
|
1265
|
-
this.#live = void 0;
|
|
1266
|
-
await this.#subscription?.consumer.stop();
|
|
1267
|
-
await live?.publisher.channel.close().catch(() => void 0);
|
|
1268
|
-
})
|
|
1269
|
-
);
|
|
1270
1504
|
}
|
|
1271
1505
|
async #establishChannels(connection) {
|
|
1272
1506
|
await this.#topologyWork.run(async () => {
|
|
@@ -1310,7 +1544,7 @@ var RabbitMqTransport = class {
|
|
|
1310
1544
|
this.#live = void 0;
|
|
1311
1545
|
this.#subscription?.consumer.detach();
|
|
1312
1546
|
}
|
|
1313
|
-
#toPublishError(error, eventId, destination) {
|
|
1547
|
+
#toPublishError(error, { eventId, destination }) {
|
|
1314
1548
|
const { message } = core.toErrorInfo(error);
|
|
1315
1549
|
const detail = `transport=${this.name} eventId=${eventId} destination=${destination} error=${message}`;
|
|
1316
1550
|
return message === NACK_MESSAGE ? new RabbitMqPublishRejectedError(
|
|
@@ -1340,44 +1574,27 @@ function takeUnroutable(unroutable, key) {
|
|
|
1340
1574
|
}
|
|
1341
1575
|
return true;
|
|
1342
1576
|
}
|
|
1343
|
-
function fromHeader(name) {
|
|
1344
|
-
if (name.length === 0) {
|
|
1345
|
-
throw new core.ConfigurationError("fromHeader needs a header name");
|
|
1346
|
-
}
|
|
1347
|
-
return (message) => Object.hasOwn(message.headers, name) ? message.headers[name] : void 0;
|
|
1348
|
-
}
|
|
1349
|
-
function fromPayloadField(path) {
|
|
1350
|
-
const segments = path.split(".");
|
|
1351
|
-
if (segments.some((segment) => segment.length === 0)) {
|
|
1352
|
-
throw new core.ConfigurationError(
|
|
1353
|
-
`fromPayloadField needs a dotted path of non-empty names, received '${path}'`
|
|
1354
|
-
);
|
|
1355
|
-
}
|
|
1356
|
-
return (message) => {
|
|
1357
|
-
let value = message.payload;
|
|
1358
|
-
for (const segment of segments) {
|
|
1359
|
-
if (typeof value !== "object" || value === null || !Object.hasOwn(value, segment)) {
|
|
1360
|
-
return void 0;
|
|
1361
|
-
}
|
|
1362
|
-
value = value[segment];
|
|
1363
|
-
}
|
|
1364
|
-
if (typeof value === "string") {
|
|
1365
|
-
return value;
|
|
1366
|
-
}
|
|
1367
|
-
return Number.isSafeInteger(value) ? String(value) : void 0;
|
|
1368
|
-
};
|
|
1369
|
-
}
|
|
1370
1577
|
|
|
1578
|
+
Object.defineProperty(exports, "fromHeader", {
|
|
1579
|
+
enumerable: true,
|
|
1580
|
+
get: function () { return internal.fromHeader; }
|
|
1581
|
+
});
|
|
1582
|
+
Object.defineProperty(exports, "fromPayloadField", {
|
|
1583
|
+
enumerable: true,
|
|
1584
|
+
get: function () { return internal.fromPayloadField; }
|
|
1585
|
+
});
|
|
1371
1586
|
exports.AmqpLimitExceededError = AmqpLimitExceededError;
|
|
1372
1587
|
exports.MalformedMessageError = MalformedMessageError;
|
|
1373
1588
|
exports.RabbitMqPublishRejectedError = RabbitMqPublishRejectedError;
|
|
1374
1589
|
exports.RabbitMqTransport = RabbitMqTransport;
|
|
1375
1590
|
exports.RabbitMqUnavailableError = RabbitMqUnavailableError;
|
|
1591
|
+
exports.RabbitMqUnencodableEventError = RabbitMqUnencodableEventError;
|
|
1376
1592
|
exports.UnroutableMessageError = UnroutableMessageError;
|
|
1377
1593
|
exports.UnserializablePayloadError = UnserializablePayloadError;
|
|
1594
|
+
exports.cloudEventsBinary = cloudEventsBinary;
|
|
1595
|
+
exports.cloudEventsStructured = cloudEventsStructured;
|
|
1378
1596
|
exports.decodeCloudEvent = decodeCloudEvent;
|
|
1379
|
-
exports.
|
|
1380
|
-
exports.fromPayloadField = fromPayloadField;
|
|
1597
|
+
exports.jsonEnvelope = jsonEnvelope;
|
|
1381
1598
|
exports.plainJsonDecoder = plainJsonDecoder;
|
|
1382
1599
|
//# sourceMappingURL=index.cjs.map
|
|
1383
1600
|
//# sourceMappingURL=index.cjs.map
|