@babelqueue/artemis 1.0.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/dist/index.cjs +197 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +96 -5
- package/dist/index.d.ts +96 -5
- package/dist/index.js +197 -18
- package/dist/index.js.map +1 -1
- package/package.json +17 -5
package/README.md
CHANGED
|
@@ -78,6 +78,22 @@ redelivers and bumps `delivery-count`); once `maxTries` is reached the envelope
|
|
|
78
78
|
(falling back to the body URN), so it never decodes a message it cannot handle. Unknown-URN
|
|
79
79
|
strategy is one of `fail` / `delete` / `release` / `dead_letter`.
|
|
80
80
|
|
|
81
|
+
**Version gate (§7.7).** Before the body is decoded the consumer reads the `bq_schema_version`
|
|
82
|
+
application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII
|
|
83
|
+
whitespace) follows the normal decode path; any other value is compared as-is (never trimmed) to
|
|
84
|
+
`"1"`. A message that does not match is rejected undecoded: reported to `onError`, its raw body
|
|
85
|
+
forwarded to the `deadLetterSender` (when configured), then `accept`ed — but only **after the broker
|
|
86
|
+
has accepted the DLQ delivery** (rhea `accepted` event on the DLQ sender). If the DLQ delivery is
|
|
87
|
+
rejected/released/modified, the link closes, `send` throws, or `deadLetterTimeoutMs` (default 30000)
|
|
88
|
+
elapses, the failure is reported to `onError` and the source message gets a plain `release()` instead —
|
|
89
|
+
after `deadLetterRetryDelayMs` (default 1000) so a persistently failing DLQ is not a hot redelivery
|
|
90
|
+
loop. It is never accepted and never reaches a handler. The release deliberately omits
|
|
91
|
+
`delivery_failed`: that would bump Artemis's delivery counter and, once `max-delivery-attempts` is hit,
|
|
92
|
+
route the message to the broker's DLA (or delete it when no DLA is configured). While waiting, the
|
|
93
|
+
delivery stays unsettled and holds one receiver credit. The DLQ confirmation listens on the DLQ
|
|
94
|
+
sender only for the duration of each gate send and re-dispatches events that are not its own to the
|
|
95
|
+
sender's session.
|
|
96
|
+
|
|
81
97
|
> `autoaccept: false` is required — the consumer owns the disposition. `handle(context)` is also
|
|
82
98
|
> exposed directly for testing or a custom event wiring.
|
|
83
99
|
|
|
@@ -100,6 +116,24 @@ The `bq-` application-property values are strings (integers as decimal, e.g. `"1
|
|
|
100
116
|
is `"babelqueue"`. The envelope is unchanged (`schema_version` stays `1`); Artemis is purely
|
|
101
117
|
additive.
|
|
102
118
|
|
|
119
|
+
## OpenTelemetry tracing (ADR-0028)
|
|
120
|
+
|
|
121
|
+
Cross-hop **span** linkage rides on the out-of-band `HeaderCarrier` from
|
|
122
|
+
[`@babelqueue/core@^1.4.0`](https://www.npmjs.com/package/@babelqueue/core). Pass the carrier produced by
|
|
123
|
+
`@babelqueue/core/otel`'s `publish` to this adapter's `publish({ headers })`; it is carried on
|
|
124
|
+
AMQP `application_properties`, beside the contract `bq_` properties (the contract wins a key collision; `traceparent`/`tracestate` are hyphen-free, so JMS-legal). On consume, the consumer surfaces a delivered message's headers to the handler's third argument (and a `headersOf(...)` extractor reads them back),
|
|
125
|
+
so the core's `otel` `wrapHandler` starts the consumer span as a true **child** of the producer span.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { trace } from "@opentelemetry/api";
|
|
129
|
+
import { publish as tracedPublish } from "@babelqueue/core/otel";
|
|
130
|
+
import type { HeaderCarrier } from "@babelqueue/core";
|
|
131
|
+
|
|
132
|
+
const headers: HeaderCarrier = {};
|
|
133
|
+
await tracedPublish(trace.getTracer("orders"), urn, data,
|
|
134
|
+
() => adapterPublish(urn, data, { headers }), { headers });
|
|
135
|
+
```
|
|
136
|
+
|
|
103
137
|
## License
|
|
104
138
|
|
|
105
139
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -25,20 +25,35 @@ __export(index_exports, {
|
|
|
25
25
|
JMS_TYPE_KEY: () => JMS_TYPE_KEY,
|
|
26
26
|
SCHEDULED_DELIVERY_KEY: () => SCHEDULED_DELIVERY_KEY,
|
|
27
27
|
artemisMessage: () => artemisMessage,
|
|
28
|
+
headersOf: () => headersOf,
|
|
28
29
|
messageBody: () => messageBody
|
|
29
30
|
});
|
|
30
31
|
module.exports = __toCommonJS(index_exports);
|
|
31
32
|
var import_core = require("@babelqueue/core");
|
|
33
|
+
|
|
34
|
+
// src/headers.ts
|
|
35
|
+
function sanitizeHeaders(headers) {
|
|
36
|
+
const out = {};
|
|
37
|
+
if (!headers) return out;
|
|
38
|
+
for (const key of Object.keys(headers)) {
|
|
39
|
+
const value = headers[key];
|
|
40
|
+
if (key === "" || value == null || value === "") continue;
|
|
41
|
+
out[key] = value;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/index.ts
|
|
32
47
|
var JMS_TYPE_KEY = "x-opt-jms-type";
|
|
33
48
|
var SCHEDULED_DELIVERY_KEY = "x-opt-delivery-time";
|
|
34
|
-
function artemisMessage(envelope, delayMs) {
|
|
49
|
+
function artemisMessage(envelope, delayMs, extra) {
|
|
35
50
|
const annotations = {};
|
|
36
51
|
if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;
|
|
37
|
-
const applicationProperties = {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
const applicationProperties = {};
|
|
53
|
+
for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;
|
|
54
|
+
applicationProperties["bq_schema_version"] = String(envelope.meta.schema_version);
|
|
55
|
+
applicationProperties["bq_attempts"] = String(envelope.attempts ?? 0);
|
|
56
|
+
applicationProperties["bq_app_id"] = "babelqueue";
|
|
42
57
|
if (envelope.meta.lang) applicationProperties["bq_source_lang"] = envelope.meta.lang;
|
|
43
58
|
const message = {
|
|
44
59
|
body: import_core.EnvelopeCodec.encode(envelope),
|
|
@@ -70,10 +85,30 @@ function jmsType(message) {
|
|
|
70
85
|
const value = message.message_annotations?.[JMS_TYPE_KEY];
|
|
71
86
|
return value == null ? void 0 : String(value);
|
|
72
87
|
}
|
|
88
|
+
var BLANK_ASCII = /^[ \t\n\v\f\r]*$/;
|
|
89
|
+
function unsupportedSchemaVersion(message) {
|
|
90
|
+
const raw = message.application_properties?.["bq_schema_version"];
|
|
91
|
+
if (raw == null) return void 0;
|
|
92
|
+
const value = Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw);
|
|
93
|
+
if (BLANK_ASCII.test(value)) return void 0;
|
|
94
|
+
return value === String(import_core.SCHEMA_VERSION) ? void 0 : value;
|
|
95
|
+
}
|
|
73
96
|
function deliveryCount(message) {
|
|
74
97
|
const value = message.delivery_count;
|
|
75
98
|
return typeof value === "number" && value > 0 ? value : 0;
|
|
76
99
|
}
|
|
100
|
+
function headersOf(message) {
|
|
101
|
+
const props = message.application_properties;
|
|
102
|
+
const out = {};
|
|
103
|
+
if (!props) return out;
|
|
104
|
+
for (const key of Object.keys(props)) {
|
|
105
|
+
const value = props[key];
|
|
106
|
+
if (value == null) continue;
|
|
107
|
+
const s = String(value);
|
|
108
|
+
if (s !== "") out[key] = s;
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
77
112
|
var ArtemisPublisher = class _ArtemisPublisher {
|
|
78
113
|
constructor(sender, queue) {
|
|
79
114
|
this.sender = sender;
|
|
@@ -88,10 +123,107 @@ var ArtemisPublisher = class _ArtemisPublisher {
|
|
|
88
123
|
/** Build + send the canonical envelope; returns the message id (`meta.id`). */
|
|
89
124
|
async publish(urn, data, options = {}) {
|
|
90
125
|
const envelope = import_core.EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });
|
|
91
|
-
await this.sender.send(artemisMessage(envelope, options.delayMs));
|
|
126
|
+
await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));
|
|
92
127
|
return envelope.meta.id;
|
|
93
128
|
}
|
|
94
129
|
};
|
|
130
|
+
var DEFAULT_DLQ_CONFIRM_TIMEOUT_MS = 3e4;
|
|
131
|
+
var DEFAULT_DLQ_RETRY_DELAY_MS = 1e3;
|
|
132
|
+
var CONFIRMATION_VERDICTS = ["accepted", "rejected", "released", "modified"];
|
|
133
|
+
var CONFIRMATION_CLOSES = ["sender_close", "sender_error"];
|
|
134
|
+
var ConfirmationHub = class {
|
|
135
|
+
constructor(sender, on, off) {
|
|
136
|
+
this.sender = sender;
|
|
137
|
+
this.on = on;
|
|
138
|
+
this.off = off;
|
|
139
|
+
}
|
|
140
|
+
sender;
|
|
141
|
+
on;
|
|
142
|
+
off;
|
|
143
|
+
pending = /* @__PURE__ */ new Set();
|
|
144
|
+
handlers = [];
|
|
145
|
+
add(confirmation) {
|
|
146
|
+
this.pending.add(confirmation);
|
|
147
|
+
if (this.handlers.length === 0) {
|
|
148
|
+
for (const name of [...CONFIRMATION_VERDICTS, ...CONFIRMATION_CLOSES]) {
|
|
149
|
+
const handler = (context) => this.dispatch(name, context);
|
|
150
|
+
this.handlers.push([name, handler]);
|
|
151
|
+
this.on(name, handler);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
remove(confirmation) {
|
|
156
|
+
this.pending.delete(confirmation);
|
|
157
|
+
if (this.pending.size === 0) {
|
|
158
|
+
for (const [name, handler] of this.handlers) this.off(name, handler);
|
|
159
|
+
this.handlers.length = 0;
|
|
160
|
+
hubs.delete(this.sender);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
dispatch(name, context) {
|
|
164
|
+
if (CONFIRMATION_CLOSES.includes(name)) {
|
|
165
|
+
for (const confirmation of [...this.pending]) {
|
|
166
|
+
confirmation.settle(new import_core.BabelQueueError(`The dead-letter link closed before the broker accepted the delivery (${name}).`));
|
|
167
|
+
}
|
|
168
|
+
this.forward(name, context);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const owners = [...this.pending].filter((confirmation) => confirmation.matches(context));
|
|
172
|
+
if (owners.length === 0) {
|
|
173
|
+
this.forward(name, context);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
for (const owner of owners) {
|
|
177
|
+
owner.settle(name === "accepted" ? void 0 : new import_core.BabelQueueError(`The broker did not accept the dead-letter delivery (${name}).`));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
forward(name, context) {
|
|
181
|
+
try {
|
|
182
|
+
this.sender.session?.dispatch?.(name, context);
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var hubs = /* @__PURE__ */ new WeakMap();
|
|
188
|
+
function sendConfirmed(sender, message, timeoutMs) {
|
|
189
|
+
const on = sender.on?.bind(sender);
|
|
190
|
+
const off = sender.removeListener?.bind(sender);
|
|
191
|
+
if (!on || !off) return Promise.resolve(sender.send(message)).then(() => void 0);
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
let hub = hubs.get(sender);
|
|
194
|
+
if (!hub) {
|
|
195
|
+
hub = new ConfirmationHub(sender, on, off);
|
|
196
|
+
hubs.set(sender, hub);
|
|
197
|
+
}
|
|
198
|
+
const activeHub = hub;
|
|
199
|
+
let finished = false;
|
|
200
|
+
const confirmation = {
|
|
201
|
+
delivery: void 0,
|
|
202
|
+
matches: (context) => confirmation.delivery === void 0 || context?.delivery === void 0 || context.delivery === confirmation.delivery,
|
|
203
|
+
settle: (error) => {
|
|
204
|
+
if (finished) return;
|
|
205
|
+
finished = true;
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
activeHub.remove(confirmation);
|
|
208
|
+
if (error) reject(error);
|
|
209
|
+
else resolve();
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
activeHub.add(confirmation);
|
|
213
|
+
const timer = setTimeout(
|
|
214
|
+
() => confirmation.settle(new import_core.BabelQueueError(`Timed out after ${timeoutMs}ms waiting for the broker to accept the dead-letter delivery.`)),
|
|
215
|
+
timeoutMs
|
|
216
|
+
);
|
|
217
|
+
try {
|
|
218
|
+
confirmation.delivery = sender.send(message);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
confirmation.settle(error instanceof Error ? error : new import_core.BabelQueueError(String(error)));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const delivery = confirmation.delivery;
|
|
224
|
+
if (delivery === null || typeof delivery !== "object" || delivery.settled === true) confirmation.settle();
|
|
225
|
+
});
|
|
226
|
+
}
|
|
95
227
|
var ArtemisConsumer = class {
|
|
96
228
|
constructor(handlers, options = {}) {
|
|
97
229
|
this.handlers = handlers;
|
|
@@ -111,11 +243,27 @@ var ArtemisConsumer = class {
|
|
|
111
243
|
async handle(context) {
|
|
112
244
|
const message = context.message ?? {};
|
|
113
245
|
const delivery = context.delivery;
|
|
246
|
+
const unsupported = unsupportedSchemaVersion(message);
|
|
247
|
+
if (unsupported !== void 0) {
|
|
248
|
+
const gateError = new import_core.BabelQueueError(
|
|
249
|
+
`Rejected a message from Artemis with unsupported bq_schema_version "${unsupported}" (supported: ${import_core.SCHEMA_VERSION}).`
|
|
250
|
+
);
|
|
251
|
+
try {
|
|
252
|
+
await this.deadLetterRawConfirmed(messageBody(message));
|
|
253
|
+
} catch (dlqError) {
|
|
254
|
+
this.safeReport(gateError, null, context);
|
|
255
|
+
this.safeReport(dlqError, null, context);
|
|
256
|
+
await this.releaseAfterBackoff(delivery);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
delivery?.accept();
|
|
260
|
+
this.safeReport(gateError, null, context);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
114
263
|
const decoded = import_core.EnvelopeCodec.decode(messageBody(message));
|
|
115
264
|
if (!import_core.EnvelopeCodec.accepts(decoded)) {
|
|
116
265
|
this.report(new import_core.BabelQueueError("Rejected a non-conformant BabelQueue envelope from Artemis."), decoded, context);
|
|
117
|
-
await this.deadLetterRaw(messageBody(message));
|
|
118
|
-
delivery?.accept();
|
|
266
|
+
await this.settleAfterDeadLetter(context, decoded, () => this.deadLetterRaw(messageBody(message)));
|
|
119
267
|
return;
|
|
120
268
|
}
|
|
121
269
|
const envelope = this.reconcile(decoded, message);
|
|
@@ -126,7 +274,7 @@ var ArtemisConsumer = class {
|
|
|
126
274
|
return;
|
|
127
275
|
}
|
|
128
276
|
try {
|
|
129
|
-
await handler(envelope, message);
|
|
277
|
+
await handler(envelope, message, headersOf(message));
|
|
130
278
|
delivery?.accept();
|
|
131
279
|
} catch (error) {
|
|
132
280
|
this.report(error, envelope, context);
|
|
@@ -146,8 +294,7 @@ var ArtemisConsumer = class {
|
|
|
146
294
|
delivery?.accept();
|
|
147
295
|
return;
|
|
148
296
|
case import_core.UnknownUrnStrategy.DEAD_LETTER:
|
|
149
|
-
await this.deadLetter(envelope, "unknown_urn");
|
|
150
|
-
delivery?.accept();
|
|
297
|
+
await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "unknown_urn"));
|
|
151
298
|
return;
|
|
152
299
|
case import_core.UnknownUrnStrategy.RELEASE:
|
|
153
300
|
delivery?.release({ delivery_failed: true });
|
|
@@ -164,12 +311,26 @@ var ArtemisConsumer = class {
|
|
|
164
311
|
if ((envelope.attempts ?? 0) + 1 < this.maxTries) {
|
|
165
312
|
delivery?.release({ delivery_failed: true });
|
|
166
313
|
} else if (this.options.deadLetterSender) {
|
|
167
|
-
await this.deadLetter(envelope, "failed", error);
|
|
168
|
-
delivery?.accept();
|
|
314
|
+
await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "failed", error));
|
|
169
315
|
} else {
|
|
170
316
|
delivery?.accept();
|
|
171
317
|
}
|
|
172
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry
|
|
321
|
+
* delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender
|
|
322
|
+
* the write is a no-op and the message is accepted (terminal drop), as before.
|
|
323
|
+
*/
|
|
324
|
+
async settleAfterDeadLetter(context, envelope, write) {
|
|
325
|
+
try {
|
|
326
|
+
await write();
|
|
327
|
+
} catch (dlqError) {
|
|
328
|
+
this.safeReport(dlqError, envelope, context);
|
|
329
|
+
await this.releaseAfterBackoff(context.delivery);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
context.delivery?.accept();
|
|
333
|
+
}
|
|
173
334
|
async deadLetter(envelope, reason, error) {
|
|
174
335
|
const sender = this.options.deadLetterSender;
|
|
175
336
|
if (!sender) return;
|
|
@@ -179,12 +340,30 @@ var ArtemisConsumer = class {
|
|
|
179
340
|
error: error instanceof Error ? error.message : null,
|
|
180
341
|
exception: error instanceof Error ? error.name : null
|
|
181
342
|
});
|
|
182
|
-
await sender
|
|
343
|
+
await sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
|
|
183
344
|
}
|
|
184
|
-
|
|
345
|
+
/** Poison path: forward the raw body, confirmed like the gate path. */
|
|
346
|
+
deadLetterRaw(raw) {
|
|
347
|
+
return this.deadLetterRawConfirmed(raw);
|
|
348
|
+
}
|
|
349
|
+
/** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */
|
|
350
|
+
async deadLetterRawConfirmed(raw) {
|
|
185
351
|
const sender = this.options.deadLetterSender;
|
|
186
352
|
if (!sender) return;
|
|
187
|
-
await sender
|
|
353
|
+
await sendConfirmed(sender, { body: raw }, this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
|
|
354
|
+
}
|
|
355
|
+
/** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
|
|
356
|
+
async releaseAfterBackoff(delivery) {
|
|
357
|
+
const delayMs = this.options.deadLetterRetryDelayMs ?? DEFAULT_DLQ_RETRY_DELAY_MS;
|
|
358
|
+
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs).unref?.());
|
|
359
|
+
delivery?.release();
|
|
360
|
+
}
|
|
361
|
+
/** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */
|
|
362
|
+
safeReport(error, envelope, context) {
|
|
363
|
+
try {
|
|
364
|
+
this.report(error, envelope, context);
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
188
367
|
}
|
|
189
368
|
report(error, envelope, context) {
|
|
190
369
|
this.options.onError?.(error, envelope, context);
|
|
@@ -197,6 +376,7 @@ var ArtemisConsumer = class {
|
|
|
197
376
|
JMS_TYPE_KEY,
|
|
198
377
|
SCHEDULED_DELIVERY_KEY,
|
|
199
378
|
artemisMessage,
|
|
379
|
+
headersOf,
|
|
200
380
|
messageBody
|
|
201
381
|
});
|
|
202
382
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Apache ActiveMQ Artemis adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **AMQP 1.0** consumer over Artemis (rhea),\n * so an Artemis-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the Java, .NET, Python and Go SDKs.\n *\n * Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —\n * per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so\n * this adapter maps onto them (the envelope stays `schema_version: 1`): the envelope JSON is the\n * message **body**; the contract fields are mirrored onto the AMQP a JMS peer reads\n * (`correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN) plus the `bq_` application properties; consume settles per message\n * (`accept` after success, `release` for redelivery); **`attempts = max(body, delivery-count)`**\n * (the AMQP counter is 0-based — no −1); terminal failures go to an opt-in `<queue>.dlq` with a\n * `dead_letter` block.\n *\n * This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide\n * the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, IncomingEnvelope } from \"@babelqueue/core\";\n\n// --- Minimal AMQP 1.0 shapes (a structural subset of rhea) ---------------------\n\n/** An AMQP message (structural subset of a rhea message). */\nexport interface AmqpMessage {\n body?: unknown;\n correlation_id?: unknown;\n creation_time?: number | Date;\n content_type?: string;\n application_properties?: Record<string, unknown>;\n message_annotations?: Record<string, unknown>;\n /** The AMQP header delivery-count (0-based: 0 on first delivery). */\n delivery_count?: number;\n}\n\n/** The disposition handle rhea passes alongside a received message. */\nexport interface AmqpDelivery {\n /** Settle (acknowledge) the message — it is removed from the queue. */\n accept(): void;\n /** Return the message for redelivery — the broker increments `delivery-count`. */\n release(params?: { delivery_failed?: boolean; undeliverable_here?: boolean }): void;\n}\n\n/** The event context rhea emits on a `message` event. */\nexport interface AmqpEventContext {\n message?: AmqpMessage;\n delivery?: AmqpDelivery;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n send(message: AmqpMessage): unknown;\n}\n\n/** The subset of a rhea receiver this adapter wires to (its `message` event). */\nexport interface AmqpReceiver {\n on(event: \"message\", handler: (context: AmqpEventContext) => void): unknown;\n}\n\n// --- Projection (contract §7.2) ------------------------------------------------\n\n/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */\nexport const JMS_TYPE_KEY = \"x-opt-jms-type\";\n/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */\nexport const SCHEDULED_DELIVERY_KEY = \"x-opt-delivery-time\";\n\n/**\n * Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,\n * `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`\n * sets the `x-opt-delivery-time` annotation for native scheduled delivery.\n */\nexport function artemisMessage(envelope: Envelope, delayMs?: number): AmqpMessage {\n const annotations: Record<string, unknown> = {};\n if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;\n\n // The bq_ property names use underscores, not hyphens: a JMS property name must be a valid\n // Java identifier, and every Artemis SDK uses the same JMS-legal form for cross-protocol parity.\n const applicationProperties: Record<string, unknown> = {\n \"bq_schema_version\": String(envelope.meta.schema_version),\n \"bq_attempts\": String(envelope.attempts ?? 0),\n \"bq_app_id\": \"babelqueue\",\n };\n if (envelope.meta.lang) applicationProperties[\"bq_source_lang\"] = envelope.meta.lang;\n\n const message: AmqpMessage = {\n body: EnvelopeCodec.encode(envelope),\n content_type: \"application/json\",\n application_properties: applicationProperties,\n message_annotations: annotations,\n };\n if (envelope.trace_id) message.correlation_id = envelope.trace_id;\n if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;\n\n if (delayMs != null && delayMs > 0) {\n applicationProperties[\"bq_delay\"] = String(delayMs);\n annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;\n }\n return message;\n}\n\n/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */\nexport function messageBody(message: AmqpMessage): string {\n const body = message.body;\n if (body == null) return \"\";\n if (typeof body === \"string\") return body;\n if (Buffer.isBuffer(body)) return body.toString(\"utf8\");\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content.toString(\"utf8\");\n if (typeof content === \"string\") return content;\n }\n return String(body);\n}\n\nfunction jmsType(message: AmqpMessage): string | undefined {\n const value = message.message_annotations?.[JMS_TYPE_KEY];\n return value == null ? undefined : String(value);\n}\n\nfunction deliveryCount(message: AmqpMessage): number {\n const value = message.delivery_count;\n return typeof value === \"number\" && value > 0 ? value : 0;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link ArtemisPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */\n delayMs?: number;\n}\n\n/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */\nexport class ArtemisPublisher {\n private constructor(\n private readonly sender: AmqpSender,\n private readonly queue: string,\n ) {}\n\n /** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */\n static create(sender: AmqpSender, queue: string): ArtemisPublisher {\n return new ArtemisPublisher(sender, queue);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });\n await this.sender.send(artemisMessage(envelope, options.delayMs));\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/** A URN handler. Receives the validated envelope and the raw AMQP message. */\nexport type BabelHandler = (envelope: Envelope, message: AmqpMessage) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link ArtemisConsumer}. */\nexport interface ArtemisConsumerOptions {\n /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */\n deadLetterSender?: AmqpSender;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a non-conformant message, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;\n}\n\n/**\n * Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the\n * handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A\n * throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP\n * `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a\n * `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP\n * counter is 0-based, so no −1.\n */\nexport class ArtemisConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly handlers: BabelHandlers,\n private readonly options: ArtemisConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */\n listen(receiver: AmqpReceiver): void {\n receiver.on(\"message\", (context) => this.handle(context));\n }\n\n /** Route + settle one delivery. Exposed for testing. */\n async handle(context: AmqpEventContext): Promise<void> {\n const message = context.message ?? {};\n const delivery = context.delivery;\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n // A non-conformant / poison message may lack the fields to annotate; forward it raw.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\"), decoded, context);\n await this.deadLetterRaw(messageBody(message));\n delivery?.accept();\n return;\n }\n\n const envelope = this.reconcile(decoded, message);\n const urn = jmsType(message) ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(context, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message);\n delivery?.accept();\n } catch (error) {\n this.report(error, envelope, context);\n await this.retryOrDeadLetter(context, envelope, error);\n }\n }\n\n /** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */\n private reconcile(envelope: IncomingEnvelope, message: AmqpMessage): Envelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n const count = deliveryCount(message);\n return { ...envelope, attempts: Math.max(body, count) } as Envelope;\n }\n\n private async onUnknownUrn(context: AmqpEventContext, envelope: Envelope, urn: string): Promise<void> {\n const delivery = context.delivery;\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n delivery?.accept();\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope, \"unknown_urn\");\n delivery?.accept();\n return;\n case UnknownUrnStrategy.RELEASE:\n delivery?.release({ delivery_failed: true });\n return;\n default: {\n // FAIL: surface and do NOT settle — the broker redelivers, then dead-letters.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, context);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(context: AmqpEventContext, envelope: Envelope, error: unknown): Promise<void> {\n const delivery = context.delivery;\n if ((envelope.attempts ?? 0) + 1 < this.maxTries) {\n // Release with delivery-failed leaves it for redelivery — the broker bumps delivery-count.\n delivery?.release({ delivery_failed: true });\n } else if (this.options.deadLetterSender) {\n await this.deadLetter(envelope, \"failed\", error);\n delivery?.accept();\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n private async deadLetter(envelope: Envelope, reason: string, error?: unknown): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n const original = envelope.meta?.queue ?? \"\";\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n await sender.send(artemisMessage(annotated));\n }\n\n private async deadLetterRaw(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sender.send({ body: raw });\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n this.options.onError?.(error, envelope, context);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache ActiveMQ Artemis adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **AMQP 1.0** consumer over Artemis (rhea),\n * so an Artemis-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the Java, .NET, Python and Go SDKs.\n *\n * Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —\n * per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so\n * this adapter maps onto them (the envelope stays `schema_version: 1`): the envelope JSON is the\n * message **body**; the contract fields are mirrored onto the AMQP a JMS peer reads\n * (`correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN) plus the `bq_` application properties; consume settles per message\n * (`accept` after success, `release` for redelivery); **`attempts = max(body, delivery-count)`**\n * (the AMQP counter is 0-based — no −1); terminal failures go to an opt-in `<queue>.dlq` with a\n * `dead_letter` block.\n *\n * This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide\n * the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional AMQP `application_properties` beside the contract `bq_`\n * properties — the contract properties win a key collision (merge-not-clobber), blanks dropped.\n * `traceparent`/`tracestate` are W3C keys with no hyphens, so they are already JMS-legal (§7 uses\n * underscores because JMS property names must be Java identifiers). On consume the inbound\n * `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract\n * sees the `traceparent` and links the consumer span as a child. A header-less publish is\n * byte-identical. GR-1: the wire envelope body is never touched.\n *\n * **Version gate (§7.7).** Before decoding the body the consumer reads the `bq_schema_version`\n * application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII\n * whitespace) follows the normal decode path; any other value is compared, untrimmed, to the supported\n * `\"1\"`, and a message that does not match is rejected undecoded — reported to `onError`, the raw body\n * forwarded to the DLQ sender (when configured) and the message accepted **only after the broker has\n * accepted the DLQ delivery**. If the DLQ send fails, is refused (rejected/released/modified), the link\n * closes or `deadLetterTimeoutMs` elapses, the failure is reported and, after `deadLetterRetryDelayMs`,\n * the source message gets a plain `released` outcome (no `delivery_failed`: the broker's delivery\n * counter and `max-delivery-attempts` stay untouched) — never accepted, never handed to a handler.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { sanitizeHeaders } from \"./headers.js\";\n\n// --- Minimal AMQP 1.0 shapes (a structural subset of rhea) ---------------------\n\n/** An AMQP message (structural subset of a rhea message). */\nexport interface AmqpMessage {\n body?: unknown;\n correlation_id?: unknown;\n creation_time?: number | Date;\n content_type?: string;\n application_properties?: Record<string, unknown>;\n message_annotations?: Record<string, unknown>;\n /** The AMQP header delivery-count (0-based: 0 on first delivery). */\n delivery_count?: number;\n}\n\n/** The disposition handle rhea passes alongside a received message. */\nexport interface AmqpDelivery {\n /** Settle (acknowledge) the message — it is removed from the queue. */\n accept(): void;\n /** Return the message for redelivery — the broker increments `delivery-count`. */\n release(params?: { delivery_failed?: boolean; undeliverable_here?: boolean }): void;\n}\n\n/** The event context rhea emits on a `message` event. */\nexport interface AmqpEventContext {\n message?: AmqpMessage;\n delivery?: AmqpDelivery;\n}\n\n/**\n * The context rhea passes to a sender link event (`accepted`, `rejected`, `sender_close`, ...). Only\n * `delivery` is inspected (to match a verdict to its delivery); everything else is opaque. Deliberately\n * no index signature: an interface-typed handler parameter (e.g. rhea's `EventContext`) must stay assignable.\n */\nexport interface AmqpLinkEventContext {\n delivery?: unknown;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n /** Queue a delivery. rhea returns the delivery synchronously; the broker's verdict arrives later as an event. */\n send(message: AmqpMessage): unknown;\n /**\n * Optional (a rhea sender has it). When both `on` and `removeListener` exist, the consumer's\n * version-gate path waits for the broker's `accepted` disposition on the DLQ delivery before it\n * settles the source message; without them only a synchronous `send` failure is observable.\n */\n on?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;\n removeListener?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;\n /**\n * Optional (a rhea link has it). rhea does not bubble an event to the session/connection while the\n * link has a listener for it, so the temporary confirmation listeners re-dispatch what is not theirs\n * (other deliveries' dispositions, link close/error) here.\n */\n session?: { dispatch?(event: string, context?: AmqpLinkEventContext): unknown };\n}\n\n/** The subset of a rhea receiver this adapter wires to (its `message` event). */\nexport interface AmqpReceiver {\n on(event: \"message\", handler: (context: AmqpEventContext) => void): unknown;\n}\n\n// --- Projection (contract §7.2) ------------------------------------------------\n\n/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */\nexport const JMS_TYPE_KEY = \"x-opt-jms-type\";\n/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */\nexport const SCHEDULED_DELIVERY_KEY = \"x-opt-delivery-time\";\n\n/**\n * Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,\n * `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`\n * sets the `x-opt-delivery-time` annotation for native scheduled delivery.\n */\nexport function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage {\n const annotations: Record<string, unknown> = {};\n if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;\n\n // Out-of-band riders (e.g. traceparent) go in first; the contract bq_ properties below win any\n // key collision (merge-not-clobber). The bq_ names use underscores, not hyphens: a JMS property\n // name must be a valid Java identifier, and every Artemis SDK uses the same JMS-legal form for\n // cross-protocol parity. traceparent/tracestate have no hyphens, so they are already JMS-legal.\n const applicationProperties: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;\n applicationProperties[\"bq_schema_version\"] = String(envelope.meta.schema_version);\n applicationProperties[\"bq_attempts\"] = String(envelope.attempts ?? 0);\n applicationProperties[\"bq_app_id\"] = \"babelqueue\";\n if (envelope.meta.lang) applicationProperties[\"bq_source_lang\"] = envelope.meta.lang;\n\n const message: AmqpMessage = {\n body: EnvelopeCodec.encode(envelope),\n content_type: \"application/json\",\n application_properties: applicationProperties,\n message_annotations: annotations,\n };\n if (envelope.trace_id) message.correlation_id = envelope.trace_id;\n if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;\n\n if (delayMs != null && delayMs > 0) {\n applicationProperties[\"bq_delay\"] = String(delayMs);\n annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;\n }\n return message;\n}\n\n/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */\nexport function messageBody(message: AmqpMessage): string {\n const body = message.body;\n if (body == null) return \"\";\n if (typeof body === \"string\") return body;\n if (Buffer.isBuffer(body)) return body.toString(\"utf8\");\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content.toString(\"utf8\");\n if (typeof content === \"string\") return content;\n }\n return String(body);\n}\n\nfunction jmsType(message: AmqpMessage): string | undefined {\n const value = message.message_annotations?.[JMS_TYPE_KEY];\n return value == null ? undefined : String(value);\n}\n\n/** Blank = empty or only ASCII whitespace (GR-5: the same set in every SDK, never `String.trim()`). */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The inbound `bq_schema_version` application property when it is present, non-blank and not exactly\n * the supported version (compared untrimmed), else `undefined` (the message may be decoded).\n */\nfunction unsupportedSchemaVersion(message: AmqpMessage): string | undefined {\n const raw = message.application_properties?.[\"bq_schema_version\"];\n if (raw == null) return undefined;\n const value = Buffer.isBuffer(raw) ? raw.toString(\"utf8\") : String(raw);\n if (BLANK_ASCII.test(value)) return undefined;\n return value === String(SCHEMA_VERSION) ? undefined : value;\n}\n\nfunction deliveryCount(message: AmqpMessage): number {\n const value = message.delivery_count;\n return typeof value === \"number\" && value > 0 ? value : 0;\n}\n\n/**\n * Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying\n * values defensively. Returns an empty object when there are none. Both the contract `bq_`\n * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract\n * reads only the keys it knows.\n */\nexport function headersOf(message: AmqpMessage): HeaderCarrier {\n const props = message.application_properties;\n const out: HeaderCarrier = {};\n if (!props) return out;\n for (const key of Object.keys(props)) {\n const value = props[key];\n if (value == null) continue;\n const s = String(value);\n if (s !== \"\") out[key] = s;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link ArtemisPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`\n * properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract properties win a key collision; an empty/omitted carrier leaves the message\n * unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */\nexport class ArtemisPublisher {\n private constructor(\n private readonly sender: AmqpSender,\n private readonly queue: string,\n ) {}\n\n /** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */\n static create(sender: AmqpSender, queue: string): ArtemisPublisher {\n return new ArtemisPublisher(sender, queue);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });\n await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));\n return envelope.meta.id;\n }\n}\n\n// --- DLQ delivery confirmation ---------------------------------------------------\n\n/** Default wait (ms) for the broker to settle a gate-path DLQ delivery. */\nconst DEFAULT_DLQ_CONFIRM_TIMEOUT_MS = 30_000;\n/** Default pause (ms) before a gate message whose DLQ write failed is released back to the broker. */\nconst DEFAULT_DLQ_RETRY_DELAY_MS = 1_000;\n\n/** One in-flight DLQ confirmation registered on a {@link ConfirmationHub}. */\ninterface PendingConfirmation {\n /** The delivery `send` returned; `undefined` while `send` is still running synchronously. */\n delivery: unknown;\n /** `true` when `context` is (or may be) this confirmation's verdict. */\n matches(context: AmqpLinkEventContext | undefined): boolean;\n /** Resolve (no argument) or reject the confirmation. */\n settle(error?: Error): void;\n}\n\nconst CONFIRMATION_VERDICTS = [\"accepted\", \"rejected\", \"released\", \"modified\"] as const;\nconst CONFIRMATION_CLOSES = [\"sender_close\", \"sender_error\"] as const;\n\n/**\n * One shared set of temporary sender listeners per {@link AmqpSender}, however many DLQ confirmations\n * are in flight. rhea bubbles a link event to the session only while the link has no listener for it,\n * so while confirmations are pending the hub owns the listeners and re-dispatches every event it does\n * not consume to `sender.session` exactly once (per-confirmation listeners would forward each event\n * once per pending confirmation). The listeners are removed as soon as no confirmation is pending.\n */\nclass ConfirmationHub {\n private readonly pending = new Set<PendingConfirmation>();\n private readonly handlers: [string, (context: AmqpLinkEventContext) => void][] = [];\n\n constructor(\n private readonly sender: AmqpSender,\n private readonly on: (event: string, handler: (context: AmqpLinkEventContext) => void) => unknown,\n private readonly off: (event: string, handler: (context: AmqpLinkEventContext) => void) => unknown,\n ) {}\n\n add(confirmation: PendingConfirmation): void {\n this.pending.add(confirmation);\n if (this.handlers.length === 0) {\n for (const name of [...CONFIRMATION_VERDICTS, ...CONFIRMATION_CLOSES]) {\n const handler = (context: AmqpLinkEventContext): void => this.dispatch(name, context);\n this.handlers.push([name, handler]);\n this.on(name, handler);\n }\n }\n }\n\n remove(confirmation: PendingConfirmation): void {\n this.pending.delete(confirmation);\n if (this.pending.size === 0) {\n for (const [name, handler] of this.handlers) this.off(name, handler);\n this.handlers.length = 0;\n hubs.delete(this.sender);\n }\n }\n\n private dispatch(name: string, context: AmqpLinkEventContext): void {\n if ((CONFIRMATION_CLOSES as readonly string[]).includes(name)) {\n for (const confirmation of [...this.pending]) {\n confirmation.settle(new BabelQueueError(`The dead-letter link closed before the broker accepted the delivery (${name}).`));\n }\n this.forward(name, context);\n return;\n }\n const owners = [...this.pending].filter((confirmation) => confirmation.matches(context));\n if (owners.length === 0) {\n this.forward(name, context);\n return;\n }\n for (const owner of owners) {\n owner.settle(name === \"accepted\" ? undefined : new BabelQueueError(`The broker did not accept the dead-letter delivery (${name}).`));\n }\n }\n\n private forward(name: string, context: AmqpLinkEventContext): void {\n try {\n this.sender.session?.dispatch?.(name, context);\n } catch {\n // forwarding is best effort; never let it change a confirmation outcome\n }\n }\n}\n\nconst hubs = new WeakMap<AmqpSender, ConfirmationHub>();\n\n/**\n * Send `message` and resolve only once the broker has `accepted` the delivery. A rhea `send` returns\n * synchronously and reports the verdict (`accepted` / `rejected` / `released` / `modified`) as a\n * sender event, so awaiting `send` alone cannot see a broker refusal. Rejects on any non-accepted\n * disposition, a link close/error, or when `timeoutMs` elapses (an unsettled delivery that is lost on\n * reconnect raises no event). A sender without event support degrades to awaiting `send` itself.\n * Events that are not this delivery's verdict (other deliveries, link close/error) are re-dispatched on\n * the sender's session, once per event, so the application still sees them (rhea suppresses bubbling\n * while the link has a listener) — see {@link ConfirmationHub}.\n */\nfunction sendConfirmed(sender: AmqpSender, message: AmqpMessage, timeoutMs: number): Promise<void> {\n const on = sender.on?.bind(sender);\n const off = sender.removeListener?.bind(sender);\n if (!on || !off) return Promise.resolve(sender.send(message)).then(() => undefined);\n\n return new Promise<void>((resolve, reject) => {\n let hub = hubs.get(sender);\n if (!hub) {\n hub = new ConfirmationHub(sender, on, off);\n hubs.set(sender, hub);\n }\n const activeHub = hub;\n let finished = false;\n\n const confirmation: PendingConfirmation = {\n delivery: undefined,\n matches: (context) =>\n confirmation.delivery === undefined || context?.delivery === undefined || context.delivery === confirmation.delivery,\n settle: (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timer);\n activeHub.remove(confirmation);\n if (error) reject(error);\n else resolve();\n },\n };\n activeHub.add(confirmation);\n\n // `settle` (above) reads `timer`; it cannot run before this line (nothing is sent or listened to yet).\n const timer = setTimeout(\n () => confirmation.settle(new BabelQueueError(`Timed out after ${timeoutMs}ms waiting for the broker to accept the dead-letter delivery.`)),\n timeoutMs,\n );\n try {\n confirmation.delivery = sender.send(message);\n } catch (error) {\n confirmation.settle(error instanceof Error ? error : new BabelQueueError(String(error)));\n return;\n }\n // A pre-settled delivery (or a sender that returns none) raises no disposition event.\n const delivery = confirmation.delivery;\n if (delivery === null || typeof delivery !== \"object\" || (delivery as { settled?: unknown }).settled === true) confirmation.settle();\n });\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band\n * {@link HeaderCarrier} read from the message's `application_properties` (empty when there are\n * none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a\n * child of the producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link ArtemisConsumer}. */\nexport interface ArtemisConsumerOptions {\n /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */\n deadLetterSender?: AmqpSender;\n /**\n * How long (ms) every dead-letter path (version gate, poison, unknown-URN `dead_letter`, terminal\n * handler failure) waits for the broker to accept the DLQ delivery before it releases the source\n * message instead of accepting it (default 30000).\n */\n deadLetterTimeoutMs?: number;\n /**\n * Pause (ms) before a dead-letter path releases a message whose DLQ write failed (default 1000).\n * The release is a plain `released` outcome (no `delivery_failed`, so the broker's delivery counter and\n * `max-delivery-attempts` are untouched); the pause keeps a persistently failing DLQ from becoming a\n * hot redelivery loop. The delivery stays unsettled (holding one credit) while it waits.\n * Note: on the terminal handler-failure path the message has already been through the handler, and\n * a plain release does not bump the delivery counter, so the redelivery runs the handler again (and\n * reports its failure again) until the DLQ write succeeds.\n */\n deadLetterRetryDelayMs?: number;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a non-conformant message, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;\n}\n\n/**\n * Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the\n * handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A\n * throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP\n * `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a\n * `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP\n * counter is 0-based, so no −1.\n */\nexport class ArtemisConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly handlers: BabelHandlers,\n private readonly options: ArtemisConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */\n listen(receiver: AmqpReceiver): void {\n receiver.on(\"message\", (context) => this.handle(context));\n }\n\n /** Route + settle one delivery. Exposed for testing. */\n async handle(context: AmqpEventContext): Promise<void> {\n const message = context.message ?? {};\n const delivery = context.delivery;\n\n // Version gate (§7.7): decided on the application property alone, before the body is decoded.\n const unsupported = unsupportedSchemaVersion(message);\n if (unsupported !== undefined) {\n const gateError = new BabelQueueError(\n `Rejected a message from Artemis with unsupported bq_schema_version \"${unsupported}\" (supported: ${SCHEMA_VERSION}).`,\n );\n // Accept only after the broker has accepted the DLQ copy; otherwise release (plain, after a pause) so\n // the message is never lost and never reaches a handler. Reports come after the settlement, or are\n // isolated, so a throwing onError can never leave the message unsettled.\n try {\n await this.deadLetterRawConfirmed(messageBody(message));\n } catch (dlqError) {\n this.safeReport(gateError, null, context);\n this.safeReport(dlqError, null, context);\n await this.releaseAfterBackoff(delivery);\n return;\n }\n delivery?.accept();\n this.safeReport(gateError, null, context);\n return;\n }\n\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n // A non-conformant / poison message may lack the fields to annotate; forward it raw.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\"), decoded, context);\n await this.settleAfterDeadLetter(context, decoded, () => this.deadLetterRaw(messageBody(message)));\n return;\n }\n\n const envelope = this.reconcile(decoded, message);\n const urn = jmsType(message) ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(context, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n delivery?.accept();\n } catch (error) {\n this.report(error, envelope, context);\n await this.retryOrDeadLetter(context, envelope, error);\n }\n }\n\n /** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */\n private reconcile(envelope: IncomingEnvelope, message: AmqpMessage): Envelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n const count = deliveryCount(message);\n return { ...envelope, attempts: Math.max(body, count) } as Envelope;\n }\n\n private async onUnknownUrn(context: AmqpEventContext, envelope: Envelope, urn: string): Promise<void> {\n const delivery = context.delivery;\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n delivery?.accept();\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, \"unknown_urn\"));\n return;\n case UnknownUrnStrategy.RELEASE:\n delivery?.release({ delivery_failed: true });\n return;\n default: {\n // FAIL: surface and do NOT settle — the broker redelivers, then dead-letters.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, context);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(context: AmqpEventContext, envelope: Envelope, error: unknown): Promise<void> {\n const delivery = context.delivery;\n if ((envelope.attempts ?? 0) + 1 < this.maxTries) {\n // Release with delivery-failed leaves it for redelivery — the broker bumps delivery-count.\n delivery?.release({ delivery_failed: true });\n } else if (this.options.deadLetterSender) {\n await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, \"failed\", error));\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n /**\n * Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry\n * delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender\n * the write is a no-op and the message is accepted (terminal drop), as before.\n */\n private async settleAfterDeadLetter(\n context: AmqpEventContext,\n envelope: IncomingEnvelope | null,\n write: () => Promise<void>,\n ): Promise<void> {\n try {\n await write();\n } catch (dlqError) {\n this.safeReport(dlqError, envelope, context);\n await this.releaseAfterBackoff(context.delivery);\n return;\n }\n context.delivery?.accept();\n }\n\n private async deadLetter(envelope: Envelope, reason: string, error?: unknown): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n const original = envelope.meta?.queue ?? \"\";\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n await sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);\n }\n\n /** Poison path: forward the raw body, confirmed like the gate path. */\n private deadLetterRaw(raw: string): Promise<void> {\n return this.deadLetterRawConfirmed(raw);\n }\n\n /** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */\n private async deadLetterRawConfirmed(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sendConfirmed(sender, { body: raw }, this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);\n }\n\n /** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */\n private async releaseAfterBackoff(delivery: AmqpDelivery | undefined): Promise<void> {\n const delayMs = this.options.deadLetterRetryDelayMs ?? DEFAULT_DLQ_RETRY_DELAY_MS;\n if (delayMs > 0) await new Promise<void>((resolve) => setTimeout(resolve, delayMs).unref?.());\n delivery?.release();\n }\n\n /** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */\n private safeReport(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n try {\n this.report(error, envelope, context);\n } catch {\n // swallowed by design\n }\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n this.options.onError?.(error, envelope, context);\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Envelope, IncomingEnvelope } from '@babelqueue/core';
|
|
1
|
+
import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Apache ActiveMQ Artemis adapter for BabelQueue.
|
|
@@ -19,6 +19,25 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
|
|
|
19
19
|
*
|
|
20
20
|
* This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide
|
|
21
21
|
* the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).
|
|
22
|
+
*
|
|
23
|
+
* **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
|
|
24
|
+
* span linkage) rides as additional AMQP `application_properties` beside the contract `bq_`
|
|
25
|
+
* properties — the contract properties win a key collision (merge-not-clobber), blanks dropped.
|
|
26
|
+
* `traceparent`/`tracestate` are W3C keys with no hyphens, so they are already JMS-legal (§7 uses
|
|
27
|
+
* underscores because JMS property names must be Java identifiers). On consume the inbound
|
|
28
|
+
* `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract
|
|
29
|
+
* sees the `traceparent` and links the consumer span as a child. A header-less publish is
|
|
30
|
+
* byte-identical. GR-1: the wire envelope body is never touched.
|
|
31
|
+
*
|
|
32
|
+
* **Version gate (§7.7).** Before decoding the body the consumer reads the `bq_schema_version`
|
|
33
|
+
* application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII
|
|
34
|
+
* whitespace) follows the normal decode path; any other value is compared, untrimmed, to the supported
|
|
35
|
+
* `"1"`, and a message that does not match is rejected undecoded — reported to `onError`, the raw body
|
|
36
|
+
* forwarded to the DLQ sender (when configured) and the message accepted **only after the broker has
|
|
37
|
+
* accepted the DLQ delivery**. If the DLQ send fails, is refused (rejected/released/modified), the link
|
|
38
|
+
* closes or `deadLetterTimeoutMs` elapses, the failure is reported and, after `deadLetterRetryDelayMs`,
|
|
39
|
+
* the source message gets a plain `released` outcome (no `delivery_failed`: the broker's delivery
|
|
40
|
+
* counter and `max-delivery-attempts` stay untouched) — never accepted, never handed to a handler.
|
|
22
41
|
*/
|
|
23
42
|
|
|
24
43
|
/** An AMQP message (structural subset of a rhea message). */
|
|
@@ -47,9 +66,33 @@ interface AmqpEventContext {
|
|
|
47
66
|
message?: AmqpMessage;
|
|
48
67
|
delivery?: AmqpDelivery;
|
|
49
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The context rhea passes to a sender link event (`accepted`, `rejected`, `sender_close`, ...). Only
|
|
71
|
+
* `delivery` is inspected (to match a verdict to its delivery); everything else is opaque. Deliberately
|
|
72
|
+
* no index signature: an interface-typed handler parameter (e.g. rhea's `EventContext`) must stay assignable.
|
|
73
|
+
*/
|
|
74
|
+
interface AmqpLinkEventContext {
|
|
75
|
+
delivery?: unknown;
|
|
76
|
+
}
|
|
50
77
|
/** The subset of a rhea sender this adapter calls. */
|
|
51
78
|
interface AmqpSender {
|
|
79
|
+
/** Queue a delivery. rhea returns the delivery synchronously; the broker's verdict arrives later as an event. */
|
|
52
80
|
send(message: AmqpMessage): unknown;
|
|
81
|
+
/**
|
|
82
|
+
* Optional (a rhea sender has it). When both `on` and `removeListener` exist, the consumer's
|
|
83
|
+
* version-gate path waits for the broker's `accepted` disposition on the DLQ delivery before it
|
|
84
|
+
* settles the source message; without them only a synchronous `send` failure is observable.
|
|
85
|
+
*/
|
|
86
|
+
on?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;
|
|
87
|
+
removeListener?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;
|
|
88
|
+
/**
|
|
89
|
+
* Optional (a rhea link has it). rhea does not bubble an event to the session/connection while the
|
|
90
|
+
* link has a listener for it, so the temporary confirmation listeners re-dispatch what is not theirs
|
|
91
|
+
* (other deliveries' dispositions, link close/error) here.
|
|
92
|
+
*/
|
|
93
|
+
session?: {
|
|
94
|
+
dispatch?(event: string, context?: AmqpLinkEventContext): unknown;
|
|
95
|
+
};
|
|
53
96
|
}
|
|
54
97
|
/** The subset of a rhea receiver this adapter wires to (its `message` event). */
|
|
55
98
|
interface AmqpReceiver {
|
|
@@ -65,14 +108,28 @@ declare const SCHEDULED_DELIVERY_KEY = "x-opt-delivery-time";
|
|
|
65
108
|
* annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`
|
|
66
109
|
* sets the `x-opt-delivery-time` annotation for native scheduled delivery.
|
|
67
110
|
*/
|
|
68
|
-
declare function artemisMessage(envelope: Envelope, delayMs?: number): AmqpMessage;
|
|
111
|
+
declare function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage;
|
|
69
112
|
/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */
|
|
70
113
|
declare function messageBody(message: AmqpMessage): string;
|
|
114
|
+
/**
|
|
115
|
+
* Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying
|
|
116
|
+
* values defensively. Returns an empty object when there are none. Both the contract `bq_`
|
|
117
|
+
* properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract
|
|
118
|
+
* reads only the keys it knows.
|
|
119
|
+
*/
|
|
120
|
+
declare function headersOf(message: AmqpMessage): HeaderCarrier;
|
|
71
121
|
/** Options for {@link ArtemisPublisher.publish}. */
|
|
72
122
|
interface PublishOptions {
|
|
73
123
|
traceId?: string;
|
|
74
124
|
/** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */
|
|
75
125
|
delayMs?: number;
|
|
126
|
+
/**
|
|
127
|
+
* Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`
|
|
128
|
+
* properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.
|
|
129
|
+
* The contract properties win a key collision; an empty/omitted carrier leaves the message
|
|
130
|
+
* unchanged.
|
|
131
|
+
*/
|
|
132
|
+
headers?: HeaderCarrier;
|
|
76
133
|
}
|
|
77
134
|
/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */
|
|
78
135
|
declare class ArtemisPublisher {
|
|
@@ -84,14 +141,35 @@ declare class ArtemisPublisher {
|
|
|
84
141
|
/** Build + send the canonical envelope; returns the message id (`meta.id`). */
|
|
85
142
|
publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
|
|
86
143
|
}
|
|
87
|
-
/**
|
|
88
|
-
|
|
144
|
+
/**
|
|
145
|
+
* A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band
|
|
146
|
+
* {@link HeaderCarrier} read from the message's `application_properties` (empty when there are
|
|
147
|
+
* none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a
|
|
148
|
+
* child of the producer span (ADR-0028).
|
|
149
|
+
*/
|
|
150
|
+
type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
|
|
89
151
|
/** A map of URN → handler. */
|
|
90
152
|
type BabelHandlers = Record<string, BabelHandler>;
|
|
91
153
|
/** Options for {@link ArtemisConsumer}. */
|
|
92
154
|
interface ArtemisConsumerOptions {
|
|
93
155
|
/** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */
|
|
94
156
|
deadLetterSender?: AmqpSender;
|
|
157
|
+
/**
|
|
158
|
+
* How long (ms) every dead-letter path (version gate, poison, unknown-URN `dead_letter`, terminal
|
|
159
|
+
* handler failure) waits for the broker to accept the DLQ delivery before it releases the source
|
|
160
|
+
* message instead of accepting it (default 30000).
|
|
161
|
+
*/
|
|
162
|
+
deadLetterTimeoutMs?: number;
|
|
163
|
+
/**
|
|
164
|
+
* Pause (ms) before a dead-letter path releases a message whose DLQ write failed (default 1000).
|
|
165
|
+
* The release is a plain `released` outcome (no `delivery_failed`, so the broker's delivery counter and
|
|
166
|
+
* `max-delivery-attempts` are untouched); the pause keeps a persistently failing DLQ from becoming a
|
|
167
|
+
* hot redelivery loop. The delivery stays unsettled (holding one credit) while it waits.
|
|
168
|
+
* Note: on the terminal handler-failure path the message has already been through the handler, and
|
|
169
|
+
* a plain release does not bump the delivery counter, so the redelivery runs the handler again (and
|
|
170
|
+
* reports its failure again) until the DLQ write succeeds.
|
|
171
|
+
*/
|
|
172
|
+
deadLetterRetryDelayMs?: number;
|
|
95
173
|
/** Attempts before terminal dead-lettering (default 3). */
|
|
96
174
|
maxTries?: number;
|
|
97
175
|
/** Strategy for a URN with no handler (default `fail`). */
|
|
@@ -121,9 +199,22 @@ declare class ArtemisConsumer {
|
|
|
121
199
|
private reconcile;
|
|
122
200
|
private onUnknownUrn;
|
|
123
201
|
private retryOrDeadLetter;
|
|
202
|
+
/**
|
|
203
|
+
* Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry
|
|
204
|
+
* delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender
|
|
205
|
+
* the write is a no-op and the message is accepted (terminal drop), as before.
|
|
206
|
+
*/
|
|
207
|
+
private settleAfterDeadLetter;
|
|
124
208
|
private deadLetter;
|
|
209
|
+
/** Poison path: forward the raw body, confirmed like the gate path. */
|
|
125
210
|
private deadLetterRaw;
|
|
211
|
+
/** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */
|
|
212
|
+
private deadLetterRawConfirmed;
|
|
213
|
+
/** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
|
|
214
|
+
private releaseAfterBackoff;
|
|
215
|
+
/** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */
|
|
216
|
+
private safeReport;
|
|
126
217
|
private report;
|
|
127
218
|
}
|
|
128
219
|
|
|
129
|
-
export { type AmqpDelivery, type AmqpEventContext, type AmqpMessage, type AmqpReceiver, type AmqpSender, ArtemisConsumer, type ArtemisConsumerOptions, ArtemisPublisher, type BabelHandler, type BabelHandlers, JMS_TYPE_KEY, type PublishOptions, SCHEDULED_DELIVERY_KEY, artemisMessage, messageBody };
|
|
220
|
+
export { type AmqpDelivery, type AmqpEventContext, type AmqpLinkEventContext, type AmqpMessage, type AmqpReceiver, type AmqpSender, ArtemisConsumer, type ArtemisConsumerOptions, ArtemisPublisher, type BabelHandler, type BabelHandlers, JMS_TYPE_KEY, type PublishOptions, SCHEDULED_DELIVERY_KEY, artemisMessage, headersOf, messageBody };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Envelope, IncomingEnvelope } from '@babelqueue/core';
|
|
1
|
+
import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Apache ActiveMQ Artemis adapter for BabelQueue.
|
|
@@ -19,6 +19,25 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
|
|
|
19
19
|
*
|
|
20
20
|
* This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide
|
|
21
21
|
* the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).
|
|
22
|
+
*
|
|
23
|
+
* **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
|
|
24
|
+
* span linkage) rides as additional AMQP `application_properties` beside the contract `bq_`
|
|
25
|
+
* properties — the contract properties win a key collision (merge-not-clobber), blanks dropped.
|
|
26
|
+
* `traceparent`/`tracestate` are W3C keys with no hyphens, so they are already JMS-legal (§7 uses
|
|
27
|
+
* underscores because JMS property names must be Java identifiers). On consume the inbound
|
|
28
|
+
* `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract
|
|
29
|
+
* sees the `traceparent` and links the consumer span as a child. A header-less publish is
|
|
30
|
+
* byte-identical. GR-1: the wire envelope body is never touched.
|
|
31
|
+
*
|
|
32
|
+
* **Version gate (§7.7).** Before decoding the body the consumer reads the `bq_schema_version`
|
|
33
|
+
* application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII
|
|
34
|
+
* whitespace) follows the normal decode path; any other value is compared, untrimmed, to the supported
|
|
35
|
+
* `"1"`, and a message that does not match is rejected undecoded — reported to `onError`, the raw body
|
|
36
|
+
* forwarded to the DLQ sender (when configured) and the message accepted **only after the broker has
|
|
37
|
+
* accepted the DLQ delivery**. If the DLQ send fails, is refused (rejected/released/modified), the link
|
|
38
|
+
* closes or `deadLetterTimeoutMs` elapses, the failure is reported and, after `deadLetterRetryDelayMs`,
|
|
39
|
+
* the source message gets a plain `released` outcome (no `delivery_failed`: the broker's delivery
|
|
40
|
+
* counter and `max-delivery-attempts` stay untouched) — never accepted, never handed to a handler.
|
|
22
41
|
*/
|
|
23
42
|
|
|
24
43
|
/** An AMQP message (structural subset of a rhea message). */
|
|
@@ -47,9 +66,33 @@ interface AmqpEventContext {
|
|
|
47
66
|
message?: AmqpMessage;
|
|
48
67
|
delivery?: AmqpDelivery;
|
|
49
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The context rhea passes to a sender link event (`accepted`, `rejected`, `sender_close`, ...). Only
|
|
71
|
+
* `delivery` is inspected (to match a verdict to its delivery); everything else is opaque. Deliberately
|
|
72
|
+
* no index signature: an interface-typed handler parameter (e.g. rhea's `EventContext`) must stay assignable.
|
|
73
|
+
*/
|
|
74
|
+
interface AmqpLinkEventContext {
|
|
75
|
+
delivery?: unknown;
|
|
76
|
+
}
|
|
50
77
|
/** The subset of a rhea sender this adapter calls. */
|
|
51
78
|
interface AmqpSender {
|
|
79
|
+
/** Queue a delivery. rhea returns the delivery synchronously; the broker's verdict arrives later as an event. */
|
|
52
80
|
send(message: AmqpMessage): unknown;
|
|
81
|
+
/**
|
|
82
|
+
* Optional (a rhea sender has it). When both `on` and `removeListener` exist, the consumer's
|
|
83
|
+
* version-gate path waits for the broker's `accepted` disposition on the DLQ delivery before it
|
|
84
|
+
* settles the source message; without them only a synchronous `send` failure is observable.
|
|
85
|
+
*/
|
|
86
|
+
on?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;
|
|
87
|
+
removeListener?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;
|
|
88
|
+
/**
|
|
89
|
+
* Optional (a rhea link has it). rhea does not bubble an event to the session/connection while the
|
|
90
|
+
* link has a listener for it, so the temporary confirmation listeners re-dispatch what is not theirs
|
|
91
|
+
* (other deliveries' dispositions, link close/error) here.
|
|
92
|
+
*/
|
|
93
|
+
session?: {
|
|
94
|
+
dispatch?(event: string, context?: AmqpLinkEventContext): unknown;
|
|
95
|
+
};
|
|
53
96
|
}
|
|
54
97
|
/** The subset of a rhea receiver this adapter wires to (its `message` event). */
|
|
55
98
|
interface AmqpReceiver {
|
|
@@ -65,14 +108,28 @@ declare const SCHEDULED_DELIVERY_KEY = "x-opt-delivery-time";
|
|
|
65
108
|
* annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`
|
|
66
109
|
* sets the `x-opt-delivery-time` annotation for native scheduled delivery.
|
|
67
110
|
*/
|
|
68
|
-
declare function artemisMessage(envelope: Envelope, delayMs?: number): AmqpMessage;
|
|
111
|
+
declare function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage;
|
|
69
112
|
/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */
|
|
70
113
|
declare function messageBody(message: AmqpMessage): string;
|
|
114
|
+
/**
|
|
115
|
+
* Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying
|
|
116
|
+
* values defensively. Returns an empty object when there are none. Both the contract `bq_`
|
|
117
|
+
* properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract
|
|
118
|
+
* reads only the keys it knows.
|
|
119
|
+
*/
|
|
120
|
+
declare function headersOf(message: AmqpMessage): HeaderCarrier;
|
|
71
121
|
/** Options for {@link ArtemisPublisher.publish}. */
|
|
72
122
|
interface PublishOptions {
|
|
73
123
|
traceId?: string;
|
|
74
124
|
/** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */
|
|
75
125
|
delayMs?: number;
|
|
126
|
+
/**
|
|
127
|
+
* Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`
|
|
128
|
+
* properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.
|
|
129
|
+
* The contract properties win a key collision; an empty/omitted carrier leaves the message
|
|
130
|
+
* unchanged.
|
|
131
|
+
*/
|
|
132
|
+
headers?: HeaderCarrier;
|
|
76
133
|
}
|
|
77
134
|
/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */
|
|
78
135
|
declare class ArtemisPublisher {
|
|
@@ -84,14 +141,35 @@ declare class ArtemisPublisher {
|
|
|
84
141
|
/** Build + send the canonical envelope; returns the message id (`meta.id`). */
|
|
85
142
|
publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
|
|
86
143
|
}
|
|
87
|
-
/**
|
|
88
|
-
|
|
144
|
+
/**
|
|
145
|
+
* A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band
|
|
146
|
+
* {@link HeaderCarrier} read from the message's `application_properties` (empty when there are
|
|
147
|
+
* none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a
|
|
148
|
+
* child of the producer span (ADR-0028).
|
|
149
|
+
*/
|
|
150
|
+
type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
|
|
89
151
|
/** A map of URN → handler. */
|
|
90
152
|
type BabelHandlers = Record<string, BabelHandler>;
|
|
91
153
|
/** Options for {@link ArtemisConsumer}. */
|
|
92
154
|
interface ArtemisConsumerOptions {
|
|
93
155
|
/** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */
|
|
94
156
|
deadLetterSender?: AmqpSender;
|
|
157
|
+
/**
|
|
158
|
+
* How long (ms) every dead-letter path (version gate, poison, unknown-URN `dead_letter`, terminal
|
|
159
|
+
* handler failure) waits for the broker to accept the DLQ delivery before it releases the source
|
|
160
|
+
* message instead of accepting it (default 30000).
|
|
161
|
+
*/
|
|
162
|
+
deadLetterTimeoutMs?: number;
|
|
163
|
+
/**
|
|
164
|
+
* Pause (ms) before a dead-letter path releases a message whose DLQ write failed (default 1000).
|
|
165
|
+
* The release is a plain `released` outcome (no `delivery_failed`, so the broker's delivery counter and
|
|
166
|
+
* `max-delivery-attempts` are untouched); the pause keeps a persistently failing DLQ from becoming a
|
|
167
|
+
* hot redelivery loop. The delivery stays unsettled (holding one credit) while it waits.
|
|
168
|
+
* Note: on the terminal handler-failure path the message has already been through the handler, and
|
|
169
|
+
* a plain release does not bump the delivery counter, so the redelivery runs the handler again (and
|
|
170
|
+
* reports its failure again) until the DLQ write succeeds.
|
|
171
|
+
*/
|
|
172
|
+
deadLetterRetryDelayMs?: number;
|
|
95
173
|
/** Attempts before terminal dead-lettering (default 3). */
|
|
96
174
|
maxTries?: number;
|
|
97
175
|
/** Strategy for a URN with no handler (default `fail`). */
|
|
@@ -121,9 +199,22 @@ declare class ArtemisConsumer {
|
|
|
121
199
|
private reconcile;
|
|
122
200
|
private onUnknownUrn;
|
|
123
201
|
private retryOrDeadLetter;
|
|
202
|
+
/**
|
|
203
|
+
* Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry
|
|
204
|
+
* delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender
|
|
205
|
+
* the write is a no-op and the message is accepted (terminal drop), as before.
|
|
206
|
+
*/
|
|
207
|
+
private settleAfterDeadLetter;
|
|
124
208
|
private deadLetter;
|
|
209
|
+
/** Poison path: forward the raw body, confirmed like the gate path. */
|
|
125
210
|
private deadLetterRaw;
|
|
211
|
+
/** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */
|
|
212
|
+
private deadLetterRawConfirmed;
|
|
213
|
+
/** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
|
|
214
|
+
private releaseAfterBackoff;
|
|
215
|
+
/** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */
|
|
216
|
+
private safeReport;
|
|
126
217
|
private report;
|
|
127
218
|
}
|
|
128
219
|
|
|
129
|
-
export { type AmqpDelivery, type AmqpEventContext, type AmqpMessage, type AmqpReceiver, type AmqpSender, ArtemisConsumer, type ArtemisConsumerOptions, ArtemisPublisher, type BabelHandler, type BabelHandlers, JMS_TYPE_KEY, type PublishOptions, SCHEDULED_DELIVERY_KEY, artemisMessage, messageBody };
|
|
220
|
+
export { type AmqpDelivery, type AmqpEventContext, type AmqpLinkEventContext, type AmqpMessage, type AmqpReceiver, type AmqpSender, ArtemisConsumer, type ArtemisConsumerOptions, ArtemisPublisher, type BabelHandler, type BabelHandlers, JMS_TYPE_KEY, type PublishOptions, SCHEDULED_DELIVERY_KEY, artemisMessage, headersOf, messageBody };
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from "@babelqueue/core";
|
|
2
|
+
import { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError, UnknownUrnStrategy } from "@babelqueue/core";
|
|
3
|
+
|
|
4
|
+
// src/headers.ts
|
|
5
|
+
function sanitizeHeaders(headers) {
|
|
6
|
+
const out = {};
|
|
7
|
+
if (!headers) return out;
|
|
8
|
+
for (const key of Object.keys(headers)) {
|
|
9
|
+
const value = headers[key];
|
|
10
|
+
if (key === "" || value == null || value === "") continue;
|
|
11
|
+
out[key] = value;
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/index.ts
|
|
3
17
|
var JMS_TYPE_KEY = "x-opt-jms-type";
|
|
4
18
|
var SCHEDULED_DELIVERY_KEY = "x-opt-delivery-time";
|
|
5
|
-
function artemisMessage(envelope, delayMs) {
|
|
19
|
+
function artemisMessage(envelope, delayMs, extra) {
|
|
6
20
|
const annotations = {};
|
|
7
21
|
if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;
|
|
8
|
-
const applicationProperties = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
22
|
+
const applicationProperties = {};
|
|
23
|
+
for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;
|
|
24
|
+
applicationProperties["bq_schema_version"] = String(envelope.meta.schema_version);
|
|
25
|
+
applicationProperties["bq_attempts"] = String(envelope.attempts ?? 0);
|
|
26
|
+
applicationProperties["bq_app_id"] = "babelqueue";
|
|
13
27
|
if (envelope.meta.lang) applicationProperties["bq_source_lang"] = envelope.meta.lang;
|
|
14
28
|
const message = {
|
|
15
29
|
body: EnvelopeCodec.encode(envelope),
|
|
@@ -41,10 +55,30 @@ function jmsType(message) {
|
|
|
41
55
|
const value = message.message_annotations?.[JMS_TYPE_KEY];
|
|
42
56
|
return value == null ? void 0 : String(value);
|
|
43
57
|
}
|
|
58
|
+
var BLANK_ASCII = /^[ \t\n\v\f\r]*$/;
|
|
59
|
+
function unsupportedSchemaVersion(message) {
|
|
60
|
+
const raw = message.application_properties?.["bq_schema_version"];
|
|
61
|
+
if (raw == null) return void 0;
|
|
62
|
+
const value = Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw);
|
|
63
|
+
if (BLANK_ASCII.test(value)) return void 0;
|
|
64
|
+
return value === String(SCHEMA_VERSION) ? void 0 : value;
|
|
65
|
+
}
|
|
44
66
|
function deliveryCount(message) {
|
|
45
67
|
const value = message.delivery_count;
|
|
46
68
|
return typeof value === "number" && value > 0 ? value : 0;
|
|
47
69
|
}
|
|
70
|
+
function headersOf(message) {
|
|
71
|
+
const props = message.application_properties;
|
|
72
|
+
const out = {};
|
|
73
|
+
if (!props) return out;
|
|
74
|
+
for (const key of Object.keys(props)) {
|
|
75
|
+
const value = props[key];
|
|
76
|
+
if (value == null) continue;
|
|
77
|
+
const s = String(value);
|
|
78
|
+
if (s !== "") out[key] = s;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
48
82
|
var ArtemisPublisher = class _ArtemisPublisher {
|
|
49
83
|
constructor(sender, queue) {
|
|
50
84
|
this.sender = sender;
|
|
@@ -59,10 +93,107 @@ var ArtemisPublisher = class _ArtemisPublisher {
|
|
|
59
93
|
/** Build + send the canonical envelope; returns the message id (`meta.id`). */
|
|
60
94
|
async publish(urn, data, options = {}) {
|
|
61
95
|
const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });
|
|
62
|
-
await this.sender.send(artemisMessage(envelope, options.delayMs));
|
|
96
|
+
await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));
|
|
63
97
|
return envelope.meta.id;
|
|
64
98
|
}
|
|
65
99
|
};
|
|
100
|
+
var DEFAULT_DLQ_CONFIRM_TIMEOUT_MS = 3e4;
|
|
101
|
+
var DEFAULT_DLQ_RETRY_DELAY_MS = 1e3;
|
|
102
|
+
var CONFIRMATION_VERDICTS = ["accepted", "rejected", "released", "modified"];
|
|
103
|
+
var CONFIRMATION_CLOSES = ["sender_close", "sender_error"];
|
|
104
|
+
var ConfirmationHub = class {
|
|
105
|
+
constructor(sender, on, off) {
|
|
106
|
+
this.sender = sender;
|
|
107
|
+
this.on = on;
|
|
108
|
+
this.off = off;
|
|
109
|
+
}
|
|
110
|
+
sender;
|
|
111
|
+
on;
|
|
112
|
+
off;
|
|
113
|
+
pending = /* @__PURE__ */ new Set();
|
|
114
|
+
handlers = [];
|
|
115
|
+
add(confirmation) {
|
|
116
|
+
this.pending.add(confirmation);
|
|
117
|
+
if (this.handlers.length === 0) {
|
|
118
|
+
for (const name of [...CONFIRMATION_VERDICTS, ...CONFIRMATION_CLOSES]) {
|
|
119
|
+
const handler = (context) => this.dispatch(name, context);
|
|
120
|
+
this.handlers.push([name, handler]);
|
|
121
|
+
this.on(name, handler);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
remove(confirmation) {
|
|
126
|
+
this.pending.delete(confirmation);
|
|
127
|
+
if (this.pending.size === 0) {
|
|
128
|
+
for (const [name, handler] of this.handlers) this.off(name, handler);
|
|
129
|
+
this.handlers.length = 0;
|
|
130
|
+
hubs.delete(this.sender);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
dispatch(name, context) {
|
|
134
|
+
if (CONFIRMATION_CLOSES.includes(name)) {
|
|
135
|
+
for (const confirmation of [...this.pending]) {
|
|
136
|
+
confirmation.settle(new BabelQueueError(`The dead-letter link closed before the broker accepted the delivery (${name}).`));
|
|
137
|
+
}
|
|
138
|
+
this.forward(name, context);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const owners = [...this.pending].filter((confirmation) => confirmation.matches(context));
|
|
142
|
+
if (owners.length === 0) {
|
|
143
|
+
this.forward(name, context);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
for (const owner of owners) {
|
|
147
|
+
owner.settle(name === "accepted" ? void 0 : new BabelQueueError(`The broker did not accept the dead-letter delivery (${name}).`));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
forward(name, context) {
|
|
151
|
+
try {
|
|
152
|
+
this.sender.session?.dispatch?.(name, context);
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
var hubs = /* @__PURE__ */ new WeakMap();
|
|
158
|
+
function sendConfirmed(sender, message, timeoutMs) {
|
|
159
|
+
const on = sender.on?.bind(sender);
|
|
160
|
+
const off = sender.removeListener?.bind(sender);
|
|
161
|
+
if (!on || !off) return Promise.resolve(sender.send(message)).then(() => void 0);
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
let hub = hubs.get(sender);
|
|
164
|
+
if (!hub) {
|
|
165
|
+
hub = new ConfirmationHub(sender, on, off);
|
|
166
|
+
hubs.set(sender, hub);
|
|
167
|
+
}
|
|
168
|
+
const activeHub = hub;
|
|
169
|
+
let finished = false;
|
|
170
|
+
const confirmation = {
|
|
171
|
+
delivery: void 0,
|
|
172
|
+
matches: (context) => confirmation.delivery === void 0 || context?.delivery === void 0 || context.delivery === confirmation.delivery,
|
|
173
|
+
settle: (error) => {
|
|
174
|
+
if (finished) return;
|
|
175
|
+
finished = true;
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
activeHub.remove(confirmation);
|
|
178
|
+
if (error) reject(error);
|
|
179
|
+
else resolve();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
activeHub.add(confirmation);
|
|
183
|
+
const timer = setTimeout(
|
|
184
|
+
() => confirmation.settle(new BabelQueueError(`Timed out after ${timeoutMs}ms waiting for the broker to accept the dead-letter delivery.`)),
|
|
185
|
+
timeoutMs
|
|
186
|
+
);
|
|
187
|
+
try {
|
|
188
|
+
confirmation.delivery = sender.send(message);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
confirmation.settle(error instanceof Error ? error : new BabelQueueError(String(error)));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const delivery = confirmation.delivery;
|
|
194
|
+
if (delivery === null || typeof delivery !== "object" || delivery.settled === true) confirmation.settle();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
66
197
|
var ArtemisConsumer = class {
|
|
67
198
|
constructor(handlers, options = {}) {
|
|
68
199
|
this.handlers = handlers;
|
|
@@ -82,11 +213,27 @@ var ArtemisConsumer = class {
|
|
|
82
213
|
async handle(context) {
|
|
83
214
|
const message = context.message ?? {};
|
|
84
215
|
const delivery = context.delivery;
|
|
216
|
+
const unsupported = unsupportedSchemaVersion(message);
|
|
217
|
+
if (unsupported !== void 0) {
|
|
218
|
+
const gateError = new BabelQueueError(
|
|
219
|
+
`Rejected a message from Artemis with unsupported bq_schema_version "${unsupported}" (supported: ${SCHEMA_VERSION}).`
|
|
220
|
+
);
|
|
221
|
+
try {
|
|
222
|
+
await this.deadLetterRawConfirmed(messageBody(message));
|
|
223
|
+
} catch (dlqError) {
|
|
224
|
+
this.safeReport(gateError, null, context);
|
|
225
|
+
this.safeReport(dlqError, null, context);
|
|
226
|
+
await this.releaseAfterBackoff(delivery);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
delivery?.accept();
|
|
230
|
+
this.safeReport(gateError, null, context);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
85
233
|
const decoded = EnvelopeCodec.decode(messageBody(message));
|
|
86
234
|
if (!EnvelopeCodec.accepts(decoded)) {
|
|
87
235
|
this.report(new BabelQueueError("Rejected a non-conformant BabelQueue envelope from Artemis."), decoded, context);
|
|
88
|
-
await this.deadLetterRaw(messageBody(message));
|
|
89
|
-
delivery?.accept();
|
|
236
|
+
await this.settleAfterDeadLetter(context, decoded, () => this.deadLetterRaw(messageBody(message)));
|
|
90
237
|
return;
|
|
91
238
|
}
|
|
92
239
|
const envelope = this.reconcile(decoded, message);
|
|
@@ -97,7 +244,7 @@ var ArtemisConsumer = class {
|
|
|
97
244
|
return;
|
|
98
245
|
}
|
|
99
246
|
try {
|
|
100
|
-
await handler(envelope, message);
|
|
247
|
+
await handler(envelope, message, headersOf(message));
|
|
101
248
|
delivery?.accept();
|
|
102
249
|
} catch (error) {
|
|
103
250
|
this.report(error, envelope, context);
|
|
@@ -117,8 +264,7 @@ var ArtemisConsumer = class {
|
|
|
117
264
|
delivery?.accept();
|
|
118
265
|
return;
|
|
119
266
|
case UnknownUrnStrategy.DEAD_LETTER:
|
|
120
|
-
await this.deadLetter(envelope, "unknown_urn");
|
|
121
|
-
delivery?.accept();
|
|
267
|
+
await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "unknown_urn"));
|
|
122
268
|
return;
|
|
123
269
|
case UnknownUrnStrategy.RELEASE:
|
|
124
270
|
delivery?.release({ delivery_failed: true });
|
|
@@ -135,12 +281,26 @@ var ArtemisConsumer = class {
|
|
|
135
281
|
if ((envelope.attempts ?? 0) + 1 < this.maxTries) {
|
|
136
282
|
delivery?.release({ delivery_failed: true });
|
|
137
283
|
} else if (this.options.deadLetterSender) {
|
|
138
|
-
await this.deadLetter(envelope, "failed", error);
|
|
139
|
-
delivery?.accept();
|
|
284
|
+
await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, "failed", error));
|
|
140
285
|
} else {
|
|
141
286
|
delivery?.accept();
|
|
142
287
|
}
|
|
143
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry
|
|
291
|
+
* delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender
|
|
292
|
+
* the write is a no-op and the message is accepted (terminal drop), as before.
|
|
293
|
+
*/
|
|
294
|
+
async settleAfterDeadLetter(context, envelope, write) {
|
|
295
|
+
try {
|
|
296
|
+
await write();
|
|
297
|
+
} catch (dlqError) {
|
|
298
|
+
this.safeReport(dlqError, envelope, context);
|
|
299
|
+
await this.releaseAfterBackoff(context.delivery);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
context.delivery?.accept();
|
|
303
|
+
}
|
|
144
304
|
async deadLetter(envelope, reason, error) {
|
|
145
305
|
const sender = this.options.deadLetterSender;
|
|
146
306
|
if (!sender) return;
|
|
@@ -150,12 +310,30 @@ var ArtemisConsumer = class {
|
|
|
150
310
|
error: error instanceof Error ? error.message : null,
|
|
151
311
|
exception: error instanceof Error ? error.name : null
|
|
152
312
|
});
|
|
153
|
-
await sender
|
|
313
|
+
await sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
|
|
154
314
|
}
|
|
155
|
-
|
|
315
|
+
/** Poison path: forward the raw body, confirmed like the gate path. */
|
|
316
|
+
deadLetterRaw(raw) {
|
|
317
|
+
return this.deadLetterRawConfirmed(raw);
|
|
318
|
+
}
|
|
319
|
+
/** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */
|
|
320
|
+
async deadLetterRawConfirmed(raw) {
|
|
156
321
|
const sender = this.options.deadLetterSender;
|
|
157
322
|
if (!sender) return;
|
|
158
|
-
await sender
|
|
323
|
+
await sendConfirmed(sender, { body: raw }, this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);
|
|
324
|
+
}
|
|
325
|
+
/** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */
|
|
326
|
+
async releaseAfterBackoff(delivery) {
|
|
327
|
+
const delayMs = this.options.deadLetterRetryDelayMs ?? DEFAULT_DLQ_RETRY_DELAY_MS;
|
|
328
|
+
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs).unref?.());
|
|
329
|
+
delivery?.release();
|
|
330
|
+
}
|
|
331
|
+
/** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */
|
|
332
|
+
safeReport(error, envelope, context) {
|
|
333
|
+
try {
|
|
334
|
+
this.report(error, envelope, context);
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
159
337
|
}
|
|
160
338
|
report(error, envelope, context) {
|
|
161
339
|
this.options.onError?.(error, envelope, context);
|
|
@@ -167,6 +345,7 @@ export {
|
|
|
167
345
|
JMS_TYPE_KEY,
|
|
168
346
|
SCHEDULED_DELIVERY_KEY,
|
|
169
347
|
artemisMessage,
|
|
348
|
+
headersOf,
|
|
170
349
|
messageBody
|
|
171
350
|
};
|
|
172
351
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Apache ActiveMQ Artemis adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **AMQP 1.0** consumer over Artemis (rhea),\n * so an Artemis-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the Java, .NET, Python and Go SDKs.\n *\n * Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —\n * per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so\n * this adapter maps onto them (the envelope stays `schema_version: 1`): the envelope JSON is the\n * message **body**; the contract fields are mirrored onto the AMQP a JMS peer reads\n * (`correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN) plus the `bq_` application properties; consume settles per message\n * (`accept` after success, `release` for redelivery); **`attempts = max(body, delivery-count)`**\n * (the AMQP counter is 0-based — no −1); terminal failures go to an opt-in `<queue>.dlq` with a\n * `dead_letter` block.\n *\n * This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide\n * the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, IncomingEnvelope } from \"@babelqueue/core\";\n\n// --- Minimal AMQP 1.0 shapes (a structural subset of rhea) ---------------------\n\n/** An AMQP message (structural subset of a rhea message). */\nexport interface AmqpMessage {\n body?: unknown;\n correlation_id?: unknown;\n creation_time?: number | Date;\n content_type?: string;\n application_properties?: Record<string, unknown>;\n message_annotations?: Record<string, unknown>;\n /** The AMQP header delivery-count (0-based: 0 on first delivery). */\n delivery_count?: number;\n}\n\n/** The disposition handle rhea passes alongside a received message. */\nexport interface AmqpDelivery {\n /** Settle (acknowledge) the message — it is removed from the queue. */\n accept(): void;\n /** Return the message for redelivery — the broker increments `delivery-count`. */\n release(params?: { delivery_failed?: boolean; undeliverable_here?: boolean }): void;\n}\n\n/** The event context rhea emits on a `message` event. */\nexport interface AmqpEventContext {\n message?: AmqpMessage;\n delivery?: AmqpDelivery;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n send(message: AmqpMessage): unknown;\n}\n\n/** The subset of a rhea receiver this adapter wires to (its `message` event). */\nexport interface AmqpReceiver {\n on(event: \"message\", handler: (context: AmqpEventContext) => void): unknown;\n}\n\n// --- Projection (contract §7.2) ------------------------------------------------\n\n/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */\nexport const JMS_TYPE_KEY = \"x-opt-jms-type\";\n/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */\nexport const SCHEDULED_DELIVERY_KEY = \"x-opt-delivery-time\";\n\n/**\n * Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,\n * `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`\n * sets the `x-opt-delivery-time` annotation for native scheduled delivery.\n */\nexport function artemisMessage(envelope: Envelope, delayMs?: number): AmqpMessage {\n const annotations: Record<string, unknown> = {};\n if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;\n\n // The bq_ property names use underscores, not hyphens: a JMS property name must be a valid\n // Java identifier, and every Artemis SDK uses the same JMS-legal form for cross-protocol parity.\n const applicationProperties: Record<string, unknown> = {\n \"bq_schema_version\": String(envelope.meta.schema_version),\n \"bq_attempts\": String(envelope.attempts ?? 0),\n \"bq_app_id\": \"babelqueue\",\n };\n if (envelope.meta.lang) applicationProperties[\"bq_source_lang\"] = envelope.meta.lang;\n\n const message: AmqpMessage = {\n body: EnvelopeCodec.encode(envelope),\n content_type: \"application/json\",\n application_properties: applicationProperties,\n message_annotations: annotations,\n };\n if (envelope.trace_id) message.correlation_id = envelope.trace_id;\n if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;\n\n if (delayMs != null && delayMs > 0) {\n applicationProperties[\"bq_delay\"] = String(delayMs);\n annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;\n }\n return message;\n}\n\n/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */\nexport function messageBody(message: AmqpMessage): string {\n const body = message.body;\n if (body == null) return \"\";\n if (typeof body === \"string\") return body;\n if (Buffer.isBuffer(body)) return body.toString(\"utf8\");\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content.toString(\"utf8\");\n if (typeof content === \"string\") return content;\n }\n return String(body);\n}\n\nfunction jmsType(message: AmqpMessage): string | undefined {\n const value = message.message_annotations?.[JMS_TYPE_KEY];\n return value == null ? undefined : String(value);\n}\n\nfunction deliveryCount(message: AmqpMessage): number {\n const value = message.delivery_count;\n return typeof value === \"number\" && value > 0 ? value : 0;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link ArtemisPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */\n delayMs?: number;\n}\n\n/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */\nexport class ArtemisPublisher {\n private constructor(\n private readonly sender: AmqpSender,\n private readonly queue: string,\n ) {}\n\n /** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */\n static create(sender: AmqpSender, queue: string): ArtemisPublisher {\n return new ArtemisPublisher(sender, queue);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });\n await this.sender.send(artemisMessage(envelope, options.delayMs));\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/** A URN handler. Receives the validated envelope and the raw AMQP message. */\nexport type BabelHandler = (envelope: Envelope, message: AmqpMessage) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link ArtemisConsumer}. */\nexport interface ArtemisConsumerOptions {\n /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */\n deadLetterSender?: AmqpSender;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a non-conformant message, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;\n}\n\n/**\n * Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the\n * handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A\n * throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP\n * `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a\n * `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP\n * counter is 0-based, so no −1.\n */\nexport class ArtemisConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly handlers: BabelHandlers,\n private readonly options: ArtemisConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */\n listen(receiver: AmqpReceiver): void {\n receiver.on(\"message\", (context) => this.handle(context));\n }\n\n /** Route + settle one delivery. Exposed for testing. */\n async handle(context: AmqpEventContext): Promise<void> {\n const message = context.message ?? {};\n const delivery = context.delivery;\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n // A non-conformant / poison message may lack the fields to annotate; forward it raw.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\"), decoded, context);\n await this.deadLetterRaw(messageBody(message));\n delivery?.accept();\n return;\n }\n\n const envelope = this.reconcile(decoded, message);\n const urn = jmsType(message) ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(context, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message);\n delivery?.accept();\n } catch (error) {\n this.report(error, envelope, context);\n await this.retryOrDeadLetter(context, envelope, error);\n }\n }\n\n /** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */\n private reconcile(envelope: IncomingEnvelope, message: AmqpMessage): Envelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n const count = deliveryCount(message);\n return { ...envelope, attempts: Math.max(body, count) } as Envelope;\n }\n\n private async onUnknownUrn(context: AmqpEventContext, envelope: Envelope, urn: string): Promise<void> {\n const delivery = context.delivery;\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n delivery?.accept();\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.deadLetter(envelope, \"unknown_urn\");\n delivery?.accept();\n return;\n case UnknownUrnStrategy.RELEASE:\n delivery?.release({ delivery_failed: true });\n return;\n default: {\n // FAIL: surface and do NOT settle — the broker redelivers, then dead-letters.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, context);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(context: AmqpEventContext, envelope: Envelope, error: unknown): Promise<void> {\n const delivery = context.delivery;\n if ((envelope.attempts ?? 0) + 1 < this.maxTries) {\n // Release with delivery-failed leaves it for redelivery — the broker bumps delivery-count.\n delivery?.release({ delivery_failed: true });\n } else if (this.options.deadLetterSender) {\n await this.deadLetter(envelope, \"failed\", error);\n delivery?.accept();\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n private async deadLetter(envelope: Envelope, reason: string, error?: unknown): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n const original = envelope.meta?.queue ?? \"\";\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n await sender.send(artemisMessage(annotated));\n }\n\n private async deadLetterRaw(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sender.send({ body: raw });\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n this.options.onError?.(error, envelope, context);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache ActiveMQ Artemis adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **AMQP 1.0** consumer over Artemis (rhea),\n * so an Artemis-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the Java, .NET, Python and Go SDKs.\n *\n * Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —\n * per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so\n * this adapter maps onto them (the envelope stays `schema_version: 1`): the envelope JSON is the\n * message **body**; the contract fields are mirrored onto the AMQP a JMS peer reads\n * (`correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN) plus the `bq_` application properties; consume settles per message\n * (`accept` after success, `release` for redelivery); **`attempts = max(body, delivery-count)`**\n * (the AMQP counter is 0-based — no −1); terminal failures go to an opt-in `<queue>.dlq` with a\n * `dead_letter` block.\n *\n * This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide\n * the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional AMQP `application_properties` beside the contract `bq_`\n * properties — the contract properties win a key collision (merge-not-clobber), blanks dropped.\n * `traceparent`/`tracestate` are W3C keys with no hyphens, so they are already JMS-legal (§7 uses\n * underscores because JMS property names must be Java identifiers). On consume the inbound\n * `application_properties` are surfaced as a `Record<string,string>` so the core's `otel` extract\n * sees the `traceparent` and links the consumer span as a child. A header-less publish is\n * byte-identical. GR-1: the wire envelope body is never touched.\n *\n * **Version gate (§7.7).** Before decoding the body the consumer reads the `bq_schema_version`\n * application property (underscores, ADR-0017). A missing or blank value (empty or only ASCII\n * whitespace) follows the normal decode path; any other value is compared, untrimmed, to the supported\n * `\"1\"`, and a message that does not match is rejected undecoded — reported to `onError`, the raw body\n * forwarded to the DLQ sender (when configured) and the message accepted **only after the broker has\n * accepted the DLQ delivery**. If the DLQ send fails, is refused (rejected/released/modified), the link\n * closes or `deadLetterTimeoutMs` elapses, the failure is reported and, after `deadLetterRetryDelayMs`,\n * the source message gets a plain `released` outcome (no `delivery_failed`: the broker's delivery\n * counter and `max-delivery-attempts` stay untouched) — never accepted, never handed to a handler.\n */\n\nimport { annotate, BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError, UnknownUrnStrategy } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { sanitizeHeaders } from \"./headers.js\";\n\n// --- Minimal AMQP 1.0 shapes (a structural subset of rhea) ---------------------\n\n/** An AMQP message (structural subset of a rhea message). */\nexport interface AmqpMessage {\n body?: unknown;\n correlation_id?: unknown;\n creation_time?: number | Date;\n content_type?: string;\n application_properties?: Record<string, unknown>;\n message_annotations?: Record<string, unknown>;\n /** The AMQP header delivery-count (0-based: 0 on first delivery). */\n delivery_count?: number;\n}\n\n/** The disposition handle rhea passes alongside a received message. */\nexport interface AmqpDelivery {\n /** Settle (acknowledge) the message — it is removed from the queue. */\n accept(): void;\n /** Return the message for redelivery — the broker increments `delivery-count`. */\n release(params?: { delivery_failed?: boolean; undeliverable_here?: boolean }): void;\n}\n\n/** The event context rhea emits on a `message` event. */\nexport interface AmqpEventContext {\n message?: AmqpMessage;\n delivery?: AmqpDelivery;\n}\n\n/**\n * The context rhea passes to a sender link event (`accepted`, `rejected`, `sender_close`, ...). Only\n * `delivery` is inspected (to match a verdict to its delivery); everything else is opaque. Deliberately\n * no index signature: an interface-typed handler parameter (e.g. rhea's `EventContext`) must stay assignable.\n */\nexport interface AmqpLinkEventContext {\n delivery?: unknown;\n}\n\n/** The subset of a rhea sender this adapter calls. */\nexport interface AmqpSender {\n /** Queue a delivery. rhea returns the delivery synchronously; the broker's verdict arrives later as an event. */\n send(message: AmqpMessage): unknown;\n /**\n * Optional (a rhea sender has it). When both `on` and `removeListener` exist, the consumer's\n * version-gate path waits for the broker's `accepted` disposition on the DLQ delivery before it\n * settles the source message; without them only a synchronous `send` failure is observable.\n */\n on?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;\n removeListener?(event: string, handler: (context: AmqpLinkEventContext) => void): unknown;\n /**\n * Optional (a rhea link has it). rhea does not bubble an event to the session/connection while the\n * link has a listener for it, so the temporary confirmation listeners re-dispatch what is not theirs\n * (other deliveries' dispositions, link close/error) here.\n */\n session?: { dispatch?(event: string, context?: AmqpLinkEventContext): unknown };\n}\n\n/** The subset of a rhea receiver this adapter wires to (its `message` event). */\nexport interface AmqpReceiver {\n on(event: \"message\", handler: (context: AmqpEventContext) => void): unknown;\n}\n\n// --- Projection (contract §7.2) ------------------------------------------------\n\n/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */\nexport const JMS_TYPE_KEY = \"x-opt-jms-type\";\n/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */\nexport const SCHEDULED_DELIVERY_KEY = \"x-opt-delivery-time\";\n\n/**\n * Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,\n * `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`\n * annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`\n * sets the `x-opt-delivery-time` annotation for native scheduled delivery.\n */\nexport function artemisMessage(envelope: Envelope, delayMs?: number, extra?: HeaderCarrier | null): AmqpMessage {\n const annotations: Record<string, unknown> = {};\n if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;\n\n // Out-of-band riders (e.g. traceparent) go in first; the contract bq_ properties below win any\n // key collision (merge-not-clobber). The bq_ names use underscores, not hyphens: a JMS property\n // name must be a valid Java identifier, and every Artemis SDK uses the same JMS-legal form for\n // cross-protocol parity. traceparent/tracestate have no hyphens, so they are already JMS-legal.\n const applicationProperties: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;\n applicationProperties[\"bq_schema_version\"] = String(envelope.meta.schema_version);\n applicationProperties[\"bq_attempts\"] = String(envelope.attempts ?? 0);\n applicationProperties[\"bq_app_id\"] = \"babelqueue\";\n if (envelope.meta.lang) applicationProperties[\"bq_source_lang\"] = envelope.meta.lang;\n\n const message: AmqpMessage = {\n body: EnvelopeCodec.encode(envelope),\n content_type: \"application/json\",\n application_properties: applicationProperties,\n message_annotations: annotations,\n };\n if (envelope.trace_id) message.correlation_id = envelope.trace_id;\n if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;\n\n if (delayMs != null && delayMs > 0) {\n applicationProperties[\"bq_delay\"] = String(delayMs);\n annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;\n }\n return message;\n}\n\n/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */\nexport function messageBody(message: AmqpMessage): string {\n const body = message.body;\n if (body == null) return \"\";\n if (typeof body === \"string\") return body;\n if (Buffer.isBuffer(body)) return body.toString(\"utf8\");\n if (typeof body === \"object\" && \"content\" in body) {\n const content = (body as { content: unknown }).content;\n if (Buffer.isBuffer(content)) return content.toString(\"utf8\");\n if (typeof content === \"string\") return content;\n }\n return String(body);\n}\n\nfunction jmsType(message: AmqpMessage): string | undefined {\n const value = message.message_annotations?.[JMS_TYPE_KEY];\n return value == null ? undefined : String(value);\n}\n\n/** Blank = empty or only ASCII whitespace (GR-5: the same set in every SDK, never `String.trim()`). */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The inbound `bq_schema_version` application property when it is present, non-blank and not exactly\n * the supported version (compared untrimmed), else `undefined` (the message may be decoded).\n */\nfunction unsupportedSchemaVersion(message: AmqpMessage): string | undefined {\n const raw = message.application_properties?.[\"bq_schema_version\"];\n if (raw == null) return undefined;\n const value = Buffer.isBuffer(raw) ? raw.toString(\"utf8\") : String(raw);\n if (BLANK_ASCII.test(value)) return undefined;\n return value === String(SCHEMA_VERSION) ? undefined : value;\n}\n\nfunction deliveryCount(message: AmqpMessage): number {\n const value = message.delivery_count;\n return typeof value === \"number\" && value > 0 ? value : 0;\n}\n\n/**\n * Read a message's `application_properties` back into a flat {@link HeaderCarrier}, stringifying\n * values defensively. Returns an empty object when there are none. Both the contract `bq_`\n * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract\n * reads only the keys it knows.\n */\nexport function headersOf(message: AmqpMessage): HeaderCarrier {\n const props = message.application_properties;\n const out: HeaderCarrier = {};\n if (!props) return out;\n for (const key of Object.keys(props)) {\n const value = props[key];\n if (value == null) continue;\n const s = String(value);\n if (s !== \"\") out[key] = s;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link ArtemisPublisher.publish}. */\nexport interface PublishOptions {\n traceId?: string;\n /** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as AMQP `application_properties` beside the contract `bq_`\n * properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`.\n * The contract properties win a key collision; an empty/omitted carrier leaves the message\n * unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */\nexport class ArtemisPublisher {\n private constructor(\n private readonly sender: AmqpSender,\n private readonly queue: string,\n ) {}\n\n /** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */\n static create(sender: AmqpSender, queue: string): ArtemisPublisher {\n return new ArtemisPublisher(sender, queue);\n }\n\n /** Build + send the canonical envelope; returns the message id (`meta.id`). */\n async publish(urn: string, data: Record<string, unknown>, options: PublishOptions = {}): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });\n await this.sender.send(artemisMessage(envelope, options.delayMs, options.headers));\n return envelope.meta.id;\n }\n}\n\n// --- DLQ delivery confirmation ---------------------------------------------------\n\n/** Default wait (ms) for the broker to settle a gate-path DLQ delivery. */\nconst DEFAULT_DLQ_CONFIRM_TIMEOUT_MS = 30_000;\n/** Default pause (ms) before a gate message whose DLQ write failed is released back to the broker. */\nconst DEFAULT_DLQ_RETRY_DELAY_MS = 1_000;\n\n/** One in-flight DLQ confirmation registered on a {@link ConfirmationHub}. */\ninterface PendingConfirmation {\n /** The delivery `send` returned; `undefined` while `send` is still running synchronously. */\n delivery: unknown;\n /** `true` when `context` is (or may be) this confirmation's verdict. */\n matches(context: AmqpLinkEventContext | undefined): boolean;\n /** Resolve (no argument) or reject the confirmation. */\n settle(error?: Error): void;\n}\n\nconst CONFIRMATION_VERDICTS = [\"accepted\", \"rejected\", \"released\", \"modified\"] as const;\nconst CONFIRMATION_CLOSES = [\"sender_close\", \"sender_error\"] as const;\n\n/**\n * One shared set of temporary sender listeners per {@link AmqpSender}, however many DLQ confirmations\n * are in flight. rhea bubbles a link event to the session only while the link has no listener for it,\n * so while confirmations are pending the hub owns the listeners and re-dispatches every event it does\n * not consume to `sender.session` exactly once (per-confirmation listeners would forward each event\n * once per pending confirmation). The listeners are removed as soon as no confirmation is pending.\n */\nclass ConfirmationHub {\n private readonly pending = new Set<PendingConfirmation>();\n private readonly handlers: [string, (context: AmqpLinkEventContext) => void][] = [];\n\n constructor(\n private readonly sender: AmqpSender,\n private readonly on: (event: string, handler: (context: AmqpLinkEventContext) => void) => unknown,\n private readonly off: (event: string, handler: (context: AmqpLinkEventContext) => void) => unknown,\n ) {}\n\n add(confirmation: PendingConfirmation): void {\n this.pending.add(confirmation);\n if (this.handlers.length === 0) {\n for (const name of [...CONFIRMATION_VERDICTS, ...CONFIRMATION_CLOSES]) {\n const handler = (context: AmqpLinkEventContext): void => this.dispatch(name, context);\n this.handlers.push([name, handler]);\n this.on(name, handler);\n }\n }\n }\n\n remove(confirmation: PendingConfirmation): void {\n this.pending.delete(confirmation);\n if (this.pending.size === 0) {\n for (const [name, handler] of this.handlers) this.off(name, handler);\n this.handlers.length = 0;\n hubs.delete(this.sender);\n }\n }\n\n private dispatch(name: string, context: AmqpLinkEventContext): void {\n if ((CONFIRMATION_CLOSES as readonly string[]).includes(name)) {\n for (const confirmation of [...this.pending]) {\n confirmation.settle(new BabelQueueError(`The dead-letter link closed before the broker accepted the delivery (${name}).`));\n }\n this.forward(name, context);\n return;\n }\n const owners = [...this.pending].filter((confirmation) => confirmation.matches(context));\n if (owners.length === 0) {\n this.forward(name, context);\n return;\n }\n for (const owner of owners) {\n owner.settle(name === \"accepted\" ? undefined : new BabelQueueError(`The broker did not accept the dead-letter delivery (${name}).`));\n }\n }\n\n private forward(name: string, context: AmqpLinkEventContext): void {\n try {\n this.sender.session?.dispatch?.(name, context);\n } catch {\n // forwarding is best effort; never let it change a confirmation outcome\n }\n }\n}\n\nconst hubs = new WeakMap<AmqpSender, ConfirmationHub>();\n\n/**\n * Send `message` and resolve only once the broker has `accepted` the delivery. A rhea `send` returns\n * synchronously and reports the verdict (`accepted` / `rejected` / `released` / `modified`) as a\n * sender event, so awaiting `send` alone cannot see a broker refusal. Rejects on any non-accepted\n * disposition, a link close/error, or when `timeoutMs` elapses (an unsettled delivery that is lost on\n * reconnect raises no event). A sender without event support degrades to awaiting `send` itself.\n * Events that are not this delivery's verdict (other deliveries, link close/error) are re-dispatched on\n * the sender's session, once per event, so the application still sees them (rhea suppresses bubbling\n * while the link has a listener) — see {@link ConfirmationHub}.\n */\nfunction sendConfirmed(sender: AmqpSender, message: AmqpMessage, timeoutMs: number): Promise<void> {\n const on = sender.on?.bind(sender);\n const off = sender.removeListener?.bind(sender);\n if (!on || !off) return Promise.resolve(sender.send(message)).then(() => undefined);\n\n return new Promise<void>((resolve, reject) => {\n let hub = hubs.get(sender);\n if (!hub) {\n hub = new ConfirmationHub(sender, on, off);\n hubs.set(sender, hub);\n }\n const activeHub = hub;\n let finished = false;\n\n const confirmation: PendingConfirmation = {\n delivery: undefined,\n matches: (context) =>\n confirmation.delivery === undefined || context?.delivery === undefined || context.delivery === confirmation.delivery,\n settle: (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timer);\n activeHub.remove(confirmation);\n if (error) reject(error);\n else resolve();\n },\n };\n activeHub.add(confirmation);\n\n // `settle` (above) reads `timer`; it cannot run before this line (nothing is sent or listened to yet).\n const timer = setTimeout(\n () => confirmation.settle(new BabelQueueError(`Timed out after ${timeoutMs}ms waiting for the broker to accept the dead-letter delivery.`)),\n timeoutMs,\n );\n try {\n confirmation.delivery = sender.send(message);\n } catch (error) {\n confirmation.settle(error instanceof Error ? error : new BabelQueueError(String(error)));\n return;\n }\n // A pre-settled delivery (or a sender that returns none) raises no disposition event.\n const delivery = confirmation.delivery;\n if (delivery === null || typeof delivery !== \"object\" || (delivery as { settled?: unknown }).settled === true) confirmation.settle();\n });\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw AMQP message, and the out-of-band\n * {@link HeaderCarrier} read from the message's `application_properties` (empty when there are\n * none). Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a\n * child of the producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: AmqpMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link ArtemisConsumer}. */\nexport interface ArtemisConsumerOptions {\n /** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */\n deadLetterSender?: AmqpSender;\n /**\n * How long (ms) every dead-letter path (version gate, poison, unknown-URN `dead_letter`, terminal\n * handler failure) waits for the broker to accept the DLQ delivery before it releases the source\n * message instead of accepting it (default 30000).\n */\n deadLetterTimeoutMs?: number;\n /**\n * Pause (ms) before a dead-letter path releases a message whose DLQ write failed (default 1000).\n * The release is a plain `released` outcome (no `delivery_failed`, so the broker's delivery counter and\n * `max-delivery-attempts` are untouched); the pause keeps a persistently failing DLQ from becoming a\n * hot redelivery loop. The delivery stays unsettled (holding one credit) while it waits.\n * Note: on the terminal handler-failure path the message has already been through the handler, and\n * a plain release does not bump the delivery counter, so the redelivery runs the handler again (and\n * reports its failure again) until the DLQ write succeeds.\n */\n deadLetterRetryDelayMs?: number;\n /** Attempts before terminal dead-lettering (default 3). */\n maxTries?: number;\n /** Strategy for a URN with no handler (default `fail`). */\n unknownUrn?: string;\n /** Called for a non-conformant message, an unmapped URN, or a throwing handler. */\n onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;\n}\n\n/**\n * Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the\n * handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A\n * throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP\n * `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a\n * `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP\n * counter is 0-based, so no −1.\n */\nexport class ArtemisConsumer {\n private readonly maxTries: number;\n private readonly unknownUrn: string;\n\n constructor(\n private readonly handlers: BabelHandlers,\n private readonly options: ArtemisConsumerOptions = {},\n ) {\n this.maxTries = options.maxTries ?? 3;\n this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;\n }\n\n /** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */\n listen(receiver: AmqpReceiver): void {\n receiver.on(\"message\", (context) => this.handle(context));\n }\n\n /** Route + settle one delivery. Exposed for testing. */\n async handle(context: AmqpEventContext): Promise<void> {\n const message = context.message ?? {};\n const delivery = context.delivery;\n\n // Version gate (§7.7): decided on the application property alone, before the body is decoded.\n const unsupported = unsupportedSchemaVersion(message);\n if (unsupported !== undefined) {\n const gateError = new BabelQueueError(\n `Rejected a message from Artemis with unsupported bq_schema_version \"${unsupported}\" (supported: ${SCHEMA_VERSION}).`,\n );\n // Accept only after the broker has accepted the DLQ copy; otherwise release (plain, after a pause) so\n // the message is never lost and never reaches a handler. Reports come after the settlement, or are\n // isolated, so a throwing onError can never leave the message unsettled.\n try {\n await this.deadLetterRawConfirmed(messageBody(message));\n } catch (dlqError) {\n this.safeReport(gateError, null, context);\n this.safeReport(dlqError, null, context);\n await this.releaseAfterBackoff(delivery);\n return;\n }\n delivery?.accept();\n this.safeReport(gateError, null, context);\n return;\n }\n\n const decoded = EnvelopeCodec.decode(messageBody(message));\n\n if (!EnvelopeCodec.accepts(decoded)) {\n // A non-conformant / poison message may lack the fields to annotate; forward it raw.\n this.report(new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Artemis.\"), decoded, context);\n await this.settleAfterDeadLetter(context, decoded, () => this.deadLetterRaw(messageBody(message)));\n return;\n }\n\n const envelope = this.reconcile(decoded, message);\n const urn = jmsType(message) ?? EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n await this.onUnknownUrn(context, envelope, urn);\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n delivery?.accept();\n } catch (error) {\n this.report(error, envelope, context);\n await this.retryOrDeadLetter(context, envelope, error);\n }\n }\n\n /** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */\n private reconcile(envelope: IncomingEnvelope, message: AmqpMessage): Envelope {\n const body = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n const count = deliveryCount(message);\n return { ...envelope, attempts: Math.max(body, count) } as Envelope;\n }\n\n private async onUnknownUrn(context: AmqpEventContext, envelope: Envelope, urn: string): Promise<void> {\n const delivery = context.delivery;\n switch (this.unknownUrn) {\n case UnknownUrnStrategy.DELETE:\n delivery?.accept();\n return;\n case UnknownUrnStrategy.DEAD_LETTER:\n await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, \"unknown_urn\"));\n return;\n case UnknownUrnStrategy.RELEASE:\n delivery?.release({ delivery_failed: true });\n return;\n default: {\n // FAIL: surface and do NOT settle — the broker redelivers, then dead-letters.\n const error = new UnknownUrnError(urn);\n this.report(error, envelope, context);\n throw error;\n }\n }\n }\n\n private async retryOrDeadLetter(context: AmqpEventContext, envelope: Envelope, error: unknown): Promise<void> {\n const delivery = context.delivery;\n if ((envelope.attempts ?? 0) + 1 < this.maxTries) {\n // Release with delivery-failed leaves it for redelivery — the broker bumps delivery-count.\n delivery?.release({ delivery_failed: true });\n } else if (this.options.deadLetterSender) {\n await this.settleAfterDeadLetter(context, envelope, () => this.deadLetter(envelope, \"failed\", error));\n } else {\n delivery?.accept(); // terminal, no DLQ → drop\n }\n }\n\n /**\n * Dead-letter write → `accept`, or, when the write fails, report + plain `release` after the retry\n * delay (never `accept`, so a failed DLQ write cannot lose the source message). Without a DLQ sender\n * the write is a no-op and the message is accepted (terminal drop), as before.\n */\n private async settleAfterDeadLetter(\n context: AmqpEventContext,\n envelope: IncomingEnvelope | null,\n write: () => Promise<void>,\n ): Promise<void> {\n try {\n await write();\n } catch (dlqError) {\n this.safeReport(dlqError, envelope, context);\n await this.releaseAfterBackoff(context.delivery);\n return;\n }\n context.delivery?.accept();\n }\n\n private async deadLetter(envelope: Envelope, reason: string, error?: unknown): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n const original = envelope.meta?.queue ?? \"\";\n const annotated = annotate(envelope, reason, original, {\n attempts: envelope.attempts ?? 0,\n error: error instanceof Error ? error.message : null,\n exception: error instanceof Error ? error.name : null,\n });\n await sendConfirmed(sender, artemisMessage(annotated), this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);\n }\n\n /** Poison path: forward the raw body, confirmed like the gate path. */\n private deadLetterRaw(raw: string): Promise<void> {\n return this.deadLetterRawConfirmed(raw);\n }\n\n /** Forward the raw body and wait for the broker's `accepted` (a no-op without a DLQ sender). */\n private async deadLetterRawConfirmed(raw: string): Promise<void> {\n const sender = this.options.deadLetterSender;\n if (!sender) return;\n await sendConfirmed(sender, { body: raw }, this.options.deadLetterTimeoutMs ?? DEFAULT_DLQ_CONFIRM_TIMEOUT_MS);\n }\n\n /** Plain `released` outcome (the broker's delivery counter is not touched), after the retry delay. */\n private async releaseAfterBackoff(delivery: AmqpDelivery | undefined): Promise<void> {\n const delayMs = this.options.deadLetterRetryDelayMs ?? DEFAULT_DLQ_RETRY_DELAY_MS;\n if (delayMs > 0) await new Promise<void>((resolve) => setTimeout(resolve, delayMs).unref?.());\n delivery?.release();\n }\n\n /** `onError` isolated: a throwing callback must not skip the settlement that follows or precedes it. */\n private safeReport(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n try {\n this.report(error, envelope, context);\n } catch {\n // swallowed by design\n }\n }\n\n private report(error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext): void {\n this.options.onError?.(error, envelope, context);\n }\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@babelqueue/artemis",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Apache ActiveMQ Artemis adapter for BabelQueue — canonical-envelope publisher and a URN-routed AMQP 1.0 consumer (accept/release settlement, native scheduled delivery, cross-language DLQ) over rhea.",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"babelqueue",
|
|
7
|
+
"artemis",
|
|
8
|
+
"activemq",
|
|
9
|
+
"amqp",
|
|
10
|
+
"rhea",
|
|
11
|
+
"jms",
|
|
12
|
+
"queue",
|
|
13
|
+
"polyglot",
|
|
14
|
+
"messaging"
|
|
15
|
+
],
|
|
6
16
|
"homepage": "https://babelqueue.com",
|
|
7
17
|
"bugs": {
|
|
8
18
|
"url": "https://github.com/BabelQueue/babelqueue-node-adapters/issues"
|
|
@@ -13,7 +23,7 @@
|
|
|
13
23
|
"directory": "packages/artemis"
|
|
14
24
|
},
|
|
15
25
|
"license": "MIT",
|
|
16
|
-
"author": "Muhammet Şafak <info@muhammetsafak.com
|
|
26
|
+
"author": "Muhammet Şafak <info@muhammetsafak.com>",
|
|
17
27
|
"type": "module",
|
|
18
28
|
"exports": {
|
|
19
29
|
".": {
|
|
@@ -30,7 +40,9 @@
|
|
|
30
40
|
"main": "./dist/index.cjs",
|
|
31
41
|
"module": "./dist/index.js",
|
|
32
42
|
"types": "./dist/index.d.ts",
|
|
33
|
-
"files": [
|
|
43
|
+
"files": [
|
|
44
|
+
"dist"
|
|
45
|
+
],
|
|
34
46
|
"sideEffects": false,
|
|
35
47
|
"engines": {
|
|
36
48
|
"node": ">=18"
|
|
@@ -41,7 +53,7 @@
|
|
|
41
53
|
"test": "node --import tsx --test test/*.test.ts"
|
|
42
54
|
},
|
|
43
55
|
"dependencies": {
|
|
44
|
-
"@babelqueue/core": "^1.
|
|
56
|
+
"@babelqueue/core": "^1.4.0"
|
|
45
57
|
},
|
|
46
58
|
"peerDependencies": {
|
|
47
59
|
"rhea": "^3.0.0"
|