@babelqueue/artemis 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/index.cjs +161 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -1
- package/dist/index.d.ts +64 -1
- package/dist/index.js +162 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -78,6 +78,22 @@ redelivers and bumps `delivery-count`); once `maxTries` is reached the envelope
|
|
|
78
78
|
(falling back to the body URN), so it never decodes a message it cannot handle. Unknown-URN
|
|
79
79
|
strategy is one of `fail` / `delete` / `release` / `dead_letter`.
|
|
80
80
|
|
|
81
|
+
**Version gate (§7.7).** Before the body is decoded the consumer reads the `bq_schema_version`
|
|
82
|
+
application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII
|
|
83
|
+
whitespace) follows the normal decode path; any other value is compared as-is (never trimmed) to
|
|
84
|
+
`"1"`. A message that does not match is rejected undecoded: reported to `onError`, its raw body
|
|
85
|
+
forwarded to the `deadLetterSender` (when configured), then `accept`ed — but only **after the broker
|
|
86
|
+
has accepted the DLQ delivery** (rhea `accepted` event on the DLQ sender). If the DLQ delivery is
|
|
87
|
+
rejected/released/modified, the link closes, `send` throws, or `deadLetterTimeoutMs` (default 30000)
|
|
88
|
+
elapses, the failure is reported to `onError` and the source message gets a plain `release()` instead —
|
|
89
|
+
after `deadLetterRetryDelayMs` (default 1000) so a persistently failing DLQ is not a hot redelivery
|
|
90
|
+
loop. It is never accepted and never reaches a handler. The release deliberately omits
|
|
91
|
+
`delivery_failed`: that would bump Artemis's delivery counter and, once `max-delivery-attempts` is hit,
|
|
92
|
+
route the message to the broker's DLA (or delete it when no DLA is configured). While waiting, the
|
|
93
|
+
delivery stays unsettled and holds one receiver credit. The DLQ confirmation listens on the DLQ
|
|
94
|
+
sender only for the duration of each gate send and re-dispatches events that are not its own to the
|
|
95
|
+
sender's session.
|
|
96
|
+
|
|
81
97
|
> `autoaccept: false` is required — the consumer owns the disposition. `handle(context)` is also
|
|
82
98
|
> exposed directly for testing or a custom event wiring.
|
|
83
99
|
|
package/dist/index.cjs
CHANGED
|
@@ -85,6 +85,14 @@ function jmsType(message) {
|
|
|
85
85
|
const value = message.message_annotations?.[JMS_TYPE_KEY];
|
|
86
86
|
return value == null ? void 0 : String(value);
|
|
87
87
|
}
|
|
88
|
+
var BLANK_ASCII = /^[ \t\n\v\f\r]*$/;
|
|
89
|
+
function unsupportedSchemaVersion(message) {
|
|
90
|
+
const raw = message.application_properties?.["bq_schema_version"];
|
|
91
|
+
if (raw == null) return void 0;
|
|
92
|
+
const value = Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw);
|
|
93
|
+
if (BLANK_ASCII.test(value)) return void 0;
|
|
94
|
+
return value === String(import_core.SCHEMA_VERSION) ? void 0 : value;
|
|
95
|
+
}
|
|
88
96
|
function deliveryCount(message) {
|
|
89
97
|
const value = message.delivery_count;
|
|
90
98
|
return typeof value === "number" && value > 0 ? value : 0;
|
|
@@ -119,6 +127,103 @@ var ArtemisPublisher = class _ArtemisPublisher {
|
|
|
119
127
|
return envelope.meta.id;
|
|
120
128
|
}
|
|
121
129
|
};
|
|
130
|
+
var DEFAULT_DLQ_CONFIRM_TIMEOUT_MS = 3e4;
|
|
131
|
+
var DEFAULT_DLQ_RETRY_DELAY_MS = 1e3;
|
|
132
|
+
var CONFIRMATION_VERDICTS = ["accepted", "rejected", "released", "modified"];
|
|
133
|
+
var CONFIRMATION_CLOSES = ["sender_close", "sender_error"];
|
|
134
|
+
var ConfirmationHub = class {
|
|
135
|
+
constructor(sender, on, off) {
|
|
136
|
+
this.sender = sender;
|
|
137
|
+
this.on = on;
|
|
138
|
+
this.off = off;
|
|
139
|
+
}
|
|
140
|
+
sender;
|
|
141
|
+
on;
|
|
142
|
+
off;
|
|
143
|
+
pending = /* @__PURE__ */ new Set();
|
|
144
|
+
handlers = [];
|
|
145
|
+
add(confirmation) {
|
|
146
|
+
this.pending.add(confirmation);
|
|
147
|
+
if (this.handlers.length === 0) {
|
|
148
|
+
for (const name of [...CONFIRMATION_VERDICTS, ...CONFIRMATION_CLOSES]) {
|
|
149
|
+
const handler = (context) => this.dispatch(name, context);
|
|
150
|
+
this.handlers.push([name, handler]);
|
|
151
|
+
this.on(name, handler);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
remove(confirmation) {
|
|
156
|
+
this.pending.delete(confirmation);
|
|
157
|
+
if (this.pending.size === 0) {
|
|
158
|
+
for (const [name, handler] of this.handlers) this.off(name, handler);
|
|
159
|
+
this.handlers.length = 0;
|
|
160
|
+
hubs.delete(this.sender);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
dispatch(name, context) {
|
|
164
|
+
if (CONFIRMATION_CLOSES.includes(name)) {
|
|
165
|
+
for (const confirmation of [...this.pending]) {
|
|
166
|
+
confirmation.settle(new import_core.BabelQueueError(`The dead-letter link closed before the broker accepted the delivery (${name}).`));
|
|
167
|
+
}
|
|
168
|
+
this.forward(name, context);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const owners = [...this.pending].filter((confirmation) => confirmation.matches(context));
|
|
172
|
+
if (owners.length === 0) {
|
|
173
|
+
this.forward(name, context);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
for (const owner of owners) {
|
|
177
|
+
owner.settle(name === "accepted" ? void 0 : new import_core.BabelQueueError(`The broker did not accept the dead-letter delivery (${name}).`));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
forward(name, context) {
|
|
181
|
+
try {
|
|
182
|
+
this.sender.session?.dispatch?.(name, context);
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var hubs = /* @__PURE__ */ new WeakMap();
|
|
188
|
+
function sendConfirmed(sender, message, timeoutMs) {
|
|
189
|
+
const on = sender.on?.bind(sender);
|
|
190
|
+
const off = sender.removeListener?.bind(sender);
|
|
191
|
+
if (!on || !off) return Promise.resolve(sender.send(message)).then(() => void 0);
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
let hub = hubs.get(sender);
|
|
194
|
+
if (!hub) {
|
|
195
|
+
hub = new ConfirmationHub(sender, on, off);
|
|
196
|
+
hubs.set(sender, hub);
|
|
197
|
+
}
|
|
198
|
+
const activeHub = hub;
|
|
199
|
+
let finished = false;
|
|
200
|
+
const confirmation = {
|
|
201
|
+
delivery: void 0,
|
|
202
|
+
matches: (context) => confirmation.delivery === void 0 || context?.delivery === void 0 || context.delivery === confirmation.delivery,
|
|
203
|
+
settle: (error) => {
|
|
204
|
+
if (finished) return;
|
|
205
|
+
finished = true;
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
activeHub.remove(confirmation);
|
|
208
|
+
if (error) reject(error);
|
|
209
|
+
else resolve();
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
activeHub.add(confirmation);
|
|
213
|
+
const timer = setTimeout(
|
|
214
|
+
() => confirmation.settle(new import_core.BabelQueueError(`Timed out after ${timeoutMs}ms waiting for the broker to accept the dead-letter delivery.`)),
|
|
215
|
+
timeoutMs
|
|
216
|
+
);
|
|
217
|
+
try {
|
|
218
|
+
confirmation.delivery = sender.send(message);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
confirmation.settle(error instanceof Error ? error : new import_core.BabelQueueError(String(error)));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const delivery = confirmation.delivery;
|
|
224
|
+
if (delivery === null || typeof delivery !== "object" || delivery.settled === true) confirmation.settle();
|
|
225
|
+
});
|
|
226
|
+
}
|
|
122
227
|
var ArtemisConsumer = class {
|
|
123
228
|
constructor(handlers, options = {}) {
|
|
124
229
|
this.handlers = handlers;
|
|
@@ -138,11 +243,27 @@ var ArtemisConsumer = class {
|
|
|
138
243
|
async handle(context) {
|
|
139
244
|
const message = context.message ?? {};
|
|
140
245
|
const delivery = context.delivery;
|
|
246
|
+
const unsupported = unsupportedSchemaVersion(message);
|
|
247
|
+
if (unsupported !== void 0) {
|
|
248
|
+
const gateError = new import_core.BabelQueueError(
|
|
249
|
+
`Rejected a message from Artemis with unsupported bq_schema_version "${unsupported}" (supported: ${import_core.SCHEMA_VERSION}).`
|
|
250
|
+
);
|
|
251
|
+
try {
|
|
252
|
+
await this.deadLetterRawConfirmed(messageBody(message));
|
|
253
|
+
} catch (dlqError) {
|
|
254
|
+
this.safeReport(gateError, null, context);
|
|
255
|
+
this.safeReport(dlqError, null, context);
|
|
256
|
+
await this.releaseAfterBackoff(delivery);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
delivery?.accept();
|
|
260
|
+
this.safeReport(gateError, null, context);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
141
263
|
const decoded = import_core.EnvelopeCodec.decode(messageBody(message));
|
|
142
264
|
if (!import_core.EnvelopeCodec.accepts(decoded)) {
|
|
143
265
|
this.report(new import_core.BabelQueueError("Rejected a non-conformant BabelQueue envelope from Artemis."), decoded, context);
|
|
144
|
-
await this.deadLetterRaw(messageBody(message));
|
|
145
|
-
delivery?.accept();
|
|
266
|
+
await this.settleAfterDeadLetter(context, decoded, () => this.deadLetterRaw(messageBody(message)));
|
|
146
267
|
return;
|
|
147
268
|
}
|
|
148
269
|
const envelope = this.reconcile(decoded, message);
|
|
@@ -173,8 +294,7 @@ var ArtemisConsumer = class {
|
|
|
173
294
|
delivery?.accept();
|
|
174
295
|
return;
|
|
175
296
|
case import_core.UnknownUrnStrategy.DEAD_LETTER:
|
|
176
|
-
await this.deadLetter(envelope, "unknown_urn");
|
|
177
|
-
delivery?.accept();
|
|
297
|
+
await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "unknown_urn"));
|
|
178
298
|
return;
|
|
179
299
|
case import_core.UnknownUrnStrategy.RELEASE:
|
|
180
300
|
delivery?.release({ delivery_failed: true });
|
|
@@ -191,12 +311,26 @@ var ArtemisConsumer = class {
|
|
|
191
311
|
if ((envelope.attempts ?? 0) + 1 < this.maxTries) {
|
|
192
312
|
delivery?.release({ delivery_failed: true });
|
|
193
313
|
} else if (this.options.deadLetterSender) {
|
|
194
|
-
await this.deadLetter(envelope, "failed", error);
|
|
195
|
-
delivery?.accept();
|
|
314
|
+
await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "failed", error));
|
|
196
315
|
} else {
|
|
197
316
|
delivery?.accept();
|
|
198
317
|
}
|
|
199
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry
|
|
321
|
+
* delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender
|
|
322
|
+
* the write is a no-op and the message is accepted (terminal drop), as before.
|
|
323
|
+
*/
|
|
324
|
+
async settleAfterDeadLetter(context, envelope, write) {
|
|
325
|
+
try {
|
|
326
|
+
await write();
|
|
327
|
+
} catch (dlqError) {
|
|
328
|
+
this.safeReport(dlqError, envelope, context);
|
|
329
|
+
await this.releaseAfterBackoff(context.delivery);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
context.delivery?.accept();
|
|
333
|
+
}
|
|
200
334
|
async deadLetter(envelope, reason, error) {
|
|
201
335
|
const sender = this.options.deadLetterSender;
|
|
202
336
|
if (!sender) return;
|
|
@@ -206,12 +340,30 @@ var ArtemisConsumer = class {
|
|
|
206
340
|
error: error instanceof Error ? error.message : null,
|
|
207
341
|
exception: error instanceof Error ? error.name : null
|
|
208
342
|
});
|
|
209
|
-
await sender
|
|
343
|
+
await sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
|
|
344
|
+
}
|
|
345
|
+
/** Poison path: forward the raw body, confirmed like the gate path. */
|
|
346
|
+
deadLetterRaw(raw) {
|
|
347
|
+
return this.deadLetterRawConfirmed(raw);
|
|
210
348
|
}
|
|
211
|
-
|
|
349
|
+
/** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */
|
|
350
|
+
async deadLetterRawConfirmed(raw) {
|
|
212
351
|
const sender = this.options.deadLetterSender;
|
|
213
352
|
if (!sender) return;
|
|
214
|
-
await sender
|
|
353
|
+
await sendConfirmed(sender, { body: raw }, this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
|
|
354
|
+
}
|
|
355
|
+
/** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
|
|
356
|
+
async releaseAfterBackoff(delivery) {
|
|
357
|
+
const delayMs = this.options.deadLetterRetryDelayMs ?? DEFAULT_DLQ_RETRY_DELAY_MS;
|
|
358
|
+
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs).unref?.());
|
|
359
|
+
delivery?.release();
|
|
360
|
+
}
|
|
361
|
+
/** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */
|
|
362
|
+
safeReport(error, envelope, context) {
|
|
363
|
+
try {
|
|
364
|
+
this.report(error, envelope, context);
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
215
367
|
}
|
|
216
368
|
report(error, envelope, context) {
|
|
217
369
|
this.options.onError?.(error, envelope, context);
|
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\nimport { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { sanitizeHeaders } from \"./headers.js\";\n\n// --- Minimal AMQP 1.0 shapes (a structural subset of rhea) ---------------------\n\n/** An AMQP message (structural subset of a rhea message). */\nexport interface AmqpMessage {\n body?: unknown;\n correlation_id?: unknown;\n creation_time?: number | Date;\n content_type?: string;\n application_properties?: Record<string, unknown>;\n message_annotations?: Record<string, unknown>;\n /** The AMQP header delivery-count (0-based: 0 on first delivery). */\n delivery_count?: number;\n}\n\n/** The disposition handle rhea passes alongside a received message. */\nexport interface AmqpDelivery {\n /** Settle (acknowledge) the message — it is removed from the queue. */\n accept(): void;\n /** Return the message for redelivery — the broker increments `delivery-count`. */\n release(params?: { delivery_failed?: boolean; undeliverable_here?: boolean }): void;\n}\n\n/** The event context rhea emits on a `message` event. */\nexport interface AmqpEventContext {\n message?: AmqpMessage;\n delivery?: AmqpDelivery;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n send(message: AmqpMessage): unknown;\n}\n\n/** The subset of a rhea receiver this adapter wires to (its `message` event). */\nexport interface AmqpReceiver {\n on(event: \"message\", handler: (context: AmqpEventContext) => void): unknown;\n}\n\n// --- Projection (contract §7.2) ------------------------------------------------\n\n/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */\nexport const JMS_TYPE_KEY = \"x-opt-jms-type\";\n/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */\nexport const SCHEDULED_DELIVERY_KEY = \"x-opt-delivery-time\";\n\n/**\n * Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,\n * `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`\n * sets the `x-opt-delivery-time` annotation for native scheduled delivery.\n */\nexport function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage {\n const annotations: Record<string, unknown> = {};\n if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;\n\n // Out-of-band riders (e.g. traceparent) go in first; the contract bq_ properties below win any\n // key collision (merge-not-clobber). The bq_ names use underscores, not hyphens: a JMS property\n // name must be a valid Java identifier, and every Artemis SDK uses the same JMS-legal form for\n // cross-protocol parity. traceparent/tracestate have no hyphens, so they are already JMS-legal.\n const applicationProperties: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;\n applicationProperties[\"bq_schema_version\"] = String(envelope.meta.schema_version);\n applicationProperties[\"bq_attempts\"] = String(envelope.attempts ?? 0);\n applicationProperties[\"bq_app_id\"] = \"babelqueue\";\n if (envelope.meta.lang) applicationProperties[\"bq_source_lang\"] = envelope.meta.lang;\n\n const message: AmqpMessage = {\n body: EnvelopeCodec.encode(envelope),\n content_type: \"application/json\",\n application_properties: applicationProperties,\n message_annotations: annotations,\n };\n if (envelope.trace_id) message.correlation_id = envelope.trace_id;\n if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;\n\n if (delayMs != null && delayMs > 0) {\n applicationProperties[\"bq_delay\"] = String(delayMs);\n annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;\n }\n return message;\n}\n\n/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */\nexport function messageBody(message: AmqpMessage): string {\n const body = message.body;\n if (body == null) return \"\";\n if (typeof body === \"string\") return body;\n if (Buffer.isBuffer(body)) return body.toString(\"utf8\");\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content.toString(\"utf8\");\n if (typeof content === \"string\") return content;\n }\n return String(body);\n}\n\nfunction jmsType(message: AmqpMessage): string | undefined {\n const value = message.message_annotations?.[JMS_TYPE_KEY];\n return value == null ? undefined : String(value);\n}\n\nfunction deliveryCount(message: AmqpMessage): number {\n const value = message.delivery_count;\n return typeof value === \"number\" && value > 0 ? value : 0;\n}\n\n/**\n * Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying\n * values defensively. Returns an empty object when there are none. Both the contract `bq_`\n * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract\n * reads only the keys it knows.\n */\nexport function headersOf(message: AmqpMessage): HeaderCarrier {\n const props = message.application_properties;\n const out: HeaderCarrier = {};\n if (!props) return out;\n for (const key of Object.keys(props)) {\n const value = props[key];\n if (value == null) continue;\n const s = String(value);\n if (s !== \"\") out[key] = s;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link ArtemisPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`\n * properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract properties win a key collision; an empty/omitted carrier leaves the message\n * unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */\nexport class ArtemisPublisher {\n private constructor(\n private readonly sender: AmqpSender,\n private readonly queue: string,\n ) {}\n\n /** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */\n static create(sender: AmqpSender, queue: string): ArtemisPublisher {\n return new ArtemisPublisher(sender, queue);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });\n await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band\n * {@link HeaderCarrier} read from the message's `application_properties` (empty when there are\n * none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a\n * child of the producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link ArtemisConsumer}. */\nexport interface ArtemisConsumerOptions {\n /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */\n deadLetterSender?: AmqpSender;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a non-conformant message, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;\n}\n\n/**\n * Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the\n * handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A\n * throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP\n * `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a\n * `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP\n * counter is 0-based, so no −1.\n */\nexport class ArtemisConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly handlers: BabelHandlers,\n private readonly options: ArtemisConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */\n listen(receiver: AmqpReceiver): void {\n receiver.on(\"message\", (context) => this.handle(context));\n }\n\n /** Route + settle one delivery. Exposed for testing. */\n async handle(context: AmqpEventContext): Promise<void> {\n const message = context.message ?? {};\n const delivery = context.delivery;\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n // A non-conformant / poison message may lack the fields to annotate; forward it raw.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\"), decoded, context);\n await this.deadLetterRaw(messageBody(message));\n delivery?.accept();\n return;\n }\n\n const envelope = this.reconcile(decoded, message);\n const urn = jmsType(message) ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(context, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n delivery?.accept();\n } catch (error) {\n this.report(error, envelope, context);\n await this.retryOrDeadLetter(context, envelope, error);\n }\n }\n\n /** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */\n private reconcile(envelope: IncomingEnvelope, message: AmqpMessage): Envelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n const count = deliveryCount(message);\n return { ...envelope, attempts: Math.max(body, count) } as Envelope;\n }\n\n private async onUnknownUrn(context: AmqpEventContext, envelope: Envelope, urn: string): Promise<void> {\n const delivery = context.delivery;\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n delivery?.accept();\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope, \"unknown_urn\");\n delivery?.accept();\n return;\n case UnknownUrnStrategy.RELEASE:\n delivery?.release({ delivery_failed: true });\n return;\n default: {\n // FAIL: surface and do NOT settle — the broker redelivers, then dead-letters.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, context);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(context: AmqpEventContext, envelope: Envelope, error: unknown): Promise<void> {\n const delivery = context.delivery;\n if ((envelope.attempts ?? 0) + 1 < this.maxTries) {\n // Release with delivery-failed leaves it for redelivery — the broker bumps delivery-count.\n delivery?.release({ delivery_failed: true });\n } else if (this.options.deadLetterSender) {\n await this.deadLetter(envelope, \"failed\", error);\n delivery?.accept();\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n private async deadLetter(envelope: Envelope, reason: string, error?: unknown): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n const original = envelope.meta?.queue ?? \"\";\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n await sender.send(artemisMessage(annotated));\n }\n\n private async deadLetterRaw(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sender.send({ body: raw });\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n this.options.onError?.(error, envelope, context);\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA8BA,kBAA8F;;;ACfvF,SAAS,gBAAgB,SAA0D;AACxF,QAAM,MAAqB,CAAC;AAC5B,MAAI,CAAC,QAAS,QAAO;AACrB,aAAW,OAAO,OAAO,KAAK,OAAO,GAAG;AACtC,UAAM,QAAQ,QAAQ,GAAG;AACzB,QAAI,QAAQ,MAAM,SAAS,QAAQ,UAAU,GAAI;AACjD,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO;AACT;;;ADoDO,IAAM,eAAe;AAErB,IAAM,yBAAyB;AAQ/B,SAAS,eAAe,UAAoB,SAAkB,OAA2C;AAC9G,QAAM,cAAuC,CAAC;AAC9C,MAAI,SAAS,IAAK,aAAY,YAAY,IAAI,SAAS;AAMvD,QAAM,wBAAiD,CAAC;AACxD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,gBAAgB,KAAK,CAAC,EAAG,uBAAsB,GAAG,IAAI;AAChG,wBAAsB,mBAAmB,IAAI,OAAO,SAAS,KAAK,cAAc;AAChF,wBAAsB,aAAa,IAAI,OAAO,SAAS,YAAY,CAAC;AACpE,wBAAsB,WAAW,IAAI;AACrC,MAAI,SAAS,KAAK,KAAM,uBAAsB,gBAAgB,IAAI,SAAS,KAAK;AAEhF,QAAM,UAAuB;AAAA,IAC3B,MAAM,0BAAc,OAAO,QAAQ;AAAA,IACnC,cAAc;AAAA,IACd,wBAAwB;AAAA,IACxB,qBAAqB;AAAA,EACvB;AACA,MAAI,SAAS,SAAU,SAAQ,iBAAiB,SAAS;AACzD,MAAI,SAAS,KAAK,cAAc,KAAM,SAAQ,gBAAgB,SAAS,KAAK;AAE5E,MAAI,WAAW,QAAQ,UAAU,GAAG;AAClC,0BAAsB,UAAU,IAAI,OAAO,OAAO;AAClD,gBAAY,sBAAsB,IAAI,KAAK,IAAI,IAAI;AAAA,EACrD;AACA,SAAO;AACT;AAGO,SAAS,YAAY,SAA8B;AACxD,QAAM,OAAO,QAAQ;AACrB,MAAI,QAAQ,KAAM,QAAO;AACzB,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,MAAI,OAAO,SAAS,IAAI,EAAG,QAAO,KAAK,SAAS,MAAM;AACtD,MAAI,OAAO,SAAS,YAAY,aAAa,MAAM;AACjD,UAAM,UAAW,KAA8B;AAC/C,QAAI,OAAO,SAAS,OAAO,EAAG,QAAO,QAAQ,SAAS,MAAM;AAC5D,QAAI,OAAO,YAAY,SAAU,QAAO;AAAA,EAC1C;AACA,SAAO,OAAO,IAAI;AACpB;AAEA,SAAS,QAAQ,SAA0C;AACzD,QAAM,QAAQ,QAAQ,sBAAsB,YAAY;AACxD,SAAO,SAAS,OAAO,SAAY,OAAO,KAAK;AACjD;AAEA,SAAS,cAAc,SAA8B;AACnD,QAAM,QAAQ,QAAQ;AACtB,SAAO,OAAO,UAAU,YAAY,QAAQ,IAAI,QAAQ;AAC1D;AAQO,SAAS,UAAU,SAAqC;AAC7D,QAAM,QAAQ,QAAQ;AACtB,QAAM,MAAqB,CAAC;AAC5B,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC,UAAM,QAAQ,MAAM,GAAG;AACvB,QAAI,SAAS,KAAM;AACnB,UAAM,IAAI,OAAO,KAAK;AACtB,QAAI,MAAM,GAAI,KAAI,GAAG,IAAI;AAAA,EAC3B;AACA,SAAO;AACT;AAmBO,IAAM,mBAAN,MAAM,kBAAiB;AAAA,EACpB,YACW,QACA,OACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EAFgB;AAAA,EACA;AAAA;AAAA,EAInB,OAAO,OAAO,QAAoB,OAAiC;AACjE,WAAO,IAAI,kBAAiB,QAAQ,KAAK;AAAA,EAC3C;AAAA;AAAA,EAGA,MAAM,QAAQ,KAAa,MAA+B,UAA0B,CAAC,GAAoB;AACvG,UAAM,WAAW,0BAAc,KAAK,KAAK,MAAM,EAAE,OAAO,KAAK,OAAO,SAAS,QAAQ,QAAQ,CAAC;AAC9F,UAAM,KAAK,OAAO,KAAK,eAAe,UAAU,QAAQ,SAAS,QAAQ,OAAO,CAAC;AACjF,WAAO,SAAS,KAAK;AAAA,EACvB;AACF;AAmCO,IAAM,kBAAN,MAAsB;AAAA,EAI3B,YACmB,UACA,UAAkC,CAAC,GACpD;AAFiB;AACA;AAEjB,SAAK,WAAW,QAAQ,YAAY;AACpC,SAAK,aAAa,QAAQ,cAAc,+BAAmB;AAAA,EAC7D;AAAA,EALmB;AAAA,EACA;AAAA,EALF;AAAA,EACA;AAAA;AAAA,EAWjB,OAAO,UAA8B;AACnC,aAAS,GAAG,WAAW,CAAC,YAAY,KAAK,OAAO,OAAO,CAAC;AAAA,EAC1D;AAAA;AAAA,EAGA,MAAM,OAAO,SAA0C;AACrD,UAAM,UAAU,QAAQ,WAAW,CAAC;AACpC,UAAM,WAAW,QAAQ;AACzB,UAAM,UAAU,0BAAc,OAAO,YAAY,OAAO,CAAC;AAEzD,QAAI,CAAC,0BAAc,QAAQ,OAAO,GAAG;AAEnC,WAAK,OAAO,IAAI,4BAAgB,6DAA6D,GAAG,SAAS,OAAO;AAChH,YAAM,KAAK,cAAc,YAAY,OAAO,CAAC;AAC7C,gBAAU,OAAO;AACjB;AAAA,IACF;AAEA,UAAM,WAAW,KAAK,UAAU,SAAS,OAAO;AAChD,UAAM,MAAM,QAAQ,OAAO,KAAK,0BAAc,IAAI,QAAQ;AAC1D,UAAM,UAAU,KAAK,SAAS,GAAG;AACjC,QAAI,CAAC,SAAS;AACZ,YAAM,KAAK,aAAa,SAAS,UAAU,GAAG;AAC9C;AAAA,IACF;AAEA,QAAI;AACF,YAAM,QAAQ,UAAU,SAAS,UAAU,OAAO,CAAC;AACnD,gBAAU,OAAO;AAAA,IACnB,SAAS,OAAO;AACd,WAAK,OAAO,OAAO,UAAU,OAAO;AACpC,YAAM,KAAK,kBAAkB,SAAS,UAAU,KAAK;AAAA,IACvD;AAAA,EACF;AAAA;AAAA,EAGQ,UAAU,UAA4B,SAAgC;AAC5E,UAAM,OAAO,OAAO,SAAS,aAAa,WAAW,SAAS,WAAW;AACzE,UAAM,QAAQ,cAAc,OAAO;AACnC,WAAO,EAAE,GAAG,UAAU,UAAU,KAAK,IAAI,MAAM,KAAK,EAAE;AAAA,EACxD;AAAA,EAEA,MAAc,aAAa,SAA2B,UAAoB,KAA4B;AACpG,UAAM,WAAW,QAAQ;AACzB,YAAQ,KAAK,YAAY;AAAA,MACvB,KAAK,+BAAmB;AACtB,kBAAU,OAAO;AACjB;AAAA,MACF,KAAK,+BAAmB;AACtB,cAAM,KAAK,WAAW,UAAU,aAAa;AAC7C,kBAAU,OAAO;AACjB;AAAA,MACF,KAAK,+BAAmB;AACtB,kBAAU,QAAQ,EAAE,iBAAiB,KAAK,CAAC;AAC3C;AAAA,MACF,SAAS;AAEP,cAAM,QAAQ,IAAI,4BAAgB,GAAG;AACrC,aAAK,OAAO,OAAO,UAAU,OAAO;AACpC,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,kBAAkB,SAA2B,UAAoB,OAA+B;AAC5G,UAAM,WAAW,QAAQ;AACzB,SAAK,SAAS,YAAY,KAAK,IAAI,KAAK,UAAU;AAEhD,gBAAU,QAAQ,EAAE,iBAAiB,KAAK,CAAC;AAAA,IAC7C,WAAW,KAAK,QAAQ,kBAAkB;AACxC,YAAM,KAAK,WAAW,UAAU,UAAU,KAAK;AAC/C,gBAAU,OAAO;AAAA,IACnB,OAAO;AACL,gBAAU,OAAO;AAAA,IACnB;AAAA,EACF;AAAA,EAEA,MAAc,WAAW,UAAoB,QAAgB,OAAgC;AAC3F,UAAM,SAAS,KAAK,QAAQ;AAC5B,QAAI,CAAC,OAAQ;AACb,UAAM,WAAW,SAAS,MAAM,SAAS;AACzC,UAAM,gBAAY,sBAAS,UAAU,QAAQ,UAAU;AAAA,MACrD,UAAU,SAAS,YAAY;AAAA,MAC/B,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MAChD,WAAW,iBAAiB,QAAQ,MAAM,OAAO;AAAA,IACnD,CAAC;AACD,UAAM,OAAO,KAAK,eAAe,SAAS,CAAC;AAAA,EAC7C;AAAA,EAEA,MAAc,cAAc,KAA4B;AACtD,UAAM,SAAS,KAAK,QAAQ;AAC5B,QAAI,CAAC,OAAQ;AACb,UAAM,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AAAA,EACjC;AAAA,EAEQ,OAAO,OAAgB,UAAmC,SAAiC;AACjG,SAAK,QAAQ,UAAU,OAAO,UAAU,OAAO;AAAA,EACjD;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache ActiveMQ Artemis adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **AMQP 1.0** consumer over Artemis (rhea),\n * so an Artemis-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the Java, .NET, Python and Go SDKs.\n *\n * Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —\n * per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so\n * this adapter maps onto them (the envelope stays `schema_version: 1`): the envelope JSON is the\n * message **body**; the contract fields are mirrored onto the AMQP a JMS peer reads\n * (`correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN) plus the `bq_` application properties; consume settles per message\n * (`accept` after success, `release` for redelivery); **`attempts = max(body, delivery-count)`**\n * (the AMQP counter is 0-based — no −1); terminal failures go to an opt-in `<queue>.dlq` with a\n * `dead_letter` block.\n *\n * This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide\n * the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional AMQP `application_properties` beside the contract `bq_`\n * properties — the contract properties win a key collision (merge-not-clobber), blanks dropped.\n * `traceparent`/`tracestate` are W3C keys with no hyphens, so they are already JMS-legal (§7 uses\n * underscores because JMS property names must be Java identifiers). On consume the inbound\n * `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract\n * sees the `traceparent` and links the consumer span as a child. A header-less publish is\n * byte-identical. GR-1: the wire envelope body is never touched.\n *\n * **Version gate (§7.7).** Before decoding the body the consumer reads the `bq_schema_version`\n * application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII\n * whitespace) follows the normal decode path; any other value is compared, untrimmed, to the supported\n * `\"1\"`, and a message that does not match is rejected undecoded — reported to `onError`, the raw body\n * forwarded to the DLQ sender (when configured) and the message accepted **only after the broker has\n * accepted the DLQ delivery**. If the DLQ send fails, is refused (rejected/released/modified), the link\n * closes or `deadLetterTimeoutMs` elapses, the failure is reported and, after `deadLetterRetryDelayMs`,\n * the source message gets a plain `released` outcome (no `delivery_failed`: the broker's delivery\n * counter and `max-delivery-attempts` stay untouched) — never accepted, never handed to a handler.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { sanitizeHeaders } from \"./headers.js\";\n\n// --- Minimal AMQP 1.0 shapes (a structural subset of rhea) ---------------------\n\n/** An AMQP message (structural subset of a rhea message). */\nexport interface AmqpMessage {\n body?: unknown;\n correlation_id?: unknown;\n creation_time?: number | Date;\n content_type?: string;\n application_properties?: Record<string, unknown>;\n message_annotations?: Record<string, unknown>;\n /** The AMQP header delivery-count (0-based: 0 on first delivery). */\n delivery_count?: number;\n}\n\n/** The disposition handle rhea passes alongside a received message. */\nexport interface AmqpDelivery {\n /** Settle (acknowledge) the message — it is removed from the queue. */\n accept(): void;\n /** Return the message for redelivery — the broker increments `delivery-count`. */\n release(params?: { delivery_failed?: boolean; undeliverable_here?: boolean }): void;\n}\n\n/** The event context rhea emits on a `message` event. */\nexport interface AmqpEventContext {\n message?: AmqpMessage;\n delivery?: AmqpDelivery;\n}\n\n/**\n * The context rhea passes to a sender link event (`accepted`, `rejected`, `sender_close`, ...). Only\n * `delivery` is inspected (to match a verdict to its delivery); everything else is opaque. Deliberately\n * no index signature: an interface-typed handler parameter (e.g. rhea's `EventContext`) must stay assignable.\n */\nexport interface AmqpLinkEventContext {\n delivery?: unknown;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n /** Queue a delivery. rhea returns the delivery synchronously; the broker's verdict arrives later as an event. */\n send(message: AmqpMessage): unknown;\n /**\n * Optional (a rhea sender has it). When both `on` and `removeListener` exist, the consumer's\n * version-gate path waits for the broker's `accepted` disposition on the DLQ delivery before it\n * settles the source message; without them only a synchronous `send` failure is observable.\n */\n on?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;\n removeListener?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;\n /**\n * Optional (a rhea link has it). rhea does not bubble an event to the session/connection while the\n * link has a listener for it, so the temporary confirmation listeners re-dispatch what is not theirs\n * (other deliveries' dispositions, link close/error) here.\n */\n session?: { dispatch?(event: string, context?: AmqpLinkEventContext): unknown };\n}\n\n/** The subset of a rhea receiver this adapter wires to (its `message` event). */\nexport interface AmqpReceiver {\n on(event: \"message\", handler: (context: AmqpEventContext) => void): unknown;\n}\n\n// --- Projection (contract §7.2) ------------------------------------------------\n\n/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */\nexport const JMS_TYPE_KEY = \"x-opt-jms-type\";\n/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */\nexport const SCHEDULED_DELIVERY_KEY = \"x-opt-delivery-time\";\n\n/**\n * Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,\n * `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`\n * sets the `x-opt-delivery-time` annotation for native scheduled delivery.\n */\nexport function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage {\n const annotations: Record<string, unknown> = {};\n if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;\n\n // Out-of-band riders (e.g. traceparent) go in first; the contract bq_ properties below win any\n // key collision (merge-not-clobber). The bq_ names use underscores, not hyphens: a JMS property\n // name must be a valid Java identifier, and every Artemis SDK uses the same JMS-legal form for\n // cross-protocol parity. traceparent/tracestate have no hyphens, so they are already JMS-legal.\n const applicationProperties: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;\n applicationProperties[\"bq_schema_version\"] = String(envelope.meta.schema_version);\n applicationProperties[\"bq_attempts\"] = String(envelope.attempts ?? 0);\n applicationProperties[\"bq_app_id\"] = \"babelqueue\";\n if (envelope.meta.lang) applicationProperties[\"bq_source_lang\"] = envelope.meta.lang;\n\n const message: AmqpMessage = {\n body: EnvelopeCodec.encode(envelope),\n content_type: \"application/json\",\n application_properties: applicationProperties,\n message_annotations: annotations,\n };\n if (envelope.trace_id) message.correlation_id = envelope.trace_id;\n if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;\n\n if (delayMs != null && delayMs > 0) {\n applicationProperties[\"bq_delay\"] = String(delayMs);\n annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;\n }\n return message;\n}\n\n/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */\nexport function messageBody(message: AmqpMessage): string {\n const body = message.body;\n if (body == null) return \"\";\n if (typeof body === \"string\") return body;\n if (Buffer.isBuffer(body)) return body.toString(\"utf8\");\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content.toString(\"utf8\");\n if (typeof content === \"string\") return content;\n }\n return String(body);\n}\n\nfunction jmsType(message: AmqpMessage): string | undefined {\n const value = message.message_annotations?.[JMS_TYPE_KEY];\n return value == null ? undefined : String(value);\n}\n\n/** Blank = empty or only ASCII whitespace (GR-5: the same set in every SDK, never `String.trim()`). */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The inbound `bq_schema_version` application property when it is present, non-blank and not exactly\n * the supported version (compared untrimmed), else `undefined` (the message may be decoded).\n */\nfunction unsupportedSchemaVersion(message: AmqpMessage): string | undefined {\n const raw = message.application_properties?.[\"bq_schema_version\"];\n if (raw == null) return undefined;\n const value = Buffer.isBuffer(raw) ? raw.toString(\"utf8\") : String(raw);\n if (BLANK_ASCII.test(value)) return undefined;\n return value === String(SCHEMA_VERSION) ? undefined : value;\n}\n\nfunction deliveryCount(message: AmqpMessage): number {\n const value = message.delivery_count;\n return typeof value === \"number\" && value > 0 ? value : 0;\n}\n\n/**\n * Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying\n * values defensively. Returns an empty object when there are none. Both the contract `bq_`\n * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract\n * reads only the keys it knows.\n */\nexport function headersOf(message: AmqpMessage): HeaderCarrier {\n const props = message.application_properties;\n const out: HeaderCarrier = {};\n if (!props) return out;\n for (const key of Object.keys(props)) {\n const value = props[key];\n if (value == null) continue;\n const s = String(value);\n if (s !== \"\") out[key] = s;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link ArtemisPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`\n * properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract properties win a key collision; an empty/omitted carrier leaves the message\n * unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */\nexport class ArtemisPublisher {\n private constructor(\n private readonly sender: AmqpSender,\n private readonly queue: string,\n ) {}\n\n /** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */\n static create(sender: AmqpSender, queue: string): ArtemisPublisher {\n return new ArtemisPublisher(sender, queue);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });\n await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));\n return envelope.meta.id;\n }\n}\n\n// --- DLQ delivery confirmation ---------------------------------------------------\n\n/** Default wait (ms) for the broker to settle a gate-path DLQ delivery. */\nconst DEFAULT_DLQ_CONFIRM_TIMEOUT_MS = 30_000;\n/** Default pause (ms) before a gate message whose DLQ write failed is released back to the broker. */\nconst DEFAULT_DLQ_RETRY_DELAY_MS = 1_000;\n\n/** One in-flight DLQ confirmation registered on a {@link ConfirmationHub}. */\ninterface PendingConfirmation {\n /** The delivery `send` returned; `undefined` while `send` is still running synchronously. */\n delivery: unknown;\n /** `true` when `context` is (or may be) this confirmation's verdict. */\n matches(context: AmqpLinkEventContext | undefined): boolean;\n /** Resolve (no argument) or reject the confirmation. */\n settle(error?: Error): void;\n}\n\nconst CONFIRMATION_VERDICTS = [\"accepted\", \"rejected\", \"released\", \"modified\"] as const;\nconst CONFIRMATION_CLOSES = [\"sender_close\", \"sender_error\"] as const;\n\n/**\n * One shared set of temporary sender listeners per {@link AmqpSender}, however many DLQ confirmations\n * are in flight. rhea bubbles a link event to the session only while the link has no listener for it,\n * so while confirmations are pending the hub owns the listeners and re-dispatches every event it does\n * not consume to `sender.session` exactly once (per-confirmation listeners would forward each event\n * once per pending confirmation). The listeners are removed as soon as no confirmation is pending.\n */\nclass ConfirmationHub {\n private readonly pending = new Set<PendingConfirmation>();\n private readonly handlers: [string, (context: AmqpLinkEventContext) => void][] = [];\n\n constructor(\n private readonly sender: AmqpSender,\n private readonly on: (event: string, handler: (context: AmqpLinkEventContext) => void) => unknown,\n private readonly off: (event: string, handler: (context: AmqpLinkEventContext) => void) => unknown,\n ) {}\n\n add(confirmation: PendingConfirmation): void {\n this.pending.add(confirmation);\n if (this.handlers.length === 0) {\n for (const name of [...CONFIRMATION_VERDICTS, ...CONFIRMATION_CLOSES]) {\n const handler = (context: AmqpLinkEventContext): void => this.dispatch(name, context);\n this.handlers.push([name, handler]);\n this.on(name, handler);\n }\n }\n }\n\n remove(confirmation: PendingConfirmation): void {\n this.pending.delete(confirmation);\n if (this.pending.size === 0) {\n for (const [name, handler] of this.handlers) this.off(name, handler);\n this.handlers.length = 0;\n hubs.delete(this.sender);\n }\n }\n\n private dispatch(name: string, context: AmqpLinkEventContext): void {\n if ((CONFIRMATION_CLOSES as readonly string[]).includes(name)) {\n for (const confirmation of [...this.pending]) {\n confirmation.settle(new BabelQueueError(`The dead-letter link closed before the broker accepted the delivery (${name}).`));\n }\n this.forward(name, context);\n return;\n }\n const owners = [...this.pending].filter((confirmation) => confirmation.matches(context));\n if (owners.length === 0) {\n this.forward(name, context);\n return;\n }\n for (const owner of owners) {\n owner.settle(name === \"accepted\" ? undefined : new BabelQueueError(`The broker did not accept the dead-letter delivery (${name}).`));\n }\n }\n\n private forward(name: string, context: AmqpLinkEventContext): void {\n try {\n this.sender.session?.dispatch?.(name, context);\n } catch {\n // forwarding is best effort; never let it change a confirmation outcome\n }\n }\n}\n\nconst hubs = new WeakMap<AmqpSender, ConfirmationHub>();\n\n/**\n * Send `message` and resolve only once the broker has `accepted` the delivery. A rhea `send` returns\n * synchronously and reports the verdict (`accepted` / `rejected` / `released` / `modified`) as a\n * sender event, so awaiting `send` alone cannot see a broker refusal. Rejects on any non-accepted\n * disposition, a link close/error, or when `timeoutMs` elapses (an unsettled delivery that is lost on\n * reconnect raises no event). A sender without event support degrades to awaiting `send` itself.\n * Events that are not this delivery's verdict (other deliveries, link close/error) are re-dispatched on\n * the sender's session, once per event, so the application still sees them (rhea suppresses bubbling\n * while the link has a listener) — see {@link ConfirmationHub}.\n */\nfunction sendConfirmed(sender: AmqpSender, message: AmqpMessage, timeoutMs: number): Promise<void> {\n const on = sender.on?.bind(sender);\n const off = sender.removeListener?.bind(sender);\n if (!on || !off) return Promise.resolve(sender.send(message)).then(() => undefined);\n\n return new Promise<void>((resolve, reject) => {\n let hub = hubs.get(sender);\n if (!hub) {\n hub = new ConfirmationHub(sender, on, off);\n hubs.set(sender, hub);\n }\n const activeHub = hub;\n let finished = false;\n\n const confirmation: PendingConfirmation = {\n delivery: undefined,\n matches: (context) =>\n confirmation.delivery === undefined || context?.delivery === undefined || context.delivery === confirmation.delivery,\n settle: (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timer);\n activeHub.remove(confirmation);\n if (error) reject(error);\n else resolve();\n },\n };\n activeHub.add(confirmation);\n\n // `settle` (above) reads `timer`; it cannot run before this line (nothing is sent or listened to yet).\n const timer = setTimeout(\n () => confirmation.settle(new BabelQueueError(`Timed out after ${timeoutMs}ms waiting for the broker to accept the dead-letter delivery.`)),\n timeoutMs,\n );\n try {\n confirmation.delivery = sender.send(message);\n } catch (error) {\n confirmation.settle(error instanceof Error ? error : new BabelQueueError(String(error)));\n return;\n }\n // A pre-settled delivery (or a sender that returns none) raises no disposition event.\n const delivery = confirmation.delivery;\n if (delivery === null || typeof delivery !== \"object\" || (delivery as { settled?: unknown }).settled === true) confirmation.settle();\n });\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band\n * {@link HeaderCarrier} read from the message's `application_properties` (empty when there are\n * none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a\n * child of the producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link ArtemisConsumer}. */\nexport interface ArtemisConsumerOptions {\n /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */\n deadLetterSender?: AmqpSender;\n /**\n * How long (ms) every dead-letter path (version gate, poison, unknown-URN `dead_letter`, terminal\n * handler failure) waits for the broker to accept the DLQ delivery before it releases the source\n * message instead of accepting it (default 30000).\n */\n deadLetterTimeoutMs?: number;\n /**\n * Pause (ms) before a dead-letter path releases a message whose DLQ write failed (default 1000).\n * The release is a plain `released` outcome (no `delivery_failed`, so the broker's delivery counter and\n * `max-delivery-attempts` are untouched); the pause keeps a persistently failing DLQ from becoming a\n * hot redelivery loop. The delivery stays unsettled (holding one credit) while it waits.\n * Note: on the terminal handler-failure path the message has already been through the handler, and\n * a plain release does not bump the delivery counter, so the redelivery runs the handler again (and\n * reports its failure again) until the DLQ write succeeds.\n */\n deadLetterRetryDelayMs?: number;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a non-conformant message, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;\n}\n\n/**\n * Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the\n * handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A\n * throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP\n * `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a\n * `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP\n * counter is 0-based, so no −1.\n */\nexport class ArtemisConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly handlers: BabelHandlers,\n private readonly options: ArtemisConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */\n listen(receiver: AmqpReceiver): void {\n receiver.on(\"message\", (context) => this.handle(context));\n }\n\n /** Route + settle one delivery. Exposed for testing. */\n async handle(context: AmqpEventContext): Promise<void> {\n const message = context.message ?? {};\n const delivery = context.delivery;\n\n // Version gate (§7.7): decided on the application property alone, before the body is decoded.\n const unsupported = unsupportedSchemaVersion(message);\n if (unsupported !== undefined) {\n const gateError = new BabelQueueError(\n `Rejected a message from Artemis with unsupported bq_schema_version \"${unsupported}\" (supported: ${SCHEMA_VERSION}).`,\n );\n // Accept only after the broker has accepted the DLQ copy; otherwise release (plain, after a pause) so\n // the message is never lost and never reaches a handler. Reports come after the settlement, or are\n // isolated, so a throwing onError can never leave the message unsettled.\n try {\n await this.deadLetterRawConfirmed(messageBody(message));\n } catch (dlqError) {\n this.safeReport(gateError, null, context);\n this.safeReport(dlqError, null, context);\n await this.releaseAfterBackoff(delivery);\n return;\n }\n delivery?.accept();\n this.safeReport(gateError, null, context);\n return;\n }\n\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n // A non-conformant / poison message may lack the fields to annotate; forward it raw.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\"), decoded, context);\n await this.settleAfterDeadLetter(context, decoded, () => this.deadLetterRaw(messageBody(message)));\n return;\n }\n\n const envelope = this.reconcile(decoded, message);\n const urn = jmsType(message) ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(context, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n delivery?.accept();\n } catch (error) {\n this.report(error, envelope, context);\n await this.retryOrDeadLetter(context, envelope, error);\n }\n }\n\n /** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */\n private reconcile(envelope: IncomingEnvelope, message: AmqpMessage): Envelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n const count = deliveryCount(message);\n return { ...envelope, attempts: Math.max(body, count) } as Envelope;\n }\n\n private async onUnknownUrn(context: AmqpEventContext, envelope: Envelope, urn: string): Promise<void> {\n const delivery = context.delivery;\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n delivery?.accept();\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, \"unknown_urn\"));\n return;\n case UnknownUrnStrategy.RELEASE:\n delivery?.release({ delivery_failed: true });\n return;\n default: {\n // FAIL: surface and do NOT settle — the broker redelivers, then dead-letters.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, context);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(context: AmqpEventContext, envelope: Envelope, error: unknown): Promise<void> {\n const delivery = context.delivery;\n if ((envelope.attempts ?? 0) + 1 < this.maxTries) {\n // Release with delivery-failed leaves it for redelivery — the broker bumps delivery-count.\n delivery?.release({ delivery_failed: true });\n } else if (this.options.deadLetterSender) {\n await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, \"failed\", error));\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n /**\n * Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry\n * delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender\n * the write is a no-op and the message is accepted (terminal drop), as before.\n */\n private async settleAfterDeadLetter(\n context: AmqpEventContext,\n envelope: IncomingEnvelope | null,\n write: () => Promise<void>,\n ): Promise<void> {\n try {\n await write();\n } catch (dlqError) {\n this.safeReport(dlqError, envelope, context);\n await this.releaseAfterBackoff(context.delivery);\n return;\n }\n context.delivery?.accept();\n }\n\n private async deadLetter(envelope: Envelope, reason: string, error?: unknown): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n const original = envelope.meta?.queue ?? \"\";\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n await sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);\n }\n\n /** Poison path: forward the raw body, confirmed like the gate path. */\n private deadLetterRaw(raw: string): Promise<void> {\n return this.deadLetterRawConfirmed(raw);\n }\n\n /** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */\n private async deadLetterRawConfirmed(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sendConfirmed(sender, { body: raw }, this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);\n }\n\n /** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */\n private async releaseAfterBackoff(delivery: AmqpDelivery | undefined): Promise<void> {\n const delayMs = this.options.deadLetterRetryDelayMs ?? DEFAULT_DLQ_RETRY_DELAY_MS;\n if (delayMs > 0) await new Promise<void>((resolve) => setTimeout(resolve, delayMs).unref?.());\n delivery?.release();\n }\n\n /** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */\n private safeReport(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n try {\n this.report(error, envelope, context);\n } catch {\n // swallowed by design\n }\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n this.options.onError?.(error, envelope, context);\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -28,6 +28,16 @@ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
|
|
|
28
28
|
* `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract
|
|
29
29
|
* sees the `traceparent` and links the consumer span as a child. A header-less publish is
|
|
30
30
|
* byte-identical. GR-1: the wire envelope body is never touched.
|
|
31
|
+
*
|
|
32
|
+
* **Version gate (§7.7).** Before decoding the body the consumer reads the `bq_schema_version`
|
|
33
|
+
* application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII
|
|
34
|
+
* whitespace) follows the normal decode path; any other value is compared, untrimmed, to the supported
|
|
35
|
+
* `"1"`, and a message that does not match is rejected undecoded — reported to `onError`, the raw body
|
|
36
|
+
* forwarded to the DLQ sender (when configured) and the message accepted **only after the broker has
|
|
37
|
+
* accepted the DLQ delivery**. If the DLQ send fails, is refused (rejected/released/modified), the link
|
|
38
|
+
* closes or `deadLetterTimeoutMs` elapses, the failure is reported and, after `deadLetterRetryDelayMs`,
|
|
39
|
+
* the source message gets a plain `released` outcome (no `delivery_failed`: the broker's delivery
|
|
40
|
+
* counter and `max-delivery-attempts` stay untouched) — never accepted, never handed to a handler.
|
|
31
41
|
*/
|
|
32
42
|
|
|
33
43
|
/** An AMQP message (structural subset of a rhea message). */
|
|
@@ -56,9 +66,33 @@ interface AmqpEventContext {
|
|
|
56
66
|
message?: AmqpMessage;
|
|
57
67
|
delivery?: AmqpDelivery;
|
|
58
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The context rhea passes to a sender link event (`accepted`, `rejected`, `sender_close`, ...). Only
|
|
71
|
+
* `delivery` is inspected (to match a verdict to its delivery); everything else is opaque. Deliberately
|
|
72
|
+
* no index signature: an interface-typed handler parameter (e.g. rhea's `EventContext`) must stay assignable.
|
|
73
|
+
*/
|
|
74
|
+
interface AmqpLinkEventContext {
|
|
75
|
+
delivery?: unknown;
|
|
76
|
+
}
|
|
59
77
|
/** The subset of a rhea sender this adapter calls. */
|
|
60
78
|
interface AmqpSender {
|
|
79
|
+
/** Queue a delivery. rhea returns the delivery synchronously; the broker's verdict arrives later as an event. */
|
|
61
80
|
send(message: AmqpMessage): unknown;
|
|
81
|
+
/**
|
|
82
|
+
* Optional (a rhea sender has it). When both `on` and `removeListener` exist, the consumer's
|
|
83
|
+
* version-gate path waits for the broker's `accepted` disposition on the DLQ delivery before it
|
|
84
|
+
* settles the source message; without them only a synchronous `send` failure is observable.
|
|
85
|
+
*/
|
|
86
|
+
on?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;
|
|
87
|
+
removeListener?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;
|
|
88
|
+
/**
|
|
89
|
+
* Optional (a rhea link has it). rhea does not bubble an event to the session/connection while the
|
|
90
|
+
* link has a listener for it, so the temporary confirmation listeners re-dispatch what is not theirs
|
|
91
|
+
* (other deliveries' dispositions, link close/error) here.
|
|
92
|
+
*/
|
|
93
|
+
session?: {
|
|
94
|
+
dispatch?(event: string, context?: AmqpLinkEventContext): unknown;
|
|
95
|
+
};
|
|
62
96
|
}
|
|
63
97
|
/** The subset of a rhea receiver this adapter wires to (its `message` event). */
|
|
64
98
|
interface AmqpReceiver {
|
|
@@ -120,6 +154,22 @@ type BabelHandlers = Record<string, BabelHandler>;
|
|
|
120
154
|
interface ArtemisConsumerOptions {
|
|
121
155
|
/** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */
|
|
122
156
|
deadLetterSender?: AmqpSender;
|
|
157
|
+
/**
|
|
158
|
+
* How long (ms) every dead-letter path (version gate, poison, unknown-URN `dead_letter`, terminal
|
|
159
|
+
* handler failure) waits for the broker to accept the DLQ delivery before it releases the source
|
|
160
|
+
* message instead of accepting it (default 30000).
|
|
161
|
+
*/
|
|
162
|
+
deadLetterTimeoutMs?: number;
|
|
163
|
+
/**
|
|
164
|
+
* Pause (ms) before a dead-letter path releases a message whose DLQ write failed (default 1000).
|
|
165
|
+
* The release is a plain `released` outcome (no `delivery_failed`, so the broker's delivery counter and
|
|
166
|
+
* `max-delivery-attempts` are untouched); the pause keeps a persistently failing DLQ from becoming a
|
|
167
|
+
* hot redelivery loop. The delivery stays unsettled (holding one credit) while it waits.
|
|
168
|
+
* Note: on the terminal handler-failure path the message has already been through the handler, and
|
|
169
|
+
* a plain release does not bump the delivery counter, so the redelivery runs the handler again (and
|
|
170
|
+
* reports its failure again) until the DLQ write succeeds.
|
|
171
|
+
*/
|
|
172
|
+
deadLetterRetryDelayMs?: number;
|
|
123
173
|
/** Attempts before terminal dead-lettering (default 3). */
|
|
124
174
|
maxTries?: number;
|
|
125
175
|
/** Strategy for a URN with no handler (default `fail`). */
|
|
@@ -149,9 +199,22 @@ declare class ArtemisConsumer {
|
|
|
149
199
|
private reconcile;
|
|
150
200
|
private onUnknownUrn;
|
|
151
201
|
private retryOrDeadLetter;
|
|
202
|
+
/**
|
|
203
|
+
* Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry
|
|
204
|
+
* delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender
|
|
205
|
+
* the write is a no-op and the message is accepted (terminal drop), as before.
|
|
206
|
+
*/
|
|
207
|
+
private settleAfterDeadLetter;
|
|
152
208
|
private deadLetter;
|
|
209
|
+
/** Poison path: forward the raw body, confirmed like the gate path. */
|
|
153
210
|
private deadLetterRaw;
|
|
211
|
+
/** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */
|
|
212
|
+
private deadLetterRawConfirmed;
|
|
213
|
+
/** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
|
|
214
|
+
private releaseAfterBackoff;
|
|
215
|
+
/** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */
|
|
216
|
+
private safeReport;
|
|
154
217
|
private report;
|
|
155
218
|
}
|
|
156
219
|
|
|
157
|
-
export { type AmqpDelivery, type AmqpEventContext, type AmqpMessage, type AmqpReceiver, type AmqpSender, ArtemisConsumer, type ArtemisConsumerOptions, ArtemisPublisher, type BabelHandler, type BabelHandlers, JMS_TYPE_KEY, type PublishOptions, SCHEDULED_DELIVERY_KEY, artemisMessage, headersOf, messageBody };
|
|
220
|
+
export { type AmqpDelivery, type AmqpEventContext, type AmqpLinkEventContext, type AmqpMessage, type AmqpReceiver, type AmqpSender, ArtemisConsumer, type ArtemisConsumerOptions, ArtemisPublisher, type BabelHandler, type BabelHandlers, JMS_TYPE_KEY, type PublishOptions, SCHEDULED_DELIVERY_KEY, artemisMessage, headersOf, messageBody };
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,16 @@ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
|
|
|
28
28
|
* `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract
|
|
29
29
|
* sees the `traceparent` and links the consumer span as a child. A header-less publish is
|
|
30
30
|
* byte-identical. GR-1: the wire envelope body is never touched.
|
|
31
|
+
*
|
|
32
|
+
* **Version gate (§7.7).** Before decoding the body the consumer reads the `bq_schema_version`
|
|
33
|
+
* application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII
|
|
34
|
+
* whitespace) follows the normal decode path; any other value is compared, untrimmed, to the supported
|
|
35
|
+
* `"1"`, and a message that does not match is rejected undecoded — reported to `onError`, the raw body
|
|
36
|
+
* forwarded to the DLQ sender (when configured) and the message accepted **only after the broker has
|
|
37
|
+
* accepted the DLQ delivery**. If the DLQ send fails, is refused (rejected/released/modified), the link
|
|
38
|
+
* closes or `deadLetterTimeoutMs` elapses, the failure is reported and, after `deadLetterRetryDelayMs`,
|
|
39
|
+
* the source message gets a plain `released` outcome (no `delivery_failed`: the broker's delivery
|
|
40
|
+
* counter and `max-delivery-attempts` stay untouched) — never accepted, never handed to a handler.
|
|
31
41
|
*/
|
|
32
42
|
|
|
33
43
|
/** An AMQP message (structural subset of a rhea message). */
|
|
@@ -56,9 +66,33 @@ interface AmqpEventContext {
|
|
|
56
66
|
message?: AmqpMessage;
|
|
57
67
|
delivery?: AmqpDelivery;
|
|
58
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The context rhea passes to a sender link event (`accepted`, `rejected`, `sender_close`, ...). Only
|
|
71
|
+
* `delivery` is inspected (to match a verdict to its delivery); everything else is opaque. Deliberately
|
|
72
|
+
* no index signature: an interface-typed handler parameter (e.g. rhea's `EventContext`) must stay assignable.
|
|
73
|
+
*/
|
|
74
|
+
interface AmqpLinkEventContext {
|
|
75
|
+
delivery?: unknown;
|
|
76
|
+
}
|
|
59
77
|
/** The subset of a rhea sender this adapter calls. */
|
|
60
78
|
interface AmqpSender {
|
|
79
|
+
/** Queue a delivery. rhea returns the delivery synchronously; the broker's verdict arrives later as an event. */
|
|
61
80
|
send(message: AmqpMessage): unknown;
|
|
81
|
+
/**
|
|
82
|
+
* Optional (a rhea sender has it). When both `on` and `removeListener` exist, the consumer's
|
|
83
|
+
* version-gate path waits for the broker's `accepted` disposition on the DLQ delivery before it
|
|
84
|
+
* settles the source message; without them only a synchronous `send` failure is observable.
|
|
85
|
+
*/
|
|
86
|
+
on?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;
|
|
87
|
+
removeListener?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;
|
|
88
|
+
/**
|
|
89
|
+
* Optional (a rhea link has it). rhea does not bubble an event to the session/connection while the
|
|
90
|
+
* link has a listener for it, so the temporary confirmation listeners re-dispatch what is not theirs
|
|
91
|
+
* (other deliveries' dispositions, link close/error) here.
|
|
92
|
+
*/
|
|
93
|
+
session?: {
|
|
94
|
+
dispatch?(event: string, context?: AmqpLinkEventContext): unknown;
|
|
95
|
+
};
|
|
62
96
|
}
|
|
63
97
|
/** The subset of a rhea receiver this adapter wires to (its `message` event). */
|
|
64
98
|
interface AmqpReceiver {
|
|
@@ -120,6 +154,22 @@ type BabelHandlers = Record<string, BabelHandler>;
|
|
|
120
154
|
interface ArtemisConsumerOptions {
|
|
121
155
|
/** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */
|
|
122
156
|
deadLetterSender?: AmqpSender;
|
|
157
|
+
/**
|
|
158
|
+
* How long (ms) every dead-letter path (version gate, poison, unknown-URN `dead_letter`, terminal
|
|
159
|
+
* handler failure) waits for the broker to accept the DLQ delivery before it releases the source
|
|
160
|
+
* message instead of accepting it (default 30000).
|
|
161
|
+
*/
|
|
162
|
+
deadLetterTimeoutMs?: number;
|
|
163
|
+
/**
|
|
164
|
+
* Pause (ms) before a dead-letter path releases a message whose DLQ write failed (default 1000).
|
|
165
|
+
* The release is a plain `released` outcome (no `delivery_failed`, so the broker's delivery counter and
|
|
166
|
+
* `max-delivery-attempts` are untouched); the pause keeps a persistently failing DLQ from becoming a
|
|
167
|
+
* hot redelivery loop. The delivery stays unsettled (holding one credit) while it waits.
|
|
168
|
+
* Note: on the terminal handler-failure path the message has already been through the handler, and
|
|
169
|
+
* a plain release does not bump the delivery counter, so the redelivery runs the handler again (and
|
|
170
|
+
* reports its failure again) until the DLQ write succeeds.
|
|
171
|
+
*/
|
|
172
|
+
deadLetterRetryDelayMs?: number;
|
|
123
173
|
/** Attempts before terminal dead-lettering (default 3). */
|
|
124
174
|
maxTries?: number;
|
|
125
175
|
/** Strategy for a URN with no handler (default `fail`). */
|
|
@@ -149,9 +199,22 @@ declare class ArtemisConsumer {
|
|
|
149
199
|
private reconcile;
|
|
150
200
|
private onUnknownUrn;
|
|
151
201
|
private retryOrDeadLetter;
|
|
202
|
+
/**
|
|
203
|
+
* Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry
|
|
204
|
+
* delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender
|
|
205
|
+
* the write is a no-op and the message is accepted (terminal drop), as before.
|
|
206
|
+
*/
|
|
207
|
+
private settleAfterDeadLetter;
|
|
152
208
|
private deadLetter;
|
|
209
|
+
/** Poison path: forward the raw body, confirmed like the gate path. */
|
|
153
210
|
private deadLetterRaw;
|
|
211
|
+
/** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */
|
|
212
|
+
private deadLetterRawConfirmed;
|
|
213
|
+
/** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
|
|
214
|
+
private releaseAfterBackoff;
|
|
215
|
+
/** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */
|
|
216
|
+
private safeReport;
|
|
154
217
|
private report;
|
|
155
218
|
}
|
|
156
219
|
|
|
157
|
-
export { type AmqpDelivery, type AmqpEventContext, type AmqpMessage, type AmqpReceiver, type AmqpSender, ArtemisConsumer, type ArtemisConsumerOptions, ArtemisPublisher, type BabelHandler, type BabelHandlers, JMS_TYPE_KEY, type PublishOptions, SCHEDULED_DELIVERY_KEY, artemisMessage, headersOf, messageBody };
|
|
220
|
+
export { type AmqpDelivery, type AmqpEventContext, type AmqpLinkEventContext, type AmqpMessage, type AmqpReceiver, type AmqpSender, ArtemisConsumer, type ArtemisConsumerOptions, ArtemisPublisher, type BabelHandler, type BabelHandlers, JMS_TYPE_KEY, type PublishOptions, SCHEDULED_DELIVERY_KEY, artemisMessage, headersOf, messageBody };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from "@babelqueue/core";
|
|
2
|
+
import { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError, UnknownUrnStrategy } from "@babelqueue/core";
|
|
3
3
|
|
|
4
4
|
// src/headers.ts
|
|
5
5
|
function sanitizeHeaders(headers) {
|
|
@@ -55,6 +55,14 @@ function jmsType(message) {
|
|
|
55
55
|
const value = message.message_annotations?.[JMS_TYPE_KEY];
|
|
56
56
|
return value == null ? void 0 : String(value);
|
|
57
57
|
}
|
|
58
|
+
var BLANK_ASCII = /^[ \t\n\v\f\r]*$/;
|
|
59
|
+
function unsupportedSchemaVersion(message) {
|
|
60
|
+
const raw = message.application_properties?.["bq_schema_version"];
|
|
61
|
+
if (raw == null) return void 0;
|
|
62
|
+
const value = Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw);
|
|
63
|
+
if (BLANK_ASCII.test(value)) return void 0;
|
|
64
|
+
return value === String(SCHEMA_VERSION) ? void 0 : value;
|
|
65
|
+
}
|
|
58
66
|
function deliveryCount(message) {
|
|
59
67
|
const value = message.delivery_count;
|
|
60
68
|
return typeof value === "number" && value > 0 ? value : 0;
|
|
@@ -89,6 +97,103 @@ var ArtemisPublisher = class _ArtemisPublisher {
|
|
|
89
97
|
return envelope.meta.id;
|
|
90
98
|
}
|
|
91
99
|
};
|
|
100
|
+
var DEFAULT_DLQ_CONFIRM_TIMEOUT_MS = 3e4;
|
|
101
|
+
var DEFAULT_DLQ_RETRY_DELAY_MS = 1e3;
|
|
102
|
+
var CONFIRMATION_VERDICTS = ["accepted", "rejected", "released", "modified"];
|
|
103
|
+
var CONFIRMATION_CLOSES = ["sender_close", "sender_error"];
|
|
104
|
+
var ConfirmationHub = class {
|
|
105
|
+
constructor(sender, on, off) {
|
|
106
|
+
this.sender = sender;
|
|
107
|
+
this.on = on;
|
|
108
|
+
this.off = off;
|
|
109
|
+
}
|
|
110
|
+
sender;
|
|
111
|
+
on;
|
|
112
|
+
off;
|
|
113
|
+
pending = /* @__PURE__ */ new Set();
|
|
114
|
+
handlers = [];
|
|
115
|
+
add(confirmation) {
|
|
116
|
+
this.pending.add(confirmation);
|
|
117
|
+
if (this.handlers.length === 0) {
|
|
118
|
+
for (const name of [...CONFIRMATION_VERDICTS, ...CONFIRMATION_CLOSES]) {
|
|
119
|
+
const handler = (context) => this.dispatch(name, context);
|
|
120
|
+
this.handlers.push([name, handler]);
|
|
121
|
+
this.on(name, handler);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
remove(confirmation) {
|
|
126
|
+
this.pending.delete(confirmation);
|
|
127
|
+
if (this.pending.size === 0) {
|
|
128
|
+
for (const [name, handler] of this.handlers) this.off(name, handler);
|
|
129
|
+
this.handlers.length = 0;
|
|
130
|
+
hubs.delete(this.sender);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
dispatch(name, context) {
|
|
134
|
+
if (CONFIRMATION_CLOSES.includes(name)) {
|
|
135
|
+
for (const confirmation of [...this.pending]) {
|
|
136
|
+
confirmation.settle(new BabelQueueError(`The dead-letter link closed before the broker accepted the delivery (${name}).`));
|
|
137
|
+
}
|
|
138
|
+
this.forward(name, context);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const owners = [...this.pending].filter((confirmation) => confirmation.matches(context));
|
|
142
|
+
if (owners.length === 0) {
|
|
143
|
+
this.forward(name, context);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
for (const owner of owners) {
|
|
147
|
+
owner.settle(name === "accepted" ? void 0 : new BabelQueueError(`The broker did not accept the dead-letter delivery (${name}).`));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
forward(name, context) {
|
|
151
|
+
try {
|
|
152
|
+
this.sender.session?.dispatch?.(name, context);
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
var hubs = /* @__PURE__ */ new WeakMap();
|
|
158
|
+
function sendConfirmed(sender, message, timeoutMs) {
|
|
159
|
+
const on = sender.on?.bind(sender);
|
|
160
|
+
const off = sender.removeListener?.bind(sender);
|
|
161
|
+
if (!on || !off) return Promise.resolve(sender.send(message)).then(() => void 0);
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
let hub = hubs.get(sender);
|
|
164
|
+
if (!hub) {
|
|
165
|
+
hub = new ConfirmationHub(sender, on, off);
|
|
166
|
+
hubs.set(sender, hub);
|
|
167
|
+
}
|
|
168
|
+
const activeHub = hub;
|
|
169
|
+
let finished = false;
|
|
170
|
+
const confirmation = {
|
|
171
|
+
delivery: void 0,
|
|
172
|
+
matches: (context) => confirmation.delivery === void 0 || context?.delivery === void 0 || context.delivery === confirmation.delivery,
|
|
173
|
+
settle: (error) => {
|
|
174
|
+
if (finished) return;
|
|
175
|
+
finished = true;
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
activeHub.remove(confirmation);
|
|
178
|
+
if (error) reject(error);
|
|
179
|
+
else resolve();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
activeHub.add(confirmation);
|
|
183
|
+
const timer = setTimeout(
|
|
184
|
+
() => confirmation.settle(new BabelQueueError(`Timed out after ${timeoutMs}ms waiting for the broker to accept the dead-letter delivery.`)),
|
|
185
|
+
timeoutMs
|
|
186
|
+
);
|
|
187
|
+
try {
|
|
188
|
+
confirmation.delivery = sender.send(message);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
confirmation.settle(error instanceof Error ? error : new BabelQueueError(String(error)));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const delivery = confirmation.delivery;
|
|
194
|
+
if (delivery === null || typeof delivery !== "object" || delivery.settled === true) confirmation.settle();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
92
197
|
var ArtemisConsumer = class {
|
|
93
198
|
constructor(handlers, options = {}) {
|
|
94
199
|
this.handlers = handlers;
|
|
@@ -108,11 +213,27 @@ var ArtemisConsumer = class {
|
|
|
108
213
|
async handle(context) {
|
|
109
214
|
const message = context.message ?? {};
|
|
110
215
|
const delivery = context.delivery;
|
|
216
|
+
const unsupported = unsupportedSchemaVersion(message);
|
|
217
|
+
if (unsupported !== void 0) {
|
|
218
|
+
const gateError = new BabelQueueError(
|
|
219
|
+
`Rejected a message from Artemis with unsupported bq_schema_version "${unsupported}" (supported: ${SCHEMA_VERSION}).`
|
|
220
|
+
);
|
|
221
|
+
try {
|
|
222
|
+
await this.deadLetterRawConfirmed(messageBody(message));
|
|
223
|
+
} catch (dlqError) {
|
|
224
|
+
this.safeReport(gateError, null, context);
|
|
225
|
+
this.safeReport(dlqError, null, context);
|
|
226
|
+
await this.releaseAfterBackoff(delivery);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
delivery?.accept();
|
|
230
|
+
this.safeReport(gateError, null, context);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
111
233
|
const decoded = EnvelopeCodec.decode(messageBody(message));
|
|
112
234
|
if (!EnvelopeCodec.accepts(decoded)) {
|
|
113
235
|
this.report(new BabelQueueError("Rejected a non-conformant BabelQueue envelope from Artemis."), decoded, context);
|
|
114
|
-
await this.deadLetterRaw(messageBody(message));
|
|
115
|
-
delivery?.accept();
|
|
236
|
+
await this.settleAfterDeadLetter(context, decoded, () => this.deadLetterRaw(messageBody(message)));
|
|
116
237
|
return;
|
|
117
238
|
}
|
|
118
239
|
const envelope = this.reconcile(decoded, message);
|
|
@@ -143,8 +264,7 @@ var ArtemisConsumer = class {
|
|
|
143
264
|
delivery?.accept();
|
|
144
265
|
return;
|
|
145
266
|
case UnknownUrnStrategy.DEAD_LETTER:
|
|
146
|
-
await this.deadLetter(envelope, "unknown_urn");
|
|
147
|
-
delivery?.accept();
|
|
267
|
+
await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "unknown_urn"));
|
|
148
268
|
return;
|
|
149
269
|
case UnknownUrnStrategy.RELEASE:
|
|
150
270
|
delivery?.release({ delivery_failed: true });
|
|
@@ -161,12 +281,26 @@ var ArtemisConsumer = class {
|
|
|
161
281
|
if ((envelope.attempts ?? 0) + 1 < this.maxTries) {
|
|
162
282
|
delivery?.release({ delivery_failed: true });
|
|
163
283
|
} else if (this.options.deadLetterSender) {
|
|
164
|
-
await this.deadLetter(envelope, "failed", error);
|
|
165
|
-
delivery?.accept();
|
|
284
|
+
await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "failed", error));
|
|
166
285
|
} else {
|
|
167
286
|
delivery?.accept();
|
|
168
287
|
}
|
|
169
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry
|
|
291
|
+
* delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender
|
|
292
|
+
* the write is a no-op and the message is accepted (terminal drop), as before.
|
|
293
|
+
*/
|
|
294
|
+
async settleAfterDeadLetter(context, envelope, write) {
|
|
295
|
+
try {
|
|
296
|
+
await write();
|
|
297
|
+
} catch (dlqError) {
|
|
298
|
+
this.safeReport(dlqError, envelope, context);
|
|
299
|
+
await this.releaseAfterBackoff(context.delivery);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
context.delivery?.accept();
|
|
303
|
+
}
|
|
170
304
|
async deadLetter(envelope, reason, error) {
|
|
171
305
|
const sender = this.options.deadLetterSender;
|
|
172
306
|
if (!sender) return;
|
|
@@ -176,12 +310,30 @@ var ArtemisConsumer = class {
|
|
|
176
310
|
error: error instanceof Error ? error.message : null,
|
|
177
311
|
exception: error instanceof Error ? error.name : null
|
|
178
312
|
});
|
|
179
|
-
await sender
|
|
313
|
+
await sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
|
|
314
|
+
}
|
|
315
|
+
/** Poison path: forward the raw body, confirmed like the gate path. */
|
|
316
|
+
deadLetterRaw(raw) {
|
|
317
|
+
return this.deadLetterRawConfirmed(raw);
|
|
180
318
|
}
|
|
181
|
-
|
|
319
|
+
/** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */
|
|
320
|
+
async deadLetterRawConfirmed(raw) {
|
|
182
321
|
const sender = this.options.deadLetterSender;
|
|
183
322
|
if (!sender) return;
|
|
184
|
-
await sender
|
|
323
|
+
await sendConfirmed(sender, { body: raw }, this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
|
|
324
|
+
}
|
|
325
|
+
/** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
|
|
326
|
+
async releaseAfterBackoff(delivery) {
|
|
327
|
+
const delayMs = this.options.deadLetterRetryDelayMs ?? DEFAULT_DLQ_RETRY_DELAY_MS;
|
|
328
|
+
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs).unref?.());
|
|
329
|
+
delivery?.release();
|
|
330
|
+
}
|
|
331
|
+
/** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */
|
|
332
|
+
safeReport(error, envelope, context) {
|
|
333
|
+
try {
|
|
334
|
+
this.report(error, envelope, context);
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
185
337
|
}
|
|
186
338
|
report(error, envelope, context) {
|
|
187
339
|
this.options.onError?.(error, envelope, context);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache ActiveMQ Artemis adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **AMQP 1.0** consumer over Artemis (rhea),\n * so an Artemis-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the Java, .NET, Python and Go SDKs.\n *\n * Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —\n * per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so\n * this adapter maps onto them (the envelope stays `schema_version: 1`): the envelope JSON is the\n * message **body**; the contract fields are mirrored onto the AMQP a JMS peer reads\n * (`correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN) plus the `bq_` application properties; consume settles per message\n * (`accept` after success, `release` for redelivery); **`attempts = max(body, delivery-count)`**\n * (the AMQP counter is 0-based — no −1); terminal failures go to an opt-in `<queue>.dlq` with a\n * `dead_letter` block.\n *\n * This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide\n * the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional AMQP `application_properties` beside the contract `bq_`\n * properties — the contract properties win a key collision (merge-not-clobber), blanks dropped.\n * `traceparent`/`tracestate` are W3C keys with no hyphens, so they are already JMS-legal (§7 uses\n * underscores because JMS property names must be Java identifiers). On consume the inbound\n * `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract\n * sees the `traceparent` and links the consumer span as a child. A header-less publish is\n * byte-identical. GR-1: the wire envelope body is never touched.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { sanitizeHeaders } from \"./headers.js\";\n\n// --- Minimal AMQP 1.0 shapes (a structural subset of rhea) ---------------------\n\n/** An AMQP message (structural subset of a rhea message). */\nexport interface AmqpMessage {\n body?: unknown;\n correlation_id?: unknown;\n creation_time?: number | Date;\n content_type?: string;\n application_properties?: Record<string, unknown>;\n message_annotations?: Record<string, unknown>;\n /** The AMQP header delivery-count (0-based: 0 on first delivery). */\n delivery_count?: number;\n}\n\n/** The disposition handle rhea passes alongside a received message. */\nexport interface AmqpDelivery {\n /** Settle (acknowledge) the message — it is removed from the queue. */\n accept(): void;\n /** Return the message for redelivery — the broker increments `delivery-count`. */\n release(params?: { delivery_failed?: boolean; undeliverable_here?: boolean }): void;\n}\n\n/** The event context rhea emits on a `message` event. */\nexport interface AmqpEventContext {\n message?: AmqpMessage;\n delivery?: AmqpDelivery;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n send(message: AmqpMessage): unknown;\n}\n\n/** The subset of a rhea receiver this adapter wires to (its `message` event). */\nexport interface AmqpReceiver {\n on(event: \"message\", handler: (context: AmqpEventContext) => void): unknown;\n}\n\n// --- Projection (contract §7.2) ------------------------------------------------\n\n/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */\nexport const JMS_TYPE_KEY = \"x-opt-jms-type\";\n/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */\nexport const SCHEDULED_DELIVERY_KEY = \"x-opt-delivery-time\";\n\n/**\n * Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,\n * `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`\n * sets the `x-opt-delivery-time` annotation for native scheduled delivery.\n */\nexport function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage {\n const annotations: Record<string, unknown> = {};\n if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;\n\n // Out-of-band riders (e.g. traceparent) go in first; the contract bq_ properties below win any\n // key collision (merge-not-clobber). The bq_ names use underscores, not hyphens: a JMS property\n // name must be a valid Java identifier, and every Artemis SDK uses the same JMS-legal form for\n // cross-protocol parity. traceparent/tracestate have no hyphens, so they are already JMS-legal.\n const applicationProperties: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;\n applicationProperties[\"bq_schema_version\"] = String(envelope.meta.schema_version);\n applicationProperties[\"bq_attempts\"] = String(envelope.attempts ?? 0);\n applicationProperties[\"bq_app_id\"] = \"babelqueue\";\n if (envelope.meta.lang) applicationProperties[\"bq_source_lang\"] = envelope.meta.lang;\n\n const message: AmqpMessage = {\n body: EnvelopeCodec.encode(envelope),\n content_type: \"application/json\",\n application_properties: applicationProperties,\n message_annotations: annotations,\n };\n if (envelope.trace_id) message.correlation_id = envelope.trace_id;\n if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;\n\n if (delayMs != null && delayMs > 0) {\n applicationProperties[\"bq_delay\"] = String(delayMs);\n annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;\n }\n return message;\n}\n\n/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */\nexport function messageBody(message: AmqpMessage): string {\n const body = message.body;\n if (body == null) return \"\";\n if (typeof body === \"string\") return body;\n if (Buffer.isBuffer(body)) return body.toString(\"utf8\");\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content.toString(\"utf8\");\n if (typeof content === \"string\") return content;\n }\n return String(body);\n}\n\nfunction jmsType(message: AmqpMessage): string | undefined {\n const value = message.message_annotations?.[JMS_TYPE_KEY];\n return value == null ? undefined : String(value);\n}\n\nfunction deliveryCount(message: AmqpMessage): number {\n const value = message.delivery_count;\n return typeof value === \"number\" && value > 0 ? value : 0;\n}\n\n/**\n * Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying\n * values defensively. Returns an empty object when there are none. Both the contract `bq_`\n * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract\n * reads only the keys it knows.\n */\nexport function headersOf(message: AmqpMessage): HeaderCarrier {\n const props = message.application_properties;\n const out: HeaderCarrier = {};\n if (!props) return out;\n for (const key of Object.keys(props)) {\n const value = props[key];\n if (value == null) continue;\n const s = String(value);\n if (s !== \"\") out[key] = s;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link ArtemisPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`\n * properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract properties win a key collision; an empty/omitted carrier leaves the message\n * unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */\nexport class ArtemisPublisher {\n private constructor(\n private readonly sender: AmqpSender,\n private readonly queue: string,\n ) {}\n\n /** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */\n static create(sender: AmqpSender, queue: string): ArtemisPublisher {\n return new ArtemisPublisher(sender, queue);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });\n await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band\n * {@link HeaderCarrier} read from the message's `application_properties` (empty when there are\n * none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a\n * child of the producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link ArtemisConsumer}. */\nexport interface ArtemisConsumerOptions {\n /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */\n deadLetterSender?: AmqpSender;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a non-conformant message, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;\n}\n\n/**\n * Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the\n * handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A\n * throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP\n * `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a\n * `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP\n * counter is 0-based, so no −1.\n */\nexport class ArtemisConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly handlers: BabelHandlers,\n private readonly options: ArtemisConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */\n listen(receiver: AmqpReceiver): void {\n receiver.on(\"message\", (context) => this.handle(context));\n }\n\n /** Route + settle one delivery. Exposed for testing. */\n async handle(context: AmqpEventContext): Promise<void> {\n const message = context.message ?? {};\n const delivery = context.delivery;\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n // A non-conformant / poison message may lack the fields to annotate; forward it raw.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\"), decoded, context);\n await this.deadLetterRaw(messageBody(message));\n delivery?.accept();\n return;\n }\n\n const envelope = this.reconcile(decoded, message);\n const urn = jmsType(message) ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(context, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n delivery?.accept();\n } catch (error) {\n this.report(error, envelope, context);\n await this.retryOrDeadLetter(context, envelope, error);\n }\n }\n\n /** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */\n private reconcile(envelope: IncomingEnvelope, message: AmqpMessage): Envelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n const count = deliveryCount(message);\n return { ...envelope, attempts: Math.max(body, count) } as Envelope;\n }\n\n private async onUnknownUrn(context: AmqpEventContext, envelope: Envelope, urn: string): Promise<void> {\n const delivery = context.delivery;\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n delivery?.accept();\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope, \"unknown_urn\");\n delivery?.accept();\n return;\n case UnknownUrnStrategy.RELEASE:\n delivery?.release({ delivery_failed: true });\n return;\n default: {\n // FAIL: surface and do NOT settle — the broker redelivers, then dead-letters.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, context);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(context: AmqpEventContext, envelope: Envelope, error: unknown): Promise<void> {\n const delivery = context.delivery;\n if ((envelope.attempts ?? 0) + 1 < this.maxTries) {\n // Release with delivery-failed leaves it for redelivery — the broker bumps delivery-count.\n delivery?.release({ delivery_failed: true });\n } else if (this.options.deadLetterSender) {\n await this.deadLetter(envelope, \"failed\", error);\n delivery?.accept();\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n private async deadLetter(envelope: Envelope, reason: string, error?: unknown): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n const original = envelope.meta?.queue ?? \"\";\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n await sender.send(artemisMessage(annotated));\n }\n\n private async deadLetterRaw(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sender.send({ body: raw });\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n this.options.onError?.(error, envelope, context);\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache ActiveMQ Artemis adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **AMQP 1.0** consumer over Artemis (rhea),\n * so an Artemis-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the Java, .NET, Python and Go SDKs.\n *\n * Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —\n * per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so\n * this adapter maps onto them (the envelope stays `schema_version: 1`): the envelope JSON is the\n * message **body**; the contract fields are mirrored onto the AMQP a JMS peer reads\n * (`correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN) plus the `bq_` application properties; consume settles per message\n * (`accept` after success, `release` for redelivery); **`attempts = max(body, delivery-count)`**\n * (the AMQP counter is 0-based — no −1); terminal failures go to an opt-in `<queue>.dlq` with a\n * `dead_letter` block.\n *\n * This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide\n * the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional AMQP `application_properties` beside the contract `bq_`\n * properties — the contract properties win a key collision (merge-not-clobber), blanks dropped.\n * `traceparent`/`tracestate` are W3C keys with no hyphens, so they are already JMS-legal (§7 uses\n * underscores because JMS property names must be Java identifiers). On consume the inbound\n * `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract\n * sees the `traceparent` and links the consumer span as a child. A header-less publish is\n * byte-identical. GR-1: the wire envelope body is never touched.\n *\n * **Version gate (§7.7).** Before decoding the body the consumer reads the `bq_schema_version`\n * application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII\n * whitespace) follows the normal decode path; any other value is compared, untrimmed, to the supported\n * `\"1\"`, and a message that does not match is rejected undecoded — reported to `onError`, the raw body\n * forwarded to the DLQ sender (when configured) and the message accepted **only after the broker has\n * accepted the DLQ delivery**. If the DLQ send fails, is refused (rejected/released/modified), the link\n * closes or `deadLetterTimeoutMs` elapses, the failure is reported and, after `deadLetterRetryDelayMs`,\n * the source message gets a plain `released` outcome (no `delivery_failed`: the broker's delivery\n * counter and `max-delivery-attempts` stay untouched) — never accepted, never handed to a handler.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { sanitizeHeaders } from \"./headers.js\";\n\n// --- Minimal AMQP 1.0 shapes (a structural subset of rhea) ---------------------\n\n/** An AMQP message (structural subset of a rhea message). */\nexport interface AmqpMessage {\n body?: unknown;\n correlation_id?: unknown;\n creation_time?: number | Date;\n content_type?: string;\n application_properties?: Record<string, unknown>;\n message_annotations?: Record<string, unknown>;\n /** The AMQP header delivery-count (0-based: 0 on first delivery). */\n delivery_count?: number;\n}\n\n/** The disposition handle rhea passes alongside a received message. */\nexport interface AmqpDelivery {\n /** Settle (acknowledge) the message — it is removed from the queue. */\n accept(): void;\n /** Return the message for redelivery — the broker increments `delivery-count`. */\n release(params?: { delivery_failed?: boolean; undeliverable_here?: boolean }): void;\n}\n\n/** The event context rhea emits on a `message` event. */\nexport interface AmqpEventContext {\n message?: AmqpMessage;\n delivery?: AmqpDelivery;\n}\n\n/**\n * The context rhea passes to a sender link event (`accepted`, `rejected`, `sender_close`, ...). Only\n * `delivery` is inspected (to match a verdict to its delivery); everything else is opaque. Deliberately\n * no index signature: an interface-typed handler parameter (e.g. rhea's `EventContext`) must stay assignable.\n */\nexport interface AmqpLinkEventContext {\n delivery?: unknown;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n /** Queue a delivery. rhea returns the delivery synchronously; the broker's verdict arrives later as an event. */\n send(message: AmqpMessage): unknown;\n /**\n * Optional (a rhea sender has it). When both `on` and `removeListener` exist, the consumer's\n * version-gate path waits for the broker's `accepted` disposition on the DLQ delivery before it\n * settles the source message; without them only a synchronous `send` failure is observable.\n */\n on?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;\n removeListener?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;\n /**\n * Optional (a rhea link has it). rhea does not bubble an event to the session/connection while the\n * link has a listener for it, so the temporary confirmation listeners re-dispatch what is not theirs\n * (other deliveries' dispositions, link close/error) here.\n */\n session?: { dispatch?(event: string, context?: AmqpLinkEventContext): unknown };\n}\n\n/** The subset of a rhea receiver this adapter wires to (its `message` event). */\nexport interface AmqpReceiver {\n on(event: \"message\", handler: (context: AmqpEventContext) => void): unknown;\n}\n\n// --- Projection (contract §7.2) ------------------------------------------------\n\n/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */\nexport const JMS_TYPE_KEY = \"x-opt-jms-type\";\n/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */\nexport const SCHEDULED_DELIVERY_KEY = \"x-opt-delivery-time\";\n\n/**\n * Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,\n * `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`\n * sets the `x-opt-delivery-time` annotation for native scheduled delivery.\n */\nexport function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage {\n const annotations: Record<string, unknown> = {};\n if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;\n\n // Out-of-band riders (e.g. traceparent) go in first; the contract bq_ properties below win any\n // key collision (merge-not-clobber). The bq_ names use underscores, not hyphens: a JMS property\n // name must be a valid Java identifier, and every Artemis SDK uses the same JMS-legal form for\n // cross-protocol parity. traceparent/tracestate have no hyphens, so they are already JMS-legal.\n const applicationProperties: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;\n applicationProperties[\"bq_schema_version\"] = String(envelope.meta.schema_version);\n applicationProperties[\"bq_attempts\"] = String(envelope.attempts ?? 0);\n applicationProperties[\"bq_app_id\"] = \"babelqueue\";\n if (envelope.meta.lang) applicationProperties[\"bq_source_lang\"] = envelope.meta.lang;\n\n const message: AmqpMessage = {\n body: EnvelopeCodec.encode(envelope),\n content_type: \"application/json\",\n application_properties: applicationProperties,\n message_annotations: annotations,\n };\n if (envelope.trace_id) message.correlation_id = envelope.trace_id;\n if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;\n\n if (delayMs != null && delayMs > 0) {\n applicationProperties[\"bq_delay\"] = String(delayMs);\n annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;\n }\n return message;\n}\n\n/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */\nexport function messageBody(message: AmqpMessage): string {\n const body = message.body;\n if (body == null) return \"\";\n if (typeof body === \"string\") return body;\n if (Buffer.isBuffer(body)) return body.toString(\"utf8\");\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content.toString(\"utf8\");\n if (typeof content === \"string\") return content;\n }\n return String(body);\n}\n\nfunction jmsType(message: AmqpMessage): string | undefined {\n const value = message.message_annotations?.[JMS_TYPE_KEY];\n return value == null ? undefined : String(value);\n}\n\n/** Blank = empty or only ASCII whitespace (GR-5: the same set in every SDK, never `String.trim()`). */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The inbound `bq_schema_version` application property when it is present, non-blank and not exactly\n * the supported version (compared untrimmed), else `undefined` (the message may be decoded).\n */\nfunction unsupportedSchemaVersion(message: AmqpMessage): string | undefined {\n const raw = message.application_properties?.[\"bq_schema_version\"];\n if (raw == null) return undefined;\n const value = Buffer.isBuffer(raw) ? raw.toString(\"utf8\") : String(raw);\n if (BLANK_ASCII.test(value)) return undefined;\n return value === String(SCHEMA_VERSION) ? undefined : value;\n}\n\nfunction deliveryCount(message: AmqpMessage): number {\n const value = message.delivery_count;\n return typeof value === \"number\" && value > 0 ? value : 0;\n}\n\n/**\n * Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying\n * values defensively. Returns an empty object when there are none. Both the contract `bq_`\n * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract\n * reads only the keys it knows.\n */\nexport function headersOf(message: AmqpMessage): HeaderCarrier {\n const props = message.application_properties;\n const out: HeaderCarrier = {};\n if (!props) return out;\n for (const key of Object.keys(props)) {\n const value = props[key];\n if (value == null) continue;\n const s = String(value);\n if (s !== \"\") out[key] = s;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link ArtemisPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`\n * properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract properties win a key collision; an empty/omitted carrier leaves the message\n * unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */\nexport class ArtemisPublisher {\n private constructor(\n private readonly sender: AmqpSender,\n private readonly queue: string,\n ) {}\n\n /** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */\n static create(sender: AmqpSender, queue: string): ArtemisPublisher {\n return new ArtemisPublisher(sender, queue);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });\n await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));\n return envelope.meta.id;\n }\n}\n\n// --- DLQ delivery confirmation ---------------------------------------------------\n\n/** Default wait (ms) for the broker to settle a gate-path DLQ delivery. */\nconst DEFAULT_DLQ_CONFIRM_TIMEOUT_MS = 30_000;\n/** Default pause (ms) before a gate message whose DLQ write failed is released back to the broker. */\nconst DEFAULT_DLQ_RETRY_DELAY_MS = 1_000;\n\n/** One in-flight DLQ confirmation registered on a {@link ConfirmationHub}. */\ninterface PendingConfirmation {\n /** The delivery `send` returned; `undefined` while `send` is still running synchronously. */\n delivery: unknown;\n /** `true` when `context` is (or may be) this confirmation's verdict. */\n matches(context: AmqpLinkEventContext | undefined): boolean;\n /** Resolve (no argument) or reject the confirmation. */\n settle(error?: Error): void;\n}\n\nconst CONFIRMATION_VERDICTS = [\"accepted\", \"rejected\", \"released\", \"modified\"] as const;\nconst CONFIRMATION_CLOSES = [\"sender_close\", \"sender_error\"] as const;\n\n/**\n * One shared set of temporary sender listeners per {@link AmqpSender}, however many DLQ confirmations\n * are in flight. rhea bubbles a link event to the session only while the link has no listener for it,\n * so while confirmations are pending the hub owns the listeners and re-dispatches every event it does\n * not consume to `sender.session` exactly once (per-confirmation listeners would forward each event\n * once per pending confirmation). The listeners are removed as soon as no confirmation is pending.\n */\nclass ConfirmationHub {\n private readonly pending = new Set<PendingConfirmation>();\n private readonly handlers: [string, (context: AmqpLinkEventContext) => void][] = [];\n\n constructor(\n private readonly sender: AmqpSender,\n private readonly on: (event: string, handler: (context: AmqpLinkEventContext) => void) => unknown,\n private readonly off: (event: string, handler: (context: AmqpLinkEventContext) => void) => unknown,\n ) {}\n\n add(confirmation: PendingConfirmation): void {\n this.pending.add(confirmation);\n if (this.handlers.length === 0) {\n for (const name of [...CONFIRMATION_VERDICTS, ...CONFIRMATION_CLOSES]) {\n const handler = (context: AmqpLinkEventContext): void => this.dispatch(name, context);\n this.handlers.push([name, handler]);\n this.on(name, handler);\n }\n }\n }\n\n remove(confirmation: PendingConfirmation): void {\n this.pending.delete(confirmation);\n if (this.pending.size === 0) {\n for (const [name, handler] of this.handlers) this.off(name, handler);\n this.handlers.length = 0;\n hubs.delete(this.sender);\n }\n }\n\n private dispatch(name: string, context: AmqpLinkEventContext): void {\n if ((CONFIRMATION_CLOSES as readonly string[]).includes(name)) {\n for (const confirmation of [...this.pending]) {\n confirmation.settle(new BabelQueueError(`The dead-letter link closed before the broker accepted the delivery (${name}).`));\n }\n this.forward(name, context);\n return;\n }\n const owners = [...this.pending].filter((confirmation) => confirmation.matches(context));\n if (owners.length === 0) {\n this.forward(name, context);\n return;\n }\n for (const owner of owners) {\n owner.settle(name === \"accepted\" ? undefined : new BabelQueueError(`The broker did not accept the dead-letter delivery (${name}).`));\n }\n }\n\n private forward(name: string, context: AmqpLinkEventContext): void {\n try {\n this.sender.session?.dispatch?.(name, context);\n } catch {\n // forwarding is best effort; never let it change a confirmation outcome\n }\n }\n}\n\nconst hubs = new WeakMap<AmqpSender, ConfirmationHub>();\n\n/**\n * Send `message` and resolve only once the broker has `accepted` the delivery. A rhea `send` returns\n * synchronously and reports the verdict (`accepted` / `rejected` / `released` / `modified`) as a\n * sender event, so awaiting `send` alone cannot see a broker refusal. Rejects on any non-accepted\n * disposition, a link close/error, or when `timeoutMs` elapses (an unsettled delivery that is lost on\n * reconnect raises no event). A sender without event support degrades to awaiting `send` itself.\n * Events that are not this delivery's verdict (other deliveries, link close/error) are re-dispatched on\n * the sender's session, once per event, so the application still sees them (rhea suppresses bubbling\n * while the link has a listener) — see {@link ConfirmationHub}.\n */\nfunction sendConfirmed(sender: AmqpSender, message: AmqpMessage, timeoutMs: number): Promise<void> {\n const on = sender.on?.bind(sender);\n const off = sender.removeListener?.bind(sender);\n if (!on || !off) return Promise.resolve(sender.send(message)).then(() => undefined);\n\n return new Promise<void>((resolve, reject) => {\n let hub = hubs.get(sender);\n if (!hub) {\n hub = new ConfirmationHub(sender, on, off);\n hubs.set(sender, hub);\n }\n const activeHub = hub;\n let finished = false;\n\n const confirmation: PendingConfirmation = {\n delivery: undefined,\n matches: (context) =>\n confirmation.delivery === undefined || context?.delivery === undefined || context.delivery === confirmation.delivery,\n settle: (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timer);\n activeHub.remove(confirmation);\n if (error) reject(error);\n else resolve();\n },\n };\n activeHub.add(confirmation);\n\n // `settle` (above) reads `timer`; it cannot run before this line (nothing is sent or listened to yet).\n const timer = setTimeout(\n () => confirmation.settle(new BabelQueueError(`Timed out after ${timeoutMs}ms waiting for the broker to accept the dead-letter delivery.`)),\n timeoutMs,\n );\n try {\n confirmation.delivery = sender.send(message);\n } catch (error) {\n confirmation.settle(error instanceof Error ? error : new BabelQueueError(String(error)));\n return;\n }\n // A pre-settled delivery (or a sender that returns none) raises no disposition event.\n const delivery = confirmation.delivery;\n if (delivery === null || typeof delivery !== \"object\" || (delivery as { settled?: unknown }).settled === true) confirmation.settle();\n });\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band\n * {@link HeaderCarrier} read from the message's `application_properties` (empty when there are\n * none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a\n * child of the producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link ArtemisConsumer}. */\nexport interface ArtemisConsumerOptions {\n /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */\n deadLetterSender?: AmqpSender;\n /**\n * How long (ms) every dead-letter path (version gate, poison, unknown-URN `dead_letter`, terminal\n * handler failure) waits for the broker to accept the DLQ delivery before it releases the source\n * message instead of accepting it (default 30000).\n */\n deadLetterTimeoutMs?: number;\n /**\n * Pause (ms) before a dead-letter path releases a message whose DLQ write failed (default 1000).\n * The release is a plain `released` outcome (no `delivery_failed`, so the broker's delivery counter and\n * `max-delivery-attempts` are untouched); the pause keeps a persistently failing DLQ from becoming a\n * hot redelivery loop. The delivery stays unsettled (holding one credit) while it waits.\n * Note: on the terminal handler-failure path the message has already been through the handler, and\n * a plain release does not bump the delivery counter, so the redelivery runs the handler again (and\n * reports its failure again) until the DLQ write succeeds.\n */\n deadLetterRetryDelayMs?: number;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a non-conformant message, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;\n}\n\n/**\n * Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the\n * handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A\n * throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP\n * `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a\n * `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP\n * counter is 0-based, so no −1.\n */\nexport class ArtemisConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly handlers: BabelHandlers,\n private readonly options: ArtemisConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */\n listen(receiver: AmqpReceiver): void {\n receiver.on(\"message\", (context) => this.handle(context));\n }\n\n /** Route + settle one delivery. Exposed for testing. */\n async handle(context: AmqpEventContext): Promise<void> {\n const message = context.message ?? {};\n const delivery = context.delivery;\n\n // Version gate (§7.7): decided on the application property alone, before the body is decoded.\n const unsupported = unsupportedSchemaVersion(message);\n if (unsupported !== undefined) {\n const gateError = new BabelQueueError(\n `Rejected a message from Artemis with unsupported bq_schema_version \"${unsupported}\" (supported: ${SCHEMA_VERSION}).`,\n );\n // Accept only after the broker has accepted the DLQ copy; otherwise release (plain, after a pause) so\n // the message is never lost and never reaches a handler. Reports come after the settlement, or are\n // isolated, so a throwing onError can never leave the message unsettled.\n try {\n await this.deadLetterRawConfirmed(messageBody(message));\n } catch (dlqError) {\n this.safeReport(gateError, null, context);\n this.safeReport(dlqError, null, context);\n await this.releaseAfterBackoff(delivery);\n return;\n }\n delivery?.accept();\n this.safeReport(gateError, null, context);\n return;\n }\n\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n // A non-conformant / poison message may lack the fields to annotate; forward it raw.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\"), decoded, context);\n await this.settleAfterDeadLetter(context, decoded, () => this.deadLetterRaw(messageBody(message)));\n return;\n }\n\n const envelope = this.reconcile(decoded, message);\n const urn = jmsType(message) ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(context, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n delivery?.accept();\n } catch (error) {\n this.report(error, envelope, context);\n await this.retryOrDeadLetter(context, envelope, error);\n }\n }\n\n /** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */\n private reconcile(envelope: IncomingEnvelope, message: AmqpMessage): Envelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n const count = deliveryCount(message);\n return { ...envelope, attempts: Math.max(body, count) } as Envelope;\n }\n\n private async onUnknownUrn(context: AmqpEventContext, envelope: Envelope, urn: string): Promise<void> {\n const delivery = context.delivery;\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n delivery?.accept();\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, \"unknown_urn\"));\n return;\n case UnknownUrnStrategy.RELEASE:\n delivery?.release({ delivery_failed: true });\n return;\n default: {\n // FAIL: surface and do NOT settle — the broker redelivers, then dead-letters.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, context);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(context: AmqpEventContext, envelope: Envelope, error: unknown): Promise<void> {\n const delivery = context.delivery;\n if ((envelope.attempts ?? 0) + 1 < this.maxTries) {\n // Release with delivery-failed leaves it for redelivery — the broker bumps delivery-count.\n delivery?.release({ delivery_failed: true });\n } else if (this.options.deadLetterSender) {\n await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, \"failed\", error));\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n /**\n * Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry\n * delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender\n * the write is a no-op and the message is accepted (terminal drop), as before.\n */\n private async settleAfterDeadLetter(\n context: AmqpEventContext,\n envelope: IncomingEnvelope | null,\n write: () => Promise<void>,\n ): Promise<void> {\n try {\n await write();\n } catch (dlqError) {\n this.safeReport(dlqError, envelope, context);\n await this.releaseAfterBackoff(context.delivery);\n return;\n }\n context.delivery?.accept();\n }\n\n private async deadLetter(envelope: Envelope, reason: string, error?: unknown): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n const original = envelope.meta?.queue ?? \"\";\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n await sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);\n }\n\n /** Poison path: forward the raw body, confirmed like the gate path. */\n private deadLetterRaw(raw: string): Promise<void> {\n return this.deadLetterRawConfirmed(raw);\n }\n\n /** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */\n private async deadLetterRawConfirmed(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sendConfirmed(sender, { body: raw }, this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);\n }\n\n /** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */\n private async releaseAfterBackoff(delivery: AmqpDelivery | undefined): Promise<void> {\n const delayMs = this.options.deadLetterRetryDelayMs ?? DEFAULT_DLQ_RETRY_DELAY_MS;\n if (delayMs > 0) await new Promise<void>((resolve) => setTimeout(resolve, delayMs).unref?.());\n delivery?.release();\n }\n\n /** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */\n private safeReport(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n try {\n this.report(error, envelope, context);\n } catch {\n // swallowed by design\n }\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n this.options.onError?.(error, envelope, context);\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@babelqueue/artemis",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Apache ActiveMQ Artemis adapter for BabelQueue — canonical-envelope publisher and a URN-routed AMQP 1.0 consumer (accept/release settlement, native scheduled delivery, cross-language DLQ) over rhea.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"babelqueue",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"directory": "packages/artemis"
|
|
24
24
|
},
|
|
25
25
|
"license": "MIT",
|
|
26
|
-
"author": "Muhammet Şafak <info@muhammetsafak.com
|
|
26
|
+
"author": "Muhammet Şafak <info@muhammetsafak.com>",
|
|
27
27
|
"type": "module",
|
|
28
28
|
"exports": {
|
|
29
29
|
".": {
|