@relaybus/rabbitmq 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -23
- package/dist/index.cjs +459 -189
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +150 -2
- package/dist/index.d.ts +150 -2
- package/dist/index.js +457 -191
- package/dist/index.js.map +1 -1
- package/package.json +22 -9
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RelayBusError, NonRetryableError, ConfigurationError, toErrorInfo, createRetryPolicy, toMilliseconds, computeRetryDelay, isRetryableError } from '@relaybus/core';
|
|
1
|
+
import { RelayBusError, NonRetryableError, assertEventType, ConfigurationError, toErrorInfo, createRetryPolicy, toMilliseconds, computeRetryDelay, isRetryableError } from '@relaybus/core';
|
|
2
2
|
|
|
3
3
|
// src/errors.ts
|
|
4
4
|
var RabbitMqUnavailableError = class extends RelayBusError {
|
|
@@ -178,9 +178,417 @@ var ConnectionSupervisor = class {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
};
|
|
181
|
+
|
|
182
|
+
// src/json-payload.ts
|
|
183
|
+
function assertJsonPayload(payload, eventId) {
|
|
184
|
+
visit(payload, "$", /* @__PURE__ */ new Set(), eventId);
|
|
185
|
+
}
|
|
186
|
+
function visit(value, path, ancestors, eventId) {
|
|
187
|
+
switch (typeof value) {
|
|
188
|
+
case "string":
|
|
189
|
+
case "boolean":
|
|
190
|
+
return;
|
|
191
|
+
case "number":
|
|
192
|
+
if (!Number.isFinite(value)) {
|
|
193
|
+
throw refuse(eventId, path, `is ${value}, which JSON turns into null`);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
case "object":
|
|
197
|
+
if (value !== null) {
|
|
198
|
+
visitObject(value, path, ancestors, eventId);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
default:
|
|
202
|
+
throw refuse(eventId, path, `is ${typeof value}, which JSON cannot represent`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function visitObject(value, path, ancestors, eventId) {
|
|
206
|
+
if (ancestors.has(value)) {
|
|
207
|
+
throw refuse(eventId, path, "is a circular reference");
|
|
208
|
+
}
|
|
209
|
+
ancestors.add(value);
|
|
210
|
+
if (Array.isArray(value)) {
|
|
211
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
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
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
const prototype = Object.getPrototypeOf(value);
|
|
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
|
+
}
|
|
228
|
+
}
|
|
229
|
+
ancestors.delete(value);
|
|
230
|
+
}
|
|
231
|
+
function refuse(eventId, path, reason) {
|
|
232
|
+
return new UnserializablePayloadError(
|
|
233
|
+
`Event ${eventId} has a payload JSON cannot represent faithfully: ${path} ${reason}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/wire-format.ts
|
|
238
|
+
var ATTRIBUTE_PREFIX = "cloudEvents:";
|
|
239
|
+
var EVENT_HEADER_PREFIX = "relaybus:";
|
|
240
|
+
var SPEC_VERSION = "1.0";
|
|
241
|
+
var JSON_CONTENT_TYPE = "application/json";
|
|
242
|
+
var SHORT_STRING_LIMIT_BYTES = 255;
|
|
243
|
+
var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
|
|
244
|
+
function encodeEnvelope(envelope) {
|
|
245
|
+
assertJsonPayload(envelope.payload, envelope.id);
|
|
246
|
+
assertShortString("Event id", envelope.id);
|
|
247
|
+
const headers = {
|
|
248
|
+
[`${ATTRIBUTE_PREFIX}specversion`]: SPEC_VERSION,
|
|
249
|
+
[`${ATTRIBUTE_PREFIX}id`]: envelope.id,
|
|
250
|
+
[`${ATTRIBUTE_PREFIX}type`]: envelope.type,
|
|
251
|
+
[`${ATTRIBUTE_PREFIX}source`]: envelope.source,
|
|
252
|
+
[`${ATTRIBUTE_PREFIX}time`]: envelope.occurredAt.toISOString(),
|
|
253
|
+
[`${ATTRIBUTE_PREFIX}datacontenttype`]: JSON_CONTENT_TYPE
|
|
254
|
+
};
|
|
255
|
+
if (envelope.key !== null) {
|
|
256
|
+
headers[`${ATTRIBUTE_PREFIX}subject`] = envelope.key;
|
|
257
|
+
}
|
|
258
|
+
if (envelope.causationId !== null) {
|
|
259
|
+
headers[`${ATTRIBUTE_PREFIX}causationid`] = envelope.causationId;
|
|
260
|
+
}
|
|
261
|
+
if (envelope.correlationId !== null) {
|
|
262
|
+
headers[`${ATTRIBUTE_PREFIX}correlationid`] = envelope.correlationId;
|
|
263
|
+
}
|
|
264
|
+
for (const [name, value] of Object.entries(envelope.headers)) {
|
|
265
|
+
assertShortString(
|
|
266
|
+
`Header name '${EVENT_HEADER_PREFIX}${name}'`,
|
|
267
|
+
`${EVENT_HEADER_PREFIX}${name}`
|
|
268
|
+
);
|
|
269
|
+
headers[`${EVENT_HEADER_PREFIX}${name}`] = value;
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
content: Buffer.from(JSON.stringify(envelope.payload), "utf8"),
|
|
273
|
+
options: {
|
|
274
|
+
contentType: JSON_CONTENT_TYPE,
|
|
275
|
+
persistent: true,
|
|
276
|
+
messageId: envelope.id,
|
|
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
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function assertShortString(label, value) {
|
|
286
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
287
|
+
if (bytes > SHORT_STRING_LIMIT_BYTES) {
|
|
288
|
+
throw new AmqpLimitExceededError(
|
|
289
|
+
`${label} is ${bytes} bytes, above the AMQP limit of ${SHORT_STRING_LIMIT_BYTES} bytes`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function mirror(name, value) {
|
|
294
|
+
return Buffer.byteLength(value, "utf8") > SHORT_STRING_LIMIT_BYTES ? {} : { [name]: value };
|
|
295
|
+
}
|
|
296
|
+
function decodeEnvelope(message) {
|
|
297
|
+
const { properties } = message;
|
|
298
|
+
const attributes = properties.headers ?? {};
|
|
299
|
+
if (attributes[`${ATTRIBUTE_PREFIX}specversion`] !== SPEC_VERSION) {
|
|
300
|
+
throw new MalformedMessageError(
|
|
301
|
+
`Message is not a CloudEvents ${SPEC_VERSION} event (header ${ATTRIBUTE_PREFIX}specversion)`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
id: required("id", text(attributes[`${ATTRIBUTE_PREFIX}id`]) ?? text(properties.messageId)),
|
|
306
|
+
type: required("type", text(attributes[`${ATTRIBUTE_PREFIX}type`]) ?? text(properties.type)),
|
|
307
|
+
source: required(
|
|
308
|
+
"source",
|
|
309
|
+
text(attributes[`${ATTRIBUTE_PREFIX}source`]) ?? text(properties.appId)
|
|
310
|
+
),
|
|
311
|
+
key: text(attributes[`${ATTRIBUTE_PREFIX}subject`]) ?? null,
|
|
312
|
+
payload: parsePayload(message.content),
|
|
313
|
+
headers: extractEventHeaders(attributes),
|
|
314
|
+
correlationId: text(attributes[`${ATTRIBUTE_PREFIX}correlationid`]) ?? text(properties.correlationId) ?? null,
|
|
315
|
+
causationId: text(attributes[`${ATTRIBUTE_PREFIX}causationid`]) ?? null,
|
|
316
|
+
occurredAt: parseTime(attributes[`${ATTRIBUTE_PREFIX}time`], properties.timestamp)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function extractEventHeaders(attributes) {
|
|
320
|
+
const headers = {};
|
|
321
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
322
|
+
if (name.startsWith(EVENT_HEADER_PREFIX) && typeof value === "string") {
|
|
323
|
+
Object.defineProperty(headers, name.slice(EVENT_HEADER_PREFIX.length), {
|
|
324
|
+
value,
|
|
325
|
+
enumerable: true,
|
|
326
|
+
writable: true,
|
|
327
|
+
configurable: true
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return headers;
|
|
332
|
+
}
|
|
333
|
+
function parsePayload(content) {
|
|
334
|
+
try {
|
|
335
|
+
return JSON.parse(content.toString("utf8"));
|
|
336
|
+
} catch {
|
|
337
|
+
throw new MalformedMessageError("Message body is not valid JSON");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function parseTime(rawTime, timestampSeconds) {
|
|
341
|
+
if (rawTime === void 0) {
|
|
342
|
+
const fallback = typeof timestampSeconds === "number" ? new Date(timestampSeconds * 1e3) : void 0;
|
|
343
|
+
if (fallback === void 0 || Number.isNaN(fallback.getTime())) {
|
|
344
|
+
throw new MalformedMessageError("Message has no valid time (header cloudEvents:time)");
|
|
345
|
+
}
|
|
346
|
+
return fallback;
|
|
347
|
+
}
|
|
348
|
+
const occurredAt = typeof rawTime === "string" && RFC3339_PATTERN.test(rawTime) ? new Date(rawTime) : void 0;
|
|
349
|
+
if (occurredAt === void 0 || Number.isNaN(occurredAt.getTime())) {
|
|
350
|
+
throw new MalformedMessageError(
|
|
351
|
+
"Message time (header cloudEvents:time) is not an RFC 3339 timestamp"
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
return occurredAt;
|
|
355
|
+
}
|
|
356
|
+
function text(value) {
|
|
357
|
+
return typeof value === "string" ? value : void 0;
|
|
358
|
+
}
|
|
359
|
+
function required(attribute, value) {
|
|
360
|
+
if (value === void 0 || value.length === 0) {
|
|
361
|
+
throw new MalformedMessageError(
|
|
362
|
+
`Message has no ${attribute} (header ${ATTRIBUTE_PREFIX}${attribute})`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
return value;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/inbound-decoders.ts
|
|
369
|
+
var STRUCTURED_MEDIA_TYPE = "application/cloudevents+json";
|
|
370
|
+
var strictUtf8 = new TextDecoder("utf-8", { fatal: true });
|
|
371
|
+
var SPEC_VERSION2 = "1.0";
|
|
372
|
+
var SPEC_VERSION_HEADER = "cloudEvents:specversion";
|
|
373
|
+
var INTERNAL_HEADER_PREFIXES = ["x-relaybus-", "x-first-death-", "x-last-death-"];
|
|
374
|
+
var STRUCTURED_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
375
|
+
"specversion",
|
|
376
|
+
"id",
|
|
377
|
+
"type",
|
|
378
|
+
"source",
|
|
379
|
+
"time",
|
|
380
|
+
"subject",
|
|
381
|
+
"datacontenttype",
|
|
382
|
+
"dataschema",
|
|
383
|
+
"data",
|
|
384
|
+
"data_base64",
|
|
385
|
+
"correlationid",
|
|
386
|
+
"causationid"
|
|
387
|
+
]);
|
|
388
|
+
function decodeCloudEvent(message) {
|
|
389
|
+
if (mediaTypeOf(message.properties.contentType) !== STRUCTURED_MEDIA_TYPE) {
|
|
390
|
+
return decodeEnvelope(message);
|
|
391
|
+
}
|
|
392
|
+
const body = parseJsonBody(message.content);
|
|
393
|
+
if (!isJsonObject(body)) {
|
|
394
|
+
throw new MalformedMessageError("A structured CloudEvent must be a JSON object");
|
|
395
|
+
}
|
|
396
|
+
if (body["specversion"] !== SPEC_VERSION2) {
|
|
397
|
+
throw new MalformedMessageError(`specversion must be '${SPEC_VERSION2}'`);
|
|
398
|
+
}
|
|
399
|
+
const type = requiredAttribute(body, "type");
|
|
400
|
+
assertValidType(type, "type");
|
|
401
|
+
assertJsonDataContentType(optionalAttribute(body, "datacontenttype"));
|
|
402
|
+
if (Object.hasOwn(body, "data_base64")) {
|
|
403
|
+
throw new MalformedMessageError(
|
|
404
|
+
"data_base64 is not supported: relaybus payloads are JSON, send them as data"
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
const time = optionalAttribute(body, "time");
|
|
408
|
+
return {
|
|
409
|
+
id: requiredAttribute(body, "id"),
|
|
410
|
+
type,
|
|
411
|
+
source: requiredAttribute(body, "source"),
|
|
412
|
+
key: optionalAttribute(body, "subject") ?? null,
|
|
413
|
+
payload: Object.hasOwn(body, "data") ? body["data"] : null,
|
|
414
|
+
headers: { ...readForeignHeaders(message.properties), ...extensionAttributes(body) },
|
|
415
|
+
correlationId: optionalAttribute(body, "correlationid") ?? null,
|
|
416
|
+
causationId: optionalAttribute(body, "causationid") ?? null,
|
|
417
|
+
occurredAt: time === void 0 ? propertyTimeOrNow(message.properties) : parseTime2(time)
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function plainJsonDecoder(options) {
|
|
421
|
+
if (typeof options.type === "string") {
|
|
422
|
+
assertEventType(options.type);
|
|
423
|
+
}
|
|
424
|
+
return (message) => {
|
|
425
|
+
const { properties } = message;
|
|
426
|
+
if (properties.headers?.[SPEC_VERSION_HEADER] !== void 0 || mediaTypeOf(properties.contentType) === STRUCTURED_MEDIA_TYPE) {
|
|
427
|
+
return decodeCloudEvent(message);
|
|
428
|
+
}
|
|
429
|
+
const plain = {
|
|
430
|
+
routingKey: message.fields.routingKey,
|
|
431
|
+
properties,
|
|
432
|
+
headers: readForeignHeaders(properties),
|
|
433
|
+
payload: parseJsonBody(message.content)
|
|
434
|
+
};
|
|
435
|
+
const type = resolveText(options.type, plain, "type");
|
|
436
|
+
if (type === void 0) {
|
|
437
|
+
throw new MalformedMessageError("Message has no event type: the type option found none");
|
|
438
|
+
}
|
|
439
|
+
assertValidType(type, "The event type");
|
|
440
|
+
const id = resolveText(options.id, plain, "id") ?? nonEmptyText(properties.messageId);
|
|
441
|
+
if (id === void 0) {
|
|
442
|
+
throw new MalformedMessageError(
|
|
443
|
+
"Message has no id: set the messageId property on the producer or configure the id option"
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
id,
|
|
448
|
+
type,
|
|
449
|
+
source: resolveText(options.source, plain, "source") ?? defaultSource(message),
|
|
450
|
+
key: resolveText(options.key, plain, "key") ?? null,
|
|
451
|
+
payload: plain.payload,
|
|
452
|
+
headers: plain.headers,
|
|
453
|
+
correlationId: null,
|
|
454
|
+
causationId: null,
|
|
455
|
+
occurredAt: resolveOccurredAt(options.occurredAt, plain)
|
|
456
|
+
};
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function completeDecodedEvent(decoded, message) {
|
|
460
|
+
if (typeof decoded !== "object" || decoded === null) {
|
|
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;
|
|
468
|
+
return {
|
|
469
|
+
id: event.id,
|
|
470
|
+
type: event.type,
|
|
471
|
+
source: event.source ?? defaultSource(message),
|
|
472
|
+
key: event.key ?? null,
|
|
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)
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function parseJsonBody(content) {
|
|
481
|
+
let text2;
|
|
482
|
+
try {
|
|
483
|
+
text2 = strictUtf8.decode(content);
|
|
484
|
+
} catch {
|
|
485
|
+
throw new MalformedMessageError("Message body is not valid UTF-8");
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
return JSON.parse(text2);
|
|
489
|
+
} catch {
|
|
490
|
+
throw new MalformedMessageError("Message body is not valid JSON");
|
|
491
|
+
}
|
|
492
|
+
}
|
|
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
|
+
function nonEmptyText(value) {
|
|
500
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
501
|
+
}
|
|
502
|
+
function defaultSource(message) {
|
|
503
|
+
return nonEmptyText(message.properties.appId) ?? message.fields.routingKey;
|
|
504
|
+
}
|
|
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
|
+
function parseTime2(text2) {
|
|
535
|
+
const time = new Date(text2);
|
|
536
|
+
if (!RFC3339_PATTERN.test(text2) || Number.isNaN(time.getTime())) {
|
|
537
|
+
throw new MalformedMessageError(`time is not an RFC 3339 timestamp: '${text2}'`);
|
|
538
|
+
}
|
|
539
|
+
return time;
|
|
540
|
+
}
|
|
541
|
+
function propertyTimeOrNow(properties) {
|
|
542
|
+
const { timestamp } = properties;
|
|
543
|
+
return typeof timestamp === "number" && Number.isFinite(timestamp) ? new Date(timestamp * 1e3) : /* @__PURE__ */ new Date();
|
|
544
|
+
}
|
|
545
|
+
function extensionAttributes(body) {
|
|
546
|
+
return Object.fromEntries(
|
|
547
|
+
Object.entries(body).filter(
|
|
548
|
+
(entry) => !STRUCTURED_ATTRIBUTES.has(entry[0]) && typeof entry[1] === "string"
|
|
549
|
+
)
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
function readForeignHeaders(properties) {
|
|
553
|
+
return Object.fromEntries(
|
|
554
|
+
Object.entries(properties.headers ?? {}).filter(
|
|
555
|
+
(entry) => typeof entry[1] === "string" && !INTERNAL_HEADER_PREFIXES.some((prefix) => entry[0].startsWith(prefix))
|
|
556
|
+
)
|
|
557
|
+
);
|
|
558
|
+
}
|
|
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
|
+
function resolveOccurredAt(option, message) {
|
|
573
|
+
const value = option?.(message);
|
|
574
|
+
if (value === void 0) {
|
|
575
|
+
return propertyTimeOrNow(message.properties);
|
|
576
|
+
}
|
|
577
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
578
|
+
observeRejection(value);
|
|
579
|
+
throw new MalformedMessageError("The occurredAt option returned no valid Date");
|
|
580
|
+
}
|
|
581
|
+
return value;
|
|
582
|
+
}
|
|
583
|
+
function observeRejection(value) {
|
|
584
|
+
if (typeof value === "object" && value !== null && "then" in value) {
|
|
585
|
+
Promise.resolve(value).catch(() => void 0);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
181
588
|
var REDELIVERIES_HEADER = "x-relaybus-redeliveries";
|
|
182
589
|
var FIRST_FAILURE_HEADER = "x-relaybus-first-failure-at";
|
|
183
590
|
var DEAD_LETTER_REASON_HEADER = "x-relaybus-dead-letter-reason";
|
|
591
|
+
var ORIGINAL_ROUTING_KEY_HEADER = "x-relaybus-routing-key";
|
|
184
592
|
var RELAYBUS_STATE_HEADERS = [REDELIVERIES_HEADER, FIRST_FAILURE_HEADER];
|
|
185
593
|
var BROKER_COUNTER_HEADERS = ["x-delivery-count", "x-acquired-count"];
|
|
186
594
|
var MAXIMUM_LADDER_TIERS = 16;
|
|
@@ -217,7 +625,7 @@ function chooseRetryTier(ladder, delayMs, random) {
|
|
|
217
625
|
return random() < (delayMs - lower) / (upper - lower) ? upper : lower;
|
|
218
626
|
}
|
|
219
627
|
function readCounterHeader(message, name) {
|
|
220
|
-
const value = message.properties.headers[name];
|
|
628
|
+
const value = message.properties.headers?.[name];
|
|
221
629
|
const parsed = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : value;
|
|
222
630
|
return typeof parsed === "number" && Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
|
|
223
631
|
}
|
|
@@ -246,6 +654,14 @@ function copyForRepublish(message, headerChanges) {
|
|
|
246
654
|
}
|
|
247
655
|
};
|
|
248
656
|
}
|
|
657
|
+
function originalRoutingKey(message) {
|
|
658
|
+
const original = message.properties.headers?.[ORIGINAL_ROUTING_KEY_HEADER];
|
|
659
|
+
return typeof original === "string" ? original : message.fields.routingKey;
|
|
660
|
+
}
|
|
661
|
+
function withOriginalRoutingKey(message) {
|
|
662
|
+
const routingKey = originalRoutingKey(message);
|
|
663
|
+
return routingKey === message.fields.routingKey ? message : { ...message, fields: { ...message.fields, routingKey } };
|
|
664
|
+
}
|
|
249
665
|
|
|
250
666
|
// src/options.ts
|
|
251
667
|
var DEFAULT_HANDLER_RETRY = {
|
|
@@ -314,6 +730,7 @@ function resolveSettings(options) {
|
|
|
314
730
|
allowUnroutable: options.allowUnroutable ?? false,
|
|
315
731
|
publishTimeoutMs: positiveDuration(options.publishTimeout ?? "30s", "publishTimeout"),
|
|
316
732
|
shutdownTimeoutMs: toMilliseconds(options.shutdownTimeout ?? "30s", "shutdownTimeout"),
|
|
733
|
+
decode: resolveDecode(options.decode),
|
|
317
734
|
logger: options.logger ?? console,
|
|
318
735
|
random: options.random ?? Math.random
|
|
319
736
|
};
|
|
@@ -329,6 +746,12 @@ function resolveQueueName(settings) {
|
|
|
329
746
|
}
|
|
330
747
|
return `${settings.queuePrefix}.${settings.consumerGroup}`;
|
|
331
748
|
}
|
|
749
|
+
function resolveDecode(decode) {
|
|
750
|
+
if (decode === void 0) {
|
|
751
|
+
return decodeEnvelope;
|
|
752
|
+
}
|
|
753
|
+
return (message) => completeDecodedEvent(decode(message), message);
|
|
754
|
+
}
|
|
332
755
|
function resolveConnect(options) {
|
|
333
756
|
const { url, connect } = options;
|
|
334
757
|
if (url === void 0 === (connect === void 0)) {
|
|
@@ -416,192 +839,6 @@ async function assertConsumerTopology(channel, topology) {
|
|
|
416
839
|
}
|
|
417
840
|
}
|
|
418
841
|
|
|
419
|
-
// src/json-payload.ts
|
|
420
|
-
function assertJsonPayload(payload, eventId) {
|
|
421
|
-
visit(payload, "$", /* @__PURE__ */ new Set(), eventId);
|
|
422
|
-
}
|
|
423
|
-
function visit(value, path, ancestors, eventId) {
|
|
424
|
-
switch (typeof value) {
|
|
425
|
-
case "string":
|
|
426
|
-
case "boolean":
|
|
427
|
-
return;
|
|
428
|
-
case "number":
|
|
429
|
-
if (!Number.isFinite(value)) {
|
|
430
|
-
throw refuse(eventId, path, `is ${value}, which JSON turns into null`);
|
|
431
|
-
}
|
|
432
|
-
return;
|
|
433
|
-
case "object":
|
|
434
|
-
if (value !== null) {
|
|
435
|
-
visitObject(value, path, ancestors, eventId);
|
|
436
|
-
}
|
|
437
|
-
return;
|
|
438
|
-
default:
|
|
439
|
-
throw refuse(eventId, path, `is ${typeof value}, which JSON cannot represent`);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
function visitObject(value, path, ancestors, eventId) {
|
|
443
|
-
if (ancestors.has(value)) {
|
|
444
|
-
throw refuse(eventId, path, "is a circular reference");
|
|
445
|
-
}
|
|
446
|
-
ancestors.add(value);
|
|
447
|
-
if (Array.isArray(value)) {
|
|
448
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
449
|
-
if (!(index in value)) {
|
|
450
|
-
throw refuse(eventId, `${path}[${index}]`, "is an array hole, which JSON turns into null");
|
|
451
|
-
}
|
|
452
|
-
visit(value[index], `${path}[${index}]`, ancestors, eventId);
|
|
453
|
-
}
|
|
454
|
-
} else {
|
|
455
|
-
const prototype = Object.getPrototypeOf(value);
|
|
456
|
-
if (prototype !== Object.prototype && prototype !== null) {
|
|
457
|
-
throw refuse(eventId, path, "is not a plain object (class instance, Date, Map or Set)");
|
|
458
|
-
}
|
|
459
|
-
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
460
|
-
throw refuse(eventId, path, "has a symbol key, which JSON drops");
|
|
461
|
-
}
|
|
462
|
-
for (const key of Object.keys(value)) {
|
|
463
|
-
visit(value[key], `${path}.${key}`, ancestors, eventId);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
ancestors.delete(value);
|
|
467
|
-
}
|
|
468
|
-
function refuse(eventId, path, reason) {
|
|
469
|
-
return new UnserializablePayloadError(
|
|
470
|
-
`Event ${eventId} has a payload JSON cannot represent faithfully: ${path} ${reason}`
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// src/wire-format.ts
|
|
475
|
-
var ATTRIBUTE_PREFIX = "cloudEvents:";
|
|
476
|
-
var EVENT_HEADER_PREFIX = "relaybus:";
|
|
477
|
-
var SPEC_VERSION = "1.0";
|
|
478
|
-
var JSON_CONTENT_TYPE = "application/json";
|
|
479
|
-
var SHORT_STRING_LIMIT_BYTES = 255;
|
|
480
|
-
var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
|
|
481
|
-
function encodeEnvelope(envelope) {
|
|
482
|
-
assertJsonPayload(envelope.payload, envelope.id);
|
|
483
|
-
assertShortString("Event id", envelope.id);
|
|
484
|
-
const headers = {
|
|
485
|
-
[`${ATTRIBUTE_PREFIX}specversion`]: SPEC_VERSION,
|
|
486
|
-
[`${ATTRIBUTE_PREFIX}id`]: envelope.id,
|
|
487
|
-
[`${ATTRIBUTE_PREFIX}type`]: envelope.type,
|
|
488
|
-
[`${ATTRIBUTE_PREFIX}source`]: envelope.source,
|
|
489
|
-
[`${ATTRIBUTE_PREFIX}time`]: envelope.occurredAt.toISOString(),
|
|
490
|
-
[`${ATTRIBUTE_PREFIX}datacontenttype`]: JSON_CONTENT_TYPE
|
|
491
|
-
};
|
|
492
|
-
if (envelope.key !== null) {
|
|
493
|
-
headers[`${ATTRIBUTE_PREFIX}subject`] = envelope.key;
|
|
494
|
-
}
|
|
495
|
-
if (envelope.causationId !== null) {
|
|
496
|
-
headers[`${ATTRIBUTE_PREFIX}causationid`] = envelope.causationId;
|
|
497
|
-
}
|
|
498
|
-
if (envelope.correlationId !== null) {
|
|
499
|
-
headers[`${ATTRIBUTE_PREFIX}correlationid`] = envelope.correlationId;
|
|
500
|
-
}
|
|
501
|
-
for (const [name, value] of Object.entries(envelope.headers)) {
|
|
502
|
-
assertShortString(
|
|
503
|
-
`Header name '${EVENT_HEADER_PREFIX}${name}'`,
|
|
504
|
-
`${EVENT_HEADER_PREFIX}${name}`
|
|
505
|
-
);
|
|
506
|
-
headers[`${EVENT_HEADER_PREFIX}${name}`] = value;
|
|
507
|
-
}
|
|
508
|
-
return {
|
|
509
|
-
content: Buffer.from(JSON.stringify(envelope.payload), "utf8"),
|
|
510
|
-
options: {
|
|
511
|
-
contentType: JSON_CONTENT_TYPE,
|
|
512
|
-
persistent: true,
|
|
513
|
-
messageId: envelope.id,
|
|
514
|
-
timestamp: Math.floor(envelope.occurredAt.getTime() / 1e3),
|
|
515
|
-
...mirror("type", envelope.type),
|
|
516
|
-
...mirror("appId", envelope.source),
|
|
517
|
-
...envelope.correlationId === null ? {} : mirror("correlationId", envelope.correlationId),
|
|
518
|
-
headers
|
|
519
|
-
}
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
function assertShortString(label, value) {
|
|
523
|
-
const bytes = Buffer.byteLength(value, "utf8");
|
|
524
|
-
if (bytes > SHORT_STRING_LIMIT_BYTES) {
|
|
525
|
-
throw new AmqpLimitExceededError(
|
|
526
|
-
`${label} is ${bytes} bytes, above the AMQP limit of ${SHORT_STRING_LIMIT_BYTES} bytes`
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
function mirror(name, value) {
|
|
531
|
-
return Buffer.byteLength(value, "utf8") > SHORT_STRING_LIMIT_BYTES ? {} : { [name]: value };
|
|
532
|
-
}
|
|
533
|
-
function decodeEnvelope(message) {
|
|
534
|
-
const { properties } = message;
|
|
535
|
-
const attributes = properties.headers ?? {};
|
|
536
|
-
if (attributes[`${ATTRIBUTE_PREFIX}specversion`] !== SPEC_VERSION) {
|
|
537
|
-
throw new MalformedMessageError(
|
|
538
|
-
`Message is not a CloudEvents ${SPEC_VERSION} event (header ${ATTRIBUTE_PREFIX}specversion)`
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
return {
|
|
542
|
-
id: required("id", text(attributes[`${ATTRIBUTE_PREFIX}id`]) ?? text(properties.messageId)),
|
|
543
|
-
type: required("type", text(attributes[`${ATTRIBUTE_PREFIX}type`]) ?? text(properties.type)),
|
|
544
|
-
source: required(
|
|
545
|
-
"source",
|
|
546
|
-
text(attributes[`${ATTRIBUTE_PREFIX}source`]) ?? text(properties.appId)
|
|
547
|
-
),
|
|
548
|
-
key: text(attributes[`${ATTRIBUTE_PREFIX}subject`]) ?? null,
|
|
549
|
-
payload: parsePayload(message.content),
|
|
550
|
-
headers: extractEventHeaders(attributes),
|
|
551
|
-
correlationId: text(attributes[`${ATTRIBUTE_PREFIX}correlationid`]) ?? text(properties.correlationId) ?? null,
|
|
552
|
-
causationId: text(attributes[`${ATTRIBUTE_PREFIX}causationid`]) ?? null,
|
|
553
|
-
occurredAt: parseTime(attributes[`${ATTRIBUTE_PREFIX}time`], properties.timestamp)
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
function extractEventHeaders(attributes) {
|
|
557
|
-
const headers = {};
|
|
558
|
-
for (const [name, value] of Object.entries(attributes)) {
|
|
559
|
-
if (name.startsWith(EVENT_HEADER_PREFIX) && typeof value === "string") {
|
|
560
|
-
Object.defineProperty(headers, name.slice(EVENT_HEADER_PREFIX.length), {
|
|
561
|
-
value,
|
|
562
|
-
enumerable: true,
|
|
563
|
-
writable: true,
|
|
564
|
-
configurable: true
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
return headers;
|
|
569
|
-
}
|
|
570
|
-
function parsePayload(content) {
|
|
571
|
-
try {
|
|
572
|
-
return JSON.parse(content.toString("utf8"));
|
|
573
|
-
} catch {
|
|
574
|
-
throw new MalformedMessageError("Message body is not valid JSON");
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
function parseTime(rawTime, timestampSeconds) {
|
|
578
|
-
if (rawTime === void 0) {
|
|
579
|
-
const fallback = typeof timestampSeconds === "number" ? new Date(timestampSeconds * 1e3) : void 0;
|
|
580
|
-
if (fallback === void 0 || Number.isNaN(fallback.getTime())) {
|
|
581
|
-
throw new MalformedMessageError("Message has no valid time (header cloudEvents:time)");
|
|
582
|
-
}
|
|
583
|
-
return fallback;
|
|
584
|
-
}
|
|
585
|
-
const occurredAt = typeof rawTime === "string" && RFC3339_PATTERN.test(rawTime) ? new Date(rawTime) : void 0;
|
|
586
|
-
if (occurredAt === void 0 || Number.isNaN(occurredAt.getTime())) {
|
|
587
|
-
throw new MalformedMessageError(
|
|
588
|
-
"Message time (header cloudEvents:time) is not an RFC 3339 timestamp"
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
return occurredAt;
|
|
592
|
-
}
|
|
593
|
-
function text(value) {
|
|
594
|
-
return typeof value === "string" ? value : void 0;
|
|
595
|
-
}
|
|
596
|
-
function required(attribute, value) {
|
|
597
|
-
if (value === void 0 || value.length === 0) {
|
|
598
|
-
throw new MalformedMessageError(
|
|
599
|
-
`Message has no ${attribute} (header ${ATTRIBUTE_PREFIX}${attribute})`
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
return value;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
842
|
// src/queue-consumer.ts
|
|
606
843
|
var QueueConsumer = class {
|
|
607
844
|
#settings;
|
|
@@ -700,7 +937,7 @@ var QueueConsumer = class {
|
|
|
700
937
|
const { queue, handlerRetry } = this.#settings;
|
|
701
938
|
let envelope;
|
|
702
939
|
try {
|
|
703
|
-
envelope =
|
|
940
|
+
envelope = this.#settings.decode(withOriginalRoutingKey(message));
|
|
704
941
|
} catch (error) {
|
|
705
942
|
this.#settings.logger.error(
|
|
706
943
|
`[QueueConsumer](process) Dead-lettering malformed message | queue=${queue} routingKey=${message.fields.routingKey} error=${toErrorInfo(error).message}`
|
|
@@ -780,7 +1017,8 @@ var QueueConsumer = class {
|
|
|
780
1017
|
async #redeliverLater(channel, message, delayMs, redeliveries, firstFailureAt) {
|
|
781
1018
|
const { content, options } = copyForRepublish(message, {
|
|
782
1019
|
[REDELIVERIES_HEADER]: String(redeliveries),
|
|
783
|
-
[FIRST_FAILURE_HEADER]: String(firstFailureAt)
|
|
1020
|
+
[FIRST_FAILURE_HEADER]: String(firstFailureAt),
|
|
1021
|
+
[ORIGINAL_ROUTING_KEY_HEADER]: originalRoutingKey(message)
|
|
784
1022
|
});
|
|
785
1023
|
try {
|
|
786
1024
|
await this.#settings.republish(
|
|
@@ -996,6 +1234,7 @@ var RabbitMqTransport = class {
|
|
|
996
1234
|
republish: (exchange, routingKey, content, options) => this.#publishConfirmed(exchange, routingKey, content, options, true),
|
|
997
1235
|
shutdownTimeoutMs: this.#settings.shutdownTimeoutMs,
|
|
998
1236
|
onMessage,
|
|
1237
|
+
decode: this.#settings.decode,
|
|
999
1238
|
onConsumerLost: (reason) => this.#supervisor.recover(reason),
|
|
1000
1239
|
logger: this.#settings.logger,
|
|
1001
1240
|
random: this.#settings.random
|
|
@@ -1099,7 +1338,34 @@ function takeUnroutable(unroutable, key) {
|
|
|
1099
1338
|
}
|
|
1100
1339
|
return true;
|
|
1101
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
|
+
}
|
|
1102
1368
|
|
|
1103
|
-
export { AmqpLimitExceededError, MalformedMessageError, RabbitMqPublishRejectedError, RabbitMqTransport, RabbitMqUnavailableError, UnroutableMessageError, UnserializablePayloadError };
|
|
1369
|
+
export { AmqpLimitExceededError, MalformedMessageError, RabbitMqPublishRejectedError, RabbitMqTransport, RabbitMqUnavailableError, UnroutableMessageError, UnserializablePayloadError, decodeCloudEvent, fromHeader, fromPayloadField, plainJsonDecoder };
|
|
1104
1370
|
//# sourceMappingURL=index.js.map
|
|
1105
1371
|
//# sourceMappingURL=index.js.map
|