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