@relaybus/rabbitmq 0.2.0 → 0.3.0

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