@babelqueue/artemis 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/dist/index.cjs +74 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -14
- package/dist/index.d.ts +56 -14
- package/dist/index.js +83 -20
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -81,9 +81,14 @@ strategy is one of `fail` / `delete` / `release` / `dead_letter`.
|
|
|
81
81
|
**Version gate (§7.7).** Before the body is decoded the consumer reads the `bq_schema_version`
|
|
82
82
|
application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII
|
|
83
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
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
`"1"`. A message that does not match is rejected undecoded and settled on its **first delivery**
|
|
85
|
+
(ADR-0037 decision 1): never decoded, never given to a handler, never retried. Under
|
|
86
|
+
`unknownUrn: "delete"`, or without a `deadLetterSender`, it is `accept`ed and dropped (reported to
|
|
87
|
+
`onError`). Otherwise its raw body, byte for byte and without a `dead_letter` block, is forwarded to the
|
|
88
|
+
`deadLetterSender`, then `accept`ed — but only **after the broker
|
|
89
|
+
has accepted the DLQ delivery** (rhea `accepted` event on the DLQ sender). Only the explicitly
|
|
90
|
+
configured `unknownUrn` strategy is read (`release` and `fail` behave alike here: a version-gated
|
|
91
|
+
delivery is never released). If the DLQ delivery is
|
|
87
92
|
rejected/released/modified, the link closes, `send` throws, or `deadLetterTimeoutMs` (default 30000)
|
|
88
93
|
elapses, the failure is reported to `onError` and the source message gets a plain `release()` instead —
|
|
89
94
|
after `deadLetterRetryDelayMs` (default 1000) so a persistently failing DLQ is not a hot redelivery
|
|
@@ -94,6 +99,17 @@ delivery stays unsettled and holds one receiver credit. The DLQ confirmation lis
|
|
|
94
99
|
sender only for the duration of each gate send and re-dispatches events that are not its own to the
|
|
95
100
|
sender's session.
|
|
96
101
|
|
|
102
|
+
**Poison (ADR-0037 decision 2, ADR-0035).** A body that does not decode into a valid envelope (not JSON,
|
|
103
|
+
not valid UTF-8, a JSON value that is not an envelope, an unsupported `meta.schema_version`, an object with
|
|
104
|
+
no valid identity) is never given to a handler and never retried. With a `deadLetterSender` the **ADR-0035
|
|
105
|
+
record** is written to `<queue>.dlq` — built with the core's `buildPoisonRecord`: an object body is spliced
|
|
106
|
+
byte for byte with a `dead_letter` block (`reason: "poison"`), anything else is wrapped as
|
|
107
|
+
`{"raw": …, "dead_letter": …}` (invalid UTF-8 as base64 with `"raw_encoding": "base64"`) — and the message is
|
|
108
|
+
`accept`ed only after the broker accepted that delivery (a failed write releases it, like the gate path).
|
|
109
|
+
`dead_letter.original_queue` is the `queue` option, else the `address` of the receiver the message arrived on (`context.receiver`), else of the receiver passed to `listen()` (set `queue` when one consumer serves several receivers). A version-gated body is forwarded to the DLQ as received (a Buffer or data-section body is not decoded to text).
|
|
110
|
+
Under `unknownUrn: "delete"`, or without a `deadLetterSender`, the message is accepted and dropped
|
|
111
|
+
(reported to `onError`). The raw forward of earlier releases is gone.
|
|
112
|
+
|
|
97
113
|
> `autoaccept: false` is required — the consumer owns the disposition. `handle(context)` is also
|
|
98
114
|
> exposed directly for testing or a custom event wiring.
|
|
99
115
|
|
package/dist/index.cjs
CHANGED
|
@@ -81,6 +81,18 @@ function messageBody(message) {
|
|
|
81
81
|
}
|
|
82
82
|
return String(body);
|
|
83
83
|
}
|
|
84
|
+
function messageBodyBytes(message) {
|
|
85
|
+
const body = message.body;
|
|
86
|
+
if (body == null) return new Uint8Array(0);
|
|
87
|
+
if (typeof body === "string") return Buffer.from(body, "utf8");
|
|
88
|
+
if (Buffer.isBuffer(body)) return body;
|
|
89
|
+
if (typeof body === "object" && "content" in body) {
|
|
90
|
+
const content = body.content;
|
|
91
|
+
if (Buffer.isBuffer(content)) return content;
|
|
92
|
+
if (typeof content === "string") return Buffer.from(content, "utf8");
|
|
93
|
+
}
|
|
94
|
+
return Buffer.from(String(body), "utf8");
|
|
95
|
+
}
|
|
84
96
|
function jmsType(message) {
|
|
85
97
|
const value = message.message_annotations?.[JMS_TYPE_KEY];
|
|
86
98
|
return value == null ? void 0 : String(value);
|
|
@@ -235,8 +247,11 @@ var ArtemisConsumer = class {
|
|
|
235
247
|
options;
|
|
236
248
|
maxTries;
|
|
237
249
|
unknownUrn;
|
|
250
|
+
listenedQueue;
|
|
238
251
|
/** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */
|
|
239
252
|
listen(receiver) {
|
|
253
|
+
const address = receiver.source?.address;
|
|
254
|
+
if (typeof address === "string" && address !== "") this.listenedQueue = address;
|
|
240
255
|
receiver.on("message", (context) => this.handle(context));
|
|
241
256
|
}
|
|
242
257
|
/** Route + settle one delivery. Exposed for testing. */
|
|
@@ -248,22 +263,14 @@ var ArtemisConsumer = class {
|
|
|
248
263
|
const gateError = new import_core.BabelQueueError(
|
|
249
264
|
`Rejected a message from Artemis with unsupported bq_schema_version "${unsupported}" (supported: ${import_core.SCHEMA_VERSION}).`
|
|
250
265
|
);
|
|
251
|
-
|
|
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);
|
|
266
|
+
await this.settleVersionGated(context, message, gateError);
|
|
261
267
|
return;
|
|
262
268
|
}
|
|
263
269
|
const decoded = import_core.EnvelopeCodec.decode(messageBody(message));
|
|
264
270
|
if (!import_core.EnvelopeCodec.accepts(decoded)) {
|
|
265
|
-
|
|
266
|
-
|
|
271
|
+
const poisonError = new import_core.BabelQueueError("Rejected a non-conformant BabelQueue envelope from Artemis.");
|
|
272
|
+
this.safeReport(poisonError, decoded, context);
|
|
273
|
+
await this.settlePoison(context, message, decoded, poisonError);
|
|
267
274
|
return;
|
|
268
275
|
}
|
|
269
276
|
const envelope = this.reconcile(decoded, message);
|
|
@@ -342,15 +349,63 @@ var ArtemisConsumer = class {
|
|
|
342
349
|
});
|
|
343
350
|
await sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
|
|
344
351
|
}
|
|
345
|
-
/**
|
|
346
|
-
|
|
347
|
-
|
|
352
|
+
/**
|
|
353
|
+
* Version-gated delivery (ADR-0037 decision 1): accepted (dropped) under `unknownUrn: "delete"` or without a DLQ
|
|
354
|
+
* sender; otherwise the raw body is forwarded to the DLQ and the message accepted only after the broker accepted
|
|
355
|
+
* that delivery. A failed write is reported and the message released (plain, after a pause) so it is never lost
|
|
356
|
+
* and never reaches a handler. Reports come after the settlement, or are isolated, so a throwing `onError` can
|
|
357
|
+
* never leave the message unsettled.
|
|
358
|
+
*/
|
|
359
|
+
async settleVersionGated(context, message, gateError) {
|
|
360
|
+
const delivery = context.delivery;
|
|
361
|
+
if (this.unknownUrn === import_core.UnknownUrnStrategy.DELETE || this.options.deadLetterSender === void 0) {
|
|
362
|
+
delivery?.accept();
|
|
363
|
+
this.safeReport(gateError, null, context);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
await this.sendToDeadLetter(this.options.deadLetterSender, { body: message.body ?? "" });
|
|
368
|
+
} catch (dlqError) {
|
|
369
|
+
this.safeReport(gateError, null, context);
|
|
370
|
+
this.safeReport(dlqError, null, context);
|
|
371
|
+
await this.releaseAfterBackoff(delivery);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
delivery?.accept();
|
|
375
|
+
this.safeReport(gateError, null, context);
|
|
348
376
|
}
|
|
349
|
-
/**
|
|
350
|
-
|
|
377
|
+
/**
|
|
378
|
+
* Poison body (ADR-0037 decision 2): accepted (dropped) under `unknownUrn: "delete"` or without a DLQ sender;
|
|
379
|
+
* otherwise the ADR-0035 record is written to the DLQ and the message accepted only after the broker accepted
|
|
380
|
+
* it. A failed write is reported and the message released, exactly like the gate path. `dead_letter.attempts`
|
|
381
|
+
* is informational: the delivered count.
|
|
382
|
+
*/
|
|
383
|
+
async settlePoison(context, message, decoded, error) {
|
|
384
|
+
const delivery = context.delivery;
|
|
351
385
|
const sender = this.options.deadLetterSender;
|
|
352
|
-
if (
|
|
353
|
-
|
|
386
|
+
if (this.unknownUrn === import_core.UnknownUrnStrategy.DELETE || sender === void 0) {
|
|
387
|
+
delivery?.accept();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
const delivered = deliveryCount(message);
|
|
392
|
+
const deadLetter = (0, import_core.annotate)({ attempts: delivered }, "poison", this.options.queue ?? context.receiver?.source?.address ?? this.listenedQueue ?? "", {
|
|
393
|
+
attempts: delivered,
|
|
394
|
+
error: error.message,
|
|
395
|
+
exception: error.name
|
|
396
|
+
}).dead_letter;
|
|
397
|
+
const record = (0, import_core.buildPoisonRecord)(messageBodyBytes(message), JSON.stringify(deadLetter));
|
|
398
|
+
await this.sendToDeadLetter(sender, { body: record });
|
|
399
|
+
} catch (dlqError) {
|
|
400
|
+
this.safeReport(dlqError, decoded, context);
|
|
401
|
+
await this.releaseAfterBackoff(delivery);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
delivery?.accept();
|
|
405
|
+
}
|
|
406
|
+
/** Send to the DLQ and wait for the broker's `accepted`. */
|
|
407
|
+
sendToDeadLetter(sender, message) {
|
|
408
|
+
return sendConfirmed(sender, message, this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
|
|
354
409
|
}
|
|
355
410
|
/** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
|
|
356
411
|
async releaseAfterBackoff(delivery) {
|
package/dist/index.cjs.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 * **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":[]}
|
|
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, ADR-0037 decision 1).** Before decoding the body the consumer reads the\n * `bq_schema_version` application property (underscores, ADR-0017). A missing or blank value (empty or only\n * ASCII 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 and settled on its **first delivery**: never\n * decoded, never given to a handler or the unknown-URN path, never retried. `unknownUrn: \"delete\"` accepts it\n * (dropped); without a DLQ sender it is accepted and dropped (reported to `onError`); otherwise the raw body\n * goes to the DLQ sender and the message is accepted **only after the broker has accepted the DLQ delivery**.\n * If the DLQ send fails, is refused (rejected/released/modified), the link closes or `deadLetterTimeoutMs`\n * elapses, the failure is reported and, after `deadLetterRetryDelayMs`, the source message gets a plain\n * `released` outcome (no `delivery_failed`: the broker's delivery counter and `max-delivery-attempts` stay\n * untouched) — never accepted, never handed to a handler. Only the explicitly configured `unknownUrn`\n * strategy is read; `release` and `fail` behave alike here (a version-gated delivery is never released).\n *\n * **Poison (ADR-0037 decision 2, ADR-0035).** A body that does not decode into a valid envelope is never\n * handed to a handler and never retried. `unknownUrn: \"delete\"` accepts it; with a DLQ sender the ADR-0035\n * record (`reason: \"poison\"`, built with the core's `buildPoisonRecord`: an object body is spliced, anything\n * else wrapped, invalid UTF-8 as base64) is written to `<queue>.dlq` and the message accepted only after the\n * broker accepted it; without a DLQ sender it is accepted and dropped (reported to `onError`).\n */\n\nimport {\n annotate,\n BabelQueueError,\n buildPoisonRecord,\n EnvelopeCodec,\n SCHEMA_VERSION,\n UnknownUrnError,\n UnknownUrnStrategy,\n} 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 /** Optional (rhea sets it): the receiver the message arrived on; its `source.address` names the queue of a poison record. */\n receiver?: { source?: { address?: string } | null };\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 /** Optional (a rhea receiver has it): its `address` names the queue for a poison record's `original_queue`. */\n source?: { address?: string } | null;\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\n/** The message body as the exact bytes received (a string body is its UTF-8 encoding), for the poison record. */\nfunction messageBodyBytes(message: AmqpMessage): Uint8Array {\n const body = message.body;\n if (body == null) return new Uint8Array(0);\n if (typeof body === \"string\") return Buffer.from(body, \"utf8\");\n if (Buffer.isBuffer(body)) return body;\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content;\n if (typeof content === \"string\") return Buffer.from(content, \"utf8\");\n }\n return Buffer.from(String(body), \"utf8\");\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 /**\n * Strategy for a URN with no handler (default `fail`). It is also the only strategy the version gate and the\n * poison route read: `delete` accepts (drops) such a delivery (ADR-0037).\n */\n unknownUrn?: string;\n /**\n * The queue this consumer reads, written to `dead_letter.original_queue` of a poison record (a decodable\n * envelope uses its own `meta.queue`). Defaults to the `address` of the receiver the message arrived on (`context.receiver`),\n * else of the receiver passed to {@link ArtemisConsumer.listen}; set it when one consumer serves several receivers.\n */\n queue?: 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 private listenedQueue: string | undefined;\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 const address = receiver.source?.address;\n if (typeof address === \"string\" && address !== \"\") this.listenedQueue = address;\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 await this.settleVersionGated(context, message, gateError);\n return;\n }\n\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n const poisonError = new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\");\n this.safeReport(poisonError, decoded, context);\n await this.settlePoison(context, message, decoded, poisonError);\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 /**\n * Version-gated delivery (ADR-0037 decision 1): accepted (dropped) under `unknownUrn: \"delete\"` or without a DLQ\n * sender; otherwise the raw body is forwarded to the DLQ and the message accepted only after the broker accepted\n * that delivery. A failed write is reported and the message released (plain, after a pause) so it is never lost\n * and never reaches a handler. Reports come after the settlement, or are isolated, so a throwing `onError` can\n * never leave the message unsettled.\n */\n private async settleVersionGated(context: AmqpEventContext, message: AmqpMessage, gateError: BabelQueueError): Promise<void> {\n const delivery = context.delivery;\n if (this.unknownUrn === UnknownUrnStrategy.DELETE || this.options.deadLetterSender === undefined) {\n delivery?.accept();\n this.safeReport(gateError, null, context);\n return;\n }\n try {\n // The body as received (a data-section object or Buffer is forwarded as is, never decoded to text and back).\n await this.sendToDeadLetter(this.options.deadLetterSender, { body: message.body ?? \"\" });\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 }\n\n /**\n * Poison body (ADR-0037 decision 2): accepted (dropped) under `unknownUrn: \"delete\"` or without a DLQ sender;\n * otherwise the ADR-0035 record is written to the DLQ and the message accepted only after the broker accepted\n * it. A failed write is reported and the message released, exactly like the gate path. `dead_letter.attempts`\n * is informational: the delivered count.\n */\n private async settlePoison(\n context: AmqpEventContext,\n message: AmqpMessage,\n decoded: IncomingEnvelope,\n error: BabelQueueError,\n ): Promise<void> {\n const delivery = context.delivery;\n const sender = this.options.deadLetterSender;\n if (this.unknownUrn === UnknownUrnStrategy.DELETE || sender === undefined) {\n delivery?.accept();\n return;\n }\n try {\n const delivered = deliveryCount(message);\n const deadLetter = annotate({ attempts: delivered } as unknown as Envelope, \"poison\", this.options.queue ?? context.receiver?.source?.address ?? this.listenedQueue ?? \"\", {\n attempts: delivered,\n error: error.message,\n exception: error.name,\n }).dead_letter;\n const record = buildPoisonRecord(messageBodyBytes(message), JSON.stringify(deadLetter));\n await this.sendToDeadLetter(sender, { body: record });\n } catch (dlqError) {\n this.safeReport(dlqError, decoded, context);\n await this.releaseAfterBackoff(delivery);\n return;\n }\n delivery?.accept();\n }\n\n /** Send to the DLQ and wait for the broker's `accepted`. */\n private sendToDeadLetter(sender: AmqpSender, message: AmqpMessage): Promise<void> {\n return sendConfirmed(sender, message, 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
|
@@ -29,15 +29,24 @@ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
|
|
|
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
31
|
*
|
|
32
|
-
* **Version gate (§7.7).** Before decoding the body the consumer reads the
|
|
33
|
-
* application property (underscores, ADR-0017). A missing or blank value (empty or only
|
|
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
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* the
|
|
40
|
-
*
|
|
32
|
+
* **Version gate (§7.7, ADR-0037 decision 1).** Before decoding the body the consumer reads the
|
|
33
|
+
* `bq_schema_version` application property (underscores, ADR-0017). A missing or blank value (empty or only
|
|
34
|
+
* ASCII 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 and settled on its **first delivery**: never
|
|
36
|
+
* decoded, never given to a handler or the unknown-URN path, never retried. `unknownUrn: "delete"` accepts it
|
|
37
|
+
* (dropped); without a DLQ sender it is accepted and dropped (reported to `onError`); otherwise the raw body
|
|
38
|
+
* goes to the DLQ sender and the message is accepted **only after the broker has accepted the DLQ delivery**.
|
|
39
|
+
* If the DLQ send fails, is refused (rejected/released/modified), the link closes or `deadLetterTimeoutMs`
|
|
40
|
+
* elapses, the failure is reported and, after `deadLetterRetryDelayMs`, the source message gets a plain
|
|
41
|
+
* `released` outcome (no `delivery_failed`: the broker's delivery counter and `max-delivery-attempts` stay
|
|
42
|
+
* untouched) — never accepted, never handed to a handler. Only the explicitly configured `unknownUrn`
|
|
43
|
+
* strategy is read; `release` and `fail` behave alike here (a version-gated delivery is never released).
|
|
44
|
+
*
|
|
45
|
+
* **Poison (ADR-0037 decision 2, ADR-0035).** A body that does not decode into a valid envelope is never
|
|
46
|
+
* handed to a handler and never retried. `unknownUrn: "delete"` accepts it; with a DLQ sender the ADR-0035
|
|
47
|
+
* record (`reason: "poison"`, built with the core's `buildPoisonRecord`: an object body is spliced, anything
|
|
48
|
+
* else wrapped, invalid UTF-8 as base64) is written to `<queue>.dlq` and the message accepted only after the
|
|
49
|
+
* broker accepted it; without a DLQ sender it is accepted and dropped (reported to `onError`).
|
|
41
50
|
*/
|
|
42
51
|
|
|
43
52
|
/** An AMQP message (structural subset of a rhea message). */
|
|
@@ -65,6 +74,12 @@ interface AmqpDelivery {
|
|
|
65
74
|
interface AmqpEventContext {
|
|
66
75
|
message?: AmqpMessage;
|
|
67
76
|
delivery?: AmqpDelivery;
|
|
77
|
+
/** Optional (rhea sets it): the receiver the message arrived on; its `source.address` names the queue of a poison record. */
|
|
78
|
+
receiver?: {
|
|
79
|
+
source?: {
|
|
80
|
+
address?: string;
|
|
81
|
+
} | null;
|
|
82
|
+
};
|
|
68
83
|
}
|
|
69
84
|
/**
|
|
70
85
|
* The context rhea passes to a sender link event (`accepted`, `rejected`, `sender_close`, ...). Only
|
|
@@ -97,6 +112,10 @@ interface AmqpSender {
|
|
|
97
112
|
/** The subset of a rhea receiver this adapter wires to (its `message` event). */
|
|
98
113
|
interface AmqpReceiver {
|
|
99
114
|
on(event: "message", handler: (context: AmqpEventContext) => void): unknown;
|
|
115
|
+
/** Optional (a rhea receiver has it): its `address` names the queue for a poison record's `original_queue`. */
|
|
116
|
+
source?: {
|
|
117
|
+
address?: string;
|
|
118
|
+
} | null;
|
|
100
119
|
}
|
|
101
120
|
/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */
|
|
102
121
|
declare const JMS_TYPE_KEY = "x-opt-jms-type";
|
|
@@ -172,8 +191,17 @@ interface ArtemisConsumerOptions {
|
|
|
172
191
|
deadLetterRetryDelayMs?: number;
|
|
173
192
|
/** Attempts before terminal dead-lettering (default 3). */
|
|
174
193
|
maxTries?: number;
|
|
175
|
-
/**
|
|
194
|
+
/**
|
|
195
|
+
* Strategy for a URN with no handler (default `fail`). It is also the only strategy the version gate and the
|
|
196
|
+
* poison route read: `delete` accepts (drops) such a delivery (ADR-0037).
|
|
197
|
+
*/
|
|
176
198
|
unknownUrn?: string;
|
|
199
|
+
/**
|
|
200
|
+
* The queue this consumer reads, written to `dead_letter.original_queue` of a poison record (a decodable
|
|
201
|
+
* envelope uses its own `meta.queue`). Defaults to the `address` of the receiver the message arrived on (`context.receiver`),
|
|
202
|
+
* else of the receiver passed to {@link ArtemisConsumer.listen}; set it when one consumer serves several receivers.
|
|
203
|
+
*/
|
|
204
|
+
queue?: string;
|
|
177
205
|
/** Called for a non-conformant message, an unmapped URN, or a throwing handler. */
|
|
178
206
|
onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;
|
|
179
207
|
}
|
|
@@ -190,6 +218,7 @@ declare class ArtemisConsumer {
|
|
|
190
218
|
private readonly options;
|
|
191
219
|
private readonly maxTries;
|
|
192
220
|
private readonly unknownUrn;
|
|
221
|
+
private listenedQueue;
|
|
193
222
|
constructor(handlers: BabelHandlers, options?: ArtemisConsumerOptions);
|
|
194
223
|
/** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */
|
|
195
224
|
listen(receiver: AmqpReceiver): void;
|
|
@@ -206,10 +235,23 @@ declare class ArtemisConsumer {
|
|
|
206
235
|
*/
|
|
207
236
|
private settleAfterDeadLetter;
|
|
208
237
|
private deadLetter;
|
|
209
|
-
/**
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Version-gated delivery (ADR-0037 decision 1): accepted (dropped) under `unknownUrn: "delete"` or without a DLQ
|
|
240
|
+
* sender; otherwise the raw body is forwarded to the DLQ and the message accepted only after the broker accepted
|
|
241
|
+
* that delivery. A failed write is reported and the message released (plain, after a pause) so it is never lost
|
|
242
|
+
* and never reaches a handler. Reports come after the settlement, or are isolated, so a throwing `onError` can
|
|
243
|
+
* never leave the message unsettled.
|
|
244
|
+
*/
|
|
245
|
+
private settleVersionGated;
|
|
246
|
+
/**
|
|
247
|
+
* Poison body (ADR-0037 decision 2): accepted (dropped) under `unknownUrn: "delete"` or without a DLQ sender;
|
|
248
|
+
* otherwise the ADR-0035 record is written to the DLQ and the message accepted only after the broker accepted
|
|
249
|
+
* it. A failed write is reported and the message released, exactly like the gate path. `dead_letter.attempts`
|
|
250
|
+
* is informational: the delivered count.
|
|
251
|
+
*/
|
|
252
|
+
private settlePoison;
|
|
253
|
+
/** Send to the DLQ and wait for the broker's `accepted`. */
|
|
254
|
+
private sendToDeadLetter;
|
|
213
255
|
/** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
|
|
214
256
|
private releaseAfterBackoff;
|
|
215
257
|
/** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */
|
package/dist/index.d.ts
CHANGED
|
@@ -29,15 +29,24 @@ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
|
|
|
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
31
|
*
|
|
32
|
-
* **Version gate (§7.7).** Before decoding the body the consumer reads the
|
|
33
|
-
* application property (underscores, ADR-0017). A missing or blank value (empty or only
|
|
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
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* the
|
|
40
|
-
*
|
|
32
|
+
* **Version gate (§7.7, ADR-0037 decision 1).** Before decoding the body the consumer reads the
|
|
33
|
+
* `bq_schema_version` application property (underscores, ADR-0017). A missing or blank value (empty or only
|
|
34
|
+
* ASCII 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 and settled on its **first delivery**: never
|
|
36
|
+
* decoded, never given to a handler or the unknown-URN path, never retried. `unknownUrn: "delete"` accepts it
|
|
37
|
+
* (dropped); without a DLQ sender it is accepted and dropped (reported to `onError`); otherwise the raw body
|
|
38
|
+
* goes to the DLQ sender and the message is accepted **only after the broker has accepted the DLQ delivery**.
|
|
39
|
+
* If the DLQ send fails, is refused (rejected/released/modified), the link closes or `deadLetterTimeoutMs`
|
|
40
|
+
* elapses, the failure is reported and, after `deadLetterRetryDelayMs`, the source message gets a plain
|
|
41
|
+
* `released` outcome (no `delivery_failed`: the broker's delivery counter and `max-delivery-attempts` stay
|
|
42
|
+
* untouched) — never accepted, never handed to a handler. Only the explicitly configured `unknownUrn`
|
|
43
|
+
* strategy is read; `release` and `fail` behave alike here (a version-gated delivery is never released).
|
|
44
|
+
*
|
|
45
|
+
* **Poison (ADR-0037 decision 2, ADR-0035).** A body that does not decode into a valid envelope is never
|
|
46
|
+
* handed to a handler and never retried. `unknownUrn: "delete"` accepts it; with a DLQ sender the ADR-0035
|
|
47
|
+
* record (`reason: "poison"`, built with the core's `buildPoisonRecord`: an object body is spliced, anything
|
|
48
|
+
* else wrapped, invalid UTF-8 as base64) is written to `<queue>.dlq` and the message accepted only after the
|
|
49
|
+
* broker accepted it; without a DLQ sender it is accepted and dropped (reported to `onError`).
|
|
41
50
|
*/
|
|
42
51
|
|
|
43
52
|
/** An AMQP message (structural subset of a rhea message). */
|
|
@@ -65,6 +74,12 @@ interface AmqpDelivery {
|
|
|
65
74
|
interface AmqpEventContext {
|
|
66
75
|
message?: AmqpMessage;
|
|
67
76
|
delivery?: AmqpDelivery;
|
|
77
|
+
/** Optional (rhea sets it): the receiver the message arrived on; its `source.address` names the queue of a poison record. */
|
|
78
|
+
receiver?: {
|
|
79
|
+
source?: {
|
|
80
|
+
address?: string;
|
|
81
|
+
} | null;
|
|
82
|
+
};
|
|
68
83
|
}
|
|
69
84
|
/**
|
|
70
85
|
* The context rhea passes to a sender link event (`accepted`, `rejected`, `sender_close`, ...). Only
|
|
@@ -97,6 +112,10 @@ interface AmqpSender {
|
|
|
97
112
|
/** The subset of a rhea receiver this adapter wires to (its `message` event). */
|
|
98
113
|
interface AmqpReceiver {
|
|
99
114
|
on(event: "message", handler: (context: AmqpEventContext) => void): unknown;
|
|
115
|
+
/** Optional (a rhea receiver has it): its `address` names the queue for a poison record's `original_queue`. */
|
|
116
|
+
source?: {
|
|
117
|
+
address?: string;
|
|
118
|
+
} | null;
|
|
100
119
|
}
|
|
101
120
|
/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */
|
|
102
121
|
declare const JMS_TYPE_KEY = "x-opt-jms-type";
|
|
@@ -172,8 +191,17 @@ interface ArtemisConsumerOptions {
|
|
|
172
191
|
deadLetterRetryDelayMs?: number;
|
|
173
192
|
/** Attempts before terminal dead-lettering (default 3). */
|
|
174
193
|
maxTries?: number;
|
|
175
|
-
/**
|
|
194
|
+
/**
|
|
195
|
+
* Strategy for a URN with no handler (default `fail`). It is also the only strategy the version gate and the
|
|
196
|
+
* poison route read: `delete` accepts (drops) such a delivery (ADR-0037).
|
|
197
|
+
*/
|
|
176
198
|
unknownUrn?: string;
|
|
199
|
+
/**
|
|
200
|
+
* The queue this consumer reads, written to `dead_letter.original_queue` of a poison record (a decodable
|
|
201
|
+
* envelope uses its own `meta.queue`). Defaults to the `address` of the receiver the message arrived on (`context.receiver`),
|
|
202
|
+
* else of the receiver passed to {@link ArtemisConsumer.listen}; set it when one consumer serves several receivers.
|
|
203
|
+
*/
|
|
204
|
+
queue?: string;
|
|
177
205
|
/** Called for a non-conformant message, an unmapped URN, or a throwing handler. */
|
|
178
206
|
onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;
|
|
179
207
|
}
|
|
@@ -190,6 +218,7 @@ declare class ArtemisConsumer {
|
|
|
190
218
|
private readonly options;
|
|
191
219
|
private readonly maxTries;
|
|
192
220
|
private readonly unknownUrn;
|
|
221
|
+
private listenedQueue;
|
|
193
222
|
constructor(handlers: BabelHandlers, options?: ArtemisConsumerOptions);
|
|
194
223
|
/** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */
|
|
195
224
|
listen(receiver: AmqpReceiver): void;
|
|
@@ -206,10 +235,23 @@ declare class ArtemisConsumer {
|
|
|
206
235
|
*/
|
|
207
236
|
private settleAfterDeadLetter;
|
|
208
237
|
private deadLetter;
|
|
209
|
-
/**
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Version-gated delivery (ADR-0037 decision 1): accepted (dropped) under `unknownUrn: "delete"` or without a DLQ
|
|
240
|
+
* sender; otherwise the raw body is forwarded to the DLQ and the message accepted only after the broker accepted
|
|
241
|
+
* that delivery. A failed write is reported and the message released (plain, after a pause) so it is never lost
|
|
242
|
+
* and never reaches a handler. Reports come after the settlement, or are isolated, so a throwing `onError` can
|
|
243
|
+
* never leave the message unsettled.
|
|
244
|
+
*/
|
|
245
|
+
private settleVersionGated;
|
|
246
|
+
/**
|
|
247
|
+
* Poison body (ADR-0037 decision 2): accepted (dropped) under `unknownUrn: "delete"` or without a DLQ sender;
|
|
248
|
+
* otherwise the ADR-0035 record is written to the DLQ and the message accepted only after the broker accepted
|
|
249
|
+
* it. A failed write is reported and the message released, exactly like the gate path. `dead_letter.attempts`
|
|
250
|
+
* is informational: the delivered count.
|
|
251
|
+
*/
|
|
252
|
+
private settlePoison;
|
|
253
|
+
/** Send to the DLQ and wait for the broker's `accepted`. */
|
|
254
|
+
private sendToDeadLetter;
|
|
213
255
|
/** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
|
|
214
256
|
private releaseAfterBackoff;
|
|
215
257
|
/** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
annotate,
|
|
4
|
+
BabelQueueError,
|
|
5
|
+
buildPoisonRecord,
|
|
6
|
+
EnvelopeCodec,
|
|
7
|
+
SCHEMA_VERSION,
|
|
8
|
+
UnknownUrnError,
|
|
9
|
+
UnknownUrnStrategy
|
|
10
|
+
} from "@babelqueue/core";
|
|
3
11
|
|
|
4
12
|
// src/headers.ts
|
|
5
13
|
function sanitizeHeaders(headers) {
|
|
@@ -51,6 +59,18 @@ function messageBody(message) {
|
|
|
51
59
|
}
|
|
52
60
|
return String(body);
|
|
53
61
|
}
|
|
62
|
+
function messageBodyBytes(message) {
|
|
63
|
+
const body = message.body;
|
|
64
|
+
if (body == null) return new Uint8Array(0);
|
|
65
|
+
if (typeof body === "string") return Buffer.from(body, "utf8");
|
|
66
|
+
if (Buffer.isBuffer(body)) return body;
|
|
67
|
+
if (typeof body === "object" && "content" in body) {
|
|
68
|
+
const content = body.content;
|
|
69
|
+
if (Buffer.isBuffer(content)) return content;
|
|
70
|
+
if (typeof content === "string") return Buffer.from(content, "utf8");
|
|
71
|
+
}
|
|
72
|
+
return Buffer.from(String(body), "utf8");
|
|
73
|
+
}
|
|
54
74
|
function jmsType(message) {
|
|
55
75
|
const value = message.message_annotations?.[JMS_TYPE_KEY];
|
|
56
76
|
return value == null ? void 0 : String(value);
|
|
@@ -205,8 +225,11 @@ var ArtemisConsumer = class {
|
|
|
205
225
|
options;
|
|
206
226
|
maxTries;
|
|
207
227
|
unknownUrn;
|
|
228
|
+
listenedQueue;
|
|
208
229
|
/** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */
|
|
209
230
|
listen(receiver) {
|
|
231
|
+
const address = receiver.source?.address;
|
|
232
|
+
if (typeof address === "string" && address !== "") this.listenedQueue = address;
|
|
210
233
|
receiver.on("message", (context) => this.handle(context));
|
|
211
234
|
}
|
|
212
235
|
/** Route + settle one delivery. Exposed for testing. */
|
|
@@ -218,22 +241,14 @@ var ArtemisConsumer = class {
|
|
|
218
241
|
const gateError = new BabelQueueError(
|
|
219
242
|
`Rejected a message from Artemis with unsupported bq_schema_version "${unsupported}" (supported: ${SCHEMA_VERSION}).`
|
|
220
243
|
);
|
|
221
|
-
|
|
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);
|
|
244
|
+
await this.settleVersionGated(context, message, gateError);
|
|
231
245
|
return;
|
|
232
246
|
}
|
|
233
247
|
const decoded = EnvelopeCodec.decode(messageBody(message));
|
|
234
248
|
if (!EnvelopeCodec.accepts(decoded)) {
|
|
235
|
-
|
|
236
|
-
|
|
249
|
+
const poisonError = new BabelQueueError("Rejected a non-conformant BabelQueue envelope from Artemis.");
|
|
250
|
+
this.safeReport(poisonError, decoded, context);
|
|
251
|
+
await this.settlePoison(context, message, decoded, poisonError);
|
|
237
252
|
return;
|
|
238
253
|
}
|
|
239
254
|
const envelope = this.reconcile(decoded, message);
|
|
@@ -312,15 +327,63 @@ var ArtemisConsumer = class {
|
|
|
312
327
|
});
|
|
313
328
|
await sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
|
|
314
329
|
}
|
|
315
|
-
/**
|
|
316
|
-
|
|
317
|
-
|
|
330
|
+
/**
|
|
331
|
+
* Version-gated delivery (ADR-0037 decision 1): accepted (dropped) under `unknownUrn: "delete"` or without a DLQ
|
|
332
|
+
* sender; otherwise the raw body is forwarded to the DLQ and the message accepted only after the broker accepted
|
|
333
|
+
* that delivery. A failed write is reported and the message released (plain, after a pause) so it is never lost
|
|
334
|
+
* and never reaches a handler. Reports come after the settlement, or are isolated, so a throwing `onError` can
|
|
335
|
+
* never leave the message unsettled.
|
|
336
|
+
*/
|
|
337
|
+
async settleVersionGated(context, message, gateError) {
|
|
338
|
+
const delivery = context.delivery;
|
|
339
|
+
if (this.unknownUrn === UnknownUrnStrategy.DELETE || this.options.deadLetterSender === void 0) {
|
|
340
|
+
delivery?.accept();
|
|
341
|
+
this.safeReport(gateError, null, context);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
await this.sendToDeadLetter(this.options.deadLetterSender, { body: message.body ?? "" });
|
|
346
|
+
} catch (dlqError) {
|
|
347
|
+
this.safeReport(gateError, null, context);
|
|
348
|
+
this.safeReport(dlqError, null, context);
|
|
349
|
+
await this.releaseAfterBackoff(delivery);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
delivery?.accept();
|
|
353
|
+
this.safeReport(gateError, null, context);
|
|
318
354
|
}
|
|
319
|
-
/**
|
|
320
|
-
|
|
355
|
+
/**
|
|
356
|
+
* Poison body (ADR-0037 decision 2): accepted (dropped) under `unknownUrn: "delete"` or without a DLQ sender;
|
|
357
|
+
* otherwise the ADR-0035 record is written to the DLQ and the message accepted only after the broker accepted
|
|
358
|
+
* it. A failed write is reported and the message released, exactly like the gate path. `dead_letter.attempts`
|
|
359
|
+
* is informational: the delivered count.
|
|
360
|
+
*/
|
|
361
|
+
async settlePoison(context, message, decoded, error) {
|
|
362
|
+
const delivery = context.delivery;
|
|
321
363
|
const sender = this.options.deadLetterSender;
|
|
322
|
-
if (
|
|
323
|
-
|
|
364
|
+
if (this.unknownUrn === UnknownUrnStrategy.DELETE || sender === void 0) {
|
|
365
|
+
delivery?.accept();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
const delivered = deliveryCount(message);
|
|
370
|
+
const deadLetter = annotate({ attempts: delivered }, "poison", this.options.queue ?? context.receiver?.source?.address ?? this.listenedQueue ?? "", {
|
|
371
|
+
attempts: delivered,
|
|
372
|
+
error: error.message,
|
|
373
|
+
exception: error.name
|
|
374
|
+
}).dead_letter;
|
|
375
|
+
const record = buildPoisonRecord(messageBodyBytes(message), JSON.stringify(deadLetter));
|
|
376
|
+
await this.sendToDeadLetter(sender, { body: record });
|
|
377
|
+
} catch (dlqError) {
|
|
378
|
+
this.safeReport(dlqError, decoded, context);
|
|
379
|
+
await this.releaseAfterBackoff(delivery);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
delivery?.accept();
|
|
383
|
+
}
|
|
384
|
+
/** Send to the DLQ and wait for the broker's `accepted`. */
|
|
385
|
+
sendToDeadLetter(sender, message) {
|
|
386
|
+
return sendConfirmed(sender, message, this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
|
|
324
387
|
}
|
|
325
388
|
/** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
|
|
326
389
|
async releaseAfterBackoff(delivery) {
|
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 * **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":[]}
|
|
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, ADR-0037 decision 1).** Before decoding the body the consumer reads the\n * `bq_schema_version` application property (underscores, ADR-0017). A missing or blank value (empty or only\n * ASCII 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 and settled on its **first delivery**: never\n * decoded, never given to a handler or the unknown-URN path, never retried. `unknownUrn: \"delete\"` accepts it\n * (dropped); without a DLQ sender it is accepted and dropped (reported to `onError`); otherwise the raw body\n * goes to the DLQ sender and the message is accepted **only after the broker has accepted the DLQ delivery**.\n * If the DLQ send fails, is refused (rejected/released/modified), the link closes or `deadLetterTimeoutMs`\n * elapses, the failure is reported and, after `deadLetterRetryDelayMs`, the source message gets a plain\n * `released` outcome (no `delivery_failed`: the broker's delivery counter and `max-delivery-attempts` stay\n * untouched) — never accepted, never handed to a handler. Only the explicitly configured `unknownUrn`\n * strategy is read; `release` and `fail` behave alike here (a version-gated delivery is never released).\n *\n * **Poison (ADR-0037 decision 2, ADR-0035).** A body that does not decode into a valid envelope is never\n * handed to a handler and never retried. `unknownUrn: \"delete\"` accepts it; with a DLQ sender the ADR-0035\n * record (`reason: \"poison\"`, built with the core's `buildPoisonRecord`: an object body is spliced, anything\n * else wrapped, invalid UTF-8 as base64) is written to `<queue>.dlq` and the message accepted only after the\n * broker accepted it; without a DLQ sender it is accepted and dropped (reported to `onError`).\n */\n\nimport {\n annotate,\n BabelQueueError,\n buildPoisonRecord,\n EnvelopeCodec,\n SCHEMA_VERSION,\n UnknownUrnError,\n UnknownUrnStrategy,\n} 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 /** Optional (rhea sets it): the receiver the message arrived on; its `source.address` names the queue of a poison record. */\n receiver?: { source?: { address?: string } | null };\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 /** Optional (a rhea receiver has it): its `address` names the queue for a poison record's `original_queue`. */\n source?: { address?: string } | null;\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\n/** The message body as the exact bytes received (a string body is its UTF-8 encoding), for the poison record. */\nfunction messageBodyBytes(message: AmqpMessage): Uint8Array {\n const body = message.body;\n if (body == null) return new Uint8Array(0);\n if (typeof body === \"string\") return Buffer.from(body, \"utf8\");\n if (Buffer.isBuffer(body)) return body;\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content;\n if (typeof content === \"string\") return Buffer.from(content, \"utf8\");\n }\n return Buffer.from(String(body), \"utf8\");\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 /**\n * Strategy for a URN with no handler (default `fail`). It is also the only strategy the version gate and the\n * poison route read: `delete` accepts (drops) such a delivery (ADR-0037).\n */\n unknownUrn?: string;\n /**\n * The queue this consumer reads, written to `dead_letter.original_queue` of a poison record (a decodable\n * envelope uses its own `meta.queue`). Defaults to the `address` of the receiver the message arrived on (`context.receiver`),\n * else of the receiver passed to {@link ArtemisConsumer.listen}; set it when one consumer serves several receivers.\n */\n queue?: 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 private listenedQueue: string | undefined;\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 const address = receiver.source?.address;\n if (typeof address === \"string\" && address !== \"\") this.listenedQueue = address;\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 await this.settleVersionGated(context, message, gateError);\n return;\n }\n\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n const poisonError = new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\");\n this.safeReport(poisonError, decoded, context);\n await this.settlePoison(context, message, decoded, poisonError);\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 /**\n * Version-gated delivery (ADR-0037 decision 1): accepted (dropped) under `unknownUrn: \"delete\"` or without a DLQ\n * sender; otherwise the raw body is forwarded to the DLQ and the message accepted only after the broker accepted\n * that delivery. A failed write is reported and the message released (plain, after a pause) so it is never lost\n * and never reaches a handler. Reports come after the settlement, or are isolated, so a throwing `onError` can\n * never leave the message unsettled.\n */\n private async settleVersionGated(context: AmqpEventContext, message: AmqpMessage, gateError: BabelQueueError): Promise<void> {\n const delivery = context.delivery;\n if (this.unknownUrn === UnknownUrnStrategy.DELETE || this.options.deadLetterSender === undefined) {\n delivery?.accept();\n this.safeReport(gateError, null, context);\n return;\n }\n try {\n // The body as received (a data-section object or Buffer is forwarded as is, never decoded to text and back).\n await this.sendToDeadLetter(this.options.deadLetterSender, { body: message.body ?? \"\" });\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 }\n\n /**\n * Poison body (ADR-0037 decision 2): accepted (dropped) under `unknownUrn: \"delete\"` or without a DLQ sender;\n * otherwise the ADR-0035 record is written to the DLQ and the message accepted only after the broker accepted\n * it. A failed write is reported and the message released, exactly like the gate path. `dead_letter.attempts`\n * is informational: the delivered count.\n */\n private async settlePoison(\n context: AmqpEventContext,\n message: AmqpMessage,\n decoded: IncomingEnvelope,\n error: BabelQueueError,\n ): Promise<void> {\n const delivery = context.delivery;\n const sender = this.options.deadLetterSender;\n if (this.unknownUrn === UnknownUrnStrategy.DELETE || sender === undefined) {\n delivery?.accept();\n return;\n }\n try {\n const delivered = deliveryCount(message);\n const deadLetter = annotate({ attempts: delivered } as unknown as Envelope, \"poison\", this.options.queue ?? context.receiver?.source?.address ?? this.listenedQueue ?? \"\", {\n attempts: delivered,\n error: error.message,\n exception: error.name,\n }).dead_letter;\n const record = buildPoisonRecord(messageBodyBytes(message), JSON.stringify(deadLetter));\n await this.sendToDeadLetter(sender, { body: record });\n } catch (dlqError) {\n this.safeReport(dlqError, decoded, context);\n await this.releaseAfterBackoff(delivery);\n return;\n }\n delivery?.accept();\n }\n\n /** Send to the DLQ and wait for the broker's `accepted`. */\n private sendToDeadLetter(sender: AmqpSender, message: AmqpMessage): Promise<void> {\n return sendConfirmed(sender, message, 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.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Apache ActiveMQ Artemis adapter for BabelQueue — canonical-envelope publisher and a URN-routed AMQP 1.0 consumer (accept/release settlement, native scheduled delivery, cross-language DLQ) over rhea.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"babelqueue",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"test": "node --import tsx --test test/*.test.ts"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@babelqueue/core": "^1.
|
|
56
|
+
"@babelqueue/core": "^1.13.0"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"rhea": "^3.0.0"
|