@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/dist/index.js CHANGED
@@ -1,33 +1,47 @@
1
- import { RelayBusError, NonRetryableError, ConfigurationError, toErrorInfo, createRetryPolicy, toMilliseconds, computeRetryDelay, isRetryableError } from '@relaybus/core';
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
- clearTimeout(timer);
40
+ cancel();
27
41
  signal.removeEventListener("abort", finish);
28
42
  resolve();
29
43
  };
30
- const timer = setTimeout(finish, milliseconds);
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
- connection.close().catch(() => void 0);
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
- await connection?.close().catch(() => void 0);
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.close().catch(() => void 0);
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](reconnect) Reconnecting | attempt=${attempt} delayMs=${delayMs}`
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](reconnect) Reconnected | attempt=${attempt}`
225
+ `[ConnectionSupervisor](reconnectUntilConnected) Reconnected | attempt=${attempt}`
171
226
  );
172
227
  return;
173
228
  } catch (error) {
174
229
  this.#settings.logger.warn(
175
- `[ConnectionSupervisor](reconnect) Attempt failed | attempt=${attempt} error=${toErrorInfo(error).message}`
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 = positiveInteger(options.prefetch ?? (ordered ? 1 : 50), "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: positiveInteger(options.deliveryLimit ?? 10, "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
- return amqplib.connect(url);
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 { queue, handlerRetry } = this.#settings;
701
- let envelope;
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
- let redeliveries = readCounterHeader(message, REDELIVERIES_HEADER) ?? 0;
712
- let firstFailureAt = readCounterHeader(message, FIRST_FAILURE_HEADER);
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
- const failure = `queue=${queue} eventId=${envelope.id} attempt=${attempt} error=${toErrorInfo(error).message}`;
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, message, delayMs, redeliveries, firstFailureAt) {
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
- retryQueueName(this.#settings.queue, delayMs),
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, message, reason) {
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
- this.#settings.queue,
816
- content,
817
- options
1269
+ { exchange: this.#settings.deadLetterExchange, routingKey: this.#settings.queue },
1270
+ { content, options }
818
1271
  );
819
- } catch (error) {
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(error).message}`
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
- #closed = false;
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 } = encodeEnvelope(envelope);
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(exchange, routingKey, content, options, mandatory) {
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 timer = setTimeout(() => {
926
- reject(
927
- new RabbitMqUnavailableError(
928
- `[RabbitMqTransport](publish) No broker confirm within ${this.#settings.publishTimeoutMs}ms | transport=${this.name} eventId=${eventId} destination=${routingKey}`
929
- )
930
- );
931
- }, this.#settings.publishTimeoutMs);
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
- clearTimeout(timer);
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
- clearTimeout(timer);
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
- async subscribe(destinations, onMessage) {
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
- if (this.#closed) {
1018
- return;
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