@relaybus/rabbitmq 0.2.0 → 0.3.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 +88 -46
- package/dist/index.cjs +624 -398
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +121 -45
- package/dist/index.d.ts +121 -45
- package/dist/index.js +611 -394
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,33 +1,140 @@
|
|
|
1
|
-
import { RelayBusError, NonRetryableError, assertEventType, ConfigurationError,
|
|
1
|
+
import { RelayBusError, NonRetryableError, assertEventType, ConfigurationError, systemClock, toErrorInfo, computeRetryDelay, isRetryableError, createRetryPolicy } from '@relaybus/core';
|
|
2
|
+
import { createCloudEventReaders, DecodedEventReader, mediaTypeOf, isJsonObject, observeRejection, createJsonEnvelopeFormat, SubscribingTransport, writeStructuredCloudEvent, schedule, NonEmptyText, PositiveInteger, Milliseconds } from '@relaybus/core/internal';
|
|
3
|
+
export { fromHeader, fromPayloadField } from '@relaybus/core/internal';
|
|
2
4
|
|
|
3
5
|
// src/errors.ts
|
|
4
6
|
var RabbitMqUnavailableError = class extends RelayBusError {
|
|
7
|
+
name = "RabbitMqUnavailableError";
|
|
8
|
+
code = "RELAYBUS_RABBITMQ_UNAVAILABLE";
|
|
5
9
|
};
|
|
6
10
|
var RabbitMqPublishRejectedError = class extends RelayBusError {
|
|
11
|
+
name = "RabbitMqPublishRejectedError";
|
|
12
|
+
code = "RELAYBUS_RABBITMQ_PUBLISH_REJECTED";
|
|
7
13
|
};
|
|
8
14
|
var UnroutableMessageError = class extends RelayBusError {
|
|
15
|
+
name = "UnroutableMessageError";
|
|
16
|
+
code = "RELAYBUS_RABBITMQ_UNROUTABLE_MESSAGE";
|
|
9
17
|
};
|
|
10
18
|
var MalformedMessageError = class extends RelayBusError {
|
|
19
|
+
name = "MalformedMessageError";
|
|
20
|
+
code = "RELAYBUS_RABBITMQ_MALFORMED_MESSAGE";
|
|
11
21
|
};
|
|
12
22
|
var UnserializablePayloadError = class extends NonRetryableError {
|
|
13
23
|
name = "UnserializablePayloadError";
|
|
24
|
+
code = "RELAYBUS_RABBITMQ_UNSERIALIZABLE_PAYLOAD";
|
|
14
25
|
};
|
|
15
26
|
var AmqpLimitExceededError = class extends NonRetryableError {
|
|
16
27
|
name = "AmqpLimitExceededError";
|
|
28
|
+
code = "RELAYBUS_RABBITMQ_AMQP_LIMIT_EXCEEDED";
|
|
17
29
|
};
|
|
18
|
-
|
|
19
|
-
|
|
30
|
+
var RabbitMqUnencodableEventError = class extends NonRetryableError {
|
|
31
|
+
name = "RabbitMqUnencodableEventError";
|
|
32
|
+
code = "RELAYBUS_RABBITMQ_UNENCODABLE_EVENT";
|
|
33
|
+
};
|
|
34
|
+
var NACK_MESSAGE = "message nacked";
|
|
35
|
+
var ConfirmedPublishing = class {
|
|
36
|
+
#transportName;
|
|
37
|
+
#publishTimeoutMs;
|
|
38
|
+
constructor(transportName, publishTimeoutMs) {
|
|
39
|
+
this.#transportName = transportName;
|
|
40
|
+
this.#publishTimeoutMs = publishTimeoutMs;
|
|
41
|
+
}
|
|
42
|
+
async publish(publisher, input) {
|
|
43
|
+
const { exchange, routingKey, content, options, mandatory } = input;
|
|
44
|
+
const eventId = options.messageId ?? "unknown";
|
|
45
|
+
if (publisher === void 0) {
|
|
46
|
+
throw new RabbitMqUnavailableError(
|
|
47
|
+
`[RabbitMqTransport](publish) Not connected to the broker | transport=${this.#transportName} destination=${routingKey}`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const returnKey = unroutableKey(options.messageId, routingKey);
|
|
51
|
+
const target = { eventId, destination: routingKey };
|
|
52
|
+
await new Promise((resolve, reject) => {
|
|
53
|
+
const cancelTimeout = this.#startTimeout(target, reject);
|
|
54
|
+
const pending = { ...target, publisher, returnKey, cancelTimeout, resolve, reject };
|
|
55
|
+
try {
|
|
56
|
+
publisher.channel.publish(
|
|
57
|
+
exchange,
|
|
58
|
+
routingKey,
|
|
59
|
+
content,
|
|
60
|
+
{ ...options, mandatory },
|
|
61
|
+
(error) => this.#confirmed(pending, error)
|
|
62
|
+
);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
cancelTimeout();
|
|
65
|
+
reject(this.#toPublishError(error, target));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
#startTimeout({ eventId, destination }, reject) {
|
|
70
|
+
return schedule(systemClock, {
|
|
71
|
+
run: () => {
|
|
72
|
+
reject(
|
|
73
|
+
new RabbitMqUnavailableError(
|
|
74
|
+
`[RabbitMqTransport](publish) No broker confirm within ${this.#publishTimeoutMs}ms | transport=${this.#transportName} eventId=${eventId} destination=${destination}`
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
delayMs: this.#publishTimeoutMs
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** The broker's confirm (or nack) of one message arrived. */
|
|
82
|
+
#confirmed(pending, error) {
|
|
83
|
+
pending.cancelTimeout();
|
|
84
|
+
const returned = takeUnroutable(pending.publisher.unroutable, pending.returnKey);
|
|
85
|
+
if (error) {
|
|
86
|
+
pending.reject(this.#toPublishError(error, pending));
|
|
87
|
+
} else if (returned) {
|
|
88
|
+
pending.reject(
|
|
89
|
+
new UnroutableMessageError(
|
|
90
|
+
`[RabbitMqTransport](publish) No queue is bound to the routing key | transport=${this.#transportName} eventId=${pending.eventId} destination=${pending.destination}`
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
} else {
|
|
94
|
+
pending.resolve();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
#toPublishError(error, { eventId, destination }) {
|
|
98
|
+
const { message } = toErrorInfo(error);
|
|
99
|
+
const detail = `transport=${this.#transportName} eventId=${eventId} destination=${destination} error=${message}`;
|
|
100
|
+
return message === NACK_MESSAGE ? new RabbitMqPublishRejectedError(
|
|
101
|
+
`[RabbitMqTransport](publish) Broker nacked the message | ${detail}`,
|
|
102
|
+
{ cause: error }
|
|
103
|
+
) : new RabbitMqUnavailableError(`[RabbitMqTransport](publish) Publish failed | ${detail}`, {
|
|
104
|
+
cause: error
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
function unroutableKey(messageId, routingKey) {
|
|
109
|
+
return `${String(messageId)}
|
|
110
|
+
${routingKey}`;
|
|
111
|
+
}
|
|
112
|
+
function recordUnroutable(unroutable, key) {
|
|
113
|
+
unroutable.set(key, (unroutable.get(key) ?? 0) + 1);
|
|
114
|
+
}
|
|
115
|
+
function takeUnroutable(unroutable, key) {
|
|
116
|
+
const count = unroutable.get(key);
|
|
117
|
+
if (count === void 0) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (count === 1) {
|
|
121
|
+
unroutable.delete(key);
|
|
122
|
+
} else {
|
|
123
|
+
unroutable.set(key, count - 1);
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
20
127
|
function abortableSleep(milliseconds, signal) {
|
|
21
128
|
if (signal.aborted) {
|
|
22
129
|
return Promise.resolve();
|
|
23
130
|
}
|
|
24
131
|
return new Promise((resolve) => {
|
|
25
132
|
const finish = () => {
|
|
26
|
-
|
|
133
|
+
cancel();
|
|
27
134
|
signal.removeEventListener("abort", finish);
|
|
28
135
|
resolve();
|
|
29
136
|
};
|
|
30
|
-
const
|
|
137
|
+
const cancel = schedule(systemClock, { run: finish, delayMs: milliseconds });
|
|
31
138
|
signal.addEventListener("abort", finish, { once: true });
|
|
32
139
|
});
|
|
33
140
|
}
|
|
@@ -36,7 +143,17 @@ function abortableSleep(milliseconds, signal) {
|
|
|
36
143
|
var ConnectionSupervisor = class {
|
|
37
144
|
#settings;
|
|
38
145
|
#stop = new AbortController();
|
|
146
|
+
/** Aborted by `forceClose`: destroys a socket that is still connecting. */
|
|
147
|
+
#force = new AbortController();
|
|
148
|
+
/** Resolves on `forceClose`: closing stops waiting for the broker. */
|
|
149
|
+
#forcing = new Promise((resolve) => {
|
|
150
|
+
this.#force.signal.addEventListener("abort", () => resolve(), { once: true });
|
|
151
|
+
});
|
|
39
152
|
#current;
|
|
153
|
+
/** Connections being closed, until their closing handshake finishes. */
|
|
154
|
+
#closing = /* @__PURE__ */ new Set();
|
|
155
|
+
/** Set by `forceClose`: connections are cut instead of closed. */
|
|
156
|
+
#forced = false;
|
|
40
157
|
#ready = false;
|
|
41
158
|
#started = false;
|
|
42
159
|
#closed = false;
|
|
@@ -83,7 +200,7 @@ var ConnectionSupervisor = class {
|
|
|
83
200
|
return;
|
|
84
201
|
}
|
|
85
202
|
this.#handleLoss(connection, reason);
|
|
86
|
-
|
|
203
|
+
void this.#closeConnection(connection);
|
|
87
204
|
}
|
|
88
205
|
/**
|
|
89
206
|
* Stops reconnecting, runs `beforeDisconnect` while the connection is still
|
|
@@ -92,23 +209,54 @@ var ConnectionSupervisor = class {
|
|
|
92
209
|
async close(beforeDisconnect) {
|
|
93
210
|
this.#closed = true;
|
|
94
211
|
this.#stop.abort();
|
|
95
|
-
await this.#reconnecting;
|
|
212
|
+
await Promise.race([this.#reconnecting, this.#forcing]);
|
|
96
213
|
try {
|
|
97
|
-
await beforeDisconnect();
|
|
214
|
+
await Promise.race([beforeDisconnect(), this.#forcing]);
|
|
98
215
|
} finally {
|
|
99
216
|
const connection = this.#current;
|
|
100
217
|
this.#current = void 0;
|
|
101
218
|
this.#ready = false;
|
|
102
|
-
|
|
219
|
+
if (connection !== void 0) {
|
|
220
|
+
await this.#closeConnection(connection);
|
|
221
|
+
}
|
|
103
222
|
}
|
|
104
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Stops reconnecting and cuts every connection, open or closing, without the closing handshake. A
|
|
226
|
+
* connection that finishes opening afterwards is cut at once.
|
|
227
|
+
*/
|
|
228
|
+
forceClose() {
|
|
229
|
+
this.#closed = true;
|
|
230
|
+
this.#forced = true;
|
|
231
|
+
this.#stop.abort();
|
|
232
|
+
this.#force.abort();
|
|
233
|
+
for (const connection of [this.#current, ...this.#closing]) {
|
|
234
|
+
if (connection !== void 0) {
|
|
235
|
+
destroySocket(connection);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** Closes a connection, which `forceClose` can cut until the broker answered; cuts it once forced. */
|
|
240
|
+
async #closeConnection(connection) {
|
|
241
|
+
if (this.#forced) {
|
|
242
|
+
destroySocket(connection);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
this.#closing.add(connection);
|
|
246
|
+
await connection.close().catch(() => void 0);
|
|
247
|
+
this.#closing.delete(connection);
|
|
248
|
+
}
|
|
105
249
|
async #establish() {
|
|
106
|
-
const connection = await this.#settings.connect();
|
|
250
|
+
const connection = await this.#settings.connect({ signal: this.#force.signal });
|
|
107
251
|
connection.on("error", (error) => {
|
|
108
252
|
this.#settings.logger.warn(
|
|
109
253
|
`[ConnectionSupervisor](establish) Connection error | error=${error.message}`
|
|
110
254
|
);
|
|
111
255
|
});
|
|
256
|
+
if (this.#closed) {
|
|
257
|
+
await this.#closeConnection(connection);
|
|
258
|
+
throw new RabbitMqUnavailableError("[ConnectionSupervisor](establish) Transport is closed");
|
|
259
|
+
}
|
|
112
260
|
connection.on("close", (error) => {
|
|
113
261
|
this.#handleLoss(connection, error?.message ?? "closed by peer");
|
|
114
262
|
});
|
|
@@ -131,7 +279,7 @@ var ConnectionSupervisor = class {
|
|
|
131
279
|
this.#current = void 0;
|
|
132
280
|
}
|
|
133
281
|
this.#settings.onDisconnected();
|
|
134
|
-
await connection
|
|
282
|
+
await this.#closeConnection(connection);
|
|
135
283
|
}
|
|
136
284
|
#handleLoss(connection, reason) {
|
|
137
285
|
if (connection !== this.#current) {
|
|
@@ -158,7 +306,7 @@ var ConnectionSupervisor = class {
|
|
|
158
306
|
for (let attempt = 1; !this.#closed; attempt += 1) {
|
|
159
307
|
const delayMs = computeRetryDelay(policy, attempt, this.#settings.random);
|
|
160
308
|
this.#settings.logger.log(
|
|
161
|
-
`[ConnectionSupervisor](
|
|
309
|
+
`[ConnectionSupervisor](reconnectUntilConnected) Reconnecting | attempt=${attempt} delayMs=${delayMs}`
|
|
162
310
|
);
|
|
163
311
|
await abortableSleep(delayMs, this.#stop.signal);
|
|
164
312
|
if (this.#closed) {
|
|
@@ -167,78 +315,111 @@ var ConnectionSupervisor = class {
|
|
|
167
315
|
try {
|
|
168
316
|
await this.#establish();
|
|
169
317
|
this.#settings.logger.log(
|
|
170
|
-
`[ConnectionSupervisor](
|
|
318
|
+
`[ConnectionSupervisor](reconnectUntilConnected) Reconnected | attempt=${attempt}`
|
|
171
319
|
);
|
|
172
320
|
return;
|
|
173
321
|
} catch (error) {
|
|
174
322
|
this.#settings.logger.warn(
|
|
175
|
-
`[ConnectionSupervisor](
|
|
323
|
+
`[ConnectionSupervisor](reconnectUntilConnected) Attempt failed | attempt=${attempt} error=${toErrorInfo(error).message}`
|
|
176
324
|
);
|
|
177
325
|
}
|
|
178
326
|
}
|
|
179
327
|
}
|
|
180
328
|
};
|
|
329
|
+
function destroySocket(connection) {
|
|
330
|
+
const model = connection;
|
|
331
|
+
model.connection?.stream?.destroy(
|
|
332
|
+
new RabbitMqUnavailableError("Connection cut: the shutdown deadline passed")
|
|
333
|
+
);
|
|
334
|
+
}
|
|
181
335
|
|
|
182
336
|
// src/json-payload.ts
|
|
183
337
|
function assertJsonPayload(payload, eventId) {
|
|
184
|
-
visit(payload, "$", /* @__PURE__ */ new Set(), eventId);
|
|
338
|
+
visit(payload, { path: "$", ancestors: /* @__PURE__ */ new Set(), eventId });
|
|
185
339
|
}
|
|
186
|
-
function visit(value,
|
|
340
|
+
function visit(value, {
|
|
341
|
+
path,
|
|
342
|
+
ancestors,
|
|
343
|
+
eventId
|
|
344
|
+
}) {
|
|
187
345
|
switch (typeof value) {
|
|
188
346
|
case "string":
|
|
189
347
|
case "boolean":
|
|
190
348
|
return;
|
|
191
349
|
case "number":
|
|
192
350
|
if (!Number.isFinite(value)) {
|
|
193
|
-
throw refuse(eventId, path, `is ${value}, which JSON turns into null`);
|
|
351
|
+
throw refuse(eventId, { path, reason: `is ${value}, which JSON turns into null` });
|
|
194
352
|
}
|
|
195
353
|
return;
|
|
196
354
|
case "object":
|
|
197
355
|
if (value !== null) {
|
|
198
|
-
visitObject(value, path, ancestors, eventId);
|
|
356
|
+
visitObject(value, { path, ancestors, eventId });
|
|
199
357
|
}
|
|
200
358
|
return;
|
|
201
359
|
default:
|
|
202
|
-
throw refuse(eventId, path, `is ${typeof value}, which JSON cannot represent`);
|
|
360
|
+
throw refuse(eventId, { path, reason: `is ${typeof value}, which JSON cannot represent` });
|
|
203
361
|
}
|
|
204
362
|
}
|
|
205
|
-
function visitObject(value,
|
|
363
|
+
function visitObject(value, {
|
|
364
|
+
path,
|
|
365
|
+
ancestors,
|
|
366
|
+
eventId
|
|
367
|
+
}) {
|
|
206
368
|
if (ancestors.has(value)) {
|
|
207
|
-
throw refuse(eventId, path, "is a circular reference");
|
|
369
|
+
throw refuse(eventId, { path, reason: "is a circular reference" });
|
|
208
370
|
}
|
|
209
371
|
ancestors.add(value);
|
|
210
372
|
if (Array.isArray(value)) {
|
|
211
|
-
|
|
212
|
-
if (!(index in value)) {
|
|
213
|
-
throw refuse(eventId, `${path}[${index}]`, "is an array hole, which JSON turns into null");
|
|
214
|
-
}
|
|
215
|
-
visit(value[index], `${path}[${index}]`, ancestors, eventId);
|
|
216
|
-
}
|
|
373
|
+
visitArray(value, { path, ancestors, eventId });
|
|
217
374
|
} else {
|
|
218
|
-
|
|
219
|
-
if (prototype !== Object.prototype && prototype !== null) {
|
|
220
|
-
throw refuse(eventId, path, "is not a plain object (class instance, Date, Map or Set)");
|
|
221
|
-
}
|
|
222
|
-
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
223
|
-
throw refuse(eventId, path, "has a symbol key, which JSON drops");
|
|
224
|
-
}
|
|
225
|
-
for (const key of Object.keys(value)) {
|
|
226
|
-
visit(value[key], `${path}.${key}`, ancestors, eventId);
|
|
227
|
-
}
|
|
375
|
+
visitPlainObject(value, { path, ancestors, eventId });
|
|
228
376
|
}
|
|
229
377
|
ancestors.delete(value);
|
|
230
378
|
}
|
|
231
|
-
function
|
|
379
|
+
function visitArray(value, { path, ancestors, eventId }) {
|
|
380
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
381
|
+
if (!(index in value)) {
|
|
382
|
+
throw refuse(eventId, {
|
|
383
|
+
path: `${path}[${index}]`,
|
|
384
|
+
reason: "is an array hole, which JSON turns into null"
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
visit(value[index], { path: `${path}[${index}]`, ancestors, eventId });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function visitPlainObject(value, { path, ancestors, eventId }) {
|
|
391
|
+
const prototype = Object.getPrototypeOf(value);
|
|
392
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
393
|
+
throw refuse(eventId, {
|
|
394
|
+
path,
|
|
395
|
+
reason: "is not a plain object (class instance, Date, Map or Set)"
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
399
|
+
throw refuse(eventId, { path, reason: "has a symbol key, which JSON drops" });
|
|
400
|
+
}
|
|
401
|
+
for (const key of Object.keys(value)) {
|
|
402
|
+
visit(value[key], {
|
|
403
|
+
path: `${path}.${key}`,
|
|
404
|
+
ancestors,
|
|
405
|
+
eventId
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function refuse(eventId, { path, reason }) {
|
|
232
410
|
return new UnserializablePayloadError(
|
|
233
411
|
`Event ${eventId} has a payload JSON cannot represent faithfully: ${path} ${reason}`
|
|
234
412
|
);
|
|
235
413
|
}
|
|
236
414
|
|
|
237
415
|
// src/wire-format.ts
|
|
416
|
+
var MILLISECONDS_PER_SECOND = 1e3;
|
|
238
417
|
var ATTRIBUTE_PREFIX = "cloudEvents:";
|
|
239
418
|
var EVENT_HEADER_PREFIX = "relaybus:";
|
|
240
419
|
var SPEC_VERSION = "1.0";
|
|
241
420
|
var JSON_CONTENT_TYPE = "application/json";
|
|
421
|
+
var STRUCTURED_CONTENT_TYPE = "application/cloudevents+json";
|
|
422
|
+
var RESERVED_HEADER_PREFIXES = ["x-death", "x-first-death-", "x-last-death-", "x-relaybus-"];
|
|
242
423
|
var SHORT_STRING_LIMIT_BYTES = 255;
|
|
243
424
|
var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
|
|
244
425
|
function encodeEnvelope(envelope) {
|
|
@@ -252,6 +433,22 @@ function encodeEnvelope(envelope) {
|
|
|
252
433
|
[`${ATTRIBUTE_PREFIX}time`]: envelope.occurredAt.toISOString(),
|
|
253
434
|
[`${ATTRIBUTE_PREFIX}datacontenttype`]: JSON_CONTENT_TYPE
|
|
254
435
|
};
|
|
436
|
+
addOptionalAttributes(headers, envelope);
|
|
437
|
+
return {
|
|
438
|
+
content: Buffer.from(JSON.stringify(envelope.payload), "utf8"),
|
|
439
|
+
options: {
|
|
440
|
+
contentType: JSON_CONTENT_TYPE,
|
|
441
|
+
persistent: true,
|
|
442
|
+
messageId: envelope.id,
|
|
443
|
+
timestamp: Math.floor(envelope.occurredAt.getTime() / MILLISECONDS_PER_SECOND),
|
|
444
|
+
...mirror("type", envelope.type),
|
|
445
|
+
...mirror("appId", envelope.source),
|
|
446
|
+
...envelope.correlationId === null ? {} : mirror("correlationId", envelope.correlationId),
|
|
447
|
+
headers
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function addOptionalAttributes(headers, envelope) {
|
|
255
452
|
if (envelope.key !== null) {
|
|
256
453
|
headers[`${ATTRIBUTE_PREFIX}subject`] = envelope.key;
|
|
257
454
|
}
|
|
@@ -268,20 +465,44 @@ function encodeEnvelope(envelope) {
|
|
|
268
465
|
);
|
|
269
466
|
headers[`${EVENT_HEADER_PREFIX}${name}`] = value;
|
|
270
467
|
}
|
|
468
|
+
}
|
|
469
|
+
function encodeStructuredEnvelope(envelope) {
|
|
470
|
+
assertJsonPayload(envelope.payload, envelope.id);
|
|
471
|
+
assertShortString("Event id", envelope.id);
|
|
472
|
+
const document = writeStructuredCloudEvent(envelope, {
|
|
473
|
+
time: envelope.occurredAt.toISOString(),
|
|
474
|
+
data: JSON.stringify(envelope.payload)
|
|
475
|
+
});
|
|
271
476
|
return {
|
|
272
|
-
content: Buffer.from(
|
|
477
|
+
content: Buffer.from(document, "utf8"),
|
|
273
478
|
options: {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
timestamp: Math.floor(envelope.occurredAt.getTime() / 1e3),
|
|
278
|
-
...mirror("type", envelope.type),
|
|
279
|
-
...mirror("appId", envelope.source),
|
|
280
|
-
...envelope.correlationId === null ? {} : mirror("correlationId", envelope.correlationId),
|
|
281
|
-
headers
|
|
479
|
+
...messageProperties(envelope),
|
|
480
|
+
contentType: STRUCTURED_CONTENT_TYPE,
|
|
481
|
+
headers: plainHeaders(envelope, envelope.headers)
|
|
282
482
|
}
|
|
283
483
|
};
|
|
284
484
|
}
|
|
485
|
+
function messageProperties(envelope) {
|
|
486
|
+
return {
|
|
487
|
+
persistent: true,
|
|
488
|
+
messageId: envelope.id,
|
|
489
|
+
timestamp: Math.floor(envelope.occurredAt.getTime() / MILLISECONDS_PER_SECOND),
|
|
490
|
+
...mirror("type", envelope.type),
|
|
491
|
+
...mirror("appId", envelope.source),
|
|
492
|
+
...envelope.correlationId === null ? {} : mirror("correlationId", envelope.correlationId)
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function plainHeaders(envelope, headers) {
|
|
496
|
+
for (const name of Object.keys(headers)) {
|
|
497
|
+
if (RESERVED_HEADER_PREFIXES.some((prefix) => name.startsWith(prefix))) {
|
|
498
|
+
throw new RabbitMqUnencodableEventError(
|
|
499
|
+
`Event ${envelope.id} has a header '${name}', a name the broker or the transport reserves`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
assertShortString(`Header name '${name}'`, name);
|
|
503
|
+
}
|
|
504
|
+
return Object.fromEntries(Object.entries(headers));
|
|
505
|
+
}
|
|
285
506
|
function assertShortString(label, value) {
|
|
286
507
|
const bytes = Buffer.byteLength(value, "utf8");
|
|
287
508
|
if (bytes > SHORT_STRING_LIMIT_BYTES) {
|
|
@@ -339,7 +560,7 @@ function parsePayload(content) {
|
|
|
339
560
|
}
|
|
340
561
|
function parseTime(rawTime, timestampSeconds) {
|
|
341
562
|
if (rawTime === void 0) {
|
|
342
|
-
const fallback = typeof timestampSeconds === "number" ? new Date(timestampSeconds *
|
|
563
|
+
const fallback = typeof timestampSeconds === "number" ? new Date(timestampSeconds * MILLISECONDS_PER_SECOND) : void 0;
|
|
343
564
|
if (fallback === void 0 || Number.isNaN(fallback.getTime())) {
|
|
344
565
|
throw new MalformedMessageError("Message has no valid time (header cloudEvents:time)");
|
|
345
566
|
}
|
|
@@ -366,6 +587,8 @@ function required(attribute, value) {
|
|
|
366
587
|
}
|
|
367
588
|
|
|
368
589
|
// src/inbound-decoders.ts
|
|
590
|
+
var MILLISECONDS_PER_SECOND2 = 1e3;
|
|
591
|
+
var { optionalAttribute, requiredAttribute, assertValidType, assertJsonDataContentType } = createCloudEventReaders((reason) => new MalformedMessageError(reason));
|
|
369
592
|
var STRUCTURED_MEDIA_TYPE = "application/cloudevents+json";
|
|
370
593
|
var strictUtf8 = new TextDecoder("utf-8", { fatal: true });
|
|
371
594
|
var SPEC_VERSION2 = "1.0";
|
|
@@ -426,18 +649,13 @@ function plainJsonDecoder(options) {
|
|
|
426
649
|
if (properties.headers?.[SPEC_VERSION_HEADER] !== void 0 || mediaTypeOf(properties.contentType) === STRUCTURED_MEDIA_TYPE) {
|
|
427
650
|
return decodeCloudEvent(message);
|
|
428
651
|
}
|
|
429
|
-
const plain =
|
|
430
|
-
|
|
431
|
-
properties,
|
|
432
|
-
headers: readForeignHeaders(properties),
|
|
433
|
-
payload: parseJsonBody(message.content)
|
|
434
|
-
};
|
|
435
|
-
const type = resolveText(options.type, plain, "type");
|
|
652
|
+
const plain = plainMessageOf(message);
|
|
653
|
+
const type = RABBITMQ_EVENTS.text(options.type, { plain, name: "type" });
|
|
436
654
|
if (type === void 0) {
|
|
437
655
|
throw new MalformedMessageError("Message has no event type: the type option found none");
|
|
438
656
|
}
|
|
439
657
|
assertValidType(type, "The event type");
|
|
440
|
-
const id =
|
|
658
|
+
const id = RABBITMQ_EVENTS.text(options.id, { plain, name: "id" }) ?? nonEmptyText(properties.messageId);
|
|
441
659
|
if (id === void 0) {
|
|
442
660
|
throw new MalformedMessageError(
|
|
443
661
|
"Message has no id: set the messageId property on the producer or configure the id option"
|
|
@@ -446,8 +664,8 @@ function plainJsonDecoder(options) {
|
|
|
446
664
|
return {
|
|
447
665
|
id,
|
|
448
666
|
type,
|
|
449
|
-
source:
|
|
450
|
-
key:
|
|
667
|
+
source: RABBITMQ_EVENTS.text(options.source, { plain, name: "source" }) ?? defaultSource(message),
|
|
668
|
+
key: RABBITMQ_EVENTS.text(options.key, { plain, name: "key" }) ?? null,
|
|
451
669
|
payload: plain.payload,
|
|
452
670
|
headers: plain.headers,
|
|
453
671
|
correlationId: null,
|
|
@@ -456,27 +674,35 @@ function plainJsonDecoder(options) {
|
|
|
456
674
|
};
|
|
457
675
|
};
|
|
458
676
|
}
|
|
459
|
-
function
|
|
460
|
-
|
|
461
|
-
throw new MalformedMessageError("The decoder must return an event object synchronously");
|
|
462
|
-
}
|
|
463
|
-
if ("then" in decoded) {
|
|
464
|
-
observeRejection(decoded);
|
|
465
|
-
throw new MalformedMessageError("The decoder must return an event object synchronously");
|
|
466
|
-
}
|
|
467
|
-
const event = decoded;
|
|
677
|
+
function plainMessageOf(message) {
|
|
678
|
+
const { properties } = message;
|
|
468
679
|
return {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
payload: event.payload,
|
|
474
|
-
headers: event.headers ?? {},
|
|
475
|
-
correlationId: event.correlationId ?? null,
|
|
476
|
-
causationId: event.causationId ?? null,
|
|
477
|
-
occurredAt: event.occurredAt ?? propertyTimeOrNow(message.properties)
|
|
680
|
+
routingKey: message.fields.routingKey,
|
|
681
|
+
properties,
|
|
682
|
+
headers: readForeignHeaders(properties),
|
|
683
|
+
payload: parseJsonBody(message.content)
|
|
478
684
|
};
|
|
479
685
|
}
|
|
686
|
+
var RabbitMqDecodedEvents = class extends DecodedEventReader {
|
|
687
|
+
invalid(reason) {
|
|
688
|
+
return new MalformedMessageError(reason);
|
|
689
|
+
}
|
|
690
|
+
defaultSource(message) {
|
|
691
|
+
return defaultSource(message);
|
|
692
|
+
}
|
|
693
|
+
/** None: AMQP messages carry no ordering key. */
|
|
694
|
+
defaultKey() {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
/** The `timestamp` property, else the time of receipt. */
|
|
698
|
+
defaultTime(message) {
|
|
699
|
+
return propertyTimeOrNow(message.properties);
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
var RABBITMQ_EVENTS = new RabbitMqDecodedEvents();
|
|
703
|
+
function completeDecodedEvent(decoded, message) {
|
|
704
|
+
return RABBITMQ_EVENTS.complete(decoded, message);
|
|
705
|
+
}
|
|
480
706
|
function parseJsonBody(content) {
|
|
481
707
|
let text2;
|
|
482
708
|
try {
|
|
@@ -490,47 +716,12 @@ function parseJsonBody(content) {
|
|
|
490
716
|
throw new MalformedMessageError("Message body is not valid JSON");
|
|
491
717
|
}
|
|
492
718
|
}
|
|
493
|
-
function mediaTypeOf(contentType) {
|
|
494
|
-
return typeof contentType === "string" ? contentType.split(";")[0].trim().toLowerCase() : void 0;
|
|
495
|
-
}
|
|
496
|
-
function isJsonObject(value) {
|
|
497
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
498
|
-
}
|
|
499
719
|
function nonEmptyText(value) {
|
|
500
720
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
501
721
|
}
|
|
502
722
|
function defaultSource(message) {
|
|
503
723
|
return nonEmptyText(message.properties.appId) ?? message.fields.routingKey;
|
|
504
724
|
}
|
|
505
|
-
function optionalAttribute(body, name) {
|
|
506
|
-
const value = body[name];
|
|
507
|
-
if (value !== void 0 && typeof value !== "string") {
|
|
508
|
-
throw new MalformedMessageError(`Attribute '${name}' must be a string`);
|
|
509
|
-
}
|
|
510
|
-
return value;
|
|
511
|
-
}
|
|
512
|
-
function requiredAttribute(body, name) {
|
|
513
|
-
const value = optionalAttribute(body, name);
|
|
514
|
-
if (value === void 0 || value.length === 0) {
|
|
515
|
-
throw new MalformedMessageError(`Attribute '${name}' is missing or empty`);
|
|
516
|
-
}
|
|
517
|
-
return value;
|
|
518
|
-
}
|
|
519
|
-
function assertValidType(type, label) {
|
|
520
|
-
try {
|
|
521
|
-
assertEventType(type);
|
|
522
|
-
} catch (error) {
|
|
523
|
-
throw new MalformedMessageError(
|
|
524
|
-
`${label} is not a valid event type: ${error.message}`
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
function assertJsonDataContentType(dataContentType) {
|
|
529
|
-
const mediaType = mediaTypeOf(dataContentType);
|
|
530
|
-
if (mediaType !== void 0 && mediaType !== "application/json" && mediaType !== "text/json" && !mediaType.endsWith("+json")) {
|
|
531
|
-
throw new MalformedMessageError(`datacontenttype must be JSON, received '${dataContentType}'`);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
725
|
function parseTime2(text2) {
|
|
535
726
|
const time = new Date(text2);
|
|
536
727
|
if (!RFC3339_PATTERN.test(text2) || Number.isNaN(time.getTime())) {
|
|
@@ -540,7 +731,7 @@ function parseTime2(text2) {
|
|
|
540
731
|
}
|
|
541
732
|
function propertyTimeOrNow(properties) {
|
|
542
733
|
const { timestamp } = properties;
|
|
543
|
-
return typeof timestamp === "number" && Number.isFinite(timestamp) ? new Date(timestamp *
|
|
734
|
+
return typeof timestamp === "number" && Number.isFinite(timestamp) ? new Date(timestamp * MILLISECONDS_PER_SECOND2) : /* @__PURE__ */ new Date();
|
|
544
735
|
}
|
|
545
736
|
function extensionAttributes(body) {
|
|
546
737
|
return Object.fromEntries(
|
|
@@ -556,19 +747,6 @@ function readForeignHeaders(properties) {
|
|
|
556
747
|
)
|
|
557
748
|
);
|
|
558
749
|
}
|
|
559
|
-
function resolveText(option, message, name) {
|
|
560
|
-
const value = typeof option === "function" ? option(message) : option;
|
|
561
|
-
if (value === void 0 || value === null) {
|
|
562
|
-
return void 0;
|
|
563
|
-
}
|
|
564
|
-
if (typeof value !== "string") {
|
|
565
|
-
observeRejection(value);
|
|
566
|
-
throw new MalformedMessageError(
|
|
567
|
-
`The ${name} option returned ${typeof value}, expected a string`
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
return value.length === 0 ? void 0 : value;
|
|
571
|
-
}
|
|
572
750
|
function resolveOccurredAt(option, message) {
|
|
573
751
|
const value = option?.(message);
|
|
574
752
|
if (value === void 0) {
|
|
@@ -580,14 +758,62 @@ function resolveOccurredAt(option, message) {
|
|
|
580
758
|
}
|
|
581
759
|
return value;
|
|
582
760
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
761
|
+
|
|
762
|
+
// src/codecs.ts
|
|
763
|
+
var cloudEventsBinary = Object.freeze({
|
|
764
|
+
encode: encodeEnvelope,
|
|
765
|
+
decode: decodeEnvelope
|
|
766
|
+
});
|
|
767
|
+
var cloudEventsStructured = Object.freeze({
|
|
768
|
+
encode: encodeStructuredEnvelope,
|
|
769
|
+
decode: decodeCloudEvent
|
|
770
|
+
});
|
|
771
|
+
var MESSAGE_ID_PROPERTY = "message-id-property";
|
|
772
|
+
var DEFAULT_CONTENT_TYPE = "application/json";
|
|
773
|
+
function jsonEnvelope(mapping) {
|
|
774
|
+
const format = createJsonEnvelopeFormat(mapping, {
|
|
775
|
+
transportIds: [MESSAGE_ID_PROPERTY],
|
|
776
|
+
failures: {
|
|
777
|
+
invalid: (reason) => new MalformedMessageError(reason),
|
|
778
|
+
unencodable: (reason) => new RabbitMqUnencodableEventError(reason)
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
const contentType = mapping.contentType ?? DEFAULT_CONTENT_TYPE;
|
|
782
|
+
return {
|
|
783
|
+
encode: (envelope) => {
|
|
784
|
+
assertJsonPayload(envelope.payload, envelope.id);
|
|
785
|
+
assertShortString("Event id", envelope.id);
|
|
786
|
+
const { body, headers } = format.write(envelope);
|
|
787
|
+
return {
|
|
788
|
+
content: Buffer.from(JSON.stringify(body), "utf8"),
|
|
789
|
+
options: {
|
|
790
|
+
...messageProperties(envelope),
|
|
791
|
+
contentType,
|
|
792
|
+
headers: plainHeaders(envelope, headers)
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
},
|
|
796
|
+
decode: (message) => {
|
|
797
|
+
const fields = format.read({
|
|
798
|
+
body: parseJsonBody(message.content),
|
|
799
|
+
headers: readForeignHeaders(message.properties)
|
|
800
|
+
});
|
|
801
|
+
return { ...fields, id: fields.id ?? requireMessageId(message) };
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
function requireMessageId(message) {
|
|
806
|
+
const id = nonEmptyText(message.properties.messageId);
|
|
807
|
+
if (id === void 0) {
|
|
808
|
+
throw new MalformedMessageError("Message has no id: the messageId property is missing");
|
|
586
809
|
}
|
|
810
|
+
return id;
|
|
587
811
|
}
|
|
588
812
|
var REDELIVERIES_HEADER = "x-relaybus-redeliveries";
|
|
589
813
|
var FIRST_FAILURE_HEADER = "x-relaybus-first-failure-at";
|
|
590
814
|
var DEAD_LETTER_REASON_HEADER = "x-relaybus-dead-letter-reason";
|
|
815
|
+
var DEAD_LETTER_ERROR_NAME_HEADER = "x-relaybus-dead-letter-error-name";
|
|
816
|
+
var DEAD_LETTER_ERROR_CODE_HEADER = "x-relaybus-dead-letter-error-code";
|
|
591
817
|
var ORIGINAL_ROUTING_KEY_HEADER = "x-relaybus-routing-key";
|
|
592
818
|
var RELAYBUS_STATE_HEADERS = [REDELIVERIES_HEADER, FIRST_FAILURE_HEADER];
|
|
593
819
|
var BROKER_COUNTER_HEADERS = ["x-delivery-count", "x-acquired-count"];
|
|
@@ -612,7 +838,7 @@ function buildRetryLadder(policy) {
|
|
|
612
838
|
delay *= policy.factor;
|
|
613
839
|
}
|
|
614
840
|
}
|
|
615
|
-
function chooseRetryTier(ladder, delayMs, random) {
|
|
841
|
+
function chooseRetryTier(ladder, { delayMs, random }) {
|
|
616
842
|
const upperIndex = ladder.findIndex((tier) => tier >= delayMs);
|
|
617
843
|
if (upperIndex === -1) {
|
|
618
844
|
return ladder.at(-1);
|
|
@@ -635,6 +861,8 @@ function copyForRepublish(message, headerChanges) {
|
|
|
635
861
|
for (const name of [
|
|
636
862
|
...RELAYBUS_STATE_HEADERS,
|
|
637
863
|
DEAD_LETTER_REASON_HEADER,
|
|
864
|
+
DEAD_LETTER_ERROR_NAME_HEADER,
|
|
865
|
+
DEAD_LETTER_ERROR_CODE_HEADER,
|
|
638
866
|
...BROKER_COUNTER_HEADERS
|
|
639
867
|
]) {
|
|
640
868
|
delete headers[name];
|
|
@@ -664,6 +892,8 @@ function withOriginalRoutingKey(message) {
|
|
|
664
892
|
}
|
|
665
893
|
|
|
666
894
|
// src/options.ts
|
|
895
|
+
var DEFAULT_PREFETCH = 50;
|
|
896
|
+
var DEFAULT_DELIVERY_LIMIT = 10;
|
|
667
897
|
var DEFAULT_HANDLER_RETRY = {
|
|
668
898
|
attempts: 3,
|
|
669
899
|
initialDelayMs: 200,
|
|
@@ -690,7 +920,15 @@ var DEFAULT_RECONNECT = {
|
|
|
690
920
|
retryIf: void 0
|
|
691
921
|
};
|
|
692
922
|
function resolveSettings(options) {
|
|
693
|
-
const
|
|
923
|
+
const basics = resolveBasics(options);
|
|
924
|
+
return {
|
|
925
|
+
...resolveTopology(options, basics),
|
|
926
|
+
...resolveRetries(options, basics),
|
|
927
|
+
...resolveRuntime(options, basics)
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function resolveBasics(options) {
|
|
931
|
+
const exchange = NonEmptyText.of(options.exchange ?? "relaybus", "exchange");
|
|
694
932
|
const failOnConnectError = options.failOnConnectError ?? true;
|
|
695
933
|
if (!failOnConnectError && options.reconnect === false) {
|
|
696
934
|
throw new ConfigurationError(
|
|
@@ -698,39 +936,56 @@ function resolveSettings(options) {
|
|
|
698
936
|
);
|
|
699
937
|
}
|
|
700
938
|
const ordered = options.ordered ?? false;
|
|
701
|
-
const prefetch =
|
|
939
|
+
const prefetch = PositiveInteger.of(
|
|
940
|
+
options.prefetch ?? (ordered ? 1 : DEFAULT_PREFETCH),
|
|
941
|
+
"prefetch"
|
|
942
|
+
);
|
|
702
943
|
if (ordered && prefetch !== 1) {
|
|
703
944
|
throw new ConfigurationError(`ordered consumption needs prefetch 1, received ${prefetch}`);
|
|
704
945
|
}
|
|
705
946
|
const redelivery = createRetryPolicy(options.redelivery, DEFAULT_REDELIVERY);
|
|
947
|
+
return { exchange, failOnConnectError, ordered, prefetch, redelivery };
|
|
948
|
+
}
|
|
949
|
+
function resolveTopology(options, { exchange, prefetch }) {
|
|
706
950
|
return {
|
|
707
|
-
name:
|
|
951
|
+
name: NonEmptyText.of(options.name ?? "rabbitmq", "name"),
|
|
708
952
|
connect: resolveConnect(options),
|
|
709
953
|
exchange,
|
|
710
954
|
prefetch,
|
|
711
|
-
queuePrefix:
|
|
712
|
-
consumerGroup:
|
|
713
|
-
queue:
|
|
714
|
-
deadLetterExchange:
|
|
955
|
+
queuePrefix: NonEmptyText.of(options.queuePrefix ?? "relaybus", "queuePrefix"),
|
|
956
|
+
consumerGroup: NonEmptyText.optional(options.consumerGroup, "consumerGroup"),
|
|
957
|
+
queue: NonEmptyText.optional(options.queue, "queue"),
|
|
958
|
+
deadLetterExchange: NonEmptyText.of(
|
|
715
959
|
options.deadLetterExchange ?? `${exchange}.dead`,
|
|
716
960
|
"deadLetterExchange"
|
|
717
961
|
),
|
|
718
|
-
deadLetterQueue:
|
|
719
|
-
deliveryLimit:
|
|
962
|
+
deadLetterQueue: NonEmptyText.optional(options.deadLetterQueue, "deadLetterQueue"),
|
|
963
|
+
deliveryLimit: PositiveInteger.of(
|
|
964
|
+
options.deliveryLimit ?? DEFAULT_DELIVERY_LIMIT,
|
|
965
|
+
"deliveryLimit"
|
|
966
|
+
)
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
function resolveRetries(options, { ordered, redelivery }) {
|
|
970
|
+
return {
|
|
720
971
|
handlerRetry: createRetryPolicy(options.handlerRetry, DEFAULT_HANDLER_RETRY),
|
|
721
972
|
redelivery,
|
|
722
973
|
retryLadder: ordered ? [] : buildRetryLadder(redelivery),
|
|
723
|
-
redeliveryMaxAgeMs:
|
|
974
|
+
redeliveryMaxAgeMs: Milliseconds.positive(
|
|
724
975
|
options.redelivery?.maxAge ?? DEFAULT_REDELIVERY_MAX_AGE,
|
|
725
976
|
"redelivery.maxAge"
|
|
726
977
|
),
|
|
727
|
-
ordered
|
|
978
|
+
ordered
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
function resolveRuntime(options, { failOnConnectError }) {
|
|
982
|
+
return {
|
|
728
983
|
reconnect: resolveReconnect(options.reconnect),
|
|
729
984
|
failOnConnectError,
|
|
730
985
|
allowUnroutable: options.allowUnroutable ?? false,
|
|
731
|
-
publishTimeoutMs:
|
|
732
|
-
shutdownTimeoutMs:
|
|
733
|
-
|
|
986
|
+
publishTimeoutMs: Milliseconds.positive(options.publishTimeout ?? "30s", "publishTimeout"),
|
|
987
|
+
shutdownTimeoutMs: Milliseconds.of(options.shutdownTimeout ?? "30s", "shutdownTimeout"),
|
|
988
|
+
...resolveCodec(options),
|
|
734
989
|
logger: options.logger ?? console,
|
|
735
990
|
random: options.random ?? Math.random
|
|
736
991
|
};
|
|
@@ -746,11 +1001,21 @@ function resolveQueueName(settings) {
|
|
|
746
1001
|
}
|
|
747
1002
|
return `${settings.queuePrefix}.${settings.consumerGroup}`;
|
|
748
1003
|
}
|
|
749
|
-
function
|
|
750
|
-
|
|
751
|
-
|
|
1004
|
+
function resolveCodec({
|
|
1005
|
+
codec,
|
|
1006
|
+
decode
|
|
1007
|
+
}) {
|
|
1008
|
+
if (decode !== void 0 && codec !== void 0) {
|
|
1009
|
+
throw new ConfigurationError("Give 'codec' or the deprecated 'decode', not both");
|
|
1010
|
+
}
|
|
1011
|
+
const chosen = decode === void 0 ? codec ?? cloudEventsBinary : { encode: encodeEnvelope, decode };
|
|
1012
|
+
if (chosen === cloudEventsBinary) {
|
|
1013
|
+
return { encode: encodeEnvelope, decode: decodeEnvelope };
|
|
752
1014
|
}
|
|
753
|
-
return
|
|
1015
|
+
return {
|
|
1016
|
+
encode: chosen.encode,
|
|
1017
|
+
decode: (message) => completeDecodedEvent(chosen.decode(message), message)
|
|
1018
|
+
};
|
|
754
1019
|
}
|
|
755
1020
|
function resolveConnect(options) {
|
|
756
1021
|
const { url, connect } = options;
|
|
@@ -760,9 +1025,10 @@ function resolveConnect(options) {
|
|
|
760
1025
|
if (connect !== void 0) {
|
|
761
1026
|
return connect;
|
|
762
1027
|
}
|
|
763
|
-
return async () => {
|
|
1028
|
+
return async ({ signal }) => {
|
|
764
1029
|
const amqplib = await import('amqplib');
|
|
765
|
-
|
|
1030
|
+
const socketOptions = { signal };
|
|
1031
|
+
return amqplib.connect(url, socketOptions);
|
|
766
1032
|
};
|
|
767
1033
|
}
|
|
768
1034
|
function resolveReconnect(reconnect) {
|
|
@@ -771,28 +1037,6 @@ function resolveReconnect(reconnect) {
|
|
|
771
1037
|
}
|
|
772
1038
|
return createRetryPolicy({ ...reconnect, attempts: 1 }, DEFAULT_RECONNECT);
|
|
773
1039
|
}
|
|
774
|
-
function nonEmpty(value, name) {
|
|
775
|
-
if (value.length === 0) {
|
|
776
|
-
throw new ConfigurationError(`${name} must not be empty`);
|
|
777
|
-
}
|
|
778
|
-
return value;
|
|
779
|
-
}
|
|
780
|
-
function optionalNonEmpty(value, name) {
|
|
781
|
-
return value === void 0 ? void 0 : nonEmpty(value, name);
|
|
782
|
-
}
|
|
783
|
-
function positiveInteger(value, name) {
|
|
784
|
-
if (!Number.isInteger(value) || value < 1) {
|
|
785
|
-
throw new ConfigurationError(`${name} must be a positive integer, received ${value}`);
|
|
786
|
-
}
|
|
787
|
-
return value;
|
|
788
|
-
}
|
|
789
|
-
function positiveDuration(value, name) {
|
|
790
|
-
const milliseconds = toMilliseconds(value, name);
|
|
791
|
-
if (milliseconds === 0) {
|
|
792
|
-
throw new ConfigurationError(`${name} must be greater than zero`);
|
|
793
|
-
}
|
|
794
|
-
return milliseconds;
|
|
795
|
-
}
|
|
796
1040
|
|
|
797
1041
|
// src/topology.ts
|
|
798
1042
|
function retryQueueName(queue, delayMs) {
|
|
@@ -934,74 +1178,115 @@ var QueueConsumer = class {
|
|
|
934
1178
|
this.#inFlight.add(task);
|
|
935
1179
|
}
|
|
936
1180
|
async #process(channel, message) {
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
try {
|
|
940
|
-
envelope = this.#settings.decode(withOriginalRoutingKey(message));
|
|
941
|
-
} catch (error) {
|
|
942
|
-
this.#settings.logger.error(
|
|
943
|
-
`[QueueConsumer](process) Dead-lettering malformed message | queue=${queue} routingKey=${message.fields.routingKey} error=${toErrorInfo(error).message}`
|
|
944
|
-
);
|
|
945
|
-
await this.#deadLetter(channel, message, "malformed message");
|
|
1181
|
+
const decoded = await this.#decodeOrDeadLetter(channel, message);
|
|
1182
|
+
if (decoded === void 0) {
|
|
946
1183
|
return;
|
|
947
1184
|
}
|
|
948
|
-
|
|
949
|
-
|
|
1185
|
+
const { envelope } = decoded;
|
|
1186
|
+
const delivery = {
|
|
1187
|
+
channel,
|
|
1188
|
+
message,
|
|
1189
|
+
envelope,
|
|
1190
|
+
redeliveries: readCounterHeader(message, REDELIVERIES_HEADER) ?? 0,
|
|
1191
|
+
firstFailureAt: readCounterHeader(message, FIRST_FAILURE_HEADER)
|
|
1192
|
+
};
|
|
950
1193
|
for (let attempt = 1; ; attempt += 1) {
|
|
951
1194
|
try {
|
|
952
|
-
await this.#settings.onMessage(envelope);
|
|
953
|
-
this.#settle(channel, message, "ack");
|
|
1195
|
+
await this.#settings.onMessage(envelope, { destination: message.fields.routingKey });
|
|
1196
|
+
this.#settle(channel, { message, settlement: "ack" });
|
|
954
1197
|
return;
|
|
955
1198
|
} catch (error) {
|
|
956
|
-
|
|
957
|
-
if (!isRetryableError(handlerRetry, error)) {
|
|
958
|
-
this.#settings.logger.error(
|
|
959
|
-
`[QueueConsumer](process) Dead-lettering message the handler cannot process | ${failure}`
|
|
960
|
-
);
|
|
961
|
-
await this.#deadLetter(channel, message, "non-retryable handler error");
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
if (attempt < handlerRetry.attempts) {
|
|
965
|
-
const delayMs2 = computeRetryDelay(handlerRetry, attempt, this.#settings.random);
|
|
966
|
-
this.#settings.logger.warn(
|
|
967
|
-
`[QueueConsumer](process) Handler failed, retrying | ${failure} delayMs=${delayMs2}`
|
|
968
|
-
);
|
|
969
|
-
if (await this.#interrupted(delayMs2)) {
|
|
970
|
-
this.#settle(channel, message, "requeue");
|
|
971
|
-
return;
|
|
972
|
-
}
|
|
973
|
-
continue;
|
|
974
|
-
}
|
|
975
|
-
redeliveries += 1;
|
|
976
|
-
const now = Date.now();
|
|
977
|
-
firstFailureAt ??= now;
|
|
978
|
-
const delayMs = this.#redeliveryDelay(redeliveries);
|
|
979
|
-
if (now + delayMs - firstFailureAt > this.#settings.redeliveryMaxAgeMs) {
|
|
980
|
-
this.#settings.logger.error(
|
|
981
|
-
`[QueueConsumer](process) Dead-lettering message that kept failing | ${failure} redeliveries=${redeliveries - 1} failingForMs=${now - firstFailureAt} maxAgeMs=${this.#settings.redeliveryMaxAgeMs}`
|
|
982
|
-
);
|
|
983
|
-
await this.#deadLetter(channel, message, "redelivery window exhausted");
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
this.#settings.logger.warn(
|
|
987
|
-
`[QueueConsumer](process) Handler attempts spent, retrying later | ${failure} redeliveries=${redeliveries} delayMs=${delayMs}`
|
|
988
|
-
);
|
|
989
|
-
if (!this.#settings.ordered) {
|
|
990
|
-
await this.#redeliverLater(channel, message, delayMs, redeliveries, firstFailureAt);
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
if (await this.#interrupted(delayMs)) {
|
|
994
|
-
this.#settle(channel, message, "requeue");
|
|
1199
|
+
if (await this.#afterFailure(delivery, { attempt, error }) === "settled") {
|
|
995
1200
|
return;
|
|
996
1201
|
}
|
|
997
1202
|
}
|
|
998
1203
|
}
|
|
999
1204
|
}
|
|
1205
|
+
/** The event of `message`, or undefined after dead-lettering a message that is not one. */
|
|
1206
|
+
async #decodeOrDeadLetter(channel, message) {
|
|
1207
|
+
try {
|
|
1208
|
+
return { envelope: this.#settings.decode(withOriginalRoutingKey(message)) };
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
this.#settings.logger.error(
|
|
1211
|
+
`[QueueConsumer](decodeOrDeadLetter) Dead-lettering malformed message | queue=${this.#settings.queue} routingKey=${message.fields.routingKey} ${describeError(error)}`
|
|
1212
|
+
);
|
|
1213
|
+
this.#settings.onInvalidMessage?.({ destination: message.fields.routingKey, error });
|
|
1214
|
+
await this.#deadLetter(channel, { message, reason: "malformed message", error });
|
|
1215
|
+
return void 0;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Decides what follows a failed attempt: dead-letter a non-retryable error, retry on the same
|
|
1220
|
+
* delivery while local attempts remain, else redeliver later. `retry` runs the handler again.
|
|
1221
|
+
*/
|
|
1222
|
+
async #afterFailure(delivery, { attempt, error }) {
|
|
1223
|
+
const { channel, message, envelope } = delivery;
|
|
1224
|
+
const { queue, handlerRetry } = this.#settings;
|
|
1225
|
+
const failure = `queue=${queue} eventId=${envelope.id} attempt=${attempt} ${describeError(error)}`;
|
|
1226
|
+
if (!isRetryableError(handlerRetry, error)) {
|
|
1227
|
+
this.#settings.logger.error(
|
|
1228
|
+
`[QueueConsumer](afterFailure) Dead-lettering message the handler cannot process | ${failure}`
|
|
1229
|
+
);
|
|
1230
|
+
await this.#deadLetter(channel, {
|
|
1231
|
+
message,
|
|
1232
|
+
reason: "non-retryable handler error",
|
|
1233
|
+
error
|
|
1234
|
+
});
|
|
1235
|
+
return "settled";
|
|
1236
|
+
}
|
|
1237
|
+
if (attempt < handlerRetry.attempts) {
|
|
1238
|
+
const delayMs = computeRetryDelay(handlerRetry, attempt, this.#settings.random);
|
|
1239
|
+
this.#settings.logger.warn(
|
|
1240
|
+
`[QueueConsumer](afterFailure) Handler failed, retrying | ${failure} delayMs=${delayMs}`
|
|
1241
|
+
);
|
|
1242
|
+
return this.#waitOrRequeue(delivery, delayMs);
|
|
1243
|
+
}
|
|
1244
|
+
return this.#afterSpentAttempts(delivery, { failure, error });
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* The local attempts are spent: dead-letter a message that kept failing past `redeliveryMaxAgeMs`,
|
|
1248
|
+
* else park it in a retry queue, or, when ordered, wait and retry it in place.
|
|
1249
|
+
*/
|
|
1250
|
+
async #afterSpentAttempts(delivery, { failure, error }) {
|
|
1251
|
+
const { channel, message } = delivery;
|
|
1252
|
+
delivery.redeliveries += 1;
|
|
1253
|
+
const now = Date.now();
|
|
1254
|
+
delivery.firstFailureAt ??= now;
|
|
1255
|
+
const { redeliveries, firstFailureAt } = delivery;
|
|
1256
|
+
const delayMs = this.#redeliveryDelay(redeliveries);
|
|
1257
|
+
if (now + delayMs - firstFailureAt > this.#settings.redeliveryMaxAgeMs) {
|
|
1258
|
+
this.#settings.logger.error(
|
|
1259
|
+
`[QueueConsumer](afterSpentAttempts) Dead-lettering message that kept failing | ${failure} redeliveries=${redeliveries - 1} failingForMs=${now - firstFailureAt} maxAgeMs=${this.#settings.redeliveryMaxAgeMs}`
|
|
1260
|
+
);
|
|
1261
|
+
await this.#deadLetter(channel, {
|
|
1262
|
+
message,
|
|
1263
|
+
reason: "redelivery window exhausted",
|
|
1264
|
+
error
|
|
1265
|
+
});
|
|
1266
|
+
return "settled";
|
|
1267
|
+
}
|
|
1268
|
+
this.#settings.logger.warn(
|
|
1269
|
+
`[QueueConsumer](afterSpentAttempts) Handler attempts spent, retrying later | ${failure} redeliveries=${redeliveries} delayMs=${delayMs}`
|
|
1270
|
+
);
|
|
1271
|
+
if (!this.#settings.ordered) {
|
|
1272
|
+
await this.#redeliverLater(channel, { message, delayMs, redeliveries, firstFailureAt });
|
|
1273
|
+
return "settled";
|
|
1274
|
+
}
|
|
1275
|
+
return this.#waitOrRequeue(delivery, delayMs);
|
|
1276
|
+
}
|
|
1277
|
+
/** Waits `delayMs` before the next attempt; requeues instead when the consumer stops meanwhile. */
|
|
1278
|
+
async #waitOrRequeue(delivery, delayMs) {
|
|
1279
|
+
if (await this.#interrupted(delayMs)) {
|
|
1280
|
+
this.#settle(delivery.channel, { message: delivery.message, settlement: "requeue" });
|
|
1281
|
+
return "settled";
|
|
1282
|
+
}
|
|
1283
|
+
return "retry";
|
|
1284
|
+
}
|
|
1000
1285
|
/** The backoff before the next delayed redelivery: a retry queue's TTL, or an exact wait when ordered. */
|
|
1001
1286
|
#redeliveryDelay(redeliveries) {
|
|
1002
1287
|
const { redelivery, retryLadder, ordered, random } = this.#settings;
|
|
1003
1288
|
const delayMs = computeRetryDelay(redelivery, redeliveries, random);
|
|
1004
|
-
return ordered ? delayMs : chooseRetryTier(retryLadder, delayMs, random);
|
|
1289
|
+
return ordered ? delayMs : chooseRetryTier(retryLadder, { delayMs, random });
|
|
1005
1290
|
}
|
|
1006
1291
|
/** Sleeps; true when the consumer is stopping, so the caller must give the message back. */
|
|
1007
1292
|
async #interrupted(delayMs) {
|
|
@@ -1014,7 +1299,8 @@ var QueueConsumer = class {
|
|
|
1014
1299
|
* the original is requeued after the same delay, so it is neither lost nor
|
|
1015
1300
|
* hot-looped.
|
|
1016
1301
|
*/
|
|
1017
|
-
async #redeliverLater(channel,
|
|
1302
|
+
async #redeliverLater(channel, input) {
|
|
1303
|
+
const { message, delayMs, redeliveries, firstFailureAt } = input;
|
|
1018
1304
|
const { content, options } = copyForRepublish(message, {
|
|
1019
1305
|
[REDELIVERIES_HEADER]: String(redeliveries),
|
|
1020
1306
|
[FIRST_FAILURE_HEADER]: String(firstFailureAt),
|
|
@@ -1022,20 +1308,18 @@ var QueueConsumer = class {
|
|
|
1022
1308
|
});
|
|
1023
1309
|
try {
|
|
1024
1310
|
await this.#settings.republish(
|
|
1025
|
-
"",
|
|
1026
|
-
|
|
1027
|
-
content,
|
|
1028
|
-
options
|
|
1311
|
+
{ exchange: "", routingKey: retryQueueName(this.#settings.queue, delayMs) },
|
|
1312
|
+
{ content, options }
|
|
1029
1313
|
);
|
|
1030
1314
|
} catch (error) {
|
|
1031
1315
|
this.#settings.logger.warn(
|
|
1032
1316
|
`[QueueConsumer](redeliverLater) Could not park the message in the retry queue, requeueing it | queue=${this.#settings.queue} delayMs=${delayMs} error=${toErrorInfo(error).message}`
|
|
1033
1317
|
);
|
|
1034
1318
|
await this.#interrupted(delayMs);
|
|
1035
|
-
this.#settle(channel, message, "requeue");
|
|
1319
|
+
this.#settle(channel, { message, settlement: "requeue" });
|
|
1036
1320
|
return;
|
|
1037
1321
|
}
|
|
1038
|
-
this.#settle(channel, message, "ack");
|
|
1322
|
+
this.#settle(channel, { message, settlement: "ack" });
|
|
1039
1323
|
}
|
|
1040
1324
|
/**
|
|
1041
1325
|
* Moves the message to the dead-letter queue through a confirmed copy that
|
|
@@ -1043,27 +1327,29 @@ var QueueConsumer = class {
|
|
|
1043
1327
|
* message is rejected without requeue, which makes the broker dead-letter it
|
|
1044
1328
|
* (at-least-once), so it still ends up in the dead-letter queue.
|
|
1045
1329
|
*/
|
|
1046
|
-
async #deadLetter(channel,
|
|
1330
|
+
async #deadLetter(channel, input) {
|
|
1331
|
+
const { message, reason, error } = input;
|
|
1332
|
+
const { name, code } = toErrorInfo(error);
|
|
1047
1333
|
const { content, options } = copyForRepublish(message, {
|
|
1048
|
-
[DEAD_LETTER_REASON_HEADER]: reason
|
|
1334
|
+
[DEAD_LETTER_REASON_HEADER]: reason,
|
|
1335
|
+
[DEAD_LETTER_ERROR_NAME_HEADER]: name,
|
|
1336
|
+
...code === void 0 ? {} : { [DEAD_LETTER_ERROR_CODE_HEADER]: code }
|
|
1049
1337
|
});
|
|
1050
1338
|
try {
|
|
1051
1339
|
await this.#settings.republish(
|
|
1052
|
-
this.#settings.deadLetterExchange,
|
|
1053
|
-
|
|
1054
|
-
content,
|
|
1055
|
-
options
|
|
1340
|
+
{ exchange: this.#settings.deadLetterExchange, routingKey: this.#settings.queue },
|
|
1341
|
+
{ content, options }
|
|
1056
1342
|
);
|
|
1057
|
-
} catch (
|
|
1343
|
+
} catch (error2) {
|
|
1058
1344
|
this.#settings.logger.warn(
|
|
1059
|
-
`[QueueConsumer](deadLetter) Could not publish to the dead-letter exchange, leaving it to the broker | queue=${this.#settings.queue} error=${toErrorInfo(
|
|
1345
|
+
`[QueueConsumer](deadLetter) Could not publish to the dead-letter exchange, leaving it to the broker | queue=${this.#settings.queue} error=${toErrorInfo(error2).message}`
|
|
1060
1346
|
);
|
|
1061
|
-
this.#settle(channel, message, "dead-letter");
|
|
1347
|
+
this.#settle(channel, { message, settlement: "dead-letter" });
|
|
1062
1348
|
return;
|
|
1063
1349
|
}
|
|
1064
|
-
this.#settle(channel, message, "ack");
|
|
1350
|
+
this.#settle(channel, { message, settlement: "ack" });
|
|
1065
1351
|
}
|
|
1066
|
-
#settle(channel, message, settlement) {
|
|
1352
|
+
#settle(channel, { message, settlement }) {
|
|
1067
1353
|
try {
|
|
1068
1354
|
if (settlement === "ack") {
|
|
1069
1355
|
channel.ack(message);
|
|
@@ -1077,6 +1363,10 @@ var QueueConsumer = class {
|
|
|
1077
1363
|
}
|
|
1078
1364
|
}
|
|
1079
1365
|
};
|
|
1366
|
+
function describeError(error) {
|
|
1367
|
+
const { message, name, code } = toErrorInfo(error);
|
|
1368
|
+
return `error=${message} errorName=${name}${code === void 0 ? "" : ` code=${code}`}`;
|
|
1369
|
+
}
|
|
1080
1370
|
|
|
1081
1371
|
// src/serial-queue.ts
|
|
1082
1372
|
var SerialQueue = class {
|
|
@@ -1089,19 +1379,22 @@ var SerialQueue = class {
|
|
|
1089
1379
|
};
|
|
1090
1380
|
|
|
1091
1381
|
// src/rabbitmq-transport.ts
|
|
1092
|
-
var
|
|
1093
|
-
var RabbitMqTransport = class {
|
|
1382
|
+
var RabbitMqTransport = class extends SubscribingTransport {
|
|
1094
1383
|
name;
|
|
1384
|
+
system = "rabbitmq";
|
|
1095
1385
|
#settings;
|
|
1096
1386
|
#supervisor;
|
|
1387
|
+
#publishing;
|
|
1097
1388
|
/** Serializes topology work so a subscription never races a reconnect. */
|
|
1098
1389
|
#topologyWork = new SerialQueue();
|
|
1099
1390
|
#live;
|
|
1100
1391
|
#subscription;
|
|
1101
|
-
#
|
|
1392
|
+
#closing;
|
|
1102
1393
|
constructor(options) {
|
|
1394
|
+
super();
|
|
1103
1395
|
this.#settings = resolveSettings(options);
|
|
1104
1396
|
this.name = this.#settings.name;
|
|
1397
|
+
this.#publishing = new ConfirmedPublishing(this.name, this.#settings.publishTimeoutMs);
|
|
1105
1398
|
this.#supervisor = new ConnectionSupervisor({
|
|
1106
1399
|
connect: this.#settings.connect,
|
|
1107
1400
|
reconnect: this.#settings.reconnect,
|
|
@@ -1138,62 +1431,17 @@ var RabbitMqTransport = class {
|
|
|
1138
1431
|
* @throws {RabbitMqUnavailableError} not connected, channel lost, or confirm timed out
|
|
1139
1432
|
* @throws {RabbitMqPublishRejectedError} the broker nacked the message
|
|
1140
1433
|
* @throws {UnroutableMessageError} no queue is bound to `destination` (unless `allowUnroutable`)
|
|
1434
|
+
* @throws whatever the `codec`'s `encode` throws, such as {@link RabbitMqUnencodableEventError}
|
|
1141
1435
|
*/
|
|
1142
1436
|
async publish(envelope, destination) {
|
|
1143
1437
|
assertShortString("Destination (routing key)", destination);
|
|
1144
|
-
const { content, options } =
|
|
1145
|
-
await this.#
|
|
1146
|
-
this.#settings.exchange,
|
|
1147
|
-
destination,
|
|
1438
|
+
const { content, options } = this.#settings.encode(envelope);
|
|
1439
|
+
await this.#publishing.publish(this.#live?.publisher, {
|
|
1440
|
+
exchange: this.#settings.exchange,
|
|
1441
|
+
routingKey: destination,
|
|
1148
1442
|
content,
|
|
1149
1443
|
options,
|
|
1150
|
-
!this.#settings.allowUnroutable
|
|
1151
|
-
);
|
|
1152
|
-
}
|
|
1153
|
-
async #publishConfirmed(exchange, routingKey, content, options, mandatory) {
|
|
1154
|
-
const publisher = this.#live?.publisher;
|
|
1155
|
-
const eventId = options.messageId ?? "unknown";
|
|
1156
|
-
if (publisher === void 0) {
|
|
1157
|
-
throw new RabbitMqUnavailableError(
|
|
1158
|
-
`[RabbitMqTransport](publish) Not connected to the broker | transport=${this.name} destination=${routingKey}`
|
|
1159
|
-
);
|
|
1160
|
-
}
|
|
1161
|
-
const returnKey = unroutableKey(options.messageId, routingKey);
|
|
1162
|
-
await new Promise((resolve, reject) => {
|
|
1163
|
-
const timer = setTimeout(() => {
|
|
1164
|
-
reject(
|
|
1165
|
-
new RabbitMqUnavailableError(
|
|
1166
|
-
`[RabbitMqTransport](publish) No broker confirm within ${this.#settings.publishTimeoutMs}ms | transport=${this.name} eventId=${eventId} destination=${routingKey}`
|
|
1167
|
-
)
|
|
1168
|
-
);
|
|
1169
|
-
}, this.#settings.publishTimeoutMs);
|
|
1170
|
-
const onConfirm = (error) => {
|
|
1171
|
-
clearTimeout(timer);
|
|
1172
|
-
const returned = takeUnroutable(publisher.unroutable, returnKey);
|
|
1173
|
-
if (error) {
|
|
1174
|
-
reject(this.#toPublishError(error, eventId, routingKey));
|
|
1175
|
-
} else if (returned) {
|
|
1176
|
-
reject(
|
|
1177
|
-
new UnroutableMessageError(
|
|
1178
|
-
`[RabbitMqTransport](publish) No queue is bound to the routing key | transport=${this.name} eventId=${eventId} destination=${routingKey}`
|
|
1179
|
-
)
|
|
1180
|
-
);
|
|
1181
|
-
} else {
|
|
1182
|
-
resolve();
|
|
1183
|
-
}
|
|
1184
|
-
};
|
|
1185
|
-
try {
|
|
1186
|
-
publisher.channel.publish(
|
|
1187
|
-
exchange,
|
|
1188
|
-
routingKey,
|
|
1189
|
-
content,
|
|
1190
|
-
{ ...options, mandatory },
|
|
1191
|
-
onConfirm
|
|
1192
|
-
);
|
|
1193
|
-
} catch (error) {
|
|
1194
|
-
clearTimeout(timer);
|
|
1195
|
-
reject(this.#toPublishError(error, eventId, routingKey));
|
|
1196
|
-
}
|
|
1444
|
+
mandatory: !this.#settings.allowUnroutable
|
|
1197
1445
|
});
|
|
1198
1446
|
}
|
|
1199
1447
|
/**
|
|
@@ -1201,7 +1449,11 @@ var RabbitMqTransport = class {
|
|
|
1201
1449
|
* to `onMessage`. Call it once per transport; give each subscription its own
|
|
1202
1450
|
* transport instance, with its own `name` and `consumerGroup`.
|
|
1203
1451
|
*/
|
|
1204
|
-
async
|
|
1452
|
+
async openSubscription({
|
|
1453
|
+
destinations,
|
|
1454
|
+
onMessage,
|
|
1455
|
+
hooks
|
|
1456
|
+
}) {
|
|
1205
1457
|
if (this.#subscription !== void 0) {
|
|
1206
1458
|
throw new ConfigurationError(
|
|
1207
1459
|
`Transport '${this.name}' already has a subscription; create another transport for another consumer group`
|
|
@@ -1212,33 +1464,8 @@ var RabbitMqTransport = class {
|
|
|
1212
1464
|
}
|
|
1213
1465
|
const queue = resolveQueueName(this.#settings);
|
|
1214
1466
|
const subscription = {
|
|
1215
|
-
topology:
|
|
1216
|
-
|
|
1217
|
-
queue,
|
|
1218
|
-
deadLetterExchange: this.#settings.deadLetterExchange,
|
|
1219
|
-
deadLetterQueue: this.#settings.deadLetterQueue ?? `${queue}.dead`,
|
|
1220
|
-
deliveryLimit: this.#settings.deliveryLimit,
|
|
1221
|
-
ordered: this.#settings.ordered,
|
|
1222
|
-
retryDelaysMs: this.#settings.retryLadder,
|
|
1223
|
-
destinations
|
|
1224
|
-
},
|
|
1225
|
-
consumer: new QueueConsumer({
|
|
1226
|
-
queue,
|
|
1227
|
-
prefetch: this.#settings.prefetch,
|
|
1228
|
-
handlerRetry: this.#settings.handlerRetry,
|
|
1229
|
-
redelivery: this.#settings.redelivery,
|
|
1230
|
-
retryLadder: this.#settings.retryLadder,
|
|
1231
|
-
redeliveryMaxAgeMs: this.#settings.redeliveryMaxAgeMs,
|
|
1232
|
-
ordered: this.#settings.ordered,
|
|
1233
|
-
deadLetterExchange: this.#settings.deadLetterExchange,
|
|
1234
|
-
republish: (exchange, routingKey, content, options) => this.#publishConfirmed(exchange, routingKey, content, options, true),
|
|
1235
|
-
shutdownTimeoutMs: this.#settings.shutdownTimeoutMs,
|
|
1236
|
-
onMessage,
|
|
1237
|
-
decode: this.#settings.decode,
|
|
1238
|
-
onConsumerLost: (reason) => this.#supervisor.recover(reason),
|
|
1239
|
-
logger: this.#settings.logger,
|
|
1240
|
-
random: this.#settings.random
|
|
1241
|
-
})
|
|
1467
|
+
topology: this.#topologyOf(queue, destinations),
|
|
1468
|
+
consumer: this.#createConsumer(queue, { onMessage, hooks })
|
|
1242
1469
|
};
|
|
1243
1470
|
await this.#topologyWork.run(async () => {
|
|
1244
1471
|
this.#subscription = subscription;
|
|
@@ -1247,24 +1474,70 @@ var RabbitMqTransport = class {
|
|
|
1247
1474
|
}
|
|
1248
1475
|
});
|
|
1249
1476
|
}
|
|
1477
|
+
#topologyOf(queue, destinations) {
|
|
1478
|
+
return {
|
|
1479
|
+
exchange: this.#settings.exchange,
|
|
1480
|
+
queue,
|
|
1481
|
+
deadLetterExchange: this.#settings.deadLetterExchange,
|
|
1482
|
+
deadLetterQueue: this.#settings.deadLetterQueue ?? `${queue}.dead`,
|
|
1483
|
+
deliveryLimit: this.#settings.deliveryLimit,
|
|
1484
|
+
ordered: this.#settings.ordered,
|
|
1485
|
+
retryDelaysMs: this.#settings.retryLadder,
|
|
1486
|
+
destinations
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
#createConsumer(queue, { onMessage, hooks }) {
|
|
1490
|
+
return new QueueConsumer({
|
|
1491
|
+
queue,
|
|
1492
|
+
prefetch: this.#settings.prefetch,
|
|
1493
|
+
handlerRetry: this.#settings.handlerRetry,
|
|
1494
|
+
redelivery: this.#settings.redelivery,
|
|
1495
|
+
retryLadder: this.#settings.retryLadder,
|
|
1496
|
+
redeliveryMaxAgeMs: this.#settings.redeliveryMaxAgeMs,
|
|
1497
|
+
ordered: this.#settings.ordered,
|
|
1498
|
+
deadLetterExchange: this.#settings.deadLetterExchange,
|
|
1499
|
+
republish: ({ exchange, routingKey }, { content, options }) => this.#publishing.publish(this.#live?.publisher, {
|
|
1500
|
+
exchange,
|
|
1501
|
+
routingKey,
|
|
1502
|
+
content,
|
|
1503
|
+
options,
|
|
1504
|
+
mandatory: true
|
|
1505
|
+
}),
|
|
1506
|
+
shutdownTimeoutMs: this.#settings.shutdownTimeoutMs,
|
|
1507
|
+
onMessage,
|
|
1508
|
+
...hooks === void 0 ? {} : { onInvalidMessage: hooks.onInvalidMessage },
|
|
1509
|
+
decode: this.#settings.decode,
|
|
1510
|
+
onConsumerLost: (reason) => this.#supervisor.recover(reason),
|
|
1511
|
+
logger: this.#settings.logger,
|
|
1512
|
+
random: this.#settings.random
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1250
1515
|
/**
|
|
1251
1516
|
* Stops consuming, waits up to `shutdownTimeout` for running handlers
|
|
1252
1517
|
* (messages still being retried go back to the queue), then closes the
|
|
1253
|
-
* channels and the connection.
|
|
1518
|
+
* channels and the connection. Once `options.signal` aborts (the bus shutdown deadline passed),
|
|
1519
|
+
* the connection's socket is destroyed instead of waiting for the broker.
|
|
1254
1520
|
*/
|
|
1255
|
-
async close() {
|
|
1256
|
-
|
|
1257
|
-
|
|
1521
|
+
async close(options = {}) {
|
|
1522
|
+
const { signal } = options;
|
|
1523
|
+
const force = () => this.#supervisor.forceClose();
|
|
1524
|
+
if (signal?.aborted === true) {
|
|
1525
|
+
force();
|
|
1526
|
+
}
|
|
1527
|
+
signal?.addEventListener("abort", force, { once: true });
|
|
1528
|
+
try {
|
|
1529
|
+
this.#closing ??= this.#supervisor.close(
|
|
1530
|
+
() => this.#topologyWork.run(async () => {
|
|
1531
|
+
const live = this.#live;
|
|
1532
|
+
this.#live = void 0;
|
|
1533
|
+
await this.#subscription?.consumer.stop();
|
|
1534
|
+
await live?.publisher.channel.close().catch(() => void 0);
|
|
1535
|
+
})
|
|
1536
|
+
);
|
|
1537
|
+
await this.#closing;
|
|
1538
|
+
} finally {
|
|
1539
|
+
signal?.removeEventListener("abort", force);
|
|
1258
1540
|
}
|
|
1259
|
-
this.#closed = true;
|
|
1260
|
-
await this.#supervisor.close(
|
|
1261
|
-
() => this.#topologyWork.run(async () => {
|
|
1262
|
-
const live = this.#live;
|
|
1263
|
-
this.#live = void 0;
|
|
1264
|
-
await this.#subscription?.consumer.stop();
|
|
1265
|
-
await live?.publisher.channel.close().catch(() => void 0);
|
|
1266
|
-
})
|
|
1267
|
-
);
|
|
1268
1541
|
}
|
|
1269
1542
|
async #establishChannels(connection) {
|
|
1270
1543
|
await this.#topologyWork.run(async () => {
|
|
@@ -1308,64 +1581,8 @@ var RabbitMqTransport = class {
|
|
|
1308
1581
|
this.#live = void 0;
|
|
1309
1582
|
this.#subscription?.consumer.detach();
|
|
1310
1583
|
}
|
|
1311
|
-
#toPublishError(error, eventId, destination) {
|
|
1312
|
-
const { message } = toErrorInfo(error);
|
|
1313
|
-
const detail = `transport=${this.name} eventId=${eventId} destination=${destination} error=${message}`;
|
|
1314
|
-
return message === NACK_MESSAGE ? new RabbitMqPublishRejectedError(
|
|
1315
|
-
`[RabbitMqTransport](publish) Broker nacked the message | ${detail}`,
|
|
1316
|
-
{ cause: error }
|
|
1317
|
-
) : new RabbitMqUnavailableError(`[RabbitMqTransport](publish) Publish failed | ${detail}`, {
|
|
1318
|
-
cause: error
|
|
1319
|
-
});
|
|
1320
|
-
}
|
|
1321
1584
|
};
|
|
1322
|
-
function unroutableKey(messageId, routingKey) {
|
|
1323
|
-
return `${String(messageId)}
|
|
1324
|
-
${routingKey}`;
|
|
1325
|
-
}
|
|
1326
|
-
function recordUnroutable(unroutable, key) {
|
|
1327
|
-
unroutable.set(key, (unroutable.get(key) ?? 0) + 1);
|
|
1328
|
-
}
|
|
1329
|
-
function takeUnroutable(unroutable, key) {
|
|
1330
|
-
const count = unroutable.get(key);
|
|
1331
|
-
if (count === void 0) {
|
|
1332
|
-
return false;
|
|
1333
|
-
}
|
|
1334
|
-
if (count === 1) {
|
|
1335
|
-
unroutable.delete(key);
|
|
1336
|
-
} else {
|
|
1337
|
-
unroutable.set(key, count - 1);
|
|
1338
|
-
}
|
|
1339
|
-
return true;
|
|
1340
|
-
}
|
|
1341
|
-
function fromHeader(name) {
|
|
1342
|
-
if (name.length === 0) {
|
|
1343
|
-
throw new ConfigurationError("fromHeader needs a header name");
|
|
1344
|
-
}
|
|
1345
|
-
return (message) => Object.hasOwn(message.headers, name) ? message.headers[name] : void 0;
|
|
1346
|
-
}
|
|
1347
|
-
function fromPayloadField(path) {
|
|
1348
|
-
const segments = path.split(".");
|
|
1349
|
-
if (segments.some((segment) => segment.length === 0)) {
|
|
1350
|
-
throw new ConfigurationError(
|
|
1351
|
-
`fromPayloadField needs a dotted path of non-empty names, received '${path}'`
|
|
1352
|
-
);
|
|
1353
|
-
}
|
|
1354
|
-
return (message) => {
|
|
1355
|
-
let value = message.payload;
|
|
1356
|
-
for (const segment of segments) {
|
|
1357
|
-
if (typeof value !== "object" || value === null || !Object.hasOwn(value, segment)) {
|
|
1358
|
-
return void 0;
|
|
1359
|
-
}
|
|
1360
|
-
value = value[segment];
|
|
1361
|
-
}
|
|
1362
|
-
if (typeof value === "string") {
|
|
1363
|
-
return value;
|
|
1364
|
-
}
|
|
1365
|
-
return Number.isSafeInteger(value) ? String(value) : void 0;
|
|
1366
|
-
};
|
|
1367
|
-
}
|
|
1368
1585
|
|
|
1369
|
-
export { AmqpLimitExceededError, MalformedMessageError, RabbitMqPublishRejectedError, RabbitMqTransport, RabbitMqUnavailableError, UnroutableMessageError, UnserializablePayloadError,
|
|
1586
|
+
export { AmqpLimitExceededError, MalformedMessageError, RabbitMqPublishRejectedError, RabbitMqTransport, RabbitMqUnavailableError, RabbitMqUnencodableEventError, UnroutableMessageError, UnserializablePayloadError, cloudEventsBinary, cloudEventsStructured, decodeCloudEvent, jsonEnvelope, plainJsonDecoder };
|
|
1370
1587
|
//# sourceMappingURL=index.js.map
|
|
1371
1588
|
//# sourceMappingURL=index.js.map
|