@babelqueue/artemis 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -0
- package/dist/index.cjs +202 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +129 -0
- package/dist/index.d.ts +129 -0
- package/dist/index.js +172 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# @babelqueue/artemis
|
|
2
|
+
|
|
3
|
+
Apache ActiveMQ Artemis adapter for [BabelQueue](https://babelqueue.com) — a canonical-envelope
|
|
4
|
+
**publisher** and a URN-routed **AMQP 1.0** consumer over [rhea](https://github.com/amqp/rhea),
|
|
5
|
+
so an Artemis-based Node service speaks the same wire contract (envelope shape, URN identity,
|
|
6
|
+
trace propagation) as the Java, .NET, Python and Go SDKs. Implements
|
|
7
|
+
[§7 of the broker-bindings contract](https://babelqueue.com/docs/spec/1.x/broker-bindings#apache-activemq-artemis).
|
|
8
|
+
|
|
9
|
+
Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —
|
|
10
|
+
per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so
|
|
11
|
+
this adapter maps onto them (the envelope stays `schema_version: 1`):
|
|
12
|
+
|
|
13
|
+
- the envelope JSON is the message **body**; the contract fields are mirrored onto the AMQP a JMS
|
|
14
|
+
peer reads — `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the
|
|
15
|
+
`x-opt-jms-type` annotation = URN (so a Java/JMS or AMQP consumer routes without decoding the
|
|
16
|
+
body) — plus the `bq-` application properties;
|
|
17
|
+
- consume settles per message: **`accept` after success**; a throwing handler **`release`s** the
|
|
18
|
+
message so the broker redelivers it (incrementing the AMQP `delivery-count`);
|
|
19
|
+
- **`attempts = max(body, delivery-count)`** — the AMQP delivery-count is 0-based (0 on first
|
|
20
|
+
delivery), so it maps directly with no −1 (the Java JMS binding reads the 1-based
|
|
21
|
+
`JMSXDeliveryCount` and subtracts 1, arriving at the same 0-based `attempts`);
|
|
22
|
+
- delay uses Artemis's **native** AMQP scheduled delivery (`x-opt-delivery-time`); terminal
|
|
23
|
+
failures go to an opt-in `<queue>.dlq` with a `dead_letter` block.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @babelqueue/artemis rhea
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`rhea` is an optional peer — you provide the sender/receiver; a rhea sender/receiver satisfies
|
|
32
|
+
the adapter structurally.
|
|
33
|
+
|
|
34
|
+
## Produce
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { Container } from "rhea";
|
|
38
|
+
import { ArtemisPublisher } from "@babelqueue/artemis";
|
|
39
|
+
|
|
40
|
+
const container = new Container();
|
|
41
|
+
const connection = container.connect({ host: "localhost", port: 5672 });
|
|
42
|
+
const sender = connection.open_sender("orders");
|
|
43
|
+
|
|
44
|
+
const publisher = ArtemisPublisher.create(sender, "orders");
|
|
45
|
+
const id = await publisher.publish("urn:babel:orders:created", { order_id: 1042 });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`publish(urn, data, options?)` returns the message `meta.id`; `options` adds a `traceId` and a
|
|
49
|
+
`delayMs` (native scheduled delivery).
|
|
50
|
+
|
|
51
|
+
## Consume
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { Container } from "rhea";
|
|
55
|
+
import { ArtemisConsumer, type BabelHandlers } from "@babelqueue/artemis";
|
|
56
|
+
|
|
57
|
+
const receiver = connection.open_receiver({ source: "orders", autoaccept: false, credit_window: 10 });
|
|
58
|
+
const dlqSender = connection.open_sender("orders.dlq");
|
|
59
|
+
|
|
60
|
+
const handlers: BabelHandlers = {
|
|
61
|
+
"urn:babel:orders:created": (envelope, message) => {
|
|
62
|
+
// envelope.data, envelope.trace_id, envelope.attempts ...
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const consumer = new ArtemisConsumer(handlers, {
|
|
67
|
+
deadLetterSender: dlqSender, // enables the cross-language <queue>.dlq
|
|
68
|
+
maxTries: 3,
|
|
69
|
+
onError: (err, envelope, context) => console.error(err),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
consumer.listen(receiver); // wires receiver.on("message") → accept / release / dead-letter
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A successful handler `accept`s the message. A throwing handler `release`s it (the broker
|
|
76
|
+
redelivers and bumps `delivery-count`); once `maxTries` is reached the envelope goes to
|
|
77
|
+
`<queue>.dlq` with a `dead_letter` block. The consumer routes on the `x-opt-jms-type` annotation
|
|
78
|
+
(falling back to the body URN), so it never decodes a message it cannot handle. Unknown-URN
|
|
79
|
+
strategy is one of `fail` / `delete` / `release` / `dead_letter`.
|
|
80
|
+
|
|
81
|
+
> `autoaccept: false` is required — the consumer owns the disposition. `handle(context)` is also
|
|
82
|
+
> exposed directly for testing or a custom event wiring.
|
|
83
|
+
|
|
84
|
+
## Contract mapping (§7)
|
|
85
|
+
|
|
86
|
+
| Envelope | Apache ActiveMQ Artemis (AMQP 1.0) |
|
|
87
|
+
| :--- | :--- |
|
|
88
|
+
| body | message body (byte-identical across SDKs) |
|
|
89
|
+
| `job` (URN) | `x-opt-jms-type` annotation → JMSType (consumer routes on this) |
|
|
90
|
+
| `trace_id` | `correlation-id` → JMSCorrelationID |
|
|
91
|
+
| `meta.created_at` | `creation-time` → JMSTimestamp (Unix ms) |
|
|
92
|
+
| `meta.schema_version` | application property `bq-schema-version` (`"1"`) |
|
|
93
|
+
| `meta.lang` | application property `bq-source-lang` |
|
|
94
|
+
| `attempts` | `max(body, delivery-count)` (AMQP counter is 0-based) |
|
|
95
|
+
| reserve / ack | message event → process → **`accept`** |
|
|
96
|
+
| retry / delay | `release` redelivery · native `x-opt-delivery-time` |
|
|
97
|
+
| dead-letter | `<queue>.dlq` + `dead_letter` block (alongside the native DLA) |
|
|
98
|
+
|
|
99
|
+
The `bq-` application-property values are strings (integers as decimal, e.g. `"1"`); `bq-app-id`
|
|
100
|
+
is `"babelqueue"`. The envelope is unchanged (`schema_version` stays `1`); Artemis is purely
|
|
101
|
+
additive.
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ArtemisConsumer: () => ArtemisConsumer,
|
|
24
|
+
ArtemisPublisher: () => ArtemisPublisher,
|
|
25
|
+
JMS_TYPE_KEY: () => JMS_TYPE_KEY,
|
|
26
|
+
SCHEDULED_DELIVERY_KEY: () => SCHEDULED_DELIVERY_KEY,
|
|
27
|
+
artemisMessage: () => artemisMessage,
|
|
28
|
+
messageBody: () => messageBody
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
var import_core = require("@babelqueue/core");
|
|
32
|
+
var JMS_TYPE_KEY = "x-opt-jms-type";
|
|
33
|
+
var SCHEDULED_DELIVERY_KEY = "x-opt-delivery-time";
|
|
34
|
+
function artemisMessage(envelope, delayMs) {
|
|
35
|
+
const annotations = {};
|
|
36
|
+
if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;
|
|
37
|
+
const applicationProperties = {
|
|
38
|
+
"bq_schema_version": String(envelope.meta.schema_version),
|
|
39
|
+
"bq_attempts": String(envelope.attempts ?? 0),
|
|
40
|
+
"bq_app_id": "babelqueue"
|
|
41
|
+
};
|
|
42
|
+
if (envelope.meta.lang) applicationProperties["bq_source_lang"] = envelope.meta.lang;
|
|
43
|
+
const message = {
|
|
44
|
+
body: import_core.EnvelopeCodec.encode(envelope),
|
|
45
|
+
content_type: "application/json",
|
|
46
|
+
application_properties: applicationProperties,
|
|
47
|
+
message_annotations: annotations
|
|
48
|
+
};
|
|
49
|
+
if (envelope.trace_id) message.correlation_id = envelope.trace_id;
|
|
50
|
+
if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;
|
|
51
|
+
if (delayMs != null && delayMs > 0) {
|
|
52
|
+
applicationProperties["bq_delay"] = String(delayMs);
|
|
53
|
+
annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;
|
|
54
|
+
}
|
|
55
|
+
return message;
|
|
56
|
+
}
|
|
57
|
+
function messageBody(message) {
|
|
58
|
+
const body = message.body;
|
|
59
|
+
if (body == null) return "";
|
|
60
|
+
if (typeof body === "string") return body;
|
|
61
|
+
if (Buffer.isBuffer(body)) return body.toString("utf8");
|
|
62
|
+
if (typeof body === "object" && "content" in body) {
|
|
63
|
+
const content = body.content;
|
|
64
|
+
if (Buffer.isBuffer(content)) return content.toString("utf8");
|
|
65
|
+
if (typeof content === "string") return content;
|
|
66
|
+
}
|
|
67
|
+
return String(body);
|
|
68
|
+
}
|
|
69
|
+
function jmsType(message) {
|
|
70
|
+
const value = message.message_annotations?.[JMS_TYPE_KEY];
|
|
71
|
+
return value == null ? void 0 : String(value);
|
|
72
|
+
}
|
|
73
|
+
function deliveryCount(message) {
|
|
74
|
+
const value = message.delivery_count;
|
|
75
|
+
return typeof value === "number" && value > 0 ? value : 0;
|
|
76
|
+
}
|
|
77
|
+
var ArtemisPublisher = class _ArtemisPublisher {
|
|
78
|
+
constructor(sender, queue) {
|
|
79
|
+
this.sender = sender;
|
|
80
|
+
this.queue = queue;
|
|
81
|
+
}
|
|
82
|
+
sender;
|
|
83
|
+
queue;
|
|
84
|
+
/** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */
|
|
85
|
+
static create(sender, queue) {
|
|
86
|
+
return new _ArtemisPublisher(sender, queue);
|
|
87
|
+
}
|
|
88
|
+
/** Build + send the canonical envelope; returns the message id (`meta.id`). */
|
|
89
|
+
async publish(urn, data, options = {}) {
|
|
90
|
+
const envelope = import_core.EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });
|
|
91
|
+
await this.sender.send(artemisMessage(envelope, options.delayMs));
|
|
92
|
+
return envelope.meta.id;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
var ArtemisConsumer = class {
|
|
96
|
+
constructor(handlers, options = {}) {
|
|
97
|
+
this.handlers = handlers;
|
|
98
|
+
this.options = options;
|
|
99
|
+
this.maxTries = options.maxTries ?? 3;
|
|
100
|
+
this.unknownUrn = options.unknownUrn ?? import_core.UnknownUrnStrategy.FAIL;
|
|
101
|
+
}
|
|
102
|
+
handlers;
|
|
103
|
+
options;
|
|
104
|
+
maxTries;
|
|
105
|
+
unknownUrn;
|
|
106
|
+
/** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */
|
|
107
|
+
listen(receiver) {
|
|
108
|
+
receiver.on("message", (context) => this.handle(context));
|
|
109
|
+
}
|
|
110
|
+
/** Route + settle one delivery. Exposed for testing. */
|
|
111
|
+
async handle(context) {
|
|
112
|
+
const message = context.message ?? {};
|
|
113
|
+
const delivery = context.delivery;
|
|
114
|
+
const decoded = import_core.EnvelopeCodec.decode(messageBody(message));
|
|
115
|
+
if (!import_core.EnvelopeCodec.accepts(decoded)) {
|
|
116
|
+
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();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const envelope = this.reconcile(decoded, message);
|
|
122
|
+
const urn = jmsType(message) ?? import_core.EnvelopeCodec.urn(envelope);
|
|
123
|
+
const handler = this.handlers[urn];
|
|
124
|
+
if (!handler) {
|
|
125
|
+
await this.onUnknownUrn(context, envelope, urn);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
await handler(envelope, message);
|
|
130
|
+
delivery?.accept();
|
|
131
|
+
} catch (error) {
|
|
132
|
+
this.report(error, envelope, context);
|
|
133
|
+
await this.retryOrDeadLetter(context, envelope, error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */
|
|
137
|
+
reconcile(envelope, message) {
|
|
138
|
+
const body = typeof envelope.attempts === "number" ? envelope.attempts : 0;
|
|
139
|
+
const count = deliveryCount(message);
|
|
140
|
+
return { ...envelope, attempts: Math.max(body, count) };
|
|
141
|
+
}
|
|
142
|
+
async onUnknownUrn(context, envelope, urn) {
|
|
143
|
+
const delivery = context.delivery;
|
|
144
|
+
switch (this.unknownUrn) {
|
|
145
|
+
case import_core.UnknownUrnStrategy.DELETE:
|
|
146
|
+
delivery?.accept();
|
|
147
|
+
return;
|
|
148
|
+
case import_core.UnknownUrnStrategy.DEAD_LETTER:
|
|
149
|
+
await this.deadLetter(envelope, "unknown_urn");
|
|
150
|
+
delivery?.accept();
|
|
151
|
+
return;
|
|
152
|
+
case import_core.UnknownUrnStrategy.RELEASE:
|
|
153
|
+
delivery?.release({ delivery_failed: true });
|
|
154
|
+
return;
|
|
155
|
+
default: {
|
|
156
|
+
const error = new import_core.UnknownUrnError(urn);
|
|
157
|
+
this.report(error, envelope, context);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async retryOrDeadLetter(context, envelope, error) {
|
|
163
|
+
const delivery = context.delivery;
|
|
164
|
+
if ((envelope.attempts ?? 0) + 1 < this.maxTries) {
|
|
165
|
+
delivery?.release({ delivery_failed: true });
|
|
166
|
+
} else if (this.options.deadLetterSender) {
|
|
167
|
+
await this.deadLetter(envelope, "failed", error);
|
|
168
|
+
delivery?.accept();
|
|
169
|
+
} else {
|
|
170
|
+
delivery?.accept();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async deadLetter(envelope, reason, error) {
|
|
174
|
+
const sender = this.options.deadLetterSender;
|
|
175
|
+
if (!sender) return;
|
|
176
|
+
const original = envelope.meta?.queue ?? "";
|
|
177
|
+
const annotated = (0, import_core.annotate)(envelope, reason, original, {
|
|
178
|
+
attempts: envelope.attempts ?? 0,
|
|
179
|
+
error: error instanceof Error ? error.message : null,
|
|
180
|
+
exception: error instanceof Error ? error.name : null
|
|
181
|
+
});
|
|
182
|
+
await sender.send(artemisMessage(annotated));
|
|
183
|
+
}
|
|
184
|
+
async deadLetterRaw(raw) {
|
|
185
|
+
const sender = this.options.deadLetterSender;
|
|
186
|
+
if (!sender) return;
|
|
187
|
+
await sender.send({ body: raw });
|
|
188
|
+
}
|
|
189
|
+
report(error, envelope, context) {
|
|
190
|
+
this.options.onError?.(error, envelope, context);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
194
|
+
0 && (module.exports = {
|
|
195
|
+
ArtemisConsumer,
|
|
196
|
+
ArtemisPublisher,
|
|
197
|
+
JMS_TYPE_KEY,
|
|
198
|
+
SCHEDULED_DELIVERY_KEY,
|
|
199
|
+
artemisMessage,
|
|
200
|
+
messageBody
|
|
201
|
+
});
|
|
202
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Envelope, IncomingEnvelope } from '@babelqueue/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Apache ActiveMQ Artemis adapter for BabelQueue.
|
|
5
|
+
*
|
|
6
|
+
* A canonical-envelope **publisher** and a URN-routed **AMQP 1.0** consumer over Artemis (rhea),
|
|
7
|
+
* so an Artemis-based Node service speaks the same contract (envelope shape, URN identity, trace
|
|
8
|
+
* propagation) as the Java, .NET, Python and Go SDKs.
|
|
9
|
+
*
|
|
10
|
+
* Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —
|
|
11
|
+
* per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so
|
|
12
|
+
* this adapter maps onto them (the envelope stays `schema_version: 1`): the envelope JSON is the
|
|
13
|
+
* message **body**; the contract fields are mirrored onto the AMQP a JMS peer reads
|
|
14
|
+
* (`correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`
|
|
15
|
+
* annotation = URN) plus the `bq_` application properties; consume settles per message
|
|
16
|
+
* (`accept` after success, `release` for redelivery); **`attempts = max(body, delivery-count)`**
|
|
17
|
+
* (the AMQP counter is 0-based — no −1); terminal failures go to an opt-in `<queue>.dlq` with a
|
|
18
|
+
* `dead_letter` block.
|
|
19
|
+
*
|
|
20
|
+
* This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide
|
|
21
|
+
* the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** An AMQP message (structural subset of a rhea message). */
|
|
25
|
+
interface AmqpMessage {
|
|
26
|
+
body?: unknown;
|
|
27
|
+
correlation_id?: unknown;
|
|
28
|
+
creation_time?: number | Date;
|
|
29
|
+
content_type?: string;
|
|
30
|
+
application_properties?: Record<string, unknown>;
|
|
31
|
+
message_annotations?: Record<string, unknown>;
|
|
32
|
+
/** The AMQP header delivery-count (0-based: 0 on first delivery). */
|
|
33
|
+
delivery_count?: number;
|
|
34
|
+
}
|
|
35
|
+
/** The disposition handle rhea passes alongside a received message. */
|
|
36
|
+
interface AmqpDelivery {
|
|
37
|
+
/** Settle (acknowledge) the message — it is removed from the queue. */
|
|
38
|
+
accept(): void;
|
|
39
|
+
/** Return the message for redelivery — the broker increments `delivery-count`. */
|
|
40
|
+
release(params?: {
|
|
41
|
+
delivery_failed?: boolean;
|
|
42
|
+
undeliverable_here?: boolean;
|
|
43
|
+
}): void;
|
|
44
|
+
}
|
|
45
|
+
/** The event context rhea emits on a `message` event. */
|
|
46
|
+
interface AmqpEventContext {
|
|
47
|
+
message?: AmqpMessage;
|
|
48
|
+
delivery?: AmqpDelivery;
|
|
49
|
+
}
|
|
50
|
+
/** The subset of a rhea sender this adapter calls. */
|
|
51
|
+
interface AmqpSender {
|
|
52
|
+
send(message: AmqpMessage): unknown;
|
|
53
|
+
}
|
|
54
|
+
/** The subset of a rhea receiver this adapter wires to (its `message` event). */
|
|
55
|
+
interface AmqpReceiver {
|
|
56
|
+
on(event: "message", handler: (context: AmqpEventContext) => void): unknown;
|
|
57
|
+
}
|
|
58
|
+
/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */
|
|
59
|
+
declare const JMS_TYPE_KEY = "x-opt-jms-type";
|
|
60
|
+
/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */
|
|
61
|
+
declare const SCHEDULED_DELIVERY_KEY = "x-opt-delivery-time";
|
|
62
|
+
/**
|
|
63
|
+
* Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,
|
|
64
|
+
* `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`
|
|
65
|
+
* annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`
|
|
66
|
+
* sets the `x-opt-delivery-time` annotation for native scheduled delivery.
|
|
67
|
+
*/
|
|
68
|
+
declare function artemisMessage(envelope: Envelope, delayMs?: number): AmqpMessage;
|
|
69
|
+
/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */
|
|
70
|
+
declare function messageBody(message: AmqpMessage): string;
|
|
71
|
+
/** Options for {@link ArtemisPublisher.publish}. */
|
|
72
|
+
interface PublishOptions {
|
|
73
|
+
traceId?: string;
|
|
74
|
+
/** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */
|
|
75
|
+
delayMs?: number;
|
|
76
|
+
}
|
|
77
|
+
/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */
|
|
78
|
+
declare class ArtemisPublisher {
|
|
79
|
+
private readonly sender;
|
|
80
|
+
private readonly queue;
|
|
81
|
+
private constructor();
|
|
82
|
+
/** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */
|
|
83
|
+
static create(sender: AmqpSender, queue: string): ArtemisPublisher;
|
|
84
|
+
/** Build + send the canonical envelope; returns the message id (`meta.id`). */
|
|
85
|
+
publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
|
|
86
|
+
}
|
|
87
|
+
/** A URN handler. Receives the validated envelope and the raw AMQP message. */
|
|
88
|
+
type BabelHandler = (envelope: Envelope, message: AmqpMessage) => unknown | Promise<unknown>;
|
|
89
|
+
/** A map of URN → handler. */
|
|
90
|
+
type BabelHandlers = Record<string, BabelHandler>;
|
|
91
|
+
/** Options for {@link ArtemisConsumer}. */
|
|
92
|
+
interface ArtemisConsumerOptions {
|
|
93
|
+
/** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */
|
|
94
|
+
deadLetterSender?: AmqpSender;
|
|
95
|
+
/** Attempts before terminal dead-lettering (default 3). */
|
|
96
|
+
maxTries?: number;
|
|
97
|
+
/** Strategy for a URN with no handler (default `fail`). */
|
|
98
|
+
unknownUrn?: string;
|
|
99
|
+
/** Called for a non-conformant message, an unmapped URN, or a throwing handler. */
|
|
100
|
+
onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the
|
|
104
|
+
* handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A
|
|
105
|
+
* throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP
|
|
106
|
+
* `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a
|
|
107
|
+
* `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP
|
|
108
|
+
* counter is 0-based, so no −1.
|
|
109
|
+
*/
|
|
110
|
+
declare class ArtemisConsumer {
|
|
111
|
+
private readonly handlers;
|
|
112
|
+
private readonly options;
|
|
113
|
+
private readonly maxTries;
|
|
114
|
+
private readonly unknownUrn;
|
|
115
|
+
constructor(handlers: BabelHandlers, options?: ArtemisConsumerOptions);
|
|
116
|
+
/** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */
|
|
117
|
+
listen(receiver: AmqpReceiver): void;
|
|
118
|
+
/** Route + settle one delivery. Exposed for testing. */
|
|
119
|
+
handle(context: AmqpEventContext): Promise<void>;
|
|
120
|
+
/** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */
|
|
121
|
+
private reconcile;
|
|
122
|
+
private onUnknownUrn;
|
|
123
|
+
private retryOrDeadLetter;
|
|
124
|
+
private deadLetter;
|
|
125
|
+
private deadLetterRaw;
|
|
126
|
+
private report;
|
|
127
|
+
}
|
|
128
|
+
|
|
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 };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Envelope, IncomingEnvelope } from '@babelqueue/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Apache ActiveMQ Artemis adapter for BabelQueue.
|
|
5
|
+
*
|
|
6
|
+
* A canonical-envelope **publisher** and a URN-routed **AMQP 1.0** consumer over Artemis (rhea),
|
|
7
|
+
* so an Artemis-based Node service speaks the same contract (envelope shape, URN identity, trace
|
|
8
|
+
* propagation) as the Java, .NET, Python and Go SDKs.
|
|
9
|
+
*
|
|
10
|
+
* Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1) and gives the binding native primitives —
|
|
11
|
+
* per-message settlement, scheduled delivery, a delivery counter and a dead-letter address — so
|
|
12
|
+
* this adapter maps onto them (the envelope stays `schema_version: 1`): the envelope JSON is the
|
|
13
|
+
* message **body**; the contract fields are mirrored onto the AMQP a JMS peer reads
|
|
14
|
+
* (`correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`
|
|
15
|
+
* annotation = URN) plus the `bq_` application properties; consume settles per message
|
|
16
|
+
* (`accept` after success, `release` for redelivery); **`attempts = max(body, delivery-count)`**
|
|
17
|
+
* (the AMQP counter is 0-based — no −1); terminal failures go to an opt-in `<queue>.dlq` with a
|
|
18
|
+
* `dead_letter` block.
|
|
19
|
+
*
|
|
20
|
+
* This implements §7 of the broker-bindings contract. `rhea` is an optional peer — you provide
|
|
21
|
+
* the sender/receiver (a rhea sender/receiver satisfies the adapter structurally).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** An AMQP message (structural subset of a rhea message). */
|
|
25
|
+
interface AmqpMessage {
|
|
26
|
+
body?: unknown;
|
|
27
|
+
correlation_id?: unknown;
|
|
28
|
+
creation_time?: number | Date;
|
|
29
|
+
content_type?: string;
|
|
30
|
+
application_properties?: Record<string, unknown>;
|
|
31
|
+
message_annotations?: Record<string, unknown>;
|
|
32
|
+
/** The AMQP header delivery-count (0-based: 0 on first delivery). */
|
|
33
|
+
delivery_count?: number;
|
|
34
|
+
}
|
|
35
|
+
/** The disposition handle rhea passes alongside a received message. */
|
|
36
|
+
interface AmqpDelivery {
|
|
37
|
+
/** Settle (acknowledge) the message — it is removed from the queue. */
|
|
38
|
+
accept(): void;
|
|
39
|
+
/** Return the message for redelivery — the broker increments `delivery-count`. */
|
|
40
|
+
release(params?: {
|
|
41
|
+
delivery_failed?: boolean;
|
|
42
|
+
undeliverable_here?: boolean;
|
|
43
|
+
}): void;
|
|
44
|
+
}
|
|
45
|
+
/** The event context rhea emits on a `message` event. */
|
|
46
|
+
interface AmqpEventContext {
|
|
47
|
+
message?: AmqpMessage;
|
|
48
|
+
delivery?: AmqpDelivery;
|
|
49
|
+
}
|
|
50
|
+
/** The subset of a rhea sender this adapter calls. */
|
|
51
|
+
interface AmqpSender {
|
|
52
|
+
send(message: AmqpMessage): unknown;
|
|
53
|
+
}
|
|
54
|
+
/** The subset of a rhea receiver this adapter wires to (its `message` event). */
|
|
55
|
+
interface AmqpReceiver {
|
|
56
|
+
on(event: "message", handler: (context: AmqpEventContext) => void): unknown;
|
|
57
|
+
}
|
|
58
|
+
/** The message annotation carrying the URN (the AMQP-JMS mapping of `JMSType`). */
|
|
59
|
+
declare const JMS_TYPE_KEY = "x-opt-jms-type";
|
|
60
|
+
/** The message annotation Artemis honours for AMQP scheduled delivery (absolute Unix ms). */
|
|
61
|
+
declare const SCHEDULED_DELIVERY_KEY = "x-opt-delivery-time";
|
|
62
|
+
/**
|
|
63
|
+
* Project the envelope onto the AMQP 1.0 message a JMS peer reads: body = envelope JSON,
|
|
64
|
+
* `correlation-id` = `trace_id`, `creation-time` = `meta.created_at`, the `x-opt-jms-type`
|
|
65
|
+
* annotation = URN, plus the string-valued `bq_` application properties. A positive `delayMs`
|
|
66
|
+
* sets the `x-opt-delivery-time` annotation for native scheduled delivery.
|
|
67
|
+
*/
|
|
68
|
+
declare function artemisMessage(envelope: Envelope, delayMs?: number): AmqpMessage;
|
|
69
|
+
/** The message body as text (AMQP value string, or UTF-8-decoded binary / rhea Data section). */
|
|
70
|
+
declare function messageBody(message: AmqpMessage): string;
|
|
71
|
+
/** Options for {@link ArtemisPublisher.publish}. */
|
|
72
|
+
interface PublishOptions {
|
|
73
|
+
traceId?: string;
|
|
74
|
+
/** Schedule via Artemis's native AMQP scheduled delivery (`x-opt-delivery-time`). */
|
|
75
|
+
delayMs?: number;
|
|
76
|
+
}
|
|
77
|
+
/** Sends canonical-envelope messages to one Artemis address with the §7 projection. */
|
|
78
|
+
declare class ArtemisPublisher {
|
|
79
|
+
private readonly sender;
|
|
80
|
+
private readonly queue;
|
|
81
|
+
private constructor();
|
|
82
|
+
/** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */
|
|
83
|
+
static create(sender: AmqpSender, queue: string): ArtemisPublisher;
|
|
84
|
+
/** Build + send the canonical envelope; returns the message id (`meta.id`). */
|
|
85
|
+
publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
|
|
86
|
+
}
|
|
87
|
+
/** A URN handler. Receives the validated envelope and the raw AMQP message. */
|
|
88
|
+
type BabelHandler = (envelope: Envelope, message: AmqpMessage) => unknown | Promise<unknown>;
|
|
89
|
+
/** A map of URN → handler. */
|
|
90
|
+
type BabelHandlers = Record<string, BabelHandler>;
|
|
91
|
+
/** Options for {@link ArtemisConsumer}. */
|
|
92
|
+
interface ArtemisConsumerOptions {
|
|
93
|
+
/** A sender to `<queue>.dlq`; enables cross-language dead-lettering. Without it a terminal failure drops. */
|
|
94
|
+
deadLetterSender?: AmqpSender;
|
|
95
|
+
/** Attempts before terminal dead-lettering (default 3). */
|
|
96
|
+
maxTries?: number;
|
|
97
|
+
/** Strategy for a URN with no handler (default `fail`). */
|
|
98
|
+
unknownUrn?: string;
|
|
99
|
+
/** Called for a non-conformant message, an unmapped URN, or a throwing handler. */
|
|
100
|
+
onError?: (error: unknown, envelope: IncomingEnvelope | null, context: AmqpEventContext) => void;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Consumes an Artemis address over AMQP 1.0: each message is decoded, validated, routed to the
|
|
104
|
+
* handler for its URN (read from the `x-opt-jms-type` annotation), and `accept`ed on success. A
|
|
105
|
+
* throwing handler `release`s the message so the broker redelivers it (incrementing the AMQP
|
|
106
|
+
* `delivery-count`); once max-tries is reached the envelope goes to `<queue>.dlq` with a
|
|
107
|
+
* `dead_letter` block. `attempts` is reconciled to `max(body, delivery-count)` — the AMQP
|
|
108
|
+
* counter is 0-based, so no −1.
|
|
109
|
+
*/
|
|
110
|
+
declare class ArtemisConsumer {
|
|
111
|
+
private readonly handlers;
|
|
112
|
+
private readonly options;
|
|
113
|
+
private readonly maxTries;
|
|
114
|
+
private readonly unknownUrn;
|
|
115
|
+
constructor(handlers: BabelHandlers, options?: ArtemisConsumerOptions);
|
|
116
|
+
/** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */
|
|
117
|
+
listen(receiver: AmqpReceiver): void;
|
|
118
|
+
/** Route + settle one delivery. Exposed for testing. */
|
|
119
|
+
handle(context: AmqpEventContext): Promise<void>;
|
|
120
|
+
/** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */
|
|
121
|
+
private reconcile;
|
|
122
|
+
private onUnknownUrn;
|
|
123
|
+
private retryOrDeadLetter;
|
|
124
|
+
private deadLetter;
|
|
125
|
+
private deadLetterRaw;
|
|
126
|
+
private report;
|
|
127
|
+
}
|
|
128
|
+
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { annotate, BabelQueueError, EnvelopeCodec, UnknownUrnError, UnknownUrnStrategy } from "@babelqueue/core";
|
|
3
|
+
var JMS_TYPE_KEY = "x-opt-jms-type";
|
|
4
|
+
var SCHEDULED_DELIVERY_KEY = "x-opt-delivery-time";
|
|
5
|
+
function artemisMessage(envelope, delayMs) {
|
|
6
|
+
const annotations = {};
|
|
7
|
+
if (envelope.job) annotations[JMS_TYPE_KEY] = envelope.job;
|
|
8
|
+
const applicationProperties = {
|
|
9
|
+
"bq_schema_version": String(envelope.meta.schema_version),
|
|
10
|
+
"bq_attempts": String(envelope.attempts ?? 0),
|
|
11
|
+
"bq_app_id": "babelqueue"
|
|
12
|
+
};
|
|
13
|
+
if (envelope.meta.lang) applicationProperties["bq_source_lang"] = envelope.meta.lang;
|
|
14
|
+
const message = {
|
|
15
|
+
body: EnvelopeCodec.encode(envelope),
|
|
16
|
+
content_type: "application/json",
|
|
17
|
+
application_properties: applicationProperties,
|
|
18
|
+
message_annotations: annotations
|
|
19
|
+
};
|
|
20
|
+
if (envelope.trace_id) message.correlation_id = envelope.trace_id;
|
|
21
|
+
if (envelope.meta.created_at != null) message.creation_time = envelope.meta.created_at;
|
|
22
|
+
if (delayMs != null && delayMs > 0) {
|
|
23
|
+
applicationProperties["bq_delay"] = String(delayMs);
|
|
24
|
+
annotations[SCHEDULED_DELIVERY_KEY] = Date.now() + delayMs;
|
|
25
|
+
}
|
|
26
|
+
return message;
|
|
27
|
+
}
|
|
28
|
+
function messageBody(message) {
|
|
29
|
+
const body = message.body;
|
|
30
|
+
if (body == null) return "";
|
|
31
|
+
if (typeof body === "string") return body;
|
|
32
|
+
if (Buffer.isBuffer(body)) return body.toString("utf8");
|
|
33
|
+
if (typeof body === "object" && "content" in body) {
|
|
34
|
+
const content = body.content;
|
|
35
|
+
if (Buffer.isBuffer(content)) return content.toString("utf8");
|
|
36
|
+
if (typeof content === "string") return content;
|
|
37
|
+
}
|
|
38
|
+
return String(body);
|
|
39
|
+
}
|
|
40
|
+
function jmsType(message) {
|
|
41
|
+
const value = message.message_annotations?.[JMS_TYPE_KEY];
|
|
42
|
+
return value == null ? void 0 : String(value);
|
|
43
|
+
}
|
|
44
|
+
function deliveryCount(message) {
|
|
45
|
+
const value = message.delivery_count;
|
|
46
|
+
return typeof value === "number" && value > 0 ? value : 0;
|
|
47
|
+
}
|
|
48
|
+
var ArtemisPublisher = class _ArtemisPublisher {
|
|
49
|
+
constructor(sender, queue) {
|
|
50
|
+
this.sender = sender;
|
|
51
|
+
this.queue = queue;
|
|
52
|
+
}
|
|
53
|
+
sender;
|
|
54
|
+
queue;
|
|
55
|
+
/** A publisher over a sender bound to `queue` (the address is stamped onto `meta.queue`). */
|
|
56
|
+
static create(sender, queue) {
|
|
57
|
+
return new _ArtemisPublisher(sender, queue);
|
|
58
|
+
}
|
|
59
|
+
/** Build + send the canonical envelope; returns the message id (`meta.id`). */
|
|
60
|
+
async publish(urn, data, options = {}) {
|
|
61
|
+
const envelope = EnvelopeCodec.make(urn, data, { queue: this.queue, traceId: options.traceId });
|
|
62
|
+
await this.sender.send(artemisMessage(envelope, options.delayMs));
|
|
63
|
+
return envelope.meta.id;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var ArtemisConsumer = class {
|
|
67
|
+
constructor(handlers, options = {}) {
|
|
68
|
+
this.handlers = handlers;
|
|
69
|
+
this.options = options;
|
|
70
|
+
this.maxTries = options.maxTries ?? 3;
|
|
71
|
+
this.unknownUrn = options.unknownUrn ?? UnknownUrnStrategy.FAIL;
|
|
72
|
+
}
|
|
73
|
+
handlers;
|
|
74
|
+
options;
|
|
75
|
+
maxTries;
|
|
76
|
+
unknownUrn;
|
|
77
|
+
/** Wire this consumer to a rhea receiver's `message` event (auto-accept must be off). */
|
|
78
|
+
listen(receiver) {
|
|
79
|
+
receiver.on("message", (context) => this.handle(context));
|
|
80
|
+
}
|
|
81
|
+
/** Route + settle one delivery. Exposed for testing. */
|
|
82
|
+
async handle(context) {
|
|
83
|
+
const message = context.message ?? {};
|
|
84
|
+
const delivery = context.delivery;
|
|
85
|
+
const decoded = EnvelopeCodec.decode(messageBody(message));
|
|
86
|
+
if (!EnvelopeCodec.accepts(decoded)) {
|
|
87
|
+
this.report(new BabelQueueError("Rejected a non-conformant BabelQueue envelope from Artemis."), decoded, context);
|
|
88
|
+
await this.deadLetterRaw(messageBody(message));
|
|
89
|
+
delivery?.accept();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const envelope = this.reconcile(decoded, message);
|
|
93
|
+
const urn = jmsType(message) ?? EnvelopeCodec.urn(envelope);
|
|
94
|
+
const handler = this.handlers[urn];
|
|
95
|
+
if (!handler) {
|
|
96
|
+
await this.onUnknownUrn(context, envelope, urn);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await handler(envelope, message);
|
|
101
|
+
delivery?.accept();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
this.report(error, envelope, context);
|
|
104
|
+
await this.retryOrDeadLetter(context, envelope, error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** `attempts = max(body, delivery-count)`: the AMQP counter is 0-based, so no −1. */
|
|
108
|
+
reconcile(envelope, message) {
|
|
109
|
+
const body = typeof envelope.attempts === "number" ? envelope.attempts : 0;
|
|
110
|
+
const count = deliveryCount(message);
|
|
111
|
+
return { ...envelope, attempts: Math.max(body, count) };
|
|
112
|
+
}
|
|
113
|
+
async onUnknownUrn(context, envelope, urn) {
|
|
114
|
+
const delivery = context.delivery;
|
|
115
|
+
switch (this.unknownUrn) {
|
|
116
|
+
case UnknownUrnStrategy.DELETE:
|
|
117
|
+
delivery?.accept();
|
|
118
|
+
return;
|
|
119
|
+
case UnknownUrnStrategy.DEAD_LETTER:
|
|
120
|
+
await this.deadLetter(envelope, "unknown_urn");
|
|
121
|
+
delivery?.accept();
|
|
122
|
+
return;
|
|
123
|
+
case UnknownUrnStrategy.RELEASE:
|
|
124
|
+
delivery?.release({ delivery_failed: true });
|
|
125
|
+
return;
|
|
126
|
+
default: {
|
|
127
|
+
const error = new UnknownUrnError(urn);
|
|
128
|
+
this.report(error, envelope, context);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async retryOrDeadLetter(context, envelope, error) {
|
|
134
|
+
const delivery = context.delivery;
|
|
135
|
+
if ((envelope.attempts ?? 0) + 1 < this.maxTries) {
|
|
136
|
+
delivery?.release({ delivery_failed: true });
|
|
137
|
+
} else if (this.options.deadLetterSender) {
|
|
138
|
+
await this.deadLetter(envelope, "failed", error);
|
|
139
|
+
delivery?.accept();
|
|
140
|
+
} else {
|
|
141
|
+
delivery?.accept();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async deadLetter(envelope, reason, error) {
|
|
145
|
+
const sender = this.options.deadLetterSender;
|
|
146
|
+
if (!sender) return;
|
|
147
|
+
const original = envelope.meta?.queue ?? "";
|
|
148
|
+
const annotated = annotate(envelope, reason, original, {
|
|
149
|
+
attempts: envelope.attempts ?? 0,
|
|
150
|
+
error: error instanceof Error ? error.message : null,
|
|
151
|
+
exception: error instanceof Error ? error.name : null
|
|
152
|
+
});
|
|
153
|
+
await sender.send(artemisMessage(annotated));
|
|
154
|
+
}
|
|
155
|
+
async deadLetterRaw(raw) {
|
|
156
|
+
const sender = this.options.deadLetterSender;
|
|
157
|
+
if (!sender) return;
|
|
158
|
+
await sender.send({ body: raw });
|
|
159
|
+
}
|
|
160
|
+
report(error, envelope, context) {
|
|
161
|
+
this.options.onError?.(error, envelope, context);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
export {
|
|
165
|
+
ArtemisConsumer,
|
|
166
|
+
ArtemisPublisher,
|
|
167
|
+
JMS_TYPE_KEY,
|
|
168
|
+
SCHEDULED_DELIVERY_KEY,
|
|
169
|
+
artemisMessage,
|
|
170
|
+
messageBody
|
|
171
|
+
};
|
|
172
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@babelqueue/artemis",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Apache ActiveMQ Artemis adapter for BabelQueue — canonical-envelope publisher and a URN-routed AMQP 1.0 consumer (accept/release settlement, native scheduled delivery, cross-language DLQ) over rhea.",
|
|
5
|
+
"keywords": ["babelqueue", "artemis", "activemq", "amqp", "rhea", "jms", "queue", "polyglot", "messaging"],
|
|
6
|
+
"homepage": "https://babelqueue.com",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/BabelQueue/babelqueue-node-adapters/issues"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/BabelQueue/babelqueue-node-adapters.git",
|
|
13
|
+
"directory": "packages/artemis"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Muhammet Şafak <info@muhammetsafak.com.tr>",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"import": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"require": {
|
|
25
|
+
"types": "./dist/index.d.cts",
|
|
26
|
+
"default": "./dist/index.cjs"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.cjs",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"files": ["dist"],
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"test": "node --import tsx --test test/*.test.ts"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@babelqueue/core": "^1.0.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"rhea": "^3.0.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"rhea": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
}
|
|
57
|
+
}
|