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