@relaybus/rabbitmq 0.1.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 +166 -24
- package/dist/index.cjs +806 -319
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +231 -7
- package/dist/index.d.ts +231 -7
- package/dist/index.js +795 -321
- package/dist/index.js.map +1 -1
- package/package.json +22 -9
package/dist/index.js
CHANGED
|
@@ -1,33 +1,47 @@
|
|
|
1
|
-
import { RelayBusError, NonRetryableError,
|
|
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
|
+
}
|
|
103
129
|
}
|
|
104
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
|
+
}
|
|
144
|
+
}
|
|
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,20 +222,512 @@ 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
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/json-payload.ts
|
|
244
|
+
function assertJsonPayload(payload, eventId) {
|
|
245
|
+
visit(payload, { path: "$", ancestors: /* @__PURE__ */ new Set(), eventId });
|
|
246
|
+
}
|
|
247
|
+
function visit(value, {
|
|
248
|
+
path,
|
|
249
|
+
ancestors,
|
|
250
|
+
eventId
|
|
251
|
+
}) {
|
|
252
|
+
switch (typeof value) {
|
|
253
|
+
case "string":
|
|
254
|
+
case "boolean":
|
|
255
|
+
return;
|
|
256
|
+
case "number":
|
|
257
|
+
if (!Number.isFinite(value)) {
|
|
258
|
+
throw refuse(eventId, { path, reason: `is ${value}, which JSON turns into null` });
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
case "object":
|
|
262
|
+
if (value !== null) {
|
|
263
|
+
visitObject(value, { path, ancestors, eventId });
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
default:
|
|
267
|
+
throw refuse(eventId, { path, reason: `is ${typeof value}, which JSON cannot represent` });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function visitObject(value, {
|
|
271
|
+
path,
|
|
272
|
+
ancestors,
|
|
273
|
+
eventId
|
|
274
|
+
}) {
|
|
275
|
+
if (ancestors.has(value)) {
|
|
276
|
+
throw refuse(eventId, { path, reason: "is a circular reference" });
|
|
277
|
+
}
|
|
278
|
+
ancestors.add(value);
|
|
279
|
+
if (Array.isArray(value)) {
|
|
280
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
281
|
+
if (!(index in value)) {
|
|
282
|
+
throw refuse(eventId, {
|
|
283
|
+
path: `${path}[${index}]`,
|
|
284
|
+
reason: "is an array hole, which JSON turns into null"
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
visit(value[index], { path: `${path}[${index}]`, ancestors, eventId });
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
const prototype = Object.getPrototypeOf(value);
|
|
291
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
292
|
+
throw refuse(eventId, {
|
|
293
|
+
path,
|
|
294
|
+
reason: "is not a plain object (class instance, Date, Map or Set)"
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
298
|
+
throw refuse(eventId, { path, reason: "has a symbol key, which JSON drops" });
|
|
299
|
+
}
|
|
300
|
+
for (const key of Object.keys(value)) {
|
|
301
|
+
visit(value[key], {
|
|
302
|
+
path: `${path}.${key}`,
|
|
303
|
+
ancestors,
|
|
304
|
+
eventId
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
ancestors.delete(value);
|
|
309
|
+
}
|
|
310
|
+
function refuse(eventId, { path, reason }) {
|
|
311
|
+
return new UnserializablePayloadError(
|
|
312
|
+
`Event ${eventId} has a payload JSON cannot represent faithfully: ${path} ${reason}`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/wire-format.ts
|
|
317
|
+
var MILLISECONDS_PER_SECOND = 1e3;
|
|
318
|
+
var ATTRIBUTE_PREFIX = "cloudEvents:";
|
|
319
|
+
var EVENT_HEADER_PREFIX = "relaybus:";
|
|
320
|
+
var SPEC_VERSION = "1.0";
|
|
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-"];
|
|
324
|
+
var SHORT_STRING_LIMIT_BYTES = 255;
|
|
325
|
+
var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
|
|
326
|
+
function encodeEnvelope(envelope) {
|
|
327
|
+
assertJsonPayload(envelope.payload, envelope.id);
|
|
328
|
+
assertShortString("Event id", envelope.id);
|
|
329
|
+
const headers = {
|
|
330
|
+
[`${ATTRIBUTE_PREFIX}specversion`]: SPEC_VERSION,
|
|
331
|
+
[`${ATTRIBUTE_PREFIX}id`]: envelope.id,
|
|
332
|
+
[`${ATTRIBUTE_PREFIX}type`]: envelope.type,
|
|
333
|
+
[`${ATTRIBUTE_PREFIX}source`]: envelope.source,
|
|
334
|
+
[`${ATTRIBUTE_PREFIX}time`]: envelope.occurredAt.toISOString(),
|
|
335
|
+
[`${ATTRIBUTE_PREFIX}datacontenttype`]: JSON_CONTENT_TYPE
|
|
336
|
+
};
|
|
337
|
+
if (envelope.key !== null) {
|
|
338
|
+
headers[`${ATTRIBUTE_PREFIX}subject`] = envelope.key;
|
|
339
|
+
}
|
|
340
|
+
if (envelope.causationId !== null) {
|
|
341
|
+
headers[`${ATTRIBUTE_PREFIX}causationid`] = envelope.causationId;
|
|
342
|
+
}
|
|
343
|
+
if (envelope.correlationId !== null) {
|
|
344
|
+
headers[`${ATTRIBUTE_PREFIX}correlationid`] = envelope.correlationId;
|
|
345
|
+
}
|
|
346
|
+
for (const [name, value] of Object.entries(envelope.headers)) {
|
|
347
|
+
assertShortString(
|
|
348
|
+
`Header name '${EVENT_HEADER_PREFIX}${name}'`,
|
|
349
|
+
`${EVENT_HEADER_PREFIX}${name}`
|
|
350
|
+
);
|
|
351
|
+
headers[`${EVENT_HEADER_PREFIX}${name}`] = value;
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
content: Buffer.from(JSON.stringify(envelope.payload), "utf8"),
|
|
355
|
+
options: {
|
|
356
|
+
contentType: JSON_CONTENT_TYPE,
|
|
357
|
+
persistent: true,
|
|
358
|
+
messageId: envelope.id,
|
|
359
|
+
timestamp: Math.floor(envelope.occurredAt.getTime() / MILLISECONDS_PER_SECOND),
|
|
360
|
+
...mirror("type", envelope.type),
|
|
361
|
+
...mirror("appId", envelope.source),
|
|
362
|
+
...envelope.correlationId === null ? {} : mirror("correlationId", envelope.correlationId),
|
|
363
|
+
headers
|
|
364
|
+
}
|
|
365
|
+
};
|
|
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
|
+
}
|
|
404
|
+
function assertShortString(label, value) {
|
|
405
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
406
|
+
if (bytes > SHORT_STRING_LIMIT_BYTES) {
|
|
407
|
+
throw new AmqpLimitExceededError(
|
|
408
|
+
`${label} is ${bytes} bytes, above the AMQP limit of ${SHORT_STRING_LIMIT_BYTES} bytes`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function mirror(name, value) {
|
|
413
|
+
return Buffer.byteLength(value, "utf8") > SHORT_STRING_LIMIT_BYTES ? {} : { [name]: value };
|
|
414
|
+
}
|
|
415
|
+
function decodeEnvelope(message) {
|
|
416
|
+
const { properties } = message;
|
|
417
|
+
const attributes = properties.headers ?? {};
|
|
418
|
+
if (attributes[`${ATTRIBUTE_PREFIX}specversion`] !== SPEC_VERSION) {
|
|
419
|
+
throw new MalformedMessageError(
|
|
420
|
+
`Message is not a CloudEvents ${SPEC_VERSION} event (header ${ATTRIBUTE_PREFIX}specversion)`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
id: required("id", text(attributes[`${ATTRIBUTE_PREFIX}id`]) ?? text(properties.messageId)),
|
|
425
|
+
type: required("type", text(attributes[`${ATTRIBUTE_PREFIX}type`]) ?? text(properties.type)),
|
|
426
|
+
source: required(
|
|
427
|
+
"source",
|
|
428
|
+
text(attributes[`${ATTRIBUTE_PREFIX}source`]) ?? text(properties.appId)
|
|
429
|
+
),
|
|
430
|
+
key: text(attributes[`${ATTRIBUTE_PREFIX}subject`]) ?? null,
|
|
431
|
+
payload: parsePayload(message.content),
|
|
432
|
+
headers: extractEventHeaders(attributes),
|
|
433
|
+
correlationId: text(attributes[`${ATTRIBUTE_PREFIX}correlationid`]) ?? text(properties.correlationId) ?? null,
|
|
434
|
+
causationId: text(attributes[`${ATTRIBUTE_PREFIX}causationid`]) ?? null,
|
|
435
|
+
occurredAt: parseTime(attributes[`${ATTRIBUTE_PREFIX}time`], properties.timestamp)
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function extractEventHeaders(attributes) {
|
|
439
|
+
const headers = {};
|
|
440
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
441
|
+
if (name.startsWith(EVENT_HEADER_PREFIX) && typeof value === "string") {
|
|
442
|
+
Object.defineProperty(headers, name.slice(EVENT_HEADER_PREFIX.length), {
|
|
443
|
+
value,
|
|
444
|
+
enumerable: true,
|
|
445
|
+
writable: true,
|
|
446
|
+
configurable: true
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return headers;
|
|
451
|
+
}
|
|
452
|
+
function parsePayload(content) {
|
|
453
|
+
try {
|
|
454
|
+
return JSON.parse(content.toString("utf8"));
|
|
455
|
+
} catch {
|
|
456
|
+
throw new MalformedMessageError("Message body is not valid JSON");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function parseTime(rawTime, timestampSeconds) {
|
|
460
|
+
if (rawTime === void 0) {
|
|
461
|
+
const fallback = typeof timestampSeconds === "number" ? new Date(timestampSeconds * MILLISECONDS_PER_SECOND) : void 0;
|
|
462
|
+
if (fallback === void 0 || Number.isNaN(fallback.getTime())) {
|
|
463
|
+
throw new MalformedMessageError("Message has no valid time (header cloudEvents:time)");
|
|
464
|
+
}
|
|
465
|
+
return fallback;
|
|
466
|
+
}
|
|
467
|
+
const occurredAt = typeof rawTime === "string" && RFC3339_PATTERN.test(rawTime) ? new Date(rawTime) : void 0;
|
|
468
|
+
if (occurredAt === void 0 || Number.isNaN(occurredAt.getTime())) {
|
|
469
|
+
throw new MalformedMessageError(
|
|
470
|
+
"Message time (header cloudEvents:time) is not an RFC 3339 timestamp"
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
return occurredAt;
|
|
474
|
+
}
|
|
475
|
+
function text(value) {
|
|
476
|
+
return typeof value === "string" ? value : void 0;
|
|
477
|
+
}
|
|
478
|
+
function required(attribute, value) {
|
|
479
|
+
if (value === void 0 || value.length === 0) {
|
|
480
|
+
throw new MalformedMessageError(
|
|
481
|
+
`Message has no ${attribute} (header ${ATTRIBUTE_PREFIX}${attribute})`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
return value;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/inbound-decoders.ts
|
|
488
|
+
var MILLISECONDS_PER_SECOND2 = 1e3;
|
|
489
|
+
var { optionalAttribute, requiredAttribute, assertValidType, assertJsonDataContentType } = createCloudEventReaders((reason) => new MalformedMessageError(reason));
|
|
490
|
+
var STRUCTURED_MEDIA_TYPE = "application/cloudevents+json";
|
|
491
|
+
var strictUtf8 = new TextDecoder("utf-8", { fatal: true });
|
|
492
|
+
var SPEC_VERSION2 = "1.0";
|
|
493
|
+
var SPEC_VERSION_HEADER = "cloudEvents:specversion";
|
|
494
|
+
var INTERNAL_HEADER_PREFIXES = ["x-relaybus-", "x-first-death-", "x-last-death-"];
|
|
495
|
+
var STRUCTURED_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
496
|
+
"specversion",
|
|
497
|
+
"id",
|
|
498
|
+
"type",
|
|
499
|
+
"source",
|
|
500
|
+
"time",
|
|
501
|
+
"subject",
|
|
502
|
+
"datacontenttype",
|
|
503
|
+
"dataschema",
|
|
504
|
+
"data",
|
|
505
|
+
"data_base64",
|
|
506
|
+
"correlationid",
|
|
507
|
+
"causationid"
|
|
508
|
+
]);
|
|
509
|
+
function decodeCloudEvent(message) {
|
|
510
|
+
if (mediaTypeOf(message.properties.contentType) !== STRUCTURED_MEDIA_TYPE) {
|
|
511
|
+
return decodeEnvelope(message);
|
|
512
|
+
}
|
|
513
|
+
const body = parseJsonBody(message.content);
|
|
514
|
+
if (!isJsonObject(body)) {
|
|
515
|
+
throw new MalformedMessageError("A structured CloudEvent must be a JSON object");
|
|
516
|
+
}
|
|
517
|
+
if (body["specversion"] !== SPEC_VERSION2) {
|
|
518
|
+
throw new MalformedMessageError(`specversion must be '${SPEC_VERSION2}'`);
|
|
519
|
+
}
|
|
520
|
+
const type = requiredAttribute(body, "type");
|
|
521
|
+
assertValidType(type, "type");
|
|
522
|
+
assertJsonDataContentType(optionalAttribute(body, "datacontenttype"));
|
|
523
|
+
if (Object.hasOwn(body, "data_base64")) {
|
|
524
|
+
throw new MalformedMessageError(
|
|
525
|
+
"data_base64 is not supported: relaybus payloads are JSON, send them as data"
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
const time = optionalAttribute(body, "time");
|
|
529
|
+
return {
|
|
530
|
+
id: requiredAttribute(body, "id"),
|
|
531
|
+
type,
|
|
532
|
+
source: requiredAttribute(body, "source"),
|
|
533
|
+
key: optionalAttribute(body, "subject") ?? null,
|
|
534
|
+
payload: Object.hasOwn(body, "data") ? body["data"] : null,
|
|
535
|
+
headers: { ...readForeignHeaders(message.properties), ...extensionAttributes(body) },
|
|
536
|
+
correlationId: optionalAttribute(body, "correlationid") ?? null,
|
|
537
|
+
causationId: optionalAttribute(body, "causationid") ?? null,
|
|
538
|
+
occurredAt: time === void 0 ? propertyTimeOrNow(message.properties) : parseTime2(time)
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function plainJsonDecoder(options) {
|
|
542
|
+
if (typeof options.type === "string") {
|
|
543
|
+
assertEventType(options.type);
|
|
544
|
+
}
|
|
545
|
+
return (message) => {
|
|
546
|
+
const { properties } = message;
|
|
547
|
+
if (properties.headers?.[SPEC_VERSION_HEADER] !== void 0 || mediaTypeOf(properties.contentType) === STRUCTURED_MEDIA_TYPE) {
|
|
548
|
+
return decodeCloudEvent(message);
|
|
549
|
+
}
|
|
550
|
+
const plain = {
|
|
551
|
+
routingKey: message.fields.routingKey,
|
|
552
|
+
properties,
|
|
553
|
+
headers: readForeignHeaders(properties),
|
|
554
|
+
payload: parseJsonBody(message.content)
|
|
555
|
+
};
|
|
556
|
+
const type = resolveText(options.type, { message: plain, name: "type" });
|
|
557
|
+
if (type === void 0) {
|
|
558
|
+
throw new MalformedMessageError("Message has no event type: the type option found none");
|
|
559
|
+
}
|
|
560
|
+
assertValidType(type, "The event type");
|
|
561
|
+
const id = resolveText(options.id, { message: plain, name: "id" }) ?? nonEmptyText(properties.messageId);
|
|
562
|
+
if (id === void 0) {
|
|
563
|
+
throw new MalformedMessageError(
|
|
564
|
+
"Message has no id: set the messageId property on the producer or configure the id option"
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
id,
|
|
569
|
+
type,
|
|
570
|
+
source: resolveText(options.source, { message: plain, name: "source" }) ?? defaultSource(message),
|
|
571
|
+
key: resolveText(options.key, { message: plain, name: "key" }) ?? null,
|
|
572
|
+
payload: plain.payload,
|
|
573
|
+
headers: plain.headers,
|
|
574
|
+
correlationId: null,
|
|
575
|
+
causationId: null,
|
|
576
|
+
occurredAt: resolveOccurredAt(options.occurredAt, plain)
|
|
577
|
+
};
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function completeDecodedEvent(decoded, message) {
|
|
581
|
+
if (typeof decoded !== "object" || decoded === null) {
|
|
582
|
+
throw new MalformedMessageError("The decoder must return an event object synchronously");
|
|
583
|
+
}
|
|
584
|
+
if ("then" in decoded) {
|
|
585
|
+
observeRejection(decoded);
|
|
586
|
+
throw new MalformedMessageError("The decoder must return an event object synchronously");
|
|
587
|
+
}
|
|
588
|
+
const event = decoded;
|
|
589
|
+
return {
|
|
590
|
+
id: event.id,
|
|
591
|
+
type: event.type,
|
|
592
|
+
source: event.source ?? defaultSource(message),
|
|
593
|
+
key: event.key ?? null,
|
|
594
|
+
payload: event.payload,
|
|
595
|
+
headers: event.headers ?? {},
|
|
596
|
+
correlationId: event.correlationId ?? null,
|
|
597
|
+
causationId: event.causationId ?? null,
|
|
598
|
+
occurredAt: event.occurredAt ?? propertyTimeOrNow(message.properties)
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function parseJsonBody(content) {
|
|
602
|
+
let text2;
|
|
603
|
+
try {
|
|
604
|
+
text2 = strictUtf8.decode(content);
|
|
605
|
+
} catch {
|
|
606
|
+
throw new MalformedMessageError("Message body is not valid UTF-8");
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
return JSON.parse(text2);
|
|
610
|
+
} catch {
|
|
611
|
+
throw new MalformedMessageError("Message body is not valid JSON");
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function nonEmptyText(value) {
|
|
615
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
616
|
+
}
|
|
617
|
+
function defaultSource(message) {
|
|
618
|
+
return nonEmptyText(message.properties.appId) ?? message.fields.routingKey;
|
|
619
|
+
}
|
|
620
|
+
function parseTime2(text2) {
|
|
621
|
+
const time = new Date(text2);
|
|
622
|
+
if (!RFC3339_PATTERN.test(text2) || Number.isNaN(time.getTime())) {
|
|
623
|
+
throw new MalformedMessageError(`time is not an RFC 3339 timestamp: '${text2}'`);
|
|
624
|
+
}
|
|
625
|
+
return time;
|
|
626
|
+
}
|
|
627
|
+
function propertyTimeOrNow(properties) {
|
|
628
|
+
const { timestamp } = properties;
|
|
629
|
+
return typeof timestamp === "number" && Number.isFinite(timestamp) ? new Date(timestamp * MILLISECONDS_PER_SECOND2) : /* @__PURE__ */ new Date();
|
|
630
|
+
}
|
|
631
|
+
function extensionAttributes(body) {
|
|
632
|
+
return Object.fromEntries(
|
|
633
|
+
Object.entries(body).filter(
|
|
634
|
+
(entry) => !STRUCTURED_ATTRIBUTES.has(entry[0]) && typeof entry[1] === "string"
|
|
635
|
+
)
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
function readForeignHeaders(properties) {
|
|
639
|
+
return Object.fromEntries(
|
|
640
|
+
Object.entries(properties.headers ?? {}).filter(
|
|
641
|
+
(entry) => typeof entry[1] === "string" && !INTERNAL_HEADER_PREFIXES.some((prefix) => entry[0].startsWith(prefix))
|
|
642
|
+
)
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
function resolveText(option, { message, name }) {
|
|
646
|
+
const value = typeof option === "function" ? option(message) : option;
|
|
647
|
+
if (value === void 0 || value === null) {
|
|
648
|
+
return void 0;
|
|
649
|
+
}
|
|
650
|
+
if (typeof value !== "string") {
|
|
651
|
+
observeRejection(value);
|
|
652
|
+
throw new MalformedMessageError(
|
|
653
|
+
`The ${name} option returned ${typeof value}, expected a string`
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
return value.length === 0 ? void 0 : value;
|
|
657
|
+
}
|
|
658
|
+
function resolveOccurredAt(option, message) {
|
|
659
|
+
const value = option?.(message);
|
|
660
|
+
if (value === void 0) {
|
|
661
|
+
return propertyTimeOrNow(message.properties);
|
|
662
|
+
}
|
|
663
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
664
|
+
observeRejection(value);
|
|
665
|
+
throw new MalformedMessageError("The occurredAt option returned no valid Date");
|
|
666
|
+
}
|
|
667
|
+
return value;
|
|
668
|
+
}
|
|
669
|
+
function observeRejection(value) {
|
|
670
|
+
if (typeof value === "object" && value !== null && "then" in value) {
|
|
671
|
+
Promise.resolve(value).catch(() => void 0);
|
|
672
|
+
}
|
|
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
|
+
}
|
|
181
725
|
var REDELIVERIES_HEADER = "x-relaybus-redeliveries";
|
|
182
726
|
var FIRST_FAILURE_HEADER = "x-relaybus-first-failure-at";
|
|
183
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";
|
|
730
|
+
var ORIGINAL_ROUTING_KEY_HEADER = "x-relaybus-routing-key";
|
|
184
731
|
var RELAYBUS_STATE_HEADERS = [REDELIVERIES_HEADER, FIRST_FAILURE_HEADER];
|
|
185
732
|
var BROKER_COUNTER_HEADERS = ["x-delivery-count", "x-acquired-count"];
|
|
186
733
|
var MAXIMUM_LADDER_TIERS = 16;
|
|
@@ -204,7 +751,7 @@ function buildRetryLadder(policy) {
|
|
|
204
751
|
delay *= policy.factor;
|
|
205
752
|
}
|
|
206
753
|
}
|
|
207
|
-
function chooseRetryTier(ladder, delayMs, random) {
|
|
754
|
+
function chooseRetryTier(ladder, { delayMs, random }) {
|
|
208
755
|
const upperIndex = ladder.findIndex((tier) => tier >= delayMs);
|
|
209
756
|
if (upperIndex === -1) {
|
|
210
757
|
return ladder.at(-1);
|
|
@@ -217,7 +764,7 @@ function chooseRetryTier(ladder, delayMs, random) {
|
|
|
217
764
|
return random() < (delayMs - lower) / (upper - lower) ? upper : lower;
|
|
218
765
|
}
|
|
219
766
|
function readCounterHeader(message, name) {
|
|
220
|
-
const value = message.properties.headers[name];
|
|
767
|
+
const value = message.properties.headers?.[name];
|
|
221
768
|
const parsed = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : value;
|
|
222
769
|
return typeof parsed === "number" && Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
|
|
223
770
|
}
|
|
@@ -227,6 +774,8 @@ function copyForRepublish(message, headerChanges) {
|
|
|
227
774
|
for (const name of [
|
|
228
775
|
...RELAYBUS_STATE_HEADERS,
|
|
229
776
|
DEAD_LETTER_REASON_HEADER,
|
|
777
|
+
DEAD_LETTER_ERROR_NAME_HEADER,
|
|
778
|
+
DEAD_LETTER_ERROR_CODE_HEADER,
|
|
230
779
|
...BROKER_COUNTER_HEADERS
|
|
231
780
|
]) {
|
|
232
781
|
delete headers[name];
|
|
@@ -246,8 +795,18 @@ function copyForRepublish(message, headerChanges) {
|
|
|
246
795
|
}
|
|
247
796
|
};
|
|
248
797
|
}
|
|
798
|
+
function originalRoutingKey(message) {
|
|
799
|
+
const original = message.properties.headers?.[ORIGINAL_ROUTING_KEY_HEADER];
|
|
800
|
+
return typeof original === "string" ? original : message.fields.routingKey;
|
|
801
|
+
}
|
|
802
|
+
function withOriginalRoutingKey(message) {
|
|
803
|
+
const routingKey = originalRoutingKey(message);
|
|
804
|
+
return routingKey === message.fields.routingKey ? message : { ...message, fields: { ...message.fields, routingKey } };
|
|
805
|
+
}
|
|
249
806
|
|
|
250
807
|
// src/options.ts
|
|
808
|
+
var DEFAULT_PREFETCH = 50;
|
|
809
|
+
var DEFAULT_DELIVERY_LIMIT = 10;
|
|
251
810
|
var DEFAULT_HANDLER_RETRY = {
|
|
252
811
|
attempts: 3,
|
|
253
812
|
initialDelayMs: 200,
|
|
@@ -274,6 +833,14 @@ var DEFAULT_RECONNECT = {
|
|
|
274
833
|
retryIf: void 0
|
|
275
834
|
};
|
|
276
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) {
|
|
277
844
|
const exchange = nonEmpty(options.exchange ?? "relaybus", "exchange");
|
|
278
845
|
const failOnConnectError = options.failOnConnectError ?? true;
|
|
279
846
|
if (!failOnConnectError && options.reconnect === false) {
|
|
@@ -282,11 +849,17 @@ function resolveSettings(options) {
|
|
|
282
849
|
);
|
|
283
850
|
}
|
|
284
851
|
const ordered = options.ordered ?? false;
|
|
285
|
-
const prefetch =
|
|
852
|
+
const prefetch = assertPositiveInteger(
|
|
853
|
+
options.prefetch ?? (ordered ? 1 : DEFAULT_PREFETCH),
|
|
854
|
+
"prefetch"
|
|
855
|
+
);
|
|
286
856
|
if (ordered && prefetch !== 1) {
|
|
287
857
|
throw new ConfigurationError(`ordered consumption needs prefetch 1, received ${prefetch}`);
|
|
288
858
|
}
|
|
289
859
|
const redelivery = createRetryPolicy(options.redelivery, DEFAULT_REDELIVERY);
|
|
860
|
+
return { exchange, failOnConnectError, ordered, prefetch, redelivery };
|
|
861
|
+
}
|
|
862
|
+
function resolveTopology(options, { exchange, prefetch }) {
|
|
290
863
|
return {
|
|
291
864
|
name: nonEmpty(options.name ?? "rabbitmq", "name"),
|
|
292
865
|
connect: resolveConnect(options),
|
|
@@ -300,7 +873,14 @@ function resolveSettings(options) {
|
|
|
300
873
|
"deadLetterExchange"
|
|
301
874
|
),
|
|
302
875
|
deadLetterQueue: optionalNonEmpty(options.deadLetterQueue, "deadLetterQueue"),
|
|
303
|
-
deliveryLimit:
|
|
876
|
+
deliveryLimit: assertPositiveInteger(
|
|
877
|
+
options.deliveryLimit ?? DEFAULT_DELIVERY_LIMIT,
|
|
878
|
+
"deliveryLimit"
|
|
879
|
+
)
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
function resolveRetries(options, { ordered, redelivery }) {
|
|
883
|
+
return {
|
|
304
884
|
handlerRetry: createRetryPolicy(options.handlerRetry, DEFAULT_HANDLER_RETRY),
|
|
305
885
|
redelivery,
|
|
306
886
|
retryLadder: ordered ? [] : buildRetryLadder(redelivery),
|
|
@@ -308,12 +888,17 @@ function resolveSettings(options) {
|
|
|
308
888
|
options.redelivery?.maxAge ?? DEFAULT_REDELIVERY_MAX_AGE,
|
|
309
889
|
"redelivery.maxAge"
|
|
310
890
|
),
|
|
311
|
-
ordered
|
|
891
|
+
ordered
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
function resolveRuntime(options, { failOnConnectError }) {
|
|
895
|
+
return {
|
|
312
896
|
reconnect: resolveReconnect(options.reconnect),
|
|
313
897
|
failOnConnectError,
|
|
314
898
|
allowUnroutable: options.allowUnroutable ?? false,
|
|
315
899
|
publishTimeoutMs: positiveDuration(options.publishTimeout ?? "30s", "publishTimeout"),
|
|
316
900
|
shutdownTimeoutMs: toMilliseconds(options.shutdownTimeout ?? "30s", "shutdownTimeout"),
|
|
901
|
+
...resolveCodec(options),
|
|
317
902
|
logger: options.logger ?? console,
|
|
318
903
|
random: options.random ?? Math.random
|
|
319
904
|
};
|
|
@@ -329,6 +914,22 @@ function resolveQueueName(settings) {
|
|
|
329
914
|
}
|
|
330
915
|
return `${settings.queuePrefix}.${settings.consumerGroup}`;
|
|
331
916
|
}
|
|
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 };
|
|
927
|
+
}
|
|
928
|
+
return {
|
|
929
|
+
encode: chosen.encode,
|
|
930
|
+
decode: (message) => completeDecodedEvent(chosen.decode(message), message)
|
|
931
|
+
};
|
|
932
|
+
}
|
|
332
933
|
function resolveConnect(options) {
|
|
333
934
|
const { url, connect } = options;
|
|
334
935
|
if (url === void 0 === (connect === void 0)) {
|
|
@@ -337,9 +938,10 @@ function resolveConnect(options) {
|
|
|
337
938
|
if (connect !== void 0) {
|
|
338
939
|
return connect;
|
|
339
940
|
}
|
|
340
|
-
return async () => {
|
|
941
|
+
return async ({ signal }) => {
|
|
341
942
|
const amqplib = await import('amqplib');
|
|
342
|
-
|
|
943
|
+
const socketOptions = { signal };
|
|
944
|
+
return amqplib.connect(url, socketOptions);
|
|
343
945
|
};
|
|
344
946
|
}
|
|
345
947
|
function resolveReconnect(reconnect) {
|
|
@@ -357,12 +959,6 @@ function nonEmpty(value, name) {
|
|
|
357
959
|
function optionalNonEmpty(value, name) {
|
|
358
960
|
return value === void 0 ? void 0 : nonEmpty(value, name);
|
|
359
961
|
}
|
|
360
|
-
function positiveInteger(value, name) {
|
|
361
|
-
if (!Number.isInteger(value) || value < 1) {
|
|
362
|
-
throw new ConfigurationError(`${name} must be a positive integer, received ${value}`);
|
|
363
|
-
}
|
|
364
|
-
return value;
|
|
365
|
-
}
|
|
366
962
|
function positiveDuration(value, name) {
|
|
367
963
|
const milliseconds = toMilliseconds(value, name);
|
|
368
964
|
if (milliseconds === 0) {
|
|
@@ -416,192 +1012,6 @@ async function assertConsumerTopology(channel, topology) {
|
|
|
416
1012
|
}
|
|
417
1013
|
}
|
|
418
1014
|
|
|
419
|
-
// src/json-payload.ts
|
|
420
|
-
function assertJsonPayload(payload, eventId) {
|
|
421
|
-
visit(payload, "$", /* @__PURE__ */ new Set(), eventId);
|
|
422
|
-
}
|
|
423
|
-
function visit(value, path, ancestors, eventId) {
|
|
424
|
-
switch (typeof value) {
|
|
425
|
-
case "string":
|
|
426
|
-
case "boolean":
|
|
427
|
-
return;
|
|
428
|
-
case "number":
|
|
429
|
-
if (!Number.isFinite(value)) {
|
|
430
|
-
throw refuse(eventId, path, `is ${value}, which JSON turns into null`);
|
|
431
|
-
}
|
|
432
|
-
return;
|
|
433
|
-
case "object":
|
|
434
|
-
if (value !== null) {
|
|
435
|
-
visitObject(value, path, ancestors, eventId);
|
|
436
|
-
}
|
|
437
|
-
return;
|
|
438
|
-
default:
|
|
439
|
-
throw refuse(eventId, path, `is ${typeof value}, which JSON cannot represent`);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
function visitObject(value, path, ancestors, eventId) {
|
|
443
|
-
if (ancestors.has(value)) {
|
|
444
|
-
throw refuse(eventId, path, "is a circular reference");
|
|
445
|
-
}
|
|
446
|
-
ancestors.add(value);
|
|
447
|
-
if (Array.isArray(value)) {
|
|
448
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
449
|
-
if (!(index in value)) {
|
|
450
|
-
throw refuse(eventId, `${path}[${index}]`, "is an array hole, which JSON turns into null");
|
|
451
|
-
}
|
|
452
|
-
visit(value[index], `${path}[${index}]`, ancestors, eventId);
|
|
453
|
-
}
|
|
454
|
-
} else {
|
|
455
|
-
const prototype = Object.getPrototypeOf(value);
|
|
456
|
-
if (prototype !== Object.prototype && prototype !== null) {
|
|
457
|
-
throw refuse(eventId, path, "is not a plain object (class instance, Date, Map or Set)");
|
|
458
|
-
}
|
|
459
|
-
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
460
|
-
throw refuse(eventId, path, "has a symbol key, which JSON drops");
|
|
461
|
-
}
|
|
462
|
-
for (const key of Object.keys(value)) {
|
|
463
|
-
visit(value[key], `${path}.${key}`, ancestors, eventId);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
ancestors.delete(value);
|
|
467
|
-
}
|
|
468
|
-
function refuse(eventId, path, reason) {
|
|
469
|
-
return new UnserializablePayloadError(
|
|
470
|
-
`Event ${eventId} has a payload JSON cannot represent faithfully: ${path} ${reason}`
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// src/wire-format.ts
|
|
475
|
-
var ATTRIBUTE_PREFIX = "cloudEvents:";
|
|
476
|
-
var EVENT_HEADER_PREFIX = "relaybus:";
|
|
477
|
-
var SPEC_VERSION = "1.0";
|
|
478
|
-
var JSON_CONTENT_TYPE = "application/json";
|
|
479
|
-
var SHORT_STRING_LIMIT_BYTES = 255;
|
|
480
|
-
var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
|
|
481
|
-
function encodeEnvelope(envelope) {
|
|
482
|
-
assertJsonPayload(envelope.payload, envelope.id);
|
|
483
|
-
assertShortString("Event id", envelope.id);
|
|
484
|
-
const headers = {
|
|
485
|
-
[`${ATTRIBUTE_PREFIX}specversion`]: SPEC_VERSION,
|
|
486
|
-
[`${ATTRIBUTE_PREFIX}id`]: envelope.id,
|
|
487
|
-
[`${ATTRIBUTE_PREFIX}type`]: envelope.type,
|
|
488
|
-
[`${ATTRIBUTE_PREFIX}source`]: envelope.source,
|
|
489
|
-
[`${ATTRIBUTE_PREFIX}time`]: envelope.occurredAt.toISOString(),
|
|
490
|
-
[`${ATTRIBUTE_PREFIX}datacontenttype`]: JSON_CONTENT_TYPE
|
|
491
|
-
};
|
|
492
|
-
if (envelope.key !== null) {
|
|
493
|
-
headers[`${ATTRIBUTE_PREFIX}subject`] = envelope.key;
|
|
494
|
-
}
|
|
495
|
-
if (envelope.causationId !== null) {
|
|
496
|
-
headers[`${ATTRIBUTE_PREFIX}causationid`] = envelope.causationId;
|
|
497
|
-
}
|
|
498
|
-
if (envelope.correlationId !== null) {
|
|
499
|
-
headers[`${ATTRIBUTE_PREFIX}correlationid`] = envelope.correlationId;
|
|
500
|
-
}
|
|
501
|
-
for (const [name, value] of Object.entries(envelope.headers)) {
|
|
502
|
-
assertShortString(
|
|
503
|
-
`Header name '${EVENT_HEADER_PREFIX}${name}'`,
|
|
504
|
-
`${EVENT_HEADER_PREFIX}${name}`
|
|
505
|
-
);
|
|
506
|
-
headers[`${EVENT_HEADER_PREFIX}${name}`] = value;
|
|
507
|
-
}
|
|
508
|
-
return {
|
|
509
|
-
content: Buffer.from(JSON.stringify(envelope.payload), "utf8"),
|
|
510
|
-
options: {
|
|
511
|
-
contentType: JSON_CONTENT_TYPE,
|
|
512
|
-
persistent: true,
|
|
513
|
-
messageId: envelope.id,
|
|
514
|
-
timestamp: Math.floor(envelope.occurredAt.getTime() / 1e3),
|
|
515
|
-
...mirror("type", envelope.type),
|
|
516
|
-
...mirror("appId", envelope.source),
|
|
517
|
-
...envelope.correlationId === null ? {} : mirror("correlationId", envelope.correlationId),
|
|
518
|
-
headers
|
|
519
|
-
}
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
function assertShortString(label, value) {
|
|
523
|
-
const bytes = Buffer.byteLength(value, "utf8");
|
|
524
|
-
if (bytes > SHORT_STRING_LIMIT_BYTES) {
|
|
525
|
-
throw new AmqpLimitExceededError(
|
|
526
|
-
`${label} is ${bytes} bytes, above the AMQP limit of ${SHORT_STRING_LIMIT_BYTES} bytes`
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
function mirror(name, value) {
|
|
531
|
-
return Buffer.byteLength(value, "utf8") > SHORT_STRING_LIMIT_BYTES ? {} : { [name]: value };
|
|
532
|
-
}
|
|
533
|
-
function decodeEnvelope(message) {
|
|
534
|
-
const { properties } = message;
|
|
535
|
-
const attributes = properties.headers ?? {};
|
|
536
|
-
if (attributes[`${ATTRIBUTE_PREFIX}specversion`] !== SPEC_VERSION) {
|
|
537
|
-
throw new MalformedMessageError(
|
|
538
|
-
`Message is not a CloudEvents ${SPEC_VERSION} event (header ${ATTRIBUTE_PREFIX}specversion)`
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
return {
|
|
542
|
-
id: required("id", text(attributes[`${ATTRIBUTE_PREFIX}id`]) ?? text(properties.messageId)),
|
|
543
|
-
type: required("type", text(attributes[`${ATTRIBUTE_PREFIX}type`]) ?? text(properties.type)),
|
|
544
|
-
source: required(
|
|
545
|
-
"source",
|
|
546
|
-
text(attributes[`${ATTRIBUTE_PREFIX}source`]) ?? text(properties.appId)
|
|
547
|
-
),
|
|
548
|
-
key: text(attributes[`${ATTRIBUTE_PREFIX}subject`]) ?? null,
|
|
549
|
-
payload: parsePayload(message.content),
|
|
550
|
-
headers: extractEventHeaders(attributes),
|
|
551
|
-
correlationId: text(attributes[`${ATTRIBUTE_PREFIX}correlationid`]) ?? text(properties.correlationId) ?? null,
|
|
552
|
-
causationId: text(attributes[`${ATTRIBUTE_PREFIX}causationid`]) ?? null,
|
|
553
|
-
occurredAt: parseTime(attributes[`${ATTRIBUTE_PREFIX}time`], properties.timestamp)
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
function extractEventHeaders(attributes) {
|
|
557
|
-
const headers = {};
|
|
558
|
-
for (const [name, value] of Object.entries(attributes)) {
|
|
559
|
-
if (name.startsWith(EVENT_HEADER_PREFIX) && typeof value === "string") {
|
|
560
|
-
Object.defineProperty(headers, name.slice(EVENT_HEADER_PREFIX.length), {
|
|
561
|
-
value,
|
|
562
|
-
enumerable: true,
|
|
563
|
-
writable: true,
|
|
564
|
-
configurable: true
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
return headers;
|
|
569
|
-
}
|
|
570
|
-
function parsePayload(content) {
|
|
571
|
-
try {
|
|
572
|
-
return JSON.parse(content.toString("utf8"));
|
|
573
|
-
} catch {
|
|
574
|
-
throw new MalformedMessageError("Message body is not valid JSON");
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
function parseTime(rawTime, timestampSeconds) {
|
|
578
|
-
if (rawTime === void 0) {
|
|
579
|
-
const fallback = typeof timestampSeconds === "number" ? new Date(timestampSeconds * 1e3) : void 0;
|
|
580
|
-
if (fallback === void 0 || Number.isNaN(fallback.getTime())) {
|
|
581
|
-
throw new MalformedMessageError("Message has no valid time (header cloudEvents:time)");
|
|
582
|
-
}
|
|
583
|
-
return fallback;
|
|
584
|
-
}
|
|
585
|
-
const occurredAt = typeof rawTime === "string" && RFC3339_PATTERN.test(rawTime) ? new Date(rawTime) : void 0;
|
|
586
|
-
if (occurredAt === void 0 || Number.isNaN(occurredAt.getTime())) {
|
|
587
|
-
throw new MalformedMessageError(
|
|
588
|
-
"Message time (header cloudEvents:time) is not an RFC 3339 timestamp"
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
return occurredAt;
|
|
592
|
-
}
|
|
593
|
-
function text(value) {
|
|
594
|
-
return typeof value === "string" ? value : void 0;
|
|
595
|
-
}
|
|
596
|
-
function required(attribute, value) {
|
|
597
|
-
if (value === void 0 || value.length === 0) {
|
|
598
|
-
throw new MalformedMessageError(
|
|
599
|
-
`Message has no ${attribute} (header ${ATTRIBUTE_PREFIX}${attribute})`
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
return value;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
1015
|
// src/queue-consumer.ts
|
|
606
1016
|
var QueueConsumer = class {
|
|
607
1017
|
#settings;
|
|
@@ -697,74 +1107,115 @@ var QueueConsumer = class {
|
|
|
697
1107
|
this.#inFlight.add(task);
|
|
698
1108
|
}
|
|
699
1109
|
async #process(channel, message) {
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
try {
|
|
703
|
-
envelope = decodeEnvelope(message);
|
|
704
|
-
} catch (error) {
|
|
705
|
-
this.#settings.logger.error(
|
|
706
|
-
`[QueueConsumer](process) Dead-lettering malformed message | queue=${queue} routingKey=${message.fields.routingKey} error=${toErrorInfo(error).message}`
|
|
707
|
-
);
|
|
708
|
-
await this.#deadLetter(channel, message, "malformed message");
|
|
1110
|
+
const decoded = await this.#decodeOrDeadLetter(channel, message);
|
|
1111
|
+
if (decoded === void 0) {
|
|
709
1112
|
return;
|
|
710
1113
|
}
|
|
711
|
-
|
|
712
|
-
|
|
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
|
+
};
|
|
713
1122
|
for (let attempt = 1; ; attempt += 1) {
|
|
714
1123
|
try {
|
|
715
|
-
await this.#settings.onMessage(envelope);
|
|
716
|
-
this.#settle(channel, message, "ack");
|
|
1124
|
+
await this.#settings.onMessage(envelope, { destination: message.fields.routingKey });
|
|
1125
|
+
this.#settle(channel, { message, settlement: "ack" });
|
|
717
1126
|
return;
|
|
718
1127
|
} catch (error) {
|
|
719
|
-
|
|
720
|
-
if (!isRetryableError(handlerRetry, error)) {
|
|
721
|
-
this.#settings.logger.error(
|
|
722
|
-
`[QueueConsumer](process) Dead-lettering message the handler cannot process | ${failure}`
|
|
723
|
-
);
|
|
724
|
-
await this.#deadLetter(channel, message, "non-retryable handler error");
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
if (attempt < handlerRetry.attempts) {
|
|
728
|
-
const delayMs2 = computeRetryDelay(handlerRetry, attempt, this.#settings.random);
|
|
729
|
-
this.#settings.logger.warn(
|
|
730
|
-
`[QueueConsumer](process) Handler failed, retrying | ${failure} delayMs=${delayMs2}`
|
|
731
|
-
);
|
|
732
|
-
if (await this.#interrupted(delayMs2)) {
|
|
733
|
-
this.#settle(channel, message, "requeue");
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
continue;
|
|
737
|
-
}
|
|
738
|
-
redeliveries += 1;
|
|
739
|
-
const now = Date.now();
|
|
740
|
-
firstFailureAt ??= now;
|
|
741
|
-
const delayMs = this.#redeliveryDelay(redeliveries);
|
|
742
|
-
if (now + delayMs - firstFailureAt > this.#settings.redeliveryMaxAgeMs) {
|
|
743
|
-
this.#settings.logger.error(
|
|
744
|
-
`[QueueConsumer](process) Dead-lettering message that kept failing | ${failure} redeliveries=${redeliveries - 1} failingForMs=${now - firstFailureAt} maxAgeMs=${this.#settings.redeliveryMaxAgeMs}`
|
|
745
|
-
);
|
|
746
|
-
await this.#deadLetter(channel, message, "redelivery window exhausted");
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
this.#settings.logger.warn(
|
|
750
|
-
`[QueueConsumer](process) Handler attempts spent, retrying later | ${failure} redeliveries=${redeliveries} delayMs=${delayMs}`
|
|
751
|
-
);
|
|
752
|
-
if (!this.#settings.ordered) {
|
|
753
|
-
await this.#redeliverLater(channel, message, delayMs, redeliveries, firstFailureAt);
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
if (await this.#interrupted(delayMs)) {
|
|
757
|
-
this.#settle(channel, message, "requeue");
|
|
1128
|
+
if (await this.#afterFailure(delivery, { attempt, error }) === "settled") {
|
|
758
1129
|
return;
|
|
759
1130
|
}
|
|
760
1131
|
}
|
|
761
1132
|
}
|
|
762
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
|
+
}
|
|
763
1214
|
/** The backoff before the next delayed redelivery: a retry queue's TTL, or an exact wait when ordered. */
|
|
764
1215
|
#redeliveryDelay(redeliveries) {
|
|
765
1216
|
const { redelivery, retryLadder, ordered, random } = this.#settings;
|
|
766
1217
|
const delayMs = computeRetryDelay(redelivery, redeliveries, random);
|
|
767
|
-
return ordered ? delayMs : chooseRetryTier(retryLadder, delayMs, random);
|
|
1218
|
+
return ordered ? delayMs : chooseRetryTier(retryLadder, { delayMs, random });
|
|
768
1219
|
}
|
|
769
1220
|
/** Sleeps; true when the consumer is stopping, so the caller must give the message back. */
|
|
770
1221
|
async #interrupted(delayMs) {
|
|
@@ -777,27 +1228,27 @@ var QueueConsumer = class {
|
|
|
777
1228
|
* the original is requeued after the same delay, so it is neither lost nor
|
|
778
1229
|
* hot-looped.
|
|
779
1230
|
*/
|
|
780
|
-
async #redeliverLater(channel,
|
|
1231
|
+
async #redeliverLater(channel, input) {
|
|
1232
|
+
const { message, delayMs, redeliveries, firstFailureAt } = input;
|
|
781
1233
|
const { content, options } = copyForRepublish(message, {
|
|
782
1234
|
[REDELIVERIES_HEADER]: String(redeliveries),
|
|
783
|
-
[FIRST_FAILURE_HEADER]: String(firstFailureAt)
|
|
1235
|
+
[FIRST_FAILURE_HEADER]: String(firstFailureAt),
|
|
1236
|
+
[ORIGINAL_ROUTING_KEY_HEADER]: originalRoutingKey(message)
|
|
784
1237
|
});
|
|
785
1238
|
try {
|
|
786
1239
|
await this.#settings.republish(
|
|
787
|
-
"",
|
|
788
|
-
|
|
789
|
-
content,
|
|
790
|
-
options
|
|
1240
|
+
{ exchange: "", routingKey: retryQueueName(this.#settings.queue, delayMs) },
|
|
1241
|
+
{ content, options }
|
|
791
1242
|
);
|
|
792
1243
|
} catch (error) {
|
|
793
1244
|
this.#settings.logger.warn(
|
|
794
1245
|
`[QueueConsumer](redeliverLater) Could not park the message in the retry queue, requeueing it | queue=${this.#settings.queue} delayMs=${delayMs} error=${toErrorInfo(error).message}`
|
|
795
1246
|
);
|
|
796
1247
|
await this.#interrupted(delayMs);
|
|
797
|
-
this.#settle(channel, message, "requeue");
|
|
1248
|
+
this.#settle(channel, { message, settlement: "requeue" });
|
|
798
1249
|
return;
|
|
799
1250
|
}
|
|
800
|
-
this.#settle(channel, message, "ack");
|
|
1251
|
+
this.#settle(channel, { message, settlement: "ack" });
|
|
801
1252
|
}
|
|
802
1253
|
/**
|
|
803
1254
|
* Moves the message to the dead-letter queue through a confirmed copy that
|
|
@@ -805,27 +1256,29 @@ var QueueConsumer = class {
|
|
|
805
1256
|
* message is rejected without requeue, which makes the broker dead-letter it
|
|
806
1257
|
* (at-least-once), so it still ends up in the dead-letter queue.
|
|
807
1258
|
*/
|
|
808
|
-
async #deadLetter(channel,
|
|
1259
|
+
async #deadLetter(channel, input) {
|
|
1260
|
+
const { message, reason, error } = input;
|
|
1261
|
+
const { name, code } = toErrorInfo(error);
|
|
809
1262
|
const { content, options } = copyForRepublish(message, {
|
|
810
|
-
[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 }
|
|
811
1266
|
});
|
|
812
1267
|
try {
|
|
813
1268
|
await this.#settings.republish(
|
|
814
|
-
this.#settings.deadLetterExchange,
|
|
815
|
-
|
|
816
|
-
content,
|
|
817
|
-
options
|
|
1269
|
+
{ exchange: this.#settings.deadLetterExchange, routingKey: this.#settings.queue },
|
|
1270
|
+
{ content, options }
|
|
818
1271
|
);
|
|
819
|
-
} catch (
|
|
1272
|
+
} catch (error2) {
|
|
820
1273
|
this.#settings.logger.warn(
|
|
821
|
-
`[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}`
|
|
822
1275
|
);
|
|
823
|
-
this.#settle(channel, message, "dead-letter");
|
|
1276
|
+
this.#settle(channel, { message, settlement: "dead-letter" });
|
|
824
1277
|
return;
|
|
825
1278
|
}
|
|
826
|
-
this.#settle(channel, message, "ack");
|
|
1279
|
+
this.#settle(channel, { message, settlement: "ack" });
|
|
827
1280
|
}
|
|
828
|
-
#settle(channel, message, settlement) {
|
|
1281
|
+
#settle(channel, { message, settlement }) {
|
|
829
1282
|
try {
|
|
830
1283
|
if (settlement === "ack") {
|
|
831
1284
|
channel.ack(message);
|
|
@@ -839,6 +1292,10 @@ var QueueConsumer = class {
|
|
|
839
1292
|
}
|
|
840
1293
|
}
|
|
841
1294
|
};
|
|
1295
|
+
function describeError(error) {
|
|
1296
|
+
const { message, name, code } = toErrorInfo(error);
|
|
1297
|
+
return `error=${message} errorName=${name}${code === void 0 ? "" : ` code=${code}`}`;
|
|
1298
|
+
}
|
|
842
1299
|
|
|
843
1300
|
// src/serial-queue.ts
|
|
844
1301
|
var SerialQueue = class {
|
|
@@ -854,13 +1311,14 @@ var SerialQueue = class {
|
|
|
854
1311
|
var NACK_MESSAGE = "message nacked";
|
|
855
1312
|
var RabbitMqTransport = class {
|
|
856
1313
|
name;
|
|
1314
|
+
system = "rabbitmq";
|
|
857
1315
|
#settings;
|
|
858
1316
|
#supervisor;
|
|
859
1317
|
/** Serializes topology work so a subscription never races a reconnect. */
|
|
860
1318
|
#topologyWork = new SerialQueue();
|
|
861
1319
|
#live;
|
|
862
1320
|
#subscription;
|
|
863
|
-
#
|
|
1321
|
+
#closing;
|
|
864
1322
|
constructor(options) {
|
|
865
1323
|
this.#settings = resolveSettings(options);
|
|
866
1324
|
this.name = this.#settings.name;
|
|
@@ -900,19 +1358,21 @@ var RabbitMqTransport = class {
|
|
|
900
1358
|
* @throws {RabbitMqUnavailableError} not connected, channel lost, or confirm timed out
|
|
901
1359
|
* @throws {RabbitMqPublishRejectedError} the broker nacked the message
|
|
902
1360
|
* @throws {UnroutableMessageError} no queue is bound to `destination` (unless `allowUnroutable`)
|
|
1361
|
+
* @throws whatever the `codec`'s `encode` throws, such as {@link RabbitMqUnencodableEventError}
|
|
903
1362
|
*/
|
|
904
1363
|
async publish(envelope, destination) {
|
|
905
1364
|
assertShortString("Destination (routing key)", destination);
|
|
906
|
-
const { content, options } =
|
|
907
|
-
await this.#publishConfirmed(
|
|
908
|
-
this.#settings.exchange,
|
|
909
|
-
destination,
|
|
1365
|
+
const { content, options } = this.#settings.encode(envelope);
|
|
1366
|
+
await this.#publishConfirmed({
|
|
1367
|
+
exchange: this.#settings.exchange,
|
|
1368
|
+
routingKey: destination,
|
|
910
1369
|
content,
|
|
911
1370
|
options,
|
|
912
|
-
!this.#settings.allowUnroutable
|
|
913
|
-
);
|
|
1371
|
+
mandatory: !this.#settings.allowUnroutable
|
|
1372
|
+
});
|
|
914
1373
|
}
|
|
915
|
-
async #publishConfirmed(
|
|
1374
|
+
async #publishConfirmed(input) {
|
|
1375
|
+
const { exchange, routingKey, content, options, mandatory } = input;
|
|
916
1376
|
const publisher = this.#live?.publisher;
|
|
917
1377
|
const eventId = options.messageId ?? "unknown";
|
|
918
1378
|
if (publisher === void 0) {
|
|
@@ -922,18 +1382,21 @@ var RabbitMqTransport = class {
|
|
|
922
1382
|
}
|
|
923
1383
|
const returnKey = unroutableKey(options.messageId, routingKey);
|
|
924
1384
|
await new Promise((resolve, reject) => {
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
+
});
|
|
932
1395
|
const onConfirm = (error) => {
|
|
933
|
-
|
|
1396
|
+
cancelTimeout();
|
|
934
1397
|
const returned = takeUnroutable(publisher.unroutable, returnKey);
|
|
935
1398
|
if (error) {
|
|
936
|
-
reject(this.#toPublishError(error, eventId, routingKey));
|
|
1399
|
+
reject(this.#toPublishError(error, { eventId, destination: routingKey }));
|
|
937
1400
|
} else if (returned) {
|
|
938
1401
|
reject(
|
|
939
1402
|
new UnroutableMessageError(
|
|
@@ -953,8 +1416,8 @@ var RabbitMqTransport = class {
|
|
|
953
1416
|
onConfirm
|
|
954
1417
|
);
|
|
955
1418
|
} catch (error) {
|
|
956
|
-
|
|
957
|
-
reject(this.#toPublishError(error, eventId, routingKey));
|
|
1419
|
+
cancelTimeout();
|
|
1420
|
+
reject(this.#toPublishError(error, { eventId, destination: routingKey }));
|
|
958
1421
|
}
|
|
959
1422
|
});
|
|
960
1423
|
}
|
|
@@ -963,7 +1426,8 @@ var RabbitMqTransport = class {
|
|
|
963
1426
|
* to `onMessage`. Call it once per transport; give each subscription its own
|
|
964
1427
|
* transport instance, with its own `name` and `consumerGroup`.
|
|
965
1428
|
*/
|
|
966
|
-
|
|
1429
|
+
// eslint-disable-next-line relaybus/max-two-params -- the Transport.subscribe signature
|
|
1430
|
+
async subscribe(destinations, onMessage, hooks) {
|
|
967
1431
|
if (this.#subscription !== void 0) {
|
|
968
1432
|
throw new ConfigurationError(
|
|
969
1433
|
`Transport '${this.name}' already has a subscription; create another transport for another consumer group`
|
|
@@ -993,9 +1457,11 @@ var RabbitMqTransport = class {
|
|
|
993
1457
|
redeliveryMaxAgeMs: this.#settings.redeliveryMaxAgeMs,
|
|
994
1458
|
ordered: this.#settings.ordered,
|
|
995
1459
|
deadLetterExchange: this.#settings.deadLetterExchange,
|
|
996
|
-
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 }),
|
|
997
1461
|
shutdownTimeoutMs: this.#settings.shutdownTimeoutMs,
|
|
998
1462
|
onMessage,
|
|
1463
|
+
...hooks === void 0 ? {} : { onInvalidMessage: hooks.onInvalidMessage },
|
|
1464
|
+
decode: this.#settings.decode,
|
|
999
1465
|
onConsumerLost: (reason) => this.#supervisor.recover(reason),
|
|
1000
1466
|
logger: this.#settings.logger,
|
|
1001
1467
|
random: this.#settings.random
|
|
@@ -1011,21 +1477,29 @@ var RabbitMqTransport = class {
|
|
|
1011
1477
|
/**
|
|
1012
1478
|
* Stops consuming, waits up to `shutdownTimeout` for running handlers
|
|
1013
1479
|
* (messages still being retried go back to the queue), then closes the
|
|
1014
|
-
* 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.
|
|
1015
1482
|
*/
|
|
1016
|
-
async close() {
|
|
1017
|
-
|
|
1018
|
-
|
|
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);
|
|
1019
1502
|
}
|
|
1020
|
-
this.#closed = true;
|
|
1021
|
-
await this.#supervisor.close(
|
|
1022
|
-
() => this.#topologyWork.run(async () => {
|
|
1023
|
-
const live = this.#live;
|
|
1024
|
-
this.#live = void 0;
|
|
1025
|
-
await this.#subscription?.consumer.stop();
|
|
1026
|
-
await live?.publisher.channel.close().catch(() => void 0);
|
|
1027
|
-
})
|
|
1028
|
-
);
|
|
1029
1503
|
}
|
|
1030
1504
|
async #establishChannels(connection) {
|
|
1031
1505
|
await this.#topologyWork.run(async () => {
|
|
@@ -1069,7 +1543,7 @@ var RabbitMqTransport = class {
|
|
|
1069
1543
|
this.#live = void 0;
|
|
1070
1544
|
this.#subscription?.consumer.detach();
|
|
1071
1545
|
}
|
|
1072
|
-
#toPublishError(error, eventId, destination) {
|
|
1546
|
+
#toPublishError(error, { eventId, destination }) {
|
|
1073
1547
|
const { message } = toErrorInfo(error);
|
|
1074
1548
|
const detail = `transport=${this.name} eventId=${eventId} destination=${destination} error=${message}`;
|
|
1075
1549
|
return message === NACK_MESSAGE ? new RabbitMqPublishRejectedError(
|
|
@@ -1100,6 +1574,6 @@ function takeUnroutable(unroutable, key) {
|
|
|
1100
1574
|
return true;
|
|
1101
1575
|
}
|
|
1102
1576
|
|
|
1103
|
-
export { AmqpLimitExceededError, MalformedMessageError, RabbitMqPublishRejectedError, RabbitMqTransport, RabbitMqUnavailableError, UnroutableMessageError, UnserializablePayloadError };
|
|
1577
|
+
export { AmqpLimitExceededError, MalformedMessageError, RabbitMqPublishRejectedError, RabbitMqTransport, RabbitMqUnavailableError, RabbitMqUnencodableEventError, UnroutableMessageError, UnserializablePayloadError, cloudEventsBinary, cloudEventsStructured, decodeCloudEvent, jsonEnvelope, plainJsonDecoder };
|
|
1104
1578
|
//# sourceMappingURL=index.js.map
|
|
1105
1579
|
//# sourceMappingURL=index.js.map
|