@babelqueue/artemis 1.1.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
 
package/dist/index.cjs CHANGED
@@ -85,6 +85,14 @@ function jmsType(message) {
85
85
  const value = message.message_annotations?.[JMS_TYPE_KEY];
86
86
  return value == null ? void 0 : String(value);
87
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
+ }
88
96
  function deliveryCount(message) {
89
97
  const value = message.delivery_count;
90
98
  return typeof value === "number" && value > 0 ? value : 0;
@@ -119,6 +127,103 @@ var ArtemisPublisher = class _ArtemisPublisher {
119
127
  return envelope.meta.id;
120
128
  }
121
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
+ }
122
227
  var ArtemisConsumer = class {
123
228
  constructor(handlers, options = {}) {
124
229
  this.handlers = handlers;
@@ -138,11 +243,27 @@ var ArtemisConsumer = class {
138
243
  async handle(context) {
139
244
  const message = context.message ?? {};
140
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
+ }
141
263
  const decoded = import_core.EnvelopeCodec.decode(messageBody(message));
142
264
  if (!import_core.EnvelopeCodec.accepts(decoded)) {
143
265
  this.report(new import_core.BabelQueueError("Rejected a non-conformant BabelQueue envelope from Artemis."), decoded, context);
144
- await this.deadLetterRaw(messageBody(message));
145
- delivery?.accept();
266
+ await this.settleAfterDeadLetter(context, decoded, () => this.deadLetterRaw(messageBody(message)));
146
267
  return;
147
268
  }
148
269
  const envelope = this.reconcile(decoded, message);
@@ -173,8 +294,7 @@ var ArtemisConsumer = class {
173
294
  delivery?.accept();
174
295
  return;
175
296
  case import_core.UnknownUrnStrategy.DEAD_LETTER:
176
- await this.deadLetter(envelope, "unknown_urn");
177
- delivery?.accept();
297
+ await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "unknown_urn"));
178
298
  return;
179
299
  case import_core.UnknownUrnStrategy.RELEASE:
180
300
  delivery?.release({ delivery_failed: true });
@@ -191,12 +311,26 @@ var ArtemisConsumer = class {
191
311
  if ((envelope.attempts ?? 0) + 1 < this.maxTries) {
192
312
  delivery?.release({ delivery_failed: true });
193
313
  } else if (this.options.deadLetterSender) {
194
- await this.deadLetter(envelope, "failed", error);
195
- delivery?.accept();
314
+ await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "failed", error));
196
315
  } else {
197
316
  delivery?.accept();
198
317
  }
199
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
+ }
200
334
  async deadLetter(envelope, reason, error) {
201
335
  const sender = this.options.deadLetterSender;
202
336
  if (!sender) return;
@@ -206,12 +340,30 @@ var ArtemisConsumer = class {
206
340
  error: error instanceof Error ? error.message : null,
207
341
  exception: error instanceof Error ? error.name : null
208
342
  });
209
- await sender.send(artemisMessage(annotated));
343
+ await sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
344
+ }
345
+ /** Poison path: forward the raw body, confirmed like the gate path. */
346
+ deadLetterRaw(raw) {
347
+ return this.deadLetterRawConfirmed(raw);
210
348
  }
211
- async deadLetterRaw(raw) {
349
+ /** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */
350
+ async deadLetterRawConfirmed(raw) {
212
351
  const sender = this.options.deadLetterSender;
213
352
  if (!sender) return;
214
- 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
+ }
215
367
  }
216
368
  report(error, envelope, context) {
217
369
  this.options.onError?.(error, envelope, context);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache ActiveMQ Artemis adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **AMQP 1.0** consumer over Artemis (rhea),\n * so an Artemis-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the Java, .NET, Python and Go SDKs.\n *\n * Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —\n * per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so\n * this adapter maps onto them (the envelope stays `schema_version: 1`): the envelope JSON is the\n * message **body**; the contract fields are mirrored onto the AMQP a JMS peer reads\n * (`correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN) plus the `bq_` application properties; consume settles per message\n * (`accept` after success, `release` for redelivery); **`attempts = max(body, delivery-count)`**\n * (the AMQP counter is 0-based — no −1); terminal failures go to an opt-in `<queue>.dlq` with a\n * `dead_letter` block.\n *\n * This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide\n * the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional AMQP `application_properties` beside the contract `bq_`\n * properties — the contract properties win a key collision (merge-not-clobber), blanks dropped.\n * `traceparent`/`tracestate` are W3C keys with no hyphens, so they are already JMS-legal (§7 uses\n * underscores because JMS property names must be Java identifiers). On consume the inbound\n * `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract\n * sees the `traceparent` and links the consumer span as a child. A header-less publish is\n * byte-identical. GR-1: the wire envelope body is never touched.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { sanitizeHeaders } from \"./headers.js\";\n\n// --- Minimal AMQP 1.0 shapes (a structural subset of rhea) ---------------------\n\n/** An AMQP message (structural subset of a rhea message). */\nexport interface AmqpMessage {\n body?: unknown;\n correlation_id?: unknown;\n creation_time?: number | Date;\n content_type?: string;\n application_properties?: Record<string, unknown>;\n message_annotations?: Record<string, unknown>;\n /** The AMQP header delivery-count (0-based: 0 on first delivery). */\n delivery_count?: number;\n}\n\n/** The disposition handle rhea passes alongside a received message. */\nexport interface AmqpDelivery {\n /** Settle (acknowledge) the message — it is removed from the queue. */\n accept(): void;\n /** Return the message for redelivery — the broker increments `delivery-count`. */\n release(params?: { delivery_failed?: boolean; undeliverable_here?: boolean }): void;\n}\n\n/** The event context rhea emits on a `message` event. */\nexport interface AmqpEventContext {\n message?: AmqpMessage;\n delivery?: AmqpDelivery;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n send(message: AmqpMessage): unknown;\n}\n\n/** The subset of a rhea receiver this adapter wires to (its `message` event). */\nexport interface AmqpReceiver {\n on(event: \"message\", handler: (context: AmqpEventContext) => void): unknown;\n}\n\n// --- Projection (contract §7.2) ------------------------------------------------\n\n/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */\nexport const JMS_TYPE_KEY = \"x-opt-jms-type\";\n/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */\nexport const SCHEDULED_DELIVERY_KEY = \"x-opt-delivery-time\";\n\n/**\n * Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,\n * `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`\n * sets the `x-opt-delivery-time` annotation for native scheduled delivery.\n */\nexport function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage {\n const annotations: Record<string, unknown> = {};\n if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;\n\n // Out-of-band riders (e.g. traceparent) go in first; the contract bq_ properties below win any\n // key collision (merge-not-clobber). The bq_ names use underscores, not hyphens: a JMS property\n // name must be a valid Java identifier, and every Artemis SDK uses the same JMS-legal form for\n // cross-protocol parity. traceparent/tracestate have no hyphens, so they are already JMS-legal.\n const applicationProperties: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;\n applicationProperties[\"bq_schema_version\"] = String(envelope.meta.schema_version);\n applicationProperties[\"bq_attempts\"] = String(envelope.attempts ?? 0);\n applicationProperties[\"bq_app_id\"] = \"babelqueue\";\n if (envelope.meta.lang) applicationProperties[\"bq_source_lang\"] = envelope.meta.lang;\n\n const message: AmqpMessage = {\n body: EnvelopeCodec.encode(envelope),\n content_type: \"application/json\",\n application_properties: applicationProperties,\n message_annotations: annotations,\n };\n if (envelope.trace_id) message.correlation_id = envelope.trace_id;\n if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;\n\n if (delayMs != null && delayMs > 0) {\n applicationProperties[\"bq_delay\"] = String(delayMs);\n annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;\n }\n return message;\n}\n\n/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */\nexport function messageBody(message: AmqpMessage): string {\n const body = message.body;\n if (body == null) return \"\";\n if (typeof body === \"string\") return body;\n if (Buffer.isBuffer(body)) return body.toString(\"utf8\");\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content.toString(\"utf8\");\n if (typeof content === \"string\") return content;\n }\n return String(body);\n}\n\nfunction jmsType(message: AmqpMessage): string | undefined {\n const value = message.message_annotations?.[JMS_TYPE_KEY];\n return value == null ? undefined : String(value);\n}\n\nfunction deliveryCount(message: AmqpMessage): number {\n const value = message.delivery_count;\n return typeof value === \"number\" && value > 0 ? value : 0;\n}\n\n/**\n * Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying\n * values defensively. Returns an empty object when there are none. Both the contract `bq_`\n * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract\n * reads only the keys it knows.\n */\nexport function headersOf(message: AmqpMessage): HeaderCarrier {\n const props = message.application_properties;\n const out: HeaderCarrier = {};\n if (!props) return out;\n for (const key of Object.keys(props)) {\n const value = props[key];\n if (value == null) continue;\n const s = String(value);\n if (s !== \"\") out[key] = s;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link ArtemisPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`\n * properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract properties win a key collision; an empty/omitted carrier leaves the message\n * unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */\nexport class ArtemisPublisher {\n private constructor(\n private readonly sender: AmqpSender,\n private readonly queue: string,\n ) {}\n\n /** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */\n static create(sender: AmqpSender, queue: string): ArtemisPublisher {\n return new ArtemisPublisher(sender, queue);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });\n await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band\n * {@link HeaderCarrier} read from the message's `application_properties` (empty when there are\n * none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a\n * child of the producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link ArtemisConsumer}. */\nexport interface ArtemisConsumerOptions {\n /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */\n deadLetterSender?: AmqpSender;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a non-conformant message, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;\n}\n\n/**\n * Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the\n * handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A\n * throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP\n * `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a\n * `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP\n * counter is 0-based, so no −1.\n */\nexport class ArtemisConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly handlers: BabelHandlers,\n private readonly options: ArtemisConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */\n listen(receiver: AmqpReceiver): void {\n receiver.on(\"message\", (context) => this.handle(context));\n }\n\n /** Route + settle one delivery. Exposed for testing. */\n async handle(context: AmqpEventContext): Promise<void> {\n const message = context.message ?? {};\n const delivery = context.delivery;\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n // A non-conformant / poison message may lack the fields to annotate; forward it raw.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\"), decoded, context);\n await this.deadLetterRaw(messageBody(message));\n delivery?.accept();\n return;\n }\n\n const envelope = this.reconcile(decoded, message);\n const urn = jmsType(message) ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(context, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n delivery?.accept();\n } catch (error) {\n this.report(error, envelope, context);\n await this.retryOrDeadLetter(context, envelope, error);\n }\n }\n\n /** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */\n private reconcile(envelope: IncomingEnvelope, message: AmqpMessage): Envelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n const count = deliveryCount(message);\n return { ...envelope, attempts: Math.max(body, count) } as Envelope;\n }\n\n private async onUnknownUrn(context: AmqpEventContext, envelope: Envelope, urn: string): Promise<void> {\n const delivery = context.delivery;\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n delivery?.accept();\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope, \"unknown_urn\");\n delivery?.accept();\n return;\n case UnknownUrnStrategy.RELEASE:\n delivery?.release({ delivery_failed: true });\n return;\n default: {\n // FAIL: surface and do NOT settle — the broker redelivers, then dead-letters.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, context);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(context: AmqpEventContext, envelope: Envelope, error: unknown): Promise<void> {\n const delivery = context.delivery;\n if ((envelope.attempts ?? 0) + 1 < this.maxTries) {\n // Release with delivery-failed leaves it for redelivery — the broker bumps delivery-count.\n delivery?.release({ delivery_failed: true });\n } else if (this.options.deadLetterSender) {\n await this.deadLetter(envelope, \"failed\", error);\n delivery?.accept();\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n private async deadLetter(envelope: Envelope, reason: string, error?: unknown): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n const original = envelope.meta?.queue ?? \"\";\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n await sender.send(artemisMessage(annotated));\n }\n\n private async deadLetterRaw(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sender.send({ body: raw });\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n this.options.onError?.(error, envelope, context);\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
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/dist/index.d.cts CHANGED
@@ -28,6 +28,16 @@ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
28
28
  * `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract
29
29
  * sees the `traceparent` and links the consumer span as a child. A header-less publish is
30
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.
31
41
  */
32
42
 
33
43
  /** An AMQP message (structural subset of a rhea message). */
@@ -56,9 +66,33 @@ interface AmqpEventContext {
56
66
  message?: AmqpMessage;
57
67
  delivery?: AmqpDelivery;
58
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
+ }
59
77
  /** The subset of a rhea sender this adapter calls. */
60
78
  interface AmqpSender {
79
+ /** Queue a delivery. rhea returns the delivery synchronously; the broker's verdict arrives later as an event. */
61
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
+ };
62
96
  }
63
97
  /** The subset of a rhea receiver this adapter wires to (its `message` event). */
64
98
  interface AmqpReceiver {
@@ -120,6 +154,22 @@ type BabelHandlers = Record<string, BabelHandler>;
120
154
  interface ArtemisConsumerOptions {
121
155
  /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */
122
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;
123
173
  /** Attempts before terminal dead-lettering (default 3). */
124
174
  maxTries?: number;
125
175
  /** Strategy for a URN with no handler (default `fail`). */
@@ -149,9 +199,22 @@ declare class ArtemisConsumer {
149
199
  private reconcile;
150
200
  private onUnknownUrn;
151
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;
152
208
  private deadLetter;
209
+ /** Poison path: forward the raw body, confirmed like the gate path. */
153
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;
154
217
  private report;
155
218
  }
156
219
 
157
- export { type AmqpDelivery, type AmqpEventContext, type AmqpMessage, type AmqpReceiver, type AmqpSender, ArtemisConsumer, type ArtemisConsumerOptions, ArtemisPublisher, type BabelHandler, type BabelHandlers, JMS_TYPE_KEY, type PublishOptions, SCHEDULED_DELIVERY_KEY, artemisMessage, headersOf, messageBody };
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
@@ -28,6 +28,16 @@ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
28
28
  * `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract
29
29
  * sees the `traceparent` and links the consumer span as a child. A header-less publish is
30
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.
31
41
  */
32
42
 
33
43
  /** An AMQP message (structural subset of a rhea message). */
@@ -56,9 +66,33 @@ interface AmqpEventContext {
56
66
  message?: AmqpMessage;
57
67
  delivery?: AmqpDelivery;
58
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
+ }
59
77
  /** The subset of a rhea sender this adapter calls. */
60
78
  interface AmqpSender {
79
+ /** Queue a delivery. rhea returns the delivery synchronously; the broker's verdict arrives later as an event. */
61
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
+ };
62
96
  }
63
97
  /** The subset of a rhea receiver this adapter wires to (its `message` event). */
64
98
  interface AmqpReceiver {
@@ -120,6 +154,22 @@ type BabelHandlers = Record<string, BabelHandler>;
120
154
  interface ArtemisConsumerOptions {
121
155
  /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */
122
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;
123
173
  /** Attempts before terminal dead-lettering (default 3). */
124
174
  maxTries?: number;
125
175
  /** Strategy for a URN with no handler (default `fail`). */
@@ -149,9 +199,22 @@ declare class ArtemisConsumer {
149
199
  private reconcile;
150
200
  private onUnknownUrn;
151
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;
152
208
  private deadLetter;
209
+ /** Poison path: forward the raw body, confirmed like the gate path. */
153
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;
154
217
  private report;
155
218
  }
156
219
 
157
- export { type AmqpDelivery, type AmqpEventContext, type AmqpMessage, type AmqpReceiver, type AmqpSender, ArtemisConsumer, type ArtemisConsumerOptions, ArtemisPublisher, type BabelHandler, type BabelHandlers, JMS_TYPE_KEY, type PublishOptions, SCHEDULED_DELIVERY_KEY, artemisMessage, headersOf, messageBody };
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,5 +1,5 @@
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
3
 
4
4
  // src/headers.ts
5
5
  function sanitizeHeaders(headers) {
@@ -55,6 +55,14 @@ function jmsType(message) {
55
55
  const value = message.message_annotations?.[JMS_TYPE_KEY];
56
56
  return value == null ? void 0 : String(value);
57
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
+ }
58
66
  function deliveryCount(message) {
59
67
  const value = message.delivery_count;
60
68
  return typeof value === "number" && value > 0 ? value : 0;
@@ -89,6 +97,103 @@ var ArtemisPublisher = class _ArtemisPublisher {
89
97
  return envelope.meta.id;
90
98
  }
91
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
+ }
92
197
  var ArtemisConsumer = class {
93
198
  constructor(handlers, options = {}) {
94
199
  this.handlers = handlers;
@@ -108,11 +213,27 @@ var ArtemisConsumer = class {
108
213
  async handle(context) {
109
214
  const message = context.message ?? {};
110
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
+ }
111
233
  const decoded = EnvelopeCodec.decode(messageBody(message));
112
234
  if (!EnvelopeCodec.accepts(decoded)) {
113
235
  this.report(new BabelQueueError("Rejected a non-conformant BabelQueue envelope from Artemis."), decoded, context);
114
- await this.deadLetterRaw(messageBody(message));
115
- delivery?.accept();
236
+ await this.settleAfterDeadLetter(context, decoded, () => this.deadLetterRaw(messageBody(message)));
116
237
  return;
117
238
  }
118
239
  const envelope = this.reconcile(decoded, message);
@@ -143,8 +264,7 @@ var ArtemisConsumer = class {
143
264
  delivery?.accept();
144
265
  return;
145
266
  case UnknownUrnStrategy.DEAD_LETTER:
146
- await this.deadLetter(envelope, "unknown_urn");
147
- delivery?.accept();
267
+ await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "unknown_urn"));
148
268
  return;
149
269
  case UnknownUrnStrategy.RELEASE:
150
270
  delivery?.release({ delivery_failed: true });
@@ -161,12 +281,26 @@ var ArtemisConsumer = class {
161
281
  if ((envelope.attempts ?? 0) + 1 < this.maxTries) {
162
282
  delivery?.release({ delivery_failed: true });
163
283
  } else if (this.options.deadLetterSender) {
164
- await this.deadLetter(envelope, "failed", error);
165
- delivery?.accept();
284
+ await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "failed", error));
166
285
  } else {
167
286
  delivery?.accept();
168
287
  }
169
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
+ }
170
304
  async deadLetter(envelope, reason, error) {
171
305
  const sender = this.options.deadLetterSender;
172
306
  if (!sender) return;
@@ -176,12 +310,30 @@ var ArtemisConsumer = class {
176
310
  error: error instanceof Error ? error.message : null,
177
311
  exception: error instanceof Error ? error.name : null
178
312
  });
179
- await sender.send(artemisMessage(annotated));
313
+ await sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
314
+ }
315
+ /** Poison path: forward the raw body, confirmed like the gate path. */
316
+ deadLetterRaw(raw) {
317
+ return this.deadLetterRawConfirmed(raw);
180
318
  }
181
- async deadLetterRaw(raw) {
319
+ /** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */
320
+ async deadLetterRawConfirmed(raw) {
182
321
  const sender = this.options.deadLetterSender;
183
322
  if (!sender) return;
184
- 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
+ }
185
337
  }
186
338
  report(error, envelope, context) {
187
339
  this.options.onError?.(error, envelope, context);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache ActiveMQ Artemis adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **AMQP 1.0** consumer over Artemis (rhea),\n * so an Artemis-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the Java, .NET, Python and Go SDKs.\n *\n * Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —\n * per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so\n * this adapter maps onto them (the envelope stays `schema_version: 1`): the envelope JSON is the\n * message **body**; the contract fields are mirrored onto the AMQP a JMS peer reads\n * (`correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN) plus the `bq_` application properties; consume settles per message\n * (`accept` after success, `release` for redelivery); **`attempts = max(body, delivery-count)`**\n * (the AMQP counter is 0-based — no −1); terminal failures go to an opt-in `<queue>.dlq` with a\n * `dead_letter` block.\n *\n * This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide\n * the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional AMQP `application_properties` beside the contract `bq_`\n * properties — the contract properties win a key collision (merge-not-clobber), blanks dropped.\n * `traceparent`/`tracestate` are W3C keys with no hyphens, so they are already JMS-legal (§7 uses\n * underscores because JMS property names must be Java identifiers). On consume the inbound\n * `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract\n * sees the `traceparent` and links the consumer span as a child. A header-less publish is\n * byte-identical. GR-1: the wire envelope body is never touched.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { sanitizeHeaders } from \"./headers.js\";\n\n// --- Minimal AMQP 1.0 shapes (a structural subset of rhea) ---------------------\n\n/** An AMQP message (structural subset of a rhea message). */\nexport interface AmqpMessage {\n body?: unknown;\n correlation_id?: unknown;\n creation_time?: number | Date;\n content_type?: string;\n application_properties?: Record<string, unknown>;\n message_annotations?: Record<string, unknown>;\n /** The AMQP header delivery-count (0-based: 0 on first delivery). */\n delivery_count?: number;\n}\n\n/** The disposition handle rhea passes alongside a received message. */\nexport interface AmqpDelivery {\n /** Settle (acknowledge) the message — it is removed from the queue. */\n accept(): void;\n /** Return the message for redelivery — the broker increments `delivery-count`. */\n release(params?: { delivery_failed?: boolean; undeliverable_here?: boolean }): void;\n}\n\n/** The event context rhea emits on a `message` event. */\nexport interface AmqpEventContext {\n message?: AmqpMessage;\n delivery?: AmqpDelivery;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n send(message: AmqpMessage): unknown;\n}\n\n/** The subset of a rhea receiver this adapter wires to (its `message` event). */\nexport interface AmqpReceiver {\n on(event: \"message\", handler: (context: AmqpEventContext) => void): unknown;\n}\n\n// --- Projection (contract §7.2) ------------------------------------------------\n\n/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */\nexport const JMS_TYPE_KEY = \"x-opt-jms-type\";\n/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */\nexport const SCHEDULED_DELIVERY_KEY = \"x-opt-delivery-time\";\n\n/**\n * Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,\n * `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`\n * sets the `x-opt-delivery-time` annotation for native scheduled delivery.\n */\nexport function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage {\n const annotations: Record<string, unknown> = {};\n if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;\n\n // Out-of-band riders (e.g. traceparent) go in first; the contract bq_ properties below win any\n // key collision (merge-not-clobber). The bq_ names use underscores, not hyphens: a JMS property\n // name must be a valid Java identifier, and every Artemis SDK uses the same JMS-legal form for\n // cross-protocol parity. traceparent/tracestate have no hyphens, so they are already JMS-legal.\n const applicationProperties: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;\n applicationProperties[\"bq_schema_version\"] = String(envelope.meta.schema_version);\n applicationProperties[\"bq_attempts\"] = String(envelope.attempts ?? 0);\n applicationProperties[\"bq_app_id\"] = \"babelqueue\";\n if (envelope.meta.lang) applicationProperties[\"bq_source_lang\"] = envelope.meta.lang;\n\n const message: AmqpMessage = {\n body: EnvelopeCodec.encode(envelope),\n content_type: \"application/json\",\n application_properties: applicationProperties,\n message_annotations: annotations,\n };\n if (envelope.trace_id) message.correlation_id = envelope.trace_id;\n if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;\n\n if (delayMs != null && delayMs > 0) {\n applicationProperties[\"bq_delay\"] = String(delayMs);\n annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;\n }\n return message;\n}\n\n/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */\nexport function messageBody(message: AmqpMessage): string {\n const body = message.body;\n if (body == null) return \"\";\n if (typeof body === \"string\") return body;\n if (Buffer.isBuffer(body)) return body.toString(\"utf8\");\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content.toString(\"utf8\");\n if (typeof content === \"string\") return content;\n }\n return String(body);\n}\n\nfunction jmsType(message: AmqpMessage): string | undefined {\n const value = message.message_annotations?.[JMS_TYPE_KEY];\n return value == null ? undefined : String(value);\n}\n\nfunction deliveryCount(message: AmqpMessage): number {\n const value = message.delivery_count;\n return typeof value === \"number\" && value > 0 ? value : 0;\n}\n\n/**\n * Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying\n * values defensively. Returns an empty object when there are none. Both the contract `bq_`\n * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract\n * reads only the keys it knows.\n */\nexport function headersOf(message: AmqpMessage): HeaderCarrier {\n const props = message.application_properties;\n const out: HeaderCarrier = {};\n if (!props) return out;\n for (const key of Object.keys(props)) {\n const value = props[key];\n if (value == null) continue;\n const s = String(value);\n if (s !== \"\") out[key] = s;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link ArtemisPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`\n * properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract properties win a key collision; an empty/omitted carrier leaves the message\n * unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */\nexport class ArtemisPublisher {\n private constructor(\n private readonly sender: AmqpSender,\n private readonly queue: string,\n ) {}\n\n /** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */\n static create(sender: AmqpSender, queue: string): ArtemisPublisher {\n return new ArtemisPublisher(sender, queue);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });\n await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band\n * {@link HeaderCarrier} read from the message's `application_properties` (empty when there are\n * none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a\n * child of the producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link ArtemisConsumer}. */\nexport interface ArtemisConsumerOptions {\n /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */\n deadLetterSender?: AmqpSender;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a non-conformant message, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;\n}\n\n/**\n * Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the\n * handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A\n * throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP\n * `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a\n * `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP\n * counter is 0-based, so no −1.\n */\nexport class ArtemisConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly handlers: BabelHandlers,\n private readonly options: ArtemisConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */\n listen(receiver: AmqpReceiver): void {\n receiver.on(\"message\", (context) => this.handle(context));\n }\n\n /** Route + settle one delivery. Exposed for testing. */\n async handle(context: AmqpEventContext): Promise<void> {\n const message = context.message ?? {};\n const delivery = context.delivery;\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n // A non-conformant / poison message may lack the fields to annotate; forward it raw.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\"), decoded, context);\n await this.deadLetterRaw(messageBody(message));\n delivery?.accept();\n return;\n }\n\n const envelope = this.reconcile(decoded, message);\n const urn = jmsType(message) ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(context, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n delivery?.accept();\n } catch (error) {\n this.report(error, envelope, context);\n await this.retryOrDeadLetter(context, envelope, error);\n }\n }\n\n /** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */\n private reconcile(envelope: IncomingEnvelope, message: AmqpMessage): Envelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n const count = deliveryCount(message);\n return { ...envelope, attempts: Math.max(body, count) } as Envelope;\n }\n\n private async onUnknownUrn(context: AmqpEventContext, envelope: Envelope, urn: string): Promise<void> {\n const delivery = context.delivery;\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n delivery?.accept();\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope, \"unknown_urn\");\n delivery?.accept();\n return;\n case UnknownUrnStrategy.RELEASE:\n delivery?.release({ delivery_failed: true });\n return;\n default: {\n // FAIL: surface and do NOT settle — the broker redelivers, then dead-letters.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, context);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(context: AmqpEventContext, envelope: Envelope, error: unknown): Promise<void> {\n const delivery = context.delivery;\n if ((envelope.attempts ?? 0) + 1 < this.maxTries) {\n // Release with delivery-failed leaves it for redelivery — the broker bumps delivery-count.\n delivery?.release({ delivery_failed: true });\n } else if (this.options.deadLetterSender) {\n await this.deadLetter(envelope, \"failed\", error);\n delivery?.accept();\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n private async deadLetter(envelope: Envelope, reason: string, error?: unknown): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n const original = envelope.meta?.queue ?? \"\";\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n await sender.send(artemisMessage(annotated));\n }\n\n private async deadLetterRaw(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sender.send({ body: raw });\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n this.options.onError?.(error, envelope, context);\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@babelqueue/artemis",
3
- "version": "1.1.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
5
  "keywords": [
6
6
  "babelqueue",
@@ -23,7 +23,7 @@
23
23
  "directory": "packages/artemis"
24
24
  },
25
25
  "license": "MIT",
26
- "author": "Muhammet Şafak <info@muhammetsafak.com.tr>",
26
+ "author": "Muhammet Şafak <info@muhammetsafak.com>",
27
27
  "type": "module",
28
28
  "exports": {
29
29
  ".": {