@relaybus/kafka 0.3.0 → 0.4.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 +91 -37
- package/dist/index.cjs +605 -435
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -21
- package/dist/index.d.ts +118 -21
- package/dist/index.js +606 -437
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/dist/index.cjs
CHANGED
|
@@ -312,25 +312,17 @@ function plainJsonDecoder(options) {
|
|
|
312
312
|
if (record.headers?.[SPEC_VERSION_HEADER] !== void 0 || internal.mediaTypeOf(headers[CONTENT_TYPE]) === STRUCTURED_MEDIA_TYPE2) {
|
|
313
313
|
return decodeCloudEvent(record);
|
|
314
314
|
}
|
|
315
|
-
const plain =
|
|
316
|
-
|
|
317
|
-
partition: record.partition,
|
|
318
|
-
offset: record.offset,
|
|
319
|
-
key: readKeyLeniently(record.key),
|
|
320
|
-
headers,
|
|
321
|
-
payload: parsePayload(record.value),
|
|
322
|
-
timestamp: record.timestamp
|
|
323
|
-
};
|
|
324
|
-
const type = resolveText(options.type, { record: plain, name: "type" });
|
|
315
|
+
const plain = plainRecordOf(record, headers);
|
|
316
|
+
const type = KAFKA_EVENTS.text(options.type, { plain, name: "type" });
|
|
325
317
|
if (type === void 0) {
|
|
326
318
|
throw invalid("Record has no event type: the type option found none");
|
|
327
319
|
}
|
|
328
320
|
assertValidType(type, "The event type");
|
|
329
321
|
return {
|
|
330
|
-
id:
|
|
322
|
+
id: KAFKA_EVENTS.text(options.id, { plain, name: "id" }) ?? `kafka:${record.topic}:${record.partition}:${record.offset}`,
|
|
331
323
|
type,
|
|
332
|
-
source:
|
|
333
|
-
key: options.key === void 0 ? plain.key :
|
|
324
|
+
source: KAFKA_EVENTS.text(options.source, { plain, name: "source" }) ?? record.topic,
|
|
325
|
+
key: options.key === void 0 ? plain.key : KAFKA_EVENTS.text(options.key, { plain, name: "key" }) ?? null,
|
|
334
326
|
payload: plain.payload,
|
|
335
327
|
headers,
|
|
336
328
|
correlationId: null,
|
|
@@ -339,27 +331,38 @@ function plainJsonDecoder(options) {
|
|
|
339
331
|
};
|
|
340
332
|
};
|
|
341
333
|
}
|
|
342
|
-
function
|
|
343
|
-
if (typeof decoded !== "object" || decoded === null) {
|
|
344
|
-
throw invalid("The decoder must return an event object synchronously");
|
|
345
|
-
}
|
|
346
|
-
if ("then" in decoded) {
|
|
347
|
-
observeRejection(decoded);
|
|
348
|
-
throw invalid("The decoder must return an event object synchronously");
|
|
349
|
-
}
|
|
350
|
-
const event = decoded;
|
|
334
|
+
function plainRecordOf(record, headers) {
|
|
351
335
|
return {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
key:
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
causationId: event.causationId ?? null,
|
|
360
|
-
occurredAt: event.occurredAt ?? requireRecordTimestamp(record)
|
|
336
|
+
topic: record.topic,
|
|
337
|
+
partition: record.partition,
|
|
338
|
+
offset: record.offset,
|
|
339
|
+
key: readKeyLeniently(record.key),
|
|
340
|
+
headers,
|
|
341
|
+
payload: parsePayload(record.value),
|
|
342
|
+
timestamp: record.timestamp
|
|
361
343
|
};
|
|
362
344
|
}
|
|
345
|
+
var KafkaDecodedEvents = class extends internal.DecodedEventReader {
|
|
346
|
+
invalid(reason) {
|
|
347
|
+
return invalid(reason);
|
|
348
|
+
}
|
|
349
|
+
/** The topic. */
|
|
350
|
+
defaultSource(record) {
|
|
351
|
+
return record.topic;
|
|
352
|
+
}
|
|
353
|
+
/** The record key as UTF-8 text. */
|
|
354
|
+
defaultKey(record) {
|
|
355
|
+
return readRecordKey(record.key);
|
|
356
|
+
}
|
|
357
|
+
/** The record timestamp; a record without a usable one is malformed. */
|
|
358
|
+
defaultTime(record) {
|
|
359
|
+
return requireRecordTimestamp(record);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
var KAFKA_EVENTS = new KafkaDecodedEvents();
|
|
363
|
+
function completeDecodedEvent(decoded, record) {
|
|
364
|
+
return KAFKA_EVENTS.complete(decoded, record);
|
|
365
|
+
}
|
|
363
366
|
function extensionAttributes(body) {
|
|
364
367
|
return Object.fromEntries(
|
|
365
368
|
Object.entries(body).filter(
|
|
@@ -405,33 +408,17 @@ function readKeyLeniently(key) {
|
|
|
405
408
|
return Buffer.from(key).toString("base64");
|
|
406
409
|
}
|
|
407
410
|
}
|
|
408
|
-
function resolveText(option, { record, name }) {
|
|
409
|
-
const value = typeof option === "function" ? option(record) : option;
|
|
410
|
-
if (value === void 0 || value === null) {
|
|
411
|
-
return void 0;
|
|
412
|
-
}
|
|
413
|
-
if (typeof value !== "string") {
|
|
414
|
-
observeRejection(value);
|
|
415
|
-
throw invalid(`The ${name} option returned ${typeof value}, expected a string`);
|
|
416
|
-
}
|
|
417
|
-
return value.length === 0 ? void 0 : value;
|
|
418
|
-
}
|
|
419
411
|
function resolveOccurredAt(option, { plain, record }) {
|
|
420
412
|
const value = option?.(plain);
|
|
421
413
|
if (value === void 0) {
|
|
422
414
|
return requireRecordTimestamp(record);
|
|
423
415
|
}
|
|
424
416
|
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
425
|
-
observeRejection(value);
|
|
417
|
+
internal.observeRejection(value);
|
|
426
418
|
throw invalid("The occurredAt option returned no valid Date");
|
|
427
419
|
}
|
|
428
420
|
return value;
|
|
429
421
|
}
|
|
430
|
-
function observeRejection(value) {
|
|
431
|
-
if (typeof value === "object" && value !== null && "then" in value) {
|
|
432
|
-
Promise.resolve(value).catch(() => void 0);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
422
|
|
|
436
423
|
// src/codecs.ts
|
|
437
424
|
var cloudEventsBinary = Object.freeze({
|
|
@@ -450,36 +437,53 @@ function jsonEnvelope(mapping) {
|
|
|
450
437
|
transportIds: [RECORD_POSITION],
|
|
451
438
|
failures: { invalid, unencodable: (reason) => new UnencodableEventError(reason) }
|
|
452
439
|
});
|
|
453
|
-
const contentType = mapping.contentType ?? DEFAULT_CONTENT_TYPE;
|
|
440
|
+
const contentType = mapping.contentTypeHeader === false ? {} : { [CONTENT_TYPE]: mapping.contentType ?? DEFAULT_CONTENT_TYPE };
|
|
454
441
|
const stampsTime = mapping.occurredAt === void 0;
|
|
455
|
-
const ownHeaders = (envelope) => mapping.id === RECORD_POSITION ? {
|
|
442
|
+
const ownHeaders = (envelope) => mapping.id === RECORD_POSITION ? { ...contentType, [EVENT_ID_HEADER]: envelope.id } : contentType;
|
|
443
|
+
const recordKey = resolveRecordKey(mapping.recordKey);
|
|
456
444
|
return {
|
|
457
|
-
encode: (envelope) => {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
|
|
445
|
+
encode: (envelope) => encodeJson(envelope, { format, stampsTime, ownHeaders, recordKey }),
|
|
446
|
+
decode: (record) => decodeJson(record, format)
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function resolveRecordKey(recordKey) {
|
|
450
|
+
if (recordKey === void 0 || recordKey === "event-key") {
|
|
451
|
+
return (envelope) => envelope.key;
|
|
452
|
+
}
|
|
453
|
+
if (recordKey === "none") {
|
|
454
|
+
return () => null;
|
|
455
|
+
}
|
|
456
|
+
if (typeof recordKey === "function") {
|
|
457
|
+
return recordKey;
|
|
458
|
+
}
|
|
459
|
+
throw new core.ConfigurationError(
|
|
460
|
+
`jsonEnvelope recordKey must be 'event-key', 'none' or a function, received '${String(recordKey)}'`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
function encodeJson(envelope, { format, stampsTime, ownHeaders, recordKey }) {
|
|
464
|
+
assertRepresentable(envelope);
|
|
465
|
+
const { body, headers } = format.write(envelope);
|
|
466
|
+
const taken = [CONTENT_TYPE, EVENT_ID_HEADER].find((name) => Object.hasOwn(headers, name));
|
|
467
|
+
if (taken !== void 0) {
|
|
468
|
+
throw new UnencodableEventError(
|
|
469
|
+
`Event ${envelope.id} has a header '${taken}', which the codec writes itself`
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
key: recordKey(envelope),
|
|
474
|
+
value: serializeJson(envelope, body),
|
|
475
|
+
headers: { ...headers, ...ownHeaders(envelope) },
|
|
476
|
+
...stampsTime ? { timestamp: recordTimestamp(envelope) } : {}
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function decodeJson(record, format) {
|
|
480
|
+
const { [EVENT_ID_HEADER]: eventId, ...headers } = withoutContentType(
|
|
481
|
+
readHeadersLeniently(record.headers)
|
|
482
|
+
);
|
|
483
|
+
const fields = format.read({ body: parsePayload(record.value), headers });
|
|
484
|
+
return {
|
|
485
|
+
...fields,
|
|
486
|
+
id: fields.id ?? (eventId === void 0 || eventId === "" ? `kafka:${record.topic}:${record.partition}:${record.offset}` : eventId)
|
|
483
487
|
};
|
|
484
488
|
}
|
|
485
489
|
function withoutContentType(headers) {
|
|
@@ -497,6 +501,222 @@ function recordTimestamp(envelope) {
|
|
|
497
501
|
formatTime(envelope);
|
|
498
502
|
return String(envelope.occurredAt.getTime());
|
|
499
503
|
}
|
|
504
|
+
var GroupJoinWatch = class {
|
|
505
|
+
#settings;
|
|
506
|
+
#waiter;
|
|
507
|
+
constructor(settings) {
|
|
508
|
+
this.#settings = settings;
|
|
509
|
+
}
|
|
510
|
+
/** The consumer joined its group. */
|
|
511
|
+
joined() {
|
|
512
|
+
this.#waiter?.resolve();
|
|
513
|
+
}
|
|
514
|
+
/** The consumer crashed while it was joining. */
|
|
515
|
+
failed(error) {
|
|
516
|
+
this.#waiter?.reject(error);
|
|
517
|
+
}
|
|
518
|
+
/** The transport closed while the consumer was joining. */
|
|
519
|
+
closed() {
|
|
520
|
+
this.#waiter?.reject(new TransportClosedError(`Transport '${this.#settings.name}' is closed`));
|
|
521
|
+
}
|
|
522
|
+
/** Connects and subscribes the consumer, runs it and resolves once it joined its group. */
|
|
523
|
+
async start(consumer, {
|
|
524
|
+
topics,
|
|
525
|
+
fromBeginning,
|
|
526
|
+
eachMessage
|
|
527
|
+
}) {
|
|
528
|
+
await consumer.connect();
|
|
529
|
+
await consumer.subscribe(this.#settings.dialect.subscription(topics, fromBeginning));
|
|
530
|
+
const joined = this.#wait(consumer);
|
|
531
|
+
try {
|
|
532
|
+
await consumer.run({ eachMessage });
|
|
533
|
+
await joined.promise;
|
|
534
|
+
} finally {
|
|
535
|
+
joined.dispose();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
#wait(consumer) {
|
|
539
|
+
if (!this.#settings.dialect.tracksMembership(consumer)) {
|
|
540
|
+
return { promise: Promise.resolve(), dispose: () => void 0 };
|
|
541
|
+
}
|
|
542
|
+
const { name, groupId, timings } = this.#settings;
|
|
543
|
+
let cancel;
|
|
544
|
+
const promise = new Promise((resolve, reject) => {
|
|
545
|
+
this.#waiter = { resolve, reject };
|
|
546
|
+
cancel = internal.schedule(core.systemClock, {
|
|
547
|
+
run: () => {
|
|
548
|
+
reject(
|
|
549
|
+
new GroupJoinTimeoutError(
|
|
550
|
+
`Consumer of transport '${name}' did not join group '${groupId}' within ${timings.startupTimeoutMs}ms`
|
|
551
|
+
)
|
|
552
|
+
);
|
|
553
|
+
},
|
|
554
|
+
delayMs: timings.startupTimeoutMs
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
promise.catch(() => void 0);
|
|
558
|
+
return {
|
|
559
|
+
promise,
|
|
560
|
+
dispose: () => {
|
|
561
|
+
cancel?.();
|
|
562
|
+
this.#waiter = void 0;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
var KafkaProducerSession = class {
|
|
568
|
+
#settings;
|
|
569
|
+
/** Whether the transport is closed: then nothing connects any more. */
|
|
570
|
+
#isClosed;
|
|
571
|
+
#producer;
|
|
572
|
+
#connected = false;
|
|
573
|
+
constructor(settings, isClosed) {
|
|
574
|
+
this.#settings = settings;
|
|
575
|
+
this.#isClosed = isClosed;
|
|
576
|
+
}
|
|
577
|
+
/** True once the producer connected, until the transport closes. */
|
|
578
|
+
get isConnected() {
|
|
579
|
+
return this.#connected;
|
|
580
|
+
}
|
|
581
|
+
/** The producer opened so far, if any: what closing must disconnect. */
|
|
582
|
+
get pending() {
|
|
583
|
+
return this.#producer;
|
|
584
|
+
}
|
|
585
|
+
markClosed() {
|
|
586
|
+
this.#connected = false;
|
|
587
|
+
}
|
|
588
|
+
connected() {
|
|
589
|
+
if (this.#isClosed()) {
|
|
590
|
+
return Promise.reject(
|
|
591
|
+
new TransportClosedError(`Transport '${this.#settings.name}' is closed`)
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
this.#producer ??= this.#open();
|
|
595
|
+
return this.#producer;
|
|
596
|
+
}
|
|
597
|
+
/** Sends with the publish timeout; the late outcome of a timed-out send is ignored. */
|
|
598
|
+
async send(producer, record) {
|
|
599
|
+
const { name, timings } = this.#settings;
|
|
600
|
+
let cancel;
|
|
601
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
602
|
+
cancel = internal.schedule(core.systemClock, {
|
|
603
|
+
run: () => {
|
|
604
|
+
reject(
|
|
605
|
+
new PublishTimeoutError(
|
|
606
|
+
`Transport '${name}' got no broker ack for topic '${record.topic}' within ${timings.publishTimeoutMs}ms`
|
|
607
|
+
)
|
|
608
|
+
);
|
|
609
|
+
},
|
|
610
|
+
delayMs: timings.publishTimeoutMs
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
const sending = producer.send(this.#settings.dialect.record(record));
|
|
614
|
+
sending.catch(() => void 0);
|
|
615
|
+
try {
|
|
616
|
+
await Promise.race([sending, timeout]);
|
|
617
|
+
} finally {
|
|
618
|
+
cancel?.();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async #open() {
|
|
622
|
+
const { kafka, dialect, injectedProducer } = this.#settings;
|
|
623
|
+
if (injectedProducer !== void 0) {
|
|
624
|
+
this.#connected = true;
|
|
625
|
+
return withoutLifecycle(injectedProducer);
|
|
626
|
+
}
|
|
627
|
+
const producer = kafka.producer(dialect.producerConfig(this.#settings));
|
|
628
|
+
try {
|
|
629
|
+
await producer.connect();
|
|
630
|
+
} catch (error) {
|
|
631
|
+
this.#producer = void 0;
|
|
632
|
+
throw error;
|
|
633
|
+
}
|
|
634
|
+
this.#connected = !this.#isClosed();
|
|
635
|
+
return producer;
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
function withoutLifecycle(producer) {
|
|
639
|
+
return {
|
|
640
|
+
send: (record) => producer.send(record),
|
|
641
|
+
disconnect: () => Promise.resolve()
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
var KAFKAJS_DIALECT = {
|
|
645
|
+
producerConfig: ({ producerConfig, maxInFlightRequests }) => ({
|
|
646
|
+
...producerConfig,
|
|
647
|
+
idempotent: true,
|
|
648
|
+
maxInFlightRequests
|
|
649
|
+
}),
|
|
650
|
+
consumerConfig: ({ consumerConfig, groupId }) => ({ ...consumerConfig, groupId }),
|
|
651
|
+
subscription: (topics, fromBeginning) => ({ topics, fromBeginning }),
|
|
652
|
+
record: (record) => record,
|
|
653
|
+
tracksMembership: (consumer) => consumer.on !== void 0 && consumer.events?.GROUP_JOIN !== void 0,
|
|
654
|
+
observe: (consumer, membership) => {
|
|
655
|
+
if (!KAFKAJS_DIALECT.tracksMembership(consumer)) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const names = consumer.events;
|
|
659
|
+
const listen = (name, listener) => {
|
|
660
|
+
if (name !== void 0) {
|
|
661
|
+
consumer.on(name, listener);
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
listen(names.GROUP_JOIN, () => membership.joined());
|
|
665
|
+
listen(names.REBALANCING, () => membership.rebalancing());
|
|
666
|
+
listen(names.CRASH, (event) => membership.crashed(event));
|
|
667
|
+
listen(names.STOP, () => membership.left());
|
|
668
|
+
listen(names.DISCONNECT, () => membership.left());
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
var ASSIGN_PARTITIONS = -175;
|
|
672
|
+
var REVOKE_PARTITIONS = -174;
|
|
673
|
+
var CONFLUENT_DIALECT = {
|
|
674
|
+
producerConfig: ({ producerConfig, maxInFlightRequests }) => {
|
|
675
|
+
const user = producerConfig;
|
|
676
|
+
return {
|
|
677
|
+
...user,
|
|
678
|
+
kafkaJS: { ...user.kafkaJS, idempotent: true, maxInFlightRequests, acks: -1 },
|
|
679
|
+
"enable.idempotence": true,
|
|
680
|
+
"max.in.flight": maxInFlightRequests,
|
|
681
|
+
acks: -1
|
|
682
|
+
};
|
|
683
|
+
},
|
|
684
|
+
consumerConfig: ({ consumerConfig, groupId, fromBeginning }, membership) => {
|
|
685
|
+
const user = consumerConfig;
|
|
686
|
+
const userCallback = typeof user.rebalance_cb === "function" ? user.rebalance_cb : void 0;
|
|
687
|
+
return {
|
|
688
|
+
...user,
|
|
689
|
+
kafkaJS: { ...user.kafkaJS, groupId, fromBeginning },
|
|
690
|
+
"group.id": groupId,
|
|
691
|
+
"auto.offset.reset": fromBeginning ? "earliest" : "latest",
|
|
692
|
+
rebalance_cb: (...args) => {
|
|
693
|
+
if (args[0].code === ASSIGN_PARTITIONS) {
|
|
694
|
+
membership.joined();
|
|
695
|
+
} else if (args[0].code === REVOKE_PARTITIONS) {
|
|
696
|
+
membership.rebalancing();
|
|
697
|
+
}
|
|
698
|
+
return userCallback?.(...args);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
},
|
|
702
|
+
subscription: (topics) => ({ topics }),
|
|
703
|
+
record: ({ acks: _acks, ...record }) => record,
|
|
704
|
+
tracksMembership: () => true,
|
|
705
|
+
observe: () => void 0
|
|
706
|
+
};
|
|
707
|
+
function clientDialect(client) {
|
|
708
|
+
if (client === void 0 || client === "kafkajs") {
|
|
709
|
+
return KAFKAJS_DIALECT;
|
|
710
|
+
}
|
|
711
|
+
if (client === "confluent") {
|
|
712
|
+
return CONFLUENT_DIALECT;
|
|
713
|
+
}
|
|
714
|
+
throw new core.ConfigurationError(
|
|
715
|
+
`Unknown Kafka client '${String(client)}'; use 'kafkajs' or 'confluent'`
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/kafka-settings.ts
|
|
500
720
|
var DEFAULTS = {
|
|
501
721
|
name: "kafka",
|
|
502
722
|
maxInFlightRequests: 1,
|
|
@@ -509,44 +729,248 @@ var DEFAULTS = {
|
|
|
509
729
|
restartInitialBackoff: "1s",
|
|
510
730
|
restartMaxBackoff: "30s"
|
|
511
731
|
};
|
|
512
|
-
var
|
|
732
|
+
var MAXIMUM_IN_FLIGHT_REQUESTS = 5;
|
|
733
|
+
function resolveKafkaSettings(options) {
|
|
734
|
+
return {
|
|
735
|
+
name: options.name ?? DEFAULTS.name,
|
|
736
|
+
kafka: options.kafka,
|
|
737
|
+
injectedProducer: resolveInjectedProducer(options),
|
|
738
|
+
dialect: clientDialect(options.client),
|
|
739
|
+
groupId: options.groupId,
|
|
740
|
+
fromBeginning: options.fromBeginning ?? true,
|
|
741
|
+
maxInFlightRequests: internal.BoundedInteger.of(
|
|
742
|
+
options.maxInFlightRequests ?? DEFAULTS.maxInFlightRequests,
|
|
743
|
+
{ name: "maxInFlightRequests", minimum: 1, maximum: MAXIMUM_IN_FLIGHT_REQUESTS }
|
|
744
|
+
),
|
|
745
|
+
producerConfig: options.producerConfig ?? {},
|
|
746
|
+
consumerConfig: options.consumerConfig ?? {},
|
|
747
|
+
invalidPolicy: options.onInvalidMessage ?? "dead-letter",
|
|
748
|
+
codec: resolveCodec(options),
|
|
749
|
+
logger: options.logger ?? console,
|
|
750
|
+
timings: resolveTimings(options)
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function resolveInjectedProducer(options) {
|
|
754
|
+
const { producer, kafka, producerConfig, maxInFlightRequests } = options;
|
|
755
|
+
if (producer === void 0) {
|
|
756
|
+
if (kafka === void 0) {
|
|
757
|
+
throw new core.ConfigurationError(
|
|
758
|
+
"Give 'kafka', or an already-connected 'producer' to publish with"
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
return void 0;
|
|
762
|
+
}
|
|
763
|
+
if (producerConfig !== void 0 || maxInFlightRequests !== void 0) {
|
|
764
|
+
throw new core.ConfigurationError(
|
|
765
|
+
"Give 'producerConfig' and 'maxInFlightRequests' to a producer you inject yourself"
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
return producer;
|
|
769
|
+
}
|
|
770
|
+
function resolveCodec({ codec, decode }) {
|
|
771
|
+
if (decode === void 0) {
|
|
772
|
+
return codec ?? cloudEventsBinary;
|
|
773
|
+
}
|
|
774
|
+
if (codec !== void 0) {
|
|
775
|
+
throw new core.ConfigurationError("Give 'codec' or the deprecated 'decode', not both");
|
|
776
|
+
}
|
|
777
|
+
return { encode: encodeEnvelope, decode };
|
|
778
|
+
}
|
|
779
|
+
function resolveTimings(options) {
|
|
780
|
+
const processing = options.processing ?? {};
|
|
781
|
+
const maxAttempts = internal.PositiveInteger.of(
|
|
782
|
+
processing.maxAttempts ?? DEFAULTS.maxAttempts,
|
|
783
|
+
"processing.maxAttempts"
|
|
784
|
+
);
|
|
785
|
+
return {
|
|
786
|
+
maxAttempts,
|
|
787
|
+
processingBackoff: new internal.ExponentialBackoff({
|
|
788
|
+
initialMs: internal.Milliseconds.of(
|
|
789
|
+
processing.initialBackoff ?? DEFAULTS.initialBackoff,
|
|
790
|
+
"processing.initialBackoff"
|
|
791
|
+
),
|
|
792
|
+
maxMs: internal.Milliseconds.of(processing.maxBackoff ?? DEFAULTS.maxBackoff, "processing.maxBackoff")
|
|
793
|
+
}),
|
|
794
|
+
startupTimeoutMs: internal.Milliseconds.of(
|
|
795
|
+
options.startupTimeout ?? DEFAULTS.startupTimeout,
|
|
796
|
+
"startupTimeout"
|
|
797
|
+
),
|
|
798
|
+
publishTimeoutMs: internal.Milliseconds.of(
|
|
799
|
+
options.publishTimeout ?? DEFAULTS.publishTimeout,
|
|
800
|
+
"publishTimeout"
|
|
801
|
+
),
|
|
802
|
+
heartbeatIntervalMs: Math.max(
|
|
803
|
+
1,
|
|
804
|
+
internal.Milliseconds.of(
|
|
805
|
+
processing.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
|
|
806
|
+
"processing.heartbeatInterval"
|
|
807
|
+
)
|
|
808
|
+
),
|
|
809
|
+
restartBackoff: resolveRestartBackoff(options.restart ?? {})
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
function resolveRestartBackoff(restart) {
|
|
813
|
+
return new internal.ExponentialBackoff({
|
|
814
|
+
initialMs: internal.Milliseconds.of(
|
|
815
|
+
restart.initialBackoff ?? DEFAULTS.restartInitialBackoff,
|
|
816
|
+
"restart.initialBackoff"
|
|
817
|
+
),
|
|
818
|
+
maxMs: internal.Milliseconds.of(restart.maxBackoff ?? DEFAULTS.restartMaxBackoff, "restart.maxBackoff")
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
function deadLetterTopicFor(policy, sourceTopic) {
|
|
822
|
+
if (policy === "dead-letter") {
|
|
823
|
+
return `${sourceTopic}.dead`;
|
|
824
|
+
}
|
|
825
|
+
if (typeof policy === "object") {
|
|
826
|
+
const { deadLetterTopic } = policy;
|
|
827
|
+
return typeof deadLetterTopic === "function" ? deadLetterTopic(sourceTopic) : deadLetterTopic;
|
|
828
|
+
}
|
|
829
|
+
return void 0;
|
|
830
|
+
}
|
|
831
|
+
function toHandlerFailure(error) {
|
|
832
|
+
const { name, message } = core.toErrorInfo(error);
|
|
833
|
+
return new HandlerFailedError(`${name}: ${message}`);
|
|
834
|
+
}
|
|
835
|
+
function assertCanSubscribe(state, {
|
|
836
|
+
settings,
|
|
837
|
+
destinations
|
|
838
|
+
}) {
|
|
839
|
+
const { name, groupId, invalidPolicy, kafka } = settings;
|
|
840
|
+
if (state === "closed") {
|
|
841
|
+
throw new TransportClosedError(`Transport '${name}' is closed`);
|
|
842
|
+
}
|
|
843
|
+
if (state === "subscribed") {
|
|
844
|
+
throw new core.ConfigurationError(
|
|
845
|
+
`Transport '${name}' already subscribed; list every topic in one subscription or use one KafkaTransport per consumer group`
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
if (kafka === void 0) {
|
|
849
|
+
throw new core.ConfigurationError(`Transport '${name}' needs 'kafka' to create its consumer`);
|
|
850
|
+
}
|
|
851
|
+
if (groupId === void 0 || groupId.length === 0) {
|
|
852
|
+
throw new core.ConfigurationError(`Transport '${name}' needs a groupId to subscribe`);
|
|
853
|
+
}
|
|
854
|
+
if (destinations.length === 0) {
|
|
855
|
+
throw new core.ConfigurationError(`Transport '${name}' subscribed to no topics`);
|
|
856
|
+
}
|
|
857
|
+
for (const topic of destinations) {
|
|
858
|
+
const deadLetterTopic = deadLetterTopicFor(invalidPolicy, topic);
|
|
859
|
+
if (deadLetterTopic !== void 0 && destinations.includes(deadLetterTopic)) {
|
|
860
|
+
throw new core.ConfigurationError(
|
|
861
|
+
`Dead letter topic '${deadLetterTopic}' is also consumed, which would loop`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/kafka-records.ts
|
|
868
|
+
function decodeRecord(codec, { topic, partition, message }) {
|
|
869
|
+
return decodeKafkaRecord(codec, {
|
|
870
|
+
topic,
|
|
871
|
+
partition,
|
|
872
|
+
offset: message.offset,
|
|
873
|
+
key: message.key,
|
|
874
|
+
value: message.value,
|
|
875
|
+
headers: message.headers,
|
|
876
|
+
timestamp: message.timestamp
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
function decodeKafkaRecord(codec, record) {
|
|
880
|
+
if (codec === cloudEventsBinary) {
|
|
881
|
+
return decodeEnvelope(record);
|
|
882
|
+
}
|
|
883
|
+
return completeDecodedEvent(codec.decode(record), record);
|
|
884
|
+
}
|
|
885
|
+
function deadLetterRecord({ topic, partition, message }, {
|
|
886
|
+
reason,
|
|
887
|
+
name,
|
|
888
|
+
code,
|
|
889
|
+
deadLetterTopic
|
|
890
|
+
}) {
|
|
891
|
+
const headers = {
|
|
892
|
+
...toOutgoingHeaders(message.headers),
|
|
893
|
+
"relaybus-invalid-reason": reason,
|
|
894
|
+
"relaybus-source-topic": topic,
|
|
895
|
+
"relaybus-source-partition": String(partition),
|
|
896
|
+
"relaybus-source-offset": message.offset,
|
|
897
|
+
"relaybus-error-name": name
|
|
898
|
+
};
|
|
899
|
+
if (code === void 0) {
|
|
900
|
+
delete headers["relaybus-error-code"];
|
|
901
|
+
} else {
|
|
902
|
+
headers["relaybus-error-code"] = code;
|
|
903
|
+
}
|
|
904
|
+
return {
|
|
905
|
+
topic: deadLetterTopic,
|
|
906
|
+
acks: -1,
|
|
907
|
+
messages: [
|
|
908
|
+
{
|
|
909
|
+
key: message.key === null ? null : Buffer.from(message.key),
|
|
910
|
+
value: message.value === null ? null : Buffer.from(message.value),
|
|
911
|
+
headers
|
|
912
|
+
}
|
|
913
|
+
]
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
var WakeableSleeps = class {
|
|
917
|
+
#sleepers = /* @__PURE__ */ new Set();
|
|
918
|
+
sleep(milliseconds) {
|
|
919
|
+
return new Promise((resolve) => {
|
|
920
|
+
const wake = () => {
|
|
921
|
+
cancel();
|
|
922
|
+
this.#sleepers.delete(wake);
|
|
923
|
+
resolve();
|
|
924
|
+
};
|
|
925
|
+
const cancel = internal.schedule(core.systemClock, { run: wake, delayMs: milliseconds });
|
|
926
|
+
this.#sleepers.add(wake);
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
wakeAll() {
|
|
930
|
+
for (const wake of this.#sleepers) {
|
|
931
|
+
wake();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Sleeps `milliseconds` in slices of at most `sliceMs`, running `between` after each, until done
|
|
936
|
+
* or `stopped`: a long backoff then never outlives the session timeout.
|
|
937
|
+
*/
|
|
938
|
+
async sleepInSlices(milliseconds, {
|
|
939
|
+
sliceMs,
|
|
940
|
+
between,
|
|
941
|
+
stopped
|
|
942
|
+
}) {
|
|
943
|
+
let remaining = milliseconds;
|
|
944
|
+
do {
|
|
945
|
+
const slice = Math.min(remaining, sliceMs);
|
|
946
|
+
await this.sleep(slice);
|
|
947
|
+
await between();
|
|
948
|
+
remaining -= slice;
|
|
949
|
+
} while (remaining > 0 && !stopped());
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
// src/kafka-transport.ts
|
|
954
|
+
var KafkaTransport = class extends internal.SubscribingTransport {
|
|
513
955
|
name;
|
|
514
956
|
system = "kafka";
|
|
515
|
-
#kafka;
|
|
516
|
-
#groupId;
|
|
517
|
-
#fromBeginning;
|
|
518
|
-
#maxInFlightRequests;
|
|
519
|
-
#producerConfig;
|
|
520
|
-
#consumerConfig;
|
|
521
|
-
#invalidPolicy;
|
|
522
|
-
#codec;
|
|
523
|
-
#logger;
|
|
524
957
|
#settings;
|
|
525
|
-
#sleepers = /* @__PURE__ */ new Set();
|
|
526
|
-
#state = "idle";
|
|
527
958
|
#producer;
|
|
959
|
+
#groupJoin;
|
|
960
|
+
#sleeps = new WakeableSleeps();
|
|
961
|
+
#state = "idle";
|
|
528
962
|
#consumer;
|
|
529
963
|
#groupJoined = false;
|
|
530
964
|
#consuming = false;
|
|
531
|
-
#producerConnected = false;
|
|
532
965
|
#restartLoop;
|
|
533
966
|
#stalledPartitions = /* @__PURE__ */ new Set();
|
|
534
967
|
#startConsuming;
|
|
535
|
-
#joinWaiter;
|
|
536
968
|
constructor(options) {
|
|
537
|
-
|
|
538
|
-
this.#
|
|
539
|
-
this
|
|
540
|
-
this.#
|
|
541
|
-
this.#
|
|
542
|
-
options.maxInFlightRequests ?? DEFAULTS.maxInFlightRequests
|
|
543
|
-
);
|
|
544
|
-
this.#producerConfig = options.producerConfig ?? {};
|
|
545
|
-
this.#consumerConfig = options.consumerConfig ?? {};
|
|
546
|
-
this.#invalidPolicy = options.onInvalidMessage ?? "dead-letter";
|
|
547
|
-
this.#codec = resolveCodec(options);
|
|
548
|
-
this.#logger = options.logger ?? console;
|
|
549
|
-
this.#settings = resolveSettings(options);
|
|
969
|
+
super();
|
|
970
|
+
this.#settings = resolveKafkaSettings(options);
|
|
971
|
+
this.name = this.#settings.name;
|
|
972
|
+
this.#producer = new KafkaProducerSession(this.#settings, () => this.#state === "closed");
|
|
973
|
+
this.#groupJoin = new GroupJoinWatch(this.#settings);
|
|
550
974
|
}
|
|
551
975
|
/**
|
|
552
976
|
* True while the consumer is joined to its group, not between a crash or
|
|
@@ -563,21 +987,21 @@ var KafkaTransport = class {
|
|
|
563
987
|
* dead consumer is being restarted.
|
|
564
988
|
*/
|
|
565
989
|
isReady() {
|
|
566
|
-
if (this.#state === "closed" || !this.#
|
|
990
|
+
if (this.#state === "closed" || !this.#producer.isConnected) {
|
|
567
991
|
return false;
|
|
568
992
|
}
|
|
569
993
|
return this.#state === "idle" || this.isConsuming();
|
|
570
994
|
}
|
|
571
995
|
/** Connects the producer. Called by the bus on start; `publish` also connects on demand. */
|
|
572
996
|
async connect() {
|
|
573
|
-
await this.#
|
|
997
|
+
await this.#producer.connected();
|
|
574
998
|
}
|
|
575
999
|
async publish(envelope, destination) {
|
|
576
|
-
const producer = await this.#
|
|
577
|
-
await this.#send(producer, {
|
|
1000
|
+
const producer = await this.#producer.connected();
|
|
1001
|
+
await this.#producer.send(producer, {
|
|
578
1002
|
topic: destination,
|
|
579
1003
|
acks: -1,
|
|
580
|
-
messages: [this.#codec.encode(envelope)]
|
|
1004
|
+
messages: [this.#settings.codec.encode(envelope)]
|
|
581
1005
|
});
|
|
582
1006
|
}
|
|
583
1007
|
/**
|
|
@@ -587,14 +1011,23 @@ var KafkaTransport = class {
|
|
|
587
1011
|
* @throws ConfigurationError when `groupId` is missing, `destinations` is empty
|
|
588
1012
|
* or the transport already subscribed (create one transport per subscription).
|
|
589
1013
|
*/
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
1014
|
+
openSubscription(subscription) {
|
|
1015
|
+
return this.#subscribe(subscription);
|
|
1016
|
+
}
|
|
1017
|
+
async #subscribe({ destinations, onMessage, hooks }) {
|
|
1018
|
+
assertCanSubscribe(this.#state, { settings: this.#settings, destinations });
|
|
593
1019
|
this.#state = "subscribed";
|
|
594
|
-
const
|
|
1020
|
+
const { kafka, dialect, groupId } = this.#settings;
|
|
1021
|
+
const membership = this.#observeConsumer();
|
|
1022
|
+
const consumer = kafka.consumer(dialect.consumerConfig(this.#settings, membership));
|
|
595
1023
|
this.#consumer = consumer;
|
|
596
|
-
this.#
|
|
597
|
-
|
|
1024
|
+
this.#groupJoined = !dialect.tracksMembership(consumer);
|
|
1025
|
+
dialect.observe(consumer, membership);
|
|
1026
|
+
const start = () => this.#groupJoin.start(consumer, {
|
|
1027
|
+
topics: [...destinations],
|
|
1028
|
+
fromBeginning: this.#settings.fromBeginning,
|
|
1029
|
+
eachMessage: (payload) => this.#handleMessage(payload, { onMessage, hooks })
|
|
1030
|
+
});
|
|
598
1031
|
this.#startConsuming = start;
|
|
599
1032
|
try {
|
|
600
1033
|
await start();
|
|
@@ -606,40 +1039,21 @@ var KafkaTransport = class {
|
|
|
606
1039
|
throw error;
|
|
607
1040
|
}
|
|
608
1041
|
this.#consuming = true;
|
|
609
|
-
this.#logger.log(
|
|
610
|
-
`[KafkaTransport](subscribe) Consuming | transport=${this.name} group=${
|
|
1042
|
+
this.#settings.logger.log(
|
|
1043
|
+
`[KafkaTransport](subscribe) Consuming | transport=${this.name} group=${groupId} topics=${destinations.join(",")}`
|
|
611
1044
|
);
|
|
612
1045
|
}
|
|
613
|
-
async #startConsumer(consumer, {
|
|
614
|
-
destinations,
|
|
615
|
-
onMessage,
|
|
616
|
-
hooks
|
|
617
|
-
}) {
|
|
618
|
-
await consumer.connect();
|
|
619
|
-
await consumer.subscribe({ topics: destinations, fromBeginning: this.#fromBeginning });
|
|
620
|
-
const joined = this.#waitForGroupJoin(consumer);
|
|
621
|
-
try {
|
|
622
|
-
await consumer.run({
|
|
623
|
-
eachMessage: (payload) => this.#handleMessage(payload, { onMessage, hooks })
|
|
624
|
-
});
|
|
625
|
-
await joined.promise;
|
|
626
|
-
} finally {
|
|
627
|
-
joined.dispose();
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
1046
|
/** Stops consuming and disconnects the consumer and the producer. Safe to call twice. */
|
|
631
1047
|
async close() {
|
|
632
1048
|
if (this.#state === "closed") {
|
|
633
1049
|
return;
|
|
634
1050
|
}
|
|
635
1051
|
this.#state = "closed";
|
|
636
|
-
const pendingProducer = this.#producer;
|
|
1052
|
+
const pendingProducer = this.#producer.pending;
|
|
637
1053
|
this.#groupJoined = false;
|
|
638
|
-
this.#
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
this.#joinWaiter?.reject(new TransportClosedError(`Transport '${this.name}' is closed`));
|
|
1054
|
+
this.#producer.markClosed();
|
|
1055
|
+
this.#sleeps.wakeAll();
|
|
1056
|
+
this.#groupJoin.closed();
|
|
643
1057
|
await this.#restartLoop;
|
|
644
1058
|
const producer = await pendingProducer?.catch(() => void 0);
|
|
645
1059
|
const results = await Promise.allSettled([
|
|
@@ -651,99 +1065,15 @@ var KafkaTransport = class {
|
|
|
651
1065
|
throw failure.reason;
|
|
652
1066
|
}
|
|
653
1067
|
}
|
|
654
|
-
#
|
|
655
|
-
if (this.#state === "closed") {
|
|
656
|
-
throw new TransportClosedError(`Transport '${this.name}' is closed`);
|
|
657
|
-
}
|
|
658
|
-
if (this.#state === "subscribed") {
|
|
659
|
-
throw new core.ConfigurationError(
|
|
660
|
-
`Transport '${this.name}' already subscribed; list every topic in one subscription or use one KafkaTransport per consumer group`
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
if (this.#groupId === void 0 || this.#groupId.length === 0) {
|
|
664
|
-
throw new core.ConfigurationError(`Transport '${this.name}' needs a groupId to subscribe`);
|
|
665
|
-
}
|
|
666
|
-
if (destinations.length === 0) {
|
|
667
|
-
throw new core.ConfigurationError(`Transport '${this.name}' subscribed to no topics`);
|
|
668
|
-
}
|
|
669
|
-
for (const topic of destinations) {
|
|
670
|
-
const deadLetterTopic = this.#deadLetterTopicFor(topic);
|
|
671
|
-
if (deadLetterTopic !== void 0 && destinations.includes(deadLetterTopic)) {
|
|
672
|
-
throw new core.ConfigurationError(
|
|
673
|
-
`Dead letter topic '${deadLetterTopic}' is also consumed, which would loop`
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
/** The dead-letter topic for records of `sourceTopic`, or undefined when the policy does not use one. */
|
|
679
|
-
#deadLetterTopicFor(sourceTopic) {
|
|
680
|
-
const policy = this.#invalidPolicy;
|
|
681
|
-
if (policy === "dead-letter") {
|
|
682
|
-
return `${sourceTopic}.dead`;
|
|
683
|
-
}
|
|
684
|
-
if (typeof policy === "object") {
|
|
685
|
-
const { deadLetterTopic } = policy;
|
|
686
|
-
return typeof deadLetterTopic === "function" ? deadLetterTopic(sourceTopic) : deadLetterTopic;
|
|
687
|
-
}
|
|
688
|
-
return void 0;
|
|
689
|
-
}
|
|
690
|
-
/** Sends with the publish timeout; the late outcome of a timed-out send is ignored. */
|
|
691
|
-
async #send(producer, record) {
|
|
692
|
-
let cancel;
|
|
693
|
-
const timeout = new Promise((_resolve, reject) => {
|
|
694
|
-
cancel = internal.schedule(core.systemClock, {
|
|
695
|
-
run: () => {
|
|
696
|
-
reject(
|
|
697
|
-
new PublishTimeoutError(
|
|
698
|
-
`Transport '${this.name}' got no broker ack for topic '${record.topic}' within ${this.#settings.publishTimeoutMs}ms`
|
|
699
|
-
)
|
|
700
|
-
);
|
|
701
|
-
},
|
|
702
|
-
delayMs: this.#settings.publishTimeoutMs
|
|
703
|
-
});
|
|
704
|
-
});
|
|
705
|
-
const sending = producer.send(record);
|
|
706
|
-
sending.catch(() => void 0);
|
|
707
|
-
try {
|
|
708
|
-
await Promise.race([sending, timeout]);
|
|
709
|
-
} finally {
|
|
710
|
-
cancel?.();
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
#connectedProducer() {
|
|
714
|
-
if (this.#state === "closed") {
|
|
715
|
-
return Promise.reject(new TransportClosedError(`Transport '${this.name}' is closed`));
|
|
716
|
-
}
|
|
717
|
-
this.#producer ??= this.#openProducer();
|
|
718
|
-
return this.#producer;
|
|
719
|
-
}
|
|
720
|
-
async #openProducer() {
|
|
721
|
-
const producer = this.#kafka.producer({
|
|
722
|
-
...this.#producerConfig,
|
|
723
|
-
idempotent: true,
|
|
724
|
-
maxInFlightRequests: this.#maxInFlightRequests
|
|
725
|
-
});
|
|
726
|
-
try {
|
|
727
|
-
await producer.connect();
|
|
728
|
-
} catch (error) {
|
|
729
|
-
this.#producer = void 0;
|
|
730
|
-
throw error;
|
|
731
|
-
}
|
|
732
|
-
this.#producerConnected = this.#state !== "closed";
|
|
733
|
-
return producer;
|
|
734
|
-
}
|
|
735
|
-
async #handleMessage(payload, {
|
|
736
|
-
onMessage,
|
|
737
|
-
hooks
|
|
738
|
-
}) {
|
|
1068
|
+
async #handleMessage(payload, route) {
|
|
739
1069
|
let envelope;
|
|
740
1070
|
try {
|
|
741
|
-
envelope = this.#
|
|
1071
|
+
envelope = decodeRecord(this.#settings.codec, payload);
|
|
742
1072
|
} catch (error) {
|
|
743
|
-
if (this.#codec === cloudEventsBinary && !(error instanceof InvalidKafkaMessageError)) {
|
|
1073
|
+
if (this.#settings.codec === cloudEventsBinary && !(error instanceof InvalidKafkaMessageError)) {
|
|
744
1074
|
throw toHandlerFailure(error);
|
|
745
1075
|
}
|
|
746
|
-
hooks?.onInvalidMessage({ destination: payload.topic, error });
|
|
1076
|
+
route.hooks?.onInvalidMessage({ destination: payload.topic, error });
|
|
747
1077
|
await this.#withRetries(payload, {
|
|
748
1078
|
method: "handleInvalid",
|
|
749
1079
|
operation: () => this.#handleInvalid(payload, error)
|
|
@@ -754,7 +1084,7 @@ var KafkaTransport = class {
|
|
|
754
1084
|
method: "deliver",
|
|
755
1085
|
operation: async () => {
|
|
756
1086
|
try {
|
|
757
|
-
await onMessage(envelope, { destination: payload.topic });
|
|
1087
|
+
await route.onMessage(envelope, { destination: payload.topic });
|
|
758
1088
|
} catch (error) {
|
|
759
1089
|
if (!(error instanceof core.NonRetryableError)) {
|
|
760
1090
|
throw error;
|
|
@@ -764,30 +1094,15 @@ var KafkaTransport = class {
|
|
|
764
1094
|
}
|
|
765
1095
|
});
|
|
766
1096
|
}
|
|
767
|
-
#decodeRecord({ topic, partition, message }) {
|
|
768
|
-
if (this.#codec === cloudEventsBinary) {
|
|
769
|
-
return decodeEnvelope(message);
|
|
770
|
-
}
|
|
771
|
-
const record = {
|
|
772
|
-
topic,
|
|
773
|
-
partition,
|
|
774
|
-
offset: message.offset,
|
|
775
|
-
key: message.key,
|
|
776
|
-
value: message.value,
|
|
777
|
-
headers: message.headers,
|
|
778
|
-
timestamp: message.timestamp
|
|
779
|
-
};
|
|
780
|
-
return completeDecodedEvent(this.#codec.decode(record), record);
|
|
781
|
-
}
|
|
782
1097
|
async #handleInvalid(payload, error) {
|
|
783
1098
|
const { topic, partition, message } = payload;
|
|
784
1099
|
const { message: reason, name, code } = core.toErrorInfo(error);
|
|
785
1100
|
const location = `topic=${topic} partition=${partition} offset=${message.offset}`;
|
|
786
1101
|
const errorFields = `errorName=${name}${code === void 0 ? "" : ` code=${code}`}`;
|
|
787
|
-
const policy = this.#invalidPolicy;
|
|
788
|
-
const deadLetterTopic =
|
|
1102
|
+
const policy = this.#settings.invalidPolicy;
|
|
1103
|
+
const deadLetterTopic = deadLetterTopicFor(policy, topic);
|
|
789
1104
|
if (policy === "skip") {
|
|
790
|
-
this.#logger.error(
|
|
1105
|
+
this.#settings.logger.error(
|
|
791
1106
|
`[KafkaTransport](handleInvalid) Skipping malformed message | ${location} reason=${reason} ${errorFields}`
|
|
792
1107
|
);
|
|
793
1108
|
} else if (typeof policy === "function") {
|
|
@@ -800,32 +1115,15 @@ var KafkaTransport = class {
|
|
|
800
1115
|
value: message.value
|
|
801
1116
|
});
|
|
802
1117
|
} else {
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
"relaybus-source-offset": message.offset,
|
|
809
|
-
"relaybus-error-name": name
|
|
810
|
-
};
|
|
811
|
-
if (code === void 0) {
|
|
812
|
-
delete headers["relaybus-error-code"];
|
|
813
|
-
} else {
|
|
814
|
-
headers["relaybus-error-code"] = code;
|
|
815
|
-
}
|
|
816
|
-
const producer = await this.#connectedProducer();
|
|
817
|
-
await this.#send(producer, {
|
|
818
|
-
topic: deadLetterTopic,
|
|
819
|
-
acks: -1,
|
|
820
|
-
messages: [
|
|
821
|
-
{
|
|
822
|
-
key: message.key === null ? null : Buffer.from(message.key),
|
|
823
|
-
value: message.value === null ? null : Buffer.from(message.value),
|
|
824
|
-
headers
|
|
825
|
-
}
|
|
826
|
-
]
|
|
1118
|
+
const record = deadLetterRecord(payload, {
|
|
1119
|
+
reason,
|
|
1120
|
+
name,
|
|
1121
|
+
code,
|
|
1122
|
+
deadLetterTopic
|
|
827
1123
|
});
|
|
828
|
-
this.#
|
|
1124
|
+
const producer = await this.#producer.connected();
|
|
1125
|
+
await this.#producer.send(producer, record);
|
|
1126
|
+
this.#settings.logger.error(
|
|
829
1127
|
`[KafkaTransport](handleInvalid) Dead-lettered malformed message | ${location} deadLetterTopic=${deadLetterTopic} reason=${reason} ${errorFields}`
|
|
830
1128
|
);
|
|
831
1129
|
}
|
|
@@ -836,7 +1134,8 @@ var KafkaTransport = class {
|
|
|
836
1134
|
* redelivers the record; heartbeats between attempts keep the session alive.
|
|
837
1135
|
*/
|
|
838
1136
|
async #withRetries(payload, { method, operation }) {
|
|
839
|
-
const { maxAttempts,
|
|
1137
|
+
const { maxAttempts, processingBackoff } = this.#settings.timings;
|
|
1138
|
+
const { logger } = this.#settings;
|
|
840
1139
|
const location = `topic=${payload.topic} partition=${payload.partition} offset=${payload.message.offset}`;
|
|
841
1140
|
const partitionKey = `${payload.topic}:${payload.partition}`;
|
|
842
1141
|
for (let attempt = 1; ; attempt += 1) {
|
|
@@ -848,82 +1147,49 @@ var KafkaTransport = class {
|
|
|
848
1147
|
const reason = core.toErrorInfo(error).message;
|
|
849
1148
|
if (attempt >= maxAttempts || this.#state === "closed") {
|
|
850
1149
|
this.#stalledPartitions.add(partitionKey);
|
|
851
|
-
|
|
1150
|
+
logger.error(
|
|
852
1151
|
`[KafkaTransport](withRetries) Giving up for now, the record stays uncommitted and will be redelivered | operation=${method} ${location} attempts=${attempt} error=${reason}`
|
|
853
1152
|
);
|
|
854
1153
|
throw toHandlerFailure(error);
|
|
855
1154
|
}
|
|
856
|
-
|
|
1155
|
+
logger.warn(
|
|
857
1156
|
`[KafkaTransport](withRetries) Attempt failed, retrying | operation=${method} ${location} attempt=${attempt} error=${reason}`
|
|
858
1157
|
);
|
|
859
|
-
await this.#
|
|
860
|
-
|
|
861
|
-
payload
|
|
862
|
-
|
|
1158
|
+
await this.#sleeps.sleepInSlices(processingBackoff.delayFor(attempt), {
|
|
1159
|
+
sliceMs: this.#settings.timings.heartbeatIntervalMs,
|
|
1160
|
+
between: () => payload.heartbeat(),
|
|
1161
|
+
stopped: () => this.#state === "closed"
|
|
1162
|
+
});
|
|
863
1163
|
}
|
|
864
1164
|
}
|
|
865
1165
|
}
|
|
866
|
-
/**
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
const cancel = internal.schedule(core.systemClock, { run: wake, delayMs: milliseconds });
|
|
884
|
-
this.#sleepers.add(wake);
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
/** Whether the client exposes the instrumentation needed to follow the group membership. */
|
|
888
|
-
#tracksMembership(consumer) {
|
|
889
|
-
return consumer.on !== void 0 && consumer.events?.GROUP_JOIN !== void 0;
|
|
890
|
-
}
|
|
891
|
-
#observeConsumer(consumer) {
|
|
892
|
-
if (!this.#tracksMembership(consumer)) {
|
|
893
|
-
this.#groupJoined = true;
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
const names = consumer.events;
|
|
897
|
-
const listen = (name, listener) => {
|
|
898
|
-
if (name !== void 0) {
|
|
899
|
-
consumer.on(name, listener);
|
|
1166
|
+
/** What the transport does when the consumer joins, rebalances, crashes or leaves its group. */
|
|
1167
|
+
#observeConsumer() {
|
|
1168
|
+
const { logger, groupId } = this.#settings;
|
|
1169
|
+
return {
|
|
1170
|
+
joined: () => {
|
|
1171
|
+
this.#groupJoined = true;
|
|
1172
|
+
this.#groupJoin.joined();
|
|
1173
|
+
logger.log(`[KafkaTransport](observeConsumer) Joined group | group=${groupId}`);
|
|
1174
|
+
},
|
|
1175
|
+
rebalancing: () => {
|
|
1176
|
+
this.#groupJoined = false;
|
|
1177
|
+
this.#stalledPartitions.clear();
|
|
1178
|
+
logger.warn(`[KafkaTransport](observeConsumer) Rebalancing | group=${groupId}`);
|
|
1179
|
+
},
|
|
1180
|
+
crashed: (event) => this.#handleCrash(event),
|
|
1181
|
+
left: () => {
|
|
1182
|
+
this.#groupJoined = false;
|
|
900
1183
|
}
|
|
901
1184
|
};
|
|
902
|
-
listen(names.GROUP_JOIN, () => {
|
|
903
|
-
this.#groupJoined = true;
|
|
904
|
-
this.#joinWaiter?.resolve();
|
|
905
|
-
this.#logger.log(`[KafkaTransport](observeConsumer) Joined group | group=${this.#groupId}`);
|
|
906
|
-
});
|
|
907
|
-
listen(names.REBALANCING, () => {
|
|
908
|
-
this.#groupJoined = false;
|
|
909
|
-
this.#stalledPartitions.clear();
|
|
910
|
-
this.#logger.warn(`[KafkaTransport](observeConsumer) Rebalancing | group=${this.#groupId}`);
|
|
911
|
-
});
|
|
912
|
-
listen(names.CRASH, (event) => this.#handleCrash(event));
|
|
913
|
-
listen(names.STOP, () => {
|
|
914
|
-
this.#groupJoined = false;
|
|
915
|
-
});
|
|
916
|
-
listen(names.DISCONNECT, () => {
|
|
917
|
-
this.#groupJoined = false;
|
|
918
|
-
});
|
|
919
1185
|
}
|
|
920
1186
|
#handleCrash(event) {
|
|
921
1187
|
const { error, restart } = event.payload ?? {};
|
|
922
1188
|
this.#groupJoined = false;
|
|
923
|
-
this.#logger.error(
|
|
924
|
-
`[KafkaTransport](handleCrash) Consumer crashed | group=${this.#groupId} restart=${String(restart)} error=${core.toErrorInfo(error).message}`
|
|
1189
|
+
this.#settings.logger.error(
|
|
1190
|
+
`[KafkaTransport](handleCrash) Consumer crashed | group=${this.#settings.groupId} restart=${String(restart)} error=${core.toErrorInfo(error).message}`
|
|
925
1191
|
);
|
|
926
|
-
this.#
|
|
1192
|
+
this.#groupJoin.failed(error);
|
|
927
1193
|
if (restart === false && this.#consuming && this.#restartLoop === void 0) {
|
|
928
1194
|
this.#restartLoop = this.#restartConsumer().finally(() => {
|
|
929
1195
|
this.#restartLoop = void 0;
|
|
@@ -935,125 +1201,28 @@ var KafkaTransport = class {
|
|
|
935
1201
|
* again, with bounded backoff, until it joined or the transport closed.
|
|
936
1202
|
*/
|
|
937
1203
|
async #restartConsumer() {
|
|
938
|
-
const {
|
|
1204
|
+
const { logger, groupId, timings } = this.#settings;
|
|
939
1205
|
const consumer = this.#consumer;
|
|
940
1206
|
for (let attempt = 1; this.#state === "subscribed"; attempt += 1) {
|
|
941
|
-
await this.#sleep(
|
|
942
|
-
Math.min(restartInitialBackoffMs * 2 ** (attempt - 1), restartMaxBackoffMs)
|
|
943
|
-
);
|
|
1207
|
+
await this.#sleeps.sleep(timings.restartBackoff.delayFor(attempt));
|
|
944
1208
|
if (this.#state !== "subscribed") {
|
|
945
1209
|
return;
|
|
946
1210
|
}
|
|
947
1211
|
await consumer.disconnect().catch(() => void 0);
|
|
948
1212
|
try {
|
|
949
1213
|
await this.#startConsuming();
|
|
950
|
-
|
|
951
|
-
`[KafkaTransport](restartConsumer) Consumer restarted | group=${
|
|
1214
|
+
logger.log(
|
|
1215
|
+
`[KafkaTransport](restartConsumer) Consumer restarted | group=${groupId} attempt=${attempt}`
|
|
952
1216
|
);
|
|
953
1217
|
return;
|
|
954
1218
|
} catch (error) {
|
|
955
|
-
|
|
956
|
-
`[KafkaTransport](restartConsumer) Restart failed, will retry | group=${
|
|
1219
|
+
logger.error(
|
|
1220
|
+
`[KafkaTransport](restartConsumer) Restart failed, will retry | group=${groupId} attempt=${attempt} error=${core.toErrorInfo(error).message}`
|
|
957
1221
|
);
|
|
958
1222
|
}
|
|
959
1223
|
}
|
|
960
1224
|
}
|
|
961
|
-
#waitForGroupJoin(consumer) {
|
|
962
|
-
if (!this.#tracksMembership(consumer)) {
|
|
963
|
-
return { promise: Promise.resolve(), dispose: () => void 0 };
|
|
964
|
-
}
|
|
965
|
-
let cancel;
|
|
966
|
-
const promise = new Promise((resolve, reject) => {
|
|
967
|
-
this.#joinWaiter = { resolve, reject };
|
|
968
|
-
cancel = internal.schedule(core.systemClock, {
|
|
969
|
-
run: () => {
|
|
970
|
-
reject(
|
|
971
|
-
new GroupJoinTimeoutError(
|
|
972
|
-
`Consumer of transport '${this.name}' did not join group '${this.#groupId}' within ${this.#settings.startupTimeoutMs}ms`
|
|
973
|
-
)
|
|
974
|
-
);
|
|
975
|
-
},
|
|
976
|
-
delayMs: this.#settings.startupTimeoutMs
|
|
977
|
-
});
|
|
978
|
-
});
|
|
979
|
-
promise.catch(() => void 0);
|
|
980
|
-
return {
|
|
981
|
-
promise,
|
|
982
|
-
dispose: () => {
|
|
983
|
-
cancel?.();
|
|
984
|
-
this.#joinWaiter = void 0;
|
|
985
|
-
}
|
|
986
|
-
};
|
|
987
|
-
}
|
|
988
1225
|
};
|
|
989
|
-
function resolveCodec({ codec, decode }) {
|
|
990
|
-
if (decode === void 0) {
|
|
991
|
-
return codec ?? cloudEventsBinary;
|
|
992
|
-
}
|
|
993
|
-
if (codec !== void 0) {
|
|
994
|
-
throw new core.ConfigurationError("Give 'codec' or the deprecated 'decode', not both");
|
|
995
|
-
}
|
|
996
|
-
return { encode: encodeEnvelope, decode };
|
|
997
|
-
}
|
|
998
|
-
function resolveSettings(options) {
|
|
999
|
-
const processing = options.processing ?? {};
|
|
1000
|
-
const maxAttempts = internal.assertPositiveInteger(
|
|
1001
|
-
processing.maxAttempts ?? DEFAULTS.maxAttempts,
|
|
1002
|
-
"processing.maxAttempts"
|
|
1003
|
-
);
|
|
1004
|
-
return {
|
|
1005
|
-
maxAttempts,
|
|
1006
|
-
initialBackoffMs: core.toMilliseconds(
|
|
1007
|
-
processing.initialBackoff ?? DEFAULTS.initialBackoff,
|
|
1008
|
-
"processing.initialBackoff"
|
|
1009
|
-
),
|
|
1010
|
-
maxBackoffMs: core.toMilliseconds(
|
|
1011
|
-
processing.maxBackoff ?? DEFAULTS.maxBackoff,
|
|
1012
|
-
"processing.maxBackoff"
|
|
1013
|
-
),
|
|
1014
|
-
startupTimeoutMs: core.toMilliseconds(
|
|
1015
|
-
options.startupTimeout ?? DEFAULTS.startupTimeout,
|
|
1016
|
-
"startupTimeout"
|
|
1017
|
-
),
|
|
1018
|
-
publishTimeoutMs: core.toMilliseconds(
|
|
1019
|
-
options.publishTimeout ?? DEFAULTS.publishTimeout,
|
|
1020
|
-
"publishTimeout"
|
|
1021
|
-
),
|
|
1022
|
-
heartbeatIntervalMs: Math.max(
|
|
1023
|
-
1,
|
|
1024
|
-
core.toMilliseconds(
|
|
1025
|
-
processing.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
|
|
1026
|
-
"processing.heartbeatInterval"
|
|
1027
|
-
)
|
|
1028
|
-
),
|
|
1029
|
-
...resolveRestartSettings(options.restart ?? {})
|
|
1030
|
-
};
|
|
1031
|
-
}
|
|
1032
|
-
function resolveRestartSettings(restart) {
|
|
1033
|
-
return {
|
|
1034
|
-
restartInitialBackoffMs: core.toMilliseconds(
|
|
1035
|
-
restart.initialBackoff ?? DEFAULTS.restartInitialBackoff,
|
|
1036
|
-
"restart.initialBackoff"
|
|
1037
|
-
),
|
|
1038
|
-
restartMaxBackoffMs: core.toMilliseconds(
|
|
1039
|
-
restart.maxBackoff ?? DEFAULTS.restartMaxBackoff,
|
|
1040
|
-
"restart.maxBackoff"
|
|
1041
|
-
)
|
|
1042
|
-
};
|
|
1043
|
-
}
|
|
1044
|
-
var MAXIMUM_IN_FLIGHT_REQUESTS = 5;
|
|
1045
|
-
function assertMaxInFlightRequests(value) {
|
|
1046
|
-
if (!Number.isInteger(value) || value < 1 || value > MAXIMUM_IN_FLIGHT_REQUESTS) {
|
|
1047
|
-
throw new core.ConfigurationError(
|
|
1048
|
-
`maxInFlightRequests must be an integer from 1 to 5, received ${value}`
|
|
1049
|
-
);
|
|
1050
|
-
}
|
|
1051
|
-
return value;
|
|
1052
|
-
}
|
|
1053
|
-
function toHandlerFailure(error) {
|
|
1054
|
-
const { name, message } = core.toErrorInfo(error);
|
|
1055
|
-
return new HandlerFailedError(`${name}: ${message}`);
|
|
1056
|
-
}
|
|
1057
1226
|
|
|
1058
1227
|
Object.defineProperty(exports, "fromHeader", {
|
|
1059
1228
|
enumerable: true,
|
|
@@ -1074,6 +1243,7 @@ exports.cloudEventsBinary = cloudEventsBinary;
|
|
|
1074
1243
|
exports.cloudEventsStructured = cloudEventsStructured;
|
|
1075
1244
|
exports.decodeCloudEvent = decodeCloudEvent;
|
|
1076
1245
|
exports.decodeEnvelope = decodeEnvelope;
|
|
1246
|
+
exports.decodeKafkaRecord = decodeKafkaRecord;
|
|
1077
1247
|
exports.encodeEnvelope = encodeEnvelope;
|
|
1078
1248
|
exports.jsonEnvelope = jsonEnvelope;
|
|
1079
1249
|
exports.plainJsonDecoder = plainJsonDecoder;
|