@babelqueue/artemis 1.0.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -78,6 +78,22 @@ redelivers and bumps `delivery-count`); once `maxTries` is reached the envelope
78
78
  (falling back to the body URN), so it never decodes a message it cannot handle. Unknown-URN
79
79
  strategy is one of `fail` / `delete` / `release` / `dead_letter`.
80
80
 
81
+ **Version gate (§7.7).** Before the body is decoded the consumer reads the `bq_schema_version`
82
+ application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII
83
+ whitespace) follows the normal decode path; any other value is compared as-is (never trimmed) to
84
+ `"1"`. A message that does not match is rejected undecoded: reported to `onError`, its raw body
85
+ forwarded to the `deadLetterSender` (when configured), then `accept`ed — but only **after the broker
86
+ has accepted the DLQ delivery** (rhea `accepted` event on the DLQ sender). If the DLQ delivery is
87
+ rejected/released/modified, the link closes, `send` throws, or `deadLetterTimeoutMs` (default 30000)
88
+ elapses, the failure is reported to `onError` and the source message gets a plain `release()` instead —
89
+ after `deadLetterRetryDelayMs` (default 1000) so a persistently failing DLQ is not a hot redelivery
90
+ loop. It is never accepted and never reaches a handler. The release deliberately omits
91
+ `delivery_failed`: that would bump Artemis's delivery counter and, once `max-delivery-attempts` is hit,
92
+ route the message to the broker's DLA (or delete it when no DLA is configured). While waiting, the
93
+ delivery stays unsettled and holds one receiver credit. The DLQ confirmation listens on the DLQ
94
+ sender only for the duration of each gate send and re-dispatches events that are not its own to the
95
+ sender's session.
96
+
81
97
  > `autoaccept: false` is required — the consumer owns the disposition. `handle(context)` is also
82
98
  > exposed directly for testing or a custom event wiring.
83
99
 
@@ -100,6 +116,24 @@ The `bq-` application-property values are strings (integers as decimal, e.g. `"1
100
116
  is `"babelqueue"`. The envelope is unchanged (`schema_version` stays `1`); Artemis is purely
101
117
  additive.
102
118
 
119
+ ## OpenTelemetry tracing (ADR-0028)
120
+
121
+ Cross-hop **span** linkage rides on the out-of-band `HeaderCarrier` from
122
+ [`@babelqueue/core@^1.4.0`](https://www.npmjs.com/package/@babelqueue/core). Pass the carrier produced by
123
+ `@babelqueue/core/otel`'s `publish` to this adapter's `publish({ headers })`; it is carried on
124
+ 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),
125
+ so the core's `otel` `wrapHandler` starts the consumer span as a true **child** of the producer span.
126
+
127
+ ```ts
128
+ import { trace } from "@opentelemetry/api";
129
+ import { publish as tracedPublish } from "@babelqueue/core/otel";
130
+ import type { HeaderCarrier } from "@babelqueue/core";
131
+
132
+ const headers: HeaderCarrier = {};
133
+ await tracedPublish(trace.getTracer("orders"), urn, data,
134
+ () => adapterPublish(urn, data, { headers }), { headers });
135
+ ```
136
+
103
137
  ## License
104
138
 
105
139
  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),
@@ -70,10 +85,30 @@ function jmsType(message) {
70
85
  const value = message.message_annotations?.[JMS_TYPE_KEY];
71
86
  return value == null ? void 0 : String(value);
72
87
  }
88
+ var BLANK_ASCII = /^[ \t\n\v\f\r]*$/;
89
+ function unsupportedSchemaVersion(message) {
90
+ const raw = message.application_properties?.["bq_schema_version"];
91
+ if (raw == null) return void 0;
92
+ const value = Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw);
93
+ if (BLANK_ASCII.test(value)) return void 0;
94
+ return value === String(import_core.SCHEMA_VERSION) ? void 0 : value;
95
+ }
73
96
  function deliveryCount(message) {
74
97
  const value = message.delivery_count;
75
98
  return typeof value === "number" && value > 0 ? value : 0;
76
99
  }
100
+ function headersOf(message) {
101
+ const props = message.application_properties;
102
+ const out = {};
103
+ if (!props) return out;
104
+ for (const key of Object.keys(props)) {
105
+ const value = props[key];
106
+ if (value == null) continue;
107
+ const s = String(value);
108
+ if (s !== "") out[key] = s;
109
+ }
110
+ return out;
111
+ }
77
112
  var ArtemisPublisher = class _ArtemisPublisher {
78
113
  constructor(sender, queue) {
79
114
  this.sender = sender;
@@ -88,10 +123,107 @@ var ArtemisPublisher = class _ArtemisPublisher {
88
123
  /** Build + send the canonical envelope; returns the message id (`meta.id`). */
89
124
  async publish(urn, data, options = {}) {
90
125
  const envelope = import_core.EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });
91
- await this.sender.send(artemisMessage(envelope, options.delayMs));
126
+ await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));
92
127
  return envelope.meta.id;
93
128
  }
94
129
  };
130
+ var DEFAULT_DLQ_CONFIRM_TIMEOUT_MS = 3e4;
131
+ var DEFAULT_DLQ_RETRY_DELAY_MS = 1e3;
132
+ var CONFIRMATION_VERDICTS = ["accepted", "rejected", "released", "modified"];
133
+ var CONFIRMATION_CLOSES = ["sender_close", "sender_error"];
134
+ var ConfirmationHub = class {
135
+ constructor(sender, on, off) {
136
+ this.sender = sender;
137
+ this.on = on;
138
+ this.off = off;
139
+ }
140
+ sender;
141
+ on;
142
+ off;
143
+ pending = /* @__PURE__ */ new Set();
144
+ handlers = [];
145
+ add(confirmation) {
146
+ this.pending.add(confirmation);
147
+ if (this.handlers.length === 0) {
148
+ for (const name of [...CONFIRMATION_VERDICTS, ...CONFIRMATION_CLOSES]) {
149
+ const handler = (context) => this.dispatch(name, context);
150
+ this.handlers.push([name, handler]);
151
+ this.on(name, handler);
152
+ }
153
+ }
154
+ }
155
+ remove(confirmation) {
156
+ this.pending.delete(confirmation);
157
+ if (this.pending.size === 0) {
158
+ for (const [name, handler] of this.handlers) this.off(name, handler);
159
+ this.handlers.length = 0;
160
+ hubs.delete(this.sender);
161
+ }
162
+ }
163
+ dispatch(name, context) {
164
+ if (CONFIRMATION_CLOSES.includes(name)) {
165
+ for (const confirmation of [...this.pending]) {
166
+ confirmation.settle(new import_core.BabelQueueError(`The dead-letter link closed before the broker accepted the delivery (${name}).`));
167
+ }
168
+ this.forward(name, context);
169
+ return;
170
+ }
171
+ const owners = [...this.pending].filter((confirmation) => confirmation.matches(context));
172
+ if (owners.length === 0) {
173
+ this.forward(name, context);
174
+ return;
175
+ }
176
+ for (const owner of owners) {
177
+ owner.settle(name === "accepted" ? void 0 : new import_core.BabelQueueError(`The broker did not accept the dead-letter delivery (${name}).`));
178
+ }
179
+ }
180
+ forward(name, context) {
181
+ try {
182
+ this.sender.session?.dispatch?.(name, context);
183
+ } catch {
184
+ }
185
+ }
186
+ };
187
+ var hubs = /* @__PURE__ */ new WeakMap();
188
+ function sendConfirmed(sender, message, timeoutMs) {
189
+ const on = sender.on?.bind(sender);
190
+ const off = sender.removeListener?.bind(sender);
191
+ if (!on || !off) return Promise.resolve(sender.send(message)).then(() => void 0);
192
+ return new Promise((resolve, reject) => {
193
+ let hub = hubs.get(sender);
194
+ if (!hub) {
195
+ hub = new ConfirmationHub(sender, on, off);
196
+ hubs.set(sender, hub);
197
+ }
198
+ const activeHub = hub;
199
+ let finished = false;
200
+ const confirmation = {
201
+ delivery: void 0,
202
+ matches: (context) => confirmation.delivery === void 0 || context?.delivery === void 0 || context.delivery === confirmation.delivery,
203
+ settle: (error) => {
204
+ if (finished) return;
205
+ finished = true;
206
+ clearTimeout(timer);
207
+ activeHub.remove(confirmation);
208
+ if (error) reject(error);
209
+ else resolve();
210
+ }
211
+ };
212
+ activeHub.add(confirmation);
213
+ const timer = setTimeout(
214
+ () => confirmation.settle(new import_core.BabelQueueError(`Timed out after ${timeoutMs}ms waiting for the broker to accept the dead-letter delivery.`)),
215
+ timeoutMs
216
+ );
217
+ try {
218
+ confirmation.delivery = sender.send(message);
219
+ } catch (error) {
220
+ confirmation.settle(error instanceof Error ? error : new import_core.BabelQueueError(String(error)));
221
+ return;
222
+ }
223
+ const delivery = confirmation.delivery;
224
+ if (delivery === null || typeof delivery !== "object" || delivery.settled === true) confirmation.settle();
225
+ });
226
+ }
95
227
  var ArtemisConsumer = class {
96
228
  constructor(handlers, options = {}) {
97
229
  this.handlers = handlers;
@@ -111,11 +243,27 @@ var ArtemisConsumer = class {
111
243
  async handle(context) {
112
244
  const message = context.message ?? {};
113
245
  const delivery = context.delivery;
246
+ const unsupported = unsupportedSchemaVersion(message);
247
+ if (unsupported !== void 0) {
248
+ const gateError = new import_core.BabelQueueError(
249
+ `Rejected a message from Artemis with unsupported bq_schema_version "${unsupported}" (supported: ${import_core.SCHEMA_VERSION}).`
250
+ );
251
+ try {
252
+ await this.deadLetterRawConfirmed(messageBody(message));
253
+ } catch (dlqError) {
254
+ this.safeReport(gateError, null, context);
255
+ this.safeReport(dlqError, null, context);
256
+ await this.releaseAfterBackoff(delivery);
257
+ return;
258
+ }
259
+ delivery?.accept();
260
+ this.safeReport(gateError, null, context);
261
+ return;
262
+ }
114
263
  const decoded = import_core.EnvelopeCodec.decode(messageBody(message));
115
264
  if (!import_core.EnvelopeCodec.accepts(decoded)) {
116
265
  this.report(new import_core.BabelQueueError("Rejected a non-conformant BabelQueue envelope from Artemis."), decoded, context);
117
- await this.deadLetterRaw(messageBody(message));
118
- delivery?.accept();
266
+ await this.settleAfterDeadLetter(context, decoded, () => this.deadLetterRaw(messageBody(message)));
119
267
  return;
120
268
  }
121
269
  const envelope = this.reconcile(decoded, message);
@@ -126,7 +274,7 @@ var ArtemisConsumer = class {
126
274
  return;
127
275
  }
128
276
  try {
129
- await handler(envelope, message);
277
+ await handler(envelope, message, headersOf(message));
130
278
  delivery?.accept();
131
279
  } catch (error) {
132
280
  this.report(error, envelope, context);
@@ -146,8 +294,7 @@ var ArtemisConsumer = class {
146
294
  delivery?.accept();
147
295
  return;
148
296
  case import_core.UnknownUrnStrategy.DEAD_LETTER:
149
- await this.deadLetter(envelope, "unknown_urn");
150
- delivery?.accept();
297
+ await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "unknown_urn"));
151
298
  return;
152
299
  case import_core.UnknownUrnStrategy.RELEASE:
153
300
  delivery?.release({ delivery_failed: true });
@@ -164,12 +311,26 @@ var ArtemisConsumer = class {
164
311
  if ((envelope.attempts ?? 0) + 1 < this.maxTries) {
165
312
  delivery?.release({ delivery_failed: true });
166
313
  } else if (this.options.deadLetterSender) {
167
- await this.deadLetter(envelope, "failed", error);
168
- delivery?.accept();
314
+ await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "failed", error));
169
315
  } else {
170
316
  delivery?.accept();
171
317
  }
172
318
  }
319
+ /**
320
+ * Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry
321
+ * delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender
322
+ * the write is a no-op and the message is accepted (terminal drop), as before.
323
+ */
324
+ async settleAfterDeadLetter(context, envelope, write) {
325
+ try {
326
+ await write();
327
+ } catch (dlqError) {
328
+ this.safeReport(dlqError, envelope, context);
329
+ await this.releaseAfterBackoff(context.delivery);
330
+ return;
331
+ }
332
+ context.delivery?.accept();
333
+ }
173
334
  async deadLetter(envelope, reason, error) {
174
335
  const sender = this.options.deadLetterSender;
175
336
  if (!sender) return;
@@ -179,12 +340,30 @@ var ArtemisConsumer = class {
179
340
  error: error instanceof Error ? error.message : null,
180
341
  exception: error instanceof Error ? error.name : null
181
342
  });
182
- await sender.send(artemisMessage(annotated));
343
+ await sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
183
344
  }
184
- async deadLetterRaw(raw) {
345
+ /** Poison path: forward the raw body, confirmed like the gate path. */
346
+ deadLetterRaw(raw) {
347
+ return this.deadLetterRawConfirmed(raw);
348
+ }
349
+ /** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */
350
+ async deadLetterRawConfirmed(raw) {
185
351
  const sender = this.options.deadLetterSender;
186
352
  if (!sender) return;
187
- await sender.send({ body: raw });
353
+ await sendConfirmed(sender, { body: raw }, this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
354
+ }
355
+ /** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
356
+ async releaseAfterBackoff(delivery) {
357
+ const delayMs = this.options.deadLetterRetryDelayMs ?? DEFAULT_DLQ_RETRY_DELAY_MS;
358
+ if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs).unref?.());
359
+ delivery?.release();
360
+ }
361
+ /** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */
362
+ safeReport(error, envelope, context) {
363
+ try {
364
+ this.report(error, envelope, context);
365
+ } catch {
366
+ }
188
367
  }
189
368
  report(error, envelope, context) {
190
369
  this.options.onError?.(error, envelope, context);
@@ -197,6 +376,7 @@ var ArtemisConsumer = class {
197
376
  JMS_TYPE_KEY,
198
377
  SCHEDULED_DELIVERY_KEY,
199
378
  artemisMessage,
379
+ headersOf,
200
380
  messageBody
201
381
  });
202
382
  //# 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 * **Version gate (§7.7).** Before decoding the body the consumer reads the `bq_schema_version`\n * application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII\n * whitespace) follows the normal decode path; any other value is compared, untrimmed, to the supported\n * `\"1\"`, and a message that does not match is rejected undecoded — reported to `onError`, the raw body\n * forwarded to the DLQ sender (when configured) and the message accepted **only after the broker has\n * accepted the DLQ delivery**. If the DLQ send fails, is refused (rejected/released/modified), the link\n * closes or `deadLetterTimeoutMs` elapses, the failure is reported and, after `deadLetterRetryDelayMs`,\n * the source message gets a plain `released` outcome (no `delivery_failed`: the broker's delivery\n * counter and `max-delivery-attempts` stay untouched) — never accepted, never handed to a handler.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, 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/**\n * The context rhea passes to a sender link event (`accepted`, `rejected`, `sender_close`, ...). Only\n * `delivery` is inspected (to match a verdict to its delivery); everything else is opaque. Deliberately\n * no index signature: an interface-typed handler parameter (e.g. rhea's `EventContext`) must stay assignable.\n */\nexport interface AmqpLinkEventContext {\n delivery?: unknown;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n /** Queue a delivery. rhea returns the delivery synchronously; the broker's verdict arrives later as an event. */\n send(message: AmqpMessage): unknown;\n /**\n * Optional (a rhea sender has it). When both `on` and `removeListener` exist, the consumer's\n * version-gate path waits for the broker's `accepted` disposition on the DLQ delivery before it\n * settles the source message; without them only a synchronous `send` failure is observable.\n */\n on?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;\n removeListener?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;\n /**\n * Optional (a rhea link has it). rhea does not bubble an event to the session/connection while the\n * link has a listener for it, so the temporary confirmation listeners re-dispatch what is not theirs\n * (other deliveries' dispositions, link close/error) here.\n */\n session?: { dispatch?(event: string, context?: AmqpLinkEventContext): 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\n/** Blank = empty or only ASCII whitespace (GR-5: the same set in every SDK, never `String.trim()`). */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The inbound `bq_schema_version` application property when it is present, non-blank and not exactly\n * the supported version (compared untrimmed), else `undefined` (the message may be decoded).\n */\nfunction unsupportedSchemaVersion(message: AmqpMessage): string | undefined {\n const raw = message.application_properties?.[\"bq_schema_version\"];\n if (raw == null) return undefined;\n const value = Buffer.isBuffer(raw) ? raw.toString(\"utf8\") : String(raw);\n if (BLANK_ASCII.test(value)) return undefined;\n return value === String(SCHEMA_VERSION) ? undefined : 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// --- DLQ delivery confirmation ---------------------------------------------------\n\n/** Default wait (ms) for the broker to settle a gate-path DLQ delivery. */\nconst DEFAULT_DLQ_CONFIRM_TIMEOUT_MS = 30_000;\n/** Default pause (ms) before a gate message whose DLQ write failed is released back to the broker. */\nconst DEFAULT_DLQ_RETRY_DELAY_MS = 1_000;\n\n/** One in-flight DLQ confirmation registered on a {@link ConfirmationHub}. */\ninterface PendingConfirmation {\n /** The delivery `send` returned; `undefined` while `send` is still running synchronously. */\n delivery: unknown;\n /** `true` when `context` is (or may be) this confirmation's verdict. */\n matches(context: AmqpLinkEventContext | undefined): boolean;\n /** Resolve (no argument) or reject the confirmation. */\n settle(error?: Error): void;\n}\n\nconst CONFIRMATION_VERDICTS = [\"accepted\", \"rejected\", \"released\", \"modified\"] as const;\nconst CONFIRMATION_CLOSES = [\"sender_close\", \"sender_error\"] as const;\n\n/**\n * One shared set of temporary sender listeners per {@link AmqpSender}, however many DLQ confirmations\n * are in flight. rhea bubbles a link event to the session only while the link has no listener for it,\n * so while confirmations are pending the hub owns the listeners and re-dispatches every event it does\n * not consume to `sender.session` exactly once (per-confirmation listeners would forward each event\n * once per pending confirmation). The listeners are removed as soon as no confirmation is pending.\n */\nclass ConfirmationHub {\n private readonly pending = new Set<PendingConfirmation>();\n private readonly handlers: [string, (context: AmqpLinkEventContext) => void][] = [];\n\n constructor(\n private readonly sender: AmqpSender,\n private readonly on: (event: string, handler: (context: AmqpLinkEventContext) => void) => unknown,\n private readonly off: (event: string, handler: (context: AmqpLinkEventContext) => void) => unknown,\n ) {}\n\n add(confirmation: PendingConfirmation): void {\n this.pending.add(confirmation);\n if (this.handlers.length === 0) {\n for (const name of [...CONFIRMATION_VERDICTS, ...CONFIRMATION_CLOSES]) {\n const handler = (context: AmqpLinkEventContext): void => this.dispatch(name, context);\n this.handlers.push([name, handler]);\n this.on(name, handler);\n }\n }\n }\n\n remove(confirmation: PendingConfirmation): void {\n this.pending.delete(confirmation);\n if (this.pending.size === 0) {\n for (const [name, handler] of this.handlers) this.off(name, handler);\n this.handlers.length = 0;\n hubs.delete(this.sender);\n }\n }\n\n private dispatch(name: string, context: AmqpLinkEventContext): void {\n if ((CONFIRMATION_CLOSES as readonly string[]).includes(name)) {\n for (const confirmation of [...this.pending]) {\n confirmation.settle(new BabelQueueError(`The dead-letter link closed before the broker accepted the delivery (${name}).`));\n }\n this.forward(name, context);\n return;\n }\n const owners = [...this.pending].filter((confirmation) => confirmation.matches(context));\n if (owners.length === 0) {\n this.forward(name, context);\n return;\n }\n for (const owner of owners) {\n owner.settle(name === \"accepted\" ? undefined : new BabelQueueError(`The broker did not accept the dead-letter delivery (${name}).`));\n }\n }\n\n private forward(name: string, context: AmqpLinkEventContext): void {\n try {\n this.sender.session?.dispatch?.(name, context);\n } catch {\n // forwarding is best effort; never let it change a confirmation outcome\n }\n }\n}\n\nconst hubs = new WeakMap<AmqpSender, ConfirmationHub>();\n\n/**\n * Send `message` and resolve only once the broker has `accepted` the delivery. A rhea `send` returns\n * synchronously and reports the verdict (`accepted` / `rejected` / `released` / `modified`) as a\n * sender event, so awaiting `send` alone cannot see a broker refusal. Rejects on any non-accepted\n * disposition, a link close/error, or when `timeoutMs` elapses (an unsettled delivery that is lost on\n * reconnect raises no event). A sender without event support degrades to awaiting `send` itself.\n * Events that are not this delivery's verdict (other deliveries, link close/error) are re-dispatched on\n * the sender's session, once per event, so the application still sees them (rhea suppresses bubbling\n * while the link has a listener) — see {@link ConfirmationHub}.\n */\nfunction sendConfirmed(sender: AmqpSender, message: AmqpMessage, timeoutMs: number): Promise<void> {\n const on = sender.on?.bind(sender);\n const off = sender.removeListener?.bind(sender);\n if (!on || !off) return Promise.resolve(sender.send(message)).then(() => undefined);\n\n return new Promise<void>((resolve, reject) => {\n let hub = hubs.get(sender);\n if (!hub) {\n hub = new ConfirmationHub(sender, on, off);\n hubs.set(sender, hub);\n }\n const activeHub = hub;\n let finished = false;\n\n const confirmation: PendingConfirmation = {\n delivery: undefined,\n matches: (context) =>\n confirmation.delivery === undefined || context?.delivery === undefined || context.delivery === confirmation.delivery,\n settle: (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timer);\n activeHub.remove(confirmation);\n if (error) reject(error);\n else resolve();\n },\n };\n activeHub.add(confirmation);\n\n // `settle` (above) reads `timer`; it cannot run before this line (nothing is sent or listened to yet).\n const timer = setTimeout(\n () => confirmation.settle(new BabelQueueError(`Timed out after ${timeoutMs}ms waiting for the broker to accept the dead-letter delivery.`)),\n timeoutMs,\n );\n try {\n confirmation.delivery = sender.send(message);\n } catch (error) {\n confirmation.settle(error instanceof Error ? error : new BabelQueueError(String(error)));\n return;\n }\n // A pre-settled delivery (or a sender that returns none) raises no disposition event.\n const delivery = confirmation.delivery;\n if (delivery === null || typeof delivery !== \"object\" || (delivery as { settled?: unknown }).settled === true) confirmation.settle();\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 /**\n * How long (ms) every dead-letter path (version gate, poison, unknown-URN `dead_letter`, terminal\n * handler failure) waits for the broker to accept the DLQ delivery before it releases the source\n * message instead of accepting it (default 30000).\n */\n deadLetterTimeoutMs?: number;\n /**\n * Pause (ms) before a dead-letter path releases a message whose DLQ write failed (default 1000).\n * The release is a plain `released` outcome (no `delivery_failed`, so the broker's delivery counter and\n * `max-delivery-attempts` are untouched); the pause keeps a persistently failing DLQ from becoming a\n * hot redelivery loop. The delivery stays unsettled (holding one credit) while it waits.\n * Note: on the terminal handler-failure path the message has already been through the handler, and\n * a plain release does not bump the delivery counter, so the redelivery runs the handler again (and\n * reports its failure again) until the DLQ write succeeds.\n */\n deadLetterRetryDelayMs?: number;\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\n // Version gate (§7.7): decided on the application property alone, before the body is decoded.\n const unsupported = unsupportedSchemaVersion(message);\n if (unsupported !== undefined) {\n const gateError = new BabelQueueError(\n `Rejected a message from Artemis with unsupported bq_schema_version \"${unsupported}\" (supported: ${SCHEMA_VERSION}).`,\n );\n // Accept only after the broker has accepted the DLQ copy; otherwise release (plain, after a pause) so\n // the message is never lost and never reaches a handler. Reports come after the settlement, or are\n // isolated, so a throwing onError can never leave the message unsettled.\n try {\n await this.deadLetterRawConfirmed(messageBody(message));\n } catch (dlqError) {\n this.safeReport(gateError, null, context);\n this.safeReport(dlqError, null, context);\n await this.releaseAfterBackoff(delivery);\n return;\n }\n delivery?.accept();\n this.safeReport(gateError, null, context);\n return;\n }\n\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.settleAfterDeadLetter(context, decoded, () => this.deadLetterRaw(messageBody(message)));\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.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, \"unknown_urn\"));\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.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, \"failed\", error));\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n /**\n * Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry\n * delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender\n * the write is a no-op and the message is accepted (terminal drop), as before.\n */\n private async settleAfterDeadLetter(\n context: AmqpEventContext,\n envelope: IncomingEnvelope | null,\n write: () => Promise<void>,\n ): Promise<void> {\n try {\n await write();\n } catch (dlqError) {\n this.safeReport(dlqError, envelope, context);\n await this.releaseAfterBackoff(context.delivery);\n return;\n }\n context.delivery?.accept();\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 sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);\n }\n\n /** Poison path: forward the raw body, confirmed like the gate path. */\n private deadLetterRaw(raw: string): Promise<void> {\n return this.deadLetterRawConfirmed(raw);\n }\n\n /** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */\n private async deadLetterRawConfirmed(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sendConfirmed(sender, { body: raw }, this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);\n }\n\n /** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */\n private async releaseAfterBackoff(delivery: AmqpDelivery | undefined): Promise<void> {\n const delayMs = this.options.deadLetterRetryDelayMs ?? DEFAULT_DLQ_RETRY_DELAY_MS;\n if (delayMs > 0) await new Promise<void>((resolve) => setTimeout(resolve, delayMs).unref?.());\n delivery?.release();\n }\n\n /** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */\n private safeReport(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n try {\n this.report(error, envelope, context);\n } catch {\n // swallowed by design\n }\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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwCA,kBAA8G;;;ACzBvG,SAAS,gBAAgB,SAA0D;AACxF,QAAM,MAAqB,CAAC;AAC5B,MAAI,CAAC,QAAS,QAAO;AACrB,aAAW,OAAO,OAAO,KAAK,OAAO,GAAG;AACtC,UAAM,QAAQ,QAAQ,GAAG;AACzB,QAAI,QAAQ,MAAM,SAAS,QAAQ,UAAU,GAAI;AACjD,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO;AACT;;;ADqFO,IAAM,eAAe;AAErB,IAAM,yBAAyB;AAQ/B,SAAS,eAAe,UAAoB,SAAkB,OAA2C;AAC9G,QAAM,cAAuC,CAAC;AAC9C,MAAI,SAAS,IAAK,aAAY,YAAY,IAAI,SAAS;AAMvD,QAAM,wBAAiD,CAAC;AACxD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,gBAAgB,KAAK,CAAC,EAAG,uBAAsB,GAAG,IAAI;AAChG,wBAAsB,mBAAmB,IAAI,OAAO,SAAS,KAAK,cAAc;AAChF,wBAAsB,aAAa,IAAI,OAAO,SAAS,YAAY,CAAC;AACpE,wBAAsB,WAAW,IAAI;AACrC,MAAI,SAAS,KAAK,KAAM,uBAAsB,gBAAgB,IAAI,SAAS,KAAK;AAEhF,QAAM,UAAuB;AAAA,IAC3B,MAAM,0BAAc,OAAO,QAAQ;AAAA,IACnC,cAAc;AAAA,IACd,wBAAwB;AAAA,IACxB,qBAAqB;AAAA,EACvB;AACA,MAAI,SAAS,SAAU,SAAQ,iBAAiB,SAAS;AACzD,MAAI,SAAS,KAAK,cAAc,KAAM,SAAQ,gBAAgB,SAAS,KAAK;AAE5E,MAAI,WAAW,QAAQ,UAAU,GAAG;AAClC,0BAAsB,UAAU,IAAI,OAAO,OAAO;AAClD,gBAAY,sBAAsB,IAAI,KAAK,IAAI,IAAI;AAAA,EACrD;AACA,SAAO;AACT;AAGO,SAAS,YAAY,SAA8B;AACxD,QAAM,OAAO,QAAQ;AACrB,MAAI,QAAQ,KAAM,QAAO;AACzB,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,MAAI,OAAO,SAAS,IAAI,EAAG,QAAO,KAAK,SAAS,MAAM;AACtD,MAAI,OAAO,SAAS,YAAY,aAAa,MAAM;AACjD,UAAM,UAAW,KAA8B;AAC/C,QAAI,OAAO,SAAS,OAAO,EAAG,QAAO,QAAQ,SAAS,MAAM;AAC5D,QAAI,OAAO,YAAY,SAAU,QAAO;AAAA,EAC1C;AACA,SAAO,OAAO,IAAI;AACpB;AAEA,SAAS,QAAQ,SAA0C;AACzD,QAAM,QAAQ,QAAQ,sBAAsB,YAAY;AACxD,SAAO,SAAS,OAAO,SAAY,OAAO,KAAK;AACjD;AAGA,IAAM,cAAc;AAMpB,SAAS,yBAAyB,SAA0C;AAC1E,QAAM,MAAM,QAAQ,yBAAyB,mBAAmB;AAChE,MAAI,OAAO,KAAM,QAAO;AACxB,QAAM,QAAQ,OAAO,SAAS,GAAG,IAAI,IAAI,SAAS,MAAM,IAAI,OAAO,GAAG;AACtE,MAAI,YAAY,KAAK,KAAK,EAAG,QAAO;AACpC,SAAO,UAAU,OAAO,0BAAc,IAAI,SAAY;AACxD;AAEA,SAAS,cAAc,SAA8B;AACnD,QAAM,QAAQ,QAAQ;AACtB,SAAO,OAAO,UAAU,YAAY,QAAQ,IAAI,QAAQ;AAC1D;AAQO,SAAS,UAAU,SAAqC;AAC7D,QAAM,QAAQ,QAAQ;AACtB,QAAM,MAAqB,CAAC;AAC5B,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC,UAAM,QAAQ,MAAM,GAAG;AACvB,QAAI,SAAS,KAAM;AACnB,UAAM,IAAI,OAAO,KAAK;AACtB,QAAI,MAAM,GAAI,KAAI,GAAG,IAAI;AAAA,EAC3B;AACA,SAAO;AACT;AAmBO,IAAM,mBAAN,MAAM,kBAAiB;AAAA,EACpB,YACW,QACA,OACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EAFgB;AAAA,EACA;AAAA;AAAA,EAInB,OAAO,OAAO,QAAoB,OAAiC;AACjE,WAAO,IAAI,kBAAiB,QAAQ,KAAK;AAAA,EAC3C;AAAA;AAAA,EAGA,MAAM,QAAQ,KAAa,MAA+B,UAA0B,CAAC,GAAoB;AACvG,UAAM,WAAW,0BAAc,KAAK,KAAK,MAAM,EAAE,OAAO,KAAK,OAAO,SAAS,QAAQ,QAAQ,CAAC;AAC9F,UAAM,KAAK,OAAO,KAAK,eAAe,UAAU,QAAQ,SAAS,QAAQ,OAAO,CAAC;AACjF,WAAO,SAAS,KAAK;AAAA,EACvB;AACF;AAKA,IAAM,iCAAiC;AAEvC,IAAM,6BAA6B;AAYnC,IAAM,wBAAwB,CAAC,YAAY,YAAY,YAAY,UAAU;AAC7E,IAAM,sBAAsB,CAAC,gBAAgB,cAAc;AAS3D,IAAM,kBAAN,MAAsB;AAAA,EAIpB,YACmB,QACA,IACA,KACjB;AAHiB;AACA;AACA;AAAA,EAChB;AAAA,EAHgB;AAAA,EACA;AAAA,EACA;AAAA,EANF,UAAU,oBAAI,IAAyB;AAAA,EACvC,WAAgE,CAAC;AAAA,EAQlF,IAAI,cAAyC;AAC3C,SAAK,QAAQ,IAAI,YAAY;AAC7B,QAAI,KAAK,SAAS,WAAW,GAAG;AAC9B,iBAAW,QAAQ,CAAC,GAAG,uBAAuB,GAAG,mBAAmB,GAAG;AACrE,cAAM,UAAU,CAAC,YAAwC,KAAK,SAAS,MAAM,OAAO;AACpF,aAAK,SAAS,KAAK,CAAC,MAAM,OAAO,CAAC;AAClC,aAAK,GAAG,MAAM,OAAO;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,OAAO,cAAyC;AAC9C,SAAK,QAAQ,OAAO,YAAY;AAChC,QAAI,KAAK,QAAQ,SAAS,GAAG;AAC3B,iBAAW,CAAC,MAAM,OAAO,KAAK,KAAK,SAAU,MAAK,IAAI,MAAM,OAAO;AACnE,WAAK,SAAS,SAAS;AACvB,WAAK,OAAO,KAAK,MAAM;AAAA,IACzB;AAAA,EACF;AAAA,EAEQ,SAAS,MAAc,SAAqC;AAClE,QAAK,oBAA0C,SAAS,IAAI,GAAG;AAC7D,iBAAW,gBAAgB,CAAC,GAAG,KAAK,OAAO,GAAG;AAC5C,qBAAa,OAAO,IAAI,4BAAgB,wEAAwE,IAAI,IAAI,CAAC;AAAA,MAC3H;AACA,WAAK,QAAQ,MAAM,OAAO;AAC1B;AAAA,IACF;AACA,UAAM,SAAS,CAAC,GAAG,KAAK,OAAO,EAAE,OAAO,CAAC,iBAAiB,aAAa,QAAQ,OAAO,CAAC;AACvF,QAAI,OAAO,WAAW,GAAG;AACvB,WAAK,QAAQ,MAAM,OAAO;AAC1B;AAAA,IACF;AACA,eAAW,SAAS,QAAQ;AAC1B,YAAM,OAAO,SAAS,aAAa,SAAY,IAAI,4BAAgB,uDAAuD,IAAI,IAAI,CAAC;AAAA,IACrI;AAAA,EACF;AAAA,EAEQ,QAAQ,MAAc,SAAqC;AACjE,QAAI;AACF,WAAK,OAAO,SAAS,WAAW,MAAM,OAAO;AAAA,IAC/C,QAAQ;AAAA,IAER;AAAA,EACF;AACF;AAEA,IAAM,OAAO,oBAAI,QAAqC;AAYtD,SAAS,cAAc,QAAoB,SAAsB,WAAkC;AACjG,QAAM,KAAK,OAAO,IAAI,KAAK,MAAM;AACjC,QAAM,MAAM,OAAO,gBAAgB,KAAK,MAAM;AAC9C,MAAI,CAAC,MAAM,CAAC,IAAK,QAAO,QAAQ,QAAQ,OAAO,KAAK,OAAO,CAAC,EAAE,KAAK,MAAM,MAAS;AAElF,SAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,QAAI,MAAM,KAAK,IAAI,MAAM;AACzB,QAAI,CAAC,KAAK;AACR,YAAM,IAAI,gBAAgB,QAAQ,IAAI,GAAG;AACzC,WAAK,IAAI,QAAQ,GAAG;AAAA,IACtB;AACA,UAAM,YAAY;AAClB,QAAI,WAAW;AAEf,UAAM,eAAoC;AAAA,MACxC,UAAU;AAAA,MACV,SAAS,CAAC,YACR,aAAa,aAAa,UAAa,SAAS,aAAa,UAAa,QAAQ,aAAa,aAAa;AAAA,MAC9G,QAAQ,CAAC,UAAU;AACjB,YAAI,SAAU;AACd,mBAAW;AACX,qBAAa,KAAK;AAClB,kBAAU,OAAO,YAAY;AAC7B,YAAI,MAAO,QAAO,KAAK;AAAA,YAClB,SAAQ;AAAA,MACf;AAAA,IACF;AACA,cAAU,IAAI,YAAY;AAG1B,UAAM,QAAQ;AAAA,MACZ,MAAM,aAAa,OAAO,IAAI,4BAAgB,mBAAmB,SAAS,+DAA+D,CAAC;AAAA,MAC1I;AAAA,IACF;AACA,QAAI;AACF,mBAAa,WAAW,OAAO,KAAK,OAAO;AAAA,IAC7C,SAAS,OAAO;AACd,mBAAa,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,4BAAgB,OAAO,KAAK,CAAC,CAAC;AACvF;AAAA,IACF;AAEA,UAAM,WAAW,aAAa;AAC9B,QAAI,aAAa,QAAQ,OAAO,aAAa,YAAa,SAAmC,YAAY,KAAM,cAAa,OAAO;AAAA,EACrI,CAAC;AACH;AAmDO,IAAM,kBAAN,MAAsB;AAAA,EAI3B,YACmB,UACA,UAAkC,CAAC,GACpD;AAFiB;AACA;AAEjB,SAAK,WAAW,QAAQ,YAAY;AACpC,SAAK,aAAa,QAAQ,cAAc,+BAAmB;AAAA,EAC7D;AAAA,EALmB;AAAA,EACA;AAAA,EALF;AAAA,EACA;AAAA;AAAA,EAWjB,OAAO,UAA8B;AACnC,aAAS,GAAG,WAAW,CAAC,YAAY,KAAK,OAAO,OAAO,CAAC;AAAA,EAC1D;AAAA;AAAA,EAGA,MAAM,OAAO,SAA0C;AACrD,UAAM,UAAU,QAAQ,WAAW,CAAC;AACpC,UAAM,WAAW,QAAQ;AAGzB,UAAM,cAAc,yBAAyB,OAAO;AACpD,QAAI,gBAAgB,QAAW;AAC7B,YAAM,YAAY,IAAI;AAAA,QACpB,uEAAuE,WAAW,iBAAiB,0BAAc;AAAA,MACnH;AAIA,UAAI;AACF,cAAM,KAAK,uBAAuB,YAAY,OAAO,CAAC;AAAA,MACxD,SAAS,UAAU;AACjB,aAAK,WAAW,WAAW,MAAM,OAAO;AACxC,aAAK,WAAW,UAAU,MAAM,OAAO;AACvC,cAAM,KAAK,oBAAoB,QAAQ;AACvC;AAAA,MACF;AACA,gBAAU,OAAO;AACjB,WAAK,WAAW,WAAW,MAAM,OAAO;AACxC;AAAA,IACF;AAEA,UAAM,UAAU,0BAAc,OAAO,YAAY,OAAO,CAAC;AAEzD,QAAI,CAAC,0BAAc,QAAQ,OAAO,GAAG;AAEnC,WAAK,OAAO,IAAI,4BAAgB,6DAA6D,GAAG,SAAS,OAAO;AAChH,YAAM,KAAK,sBAAsB,SAAS,SAAS,MAAM,KAAK,cAAc,YAAY,OAAO,CAAC,CAAC;AACjG;AAAA,IACF;AAEA,UAAM,WAAW,KAAK,UAAU,SAAS,OAAO;AAChD,UAAM,MAAM,QAAQ,OAAO,KAAK,0BAAc,IAAI,QAAQ;AAC1D,UAAM,UAAU,KAAK,SAAS,GAAG;AACjC,QAAI,CAAC,SAAS;AACZ,YAAM,KAAK,aAAa,SAAS,UAAU,GAAG;AAC9C;AAAA,IACF;AAEA,QAAI;AACF,YAAM,QAAQ,UAAU,SAAS,UAAU,OAAO,CAAC;AACnD,gBAAU,OAAO;AAAA,IACnB,SAAS,OAAO;AACd,WAAK,OAAO,OAAO,UAAU,OAAO;AACpC,YAAM,KAAK,kBAAkB,SAAS,UAAU,KAAK;AAAA,IACvD;AAAA,EACF;AAAA;AAAA,EAGQ,UAAU,UAA4B,SAAgC;AAC5E,UAAM,OAAO,OAAO,SAAS,aAAa,WAAW,SAAS,WAAW;AACzE,UAAM,QAAQ,cAAc,OAAO;AACnC,WAAO,EAAE,GAAG,UAAU,UAAU,KAAK,IAAI,MAAM,KAAK,EAAE;AAAA,EACxD;AAAA,EAEA,MAAc,aAAa,SAA2B,UAAoB,KAA4B;AACpG,UAAM,WAAW,QAAQ;AACzB,YAAQ,KAAK,YAAY;AAAA,MACvB,KAAK,+BAAmB;AACtB,kBAAU,OAAO;AACjB;AAAA,MACF,KAAK,+BAAmB;AACtB,cAAM,KAAK,sBAAsB,SAAS,UAAU,MAAM,KAAK,WAAW,UAAU,aAAa,CAAC;AAClG;AAAA,MACF,KAAK,+BAAmB;AACtB,kBAAU,QAAQ,EAAE,iBAAiB,KAAK,CAAC;AAC3C;AAAA,MACF,SAAS;AAEP,cAAM,QAAQ,IAAI,4BAAgB,GAAG;AACrC,aAAK,OAAO,OAAO,UAAU,OAAO;AACpC,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,kBAAkB,SAA2B,UAAoB,OAA+B;AAC5G,UAAM,WAAW,QAAQ;AACzB,SAAK,SAAS,YAAY,KAAK,IAAI,KAAK,UAAU;AAEhD,gBAAU,QAAQ,EAAE,iBAAiB,KAAK,CAAC;AAAA,IAC7C,WAAW,KAAK,QAAQ,kBAAkB;AACxC,YAAM,KAAK,sBAAsB,SAAS,UAAU,MAAM,KAAK,WAAW,UAAU,UAAU,KAAK,CAAC;AAAA,IACtG,OAAO;AACL,gBAAU,OAAO;AAAA,IACnB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,sBACZ,SACA,UACA,OACe;AACf,QAAI;AACF,YAAM,MAAM;AAAA,IACd,SAAS,UAAU;AACjB,WAAK,WAAW,UAAU,UAAU,OAAO;AAC3C,YAAM,KAAK,oBAAoB,QAAQ,QAAQ;AAC/C;AAAA,IACF;AACA,YAAQ,UAAU,OAAO;AAAA,EAC3B;AAAA,EAEA,MAAc,WAAW,UAAoB,QAAgB,OAAgC;AAC3F,UAAM,SAAS,KAAK,QAAQ;AAC5B,QAAI,CAAC,OAAQ;AACb,UAAM,WAAW,SAAS,MAAM,SAAS;AACzC,UAAM,gBAAY,sBAAS,UAAU,QAAQ,UAAU;AAAA,MACrD,UAAU,SAAS,YAAY;AAAA,MAC/B,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MAChD,WAAW,iBAAiB,QAAQ,MAAM,OAAO;AAAA,IACnD,CAAC;AACD,UAAM,cAAc,QAAQ,eAAe,SAAS,GAAG,KAAK,QAAQ,uBAAuB,8BAA8B;AAAA,EAC3H;AAAA;AAAA,EAGQ,cAAc,KAA4B;AAChD,WAAO,KAAK,uBAAuB,GAAG;AAAA,EACxC;AAAA;AAAA,EAGA,MAAc,uBAAuB,KAA4B;AAC/D,UAAM,SAAS,KAAK,QAAQ;AAC5B,QAAI,CAAC,OAAQ;AACb,UAAM,cAAc,QAAQ,EAAE,MAAM,IAAI,GAAG,KAAK,QAAQ,uBAAuB,8BAA8B;AAAA,EAC/G;AAAA;AAAA,EAGA,MAAc,oBAAoB,UAAmD;AACnF,UAAM,UAAU,KAAK,QAAQ,0BAA0B;AACvD,QAAI,UAAU,EAAG,OAAM,IAAI,QAAc,CAAC,YAAY,WAAW,SAAS,OAAO,EAAE,QAAQ,CAAC;AAC5F,cAAU,QAAQ;AAAA,EACpB;AAAA;AAAA,EAGQ,WAAW,OAAgB,UAAmC,SAAiC;AACrG,QAAI;AACF,WAAK,OAAO,OAAO,UAAU,OAAO;AAAA,IACtC,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,OAAO,OAAgB,UAAmC,SAAiC;AACjG,SAAK,QAAQ,UAAU,OAAO,UAAU,OAAO;AAAA,EACjD;AACF;","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,25 @@ 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.
31
+ *
32
+ * **Version gate (§7.7).** Before decoding the body the consumer reads the `bq_schema_version`
33
+ * application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII
34
+ * whitespace) follows the normal decode path; any other value is compared, untrimmed, to the supported
35
+ * `"1"`, and a message that does not match is rejected undecoded — reported to `onError`, the raw body
36
+ * forwarded to the DLQ sender (when configured) and the message accepted **only after the broker has
37
+ * accepted the DLQ delivery**. If the DLQ send fails, is refused (rejected/released/modified), the link
38
+ * closes or `deadLetterTimeoutMs` elapses, the failure is reported and, after `deadLetterRetryDelayMs`,
39
+ * the source message gets a plain `released` outcome (no `delivery_failed`: the broker's delivery
40
+ * counter and `max-delivery-attempts` stay untouched) — never accepted, never handed to a handler.
22
41
  */
23
42
 
24
43
  /** An AMQP message (structural subset of a rhea message). */
@@ -47,9 +66,33 @@ interface AmqpEventContext {
47
66
  message?: AmqpMessage;
48
67
  delivery?: AmqpDelivery;
49
68
  }
69
+ /**
70
+ * The context rhea passes to a sender link event (`accepted`, `rejected`, `sender_close`, ...). Only
71
+ * `delivery` is inspected (to match a verdict to its delivery); everything else is opaque. Deliberately
72
+ * no index signature: an interface-typed handler parameter (e.g. rhea's `EventContext`) must stay assignable.
73
+ */
74
+ interface AmqpLinkEventContext {
75
+ delivery?: unknown;
76
+ }
50
77
  /** The subset of a rhea sender this adapter calls. */
51
78
  interface AmqpSender {
79
+ /** Queue a delivery. rhea returns the delivery synchronously; the broker's verdict arrives later as an event. */
52
80
  send(message: AmqpMessage): unknown;
81
+ /**
82
+ * Optional (a rhea sender has it). When both `on` and `removeListener` exist, the consumer's
83
+ * version-gate path waits for the broker's `accepted` disposition on the DLQ delivery before it
84
+ * settles the source message; without them only a synchronous `send` failure is observable.
85
+ */
86
+ on?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;
87
+ removeListener?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;
88
+ /**
89
+ * Optional (a rhea link has it). rhea does not bubble an event to the session/connection while the
90
+ * link has a listener for it, so the temporary confirmation listeners re-dispatch what is not theirs
91
+ * (other deliveries' dispositions, link close/error) here.
92
+ */
93
+ session?: {
94
+ dispatch?(event: string, context?: AmqpLinkEventContext): unknown;
95
+ };
53
96
  }
54
97
  /** The subset of a rhea receiver this adapter wires to (its `message` event). */
55
98
  interface AmqpReceiver {
@@ -65,14 +108,28 @@ declare const SCHEDULED_DELIVERY_KEY = "x-opt-delivery-time";
65
108
  * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`
66
109
  * sets the `x-opt-delivery-time` annotation for native scheduled delivery.
67
110
  */
68
- declare function artemisMessage(envelope: Envelope, delayMs?: number): AmqpMessage;
111
+ declare function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage;
69
112
  /** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */
70
113
  declare function messageBody(message: AmqpMessage): string;
114
+ /**
115
+ * Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying
116
+ * values defensively. Returns an empty object when there are none. Both the contract `bq_`
117
+ * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract
118
+ * reads only the keys it knows.
119
+ */
120
+ declare function headersOf(message: AmqpMessage): HeaderCarrier;
71
121
  /** Options for {@link ArtemisPublisher.publish}. */
72
122
  interface PublishOptions {
73
123
  traceId?: string;
74
124
  /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */
75
125
  delayMs?: number;
126
+ /**
127
+ * Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`
128
+ * properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.
129
+ * The contract properties win a key collision; an empty/omitted carrier leaves the message
130
+ * unchanged.
131
+ */
132
+ headers?: HeaderCarrier;
76
133
  }
77
134
  /** Sends canonical-envelope messages to one Artemis address with the §7 projection. */
78
135
  declare class ArtemisPublisher {
@@ -84,14 +141,35 @@ declare class ArtemisPublisher {
84
141
  /** Build + send the canonical envelope; returns the message id (`meta.id`). */
85
142
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
86
143
  }
87
- /** A URN handler. Receives the validated envelope and the raw AMQP message. */
88
- type BabelHandler = (envelope: Envelope, message: AmqpMessage) => unknown | Promise<unknown>;
144
+ /**
145
+ * A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band
146
+ * {@link HeaderCarrier} read from the message's `application_properties` (empty when there are
147
+ * none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a
148
+ * child of the producer span (ADR-0028).
149
+ */
150
+ type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
89
151
  /** A map of URN → handler. */
90
152
  type BabelHandlers = Record<string, BabelHandler>;
91
153
  /** Options for {@link ArtemisConsumer}. */
92
154
  interface ArtemisConsumerOptions {
93
155
  /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */
94
156
  deadLetterSender?: AmqpSender;
157
+ /**
158
+ * How long (ms) every dead-letter path (version gate, poison, unknown-URN `dead_letter`, terminal
159
+ * handler failure) waits for the broker to accept the DLQ delivery before it releases the source
160
+ * message instead of accepting it (default 30000).
161
+ */
162
+ deadLetterTimeoutMs?: number;
163
+ /**
164
+ * Pause (ms) before a dead-letter path releases a message whose DLQ write failed (default 1000).
165
+ * The release is a plain `released` outcome (no `delivery_failed`, so the broker's delivery counter and
166
+ * `max-delivery-attempts` are untouched); the pause keeps a persistently failing DLQ from becoming a
167
+ * hot redelivery loop. The delivery stays unsettled (holding one credit) while it waits.
168
+ * Note: on the terminal handler-failure path the message has already been through the handler, and
169
+ * a plain release does not bump the delivery counter, so the redelivery runs the handler again (and
170
+ * reports its failure again) until the DLQ write succeeds.
171
+ */
172
+ deadLetterRetryDelayMs?: number;
95
173
  /** Attempts before terminal dead-lettering (default 3). */
96
174
  maxTries?: number;
97
175
  /** Strategy for a URN with no handler (default `fail`). */
@@ -121,9 +199,22 @@ declare class ArtemisConsumer {
121
199
  private reconcile;
122
200
  private onUnknownUrn;
123
201
  private retryOrDeadLetter;
202
+ /**
203
+ * Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry
204
+ * delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender
205
+ * the write is a no-op and the message is accepted (terminal drop), as before.
206
+ */
207
+ private settleAfterDeadLetter;
124
208
  private deadLetter;
209
+ /** Poison path: forward the raw body, confirmed like the gate path. */
125
210
  private deadLetterRaw;
211
+ /** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */
212
+ private deadLetterRawConfirmed;
213
+ /** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
214
+ private releaseAfterBackoff;
215
+ /** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */
216
+ private safeReport;
126
217
  private report;
127
218
  }
128
219
 
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 };
220
+ export { type AmqpDelivery, type AmqpEventContext, type AmqpLinkEventContext, 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,25 @@ 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.
31
+ *
32
+ * **Version gate (§7.7).** Before decoding the body the consumer reads the `bq_schema_version`
33
+ * application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII
34
+ * whitespace) follows the normal decode path; any other value is compared, untrimmed, to the supported
35
+ * `"1"`, and a message that does not match is rejected undecoded — reported to `onError`, the raw body
36
+ * forwarded to the DLQ sender (when configured) and the message accepted **only after the broker has
37
+ * accepted the DLQ delivery**. If the DLQ send fails, is refused (rejected/released/modified), the link
38
+ * closes or `deadLetterTimeoutMs` elapses, the failure is reported and, after `deadLetterRetryDelayMs`,
39
+ * the source message gets a plain `released` outcome (no `delivery_failed`: the broker's delivery
40
+ * counter and `max-delivery-attempts` stay untouched) — never accepted, never handed to a handler.
22
41
  */
23
42
 
24
43
  /** An AMQP message (structural subset of a rhea message). */
@@ -47,9 +66,33 @@ interface AmqpEventContext {
47
66
  message?: AmqpMessage;
48
67
  delivery?: AmqpDelivery;
49
68
  }
69
+ /**
70
+ * The context rhea passes to a sender link event (`accepted`, `rejected`, `sender_close`, ...). Only
71
+ * `delivery` is inspected (to match a verdict to its delivery); everything else is opaque. Deliberately
72
+ * no index signature: an interface-typed handler parameter (e.g. rhea's `EventContext`) must stay assignable.
73
+ */
74
+ interface AmqpLinkEventContext {
75
+ delivery?: unknown;
76
+ }
50
77
  /** The subset of a rhea sender this adapter calls. */
51
78
  interface AmqpSender {
79
+ /** Queue a delivery. rhea returns the delivery synchronously; the broker's verdict arrives later as an event. */
52
80
  send(message: AmqpMessage): unknown;
81
+ /**
82
+ * Optional (a rhea sender has it). When both `on` and `removeListener` exist, the consumer's
83
+ * version-gate path waits for the broker's `accepted` disposition on the DLQ delivery before it
84
+ * settles the source message; without them only a synchronous `send` failure is observable.
85
+ */
86
+ on?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;
87
+ removeListener?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;
88
+ /**
89
+ * Optional (a rhea link has it). rhea does not bubble an event to the session/connection while the
90
+ * link has a listener for it, so the temporary confirmation listeners re-dispatch what is not theirs
91
+ * (other deliveries' dispositions, link close/error) here.
92
+ */
93
+ session?: {
94
+ dispatch?(event: string, context?: AmqpLinkEventContext): unknown;
95
+ };
53
96
  }
54
97
  /** The subset of a rhea receiver this adapter wires to (its `message` event). */
55
98
  interface AmqpReceiver {
@@ -65,14 +108,28 @@ declare const SCHEDULED_DELIVERY_KEY = "x-opt-delivery-time";
65
108
  * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`
66
109
  * sets the `x-opt-delivery-time` annotation for native scheduled delivery.
67
110
  */
68
- declare function artemisMessage(envelope: Envelope, delayMs?: number): AmqpMessage;
111
+ declare function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage;
69
112
  /** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */
70
113
  declare function messageBody(message: AmqpMessage): string;
114
+ /**
115
+ * Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying
116
+ * values defensively. Returns an empty object when there are none. Both the contract `bq_`
117
+ * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract
118
+ * reads only the keys it knows.
119
+ */
120
+ declare function headersOf(message: AmqpMessage): HeaderCarrier;
71
121
  /** Options for {@link ArtemisPublisher.publish}. */
72
122
  interface PublishOptions {
73
123
  traceId?: string;
74
124
  /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */
75
125
  delayMs?: number;
126
+ /**
127
+ * Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`
128
+ * properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.
129
+ * The contract properties win a key collision; an empty/omitted carrier leaves the message
130
+ * unchanged.
131
+ */
132
+ headers?: HeaderCarrier;
76
133
  }
77
134
  /** Sends canonical-envelope messages to one Artemis address with the §7 projection. */
78
135
  declare class ArtemisPublisher {
@@ -84,14 +141,35 @@ declare class ArtemisPublisher {
84
141
  /** Build + send the canonical envelope; returns the message id (`meta.id`). */
85
142
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
86
143
  }
87
- /** A URN handler. Receives the validated envelope and the raw AMQP message. */
88
- type BabelHandler = (envelope: Envelope, message: AmqpMessage) => unknown | Promise<unknown>;
144
+ /**
145
+ * A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band
146
+ * {@link HeaderCarrier} read from the message's `application_properties` (empty when there are
147
+ * none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a
148
+ * child of the producer span (ADR-0028).
149
+ */
150
+ type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
89
151
  /** A map of URN → handler. */
90
152
  type BabelHandlers = Record<string, BabelHandler>;
91
153
  /** Options for {@link ArtemisConsumer}. */
92
154
  interface ArtemisConsumerOptions {
93
155
  /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */
94
156
  deadLetterSender?: AmqpSender;
157
+ /**
158
+ * How long (ms) every dead-letter path (version gate, poison, unknown-URN `dead_letter`, terminal
159
+ * handler failure) waits for the broker to accept the DLQ delivery before it releases the source
160
+ * message instead of accepting it (default 30000).
161
+ */
162
+ deadLetterTimeoutMs?: number;
163
+ /**
164
+ * Pause (ms) before a dead-letter path releases a message whose DLQ write failed (default 1000).
165
+ * The release is a plain `released` outcome (no `delivery_failed`, so the broker's delivery counter and
166
+ * `max-delivery-attempts` are untouched); the pause keeps a persistently failing DLQ from becoming a
167
+ * hot redelivery loop. The delivery stays unsettled (holding one credit) while it waits.
168
+ * Note: on the terminal handler-failure path the message has already been through the handler, and
169
+ * a plain release does not bump the delivery counter, so the redelivery runs the handler again (and
170
+ * reports its failure again) until the DLQ write succeeds.
171
+ */
172
+ deadLetterRetryDelayMs?: number;
95
173
  /** Attempts before terminal dead-lettering (default 3). */
96
174
  maxTries?: number;
97
175
  /** Strategy for a URN with no handler (default `fail`). */
@@ -121,9 +199,22 @@ declare class ArtemisConsumer {
121
199
  private reconcile;
122
200
  private onUnknownUrn;
123
201
  private retryOrDeadLetter;
202
+ /**
203
+ * Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry
204
+ * delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender
205
+ * the write is a no-op and the message is accepted (terminal drop), as before.
206
+ */
207
+ private settleAfterDeadLetter;
124
208
  private deadLetter;
209
+ /** Poison path: forward the raw body, confirmed like the gate path. */
125
210
  private deadLetterRaw;
211
+ /** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */
212
+ private deadLetterRawConfirmed;
213
+ /** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
214
+ private releaseAfterBackoff;
215
+ /** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */
216
+ private safeReport;
126
217
  private report;
127
218
  }
128
219
 
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 };
220
+ export { type AmqpDelivery, type AmqpEventContext, type AmqpLinkEventContext, 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
- import { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from "@babelqueue/core";
2
+ import { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, 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),
@@ -41,10 +55,30 @@ function jmsType(message) {
41
55
  const value = message.message_annotations?.[JMS_TYPE_KEY];
42
56
  return value == null ? void 0 : String(value);
43
57
  }
58
+ var BLANK_ASCII = /^[ \t\n\v\f\r]*$/;
59
+ function unsupportedSchemaVersion(message) {
60
+ const raw = message.application_properties?.["bq_schema_version"];
61
+ if (raw == null) return void 0;
62
+ const value = Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw);
63
+ if (BLANK_ASCII.test(value)) return void 0;
64
+ return value === String(SCHEMA_VERSION) ? void 0 : value;
65
+ }
44
66
  function deliveryCount(message) {
45
67
  const value = message.delivery_count;
46
68
  return typeof value === "number" && value > 0 ? value : 0;
47
69
  }
70
+ function headersOf(message) {
71
+ const props = message.application_properties;
72
+ const out = {};
73
+ if (!props) return out;
74
+ for (const key of Object.keys(props)) {
75
+ const value = props[key];
76
+ if (value == null) continue;
77
+ const s = String(value);
78
+ if (s !== "") out[key] = s;
79
+ }
80
+ return out;
81
+ }
48
82
  var ArtemisPublisher = class _ArtemisPublisher {
49
83
  constructor(sender, queue) {
50
84
  this.sender = sender;
@@ -59,10 +93,107 @@ var ArtemisPublisher = class _ArtemisPublisher {
59
93
  /** Build + send the canonical envelope; returns the message id (`meta.id`). */
60
94
  async publish(urn, data, options = {}) {
61
95
  const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });
62
- await this.sender.send(artemisMessage(envelope, options.delayMs));
96
+ await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));
63
97
  return envelope.meta.id;
64
98
  }
65
99
  };
100
+ var DEFAULT_DLQ_CONFIRM_TIMEOUT_MS = 3e4;
101
+ var DEFAULT_DLQ_RETRY_DELAY_MS = 1e3;
102
+ var CONFIRMATION_VERDICTS = ["accepted", "rejected", "released", "modified"];
103
+ var CONFIRMATION_CLOSES = ["sender_close", "sender_error"];
104
+ var ConfirmationHub = class {
105
+ constructor(sender, on, off) {
106
+ this.sender = sender;
107
+ this.on = on;
108
+ this.off = off;
109
+ }
110
+ sender;
111
+ on;
112
+ off;
113
+ pending = /* @__PURE__ */ new Set();
114
+ handlers = [];
115
+ add(confirmation) {
116
+ this.pending.add(confirmation);
117
+ if (this.handlers.length === 0) {
118
+ for (const name of [...CONFIRMATION_VERDICTS, ...CONFIRMATION_CLOSES]) {
119
+ const handler = (context) => this.dispatch(name, context);
120
+ this.handlers.push([name, handler]);
121
+ this.on(name, handler);
122
+ }
123
+ }
124
+ }
125
+ remove(confirmation) {
126
+ this.pending.delete(confirmation);
127
+ if (this.pending.size === 0) {
128
+ for (const [name, handler] of this.handlers) this.off(name, handler);
129
+ this.handlers.length = 0;
130
+ hubs.delete(this.sender);
131
+ }
132
+ }
133
+ dispatch(name, context) {
134
+ if (CONFIRMATION_CLOSES.includes(name)) {
135
+ for (const confirmation of [...this.pending]) {
136
+ confirmation.settle(new BabelQueueError(`The dead-letter link closed before the broker accepted the delivery (${name}).`));
137
+ }
138
+ this.forward(name, context);
139
+ return;
140
+ }
141
+ const owners = [...this.pending].filter((confirmation) => confirmation.matches(context));
142
+ if (owners.length === 0) {
143
+ this.forward(name, context);
144
+ return;
145
+ }
146
+ for (const owner of owners) {
147
+ owner.settle(name === "accepted" ? void 0 : new BabelQueueError(`The broker did not accept the dead-letter delivery (${name}).`));
148
+ }
149
+ }
150
+ forward(name, context) {
151
+ try {
152
+ this.sender.session?.dispatch?.(name, context);
153
+ } catch {
154
+ }
155
+ }
156
+ };
157
+ var hubs = /* @__PURE__ */ new WeakMap();
158
+ function sendConfirmed(sender, message, timeoutMs) {
159
+ const on = sender.on?.bind(sender);
160
+ const off = sender.removeListener?.bind(sender);
161
+ if (!on || !off) return Promise.resolve(sender.send(message)).then(() => void 0);
162
+ return new Promise((resolve, reject) => {
163
+ let hub = hubs.get(sender);
164
+ if (!hub) {
165
+ hub = new ConfirmationHub(sender, on, off);
166
+ hubs.set(sender, hub);
167
+ }
168
+ const activeHub = hub;
169
+ let finished = false;
170
+ const confirmation = {
171
+ delivery: void 0,
172
+ matches: (context) => confirmation.delivery === void 0 || context?.delivery === void 0 || context.delivery === confirmation.delivery,
173
+ settle: (error) => {
174
+ if (finished) return;
175
+ finished = true;
176
+ clearTimeout(timer);
177
+ activeHub.remove(confirmation);
178
+ if (error) reject(error);
179
+ else resolve();
180
+ }
181
+ };
182
+ activeHub.add(confirmation);
183
+ const timer = setTimeout(
184
+ () => confirmation.settle(new BabelQueueError(`Timed out after ${timeoutMs}ms waiting for the broker to accept the dead-letter delivery.`)),
185
+ timeoutMs
186
+ );
187
+ try {
188
+ confirmation.delivery = sender.send(message);
189
+ } catch (error) {
190
+ confirmation.settle(error instanceof Error ? error : new BabelQueueError(String(error)));
191
+ return;
192
+ }
193
+ const delivery = confirmation.delivery;
194
+ if (delivery === null || typeof delivery !== "object" || delivery.settled === true) confirmation.settle();
195
+ });
196
+ }
66
197
  var ArtemisConsumer = class {
67
198
  constructor(handlers, options = {}) {
68
199
  this.handlers = handlers;
@@ -82,11 +213,27 @@ var ArtemisConsumer = class {
82
213
  async handle(context) {
83
214
  const message = context.message ?? {};
84
215
  const delivery = context.delivery;
216
+ const unsupported = unsupportedSchemaVersion(message);
217
+ if (unsupported !== void 0) {
218
+ const gateError = new BabelQueueError(
219
+ `Rejected a message from Artemis with unsupported bq_schema_version "${unsupported}" (supported: ${SCHEMA_VERSION}).`
220
+ );
221
+ try {
222
+ await this.deadLetterRawConfirmed(messageBody(message));
223
+ } catch (dlqError) {
224
+ this.safeReport(gateError, null, context);
225
+ this.safeReport(dlqError, null, context);
226
+ await this.releaseAfterBackoff(delivery);
227
+ return;
228
+ }
229
+ delivery?.accept();
230
+ this.safeReport(gateError, null, context);
231
+ return;
232
+ }
85
233
  const decoded = EnvelopeCodec.decode(messageBody(message));
86
234
  if (!EnvelopeCodec.accepts(decoded)) {
87
235
  this.report(new BabelQueueError("Rejected a non-conformant BabelQueue envelope from Artemis."), decoded, context);
88
- await this.deadLetterRaw(messageBody(message));
89
- delivery?.accept();
236
+ await this.settleAfterDeadLetter(context, decoded, () => this.deadLetterRaw(messageBody(message)));
90
237
  return;
91
238
  }
92
239
  const envelope = this.reconcile(decoded, message);
@@ -97,7 +244,7 @@ var ArtemisConsumer = class {
97
244
  return;
98
245
  }
99
246
  try {
100
- await handler(envelope, message);
247
+ await handler(envelope, message, headersOf(message));
101
248
  delivery?.accept();
102
249
  } catch (error) {
103
250
  this.report(error, envelope, context);
@@ -117,8 +264,7 @@ var ArtemisConsumer = class {
117
264
  delivery?.accept();
118
265
  return;
119
266
  case UnknownUrnStrategy.DEAD_LETTER:
120
- await this.deadLetter(envelope, "unknown_urn");
121
- delivery?.accept();
267
+ await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "unknown_urn"));
122
268
  return;
123
269
  case UnknownUrnStrategy.RELEASE:
124
270
  delivery?.release({ delivery_failed: true });
@@ -135,12 +281,26 @@ var ArtemisConsumer = class {
135
281
  if ((envelope.attempts ?? 0) + 1 < this.maxTries) {
136
282
  delivery?.release({ delivery_failed: true });
137
283
  } else if (this.options.deadLetterSender) {
138
- await this.deadLetter(envelope, "failed", error);
139
- delivery?.accept();
284
+ await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "failed", error));
140
285
  } else {
141
286
  delivery?.accept();
142
287
  }
143
288
  }
289
+ /**
290
+ * Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry
291
+ * delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender
292
+ * the write is a no-op and the message is accepted (terminal drop), as before.
293
+ */
294
+ async settleAfterDeadLetter(context, envelope, write) {
295
+ try {
296
+ await write();
297
+ } catch (dlqError) {
298
+ this.safeReport(dlqError, envelope, context);
299
+ await this.releaseAfterBackoff(context.delivery);
300
+ return;
301
+ }
302
+ context.delivery?.accept();
303
+ }
144
304
  async deadLetter(envelope, reason, error) {
145
305
  const sender = this.options.deadLetterSender;
146
306
  if (!sender) return;
@@ -150,12 +310,30 @@ var ArtemisConsumer = class {
150
310
  error: error instanceof Error ? error.message : null,
151
311
  exception: error instanceof Error ? error.name : null
152
312
  });
153
- await sender.send(artemisMessage(annotated));
313
+ await sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
154
314
  }
155
- async deadLetterRaw(raw) {
315
+ /** Poison path: forward the raw body, confirmed like the gate path. */
316
+ deadLetterRaw(raw) {
317
+ return this.deadLetterRawConfirmed(raw);
318
+ }
319
+ /** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */
320
+ async deadLetterRawConfirmed(raw) {
156
321
  const sender = this.options.deadLetterSender;
157
322
  if (!sender) return;
158
- await sender.send({ body: raw });
323
+ await sendConfirmed(sender, { body: raw }, this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
324
+ }
325
+ /** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
326
+ async releaseAfterBackoff(delivery) {
327
+ const delayMs = this.options.deadLetterRetryDelayMs ?? DEFAULT_DLQ_RETRY_DELAY_MS;
328
+ if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs).unref?.());
329
+ delivery?.release();
330
+ }
331
+ /** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */
332
+ safeReport(error, envelope, context) {
333
+ try {
334
+ this.report(error, envelope, context);
335
+ } catch {
336
+ }
159
337
  }
160
338
  report(error, envelope, context) {
161
339
  this.options.onError?.(error, envelope, context);
@@ -167,6 +345,7 @@ export {
167
345
  JMS_TYPE_KEY,
168
346
  SCHEDULED_DELIVERY_KEY,
169
347
  artemisMessage,
348
+ headersOf,
170
349
  messageBody
171
350
  };
172
351
  //# 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 * **Version gate (§7.7).** Before decoding the body the consumer reads the `bq_schema_version`\n * application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII\n * whitespace) follows the normal decode path; any other value is compared, untrimmed, to the supported\n * `\"1\"`, and a message that does not match is rejected undecoded — reported to `onError`, the raw body\n * forwarded to the DLQ sender (when configured) and the message accepted **only after the broker has\n * accepted the DLQ delivery**. If the DLQ send fails, is refused (rejected/released/modified), the link\n * closes or `deadLetterTimeoutMs` elapses, the failure is reported and, after `deadLetterRetryDelayMs`,\n * the source message gets a plain `released` outcome (no `delivery_failed`: the broker's delivery\n * counter and `max-delivery-attempts` stay untouched) — never accepted, never handed to a handler.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, 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/**\n * The context rhea passes to a sender link event (`accepted`, `rejected`, `sender_close`, ...). Only\n * `delivery` is inspected (to match a verdict to its delivery); everything else is opaque. Deliberately\n * no index signature: an interface-typed handler parameter (e.g. rhea's `EventContext`) must stay assignable.\n */\nexport interface AmqpLinkEventContext {\n delivery?: unknown;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n /** Queue a delivery. rhea returns the delivery synchronously; the broker's verdict arrives later as an event. */\n send(message: AmqpMessage): unknown;\n /**\n * Optional (a rhea sender has it). When both `on` and `removeListener` exist, the consumer's\n * version-gate path waits for the broker's `accepted` disposition on the DLQ delivery before it\n * settles the source message; without them only a synchronous `send` failure is observable.\n */\n on?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;\n removeListener?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;\n /**\n * Optional (a rhea link has it). rhea does not bubble an event to the session/connection while the\n * link has a listener for it, so the temporary confirmation listeners re-dispatch what is not theirs\n * (other deliveries' dispositions, link close/error) here.\n */\n session?: { dispatch?(event: string, context?: AmqpLinkEventContext): 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\n/** Blank = empty or only ASCII whitespace (GR-5: the same set in every SDK, never `String.trim()`). */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The inbound `bq_schema_version` application property when it is present, non-blank and not exactly\n * the supported version (compared untrimmed), else `undefined` (the message may be decoded).\n */\nfunction unsupportedSchemaVersion(message: AmqpMessage): string | undefined {\n const raw = message.application_properties?.[\"bq_schema_version\"];\n if (raw == null) return undefined;\n const value = Buffer.isBuffer(raw) ? raw.toString(\"utf8\") : String(raw);\n if (BLANK_ASCII.test(value)) return undefined;\n return value === String(SCHEMA_VERSION) ? undefined : 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// --- DLQ delivery confirmation ---------------------------------------------------\n\n/** Default wait (ms) for the broker to settle a gate-path DLQ delivery. */\nconst DEFAULT_DLQ_CONFIRM_TIMEOUT_MS = 30_000;\n/** Default pause (ms) before a gate message whose DLQ write failed is released back to the broker. */\nconst DEFAULT_DLQ_RETRY_DELAY_MS = 1_000;\n\n/** One in-flight DLQ confirmation registered on a {@link ConfirmationHub}. */\ninterface PendingConfirmation {\n /** The delivery `send` returned; `undefined` while `send` is still running synchronously. */\n delivery: unknown;\n /** `true` when `context` is (or may be) this confirmation's verdict. */\n matches(context: AmqpLinkEventContext | undefined): boolean;\n /** Resolve (no argument) or reject the confirmation. */\n settle(error?: Error): void;\n}\n\nconst CONFIRMATION_VERDICTS = [\"accepted\", \"rejected\", \"released\", \"modified\"] as const;\nconst CONFIRMATION_CLOSES = [\"sender_close\", \"sender_error\"] as const;\n\n/**\n * One shared set of temporary sender listeners per {@link AmqpSender}, however many DLQ confirmations\n * are in flight. rhea bubbles a link event to the session only while the link has no listener for it,\n * so while confirmations are pending the hub owns the listeners and re-dispatches every event it does\n * not consume to `sender.session` exactly once (per-confirmation listeners would forward each event\n * once per pending confirmation). The listeners are removed as soon as no confirmation is pending.\n */\nclass ConfirmationHub {\n private readonly pending = new Set<PendingConfirmation>();\n private readonly handlers: [string, (context: AmqpLinkEventContext) => void][] = [];\n\n constructor(\n private readonly sender: AmqpSender,\n private readonly on: (event: string, handler: (context: AmqpLinkEventContext) => void) => unknown,\n private readonly off: (event: string, handler: (context: AmqpLinkEventContext) => void) => unknown,\n ) {}\n\n add(confirmation: PendingConfirmation): void {\n this.pending.add(confirmation);\n if (this.handlers.length === 0) {\n for (const name of [...CONFIRMATION_VERDICTS, ...CONFIRMATION_CLOSES]) {\n const handler = (context: AmqpLinkEventContext): void => this.dispatch(name, context);\n this.handlers.push([name, handler]);\n this.on(name, handler);\n }\n }\n }\n\n remove(confirmation: PendingConfirmation): void {\n this.pending.delete(confirmation);\n if (this.pending.size === 0) {\n for (const [name, handler] of this.handlers) this.off(name, handler);\n this.handlers.length = 0;\n hubs.delete(this.sender);\n }\n }\n\n private dispatch(name: string, context: AmqpLinkEventContext): void {\n if ((CONFIRMATION_CLOSES as readonly string[]).includes(name)) {\n for (const confirmation of [...this.pending]) {\n confirmation.settle(new BabelQueueError(`The dead-letter link closed before the broker accepted the delivery (${name}).`));\n }\n this.forward(name, context);\n return;\n }\n const owners = [...this.pending].filter((confirmation) => confirmation.matches(context));\n if (owners.length === 0) {\n this.forward(name, context);\n return;\n }\n for (const owner of owners) {\n owner.settle(name === \"accepted\" ? undefined : new BabelQueueError(`The broker did not accept the dead-letter delivery (${name}).`));\n }\n }\n\n private forward(name: string, context: AmqpLinkEventContext): void {\n try {\n this.sender.session?.dispatch?.(name, context);\n } catch {\n // forwarding is best effort; never let it change a confirmation outcome\n }\n }\n}\n\nconst hubs = new WeakMap<AmqpSender, ConfirmationHub>();\n\n/**\n * Send `message` and resolve only once the broker has `accepted` the delivery. A rhea `send` returns\n * synchronously and reports the verdict (`accepted` / `rejected` / `released` / `modified`) as a\n * sender event, so awaiting `send` alone cannot see a broker refusal. Rejects on any non-accepted\n * disposition, a link close/error, or when `timeoutMs` elapses (an unsettled delivery that is lost on\n * reconnect raises no event). A sender without event support degrades to awaiting `send` itself.\n * Events that are not this delivery's verdict (other deliveries, link close/error) are re-dispatched on\n * the sender's session, once per event, so the application still sees them (rhea suppresses bubbling\n * while the link has a listener) — see {@link ConfirmationHub}.\n */\nfunction sendConfirmed(sender: AmqpSender, message: AmqpMessage, timeoutMs: number): Promise<void> {\n const on = sender.on?.bind(sender);\n const off = sender.removeListener?.bind(sender);\n if (!on || !off) return Promise.resolve(sender.send(message)).then(() => undefined);\n\n return new Promise<void>((resolve, reject) => {\n let hub = hubs.get(sender);\n if (!hub) {\n hub = new ConfirmationHub(sender, on, off);\n hubs.set(sender, hub);\n }\n const activeHub = hub;\n let finished = false;\n\n const confirmation: PendingConfirmation = {\n delivery: undefined,\n matches: (context) =>\n confirmation.delivery === undefined || context?.delivery === undefined || context.delivery === confirmation.delivery,\n settle: (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timer);\n activeHub.remove(confirmation);\n if (error) reject(error);\n else resolve();\n },\n };\n activeHub.add(confirmation);\n\n // `settle` (above) reads `timer`; it cannot run before this line (nothing is sent or listened to yet).\n const timer = setTimeout(\n () => confirmation.settle(new BabelQueueError(`Timed out after ${timeoutMs}ms waiting for the broker to accept the dead-letter delivery.`)),\n timeoutMs,\n );\n try {\n confirmation.delivery = sender.send(message);\n } catch (error) {\n confirmation.settle(error instanceof Error ? error : new BabelQueueError(String(error)));\n return;\n }\n // A pre-settled delivery (or a sender that returns none) raises no disposition event.\n const delivery = confirmation.delivery;\n if (delivery === null || typeof delivery !== \"object\" || (delivery as { settled?: unknown }).settled === true) confirmation.settle();\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 /**\n * How long (ms) every dead-letter path (version gate, poison, unknown-URN `dead_letter`, terminal\n * handler failure) waits for the broker to accept the DLQ delivery before it releases the source\n * message instead of accepting it (default 30000).\n */\n deadLetterTimeoutMs?: number;\n /**\n * Pause (ms) before a dead-letter path releases a message whose DLQ write failed (default 1000).\n * The release is a plain `released` outcome (no `delivery_failed`, so the broker's delivery counter and\n * `max-delivery-attempts` are untouched); the pause keeps a persistently failing DLQ from becoming a\n * hot redelivery loop. The delivery stays unsettled (holding one credit) while it waits.\n * Note: on the terminal handler-failure path the message has already been through the handler, and\n * a plain release does not bump the delivery counter, so the redelivery runs the handler again (and\n * reports its failure again) until the DLQ write succeeds.\n */\n deadLetterRetryDelayMs?: number;\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\n // Version gate (§7.7): decided on the application property alone, before the body is decoded.\n const unsupported = unsupportedSchemaVersion(message);\n if (unsupported !== undefined) {\n const gateError = new BabelQueueError(\n `Rejected a message from Artemis with unsupported bq_schema_version \"${unsupported}\" (supported: ${SCHEMA_VERSION}).`,\n );\n // Accept only after the broker has accepted the DLQ copy; otherwise release (plain, after a pause) so\n // the message is never lost and never reaches a handler. Reports come after the settlement, or are\n // isolated, so a throwing onError can never leave the message unsettled.\n try {\n await this.deadLetterRawConfirmed(messageBody(message));\n } catch (dlqError) {\n this.safeReport(gateError, null, context);\n this.safeReport(dlqError, null, context);\n await this.releaseAfterBackoff(delivery);\n return;\n }\n delivery?.accept();\n this.safeReport(gateError, null, context);\n return;\n }\n\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.settleAfterDeadLetter(context, decoded, () => this.deadLetterRaw(messageBody(message)));\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.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, \"unknown_urn\"));\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.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, \"failed\", error));\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n /**\n * Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry\n * delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender\n * the write is a no-op and the message is accepted (terminal drop), as before.\n */\n private async settleAfterDeadLetter(\n context: AmqpEventContext,\n envelope: IncomingEnvelope | null,\n write: () => Promise<void>,\n ): Promise<void> {\n try {\n await write();\n } catch (dlqError) {\n this.safeReport(dlqError, envelope, context);\n await this.releaseAfterBackoff(context.delivery);\n return;\n }\n context.delivery?.accept();\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 sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);\n }\n\n /** Poison path: forward the raw body, confirmed like the gate path. */\n private deadLetterRaw(raw: string): Promise<void> {\n return this.deadLetterRawConfirmed(raw);\n }\n\n /** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */\n private async deadLetterRawConfirmed(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sendConfirmed(sender, { body: raw }, this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);\n }\n\n /** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */\n private async releaseAfterBackoff(delivery: AmqpDelivery | undefined): Promise<void> {\n const delayMs = this.options.deadLetterRetryDelayMs ?? DEFAULT_DLQ_RETRY_DELAY_MS;\n if (delayMs > 0) await new Promise<void>((resolve) => setTimeout(resolve, delayMs).unref?.());\n delivery?.release();\n }\n\n /** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */\n private safeReport(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n try {\n this.report(error, envelope, context);\n } catch {\n // swallowed by design\n }\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.2.1",
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"
@@ -13,7 +23,7 @@
13
23
  "directory": "packages/artemis"
14
24
  },
15
25
  "license": "MIT",
16
- "author": "Muhammet Şafak <info@muhammetsafak.com.tr>",
26
+ "author": "Muhammet Şafak <info@muhammetsafak.com>",
17
27
  "type": "module",
18
28
  "exports": {
19
29
  ".": {
@@ -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"