@relaybus/rabbitmq 0.1.0 → 0.3.0

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