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