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