@babelqueue/artemis 1.0.0 → 1.1.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/README.md CHANGED
@@ -100,6 +100,24 @@ The `bq-` application-property values are strings (integers as decimal, e.g. `"1
100
100
  is `"babelqueue"`. The envelope is unchanged (`schema_version` stays `1`); Artemis is purely
101
101
  additive.
102
102
 
103
+ ## OpenTelemetry tracing (ADR-0028)
104
+
105
+ Cross-hop **span** linkage rides on the out-of-band `HeaderCarrier` from
106
+ [`@babelqueue/core@^1.4.0`](https://www.npmjs.com/package/@babelqueue/core). Pass the carrier produced by
107
+ `@babelqueue/core/otel`'s `publish` to this adapter's `publish({ headers })`; it is carried on
108
+ AMQP `application_properties`, beside the contract `bq_` properties (the contract wins a key collision; `traceparent`/`tracestate` are hyphen-free, so JMS-legal). On consume, the consumer surfaces a delivered message's headers to the handler's third argument (and a `headersOf(...)` extractor reads them back),
109
+ so the core's `otel` `wrapHandler` starts the consumer span as a true **child** of the producer span.
110
+
111
+ ```ts
112
+ import { trace } from "@opentelemetry/api";
113
+ import { publish as tracedPublish } from "@babelqueue/core/otel";
114
+ import type { HeaderCarrier } from "@babelqueue/core";
115
+
116
+ const headers: HeaderCarrier = {};
117
+ await tracedPublish(trace.getTracer("orders"), urn, data,
118
+ () => adapterPublish(urn, data, { headers }), { headers });
119
+ ```
120
+
103
121
  ## License
104
122
 
105
123
  MIT
package/dist/index.cjs CHANGED
@@ -25,20 +25,35 @@ __export(index_exports, {
25
25
  JMS_TYPE_KEY: () => JMS_TYPE_KEY,
26
26
  SCHEDULED_DELIVERY_KEY: () => SCHEDULED_DELIVERY_KEY,
27
27
  artemisMessage: () => artemisMessage,
28
+ headersOf: () => headersOf,
28
29
  messageBody: () => messageBody
29
30
  });
30
31
  module.exports = __toCommonJS(index_exports);
31
32
  var import_core = require("@babelqueue/core");
33
+
34
+ // src/headers.ts
35
+ function sanitizeHeaders(headers) {
36
+ const out = {};
37
+ if (!headers) return out;
38
+ for (const key of Object.keys(headers)) {
39
+ const value = headers[key];
40
+ if (key === "" || value == null || value === "") continue;
41
+ out[key] = value;
42
+ }
43
+ return out;
44
+ }
45
+
46
+ // src/index.ts
32
47
  var JMS_TYPE_KEY = "x-opt-jms-type";
33
48
  var SCHEDULED_DELIVERY_KEY = "x-opt-delivery-time";
34
- function artemisMessage(envelope, delayMs) {
49
+ function artemisMessage(envelope, delayMs, extra) {
35
50
  const annotations = {};
36
51
  if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;
37
- const applicationProperties = {
38
- "bq_schema_version": String(envelope.meta.schema_version),
39
- "bq_attempts": String(envelope.attempts ?? 0),
40
- "bq_app_id": "babelqueue"
41
- };
52
+ const applicationProperties = {};
53
+ for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;
54
+ applicationProperties["bq_schema_version"] = String(envelope.meta.schema_version);
55
+ applicationProperties["bq_attempts"] = String(envelope.attempts ?? 0);
56
+ applicationProperties["bq_app_id"] = "babelqueue";
42
57
  if (envelope.meta.lang) applicationProperties["bq_source_lang"] = envelope.meta.lang;
43
58
  const message = {
44
59
  body: import_core.EnvelopeCodec.encode(envelope),
@@ -74,6 +89,18 @@ function deliveryCount(message) {
74
89
  const value = message.delivery_count;
75
90
  return typeof value === "number" && value > 0 ? value : 0;
76
91
  }
92
+ function headersOf(message) {
93
+ const props = message.application_properties;
94
+ const out = {};
95
+ if (!props) return out;
96
+ for (const key of Object.keys(props)) {
97
+ const value = props[key];
98
+ if (value == null) continue;
99
+ const s = String(value);
100
+ if (s !== "") out[key] = s;
101
+ }
102
+ return out;
103
+ }
77
104
  var ArtemisPublisher = class _ArtemisPublisher {
78
105
  constructor(sender, queue) {
79
106
  this.sender = sender;
@@ -88,7 +115,7 @@ var ArtemisPublisher = class _ArtemisPublisher {
88
115
  /** Build + send the canonical envelope; returns the message id (`meta.id`). */
89
116
  async publish(urn, data, options = {}) {
90
117
  const envelope = import_core.EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });
91
- await this.sender.send(artemisMessage(envelope, options.delayMs));
118
+ await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));
92
119
  return envelope.meta.id;
93
120
  }
94
121
  };
@@ -126,7 +153,7 @@ var ArtemisConsumer = class {
126
153
  return;
127
154
  }
128
155
  try {
129
- await handler(envelope, message);
156
+ await handler(envelope, message, headersOf(message));
130
157
  delivery?.accept();
131
158
  } catch (error) {
132
159
  this.report(error, envelope, context);
@@ -197,6 +224,7 @@ var ArtemisConsumer = class {
197
224
  JMS_TYPE_KEY,
198
225
  SCHEDULED_DELIVERY_KEY,
199
226
  artemisMessage,
227
+ headersOf,
200
228
  messageBody
201
229
  });
202
230
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Apache ActiveMQ Artemis adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **AMQP 1.0** consumer over Artemis (rhea),\n * so an Artemis-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the Java, .NET, Python and Go SDKs.\n *\n * Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —\n * per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so\n * this adapter maps onto them (the envelope stays `schema_version: 1`): the envelope JSON is the\n * message **body**; the contract fields are mirrored onto the AMQP a JMS peer reads\n * (`correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN) plus the `bq_` application properties; consume settles per message\n * (`accept` after success, `release` for redelivery); **`attempts = max(body, delivery-count)`**\n * (the AMQP counter is 0-based — no −1); terminal failures go to an opt-in `<queue>.dlq` with a\n * `dead_letter` block.\n *\n * This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide\n * the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, IncomingEnvelope } from \"@babelqueue/core\";\n\n// --- Minimal AMQP 1.0 shapes (a structural subset of rhea) ---------------------\n\n/** An AMQP message (structural subset of a rhea message). */\nexport interface AmqpMessage {\n body?: unknown;\n correlation_id?: unknown;\n creation_time?: number | Date;\n content_type?: string;\n application_properties?: Record<string, unknown>;\n message_annotations?: Record<string, unknown>;\n /** The AMQP header delivery-count (0-based: 0 on first delivery). */\n delivery_count?: number;\n}\n\n/** The disposition handle rhea passes alongside a received message. */\nexport interface AmqpDelivery {\n /** Settle (acknowledge) the message — it is removed from the queue. */\n accept(): void;\n /** Return the message for redelivery — the broker increments `delivery-count`. */\n release(params?: { delivery_failed?: boolean; undeliverable_here?: boolean }): void;\n}\n\n/** The event context rhea emits on a `message` event. */\nexport interface AmqpEventContext {\n message?: AmqpMessage;\n delivery?: AmqpDelivery;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n send(message: AmqpMessage): unknown;\n}\n\n/** The subset of a rhea receiver this adapter wires to (its `message` event). */\nexport interface AmqpReceiver {\n on(event: \"message\", handler: (context: AmqpEventContext) => void): unknown;\n}\n\n// --- Projection (contract §7.2) ------------------------------------------------\n\n/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */\nexport const JMS_TYPE_KEY = \"x-opt-jms-type\";\n/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */\nexport const SCHEDULED_DELIVERY_KEY = \"x-opt-delivery-time\";\n\n/**\n * Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,\n * `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`\n * sets the `x-opt-delivery-time` annotation for native scheduled delivery.\n */\nexport function artemisMessage(envelope: Envelope, delayMs?: number): AmqpMessage {\n const annotations: Record<string, unknown> = {};\n if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;\n\n // The bq_ property names use underscores, not hyphens: a JMS property name must be a valid\n // Java identifier, and every Artemis SDK uses the same JMS-legal form for cross-protocol parity.\n const applicationProperties: Record<string, unknown> = {\n \"bq_schema_version\": String(envelope.meta.schema_version),\n \"bq_attempts\": String(envelope.attempts ?? 0),\n \"bq_app_id\": \"babelqueue\",\n };\n if (envelope.meta.lang) applicationProperties[\"bq_source_lang\"] = envelope.meta.lang;\n\n const message: AmqpMessage = {\n body: EnvelopeCodec.encode(envelope),\n content_type: \"application/json\",\n application_properties: applicationProperties,\n message_annotations: annotations,\n };\n if (envelope.trace_id) message.correlation_id = envelope.trace_id;\n if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;\n\n if (delayMs != null && delayMs > 0) {\n applicationProperties[\"bq_delay\"] = String(delayMs);\n annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;\n }\n return message;\n}\n\n/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */\nexport function messageBody(message: AmqpMessage): string {\n const body = message.body;\n if (body == null) return \"\";\n if (typeof body === \"string\") return body;\n if (Buffer.isBuffer(body)) return body.toString(\"utf8\");\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content.toString(\"utf8\");\n if (typeof content === \"string\") return content;\n }\n return String(body);\n}\n\nfunction jmsType(message: AmqpMessage): string | undefined {\n const value = message.message_annotations?.[JMS_TYPE_KEY];\n return value == null ? undefined : String(value);\n}\n\nfunction deliveryCount(message: AmqpMessage): number {\n const value = message.delivery_count;\n return typeof value === \"number\" && value > 0 ? value : 0;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link ArtemisPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */\n delayMs?: number;\n}\n\n/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */\nexport class ArtemisPublisher {\n private constructor(\n private readonly sender: AmqpSender,\n private readonly queue: string,\n ) {}\n\n /** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */\n static create(sender: AmqpSender, queue: string): ArtemisPublisher {\n return new ArtemisPublisher(sender, queue);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });\n await this.sender.send(artemisMessage(envelope, options.delayMs));\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/** A URN handler. Receives the validated envelope and the raw AMQP message. */\nexport type BabelHandler = (envelope: Envelope, message: AmqpMessage) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link ArtemisConsumer}. */\nexport interface ArtemisConsumerOptions {\n /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */\n deadLetterSender?: AmqpSender;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a non-conformant message, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;\n}\n\n/**\n * Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the\n * handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A\n * throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP\n * `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a\n * `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP\n * counter is 0-based, so no −1.\n */\nexport class ArtemisConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly handlers: BabelHandlers,\n private readonly options: ArtemisConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */\n listen(receiver: AmqpReceiver): void {\n receiver.on(\"message\", (context) => this.handle(context));\n }\n\n /** Route + settle one delivery. Exposed for testing. */\n async handle(context: AmqpEventContext): Promise<void> {\n const message = context.message ?? {};\n const delivery = context.delivery;\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n // A non-conformant / poison message may lack the fields to annotate; forward it raw.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\"), decoded, context);\n await this.deadLetterRaw(messageBody(message));\n delivery?.accept();\n return;\n }\n\n const envelope = this.reconcile(decoded, message);\n const urn = jmsType(message) ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(context, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message);\n delivery?.accept();\n } catch (error) {\n this.report(error, envelope, context);\n await this.retryOrDeadLetter(context, envelope, error);\n }\n }\n\n /** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */\n private reconcile(envelope: IncomingEnvelope, message: AmqpMessage): Envelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n const count = deliveryCount(message);\n return { ...envelope, attempts: Math.max(body, count) } as Envelope;\n }\n\n private async onUnknownUrn(context: AmqpEventContext, envelope: Envelope, urn: string): Promise<void> {\n const delivery = context.delivery;\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n delivery?.accept();\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope, \"unknown_urn\");\n delivery?.accept();\n return;\n case UnknownUrnStrategy.RELEASE:\n delivery?.release({ delivery_failed: true });\n return;\n default: {\n // FAIL: surface and do NOT settle — the broker redelivers, then dead-letters.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, context);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(context: AmqpEventContext, envelope: Envelope, error: unknown): Promise<void> {\n const delivery = context.delivery;\n if ((envelope.attempts ?? 0) + 1 < this.maxTries) {\n // Release with delivery-failed leaves it for redelivery — the broker bumps delivery-count.\n delivery?.release({ delivery_failed: true });\n } else if (this.options.deadLetterSender) {\n await this.deadLetter(envelope, \"failed\", error);\n delivery?.accept();\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n private async deadLetter(envelope: Envelope, reason: string, error?: unknown): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n const original = envelope.meta?.queue ?? \"\";\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n await sender.send(artemisMessage(annotated));\n }\n\n private async deadLetterRaw(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sender.send({ body: raw });\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n this.options.onError?.(error, envelope, context);\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache ActiveMQ Artemis adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **AMQP 1.0** consumer over Artemis (rhea),\n * so an Artemis-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the Java, .NET, Python and Go SDKs.\n *\n * Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —\n * per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so\n * this adapter maps onto them (the envelope stays `schema_version: 1`): the envelope JSON is the\n * message **body**; the contract fields are mirrored onto the AMQP a JMS peer reads\n * (`correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN) plus the `bq_` application properties; consume settles per message\n * (`accept` after success, `release` for redelivery); **`attempts = max(body, delivery-count)`**\n * (the AMQP counter is 0-based — no −1); terminal failures go to an opt-in `<queue>.dlq` with a\n * `dead_letter` block.\n *\n * This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide\n * the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional AMQP `application_properties` beside the contract `bq_`\n * properties — the contract properties win a key collision (merge-not-clobber), blanks dropped.\n * `traceparent`/`tracestate` are W3C keys with no hyphens, so they are already JMS-legal (§7 uses\n * underscores because JMS property names must be Java identifiers). On consume the inbound\n * `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract\n * sees the `traceparent` and links the consumer span as a child. A header-less publish is\n * byte-identical. GR-1: the wire envelope body is never touched.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { sanitizeHeaders } from \"./headers.js\";\n\n// --- Minimal AMQP 1.0 shapes (a structural subset of rhea) ---------------------\n\n/** An AMQP message (structural subset of a rhea message). */\nexport interface AmqpMessage {\n body?: unknown;\n correlation_id?: unknown;\n creation_time?: number | Date;\n content_type?: string;\n application_properties?: Record<string, unknown>;\n message_annotations?: Record<string, unknown>;\n /** The AMQP header delivery-count (0-based: 0 on first delivery). */\n delivery_count?: number;\n}\n\n/** The disposition handle rhea passes alongside a received message. */\nexport interface AmqpDelivery {\n /** Settle (acknowledge) the message — it is removed from the queue. */\n accept(): void;\n /** Return the message for redelivery — the broker increments `delivery-count`. */\n release(params?: { delivery_failed?: boolean; undeliverable_here?: boolean }): void;\n}\n\n/** The event context rhea emits on a `message` event. */\nexport interface AmqpEventContext {\n message?: AmqpMessage;\n delivery?: AmqpDelivery;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n send(message: AmqpMessage): unknown;\n}\n\n/** The subset of a rhea receiver this adapter wires to (its `message` event). */\nexport interface AmqpReceiver {\n on(event: \"message\", handler: (context: AmqpEventContext) => void): unknown;\n}\n\n// --- Projection (contract §7.2) ------------------------------------------------\n\n/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */\nexport const JMS_TYPE_KEY = \"x-opt-jms-type\";\n/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */\nexport const SCHEDULED_DELIVERY_KEY = \"x-opt-delivery-time\";\n\n/**\n * Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,\n * `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`\n * sets the `x-opt-delivery-time` annotation for native scheduled delivery.\n */\nexport function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage {\n const annotations: Record<string, unknown> = {};\n if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;\n\n // Out-of-band riders (e.g. traceparent) go in first; the contract bq_ properties below win any\n // key collision (merge-not-clobber). The bq_ names use underscores, not hyphens: a JMS property\n // name must be a valid Java identifier, and every Artemis SDK uses the same JMS-legal form for\n // cross-protocol parity. traceparent/tracestate have no hyphens, so they are already JMS-legal.\n const applicationProperties: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;\n applicationProperties[\"bq_schema_version\"] = String(envelope.meta.schema_version);\n applicationProperties[\"bq_attempts\"] = String(envelope.attempts ?? 0);\n applicationProperties[\"bq_app_id\"] = \"babelqueue\";\n if (envelope.meta.lang) applicationProperties[\"bq_source_lang\"] = envelope.meta.lang;\n\n const message: AmqpMessage = {\n body: EnvelopeCodec.encode(envelope),\n content_type: \"application/json\",\n application_properties: applicationProperties,\n message_annotations: annotations,\n };\n if (envelope.trace_id) message.correlation_id = envelope.trace_id;\n if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;\n\n if (delayMs != null && delayMs > 0) {\n applicationProperties[\"bq_delay\"] = String(delayMs);\n annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;\n }\n return message;\n}\n\n/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */\nexport function messageBody(message: AmqpMessage): string {\n const body = message.body;\n if (body == null) return \"\";\n if (typeof body === \"string\") return body;\n if (Buffer.isBuffer(body)) return body.toString(\"utf8\");\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content.toString(\"utf8\");\n if (typeof content === \"string\") return content;\n }\n return String(body);\n}\n\nfunction jmsType(message: AmqpMessage): string | undefined {\n const value = message.message_annotations?.[JMS_TYPE_KEY];\n return value == null ? undefined : String(value);\n}\n\nfunction deliveryCount(message: AmqpMessage): number {\n const value = message.delivery_count;\n return typeof value === \"number\" && value > 0 ? value : 0;\n}\n\n/**\n * Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying\n * values defensively. Returns an empty object when there are none. Both the contract `bq_`\n * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract\n * reads only the keys it knows.\n */\nexport function headersOf(message: AmqpMessage): HeaderCarrier {\n const props = message.application_properties;\n const out: HeaderCarrier = {};\n if (!props) return out;\n for (const key of Object.keys(props)) {\n const value = props[key];\n if (value == null) continue;\n const s = String(value);\n if (s !== \"\") out[key] = s;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link ArtemisPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`\n * properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract properties win a key collision; an empty/omitted carrier leaves the message\n * unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */\nexport class ArtemisPublisher {\n private constructor(\n private readonly sender: AmqpSender,\n private readonly queue: string,\n ) {}\n\n /** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */\n static create(sender: AmqpSender, queue: string): ArtemisPublisher {\n return new ArtemisPublisher(sender, queue);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });\n await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band\n * {@link HeaderCarrier} read from the message's `application_properties` (empty when there are\n * none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a\n * child of the producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link ArtemisConsumer}. */\nexport interface ArtemisConsumerOptions {\n /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */\n deadLetterSender?: AmqpSender;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a non-conformant message, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;\n}\n\n/**\n * Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the\n * handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A\n * throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP\n * `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a\n * `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP\n * counter is 0-based, so no −1.\n */\nexport class ArtemisConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly handlers: BabelHandlers,\n private readonly options: ArtemisConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */\n listen(receiver: AmqpReceiver): void {\n receiver.on(\"message\", (context) => this.handle(context));\n }\n\n /** Route + settle one delivery. Exposed for testing. */\n async handle(context: AmqpEventContext): Promise<void> {\n const message = context.message ?? {};\n const delivery = context.delivery;\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n // A non-conformant / poison message may lack the fields to annotate; forward it raw.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\"), decoded, context);\n await this.deadLetterRaw(messageBody(message));\n delivery?.accept();\n return;\n }\n\n const envelope = this.reconcile(decoded, message);\n const urn = jmsType(message) ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(context, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n delivery?.accept();\n } catch (error) {\n this.report(error, envelope, context);\n await this.retryOrDeadLetter(context, envelope, error);\n }\n }\n\n /** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */\n private reconcile(envelope: IncomingEnvelope, message: AmqpMessage): Envelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n const count = deliveryCount(message);\n return { ...envelope, attempts: Math.max(body, count) } as Envelope;\n }\n\n private async onUnknownUrn(context: AmqpEventContext, envelope: Envelope, urn: string): Promise<void> {\n const delivery = context.delivery;\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n delivery?.accept();\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope, \"unknown_urn\");\n delivery?.accept();\n return;\n case UnknownUrnStrategy.RELEASE:\n delivery?.release({ delivery_failed: true });\n return;\n default: {\n // FAIL: surface and do NOT settle — the broker redelivers, then dead-letters.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, context);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(context: AmqpEventContext, envelope: Envelope, error: unknown): Promise<void> {\n const delivery = context.delivery;\n if ((envelope.attempts ?? 0) + 1 < this.maxTries) {\n // Release with delivery-failed leaves it for redelivery — the broker bumps delivery-count.\n delivery?.release({ delivery_failed: true });\n } else if (this.options.deadLetterSender) {\n await this.deadLetter(envelope, \"failed\", error);\n delivery?.accept();\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n private async deadLetter(envelope: Envelope, reason: string, error?: unknown): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n const original = envelope.meta?.queue ?? \"\";\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n await sender.send(artemisMessage(annotated));\n }\n\n private async deadLetterRaw(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sender.send({ body: raw });\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n this.options.onError?.(error, envelope, context);\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Envelope, IncomingEnvelope } from '@babelqueue/core';
1
+ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
2
2
 
3
3
  /**
4
4
  * Apache ActiveMQ Artemis adapter for BabelQueue.
@@ -19,6 +19,15 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
19
19
  *
20
20
  * This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide
21
21
  * the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).
22
+ *
23
+ * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
24
+ * span linkage) rides as additional AMQP `application_properties` beside the contract `bq_`
25
+ * properties — the contract properties win a key collision (merge-not-clobber), blanks dropped.
26
+ * `traceparent`/`tracestate` are W3C keys with no hyphens, so they are already JMS-legal (§7 uses
27
+ * underscores because JMS property names must be Java identifiers). On consume the inbound
28
+ * `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract
29
+ * sees the `traceparent` and links the consumer span as a child. A header-less publish is
30
+ * byte-identical. GR-1: the wire envelope body is never touched.
22
31
  */
23
32
 
24
33
  /** An AMQP message (structural subset of a rhea message). */
@@ -65,14 +74,28 @@ declare const SCHEDULED_DELIVERY_KEY = "x-opt-delivery-time";
65
74
  * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`
66
75
  * sets the `x-opt-delivery-time` annotation for native scheduled delivery.
67
76
  */
68
- declare function artemisMessage(envelope: Envelope, delayMs?: number): AmqpMessage;
77
+ declare function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage;
69
78
  /** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */
70
79
  declare function messageBody(message: AmqpMessage): string;
80
+ /**
81
+ * Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying
82
+ * values defensively. Returns an empty object when there are none. Both the contract `bq_`
83
+ * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract
84
+ * reads only the keys it knows.
85
+ */
86
+ declare function headersOf(message: AmqpMessage): HeaderCarrier;
71
87
  /** Options for {@link ArtemisPublisher.publish}. */
72
88
  interface PublishOptions {
73
89
  traceId?: string;
74
90
  /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */
75
91
  delayMs?: number;
92
+ /**
93
+ * Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`
94
+ * properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.
95
+ * The contract properties win a key collision; an empty/omitted carrier leaves the message
96
+ * unchanged.
97
+ */
98
+ headers?: HeaderCarrier;
76
99
  }
77
100
  /** Sends canonical-envelope messages to one Artemis address with the §7 projection. */
78
101
  declare class ArtemisPublisher {
@@ -84,8 +107,13 @@ declare class ArtemisPublisher {
84
107
  /** Build + send the canonical envelope; returns the message id (`meta.id`). */
85
108
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
86
109
  }
87
- /** A URN handler. Receives the validated envelope and the raw AMQP message. */
88
- type BabelHandler = (envelope: Envelope, message: AmqpMessage) => unknown | Promise<unknown>;
110
+ /**
111
+ * A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band
112
+ * {@link HeaderCarrier} read from the message's `application_properties` (empty when there are
113
+ * none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a
114
+ * child of the producer span (ADR-0028).
115
+ */
116
+ type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
89
117
  /** A map of URN → handler. */
90
118
  type BabelHandlers = Record<string, BabelHandler>;
91
119
  /** Options for {@link ArtemisConsumer}. */
@@ -126,4 +154,4 @@ declare class ArtemisConsumer {
126
154
  private report;
127
155
  }
128
156
 
129
- export { type AmqpDelivery, type AmqpEventContext, type AmqpMessage, type AmqpReceiver, type AmqpSender, ArtemisConsumer, type ArtemisConsumerOptions, ArtemisPublisher, type BabelHandler, type BabelHandlers, JMS_TYPE_KEY, type PublishOptions, SCHEDULED_DELIVERY_KEY, artemisMessage, messageBody };
157
+ export { type AmqpDelivery, type AmqpEventContext, type AmqpMessage, type AmqpReceiver, type AmqpSender, ArtemisConsumer, type ArtemisConsumerOptions, ArtemisPublisher, type BabelHandler, type BabelHandlers, JMS_TYPE_KEY, type PublishOptions, SCHEDULED_DELIVERY_KEY, artemisMessage, headersOf, messageBody };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Envelope, IncomingEnvelope } from '@babelqueue/core';
1
+ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
2
2
 
3
3
  /**
4
4
  * Apache ActiveMQ Artemis adapter for BabelQueue.
@@ -19,6 +19,15 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
19
19
  *
20
20
  * This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide
21
21
  * the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).
22
+ *
23
+ * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
24
+ * span linkage) rides as additional AMQP `application_properties` beside the contract `bq_`
25
+ * properties — the contract properties win a key collision (merge-not-clobber), blanks dropped.
26
+ * `traceparent`/`tracestate` are W3C keys with no hyphens, so they are already JMS-legal (§7 uses
27
+ * underscores because JMS property names must be Java identifiers). On consume the inbound
28
+ * `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract
29
+ * sees the `traceparent` and links the consumer span as a child. A header-less publish is
30
+ * byte-identical. GR-1: the wire envelope body is never touched.
22
31
  */
23
32
 
24
33
  /** An AMQP message (structural subset of a rhea message). */
@@ -65,14 +74,28 @@ declare const SCHEDULED_DELIVERY_KEY = "x-opt-delivery-time";
65
74
  * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`
66
75
  * sets the `x-opt-delivery-time` annotation for native scheduled delivery.
67
76
  */
68
- declare function artemisMessage(envelope: Envelope, delayMs?: number): AmqpMessage;
77
+ declare function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage;
69
78
  /** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */
70
79
  declare function messageBody(message: AmqpMessage): string;
80
+ /**
81
+ * Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying
82
+ * values defensively. Returns an empty object when there are none. Both the contract `bq_`
83
+ * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract
84
+ * reads only the keys it knows.
85
+ */
86
+ declare function headersOf(message: AmqpMessage): HeaderCarrier;
71
87
  /** Options for {@link ArtemisPublisher.publish}. */
72
88
  interface PublishOptions {
73
89
  traceId?: string;
74
90
  /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */
75
91
  delayMs?: number;
92
+ /**
93
+ * Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`
94
+ * properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.
95
+ * The contract properties win a key collision; an empty/omitted carrier leaves the message
96
+ * unchanged.
97
+ */
98
+ headers?: HeaderCarrier;
76
99
  }
77
100
  /** Sends canonical-envelope messages to one Artemis address with the §7 projection. */
78
101
  declare class ArtemisPublisher {
@@ -84,8 +107,13 @@ declare class ArtemisPublisher {
84
107
  /** Build + send the canonical envelope; returns the message id (`meta.id`). */
85
108
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
86
109
  }
87
- /** A URN handler. Receives the validated envelope and the raw AMQP message. */
88
- type BabelHandler = (envelope: Envelope, message: AmqpMessage) => unknown | Promise<unknown>;
110
+ /**
111
+ * A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band
112
+ * {@link HeaderCarrier} read from the message's `application_properties` (empty when there are
113
+ * none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a
114
+ * child of the producer span (ADR-0028).
115
+ */
116
+ type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
89
117
  /** A map of URN → handler. */
90
118
  type BabelHandlers = Record<string, BabelHandler>;
91
119
  /** Options for {@link ArtemisConsumer}. */
@@ -126,4 +154,4 @@ declare class ArtemisConsumer {
126
154
  private report;
127
155
  }
128
156
 
129
- export { type AmqpDelivery, type AmqpEventContext, type AmqpMessage, type AmqpReceiver, type AmqpSender, ArtemisConsumer, type ArtemisConsumerOptions, ArtemisPublisher, type BabelHandler, type BabelHandlers, JMS_TYPE_KEY, type PublishOptions, SCHEDULED_DELIVERY_KEY, artemisMessage, messageBody };
157
+ export { type AmqpDelivery, type AmqpEventContext, type AmqpMessage, type AmqpReceiver, type AmqpSender, ArtemisConsumer, type ArtemisConsumerOptions, ArtemisPublisher, type BabelHandler, type BabelHandlers, JMS_TYPE_KEY, type PublishOptions, SCHEDULED_DELIVERY_KEY, artemisMessage, headersOf, messageBody };
package/dist/index.js CHANGED
@@ -1,15 +1,29 @@
1
1
  // src/index.ts
2
2
  import { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from "@babelqueue/core";
3
+
4
+ // src/headers.ts
5
+ function sanitizeHeaders(headers) {
6
+ const out = {};
7
+ if (!headers) return out;
8
+ for (const key of Object.keys(headers)) {
9
+ const value = headers[key];
10
+ if (key === "" || value == null || value === "") continue;
11
+ out[key] = value;
12
+ }
13
+ return out;
14
+ }
15
+
16
+ // src/index.ts
3
17
  var JMS_TYPE_KEY = "x-opt-jms-type";
4
18
  var SCHEDULED_DELIVERY_KEY = "x-opt-delivery-time";
5
- function artemisMessage(envelope, delayMs) {
19
+ function artemisMessage(envelope, delayMs, extra) {
6
20
  const annotations = {};
7
21
  if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;
8
- const applicationProperties = {
9
- "bq_schema_version": String(envelope.meta.schema_version),
10
- "bq_attempts": String(envelope.attempts ?? 0),
11
- "bq_app_id": "babelqueue"
12
- };
22
+ const applicationProperties = {};
23
+ for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;
24
+ applicationProperties["bq_schema_version"] = String(envelope.meta.schema_version);
25
+ applicationProperties["bq_attempts"] = String(envelope.attempts ?? 0);
26
+ applicationProperties["bq_app_id"] = "babelqueue";
13
27
  if (envelope.meta.lang) applicationProperties["bq_source_lang"] = envelope.meta.lang;
14
28
  const message = {
15
29
  body: EnvelopeCodec.encode(envelope),
@@ -45,6 +59,18 @@ function deliveryCount(message) {
45
59
  const value = message.delivery_count;
46
60
  return typeof value === "number" && value > 0 ? value : 0;
47
61
  }
62
+ function headersOf(message) {
63
+ const props = message.application_properties;
64
+ const out = {};
65
+ if (!props) return out;
66
+ for (const key of Object.keys(props)) {
67
+ const value = props[key];
68
+ if (value == null) continue;
69
+ const s = String(value);
70
+ if (s !== "") out[key] = s;
71
+ }
72
+ return out;
73
+ }
48
74
  var ArtemisPublisher = class _ArtemisPublisher {
49
75
  constructor(sender, queue) {
50
76
  this.sender = sender;
@@ -59,7 +85,7 @@ var ArtemisPublisher = class _ArtemisPublisher {
59
85
  /** Build + send the canonical envelope; returns the message id (`meta.id`). */
60
86
  async publish(urn, data, options = {}) {
61
87
  const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });
62
- await this.sender.send(artemisMessage(envelope, options.delayMs));
88
+ await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));
63
89
  return envelope.meta.id;
64
90
  }
65
91
  };
@@ -97,7 +123,7 @@ var ArtemisConsumer = class {
97
123
  return;
98
124
  }
99
125
  try {
100
- await handler(envelope, message);
126
+ await handler(envelope, message, headersOf(message));
101
127
  delivery?.accept();
102
128
  } catch (error) {
103
129
  this.report(error, envelope, context);
@@ -167,6 +193,7 @@ export {
167
193
  JMS_TYPE_KEY,
168
194
  SCHEDULED_DELIVERY_KEY,
169
195
  artemisMessage,
196
+ headersOf,
170
197
  messageBody
171
198
  };
172
199
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Apache ActiveMQ Artemis adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **AMQP 1.0** consumer over Artemis (rhea),\n * so an Artemis-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the Java, .NET, Python and Go SDKs.\n *\n * Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —\n * per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so\n * this adapter maps onto them (the envelope stays `schema_version: 1`): the envelope JSON is the\n * message **body**; the contract fields are mirrored onto the AMQP a JMS peer reads\n * (`correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN) plus the `bq_` application properties; consume settles per message\n * (`accept` after success, `release` for redelivery); **`attempts = max(body, delivery-count)`**\n * (the AMQP counter is 0-based — no −1); terminal failures go to an opt-in `<queue>.dlq` with a\n * `dead_letter` block.\n *\n * This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide\n * the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, IncomingEnvelope } from \"@babelqueue/core\";\n\n// --- Minimal AMQP 1.0 shapes (a structural subset of rhea) ---------------------\n\n/** An AMQP message (structural subset of a rhea message). */\nexport interface AmqpMessage {\n body?: unknown;\n correlation_id?: unknown;\n creation_time?: number | Date;\n content_type?: string;\n application_properties?: Record<string, unknown>;\n message_annotations?: Record<string, unknown>;\n /** The AMQP header delivery-count (0-based: 0 on first delivery). */\n delivery_count?: number;\n}\n\n/** The disposition handle rhea passes alongside a received message. */\nexport interface AmqpDelivery {\n /** Settle (acknowledge) the message — it is removed from the queue. */\n accept(): void;\n /** Return the message for redelivery — the broker increments `delivery-count`. */\n release(params?: { delivery_failed?: boolean; undeliverable_here?: boolean }): void;\n}\n\n/** The event context rhea emits on a `message` event. */\nexport interface AmqpEventContext {\n message?: AmqpMessage;\n delivery?: AmqpDelivery;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n send(message: AmqpMessage): unknown;\n}\n\n/** The subset of a rhea receiver this adapter wires to (its `message` event). */\nexport interface AmqpReceiver {\n on(event: \"message\", handler: (context: AmqpEventContext) => void): unknown;\n}\n\n// --- Projection (contract §7.2) ------------------------------------------------\n\n/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */\nexport const JMS_TYPE_KEY = \"x-opt-jms-type\";\n/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */\nexport const SCHEDULED_DELIVERY_KEY = \"x-opt-delivery-time\";\n\n/**\n * Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,\n * `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`\n * sets the `x-opt-delivery-time` annotation for native scheduled delivery.\n */\nexport function artemisMessage(envelope: Envelope, delayMs?: number): AmqpMessage {\n const annotations: Record<string, unknown> = {};\n if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;\n\n // The bq_ property names use underscores, not hyphens: a JMS property name must be a valid\n // Java identifier, and every Artemis SDK uses the same JMS-legal form for cross-protocol parity.\n const applicationProperties: Record<string, unknown> = {\n \"bq_schema_version\": String(envelope.meta.schema_version),\n \"bq_attempts\": String(envelope.attempts ?? 0),\n \"bq_app_id\": \"babelqueue\",\n };\n if (envelope.meta.lang) applicationProperties[\"bq_source_lang\"] = envelope.meta.lang;\n\n const message: AmqpMessage = {\n body: EnvelopeCodec.encode(envelope),\n content_type: \"application/json\",\n application_properties: applicationProperties,\n message_annotations: annotations,\n };\n if (envelope.trace_id) message.correlation_id = envelope.trace_id;\n if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;\n\n if (delayMs != null && delayMs > 0) {\n applicationProperties[\"bq_delay\"] = String(delayMs);\n annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;\n }\n return message;\n}\n\n/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */\nexport function messageBody(message: AmqpMessage): string {\n const body = message.body;\n if (body == null) return \"\";\n if (typeof body === \"string\") return body;\n if (Buffer.isBuffer(body)) return body.toString(\"utf8\");\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content.toString(\"utf8\");\n if (typeof content === \"string\") return content;\n }\n return String(body);\n}\n\nfunction jmsType(message: AmqpMessage): string | undefined {\n const value = message.message_annotations?.[JMS_TYPE_KEY];\n return value == null ? undefined : String(value);\n}\n\nfunction deliveryCount(message: AmqpMessage): number {\n const value = message.delivery_count;\n return typeof value === \"number\" && value > 0 ? value : 0;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link ArtemisPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */\n delayMs?: number;\n}\n\n/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */\nexport class ArtemisPublisher {\n private constructor(\n private readonly sender: AmqpSender,\n private readonly queue: string,\n ) {}\n\n /** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */\n static create(sender: AmqpSender, queue: string): ArtemisPublisher {\n return new ArtemisPublisher(sender, queue);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });\n await this.sender.send(artemisMessage(envelope, options.delayMs));\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/** A URN handler. Receives the validated envelope and the raw AMQP message. */\nexport type BabelHandler = (envelope: Envelope, message: AmqpMessage) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link ArtemisConsumer}. */\nexport interface ArtemisConsumerOptions {\n /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */\n deadLetterSender?: AmqpSender;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a non-conformant message, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;\n}\n\n/**\n * Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the\n * handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A\n * throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP\n * `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a\n * `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP\n * counter is 0-based, so no −1.\n */\nexport class ArtemisConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly handlers: BabelHandlers,\n private readonly options: ArtemisConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */\n listen(receiver: AmqpReceiver): void {\n receiver.on(\"message\", (context) => this.handle(context));\n }\n\n /** Route + settle one delivery. Exposed for testing. */\n async handle(context: AmqpEventContext): Promise<void> {\n const message = context.message ?? {};\n const delivery = context.delivery;\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n // A non-conformant / poison message may lack the fields to annotate; forward it raw.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\"), decoded, context);\n await this.deadLetterRaw(messageBody(message));\n delivery?.accept();\n return;\n }\n\n const envelope = this.reconcile(decoded, message);\n const urn = jmsType(message) ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(context, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message);\n delivery?.accept();\n } catch (error) {\n this.report(error, envelope, context);\n await this.retryOrDeadLetter(context, envelope, error);\n }\n }\n\n /** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */\n private reconcile(envelope: IncomingEnvelope, message: AmqpMessage): Envelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n const count = deliveryCount(message);\n return { ...envelope, attempts: Math.max(body, count) } as Envelope;\n }\n\n private async onUnknownUrn(context: AmqpEventContext, envelope: Envelope, urn: string): Promise<void> {\n const delivery = context.delivery;\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n delivery?.accept();\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope, \"unknown_urn\");\n delivery?.accept();\n return;\n case UnknownUrnStrategy.RELEASE:\n delivery?.release({ delivery_failed: true });\n return;\n default: {\n // FAIL: surface and do NOT settle — the broker redelivers, then dead-letters.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, context);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(context: AmqpEventContext, envelope: Envelope, error: unknown): Promise<void> {\n const delivery = context.delivery;\n if ((envelope.attempts ?? 0) + 1 < this.maxTries) {\n // Release with delivery-failed leaves it for redelivery — the broker bumps delivery-count.\n delivery?.release({ delivery_failed: true });\n } else if (this.options.deadLetterSender) {\n await this.deadLetter(envelope, \"failed\", error);\n delivery?.accept();\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n private async deadLetter(envelope: Envelope, reason: string, error?: unknown): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n const original = envelope.meta?.queue ?? \"\";\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n await sender.send(artemisMessage(annotated));\n }\n\n private async deadLetterRaw(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sender.send({ body: raw });\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n this.options.onError?.(error, envelope, context);\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache ActiveMQ Artemis adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **AMQP 1.0** consumer over Artemis (rhea),\n * so an Artemis-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the Java, .NET, Python and Go SDKs.\n *\n * Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —\n * per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so\n * this adapter maps onto them (the envelope stays `schema_version: 1`): the envelope JSON is the\n * message **body**; the contract fields are mirrored onto the AMQP a JMS peer reads\n * (`correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN) plus the `bq_` application properties; consume settles per message\n * (`accept` after success, `release` for redelivery); **`attempts = max(body, delivery-count)`**\n * (the AMQP counter is 0-based — no −1); terminal failures go to an opt-in `<queue>.dlq` with a\n * `dead_letter` block.\n *\n * This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide\n * the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional AMQP `application_properties` beside the contract `bq_`\n * properties — the contract properties win a key collision (merge-not-clobber), blanks dropped.\n * `traceparent`/`tracestate` are W3C keys with no hyphens, so they are already JMS-legal (§7 uses\n * underscores because JMS property names must be Java identifiers). On consume the inbound\n * `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract\n * sees the `traceparent` and links the consumer span as a child. A header-less publish is\n * byte-identical. GR-1: the wire envelope body is never touched.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { sanitizeHeaders } from \"./headers.js\";\n\n// --- Minimal AMQP 1.0 shapes (a structural subset of rhea) ---------------------\n\n/** An AMQP message (structural subset of a rhea message). */\nexport interface AmqpMessage {\n body?: unknown;\n correlation_id?: unknown;\n creation_time?: number | Date;\n content_type?: string;\n application_properties?: Record<string, unknown>;\n message_annotations?: Record<string, unknown>;\n /** The AMQP header delivery-count (0-based: 0 on first delivery). */\n delivery_count?: number;\n}\n\n/** The disposition handle rhea passes alongside a received message. */\nexport interface AmqpDelivery {\n /** Settle (acknowledge) the message — it is removed from the queue. */\n accept(): void;\n /** Return the message for redelivery — the broker increments `delivery-count`. */\n release(params?: { delivery_failed?: boolean; undeliverable_here?: boolean }): void;\n}\n\n/** The event context rhea emits on a `message` event. */\nexport interface AmqpEventContext {\n message?: AmqpMessage;\n delivery?: AmqpDelivery;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n send(message: AmqpMessage): unknown;\n}\n\n/** The subset of a rhea receiver this adapter wires to (its `message` event). */\nexport interface AmqpReceiver {\n on(event: \"message\", handler: (context: AmqpEventContext) => void): unknown;\n}\n\n// --- Projection (contract §7.2) ------------------------------------------------\n\n/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */\nexport const JMS_TYPE_KEY = \"x-opt-jms-type\";\n/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */\nexport const SCHEDULED_DELIVERY_KEY = \"x-opt-delivery-time\";\n\n/**\n * Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,\n * `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`\n * sets the `x-opt-delivery-time` annotation for native scheduled delivery.\n */\nexport function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage {\n const annotations: Record<string, unknown> = {};\n if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;\n\n // Out-of-band riders (e.g. traceparent) go in first; the contract bq_ properties below win any\n // key collision (merge-not-clobber). The bq_ names use underscores, not hyphens: a JMS property\n // name must be a valid Java identifier, and every Artemis SDK uses the same JMS-legal form for\n // cross-protocol parity. traceparent/tracestate have no hyphens, so they are already JMS-legal.\n const applicationProperties: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;\n applicationProperties[\"bq_schema_version\"] = String(envelope.meta.schema_version);\n applicationProperties[\"bq_attempts\"] = String(envelope.attempts ?? 0);\n applicationProperties[\"bq_app_id\"] = \"babelqueue\";\n if (envelope.meta.lang) applicationProperties[\"bq_source_lang\"] = envelope.meta.lang;\n\n const message: AmqpMessage = {\n body: EnvelopeCodec.encode(envelope),\n content_type: \"application/json\",\n application_properties: applicationProperties,\n message_annotations: annotations,\n };\n if (envelope.trace_id) message.correlation_id = envelope.trace_id;\n if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;\n\n if (delayMs != null && delayMs > 0) {\n applicationProperties[\"bq_delay\"] = String(delayMs);\n annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;\n }\n return message;\n}\n\n/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */\nexport function messageBody(message: AmqpMessage): string {\n const body = message.body;\n if (body == null) return \"\";\n if (typeof body === \"string\") return body;\n if (Buffer.isBuffer(body)) return body.toString(\"utf8\");\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content.toString(\"utf8\");\n if (typeof content === \"string\") return content;\n }\n return String(body);\n}\n\nfunction jmsType(message: AmqpMessage): string | undefined {\n const value = message.message_annotations?.[JMS_TYPE_KEY];\n return value == null ? undefined : String(value);\n}\n\nfunction deliveryCount(message: AmqpMessage): number {\n const value = message.delivery_count;\n return typeof value === \"number\" && value > 0 ? value : 0;\n}\n\n/**\n * Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying\n * values defensively. Returns an empty object when there are none. Both the contract `bq_`\n * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract\n * reads only the keys it knows.\n */\nexport function headersOf(message: AmqpMessage): HeaderCarrier {\n const props = message.application_properties;\n const out: HeaderCarrier = {};\n if (!props) return out;\n for (const key of Object.keys(props)) {\n const value = props[key];\n if (value == null) continue;\n const s = String(value);\n if (s !== \"\") out[key] = s;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link ArtemisPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`\n * properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract properties win a key collision; an empty/omitted carrier leaves the message\n * unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */\nexport class ArtemisPublisher {\n private constructor(\n private readonly sender: AmqpSender,\n private readonly queue: string,\n ) {}\n\n /** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */\n static create(sender: AmqpSender, queue: string): ArtemisPublisher {\n return new ArtemisPublisher(sender, queue);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });\n await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band\n * {@link HeaderCarrier} read from the message's `application_properties` (empty when there are\n * none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a\n * child of the producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link ArtemisConsumer}. */\nexport interface ArtemisConsumerOptions {\n /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */\n deadLetterSender?: AmqpSender;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a non-conformant message, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;\n}\n\n/**\n * Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the\n * handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A\n * throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP\n * `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a\n * `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP\n * counter is 0-based, so no −1.\n */\nexport class ArtemisConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly handlers: BabelHandlers,\n private readonly options: ArtemisConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */\n listen(receiver: AmqpReceiver): void {\n receiver.on(\"message\", (context) => this.handle(context));\n }\n\n /** Route + settle one delivery. Exposed for testing. */\n async handle(context: AmqpEventContext): Promise<void> {\n const message = context.message ?? {};\n const delivery = context.delivery;\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n // A non-conformant / poison message may lack the fields to annotate; forward it raw.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\"), decoded, context);\n await this.deadLetterRaw(messageBody(message));\n delivery?.accept();\n return;\n }\n\n const envelope = this.reconcile(decoded, message);\n const urn = jmsType(message) ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(context, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n delivery?.accept();\n } catch (error) {\n this.report(error, envelope, context);\n await this.retryOrDeadLetter(context, envelope, error);\n }\n }\n\n /** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */\n private reconcile(envelope: IncomingEnvelope, message: AmqpMessage): Envelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n const count = deliveryCount(message);\n return { ...envelope, attempts: Math.max(body, count) } as Envelope;\n }\n\n private async onUnknownUrn(context: AmqpEventContext, envelope: Envelope, urn: string): Promise<void> {\n const delivery = context.delivery;\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n delivery?.accept();\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope, \"unknown_urn\");\n delivery?.accept();\n return;\n case UnknownUrnStrategy.RELEASE:\n delivery?.release({ delivery_failed: true });\n return;\n default: {\n // FAIL: surface and do NOT settle — the broker redelivers, then dead-letters.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, context);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(context: AmqpEventContext, envelope: Envelope, error: unknown): Promise<void> {\n const delivery = context.delivery;\n if ((envelope.attempts ?? 0) + 1 < this.maxTries) {\n // Release with delivery-failed leaves it for redelivery — the broker bumps delivery-count.\n delivery?.release({ delivery_failed: true });\n } else if (this.options.deadLetterSender) {\n await this.deadLetter(envelope, \"failed\", error);\n delivery?.accept();\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n private async deadLetter(envelope: Envelope, reason: string, error?: unknown): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n const original = envelope.meta?.queue ?? \"\";\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n await sender.send(artemisMessage(annotated));\n }\n\n private async deadLetterRaw(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sender.send({ body: raw });\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n this.options.onError?.(error, envelope, context);\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,8 +1,18 @@
1
1
  {
2
2
  "name": "@babelqueue/artemis",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Apache ActiveMQ Artemis adapter for BabelQueue — canonical-envelope publisher and a URN-routed AMQP 1.0 consumer (accept/release settlement, native scheduled delivery, cross-language DLQ) over rhea.",
5
- "keywords": ["babelqueue", "artemis", "activemq", "amqp", "rhea", "jms", "queue", "polyglot", "messaging"],
5
+ "keywords": [
6
+ "babelqueue",
7
+ "artemis",
8
+ "activemq",
9
+ "amqp",
10
+ "rhea",
11
+ "jms",
12
+ "queue",
13
+ "polyglot",
14
+ "messaging"
15
+ ],
6
16
  "homepage": "https://babelqueue.com",
7
17
  "bugs": {
8
18
  "url": "https://github.com/BabelQueue/babelqueue-node-adapters/issues"
@@ -30,7 +40,9 @@
30
40
  "main": "./dist/index.cjs",
31
41
  "module": "./dist/index.js",
32
42
  "types": "./dist/index.d.ts",
33
- "files": ["dist"],
43
+ "files": [
44
+ "dist"
45
+ ],
34
46
  "sideEffects": false,
35
47
  "engines": {
36
48
  "node": ">=18"
@@ -41,7 +53,7 @@
41
53
  "test": "node --import tsx --test test/*.test.ts"
42
54
  },
43
55
  "dependencies": {
44
- "@babelqueue/core": "^1.0.0"
56
+ "@babelqueue/core": "^1.4.0"
45
57
  },
46
58
  "peerDependencies": {
47
59
  "rhea": "^3.0.0"