@relaybus/kafka 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { RelayBusError, NonRetryableError, assertEventType, toErrorInfo, systemClock, ConfigurationError } from '@relaybus/core';
1
+ import { RelayBusError, NonRetryableError, assertEventType, ConfigurationError, toErrorInfo, systemClock } from '@relaybus/core';
2
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
 
@@ -436,15 +436,30 @@ function jsonEnvelope(mapping) {
436
436
  transportIds: [RECORD_POSITION],
437
437
  failures: { invalid, unencodable: (reason) => new UnencodableEventError(reason) }
438
438
  });
439
- const contentType = mapping.contentType ?? DEFAULT_CONTENT_TYPE;
439
+ const contentType = mapping.contentTypeHeader === false ? {} : { [CONTENT_TYPE]: mapping.contentType ?? DEFAULT_CONTENT_TYPE };
440
440
  const stampsTime = mapping.occurredAt === void 0;
441
- 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);
442
443
  return {
443
- encode: (envelope) => encodeJson(envelope, { format, stampsTime, ownHeaders }),
444
+ encode: (envelope) => encodeJson(envelope, { format, stampsTime, ownHeaders, recordKey }),
444
445
  decode: (record) => decodeJson(record, format)
445
446
  };
446
447
  }
447
- function encodeJson(envelope, { format, stampsTime, ownHeaders }) {
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 }) {
448
463
  assertRepresentable(envelope);
449
464
  const { body, headers } = format.write(envelope);
450
465
  const taken = [CONTENT_TYPE, EVENT_ID_HEADER].find((name) => Object.hasOwn(headers, name));
@@ -454,7 +469,7 @@ function encodeJson(envelope, { format, stampsTime, ownHeaders }) {
454
469
  );
455
470
  }
456
471
  return {
457
- key: envelope.key,
472
+ key: recordKey(envelope),
458
473
  value: serializeJson(envelope, body),
459
474
  headers: { ...headers, ...ownHeaders(envelope) },
460
475
  ...stampsTime ? { timestamp: recordTimestamp(envelope) } : {}
@@ -485,9 +500,6 @@ function recordTimestamp(envelope) {
485
500
  formatTime(envelope);
486
501
  return String(envelope.occurredAt.getTime());
487
502
  }
488
- function tracksMembership(consumer) {
489
- return consumer.on !== void 0 && consumer.events?.GROUP_JOIN !== void 0;
490
- }
491
503
  var GroupJoinWatch = class {
492
504
  #settings;
493
505
  #waiter;
@@ -513,7 +525,7 @@ var GroupJoinWatch = class {
513
525
  eachMessage
514
526
  }) {
515
527
  await consumer.connect();
516
- await consumer.subscribe({ topics, fromBeginning });
528
+ await consumer.subscribe(this.#settings.dialect.subscription(topics, fromBeginning));
517
529
  const joined = this.#wait(consumer);
518
530
  try {
519
531
  await consumer.run({ eachMessage });
@@ -523,7 +535,7 @@ var GroupJoinWatch = class {
523
535
  }
524
536
  }
525
537
  #wait(consumer) {
526
- if (!tracksMembership(consumer)) {
538
+ if (!this.#settings.dialect.tracksMembership(consumer)) {
527
539
  return { promise: Promise.resolve(), dispose: () => void 0 };
528
540
  }
529
541
  const { name, groupId, timings } = this.#settings;
@@ -597,7 +609,7 @@ var KafkaProducerSession = class {
597
609
  delayMs: timings.publishTimeoutMs
598
610
  });
599
611
  });
600
- const sending = producer.send(record);
612
+ const sending = producer.send(this.#settings.dialect.record(record));
601
613
  sending.catch(() => void 0);
602
614
  try {
603
615
  await Promise.race([sending, timeout]);
@@ -606,8 +618,12 @@ var KafkaProducerSession = class {
606
618
  }
607
619
  }
608
620
  async #open() {
609
- const { kafka, producerConfig, maxInFlightRequests } = this.#settings;
610
- const producer = kafka.producer({ ...producerConfig, idempotent: true, maxInFlightRequests });
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));
611
627
  try {
612
628
  await producer.connect();
613
629
  } catch (error) {
@@ -618,6 +634,88 @@ var KafkaProducerSession = class {
618
634
  return producer;
619
635
  }
620
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
621
719
  var DEFAULTS = {
622
720
  name: "kafka",
623
721
  maxInFlightRequests: 1,
@@ -635,6 +733,8 @@ function resolveKafkaSettings(options) {
635
733
  return {
636
734
  name: options.name ?? DEFAULTS.name,
637
735
  kafka: options.kafka,
736
+ injectedProducer: resolveInjectedProducer(options),
737
+ dialect: clientDialect(options.client),
638
738
  groupId: options.groupId,
639
739
  fromBeginning: options.fromBeginning ?? true,
640
740
  maxInFlightRequests: BoundedInteger.of(
@@ -649,6 +749,23 @@ function resolveKafkaSettings(options) {
649
749
  timings: resolveTimings(options)
650
750
  };
651
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
+ }
652
769
  function resolveCodec({ codec, decode }) {
653
770
  if (decode === void 0) {
654
771
  return codec ?? cloudEventsBinary;
@@ -718,7 +835,7 @@ function assertCanSubscribe(state, {
718
835
  settings,
719
836
  destinations
720
837
  }) {
721
- const { name, groupId, invalidPolicy } = settings;
838
+ const { name, groupId, invalidPolicy, kafka } = settings;
722
839
  if (state === "closed") {
723
840
  throw new TransportClosedError(`Transport '${name}' is closed`);
724
841
  }
@@ -727,6 +844,9 @@ function assertCanSubscribe(state, {
727
844
  `Transport '${name}' already subscribed; list every topic in one subscription or use one KafkaTransport per consumer group`
728
845
  );
729
846
  }
847
+ if (kafka === void 0) {
848
+ throw new ConfigurationError(`Transport '${name}' needs 'kafka' to create its consumer`);
849
+ }
730
850
  if (groupId === void 0 || groupId.length === 0) {
731
851
  throw new ConfigurationError(`Transport '${name}' needs a groupId to subscribe`);
732
852
  }
@@ -745,10 +865,7 @@ function assertCanSubscribe(state, {
745
865
 
746
866
  // src/kafka-records.ts
747
867
  function decodeRecord(codec, { topic, partition, message }) {
748
- if (codec === cloudEventsBinary) {
749
- return decodeEnvelope(message);
750
- }
751
- const record = {
868
+ return decodeKafkaRecord(codec, {
752
869
  topic,
753
870
  partition,
754
871
  offset: message.offset,
@@ -756,7 +873,12 @@ function decodeRecord(codec, { topic, partition, message }) {
756
873
  value: message.value,
757
874
  headers: message.headers,
758
875
  timestamp: message.timestamp
759
- };
876
+ });
877
+ }
878
+ function decodeKafkaRecord(codec, record) {
879
+ if (codec === cloudEventsBinary) {
880
+ return decodeEnvelope(record);
881
+ }
760
882
  return completeDecodedEvent(codec.decode(record), record);
761
883
  }
762
884
  function deadLetterRecord({ topic, partition, message }, {
@@ -894,10 +1016,12 @@ var KafkaTransport = class extends SubscribingTransport {
894
1016
  async #subscribe({ destinations, onMessage, hooks }) {
895
1017
  assertCanSubscribe(this.#state, { settings: this.#settings, destinations });
896
1018
  this.#state = "subscribed";
897
- const { kafka, consumerConfig, groupId } = this.#settings;
898
- const consumer = kafka.consumer({ ...consumerConfig, groupId });
1019
+ const { kafka, dialect, groupId } = this.#settings;
1020
+ const membership = this.#observeConsumer();
1021
+ const consumer = kafka.consumer(dialect.consumerConfig(this.#settings, membership));
899
1022
  this.#consumer = consumer;
900
- this.#observeConsumer(consumer);
1023
+ this.#groupJoined = !dialect.tracksMembership(consumer);
1024
+ dialect.observe(consumer, membership);
901
1025
  const start = () => this.#groupJoin.start(consumer, {
902
1026
  topics: [...destinations],
903
1027
  fromBeginning: this.#settings.fromBeginning,
@@ -1038,34 +1162,25 @@ var KafkaTransport = class extends SubscribingTransport {
1038
1162
  }
1039
1163
  }
1040
1164
  }
1041
- #observeConsumer(consumer) {
1042
- if (!tracksMembership(consumer)) {
1043
- this.#groupJoined = true;
1044
- return;
1045
- }
1165
+ /** What the transport does when the consumer joins, rebalances, crashes or leaves its group. */
1166
+ #observeConsumer() {
1046
1167
  const { logger, groupId } = this.#settings;
1047
- const names = consumer.events;
1048
- const listen = (name, listener) => {
1049
- if (name !== void 0) {
1050
- consumer.on(name, listener);
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;
1051
1182
  }
1052
1183
  };
1053
- listen(names.GROUP_JOIN, () => {
1054
- this.#groupJoined = true;
1055
- this.#groupJoin.joined();
1056
- logger.log(`[KafkaTransport](observeConsumer) Joined group | group=${groupId}`);
1057
- });
1058
- listen(names.REBALANCING, () => {
1059
- this.#groupJoined = false;
1060
- this.#stalledPartitions.clear();
1061
- logger.warn(`[KafkaTransport](observeConsumer) Rebalancing | group=${groupId}`);
1062
- });
1063
- listen(names.CRASH, (event) => this.#handleCrash(event));
1064
- const leftGroup = () => {
1065
- this.#groupJoined = false;
1066
- };
1067
- listen(names.STOP, leftGroup);
1068
- listen(names.DISCONNECT, leftGroup);
1069
1184
  }
1070
1185
  #handleCrash(event) {
1071
1186
  const { error, restart } = event.payload ?? {};
@@ -1108,6 +1223,6 @@ var KafkaTransport = class extends SubscribingTransport {
1108
1223
  }
1109
1224
  };
1110
1225
 
1111
- 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 };
1112
1227
  //# sourceMappingURL=index.js.map
1113
1228
  //# sourceMappingURL=index.js.map