@relaybus/rabbitmq 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { RelayBusError, NonRetryableError, ConfigurationError, toErrorInfo, createRetryPolicy, toMilliseconds, computeRetryDelay, isRetryableError } from '@relaybus/core';
1
+ import { RelayBusError, NonRetryableError, assertEventType, ConfigurationError, toErrorInfo, createRetryPolicy, toMilliseconds, computeRetryDelay, isRetryableError } from '@relaybus/core';
2
2
 
3
3
  // src/errors.ts
4
4
  var RabbitMqUnavailableError = class extends RelayBusError {
@@ -178,9 +178,417 @@ var ConnectionSupervisor = class {
178
178
  }
179
179
  }
180
180
  };
181
+
182
+ // src/json-payload.ts
183
+ function assertJsonPayload(payload, eventId) {
184
+ visit(payload, "$", /* @__PURE__ */ new Set(), eventId);
185
+ }
186
+ function visit(value, path, ancestors, eventId) {
187
+ switch (typeof value) {
188
+ case "string":
189
+ case "boolean":
190
+ return;
191
+ case "number":
192
+ if (!Number.isFinite(value)) {
193
+ throw refuse(eventId, path, `is ${value}, which JSON turns into null`);
194
+ }
195
+ return;
196
+ case "object":
197
+ if (value !== null) {
198
+ visitObject(value, path, ancestors, eventId);
199
+ }
200
+ return;
201
+ default:
202
+ throw refuse(eventId, path, `is ${typeof value}, which JSON cannot represent`);
203
+ }
204
+ }
205
+ function visitObject(value, path, ancestors, eventId) {
206
+ if (ancestors.has(value)) {
207
+ throw refuse(eventId, path, "is a circular reference");
208
+ }
209
+ ancestors.add(value);
210
+ if (Array.isArray(value)) {
211
+ for (let index = 0; index < value.length; index += 1) {
212
+ if (!(index in value)) {
213
+ throw refuse(eventId, `${path}[${index}]`, "is an array hole, which JSON turns into null");
214
+ }
215
+ visit(value[index], `${path}[${index}]`, ancestors, eventId);
216
+ }
217
+ } else {
218
+ const prototype = Object.getPrototypeOf(value);
219
+ if (prototype !== Object.prototype && prototype !== null) {
220
+ throw refuse(eventId, path, "is not a plain object (class instance, Date, Map or Set)");
221
+ }
222
+ if (Object.getOwnPropertySymbols(value).length > 0) {
223
+ throw refuse(eventId, path, "has a symbol key, which JSON drops");
224
+ }
225
+ for (const key of Object.keys(value)) {
226
+ visit(value[key], `${path}.${key}`, ancestors, eventId);
227
+ }
228
+ }
229
+ ancestors.delete(value);
230
+ }
231
+ function refuse(eventId, path, reason) {
232
+ return new UnserializablePayloadError(
233
+ `Event ${eventId} has a payload JSON cannot represent faithfully: ${path} ${reason}`
234
+ );
235
+ }
236
+
237
+ // src/wire-format.ts
238
+ var ATTRIBUTE_PREFIX = "cloudEvents:";
239
+ var EVENT_HEADER_PREFIX = "relaybus:";
240
+ var SPEC_VERSION = "1.0";
241
+ var JSON_CONTENT_TYPE = "application/json";
242
+ var SHORT_STRING_LIMIT_BYTES = 255;
243
+ var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
244
+ function encodeEnvelope(envelope) {
245
+ assertJsonPayload(envelope.payload, envelope.id);
246
+ assertShortString("Event id", envelope.id);
247
+ const headers = {
248
+ [`${ATTRIBUTE_PREFIX}specversion`]: SPEC_VERSION,
249
+ [`${ATTRIBUTE_PREFIX}id`]: envelope.id,
250
+ [`${ATTRIBUTE_PREFIX}type`]: envelope.type,
251
+ [`${ATTRIBUTE_PREFIX}source`]: envelope.source,
252
+ [`${ATTRIBUTE_PREFIX}time`]: envelope.occurredAt.toISOString(),
253
+ [`${ATTRIBUTE_PREFIX}datacontenttype`]: JSON_CONTENT_TYPE
254
+ };
255
+ if (envelope.key !== null) {
256
+ headers[`${ATTRIBUTE_PREFIX}subject`] = envelope.key;
257
+ }
258
+ if (envelope.causationId !== null) {
259
+ headers[`${ATTRIBUTE_PREFIX}causationid`] = envelope.causationId;
260
+ }
261
+ if (envelope.correlationId !== null) {
262
+ headers[`${ATTRIBUTE_PREFIX}correlationid`] = envelope.correlationId;
263
+ }
264
+ for (const [name, value] of Object.entries(envelope.headers)) {
265
+ assertShortString(
266
+ `Header name '${EVENT_HEADER_PREFIX}${name}'`,
267
+ `${EVENT_HEADER_PREFIX}${name}`
268
+ );
269
+ headers[`${EVENT_HEADER_PREFIX}${name}`] = value;
270
+ }
271
+ return {
272
+ content: Buffer.from(JSON.stringify(envelope.payload), "utf8"),
273
+ options: {
274
+ contentType: JSON_CONTENT_TYPE,
275
+ persistent: true,
276
+ messageId: envelope.id,
277
+ timestamp: Math.floor(envelope.occurredAt.getTime() / 1e3),
278
+ ...mirror("type", envelope.type),
279
+ ...mirror("appId", envelope.source),
280
+ ...envelope.correlationId === null ? {} : mirror("correlationId", envelope.correlationId),
281
+ headers
282
+ }
283
+ };
284
+ }
285
+ function assertShortString(label, value) {
286
+ const bytes = Buffer.byteLength(value, "utf8");
287
+ if (bytes > SHORT_STRING_LIMIT_BYTES) {
288
+ throw new AmqpLimitExceededError(
289
+ `${label} is ${bytes} bytes, above the AMQP limit of ${SHORT_STRING_LIMIT_BYTES} bytes`
290
+ );
291
+ }
292
+ }
293
+ function mirror(name, value) {
294
+ return Buffer.byteLength(value, "utf8") > SHORT_STRING_LIMIT_BYTES ? {} : { [name]: value };
295
+ }
296
+ function decodeEnvelope(message) {
297
+ const { properties } = message;
298
+ const attributes = properties.headers ?? {};
299
+ if (attributes[`${ATTRIBUTE_PREFIX}specversion`] !== SPEC_VERSION) {
300
+ throw new MalformedMessageError(
301
+ `Message is not a CloudEvents ${SPEC_VERSION} event (header ${ATTRIBUTE_PREFIX}specversion)`
302
+ );
303
+ }
304
+ return {
305
+ id: required("id", text(attributes[`${ATTRIBUTE_PREFIX}id`]) ?? text(properties.messageId)),
306
+ type: required("type", text(attributes[`${ATTRIBUTE_PREFIX}type`]) ?? text(properties.type)),
307
+ source: required(
308
+ "source",
309
+ text(attributes[`${ATTRIBUTE_PREFIX}source`]) ?? text(properties.appId)
310
+ ),
311
+ key: text(attributes[`${ATTRIBUTE_PREFIX}subject`]) ?? null,
312
+ payload: parsePayload(message.content),
313
+ headers: extractEventHeaders(attributes),
314
+ correlationId: text(attributes[`${ATTRIBUTE_PREFIX}correlationid`]) ?? text(properties.correlationId) ?? null,
315
+ causationId: text(attributes[`${ATTRIBUTE_PREFIX}causationid`]) ?? null,
316
+ occurredAt: parseTime(attributes[`${ATTRIBUTE_PREFIX}time`], properties.timestamp)
317
+ };
318
+ }
319
+ function extractEventHeaders(attributes) {
320
+ const headers = {};
321
+ for (const [name, value] of Object.entries(attributes)) {
322
+ if (name.startsWith(EVENT_HEADER_PREFIX) && typeof value === "string") {
323
+ Object.defineProperty(headers, name.slice(EVENT_HEADER_PREFIX.length), {
324
+ value,
325
+ enumerable: true,
326
+ writable: true,
327
+ configurable: true
328
+ });
329
+ }
330
+ }
331
+ return headers;
332
+ }
333
+ function parsePayload(content) {
334
+ try {
335
+ return JSON.parse(content.toString("utf8"));
336
+ } catch {
337
+ throw new MalformedMessageError("Message body is not valid JSON");
338
+ }
339
+ }
340
+ function parseTime(rawTime, timestampSeconds) {
341
+ if (rawTime === void 0) {
342
+ const fallback = typeof timestampSeconds === "number" ? new Date(timestampSeconds * 1e3) : void 0;
343
+ if (fallback === void 0 || Number.isNaN(fallback.getTime())) {
344
+ throw new MalformedMessageError("Message has no valid time (header cloudEvents:time)");
345
+ }
346
+ return fallback;
347
+ }
348
+ const occurredAt = typeof rawTime === "string" && RFC3339_PATTERN.test(rawTime) ? new Date(rawTime) : void 0;
349
+ if (occurredAt === void 0 || Number.isNaN(occurredAt.getTime())) {
350
+ throw new MalformedMessageError(
351
+ "Message time (header cloudEvents:time) is not an RFC 3339 timestamp"
352
+ );
353
+ }
354
+ return occurredAt;
355
+ }
356
+ function text(value) {
357
+ return typeof value === "string" ? value : void 0;
358
+ }
359
+ function required(attribute, value) {
360
+ if (value === void 0 || value.length === 0) {
361
+ throw new MalformedMessageError(
362
+ `Message has no ${attribute} (header ${ATTRIBUTE_PREFIX}${attribute})`
363
+ );
364
+ }
365
+ return value;
366
+ }
367
+
368
+ // src/inbound-decoders.ts
369
+ var STRUCTURED_MEDIA_TYPE = "application/cloudevents+json";
370
+ var strictUtf8 = new TextDecoder("utf-8", { fatal: true });
371
+ var SPEC_VERSION2 = "1.0";
372
+ var SPEC_VERSION_HEADER = "cloudEvents:specversion";
373
+ var INTERNAL_HEADER_PREFIXES = ["x-relaybus-", "x-first-death-", "x-last-death-"];
374
+ var STRUCTURED_ATTRIBUTES = /* @__PURE__ */ new Set([
375
+ "specversion",
376
+ "id",
377
+ "type",
378
+ "source",
379
+ "time",
380
+ "subject",
381
+ "datacontenttype",
382
+ "dataschema",
383
+ "data",
384
+ "data_base64",
385
+ "correlationid",
386
+ "causationid"
387
+ ]);
388
+ function decodeCloudEvent(message) {
389
+ if (mediaTypeOf(message.properties.contentType) !== STRUCTURED_MEDIA_TYPE) {
390
+ return decodeEnvelope(message);
391
+ }
392
+ const body = parseJsonBody(message.content);
393
+ if (!isJsonObject(body)) {
394
+ throw new MalformedMessageError("A structured CloudEvent must be a JSON object");
395
+ }
396
+ if (body["specversion"] !== SPEC_VERSION2) {
397
+ throw new MalformedMessageError(`specversion must be '${SPEC_VERSION2}'`);
398
+ }
399
+ const type = requiredAttribute(body, "type");
400
+ assertValidType(type, "type");
401
+ assertJsonDataContentType(optionalAttribute(body, "datacontenttype"));
402
+ if (Object.hasOwn(body, "data_base64")) {
403
+ throw new MalformedMessageError(
404
+ "data_base64 is not supported: relaybus payloads are JSON, send them as data"
405
+ );
406
+ }
407
+ const time = optionalAttribute(body, "time");
408
+ return {
409
+ id: requiredAttribute(body, "id"),
410
+ type,
411
+ source: requiredAttribute(body, "source"),
412
+ key: optionalAttribute(body, "subject") ?? null,
413
+ payload: Object.hasOwn(body, "data") ? body["data"] : null,
414
+ headers: { ...readForeignHeaders(message.properties), ...extensionAttributes(body) },
415
+ correlationId: optionalAttribute(body, "correlationid") ?? null,
416
+ causationId: optionalAttribute(body, "causationid") ?? null,
417
+ occurredAt: time === void 0 ? propertyTimeOrNow(message.properties) : parseTime2(time)
418
+ };
419
+ }
420
+ function plainJsonDecoder(options) {
421
+ if (typeof options.type === "string") {
422
+ assertEventType(options.type);
423
+ }
424
+ return (message) => {
425
+ const { properties } = message;
426
+ if (properties.headers?.[SPEC_VERSION_HEADER] !== void 0 || mediaTypeOf(properties.contentType) === STRUCTURED_MEDIA_TYPE) {
427
+ return decodeCloudEvent(message);
428
+ }
429
+ const plain = {
430
+ routingKey: message.fields.routingKey,
431
+ properties,
432
+ headers: readForeignHeaders(properties),
433
+ payload: parseJsonBody(message.content)
434
+ };
435
+ const type = resolveText(options.type, plain, "type");
436
+ if (type === void 0) {
437
+ throw new MalformedMessageError("Message has no event type: the type option found none");
438
+ }
439
+ assertValidType(type, "The event type");
440
+ const id = resolveText(options.id, plain, "id") ?? nonEmptyText(properties.messageId);
441
+ if (id === void 0) {
442
+ throw new MalformedMessageError(
443
+ "Message has no id: set the messageId property on the producer or configure the id option"
444
+ );
445
+ }
446
+ return {
447
+ id,
448
+ type,
449
+ source: resolveText(options.source, plain, "source") ?? defaultSource(message),
450
+ key: resolveText(options.key, plain, "key") ?? null,
451
+ payload: plain.payload,
452
+ headers: plain.headers,
453
+ correlationId: null,
454
+ causationId: null,
455
+ occurredAt: resolveOccurredAt(options.occurredAt, plain)
456
+ };
457
+ };
458
+ }
459
+ function completeDecodedEvent(decoded, message) {
460
+ if (typeof decoded !== "object" || decoded === null) {
461
+ throw new MalformedMessageError("The decoder must return an event object synchronously");
462
+ }
463
+ if ("then" in decoded) {
464
+ observeRejection(decoded);
465
+ throw new MalformedMessageError("The decoder must return an event object synchronously");
466
+ }
467
+ const event = decoded;
468
+ return {
469
+ id: event.id,
470
+ type: event.type,
471
+ source: event.source ?? defaultSource(message),
472
+ key: event.key ?? null,
473
+ payload: event.payload,
474
+ headers: event.headers ?? {},
475
+ correlationId: event.correlationId ?? null,
476
+ causationId: event.causationId ?? null,
477
+ occurredAt: event.occurredAt ?? propertyTimeOrNow(message.properties)
478
+ };
479
+ }
480
+ function parseJsonBody(content) {
481
+ let text2;
482
+ try {
483
+ text2 = strictUtf8.decode(content);
484
+ } catch {
485
+ throw new MalformedMessageError("Message body is not valid UTF-8");
486
+ }
487
+ try {
488
+ return JSON.parse(text2);
489
+ } catch {
490
+ throw new MalformedMessageError("Message body is not valid JSON");
491
+ }
492
+ }
493
+ function mediaTypeOf(contentType) {
494
+ return typeof contentType === "string" ? contentType.split(";")[0].trim().toLowerCase() : void 0;
495
+ }
496
+ function isJsonObject(value) {
497
+ return typeof value === "object" && value !== null && !Array.isArray(value);
498
+ }
499
+ function nonEmptyText(value) {
500
+ return typeof value === "string" && value.length > 0 ? value : void 0;
501
+ }
502
+ function defaultSource(message) {
503
+ return nonEmptyText(message.properties.appId) ?? message.fields.routingKey;
504
+ }
505
+ function optionalAttribute(body, name) {
506
+ const value = body[name];
507
+ if (value !== void 0 && typeof value !== "string") {
508
+ throw new MalformedMessageError(`Attribute '${name}' must be a string`);
509
+ }
510
+ return value;
511
+ }
512
+ function requiredAttribute(body, name) {
513
+ const value = optionalAttribute(body, name);
514
+ if (value === void 0 || value.length === 0) {
515
+ throw new MalformedMessageError(`Attribute '${name}' is missing or empty`);
516
+ }
517
+ return value;
518
+ }
519
+ function assertValidType(type, label) {
520
+ try {
521
+ assertEventType(type);
522
+ } catch (error) {
523
+ throw new MalformedMessageError(
524
+ `${label} is not a valid event type: ${error.message}`
525
+ );
526
+ }
527
+ }
528
+ function assertJsonDataContentType(dataContentType) {
529
+ const mediaType = mediaTypeOf(dataContentType);
530
+ if (mediaType !== void 0 && mediaType !== "application/json" && mediaType !== "text/json" && !mediaType.endsWith("+json")) {
531
+ throw new MalformedMessageError(`datacontenttype must be JSON, received '${dataContentType}'`);
532
+ }
533
+ }
534
+ function parseTime2(text2) {
535
+ const time = new Date(text2);
536
+ if (!RFC3339_PATTERN.test(text2) || Number.isNaN(time.getTime())) {
537
+ throw new MalformedMessageError(`time is not an RFC 3339 timestamp: '${text2}'`);
538
+ }
539
+ return time;
540
+ }
541
+ function propertyTimeOrNow(properties) {
542
+ const { timestamp } = properties;
543
+ return typeof timestamp === "number" && Number.isFinite(timestamp) ? new Date(timestamp * 1e3) : /* @__PURE__ */ new Date();
544
+ }
545
+ function extensionAttributes(body) {
546
+ return Object.fromEntries(
547
+ Object.entries(body).filter(
548
+ (entry) => !STRUCTURED_ATTRIBUTES.has(entry[0]) && typeof entry[1] === "string"
549
+ )
550
+ );
551
+ }
552
+ function readForeignHeaders(properties) {
553
+ return Object.fromEntries(
554
+ Object.entries(properties.headers ?? {}).filter(
555
+ (entry) => typeof entry[1] === "string" && !INTERNAL_HEADER_PREFIXES.some((prefix) => entry[0].startsWith(prefix))
556
+ )
557
+ );
558
+ }
559
+ function resolveText(option, message, name) {
560
+ const value = typeof option === "function" ? option(message) : option;
561
+ if (value === void 0 || value === null) {
562
+ return void 0;
563
+ }
564
+ if (typeof value !== "string") {
565
+ observeRejection(value);
566
+ throw new MalformedMessageError(
567
+ `The ${name} option returned ${typeof value}, expected a string`
568
+ );
569
+ }
570
+ return value.length === 0 ? void 0 : value;
571
+ }
572
+ function resolveOccurredAt(option, message) {
573
+ const value = option?.(message);
574
+ if (value === void 0) {
575
+ return propertyTimeOrNow(message.properties);
576
+ }
577
+ if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
578
+ observeRejection(value);
579
+ throw new MalformedMessageError("The occurredAt option returned no valid Date");
580
+ }
581
+ return value;
582
+ }
583
+ function observeRejection(value) {
584
+ if (typeof value === "object" && value !== null && "then" in value) {
585
+ Promise.resolve(value).catch(() => void 0);
586
+ }
587
+ }
181
588
  var REDELIVERIES_HEADER = "x-relaybus-redeliveries";
182
589
  var FIRST_FAILURE_HEADER = "x-relaybus-first-failure-at";
183
590
  var DEAD_LETTER_REASON_HEADER = "x-relaybus-dead-letter-reason";
591
+ var ORIGINAL_ROUTING_KEY_HEADER = "x-relaybus-routing-key";
184
592
  var RELAYBUS_STATE_HEADERS = [REDELIVERIES_HEADER, FIRST_FAILURE_HEADER];
185
593
  var BROKER_COUNTER_HEADERS = ["x-delivery-count", "x-acquired-count"];
186
594
  var MAXIMUM_LADDER_TIERS = 16;
@@ -217,7 +625,7 @@ function chooseRetryTier(ladder, delayMs, random) {
217
625
  return random() < (delayMs - lower) / (upper - lower) ? upper : lower;
218
626
  }
219
627
  function readCounterHeader(message, name) {
220
- const value = message.properties.headers[name];
628
+ const value = message.properties.headers?.[name];
221
629
  const parsed = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : value;
222
630
  return typeof parsed === "number" && Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
223
631
  }
@@ -246,6 +654,14 @@ function copyForRepublish(message, headerChanges) {
246
654
  }
247
655
  };
248
656
  }
657
+ function originalRoutingKey(message) {
658
+ const original = message.properties.headers?.[ORIGINAL_ROUTING_KEY_HEADER];
659
+ return typeof original === "string" ? original : message.fields.routingKey;
660
+ }
661
+ function withOriginalRoutingKey(message) {
662
+ const routingKey = originalRoutingKey(message);
663
+ return routingKey === message.fields.routingKey ? message : { ...message, fields: { ...message.fields, routingKey } };
664
+ }
249
665
 
250
666
  // src/options.ts
251
667
  var DEFAULT_HANDLER_RETRY = {
@@ -314,6 +730,7 @@ function resolveSettings(options) {
314
730
  allowUnroutable: options.allowUnroutable ?? false,
315
731
  publishTimeoutMs: positiveDuration(options.publishTimeout ?? "30s", "publishTimeout"),
316
732
  shutdownTimeoutMs: toMilliseconds(options.shutdownTimeout ?? "30s", "shutdownTimeout"),
733
+ decode: resolveDecode(options.decode),
317
734
  logger: options.logger ?? console,
318
735
  random: options.random ?? Math.random
319
736
  };
@@ -329,6 +746,12 @@ function resolveQueueName(settings) {
329
746
  }
330
747
  return `${settings.queuePrefix}.${settings.consumerGroup}`;
331
748
  }
749
+ function resolveDecode(decode) {
750
+ if (decode === void 0) {
751
+ return decodeEnvelope;
752
+ }
753
+ return (message) => completeDecodedEvent(decode(message), message);
754
+ }
332
755
  function resolveConnect(options) {
333
756
  const { url, connect } = options;
334
757
  if (url === void 0 === (connect === void 0)) {
@@ -416,192 +839,6 @@ async function assertConsumerTopology(channel, topology) {
416
839
  }
417
840
  }
418
841
 
419
- // src/json-payload.ts
420
- function assertJsonPayload(payload, eventId) {
421
- visit(payload, "$", /* @__PURE__ */ new Set(), eventId);
422
- }
423
- function visit(value, path, ancestors, eventId) {
424
- switch (typeof value) {
425
- case "string":
426
- case "boolean":
427
- return;
428
- case "number":
429
- if (!Number.isFinite(value)) {
430
- throw refuse(eventId, path, `is ${value}, which JSON turns into null`);
431
- }
432
- return;
433
- case "object":
434
- if (value !== null) {
435
- visitObject(value, path, ancestors, eventId);
436
- }
437
- return;
438
- default:
439
- throw refuse(eventId, path, `is ${typeof value}, which JSON cannot represent`);
440
- }
441
- }
442
- function visitObject(value, path, ancestors, eventId) {
443
- if (ancestors.has(value)) {
444
- throw refuse(eventId, path, "is a circular reference");
445
- }
446
- ancestors.add(value);
447
- if (Array.isArray(value)) {
448
- for (let index = 0; index < value.length; index += 1) {
449
- if (!(index in value)) {
450
- throw refuse(eventId, `${path}[${index}]`, "is an array hole, which JSON turns into null");
451
- }
452
- visit(value[index], `${path}[${index}]`, ancestors, eventId);
453
- }
454
- } else {
455
- const prototype = Object.getPrototypeOf(value);
456
- if (prototype !== Object.prototype && prototype !== null) {
457
- throw refuse(eventId, path, "is not a plain object (class instance, Date, Map or Set)");
458
- }
459
- if (Object.getOwnPropertySymbols(value).length > 0) {
460
- throw refuse(eventId, path, "has a symbol key, which JSON drops");
461
- }
462
- for (const key of Object.keys(value)) {
463
- visit(value[key], `${path}.${key}`, ancestors, eventId);
464
- }
465
- }
466
- ancestors.delete(value);
467
- }
468
- function refuse(eventId, path, reason) {
469
- return new UnserializablePayloadError(
470
- `Event ${eventId} has a payload JSON cannot represent faithfully: ${path} ${reason}`
471
- );
472
- }
473
-
474
- // src/wire-format.ts
475
- var ATTRIBUTE_PREFIX = "cloudEvents:";
476
- var EVENT_HEADER_PREFIX = "relaybus:";
477
- var SPEC_VERSION = "1.0";
478
- var JSON_CONTENT_TYPE = "application/json";
479
- var SHORT_STRING_LIMIT_BYTES = 255;
480
- var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
481
- function encodeEnvelope(envelope) {
482
- assertJsonPayload(envelope.payload, envelope.id);
483
- assertShortString("Event id", envelope.id);
484
- const headers = {
485
- [`${ATTRIBUTE_PREFIX}specversion`]: SPEC_VERSION,
486
- [`${ATTRIBUTE_PREFIX}id`]: envelope.id,
487
- [`${ATTRIBUTE_PREFIX}type`]: envelope.type,
488
- [`${ATTRIBUTE_PREFIX}source`]: envelope.source,
489
- [`${ATTRIBUTE_PREFIX}time`]: envelope.occurredAt.toISOString(),
490
- [`${ATTRIBUTE_PREFIX}datacontenttype`]: JSON_CONTENT_TYPE
491
- };
492
- if (envelope.key !== null) {
493
- headers[`${ATTRIBUTE_PREFIX}subject`] = envelope.key;
494
- }
495
- if (envelope.causationId !== null) {
496
- headers[`${ATTRIBUTE_PREFIX}causationid`] = envelope.causationId;
497
- }
498
- if (envelope.correlationId !== null) {
499
- headers[`${ATTRIBUTE_PREFIX}correlationid`] = envelope.correlationId;
500
- }
501
- for (const [name, value] of Object.entries(envelope.headers)) {
502
- assertShortString(
503
- `Header name '${EVENT_HEADER_PREFIX}${name}'`,
504
- `${EVENT_HEADER_PREFIX}${name}`
505
- );
506
- headers[`${EVENT_HEADER_PREFIX}${name}`] = value;
507
- }
508
- return {
509
- content: Buffer.from(JSON.stringify(envelope.payload), "utf8"),
510
- options: {
511
- contentType: JSON_CONTENT_TYPE,
512
- persistent: true,
513
- messageId: envelope.id,
514
- timestamp: Math.floor(envelope.occurredAt.getTime() / 1e3),
515
- ...mirror("type", envelope.type),
516
- ...mirror("appId", envelope.source),
517
- ...envelope.correlationId === null ? {} : mirror("correlationId", envelope.correlationId),
518
- headers
519
- }
520
- };
521
- }
522
- function assertShortString(label, value) {
523
- const bytes = Buffer.byteLength(value, "utf8");
524
- if (bytes > SHORT_STRING_LIMIT_BYTES) {
525
- throw new AmqpLimitExceededError(
526
- `${label} is ${bytes} bytes, above the AMQP limit of ${SHORT_STRING_LIMIT_BYTES} bytes`
527
- );
528
- }
529
- }
530
- function mirror(name, value) {
531
- return Buffer.byteLength(value, "utf8") > SHORT_STRING_LIMIT_BYTES ? {} : { [name]: value };
532
- }
533
- function decodeEnvelope(message) {
534
- const { properties } = message;
535
- const attributes = properties.headers ?? {};
536
- if (attributes[`${ATTRIBUTE_PREFIX}specversion`] !== SPEC_VERSION) {
537
- throw new MalformedMessageError(
538
- `Message is not a CloudEvents ${SPEC_VERSION} event (header ${ATTRIBUTE_PREFIX}specversion)`
539
- );
540
- }
541
- return {
542
- id: required("id", text(attributes[`${ATTRIBUTE_PREFIX}id`]) ?? text(properties.messageId)),
543
- type: required("type", text(attributes[`${ATTRIBUTE_PREFIX}type`]) ?? text(properties.type)),
544
- source: required(
545
- "source",
546
- text(attributes[`${ATTRIBUTE_PREFIX}source`]) ?? text(properties.appId)
547
- ),
548
- key: text(attributes[`${ATTRIBUTE_PREFIX}subject`]) ?? null,
549
- payload: parsePayload(message.content),
550
- headers: extractEventHeaders(attributes),
551
- correlationId: text(attributes[`${ATTRIBUTE_PREFIX}correlationid`]) ?? text(properties.correlationId) ?? null,
552
- causationId: text(attributes[`${ATTRIBUTE_PREFIX}causationid`]) ?? null,
553
- occurredAt: parseTime(attributes[`${ATTRIBUTE_PREFIX}time`], properties.timestamp)
554
- };
555
- }
556
- function extractEventHeaders(attributes) {
557
- const headers = {};
558
- for (const [name, value] of Object.entries(attributes)) {
559
- if (name.startsWith(EVENT_HEADER_PREFIX) && typeof value === "string") {
560
- Object.defineProperty(headers, name.slice(EVENT_HEADER_PREFIX.length), {
561
- value,
562
- enumerable: true,
563
- writable: true,
564
- configurable: true
565
- });
566
- }
567
- }
568
- return headers;
569
- }
570
- function parsePayload(content) {
571
- try {
572
- return JSON.parse(content.toString("utf8"));
573
- } catch {
574
- throw new MalformedMessageError("Message body is not valid JSON");
575
- }
576
- }
577
- function parseTime(rawTime, timestampSeconds) {
578
- if (rawTime === void 0) {
579
- const fallback = typeof timestampSeconds === "number" ? new Date(timestampSeconds * 1e3) : void 0;
580
- if (fallback === void 0 || Number.isNaN(fallback.getTime())) {
581
- throw new MalformedMessageError("Message has no valid time (header cloudEvents:time)");
582
- }
583
- return fallback;
584
- }
585
- const occurredAt = typeof rawTime === "string" && RFC3339_PATTERN.test(rawTime) ? new Date(rawTime) : void 0;
586
- if (occurredAt === void 0 || Number.isNaN(occurredAt.getTime())) {
587
- throw new MalformedMessageError(
588
- "Message time (header cloudEvents:time) is not an RFC 3339 timestamp"
589
- );
590
- }
591
- return occurredAt;
592
- }
593
- function text(value) {
594
- return typeof value === "string" ? value : void 0;
595
- }
596
- function required(attribute, value) {
597
- if (value === void 0 || value.length === 0) {
598
- throw new MalformedMessageError(
599
- `Message has no ${attribute} (header ${ATTRIBUTE_PREFIX}${attribute})`
600
- );
601
- }
602
- return value;
603
- }
604
-
605
842
  // src/queue-consumer.ts
606
843
  var QueueConsumer = class {
607
844
  #settings;
@@ -700,7 +937,7 @@ var QueueConsumer = class {
700
937
  const { queue, handlerRetry } = this.#settings;
701
938
  let envelope;
702
939
  try {
703
- envelope = decodeEnvelope(message);
940
+ envelope = this.#settings.decode(withOriginalRoutingKey(message));
704
941
  } catch (error) {
705
942
  this.#settings.logger.error(
706
943
  `[QueueConsumer](process) Dead-lettering malformed message | queue=${queue} routingKey=${message.fields.routingKey} error=${toErrorInfo(error).message}`
@@ -780,7 +1017,8 @@ var QueueConsumer = class {
780
1017
  async #redeliverLater(channel, message, delayMs, redeliveries, firstFailureAt) {
781
1018
  const { content, options } = copyForRepublish(message, {
782
1019
  [REDELIVERIES_HEADER]: String(redeliveries),
783
- [FIRST_FAILURE_HEADER]: String(firstFailureAt)
1020
+ [FIRST_FAILURE_HEADER]: String(firstFailureAt),
1021
+ [ORIGINAL_ROUTING_KEY_HEADER]: originalRoutingKey(message)
784
1022
  });
785
1023
  try {
786
1024
  await this.#settings.republish(
@@ -996,6 +1234,7 @@ var RabbitMqTransport = class {
996
1234
  republish: (exchange, routingKey, content, options) => this.#publishConfirmed(exchange, routingKey, content, options, true),
997
1235
  shutdownTimeoutMs: this.#settings.shutdownTimeoutMs,
998
1236
  onMessage,
1237
+ decode: this.#settings.decode,
999
1238
  onConsumerLost: (reason) => this.#supervisor.recover(reason),
1000
1239
  logger: this.#settings.logger,
1001
1240
  random: this.#settings.random
@@ -1099,7 +1338,34 @@ function takeUnroutable(unroutable, key) {
1099
1338
  }
1100
1339
  return true;
1101
1340
  }
1341
+ function fromHeader(name) {
1342
+ if (name.length === 0) {
1343
+ throw new ConfigurationError("fromHeader needs a header name");
1344
+ }
1345
+ return (message) => Object.hasOwn(message.headers, name) ? message.headers[name] : void 0;
1346
+ }
1347
+ function fromPayloadField(path) {
1348
+ const segments = path.split(".");
1349
+ if (segments.some((segment) => segment.length === 0)) {
1350
+ throw new ConfigurationError(
1351
+ `fromPayloadField needs a dotted path of non-empty names, received '${path}'`
1352
+ );
1353
+ }
1354
+ return (message) => {
1355
+ let value = message.payload;
1356
+ for (const segment of segments) {
1357
+ if (typeof value !== "object" || value === null || !Object.hasOwn(value, segment)) {
1358
+ return void 0;
1359
+ }
1360
+ value = value[segment];
1361
+ }
1362
+ if (typeof value === "string") {
1363
+ return value;
1364
+ }
1365
+ return Number.isSafeInteger(value) ? String(value) : void 0;
1366
+ };
1367
+ }
1102
1368
 
1103
- export { AmqpLimitExceededError, MalformedMessageError, RabbitMqPublishRejectedError, RabbitMqTransport, RabbitMqUnavailableError, UnroutableMessageError, UnserializablePayloadError };
1369
+ export { AmqpLimitExceededError, MalformedMessageError, RabbitMqPublishRejectedError, RabbitMqTransport, RabbitMqUnavailableError, UnroutableMessageError, UnserializablePayloadError, decodeCloudEvent, fromHeader, fromPayloadField, plainJsonDecoder };
1104
1370
  //# sourceMappingURL=index.js.map
1105
1371
  //# sourceMappingURL=index.js.map