@relaybus/rabbitmq 0.2.0 → 0.3.1

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,140 @@
1
- import { RelayBusError, NonRetryableError, assertEventType, ConfigurationError, toErrorInfo, createRetryPolicy, toMilliseconds, computeRetryDelay, isRetryableError } from '@relaybus/core';
1
+ import { RelayBusError, NonRetryableError, assertEventType, ConfigurationError, systemClock, toErrorInfo, computeRetryDelay, isRetryableError, createRetryPolicy } from '@relaybus/core';
2
+ import { createCloudEventReaders, DecodedEventReader, mediaTypeOf, isJsonObject, observeRejection, createJsonEnvelopeFormat, SubscribingTransport, writeStructuredCloudEvent, schedule, NonEmptyText, PositiveInteger, Milliseconds } 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";
17
29
  };
18
-
19
- // src/abortable-sleep.ts
30
+ var RabbitMqUnencodableEventError = class extends NonRetryableError {
31
+ name = "RabbitMqUnencodableEventError";
32
+ code = "RELAYBUS_RABBITMQ_UNENCODABLE_EVENT";
33
+ };
34
+ var NACK_MESSAGE = "message nacked";
35
+ var ConfirmedPublishing = class {
36
+ #transportName;
37
+ #publishTimeoutMs;
38
+ constructor(transportName, publishTimeoutMs) {
39
+ this.#transportName = transportName;
40
+ this.#publishTimeoutMs = publishTimeoutMs;
41
+ }
42
+ async publish(publisher, input) {
43
+ const { exchange, routingKey, content, options, mandatory } = input;
44
+ const eventId = options.messageId ?? "unknown";
45
+ if (publisher === void 0) {
46
+ throw new RabbitMqUnavailableError(
47
+ `[RabbitMqTransport](publish) Not connected to the broker | transport=${this.#transportName} destination=${routingKey}`
48
+ );
49
+ }
50
+ const returnKey = unroutableKey(options.messageId, routingKey);
51
+ const target = { eventId, destination: routingKey };
52
+ await new Promise((resolve, reject) => {
53
+ const cancelTimeout = this.#startTimeout(target, reject);
54
+ const pending = { ...target, publisher, returnKey, cancelTimeout, resolve, reject };
55
+ try {
56
+ publisher.channel.publish(
57
+ exchange,
58
+ routingKey,
59
+ content,
60
+ { ...options, mandatory },
61
+ (error) => this.#confirmed(pending, error)
62
+ );
63
+ } catch (error) {
64
+ cancelTimeout();
65
+ reject(this.#toPublishError(error, target));
66
+ }
67
+ });
68
+ }
69
+ #startTimeout({ eventId, destination }, reject) {
70
+ return schedule(systemClock, {
71
+ run: () => {
72
+ reject(
73
+ new RabbitMqUnavailableError(
74
+ `[RabbitMqTransport](publish) No broker confirm within ${this.#publishTimeoutMs}ms | transport=${this.#transportName} eventId=${eventId} destination=${destination}`
75
+ )
76
+ );
77
+ },
78
+ delayMs: this.#publishTimeoutMs
79
+ });
80
+ }
81
+ /** The broker's confirm (or nack) of one message arrived. */
82
+ #confirmed(pending, error) {
83
+ pending.cancelTimeout();
84
+ const returned = takeUnroutable(pending.publisher.unroutable, pending.returnKey);
85
+ if (error) {
86
+ pending.reject(this.#toPublishError(error, pending));
87
+ } else if (returned) {
88
+ pending.reject(
89
+ new UnroutableMessageError(
90
+ `[RabbitMqTransport](publish) No queue is bound to the routing key | transport=${this.#transportName} eventId=${pending.eventId} destination=${pending.destination}`
91
+ )
92
+ );
93
+ } else {
94
+ pending.resolve();
95
+ }
96
+ }
97
+ #toPublishError(error, { eventId, destination }) {
98
+ const { message } = toErrorInfo(error);
99
+ const detail = `transport=${this.#transportName} eventId=${eventId} destination=${destination} error=${message}`;
100
+ return message === NACK_MESSAGE ? new RabbitMqPublishRejectedError(
101
+ `[RabbitMqTransport](publish) Broker nacked the message | ${detail}`,
102
+ { cause: error }
103
+ ) : new RabbitMqUnavailableError(`[RabbitMqTransport](publish) Publish failed | ${detail}`, {
104
+ cause: error
105
+ });
106
+ }
107
+ };
108
+ function unroutableKey(messageId, routingKey) {
109
+ return `${String(messageId)}
110
+ ${routingKey}`;
111
+ }
112
+ function recordUnroutable(unroutable, key) {
113
+ unroutable.set(key, (unroutable.get(key) ?? 0) + 1);
114
+ }
115
+ function takeUnroutable(unroutable, key) {
116
+ const count = unroutable.get(key);
117
+ if (count === void 0) {
118
+ return false;
119
+ }
120
+ if (count === 1) {
121
+ unroutable.delete(key);
122
+ } else {
123
+ unroutable.set(key, count - 1);
124
+ }
125
+ return true;
126
+ }
20
127
  function abortableSleep(milliseconds, signal) {
21
128
  if (signal.aborted) {
22
129
  return Promise.resolve();
23
130
  }
24
131
  return new Promise((resolve) => {
25
132
  const finish = () => {
26
- clearTimeout(timer);
133
+ cancel();
27
134
  signal.removeEventListener("abort", finish);
28
135
  resolve();
29
136
  };
30
- const timer = setTimeout(finish, milliseconds);
137
+ const cancel = schedule(systemClock, { run: finish, delayMs: milliseconds });
31
138
  signal.addEventListener("abort", finish, { once: true });
32
139
  });
33
140
  }
@@ -36,7 +143,17 @@ function abortableSleep(milliseconds, signal) {
36
143
  var ConnectionSupervisor = class {
37
144
  #settings;
38
145
  #stop = new AbortController();
146
+ /** Aborted by `forceClose`: destroys a socket that is still connecting. */
147
+ #force = new AbortController();
148
+ /** Resolves on `forceClose`: closing stops waiting for the broker. */
149
+ #forcing = new Promise((resolve) => {
150
+ this.#force.signal.addEventListener("abort", () => resolve(), { once: true });
151
+ });
39
152
  #current;
153
+ /** Connections being closed, until their closing handshake finishes. */
154
+ #closing = /* @__PURE__ */ new Set();
155
+ /** Set by `forceClose`: connections are cut instead of closed. */
156
+ #forced = false;
40
157
  #ready = false;
41
158
  #started = false;
42
159
  #closed = false;
@@ -83,7 +200,7 @@ var ConnectionSupervisor = class {
83
200
  return;
84
201
  }
85
202
  this.#handleLoss(connection, reason);
86
- connection.close().catch(() => void 0);
203
+ void this.#closeConnection(connection);
87
204
  }
88
205
  /**
89
206
  * Stops reconnecting, runs `beforeDisconnect` while the connection is still
@@ -92,23 +209,54 @@ var ConnectionSupervisor = class {
92
209
  async close(beforeDisconnect) {
93
210
  this.#closed = true;
94
211
  this.#stop.abort();
95
- await this.#reconnecting;
212
+ await Promise.race([this.#reconnecting, this.#forcing]);
96
213
  try {
97
- await beforeDisconnect();
214
+ await Promise.race([beforeDisconnect(), this.#forcing]);
98
215
  } finally {
99
216
  const connection = this.#current;
100
217
  this.#current = void 0;
101
218
  this.#ready = false;
102
- await connection?.close().catch(() => void 0);
219
+ if (connection !== void 0) {
220
+ await this.#closeConnection(connection);
221
+ }
103
222
  }
104
223
  }
224
+ /**
225
+ * Stops reconnecting and cuts every connection, open or closing, without the closing handshake. A
226
+ * connection that finishes opening afterwards is cut at once.
227
+ */
228
+ forceClose() {
229
+ this.#closed = true;
230
+ this.#forced = true;
231
+ this.#stop.abort();
232
+ this.#force.abort();
233
+ for (const connection of [this.#current, ...this.#closing]) {
234
+ if (connection !== void 0) {
235
+ destroySocket(connection);
236
+ }
237
+ }
238
+ }
239
+ /** Closes a connection, which `forceClose` can cut until the broker answered; cuts it once forced. */
240
+ async #closeConnection(connection) {
241
+ if (this.#forced) {
242
+ destroySocket(connection);
243
+ return;
244
+ }
245
+ this.#closing.add(connection);
246
+ await connection.close().catch(() => void 0);
247
+ this.#closing.delete(connection);
248
+ }
105
249
  async #establish() {
106
- const connection = await this.#settings.connect();
250
+ const connection = await this.#settings.connect({ signal: this.#force.signal });
107
251
  connection.on("error", (error) => {
108
252
  this.#settings.logger.warn(
109
253
  `[ConnectionSupervisor](establish) Connection error | error=${error.message}`
110
254
  );
111
255
  });
256
+ if (this.#closed) {
257
+ await this.#closeConnection(connection);
258
+ throw new RabbitMqUnavailableError("[ConnectionSupervisor](establish) Transport is closed");
259
+ }
112
260
  connection.on("close", (error) => {
113
261
  this.#handleLoss(connection, error?.message ?? "closed by peer");
114
262
  });
@@ -131,7 +279,7 @@ var ConnectionSupervisor = class {
131
279
  this.#current = void 0;
132
280
  }
133
281
  this.#settings.onDisconnected();
134
- await connection.close().catch(() => void 0);
282
+ await this.#closeConnection(connection);
135
283
  }
136
284
  #handleLoss(connection, reason) {
137
285
  if (connection !== this.#current) {
@@ -158,7 +306,7 @@ var ConnectionSupervisor = class {
158
306
  for (let attempt = 1; !this.#closed; attempt += 1) {
159
307
  const delayMs = computeRetryDelay(policy, attempt, this.#settings.random);
160
308
  this.#settings.logger.log(
161
- `[ConnectionSupervisor](reconnect) Reconnecting | attempt=${attempt} delayMs=${delayMs}`
309
+ `[ConnectionSupervisor](reconnectUntilConnected) Reconnecting | attempt=${attempt} delayMs=${delayMs}`
162
310
  );
163
311
  await abortableSleep(delayMs, this.#stop.signal);
164
312
  if (this.#closed) {
@@ -167,78 +315,111 @@ var ConnectionSupervisor = class {
167
315
  try {
168
316
  await this.#establish();
169
317
  this.#settings.logger.log(
170
- `[ConnectionSupervisor](reconnect) Reconnected | attempt=${attempt}`
318
+ `[ConnectionSupervisor](reconnectUntilConnected) Reconnected | attempt=${attempt}`
171
319
  );
172
320
  return;
173
321
  } catch (error) {
174
322
  this.#settings.logger.warn(
175
- `[ConnectionSupervisor](reconnect) Attempt failed | attempt=${attempt} error=${toErrorInfo(error).message}`
323
+ `[ConnectionSupervisor](reconnectUntilConnected) Attempt failed | attempt=${attempt} error=${toErrorInfo(error).message}`
176
324
  );
177
325
  }
178
326
  }
179
327
  }
180
328
  };
329
+ function destroySocket(connection) {
330
+ const model = connection;
331
+ model.connection?.stream?.destroy(
332
+ new RabbitMqUnavailableError("Connection cut: the shutdown deadline passed")
333
+ );
334
+ }
181
335
 
182
336
  // src/json-payload.ts
183
337
  function assertJsonPayload(payload, eventId) {
184
- visit(payload, "$", /* @__PURE__ */ new Set(), eventId);
338
+ visit(payload, { path: "$", ancestors: /* @__PURE__ */ new Set(), eventId });
185
339
  }
186
- function visit(value, path, ancestors, eventId) {
340
+ function visit(value, {
341
+ path,
342
+ ancestors,
343
+ eventId
344
+ }) {
187
345
  switch (typeof value) {
188
346
  case "string":
189
347
  case "boolean":
190
348
  return;
191
349
  case "number":
192
350
  if (!Number.isFinite(value)) {
193
- throw refuse(eventId, path, `is ${value}, which JSON turns into null`);
351
+ throw refuse(eventId, { path, reason: `is ${value}, which JSON turns into null` });
194
352
  }
195
353
  return;
196
354
  case "object":
197
355
  if (value !== null) {
198
- visitObject(value, path, ancestors, eventId);
356
+ visitObject(value, { path, ancestors, eventId });
199
357
  }
200
358
  return;
201
359
  default:
202
- throw refuse(eventId, path, `is ${typeof value}, which JSON cannot represent`);
360
+ throw refuse(eventId, { path, reason: `is ${typeof value}, which JSON cannot represent` });
203
361
  }
204
362
  }
205
- function visitObject(value, path, ancestors, eventId) {
363
+ function visitObject(value, {
364
+ path,
365
+ ancestors,
366
+ eventId
367
+ }) {
206
368
  if (ancestors.has(value)) {
207
- throw refuse(eventId, path, "is a circular reference");
369
+ throw refuse(eventId, { path, reason: "is a circular reference" });
208
370
  }
209
371
  ancestors.add(value);
210
372
  if (Array.isArray(value)) {
211
- for (let index = 0; index < value.length; index += 1) {
212
- if (!(index in value)) {
213
- throw refuse(eventId, `${path}[${index}]`, "is an array hole, which JSON turns into null");
214
- }
215
- visit(value[index], `${path}[${index}]`, ancestors, eventId);
216
- }
373
+ visitArray(value, { path, ancestors, eventId });
217
374
  } else {
218
- const prototype = Object.getPrototypeOf(value);
219
- if (prototype !== Object.prototype && prototype !== null) {
220
- throw refuse(eventId, path, "is not a plain object (class instance, Date, Map or Set)");
221
- }
222
- if (Object.getOwnPropertySymbols(value).length > 0) {
223
- throw refuse(eventId, path, "has a symbol key, which JSON drops");
224
- }
225
- for (const key of Object.keys(value)) {
226
- visit(value[key], `${path}.${key}`, ancestors, eventId);
227
- }
375
+ visitPlainObject(value, { path, ancestors, eventId });
228
376
  }
229
377
  ancestors.delete(value);
230
378
  }
231
- function refuse(eventId, path, reason) {
379
+ function visitArray(value, { path, ancestors, eventId }) {
380
+ for (let index = 0; index < value.length; index += 1) {
381
+ if (!(index in value)) {
382
+ throw refuse(eventId, {
383
+ path: `${path}[${index}]`,
384
+ reason: "is an array hole, which JSON turns into null"
385
+ });
386
+ }
387
+ visit(value[index], { path: `${path}[${index}]`, ancestors, eventId });
388
+ }
389
+ }
390
+ function visitPlainObject(value, { path, ancestors, eventId }) {
391
+ const prototype = Object.getPrototypeOf(value);
392
+ if (prototype !== Object.prototype && prototype !== null) {
393
+ throw refuse(eventId, {
394
+ path,
395
+ reason: "is not a plain object (class instance, Date, Map or Set)"
396
+ });
397
+ }
398
+ if (Object.getOwnPropertySymbols(value).length > 0) {
399
+ throw refuse(eventId, { path, reason: "has a symbol key, which JSON drops" });
400
+ }
401
+ for (const key of Object.keys(value)) {
402
+ visit(value[key], {
403
+ path: `${path}.${key}`,
404
+ ancestors,
405
+ eventId
406
+ });
407
+ }
408
+ }
409
+ function refuse(eventId, { path, reason }) {
232
410
  return new UnserializablePayloadError(
233
411
  `Event ${eventId} has a payload JSON cannot represent faithfully: ${path} ${reason}`
234
412
  );
235
413
  }
236
414
 
237
415
  // src/wire-format.ts
416
+ var MILLISECONDS_PER_SECOND = 1e3;
238
417
  var ATTRIBUTE_PREFIX = "cloudEvents:";
239
418
  var EVENT_HEADER_PREFIX = "relaybus:";
240
419
  var SPEC_VERSION = "1.0";
241
420
  var JSON_CONTENT_TYPE = "application/json";
421
+ var STRUCTURED_CONTENT_TYPE = "application/cloudevents+json";
422
+ var RESERVED_HEADER_PREFIXES = ["x-death", "x-first-death-", "x-last-death-", "x-relaybus-"];
242
423
  var SHORT_STRING_LIMIT_BYTES = 255;
243
424
  var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
244
425
  function encodeEnvelope(envelope) {
@@ -252,6 +433,22 @@ function encodeEnvelope(envelope) {
252
433
  [`${ATTRIBUTE_PREFIX}time`]: envelope.occurredAt.toISOString(),
253
434
  [`${ATTRIBUTE_PREFIX}datacontenttype`]: JSON_CONTENT_TYPE
254
435
  };
436
+ addOptionalAttributes(headers, envelope);
437
+ return {
438
+ content: Buffer.from(JSON.stringify(envelope.payload), "utf8"),
439
+ options: {
440
+ contentType: JSON_CONTENT_TYPE,
441
+ persistent: true,
442
+ messageId: envelope.id,
443
+ timestamp: Math.floor(envelope.occurredAt.getTime() / MILLISECONDS_PER_SECOND),
444
+ ...mirror("type", envelope.type),
445
+ ...mirror("appId", envelope.source),
446
+ ...envelope.correlationId === null ? {} : mirror("correlationId", envelope.correlationId),
447
+ headers
448
+ }
449
+ };
450
+ }
451
+ function addOptionalAttributes(headers, envelope) {
255
452
  if (envelope.key !== null) {
256
453
  headers[`${ATTRIBUTE_PREFIX}subject`] = envelope.key;
257
454
  }
@@ -268,20 +465,44 @@ function encodeEnvelope(envelope) {
268
465
  );
269
466
  headers[`${EVENT_HEADER_PREFIX}${name}`] = value;
270
467
  }
468
+ }
469
+ function encodeStructuredEnvelope(envelope) {
470
+ assertJsonPayload(envelope.payload, envelope.id);
471
+ assertShortString("Event id", envelope.id);
472
+ const document = writeStructuredCloudEvent(envelope, {
473
+ time: envelope.occurredAt.toISOString(),
474
+ data: JSON.stringify(envelope.payload)
475
+ });
271
476
  return {
272
- content: Buffer.from(JSON.stringify(envelope.payload), "utf8"),
477
+ content: Buffer.from(document, "utf8"),
273
478
  options: {
274
- contentType: JSON_CONTENT_TYPE,
275
- persistent: true,
276
- messageId: envelope.id,
277
- timestamp: Math.floor(envelope.occurredAt.getTime() / 1e3),
278
- ...mirror("type", envelope.type),
279
- ...mirror("appId", envelope.source),
280
- ...envelope.correlationId === null ? {} : mirror("correlationId", envelope.correlationId),
281
- headers
479
+ ...messageProperties(envelope),
480
+ contentType: STRUCTURED_CONTENT_TYPE,
481
+ headers: plainHeaders(envelope, envelope.headers)
282
482
  }
283
483
  };
284
484
  }
485
+ function messageProperties(envelope) {
486
+ return {
487
+ persistent: true,
488
+ messageId: envelope.id,
489
+ timestamp: Math.floor(envelope.occurredAt.getTime() / MILLISECONDS_PER_SECOND),
490
+ ...mirror("type", envelope.type),
491
+ ...mirror("appId", envelope.source),
492
+ ...envelope.correlationId === null ? {} : mirror("correlationId", envelope.correlationId)
493
+ };
494
+ }
495
+ function plainHeaders(envelope, headers) {
496
+ for (const name of Object.keys(headers)) {
497
+ if (RESERVED_HEADER_PREFIXES.some((prefix) => name.startsWith(prefix))) {
498
+ throw new RabbitMqUnencodableEventError(
499
+ `Event ${envelope.id} has a header '${name}', a name the broker or the transport reserves`
500
+ );
501
+ }
502
+ assertShortString(`Header name '${name}'`, name);
503
+ }
504
+ return Object.fromEntries(Object.entries(headers));
505
+ }
285
506
  function assertShortString(label, value) {
286
507
  const bytes = Buffer.byteLength(value, "utf8");
287
508
  if (bytes > SHORT_STRING_LIMIT_BYTES) {
@@ -339,7 +560,7 @@ function parsePayload(content) {
339
560
  }
340
561
  function parseTime(rawTime, timestampSeconds) {
341
562
  if (rawTime === void 0) {
342
- const fallback = typeof timestampSeconds === "number" ? new Date(timestampSeconds * 1e3) : void 0;
563
+ const fallback = typeof timestampSeconds === "number" ? new Date(timestampSeconds * MILLISECONDS_PER_SECOND) : void 0;
343
564
  if (fallback === void 0 || Number.isNaN(fallback.getTime())) {
344
565
  throw new MalformedMessageError("Message has no valid time (header cloudEvents:time)");
345
566
  }
@@ -366,6 +587,8 @@ function required(attribute, value) {
366
587
  }
367
588
 
368
589
  // src/inbound-decoders.ts
590
+ var MILLISECONDS_PER_SECOND2 = 1e3;
591
+ var { optionalAttribute, requiredAttribute, assertValidType, assertJsonDataContentType } = createCloudEventReaders((reason) => new MalformedMessageError(reason));
369
592
  var STRUCTURED_MEDIA_TYPE = "application/cloudevents+json";
370
593
  var strictUtf8 = new TextDecoder("utf-8", { fatal: true });
371
594
  var SPEC_VERSION2 = "1.0";
@@ -426,18 +649,13 @@ function plainJsonDecoder(options) {
426
649
  if (properties.headers?.[SPEC_VERSION_HEADER] !== void 0 || mediaTypeOf(properties.contentType) === STRUCTURED_MEDIA_TYPE) {
427
650
  return decodeCloudEvent(message);
428
651
  }
429
- const plain = {
430
- routingKey: message.fields.routingKey,
431
- properties,
432
- headers: readForeignHeaders(properties),
433
- payload: parseJsonBody(message.content)
434
- };
435
- const type = resolveText(options.type, plain, "type");
652
+ const plain = plainMessageOf(message);
653
+ const type = RABBITMQ_EVENTS.text(options.type, { plain, name: "type" });
436
654
  if (type === void 0) {
437
655
  throw new MalformedMessageError("Message has no event type: the type option found none");
438
656
  }
439
657
  assertValidType(type, "The event type");
440
- const id = resolveText(options.id, plain, "id") ?? nonEmptyText(properties.messageId);
658
+ const id = RABBITMQ_EVENTS.text(options.id, { plain, name: "id" }) ?? nonEmptyText(properties.messageId);
441
659
  if (id === void 0) {
442
660
  throw new MalformedMessageError(
443
661
  "Message has no id: set the messageId property on the producer or configure the id option"
@@ -446,8 +664,8 @@ function plainJsonDecoder(options) {
446
664
  return {
447
665
  id,
448
666
  type,
449
- source: resolveText(options.source, plain, "source") ?? defaultSource(message),
450
- key: resolveText(options.key, plain, "key") ?? null,
667
+ source: RABBITMQ_EVENTS.text(options.source, { plain, name: "source" }) ?? defaultSource(message),
668
+ key: RABBITMQ_EVENTS.text(options.key, { plain, name: "key" }) ?? null,
451
669
  payload: plain.payload,
452
670
  headers: plain.headers,
453
671
  correlationId: null,
@@ -456,27 +674,35 @@ function plainJsonDecoder(options) {
456
674
  };
457
675
  };
458
676
  }
459
- function completeDecodedEvent(decoded, message) {
460
- if (typeof decoded !== "object" || decoded === null) {
461
- throw new MalformedMessageError("The decoder must return an event object synchronously");
462
- }
463
- if ("then" in decoded) {
464
- observeRejection(decoded);
465
- throw new MalformedMessageError("The decoder must return an event object synchronously");
466
- }
467
- const event = decoded;
677
+ function plainMessageOf(message) {
678
+ const { properties } = message;
468
679
  return {
469
- id: event.id,
470
- type: event.type,
471
- source: event.source ?? defaultSource(message),
472
- key: event.key ?? null,
473
- payload: event.payload,
474
- headers: event.headers ?? {},
475
- correlationId: event.correlationId ?? null,
476
- causationId: event.causationId ?? null,
477
- occurredAt: event.occurredAt ?? propertyTimeOrNow(message.properties)
680
+ routingKey: message.fields.routingKey,
681
+ properties,
682
+ headers: readForeignHeaders(properties),
683
+ payload: parseJsonBody(message.content)
478
684
  };
479
685
  }
686
+ var RabbitMqDecodedEvents = class extends DecodedEventReader {
687
+ invalid(reason) {
688
+ return new MalformedMessageError(reason);
689
+ }
690
+ defaultSource(message) {
691
+ return defaultSource(message);
692
+ }
693
+ /** None: AMQP messages carry no ordering key. */
694
+ defaultKey() {
695
+ return null;
696
+ }
697
+ /** The `timestamp` property, else the time of receipt. */
698
+ defaultTime(message) {
699
+ return propertyTimeOrNow(message.properties);
700
+ }
701
+ };
702
+ var RABBITMQ_EVENTS = new RabbitMqDecodedEvents();
703
+ function completeDecodedEvent(decoded, message) {
704
+ return RABBITMQ_EVENTS.complete(decoded, message);
705
+ }
480
706
  function parseJsonBody(content) {
481
707
  let text2;
482
708
  try {
@@ -490,47 +716,12 @@ function parseJsonBody(content) {
490
716
  throw new MalformedMessageError("Message body is not valid JSON");
491
717
  }
492
718
  }
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
719
  function nonEmptyText(value) {
500
720
  return typeof value === "string" && value.length > 0 ? value : void 0;
501
721
  }
502
722
  function defaultSource(message) {
503
723
  return nonEmptyText(message.properties.appId) ?? message.fields.routingKey;
504
724
  }
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
725
  function parseTime2(text2) {
535
726
  const time = new Date(text2);
536
727
  if (!RFC3339_PATTERN.test(text2) || Number.isNaN(time.getTime())) {
@@ -540,7 +731,7 @@ function parseTime2(text2) {
540
731
  }
541
732
  function propertyTimeOrNow(properties) {
542
733
  const { timestamp } = properties;
543
- return typeof timestamp === "number" && Number.isFinite(timestamp) ? new Date(timestamp * 1e3) : /* @__PURE__ */ new Date();
734
+ return typeof timestamp === "number" && Number.isFinite(timestamp) ? new Date(timestamp * MILLISECONDS_PER_SECOND2) : /* @__PURE__ */ new Date();
544
735
  }
545
736
  function extensionAttributes(body) {
546
737
  return Object.fromEntries(
@@ -556,19 +747,6 @@ function readForeignHeaders(properties) {
556
747
  )
557
748
  );
558
749
  }
559
- function resolveText(option, message, name) {
560
- const value = typeof option === "function" ? option(message) : option;
561
- if (value === void 0 || value === null) {
562
- return void 0;
563
- }
564
- if (typeof value !== "string") {
565
- observeRejection(value);
566
- throw new MalformedMessageError(
567
- `The ${name} option returned ${typeof value}, expected a string`
568
- );
569
- }
570
- return value.length === 0 ? void 0 : value;
571
- }
572
750
  function resolveOccurredAt(option, message) {
573
751
  const value = option?.(message);
574
752
  if (value === void 0) {
@@ -580,14 +758,62 @@ function resolveOccurredAt(option, message) {
580
758
  }
581
759
  return value;
582
760
  }
583
- function observeRejection(value) {
584
- if (typeof value === "object" && value !== null && "then" in value) {
585
- Promise.resolve(value).catch(() => void 0);
761
+
762
+ // src/codecs.ts
763
+ var cloudEventsBinary = Object.freeze({
764
+ encode: encodeEnvelope,
765
+ decode: decodeEnvelope
766
+ });
767
+ var cloudEventsStructured = Object.freeze({
768
+ encode: encodeStructuredEnvelope,
769
+ decode: decodeCloudEvent
770
+ });
771
+ var MESSAGE_ID_PROPERTY = "message-id-property";
772
+ var DEFAULT_CONTENT_TYPE = "application/json";
773
+ function jsonEnvelope(mapping) {
774
+ const format = createJsonEnvelopeFormat(mapping, {
775
+ transportIds: [MESSAGE_ID_PROPERTY],
776
+ failures: {
777
+ invalid: (reason) => new MalformedMessageError(reason),
778
+ unencodable: (reason) => new RabbitMqUnencodableEventError(reason)
779
+ }
780
+ });
781
+ const contentType = mapping.contentType ?? DEFAULT_CONTENT_TYPE;
782
+ return {
783
+ encode: (envelope) => {
784
+ assertJsonPayload(envelope.payload, envelope.id);
785
+ assertShortString("Event id", envelope.id);
786
+ const { body, headers } = format.write(envelope);
787
+ return {
788
+ content: Buffer.from(JSON.stringify(body), "utf8"),
789
+ options: {
790
+ ...messageProperties(envelope),
791
+ contentType,
792
+ headers: plainHeaders(envelope, headers)
793
+ }
794
+ };
795
+ },
796
+ decode: (message) => {
797
+ const fields = format.read({
798
+ body: parseJsonBody(message.content),
799
+ headers: readForeignHeaders(message.properties)
800
+ });
801
+ return { ...fields, id: fields.id ?? requireMessageId(message) };
802
+ }
803
+ };
804
+ }
805
+ function requireMessageId(message) {
806
+ const id = nonEmptyText(message.properties.messageId);
807
+ if (id === void 0) {
808
+ throw new MalformedMessageError("Message has no id: the messageId property is missing");
586
809
  }
810
+ return id;
587
811
  }
588
812
  var REDELIVERIES_HEADER = "x-relaybus-redeliveries";
589
813
  var FIRST_FAILURE_HEADER = "x-relaybus-first-failure-at";
590
814
  var DEAD_LETTER_REASON_HEADER = "x-relaybus-dead-letter-reason";
815
+ var DEAD_LETTER_ERROR_NAME_HEADER = "x-relaybus-dead-letter-error-name";
816
+ var DEAD_LETTER_ERROR_CODE_HEADER = "x-relaybus-dead-letter-error-code";
591
817
  var ORIGINAL_ROUTING_KEY_HEADER = "x-relaybus-routing-key";
592
818
  var RELAYBUS_STATE_HEADERS = [REDELIVERIES_HEADER, FIRST_FAILURE_HEADER];
593
819
  var BROKER_COUNTER_HEADERS = ["x-delivery-count", "x-acquired-count"];
@@ -612,7 +838,7 @@ function buildRetryLadder(policy) {
612
838
  delay *= policy.factor;
613
839
  }
614
840
  }
615
- function chooseRetryTier(ladder, delayMs, random) {
841
+ function chooseRetryTier(ladder, { delayMs, random }) {
616
842
  const upperIndex = ladder.findIndex((tier) => tier >= delayMs);
617
843
  if (upperIndex === -1) {
618
844
  return ladder.at(-1);
@@ -635,6 +861,8 @@ function copyForRepublish(message, headerChanges) {
635
861
  for (const name of [
636
862
  ...RELAYBUS_STATE_HEADERS,
637
863
  DEAD_LETTER_REASON_HEADER,
864
+ DEAD_LETTER_ERROR_NAME_HEADER,
865
+ DEAD_LETTER_ERROR_CODE_HEADER,
638
866
  ...BROKER_COUNTER_HEADERS
639
867
  ]) {
640
868
  delete headers[name];
@@ -664,6 +892,8 @@ function withOriginalRoutingKey(message) {
664
892
  }
665
893
 
666
894
  // src/options.ts
895
+ var DEFAULT_PREFETCH = 50;
896
+ var DEFAULT_DELIVERY_LIMIT = 10;
667
897
  var DEFAULT_HANDLER_RETRY = {
668
898
  attempts: 3,
669
899
  initialDelayMs: 200,
@@ -690,7 +920,15 @@ var DEFAULT_RECONNECT = {
690
920
  retryIf: void 0
691
921
  };
692
922
  function resolveSettings(options) {
693
- const exchange = nonEmpty(options.exchange ?? "relaybus", "exchange");
923
+ const basics = resolveBasics(options);
924
+ return {
925
+ ...resolveTopology(options, basics),
926
+ ...resolveRetries(options, basics),
927
+ ...resolveRuntime(options, basics)
928
+ };
929
+ }
930
+ function resolveBasics(options) {
931
+ const exchange = NonEmptyText.of(options.exchange ?? "relaybus", "exchange");
694
932
  const failOnConnectError = options.failOnConnectError ?? true;
695
933
  if (!failOnConnectError && options.reconnect === false) {
696
934
  throw new ConfigurationError(
@@ -698,39 +936,56 @@ function resolveSettings(options) {
698
936
  );
699
937
  }
700
938
  const ordered = options.ordered ?? false;
701
- const prefetch = positiveInteger(options.prefetch ?? (ordered ? 1 : 50), "prefetch");
939
+ const prefetch = PositiveInteger.of(
940
+ options.prefetch ?? (ordered ? 1 : DEFAULT_PREFETCH),
941
+ "prefetch"
942
+ );
702
943
  if (ordered && prefetch !== 1) {
703
944
  throw new ConfigurationError(`ordered consumption needs prefetch 1, received ${prefetch}`);
704
945
  }
705
946
  const redelivery = createRetryPolicy(options.redelivery, DEFAULT_REDELIVERY);
947
+ return { exchange, failOnConnectError, ordered, prefetch, redelivery };
948
+ }
949
+ function resolveTopology(options, { exchange, prefetch }) {
706
950
  return {
707
- name: nonEmpty(options.name ?? "rabbitmq", "name"),
951
+ name: NonEmptyText.of(options.name ?? "rabbitmq", "name"),
708
952
  connect: resolveConnect(options),
709
953
  exchange,
710
954
  prefetch,
711
- queuePrefix: nonEmpty(options.queuePrefix ?? "relaybus", "queuePrefix"),
712
- consumerGroup: optionalNonEmpty(options.consumerGroup, "consumerGroup"),
713
- queue: optionalNonEmpty(options.queue, "queue"),
714
- deadLetterExchange: nonEmpty(
955
+ queuePrefix: NonEmptyText.of(options.queuePrefix ?? "relaybus", "queuePrefix"),
956
+ consumerGroup: NonEmptyText.optional(options.consumerGroup, "consumerGroup"),
957
+ queue: NonEmptyText.optional(options.queue, "queue"),
958
+ deadLetterExchange: NonEmptyText.of(
715
959
  options.deadLetterExchange ?? `${exchange}.dead`,
716
960
  "deadLetterExchange"
717
961
  ),
718
- deadLetterQueue: optionalNonEmpty(options.deadLetterQueue, "deadLetterQueue"),
719
- deliveryLimit: positiveInteger(options.deliveryLimit ?? 10, "deliveryLimit"),
962
+ deadLetterQueue: NonEmptyText.optional(options.deadLetterQueue, "deadLetterQueue"),
963
+ deliveryLimit: PositiveInteger.of(
964
+ options.deliveryLimit ?? DEFAULT_DELIVERY_LIMIT,
965
+ "deliveryLimit"
966
+ )
967
+ };
968
+ }
969
+ function resolveRetries(options, { ordered, redelivery }) {
970
+ return {
720
971
  handlerRetry: createRetryPolicy(options.handlerRetry, DEFAULT_HANDLER_RETRY),
721
972
  redelivery,
722
973
  retryLadder: ordered ? [] : buildRetryLadder(redelivery),
723
- redeliveryMaxAgeMs: positiveDuration(
974
+ redeliveryMaxAgeMs: Milliseconds.positive(
724
975
  options.redelivery?.maxAge ?? DEFAULT_REDELIVERY_MAX_AGE,
725
976
  "redelivery.maxAge"
726
977
  ),
727
- ordered,
978
+ ordered
979
+ };
980
+ }
981
+ function resolveRuntime(options, { failOnConnectError }) {
982
+ return {
728
983
  reconnect: resolveReconnect(options.reconnect),
729
984
  failOnConnectError,
730
985
  allowUnroutable: options.allowUnroutable ?? false,
731
- publishTimeoutMs: positiveDuration(options.publishTimeout ?? "30s", "publishTimeout"),
732
- shutdownTimeoutMs: toMilliseconds(options.shutdownTimeout ?? "30s", "shutdownTimeout"),
733
- decode: resolveDecode(options.decode),
986
+ publishTimeoutMs: Milliseconds.positive(options.publishTimeout ?? "30s", "publishTimeout"),
987
+ shutdownTimeoutMs: Milliseconds.of(options.shutdownTimeout ?? "30s", "shutdownTimeout"),
988
+ ...resolveCodec(options),
734
989
  logger: options.logger ?? console,
735
990
  random: options.random ?? Math.random
736
991
  };
@@ -746,11 +1001,21 @@ function resolveQueueName(settings) {
746
1001
  }
747
1002
  return `${settings.queuePrefix}.${settings.consumerGroup}`;
748
1003
  }
749
- function resolveDecode(decode) {
750
- if (decode === void 0) {
751
- return decodeEnvelope;
1004
+ function resolveCodec({
1005
+ codec,
1006
+ decode
1007
+ }) {
1008
+ if (decode !== void 0 && codec !== void 0) {
1009
+ throw new ConfigurationError("Give 'codec' or the deprecated 'decode', not both");
1010
+ }
1011
+ const chosen = decode === void 0 ? codec ?? cloudEventsBinary : { encode: encodeEnvelope, decode };
1012
+ if (chosen === cloudEventsBinary) {
1013
+ return { encode: encodeEnvelope, decode: decodeEnvelope };
752
1014
  }
753
- return (message) => completeDecodedEvent(decode(message), message);
1015
+ return {
1016
+ encode: chosen.encode,
1017
+ decode: (message) => completeDecodedEvent(chosen.decode(message), message)
1018
+ };
754
1019
  }
755
1020
  function resolveConnect(options) {
756
1021
  const { url, connect } = options;
@@ -760,9 +1025,10 @@ function resolveConnect(options) {
760
1025
  if (connect !== void 0) {
761
1026
  return connect;
762
1027
  }
763
- return async () => {
1028
+ return async ({ signal }) => {
764
1029
  const amqplib = await import('amqplib');
765
- return amqplib.connect(url);
1030
+ const socketOptions = { signal };
1031
+ return amqplib.connect(url, socketOptions);
766
1032
  };
767
1033
  }
768
1034
  function resolveReconnect(reconnect) {
@@ -771,28 +1037,6 @@ function resolveReconnect(reconnect) {
771
1037
  }
772
1038
  return createRetryPolicy({ ...reconnect, attempts: 1 }, DEFAULT_RECONNECT);
773
1039
  }
774
- function nonEmpty(value, name) {
775
- if (value.length === 0) {
776
- throw new ConfigurationError(`${name} must not be empty`);
777
- }
778
- return value;
779
- }
780
- function optionalNonEmpty(value, name) {
781
- return value === void 0 ? void 0 : nonEmpty(value, name);
782
- }
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
- function positiveDuration(value, name) {
790
- const milliseconds = toMilliseconds(value, name);
791
- if (milliseconds === 0) {
792
- throw new ConfigurationError(`${name} must be greater than zero`);
793
- }
794
- return milliseconds;
795
- }
796
1040
 
797
1041
  // src/topology.ts
798
1042
  function retryQueueName(queue, delayMs) {
@@ -934,74 +1178,115 @@ var QueueConsumer = class {
934
1178
  this.#inFlight.add(task);
935
1179
  }
936
1180
  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");
1181
+ const decoded = await this.#decodeOrDeadLetter(channel, message);
1182
+ if (decoded === void 0) {
946
1183
  return;
947
1184
  }
948
- let redeliveries = readCounterHeader(message, REDELIVERIES_HEADER) ?? 0;
949
- let firstFailureAt = readCounterHeader(message, FIRST_FAILURE_HEADER);
1185
+ const { envelope } = decoded;
1186
+ const delivery = {
1187
+ channel,
1188
+ message,
1189
+ envelope,
1190
+ redeliveries: readCounterHeader(message, REDELIVERIES_HEADER) ?? 0,
1191
+ firstFailureAt: readCounterHeader(message, FIRST_FAILURE_HEADER)
1192
+ };
950
1193
  for (let attempt = 1; ; attempt += 1) {
951
1194
  try {
952
- await this.#settings.onMessage(envelope);
953
- this.#settle(channel, message, "ack");
1195
+ await this.#settings.onMessage(envelope, { destination: message.fields.routingKey });
1196
+ this.#settle(channel, { message, settlement: "ack" });
954
1197
  return;
955
1198
  } 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");
1199
+ if (await this.#afterFailure(delivery, { attempt, error }) === "settled") {
995
1200
  return;
996
1201
  }
997
1202
  }
998
1203
  }
999
1204
  }
1205
+ /** The event of `message`, or undefined after dead-lettering a message that is not one. */
1206
+ async #decodeOrDeadLetter(channel, message) {
1207
+ try {
1208
+ return { envelope: this.#settings.decode(withOriginalRoutingKey(message)) };
1209
+ } catch (error) {
1210
+ this.#settings.logger.error(
1211
+ `[QueueConsumer](decodeOrDeadLetter) Dead-lettering malformed message | queue=${this.#settings.queue} routingKey=${message.fields.routingKey} ${describeError(error)}`
1212
+ );
1213
+ this.#settings.onInvalidMessage?.({ destination: message.fields.routingKey, error });
1214
+ await this.#deadLetter(channel, { message, reason: "malformed message", error });
1215
+ return void 0;
1216
+ }
1217
+ }
1218
+ /**
1219
+ * Decides what follows a failed attempt: dead-letter a non-retryable error, retry on the same
1220
+ * delivery while local attempts remain, else redeliver later. `retry` runs the handler again.
1221
+ */
1222
+ async #afterFailure(delivery, { attempt, error }) {
1223
+ const { channel, message, envelope } = delivery;
1224
+ const { queue, handlerRetry } = this.#settings;
1225
+ const failure = `queue=${queue} eventId=${envelope.id} attempt=${attempt} ${describeError(error)}`;
1226
+ if (!isRetryableError(handlerRetry, error)) {
1227
+ this.#settings.logger.error(
1228
+ `[QueueConsumer](afterFailure) Dead-lettering message the handler cannot process | ${failure}`
1229
+ );
1230
+ await this.#deadLetter(channel, {
1231
+ message,
1232
+ reason: "non-retryable handler error",
1233
+ error
1234
+ });
1235
+ return "settled";
1236
+ }
1237
+ if (attempt < handlerRetry.attempts) {
1238
+ const delayMs = computeRetryDelay(handlerRetry, attempt, this.#settings.random);
1239
+ this.#settings.logger.warn(
1240
+ `[QueueConsumer](afterFailure) Handler failed, retrying | ${failure} delayMs=${delayMs}`
1241
+ );
1242
+ return this.#waitOrRequeue(delivery, delayMs);
1243
+ }
1244
+ return this.#afterSpentAttempts(delivery, { failure, error });
1245
+ }
1246
+ /**
1247
+ * The local attempts are spent: dead-letter a message that kept failing past `redeliveryMaxAgeMs`,
1248
+ * else park it in a retry queue, or, when ordered, wait and retry it in place.
1249
+ */
1250
+ async #afterSpentAttempts(delivery, { failure, error }) {
1251
+ const { channel, message } = delivery;
1252
+ delivery.redeliveries += 1;
1253
+ const now = Date.now();
1254
+ delivery.firstFailureAt ??= now;
1255
+ const { redeliveries, firstFailureAt } = delivery;
1256
+ const delayMs = this.#redeliveryDelay(redeliveries);
1257
+ if (now + delayMs - firstFailureAt > this.#settings.redeliveryMaxAgeMs) {
1258
+ this.#settings.logger.error(
1259
+ `[QueueConsumer](afterSpentAttempts) Dead-lettering message that kept failing | ${failure} redeliveries=${redeliveries - 1} failingForMs=${now - firstFailureAt} maxAgeMs=${this.#settings.redeliveryMaxAgeMs}`
1260
+ );
1261
+ await this.#deadLetter(channel, {
1262
+ message,
1263
+ reason: "redelivery window exhausted",
1264
+ error
1265
+ });
1266
+ return "settled";
1267
+ }
1268
+ this.#settings.logger.warn(
1269
+ `[QueueConsumer](afterSpentAttempts) Handler attempts spent, retrying later | ${failure} redeliveries=${redeliveries} delayMs=${delayMs}`
1270
+ );
1271
+ if (!this.#settings.ordered) {
1272
+ await this.#redeliverLater(channel, { message, delayMs, redeliveries, firstFailureAt });
1273
+ return "settled";
1274
+ }
1275
+ return this.#waitOrRequeue(delivery, delayMs);
1276
+ }
1277
+ /** Waits `delayMs` before the next attempt; requeues instead when the consumer stops meanwhile. */
1278
+ async #waitOrRequeue(delivery, delayMs) {
1279
+ if (await this.#interrupted(delayMs)) {
1280
+ this.#settle(delivery.channel, { message: delivery.message, settlement: "requeue" });
1281
+ return "settled";
1282
+ }
1283
+ return "retry";
1284
+ }
1000
1285
  /** The backoff before the next delayed redelivery: a retry queue's TTL, or an exact wait when ordered. */
1001
1286
  #redeliveryDelay(redeliveries) {
1002
1287
  const { redelivery, retryLadder, ordered, random } = this.#settings;
1003
1288
  const delayMs = computeRetryDelay(redelivery, redeliveries, random);
1004
- return ordered ? delayMs : chooseRetryTier(retryLadder, delayMs, random);
1289
+ return ordered ? delayMs : chooseRetryTier(retryLadder, { delayMs, random });
1005
1290
  }
1006
1291
  /** Sleeps; true when the consumer is stopping, so the caller must give the message back. */
1007
1292
  async #interrupted(delayMs) {
@@ -1014,7 +1299,8 @@ var QueueConsumer = class {
1014
1299
  * the original is requeued after the same delay, so it is neither lost nor
1015
1300
  * hot-looped.
1016
1301
  */
1017
- async #redeliverLater(channel, message, delayMs, redeliveries, firstFailureAt) {
1302
+ async #redeliverLater(channel, input) {
1303
+ const { message, delayMs, redeliveries, firstFailureAt } = input;
1018
1304
  const { content, options } = copyForRepublish(message, {
1019
1305
  [REDELIVERIES_HEADER]: String(redeliveries),
1020
1306
  [FIRST_FAILURE_HEADER]: String(firstFailureAt),
@@ -1022,20 +1308,18 @@ var QueueConsumer = class {
1022
1308
  });
1023
1309
  try {
1024
1310
  await this.#settings.republish(
1025
- "",
1026
- retryQueueName(this.#settings.queue, delayMs),
1027
- content,
1028
- options
1311
+ { exchange: "", routingKey: retryQueueName(this.#settings.queue, delayMs) },
1312
+ { content, options }
1029
1313
  );
1030
1314
  } catch (error) {
1031
1315
  this.#settings.logger.warn(
1032
1316
  `[QueueConsumer](redeliverLater) Could not park the message in the retry queue, requeueing it | queue=${this.#settings.queue} delayMs=${delayMs} error=${toErrorInfo(error).message}`
1033
1317
  );
1034
1318
  await this.#interrupted(delayMs);
1035
- this.#settle(channel, message, "requeue");
1319
+ this.#settle(channel, { message, settlement: "requeue" });
1036
1320
  return;
1037
1321
  }
1038
- this.#settle(channel, message, "ack");
1322
+ this.#settle(channel, { message, settlement: "ack" });
1039
1323
  }
1040
1324
  /**
1041
1325
  * Moves the message to the dead-letter queue through a confirmed copy that
@@ -1043,27 +1327,29 @@ var QueueConsumer = class {
1043
1327
  * message is rejected without requeue, which makes the broker dead-letter it
1044
1328
  * (at-least-once), so it still ends up in the dead-letter queue.
1045
1329
  */
1046
- async #deadLetter(channel, message, reason) {
1330
+ async #deadLetter(channel, input) {
1331
+ const { message, reason, error } = input;
1332
+ const { name, code } = toErrorInfo(error);
1047
1333
  const { content, options } = copyForRepublish(message, {
1048
- [DEAD_LETTER_REASON_HEADER]: reason
1334
+ [DEAD_LETTER_REASON_HEADER]: reason,
1335
+ [DEAD_LETTER_ERROR_NAME_HEADER]: name,
1336
+ ...code === void 0 ? {} : { [DEAD_LETTER_ERROR_CODE_HEADER]: code }
1049
1337
  });
1050
1338
  try {
1051
1339
  await this.#settings.republish(
1052
- this.#settings.deadLetterExchange,
1053
- this.#settings.queue,
1054
- content,
1055
- options
1340
+ { exchange: this.#settings.deadLetterExchange, routingKey: this.#settings.queue },
1341
+ { content, options }
1056
1342
  );
1057
- } catch (error) {
1343
+ } catch (error2) {
1058
1344
  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}`
1345
+ `[QueueConsumer](deadLetter) Could not publish to the dead-letter exchange, leaving it to the broker | queue=${this.#settings.queue} error=${toErrorInfo(error2).message}`
1060
1346
  );
1061
- this.#settle(channel, message, "dead-letter");
1347
+ this.#settle(channel, { message, settlement: "dead-letter" });
1062
1348
  return;
1063
1349
  }
1064
- this.#settle(channel, message, "ack");
1350
+ this.#settle(channel, { message, settlement: "ack" });
1065
1351
  }
1066
- #settle(channel, message, settlement) {
1352
+ #settle(channel, { message, settlement }) {
1067
1353
  try {
1068
1354
  if (settlement === "ack") {
1069
1355
  channel.ack(message);
@@ -1077,6 +1363,10 @@ var QueueConsumer = class {
1077
1363
  }
1078
1364
  }
1079
1365
  };
1366
+ function describeError(error) {
1367
+ const { message, name, code } = toErrorInfo(error);
1368
+ return `error=${message} errorName=${name}${code === void 0 ? "" : ` code=${code}`}`;
1369
+ }
1080
1370
 
1081
1371
  // src/serial-queue.ts
1082
1372
  var SerialQueue = class {
@@ -1089,19 +1379,22 @@ var SerialQueue = class {
1089
1379
  };
1090
1380
 
1091
1381
  // src/rabbitmq-transport.ts
1092
- var NACK_MESSAGE = "message nacked";
1093
- var RabbitMqTransport = class {
1382
+ var RabbitMqTransport = class extends SubscribingTransport {
1094
1383
  name;
1384
+ system = "rabbitmq";
1095
1385
  #settings;
1096
1386
  #supervisor;
1387
+ #publishing;
1097
1388
  /** Serializes topology work so a subscription never races a reconnect. */
1098
1389
  #topologyWork = new SerialQueue();
1099
1390
  #live;
1100
1391
  #subscription;
1101
- #closed = false;
1392
+ #closing;
1102
1393
  constructor(options) {
1394
+ super();
1103
1395
  this.#settings = resolveSettings(options);
1104
1396
  this.name = this.#settings.name;
1397
+ this.#publishing = new ConfirmedPublishing(this.name, this.#settings.publishTimeoutMs);
1105
1398
  this.#supervisor = new ConnectionSupervisor({
1106
1399
  connect: this.#settings.connect,
1107
1400
  reconnect: this.#settings.reconnect,
@@ -1138,62 +1431,17 @@ var RabbitMqTransport = class {
1138
1431
  * @throws {RabbitMqUnavailableError} not connected, channel lost, or confirm timed out
1139
1432
  * @throws {RabbitMqPublishRejectedError} the broker nacked the message
1140
1433
  * @throws {UnroutableMessageError} no queue is bound to `destination` (unless `allowUnroutable`)
1434
+ * @throws whatever the `codec`'s `encode` throws, such as {@link RabbitMqUnencodableEventError}
1141
1435
  */
1142
1436
  async publish(envelope, destination) {
1143
1437
  assertShortString("Destination (routing key)", destination);
1144
- const { content, options } = encodeEnvelope(envelope);
1145
- await this.#publishConfirmed(
1146
- this.#settings.exchange,
1147
- destination,
1438
+ const { content, options } = this.#settings.encode(envelope);
1439
+ await this.#publishing.publish(this.#live?.publisher, {
1440
+ exchange: this.#settings.exchange,
1441
+ routingKey: destination,
1148
1442
  content,
1149
1443
  options,
1150
- !this.#settings.allowUnroutable
1151
- );
1152
- }
1153
- async #publishConfirmed(exchange, routingKey, content, options, mandatory) {
1154
- const publisher = this.#live?.publisher;
1155
- const eventId = options.messageId ?? "unknown";
1156
- if (publisher === void 0) {
1157
- throw new RabbitMqUnavailableError(
1158
- `[RabbitMqTransport](publish) Not connected to the broker | transport=${this.name} destination=${routingKey}`
1159
- );
1160
- }
1161
- const returnKey = unroutableKey(options.messageId, routingKey);
1162
- 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);
1170
- const onConfirm = (error) => {
1171
- clearTimeout(timer);
1172
- const returned = takeUnroutable(publisher.unroutable, returnKey);
1173
- if (error) {
1174
- reject(this.#toPublishError(error, eventId, routingKey));
1175
- } else if (returned) {
1176
- reject(
1177
- new UnroutableMessageError(
1178
- `[RabbitMqTransport](publish) No queue is bound to the routing key | transport=${this.name} eventId=${eventId} destination=${routingKey}`
1179
- )
1180
- );
1181
- } else {
1182
- resolve();
1183
- }
1184
- };
1185
- try {
1186
- publisher.channel.publish(
1187
- exchange,
1188
- routingKey,
1189
- content,
1190
- { ...options, mandatory },
1191
- onConfirm
1192
- );
1193
- } catch (error) {
1194
- clearTimeout(timer);
1195
- reject(this.#toPublishError(error, eventId, routingKey));
1196
- }
1444
+ mandatory: !this.#settings.allowUnroutable
1197
1445
  });
1198
1446
  }
1199
1447
  /**
@@ -1201,7 +1449,11 @@ var RabbitMqTransport = class {
1201
1449
  * to `onMessage`. Call it once per transport; give each subscription its own
1202
1450
  * transport instance, with its own `name` and `consumerGroup`.
1203
1451
  */
1204
- async subscribe(destinations, onMessage) {
1452
+ async openSubscription({
1453
+ destinations,
1454
+ onMessage,
1455
+ hooks
1456
+ }) {
1205
1457
  if (this.#subscription !== void 0) {
1206
1458
  throw new ConfigurationError(
1207
1459
  `Transport '${this.name}' already has a subscription; create another transport for another consumer group`
@@ -1212,33 +1464,8 @@ var RabbitMqTransport = class {
1212
1464
  }
1213
1465
  const queue = resolveQueueName(this.#settings);
1214
1466
  const subscription = {
1215
- topology: {
1216
- exchange: this.#settings.exchange,
1217
- queue,
1218
- deadLetterExchange: this.#settings.deadLetterExchange,
1219
- deadLetterQueue: this.#settings.deadLetterQueue ?? `${queue}.dead`,
1220
- deliveryLimit: this.#settings.deliveryLimit,
1221
- ordered: this.#settings.ordered,
1222
- retryDelaysMs: this.#settings.retryLadder,
1223
- destinations
1224
- },
1225
- consumer: new QueueConsumer({
1226
- queue,
1227
- prefetch: this.#settings.prefetch,
1228
- handlerRetry: this.#settings.handlerRetry,
1229
- redelivery: this.#settings.redelivery,
1230
- retryLadder: this.#settings.retryLadder,
1231
- redeliveryMaxAgeMs: this.#settings.redeliveryMaxAgeMs,
1232
- ordered: this.#settings.ordered,
1233
- deadLetterExchange: this.#settings.deadLetterExchange,
1234
- republish: (exchange, routingKey, content, options) => this.#publishConfirmed(exchange, routingKey, content, options, true),
1235
- shutdownTimeoutMs: this.#settings.shutdownTimeoutMs,
1236
- onMessage,
1237
- decode: this.#settings.decode,
1238
- onConsumerLost: (reason) => this.#supervisor.recover(reason),
1239
- logger: this.#settings.logger,
1240
- random: this.#settings.random
1241
- })
1467
+ topology: this.#topologyOf(queue, destinations),
1468
+ consumer: this.#createConsumer(queue, { onMessage, hooks })
1242
1469
  };
1243
1470
  await this.#topologyWork.run(async () => {
1244
1471
  this.#subscription = subscription;
@@ -1247,24 +1474,70 @@ var RabbitMqTransport = class {
1247
1474
  }
1248
1475
  });
1249
1476
  }
1477
+ #topologyOf(queue, destinations) {
1478
+ return {
1479
+ exchange: this.#settings.exchange,
1480
+ queue,
1481
+ deadLetterExchange: this.#settings.deadLetterExchange,
1482
+ deadLetterQueue: this.#settings.deadLetterQueue ?? `${queue}.dead`,
1483
+ deliveryLimit: this.#settings.deliveryLimit,
1484
+ ordered: this.#settings.ordered,
1485
+ retryDelaysMs: this.#settings.retryLadder,
1486
+ destinations
1487
+ };
1488
+ }
1489
+ #createConsumer(queue, { onMessage, hooks }) {
1490
+ return new QueueConsumer({
1491
+ queue,
1492
+ prefetch: this.#settings.prefetch,
1493
+ handlerRetry: this.#settings.handlerRetry,
1494
+ redelivery: this.#settings.redelivery,
1495
+ retryLadder: this.#settings.retryLadder,
1496
+ redeliveryMaxAgeMs: this.#settings.redeliveryMaxAgeMs,
1497
+ ordered: this.#settings.ordered,
1498
+ deadLetterExchange: this.#settings.deadLetterExchange,
1499
+ republish: ({ exchange, routingKey }, { content, options }) => this.#publishing.publish(this.#live?.publisher, {
1500
+ exchange,
1501
+ routingKey,
1502
+ content,
1503
+ options,
1504
+ mandatory: true
1505
+ }),
1506
+ shutdownTimeoutMs: this.#settings.shutdownTimeoutMs,
1507
+ onMessage,
1508
+ ...hooks === void 0 ? {} : { onInvalidMessage: hooks.onInvalidMessage },
1509
+ decode: this.#settings.decode,
1510
+ onConsumerLost: (reason) => this.#supervisor.recover(reason),
1511
+ logger: this.#settings.logger,
1512
+ random: this.#settings.random
1513
+ });
1514
+ }
1250
1515
  /**
1251
1516
  * Stops consuming, waits up to `shutdownTimeout` for running handlers
1252
1517
  * (messages still being retried go back to the queue), then closes the
1253
- * channels and the connection.
1518
+ * channels and the connection. Once `options.signal` aborts (the bus shutdown deadline passed),
1519
+ * the connection's socket is destroyed instead of waiting for the broker.
1254
1520
  */
1255
- async close() {
1256
- if (this.#closed) {
1257
- return;
1521
+ async close(options = {}) {
1522
+ const { signal } = options;
1523
+ const force = () => this.#supervisor.forceClose();
1524
+ if (signal?.aborted === true) {
1525
+ force();
1526
+ }
1527
+ signal?.addEventListener("abort", force, { once: true });
1528
+ try {
1529
+ this.#closing ??= this.#supervisor.close(
1530
+ () => this.#topologyWork.run(async () => {
1531
+ const live = this.#live;
1532
+ this.#live = void 0;
1533
+ await this.#subscription?.consumer.stop();
1534
+ await live?.publisher.channel.close().catch(() => void 0);
1535
+ })
1536
+ );
1537
+ await this.#closing;
1538
+ } finally {
1539
+ signal?.removeEventListener("abort", force);
1258
1540
  }
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
1541
  }
1269
1542
  async #establishChannels(connection) {
1270
1543
  await this.#topologyWork.run(async () => {
@@ -1308,64 +1581,8 @@ var RabbitMqTransport = class {
1308
1581
  this.#live = void 0;
1309
1582
  this.#subscription?.consumer.detach();
1310
1583
  }
1311
- #toPublishError(error, eventId, destination) {
1312
- const { message } = toErrorInfo(error);
1313
- const detail = `transport=${this.name} eventId=${eventId} destination=${destination} error=${message}`;
1314
- return message === NACK_MESSAGE ? new RabbitMqPublishRejectedError(
1315
- `[RabbitMqTransport](publish) Broker nacked the message | ${detail}`,
1316
- { cause: error }
1317
- ) : new RabbitMqUnavailableError(`[RabbitMqTransport](publish) Publish failed | ${detail}`, {
1318
- cause: error
1319
- });
1320
- }
1321
1584
  };
1322
- function unroutableKey(messageId, routingKey) {
1323
- return `${String(messageId)}
1324
- ${routingKey}`;
1325
- }
1326
- function recordUnroutable(unroutable, key) {
1327
- unroutable.set(key, (unroutable.get(key) ?? 0) + 1);
1328
- }
1329
- function takeUnroutable(unroutable, key) {
1330
- const count = unroutable.get(key);
1331
- if (count === void 0) {
1332
- return false;
1333
- }
1334
- if (count === 1) {
1335
- unroutable.delete(key);
1336
- } else {
1337
- unroutable.set(key, count - 1);
1338
- }
1339
- return true;
1340
- }
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
1585
 
1369
- export { AmqpLimitExceededError, MalformedMessageError, RabbitMqPublishRejectedError, RabbitMqTransport, RabbitMqUnavailableError, UnroutableMessageError, UnserializablePayloadError, decodeCloudEvent, fromHeader, fromPayloadField, plainJsonDecoder };
1586
+ export { AmqpLimitExceededError, MalformedMessageError, RabbitMqPublishRejectedError, RabbitMqTransport, RabbitMqUnavailableError, RabbitMqUnencodableEventError, UnroutableMessageError, UnserializablePayloadError, cloudEventsBinary, cloudEventsStructured, decodeCloudEvent, jsonEnvelope, plainJsonDecoder };
1370
1587
  //# sourceMappingURL=index.js.map
1371
1588
  //# sourceMappingURL=index.js.map