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