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