@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.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
- topic: record.topic,
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: resolveText(options.id, { record: plain, name: "id" }) ?? `kafka:${record.topic}:${record.partition}:${record.offset}`,
322
+ id: KAFKA_EVENTS.text(options.id, { plain, name: "id" }) ?? `kafka:${record.topic}:${record.partition}:${record.offset}`,
331
323
  type,
332
- source: resolveText(options.source, { record: plain, name: "source" }) ?? record.topic,
333
- key: options.key === void 0 ? plain.key : resolveText(options.key, { record: plain, name: "key" }) ?? null,
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 completeDecodedEvent(decoded, record) {
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
- id: event.id,
353
- type: event.type,
354
- source: event.source ?? record.topic,
355
- key: event.key === void 0 ? readRecordKey(record.key) : event.key,
356
- payload: event.payload,
357
- headers: event.headers ?? {},
358
- correlationId: event.correlationId ?? null,
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 ? { [CONTENT_TYPE]: contentType, [EVENT_ID_HEADER]: envelope.id } : { [CONTENT_TYPE]: contentType };
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
- assertRepresentable(envelope);
459
- const { body, headers } = format.write(envelope);
460
- const taken = [CONTENT_TYPE, EVENT_ID_HEADER].find((name) => Object.hasOwn(headers, name));
461
- if (taken !== void 0) {
462
- throw new UnencodableEventError(
463
- `Event ${envelope.id} has a header '${taken}', which the codec writes itself`
464
- );
465
- }
466
- return {
467
- key: envelope.key,
468
- value: serializeJson(envelope, body),
469
- headers: { ...headers, ...ownHeaders(envelope) },
470
- ...stampsTime ? { timestamp: recordTimestamp(envelope) } : {}
471
- };
472
- },
473
- decode: (record) => {
474
- const { [EVENT_ID_HEADER]: eventId, ...headers } = withoutContentType(
475
- readHeadersLeniently(record.headers)
476
- );
477
- const fields = format.read({ body: parsePayload(record.value), headers });
478
- return {
479
- ...fields,
480
- id: fields.id ?? (eventId === void 0 || eventId === "" ? `kafka:${record.topic}:${record.partition}:${record.offset}` : eventId)
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 KafkaTransport = class {
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
- this.name = options.name ?? DEFAULTS.name;
538
- this.#kafka = options.kafka;
539
- this.#groupId = options.groupId;
540
- this.#fromBeginning = options.fromBeginning ?? true;
541
- this.#maxInFlightRequests = assertMaxInFlightRequests(
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.#producerConnected) {
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.#connectedProducer();
997
+ await this.#producer.connected();
574
998
  }
575
999
  async publish(envelope, destination) {
576
- const producer = await this.#connectedProducer();
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
- // eslint-disable-next-line relaybus/max-two-params -- the Transport.subscribe signature
591
- async subscribe(destinations, onMessage, hooks) {
592
- this.#assertCanSubscribe(destinations);
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 consumer = this.#kafka.consumer({ ...this.#consumerConfig, groupId: this.#groupId });
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.#observeConsumer(consumer);
597
- const start = () => this.#startConsumer(consumer, { destinations: [...destinations], onMessage, hooks });
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=${this.#groupId} topics=${destinations.join(",")}`
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.#producerConnected = false;
639
- for (const wake of this.#sleepers) {
640
- wake();
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
- #assertCanSubscribe(destinations) {
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.#decodeRecord(payload);
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 = this.#deadLetterTopicFor(topic);
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 headers = {
804
- ...toOutgoingHeaders(message.headers),
805
- "relaybus-invalid-reason": reason,
806
- "relaybus-source-topic": topic,
807
- "relaybus-source-partition": String(partition),
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.#logger.error(
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, initialBackoffMs, maxBackoffMs } = this.#settings;
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
- this.#logger.error(
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
- this.#logger.warn(
1155
+ logger.warn(
857
1156
  `[KafkaTransport](withRetries) Attempt failed, retrying | operation=${method} ${location} attempt=${attempt} error=${reason}`
858
1157
  );
859
- await this.#sleepWithHeartbeats(
860
- Math.min(initialBackoffMs * 2 ** (attempt - 1), maxBackoffMs),
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
- /** Sleeps in slices and heartbeats after each, so a long backoff never outlives the session timeout. */
867
- async #sleepWithHeartbeats(milliseconds, payload) {
868
- let remaining = milliseconds;
869
- do {
870
- const slice = Math.min(remaining, this.#settings.heartbeatIntervalMs);
871
- await this.#sleep(slice);
872
- await payload.heartbeat();
873
- remaining -= slice;
874
- } while (remaining > 0 && this.#state !== "closed");
875
- }
876
- #sleep(milliseconds) {
877
- return new Promise((resolve) => {
878
- const wake = () => {
879
- cancel();
880
- this.#sleepers.delete(wake);
881
- resolve();
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.#joinWaiter?.reject(error);
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 { restartInitialBackoffMs, restartMaxBackoffMs } = this.#settings;
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
- this.#logger.log(
951
- `[KafkaTransport](restartConsumer) Consumer restarted | group=${this.#groupId} attempt=${attempt}`
1214
+ logger.log(
1215
+ `[KafkaTransport](restartConsumer) Consumer restarted | group=${groupId} attempt=${attempt}`
952
1216
  );
953
1217
  return;
954
1218
  } catch (error) {
955
- this.#logger.error(
956
- `[KafkaTransport](restartConsumer) Restart failed, will retry | group=${this.#groupId} attempt=${attempt} error=${core.toErrorInfo(error).message}`
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;