@platformatic/kafka 2.8.0 → 2.9.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.
@@ -1,7 +1,7 @@
1
1
  import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../../apis/callbacks.js";
2
2
  import { ConfigResourceTypes, FetchIsolationLevels, FindCoordinatorKeyTypes } from "../../apis/enumerations.js";
3
3
  import { adminAclsChannel, adminClientQuotasChannel, adminConfigsChannel, adminConsumerGroupOffsetsChannel, adminGroupsChannel, adminLogDirsChannel, adminOffsetsChannel, adminTopicsChannel, createDiagnosticContext } from "../../diagnostic.js";
4
- import { MultipleErrors, UserError } from "../../errors.js";
4
+ import { findErrorBy, MultipleErrors, UserError } from "../../errors.js";
5
5
  import { Reader } from "../../protocol/reader.js";
6
6
  import { Base, kAfterCreate, kCheckNotClosed, kConnections, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from "../base/base.js";
7
7
  import { adminListOffsetsOptionsValidator, alterClientQuotasOptionsValidator, alterConfigsOptionsValidator, alterConsumerGroupOffsetsOptionsValidator, createAclsOptionsValidator, createPartitionsOptionsValidator, createTopicsOptionsValidator, deleteAclsOptionsValidator, deleteConsumerGroupOffsetsOptionsValidator, deleteGroupsOptionsValidator, deleteRecordsOptionsValidator, deleteTopicsOptionsValidator, describeAclsOptionsValidator, describeClientQuotasOptionsValidator, describeConfigsOptionsValidator, describeGroupsOptionsValidator, describeLogDirsOptionsValidator, findCoordinatorOptionsValidator, incrementalAlterConfigsOptionsValidator, listConsumerGroupOffsetsOptionsValidator, listGroupsOptionsValidator, listTopicsOptionsValidator, removeMembersFromConsumerGroupOptionsValidator } from "./options.js";
@@ -373,7 +373,7 @@ export class Admin extends Base {
373
373
  }
374
374
  }
375
375
  #handleNotControllerError(error, value, callback) {
376
- if (error && error?.findBy?.('apiCode', 41)) {
376
+ if (findErrorBy(error, 'apiCode', 41)) {
377
377
  this[kMetadata]({ topics: [], forceUpdate: true }, (metadataError, metadata) => {
378
378
  /* c8 ignore next 4 - Hard to test */
379
379
  if (metadataError) {
@@ -2,7 +2,7 @@ import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } f
2
2
  import * as apis from "../../apis/index.js";
3
3
  import { api as apiVersionsV3 } from "../../apis/metadata/api-versions-v3.js";
4
4
  import { baseApisChannel, baseMetadataChannel, createDiagnosticContext, notifyCreation } from "../../diagnostic.js";
5
- import { MultipleErrors, NetworkError, UnsupportedApiError, UserError } from "../../errors.js";
5
+ import { findErrorBy, MultipleErrors, NetworkError, UnsupportedApiError, UserError } from "../../errors.js";
6
6
  import { TypedEventEmitter } from "../../events.js";
7
7
  import { ConnectionPool } from "../../network/connection-pool.js";
8
8
  import { parseBroker } from "../../network/utils.js";
@@ -229,7 +229,12 @@ export class Base extends TypedEventEmitter {
229
229
  }
230
230
  [kCheckNotClosed](callback) {
231
231
  if (this[kClosed]) {
232
- const error = new NetworkError('Client is closed.', { closed: true, instance: this[kInstance] });
232
+ // A closed client never reopens, so this must not be retried.
233
+ const error = new NetworkError('Client is closed.', {
234
+ canRetry: false,
235
+ closed: true,
236
+ instance: this[kInstance]
237
+ });
233
238
  callback(error, undefined);
234
239
  return true;
235
240
  }
@@ -243,9 +248,8 @@ export class Base extends TypedEventEmitter {
243
248
  this.emitWithDebug('client', 'performWithRetry', operationId, attempt, retries);
244
249
  operation((error, result) => {
245
250
  if (error) {
246
- const genericError = error;
247
251
  // Only retry if all the errors in the chain are retriable
248
- const retriable = !genericError.findBy?.('canRetry', false);
252
+ const retriable = !findErrorBy(error, 'canRetry', false);
249
253
  errors.push(error);
250
254
  if (attempt < retries && retriable && !shouldSkipRetry?.(error)) {
251
255
  function onClose() {
@@ -430,7 +434,7 @@ export class Base extends TypedEventEmitter {
430
434
  }, attempt);
431
435
  }, (error, metadata) => {
432
436
  if (error) {
433
- const unknownTopicError = error.findBy('apiCode', 3);
437
+ const unknownTopicError = findErrorBy(error, 'apiCode', 3);
434
438
  if (unknownTopicError) {
435
439
  const topicIndexMatch = unknownTopicError.path?.match(/\/topics\/(\d+)/);
436
440
  /* c8 ignore next 3 - Hard to test */
@@ -439,7 +443,7 @@ export class Base extends TypedEventEmitter {
439
443
  deduplicateCallback(new UserError(`Unknown topic ${topicName}.`));
440
444
  return;
441
445
  }
442
- const hasStaleMetadata = error.findBy('hasStaleMetadata', true);
446
+ const hasStaleMetadata = findErrorBy(error, 'hasStaleMetadata', true);
443
447
  // Stale metadata, we need to fetch everything again
444
448
  /* c8 ignore next 4 - Hard to test */
445
449
  if (hasStaleMetadata) {
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../../apis/callbacks.js";
3
3
  import { FetchIsolationLevels, FindCoordinatorKeyTypes } from "../../apis/enumerations.js";
4
4
  import { consumerCommitsChannel, consumerConsumesChannel, consumerFetchesChannel, consumerGroupChannel, consumerHeartbeatChannel, consumerLagChannel, consumerOffsetsChannel, createDiagnosticContext } from "../../diagnostic.js";
5
- import { NetworkError, protocolErrors, UserError } from "../../errors.js";
5
+ import { findErrorBy, NetworkError, protocolErrors, UserError } from "../../errors.js";
6
6
  import { INT32_SIZE } from "../../protocol/definitions.js";
7
7
  import { Reader } from "../../protocol/reader.js";
8
8
  import { IS_CONTROL } from "../../protocol/records.js";
@@ -580,13 +580,12 @@ export class Consumer extends Base {
580
580
  api(connection, options.maxWaitTime ?? this[kOptions].maxWaitTime, options.minBytes ?? this[kOptions].minBytes, options.maxBytes ?? this[kOptions].maxBytes, isolationLevel, sessionId, sessionEpoch, requestTopics, forgottenTopicsData, this.#clientRack, (error, result) => {
581
581
  if (error) {
582
582
  this.#clearPreferredReadReplicas(options.topics, topicIds);
583
- const genericError = error;
584
- if (genericError.findBy?.('apiId', 'FETCH_SESSION_ID_NOT_FOUND') ||
585
- genericError.findBy?.('apiId', 'INVALID_FETCH_SESSION_EPOCH') ||
586
- genericError.findBy?.('apiId', 'FETCH_SESSION_TOPIC_ID_ERROR')) {
583
+ if (findErrorBy(error, 'apiId', 'FETCH_SESSION_ID_NOT_FOUND') ||
584
+ findErrorBy(error, 'apiId', 'INVALID_FETCH_SESSION_EPOCH') ||
585
+ findErrorBy(error, 'apiId', 'FETCH_SESSION_TOPIC_ID_ERROR')) {
587
586
  this.#fetchSessions.delete(node);
588
587
  }
589
- if (genericError.findBy?.('apiId', 'FENCED_LEADER_EPOCH')) {
588
+ if (findErrorBy(error, 'apiId', 'FENCED_LEADER_EPOCH')) {
590
589
  this.#fetchSessions.delete(node);
591
590
  this.clearMetadata();
592
591
  for (const topic of options.topics) {
@@ -791,7 +790,7 @@ export class Consumer extends Base {
791
790
  if (error) {
792
791
  if (this.#useConsumerGroupProtocol &&
793
792
  staleMemberEpochRetries < this[kOptions].retries &&
794
- error.findBy?.('apiId', 'STALE_MEMBER_EPOCH')) {
793
+ findErrorBy(error, 'apiId', 'STALE_MEMBER_EPOCH')) {
795
794
  this.#consumerGroupHeartbeat(this[kOptions], heartbeatError => {
796
795
  if (heartbeatError) {
797
796
  callback(this.#handleError(heartbeatError));
@@ -1041,7 +1040,35 @@ export class Consumer extends Base {
1041
1040
  callback(err);
1042
1041
  });
1043
1042
  }
1044
- #leaveGroupConsumerProtocol(_, callback) {
1043
+ #leaveGroupConsumerProtocol(force, callback) {
1044
+ // Remove streams that might have been exited in the meanwhile
1045
+ for (const stream of this.#streams) {
1046
+ if (stream.closed || stream.destroyed) {
1047
+ this.#streams.delete(stream);
1048
+ }
1049
+ }
1050
+ // Active streams must be closed before leaving the group, exactly like in the classic protocol.
1051
+ // Their fetch loop keeps running until they are closed and, since Consumer.close marks the client
1052
+ // as closed before leaving, any fetch scheduled in the meanwhile would fail with "Client is closed."
1053
+ // and destroy the stream with an error the user never asked for.
1054
+ if (this.#streams.size) {
1055
+ if (!force) {
1056
+ callback(new UserError('Cannot leave group while consuming messages.'));
1057
+ return;
1058
+ }
1059
+ runConcurrentCallbacks('Closing streams failed.', this.#streams, (stream, concurrentCallback) => {
1060
+ stream.close(concurrentCallback);
1061
+ }, error => {
1062
+ /* c8 ignore next 4 - Hard to test */
1063
+ if (error) {
1064
+ callback(error);
1065
+ return;
1066
+ }
1067
+ // All streams are closed, try the operation again without force
1068
+ this.#leaveGroupConsumerProtocol(false, callback);
1069
+ });
1070
+ return;
1071
+ }
1045
1072
  // Leave by sending a heartbeat with memberEpoch = -1
1046
1073
  this.#cancelHeartbeat();
1047
1074
  this.#performDeduplicateGroupOperaton('leaveGroupConsumerProtocol', (connection, groupCallback) => {
@@ -1260,7 +1287,7 @@ export class Consumer extends Base {
1260
1287
  });
1261
1288
  }, error => {
1262
1289
  if (error) {
1263
- const unknownMemberError = error.findBy?.('unknownMemberId', true);
1290
+ const unknownMemberError = findErrorBy(error, 'unknownMemberId', true);
1264
1291
  // This is to avoid throwing an error if a group join was cancelled.
1265
1292
  if (!unknownMemberError) {
1266
1293
  callback(error);
@@ -1541,7 +1568,7 @@ export class Consumer extends Base {
1541
1568
  }
1542
1569
  }
1543
1570
  #getRejoinError(error) {
1544
- const protocolError = error.findBy?.('needsRejoin', true);
1571
+ const protocolError = findErrorBy(error, 'needsRejoin', true);
1545
1572
  if (!protocolError) {
1546
1573
  return null;
1547
1574
  }
@@ -1627,17 +1654,14 @@ export class Consumer extends Base {
1627
1654
  }
1628
1655
  }
1629
1656
  #handleError(error) {
1630
- const kafkaError = error;
1631
- if (kafkaError) {
1632
- if (kafkaError.findBy('hasStaleMetadata', true)) {
1633
- this.clearMetadata();
1634
- }
1635
- else if (kafkaError.findBy('code', 'PLT_KFK_NETWORK') ||
1636
- kafkaError.findBy('apiId', 'COORDINATOR_LOAD_IN_PROGRESS') ||
1637
- kafkaError.findBy('apiId', 'COORDINATOR_NOT_AVAILABLE') ||
1638
- kafkaError.findBy('apiId', 'NOT_COORDINATOR')) {
1639
- this.#coordinatorId = null;
1640
- }
1657
+ if (findErrorBy(error, 'hasStaleMetadata', true)) {
1658
+ this.clearMetadata();
1659
+ }
1660
+ else if (findErrorBy(error, 'code', 'PLT_KFK_NETWORK') ||
1661
+ findErrorBy(error, 'apiId', 'COORDINATOR_LOAD_IN_PROGRESS') ||
1662
+ findErrorBy(error, 'apiId', 'COORDINATOR_NOT_AVAILABLE') ||
1663
+ findErrorBy(error, 'apiId', 'NOT_COORDINATOR')) {
1664
+ this.#coordinatorId = null;
1641
1665
  }
1642
1666
  return error;
1643
1667
  }
@@ -2,7 +2,7 @@ import { Readable } from 'node:stream';
2
2
  import { createPromisifiedCallback, kCallbackPromise, noopCallback } from "../../apis/callbacks.js";
3
3
  import { ListOffsetTimestamps } from "../../apis/enumerations.js";
4
4
  import { consumerReceivesChannel, createDiagnosticContext, notifyCreation } from "../../diagnostic.js";
5
- import { protocolErrors, UserError } from "../../errors.js";
5
+ import { findErrorBy, protocolErrors, UserError } from "../../errors.js";
6
6
  import { IS_CONTROL } from "../../protocol/records.js";
7
7
  import { runAsyncSeries } from "../../registries/abstract.js";
8
8
  import { kAutocommit, kGetFetchNode, kInstance, kRefreshOffsetsAndFetch } from "../../symbols.js";
@@ -454,7 +454,7 @@ export class MessagesStream extends Readable {
454
454
  });
455
455
  }
456
456
  #handleOffsetOutOfRange(error, topicIds, callback) {
457
- if (!error.findBy?.('apiId', 'OFFSET_OUT_OF_RANGE')) {
457
+ if (!findErrorBy(error, 'apiId', 'OFFSET_OUT_OF_RANGE')) {
458
458
  callback(null, false);
459
459
  return;
460
460
  }
@@ -613,6 +613,7 @@ export class MessagesStream extends Readable {
613
613
  let payloadType = 'headerKey';
614
614
  let deserializedKey;
615
615
  let deserializedValue;
616
+ let deserializationError;
616
617
  try {
617
618
  for (const [headerKey, headerValue] of record.headers) {
618
619
  payloadType = 'headerKey';
@@ -640,16 +641,46 @@ export class MessagesStream extends Readable {
640
641
  timestamp: firstTimestamp + record.timestampDelta,
641
642
  commit: commit
642
643
  };
643
- const shouldDestroy = this.#deserializationErrorHandler
644
- ? this.#deserializationErrorHandler(context) !== DeserializationErrorActions.SKIP
645
- : this.#corruptedMessageHandler(record, topic, partition, firstTimestamp, firstOffset, commit, error);
646
- if (shouldDestroy) {
644
+ const action = this.#deserializationErrorHandler
645
+ ? this.#deserializationErrorHandler(context)
646
+ : this.#corruptedMessageHandler(record, topic, partition, firstTimestamp, firstOffset, commit, error)
647
+ ? DeserializationErrorActions.FAIL
648
+ : DeserializationErrorActions.SKIP;
649
+ if (action === DeserializationErrorActions.CONTINUE) {
650
+ /*
651
+ Deliver the message in a degraded form, so that nothing is lost: whatever was
652
+ already deserialized is kept, while the payload which failed and the ones which
653
+ were never attempted, since deserialization goes headers, key and then value,
654
+ are delivered as the original bytes.
655
+
656
+ The error is reported on the message metadata and published on the diagnostic
657
+ channel, which is where callers should react to it: logging here would be a log
658
+ storm waiting to happen.
659
+ */
660
+ diagnosticContext.error = error;
661
+ consumerReceivesChannel.error.publish(diagnosticContext);
662
+ const headersFailed = payloadType === 'headerKey' || payloadType === 'headerValue';
663
+ if (headersFailed) {
664
+ headers.clear();
665
+ for (const [headerKey, headerValue] of record.headers) {
666
+ headers.set(headerKey, headerValue);
667
+ }
668
+ }
669
+ if (headersFailed || payloadType === 'key') {
670
+ deserializedKey = (record.key ?? undefined);
671
+ }
672
+ deserializedValue = (record.value ?? undefined);
673
+ deserializationError = { error, payloadType };
674
+ }
675
+ else if (action === DeserializationErrorActions.SKIP) {
676
+ continue;
677
+ }
678
+ else {
647
679
  diagnosticContext.error = error;
648
680
  consumerReceivesChannel.error.publish(diagnosticContext);
649
681
  this.destroy(new UserError('Failed to deserialize a message.', { cause: error }));
650
682
  return;
651
683
  }
652
- continue;
653
684
  }
654
685
  this.#metricsConsumedMessages?.inc();
655
686
  const message = {
@@ -661,7 +692,12 @@ export class MessagesStream extends Readable {
661
692
  timestamp: firstTimestamp + record.timestampDelta,
662
693
  offset,
663
694
  commit,
664
- metadata: messageMetadata,
695
+ // Deserializers and hooks can enrich the metadata of the message they processed
696
+ metadata: deserializationError
697
+ ? { ...messageMetadata, ...messageToConsume.metadata, deserializationError }
698
+ : messageToConsume.metadata
699
+ ? { ...messageMetadata, ...messageToConsume.metadata }
700
+ : messageMetadata,
665
701
  toJSON: messageToJSON
666
702
  };
667
703
  diagnosticContext.result = message;
@@ -747,7 +783,7 @@ export class MessagesStream extends Readable {
747
783
  // Only destroy the stream when the broker requires a group rejoin
748
784
  // (ILLEGAL_GENERATION, UNKNOWN_MEMBER_ID, REBALANCE_IN_PROGRESS).
749
785
  // Transient coordinator errors must not tear down the consumption loop.
750
- if (error.findBy?.('needsRejoin', true)) {
786
+ if (findErrorBy(error, 'needsRejoin', true)) {
751
787
  this.destroy(error);
752
788
  }
753
789
  return;
@@ -35,6 +35,7 @@ export interface PreferredReadReplica {
35
35
  export declare const DeserializationErrorActions: {
36
36
  readonly FAIL: "fail";
37
37
  readonly SKIP: "skip";
38
+ readonly CONTINUE: "continue";
38
39
  };
39
40
  export type DeserializationErrorAction = (typeof DeserializationErrorActions)[keyof typeof DeserializationErrorActions];
40
41
  export interface DeserializationErrorContext {
@@ -47,6 +48,10 @@ export interface DeserializationErrorContext {
47
48
  timestamp: bigint;
48
49
  commit: Message['commit'];
49
50
  }
51
+ export interface MessageDeserializationError {
52
+ error: unknown;
53
+ payloadType: BeforeHookPayloadType;
54
+ }
50
55
  export type DeserializationErrorHandler = (context: DeserializationErrorContext) => DeserializationErrorAction;
51
56
  /**
52
57
  * @deprecated Use `DeserializationErrorHandler` instead.
@@ -1,6 +1,7 @@
1
1
  export const DeserializationErrorActions = {
2
2
  FAIL: 'fail',
3
- SKIP: 'skip'
3
+ SKIP: 'skip',
4
+ CONTINUE: 'continue'
4
5
  };
5
6
  export const MessagesStreamModes = {
6
7
  LATEST: 'latest',
@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto';
2
2
  import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../../apis/callbacks.js";
3
3
  import { FindCoordinatorKeyTypes, ProduceAcks } from "../../apis/enumerations.js";
4
4
  import { createDiagnosticContext, producerInitIdempotentChannel, producerSendsChannel, producerTransactionsChannel } from "../../diagnostic.js";
5
- import { GenericError, UserError } from "../../errors.js";
5
+ import { findErrorBy, UserError } from "../../errors.js";
6
6
  import { runAsyncSeries } from "../../registries/abstract.js";
7
7
  import { kInstance, kTransaction, kTransactionAddOffsets, kTransactionAddPartitions, kTransactionCancel, kTransactionCommitOffset, kTransactionEnd, kTransactionFindCoordinator, kTransactionPrepare } from "../../symbols.js";
8
8
  import { emitExperimentalApiWarning, NumericMap } from "../../utils.js";
@@ -770,11 +770,8 @@ export class Producer extends Base {
770
770
  if (error) {
771
771
  // If the last error was due to stale metadata, we retry the operation with this set of messages
772
772
  // since the partition is already set, it should attempt on the new destination
773
- /* c8 ignore next 3 - Hard to test */
774
- const kafkaError = GenericError.isGenericError(error)
775
- ? error
776
- : null;
777
- const hasStaleMetadata = kafkaError?.findBy('hasStaleMetadata', true);
773
+ /* c8 ignore next - Hard to test */
774
+ const hasStaleMetadata = findErrorBy(error, 'hasStaleMetadata', true);
778
775
  if (hasStaleMetadata && repeatOnStaleMetadata) {
779
776
  this.clearMetadata();
780
777
  this.#performSingleDestinationSend(topics, messages, timeout, acks, autocreateTopics, false, produceOptions, recoverOnProducerStateLoss, callback);
@@ -796,17 +793,16 @@ export class Producer extends Base {
796
793
  callback(error, results);
797
794
  }, 0, [], error => {
798
795
  /* c8 ignore next 3 - Hard to test */
799
- if (!repeatOnStaleMetadata || !GenericError.isGenericError(error)) {
796
+ if (!repeatOnStaleMetadata) {
800
797
  return false;
801
798
  }
802
- return !!error.findBy('hasStaleMetadata', true);
799
+ return !!findErrorBy(error, 'hasStaleMetadata', true);
803
800
  });
804
801
  });
805
802
  }
806
803
  #isProducerStateLost(error) {
807
- const kafkaError = error;
808
- return !!(GenericError.isGenericError(error) &&
809
- (kafkaError.findBy('apiId', 'UNKNOWN_PRODUCER_ID') || kafkaError.findBy('apiId', 'OUT_OF_ORDER_SEQUENCE_NUMBER')));
804
+ return !!(findErrorBy(error, 'apiId', 'UNKNOWN_PRODUCER_ID') ||
805
+ findErrorBy(error, 'apiId', 'OUT_OF_ORDER_SEQUENCE_NUMBER'));
810
806
  }
811
807
  #recoverIdempotentProducer(messages, produceOptions, callback) {
812
808
  this.#producerInfo = undefined;
@@ -859,20 +855,17 @@ export class Producer extends Base {
859
855
  });
860
856
  }
861
857
  #handleFencingError(error) {
862
- if (error.findBy?.('producerFenced', true)) {
858
+ if (findErrorBy(error, 'producerFenced', true)) {
863
859
  this.#producerInfo = undefined;
864
860
  this.#transaction = undefined;
865
861
  }
866
862
  }
867
863
  #handleError(error) {
868
- const kafkaError = error;
869
- if (kafkaError) {
870
- if (kafkaError.findBy('code', 'PLT_KFK_NETWORK') ||
871
- kafkaError.findBy('apiId', 'COORDINATOR_LOAD_IN_PROGRESS') ||
872
- kafkaError.findBy('apiId', 'COORDINATOR_NOT_AVAILABLE') ||
873
- kafkaError.findBy('apiId', 'NOT_COORDINATOR')) {
874
- this.#coordinatorId = undefined;
875
- }
864
+ if (findErrorBy(error, 'code', 'PLT_KFK_NETWORK') ||
865
+ findErrorBy(error, 'apiId', 'COORDINATOR_LOAD_IN_PROGRESS') ||
866
+ findErrorBy(error, 'apiId', 'COORDINATOR_NOT_AVAILABLE') ||
867
+ findErrorBy(error, 'apiId', 'NOT_COORDINATOR')) {
868
+ this.#coordinatorId = undefined;
876
869
  }
877
870
  return error;
878
871
  }
package/dist/errors.d.ts CHANGED
@@ -28,6 +28,7 @@ export declare class MultipleErrors extends AggregateError {
28
28
  findBy<ErrorType extends GenericError | MultipleErrors = MultipleErrors>(property: string, value: unknown): ErrorType | null;
29
29
  toString(): string;
30
30
  }
31
+ export declare function findErrorBy<ErrorType extends GenericError = GenericError>(error: Error | null | undefined, property: string, value: unknown): ErrorType | null;
31
32
  export * from './protocol/errors.ts';
32
33
  export declare class AuthenticationError extends GenericError {
33
34
  static code: ErrorCode;
package/dist/errors.js CHANGED
@@ -98,6 +98,18 @@ export class MultipleErrors extends AggregateError {
98
98
  return `Error ${this.code}: ${this.message} (caused by: ${this.errors.map(error => error.toString()).join(', ')})`;
99
99
  }
100
100
  }
101
+ // findBy only exists on the library's own error classes, but errors also come from user code, Node
102
+ // internals and third parties. This brand checks first, so classification is total: an error that
103
+ // cannot be classified reports no match instead of throwing. Use it for any error whose origin is
104
+ // not statically known.
105
+ export function findErrorBy(error, property, value) {
106
+ if (!error || !GenericError.isGenericError(error)) {
107
+ return null;
108
+ }
109
+ // GenericError and MultipleErrors declare findBy with different generic defaults, so the union is
110
+ // not callable. The brand check above guarantees the call is safe either way.
111
+ return error.findBy(property, value);
112
+ }
101
113
  export * from "./protocol/errors.js";
102
114
  export class AuthenticationError extends GenericError {
103
115
  static code = 'PLT_KFK_AUTHENTICATION';
@@ -92,7 +92,9 @@ export class ConnectionPool extends TypedEventEmitter {
92
92
  }
93
93
  #get(broker, callback) {
94
94
  if (this.#closed) {
95
- callback(new Error('Connection pool is closed.'));
95
+ // A closed pool never reopens, so this must not be retried. It also has to be a library error:
96
+ // clients classify errors via findBy, which only exists on GenericError subclasses.
97
+ callback(new NetworkError('Connection pool is closed.', { canRetry: false, closed: true, broker }));
96
98
  return;
97
99
  }
98
100
  if (broker == null || broker.host == null || broker.port == null) {
@@ -71,6 +71,7 @@ export interface KafkaRecord {
71
71
  export interface MessageToConsume extends KafkaRecord {
72
72
  topic: string;
73
73
  partition: number;
74
+ metadata?: Record<string, unknown>;
74
75
  }
75
76
  export interface RecordsBatch {
76
77
  firstOffset: bigint;
@@ -1,5 +1,6 @@
1
1
  import { type ErrorObject, type Options, type ValidateFunction } from 'ajv';
2
2
  import { type Type } from 'avsc';
3
+ import { type ConnectionOptions as TLSConnectionOptions } from 'node:tls';
3
4
  import { type Root } from 'protobufjs';
4
5
  import { type Callback } from '../apis/definitions.ts';
5
6
  import { type BeforeDeserializationHook, type BeforeHookPayloadType, type BeforeSerializationHook, type Deserializers, type Serializers } from '../clients/serde.ts';
@@ -7,10 +8,25 @@ import { type CredentialProvider, UserError } from '../index.ts';
7
8
  import { type MessageToConsume, type MessageToProduce } from '../protocol/records.ts';
8
9
  import { AbstractSchemaRegistry } from './abstract.ts';
9
10
  type ConfluentSchemaRegistryMessageToProduce = MessageToProduce<unknown, unknown, unknown, unknown>;
11
+ export type UndiciDispatcher = NonNullable<RequestInit['dispatcher']>;
12
+ export interface UndiciAgentOptions {
13
+ connect: TLSConnectionOptions;
14
+ }
15
+ export type UndiciAgentConstructor = new (options: UndiciAgentOptions) => UndiciDispatcher;
16
+ export declare function createUndiciAgent(options: UndiciAgentOptions): UndiciDispatcher;
10
17
  export interface ConfluentSchemaRegistryMetadata {
11
- schemas?: Record<BeforeHookPayloadType, number>;
18
+ schemas?: Partial<Record<BeforeHookPayloadType, number>>;
19
+ }
20
+ export interface ConfluentSchemaRegistryConsumedMetadata {
21
+ lengths?: {
22
+ key?: number;
23
+ value?: number;
24
+ };
12
25
  }
13
26
  export type ConfluentSchemaRegistryProtobufTypeMapper = (id: number, type: BeforeHookPayloadType, context: ConfluentSchemaRegistryMessageToProduce | MessageToConsume) => string;
27
+ export type ConfluentSchemaRegistryHeaders = Record<string, string>;
28
+ export type ConfluentSchemaRegistryHeadersProvider = () => ConfluentSchemaRegistryHeaders | Promise<ConfluentSchemaRegistryHeaders>;
29
+ export type ConfluentSchemaRegistryRetryDelayGetter = (attempt: number, retries: number, error: Error) => number;
14
30
  export interface ConfluentSchemaRegistryOptions {
15
31
  url: string;
16
32
  auth?: {
@@ -18,10 +34,22 @@ export interface ConfluentSchemaRegistryOptions {
18
34
  password?: string | CredentialProvider;
19
35
  token?: string | CredentialProvider;
20
36
  };
37
+ headers?: ConfluentSchemaRegistryHeaders | ConfluentSchemaRegistryHeadersProvider;
38
+ timeout?: number;
39
+ retries?: number;
40
+ retryDelay?: number | ConfluentSchemaRegistryRetryDelayGetter;
41
+ tls?: TLSConnectionOptions;
42
+ ssl?: TLSConnectionOptions;
21
43
  protobufTypeMapper?: ConfluentSchemaRegistryProtobufTypeMapper;
22
44
  jsonValidateSend?: boolean;
23
45
  jsonAjvOptions?: Options;
24
46
  }
47
+ export declare const defaultConfluentSchemaRegistryOptions: {
48
+ timeout: number;
49
+ retries: number;
50
+ retryDelay: number;
51
+ };
52
+ export declare const retriableSchemaRegistryStatuses: number[];
25
53
  export type SchemaValidationPhase = 'serialization' | 'deserialization';
26
54
  export interface SchemaValidationErrorProperties {
27
55
  schemaId: number;
@@ -49,6 +77,8 @@ export interface Schema {
49
77
  type: 'avro' | 'protobuf' | 'json';
50
78
  schema: Type | Root | ValidateFunction;
51
79
  }
80
+ export declare function trackConsumedSchemaId(message: MessageToConsume | undefined, type: BeforeHookPayloadType, schemaId: number): void;
81
+ export declare function trackConsumedPayloadLength(message: MessageToConsume | undefined, type: 'key' | 'value', data: Buffer): void;
52
82
  export declare function defaultProtobufTypeMapper(_: number, type: BeforeHookPayloadType, context: ConfluentSchemaRegistryMessageToProduce | MessageToConsume): string;
53
83
  export declare class ConfluentSchemaRegistry<Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> extends AbstractSchemaRegistry<number | undefined, Schema, Key, Value, HeaderKey, HeaderValue> {
54
84
  #private;
@@ -2,19 +2,74 @@ import { Ajv } from 'ajv';
2
2
  import { Ajv2020 } from 'ajv/dist/2020.js';
3
3
  import avro from 'avsc';
4
4
  import { createRequire } from 'node:module';
5
+ import { setTimeout as sleep } from 'node:timers/promises';
5
6
  import { jsonDeserializer, jsonSerializer, stringDeserializer, stringSerializer } from "../clients/serde.js";
6
- import { EMPTY_BUFFER, UnsupportedFormatError, UserError } from "../index.js";
7
+ import { EMPTY_BUFFER, MultipleErrors, NetworkError, TimeoutError, UnsupportedFormatError, UserError } from "../index.js";
7
8
  import { getCredential } from "../protocol/sasl/utils.js";
8
9
  import { AbstractSchemaRegistry } from "./abstract.js";
9
10
  const require = createRequire(import.meta.url);
10
11
  const AjvDraft04 = require('ajv-draft-04');
11
12
  const draft06MetaSchema = require('ajv/dist/refs/json-schema-draft-06.json');
13
+ const undiciGlobalDispatcherSymbol = /^undici\.globalDispatcher\.(\d+)$/;
14
+ export function createUndiciAgent(options) {
15
+ /*
16
+ The undici globals are installed the first time undici is loaded, which creating a Request is
17
+ enough to do and which performs no I/O.
18
+ */
19
+ new Request('http://localhost'); // eslint-disable-line no-new
20
+ /*
21
+ The symbol is versioned and more than one version is registered at the same time, pointing at
22
+ different objects: on Node 26 undici.globalDispatcher.1 is a backwards compatibility wrapper and
23
+ only undici.globalDispatcher.2 is the Agent. The highest version available therefore wins.
24
+ */
25
+ const globals = globalThis;
26
+ let version = -1;
27
+ let Agent;
28
+ for (const symbol of Object.getOwnPropertySymbols(globalThis)) {
29
+ const symbolVersion = symbol.description?.match(undiciGlobalDispatcherSymbol)?.[1];
30
+ if (typeof symbolVersion === 'undefined' || Number(symbolVersion) <= version) {
31
+ continue;
32
+ }
33
+ version = Number(symbolVersion);
34
+ Agent = globals[symbol]?.constructor;
35
+ }
36
+ /* c8 ignore next 6 - Only reachable on a Node.js build which does not bundle undici */
37
+ if (typeof Agent !== 'function') {
38
+ throw new UserError('Cannot access the undici Agent bundled with Node.js, which is required to configure TLS for the Schema Registry.');
39
+ }
40
+ return new Agent(options);
41
+ }
42
+ export const defaultConfluentSchemaRegistryOptions = {
43
+ timeout: 5000,
44
+ retries: 3,
45
+ retryDelay: 1000
46
+ };
47
+ // Statuses which are worth retrying: the registry is overloaded, restarting or behind a proxy which is
48
+ export const retriableSchemaRegistryStatuses = [408, 425, 429, 500, 502, 503, 504];
12
49
  export class SchemaValidationError extends UserError {
13
50
  constructor(message, properties) {
14
51
  super(message, properties);
15
52
  this.name = 'SchemaValidationError';
16
53
  }
17
54
  }
55
+ export function trackConsumedSchemaId(message, type, schemaId) {
56
+ /* c8 ignore next 3 - The message is always provided for keys and values */
57
+ if (!message) {
58
+ return;
59
+ }
60
+ const metadata = (message.metadata ??= {});
61
+ metadata.schemas ??= {};
62
+ metadata.schemas[type] = schemaId;
63
+ }
64
+ export function trackConsumedPayloadLength(message, type, data) {
65
+ /* c8 ignore next 3 - The message is always provided for keys and values */
66
+ if (!message) {
67
+ return;
68
+ }
69
+ const metadata = (message.metadata ??= {});
70
+ metadata.lengths ??= {};
71
+ metadata.lengths[type] = data.length;
72
+ }
18
73
  /* c8 ignore next 8 */
19
74
  export function defaultProtobufTypeMapper(_, type, context) {
20
75
  // Confluent Schema Registry convention
@@ -31,6 +86,12 @@ export class ConfluentSchemaRegistry extends AbstractSchemaRegistry {
31
86
  #jsonAjvDraft04;
32
87
  #pendingFetches;
33
88
  #auth;
89
+ #headers;
90
+ #timeout;
91
+ #retries;
92
+ #retryDelay;
93
+ #tls;
94
+ #tlsDispatcher;
34
95
  constructor(options) {
35
96
  super();
36
97
  this.#url = options.url;
@@ -43,7 +104,15 @@ export class ConfluentSchemaRegistry extends AbstractSchemaRegistry {
43
104
  this.#jsonAjvDraft7.addMetaSchema(draft06MetaSchema);
44
105
  this.#jsonAjvDraft04 = new AjvDraft04(jsonAjvOptions);
45
106
  this.#auth = options.auth;
107
+ this.#headers = options.headers;
108
+ this.#timeout = options.timeout ?? defaultConfluentSchemaRegistryOptions.timeout;
109
+ this.#retries = options.retries ?? defaultConfluentSchemaRegistryOptions.retries;
110
+ this.#retryDelay = options.retryDelay ?? defaultConfluentSchemaRegistryOptions.retryDelay;
111
+ this.#tls = options.tls ?? options.ssl;
46
112
  this.#pendingFetches = new Map();
113
+ if (this.#tls && new URL(this.#url).protocol !== 'https:') {
114
+ throw new UserError('TLS options can only be used with a HTTPS Schema Registry URL.', { url: this.#url });
115
+ }
47
116
  }
48
117
  getSchemaId(message, type) {
49
118
  if (message === null) {
@@ -112,7 +181,7 @@ export class ConfluentSchemaRegistry extends AbstractSchemaRegistry {
112
181
  }
113
182
  getBeforeDeserializationHook() {
114
183
  const registry = this;
115
- return function beforeDeserialization(payload, type, _message, callback) {
184
+ return function beforeDeserialization(payload, type, message, callback) {
116
185
  // Extract the schema ID from the message metadata
117
186
  const schemaId = registry.getSchemaId(payload, type);
118
187
  // When no schema ID is found, nothing to do
@@ -120,6 +189,11 @@ export class ConfluentSchemaRegistry extends AbstractSchemaRegistry {
120
189
  callback(null);
121
190
  return;
122
191
  }
192
+ /*
193
+ Track the schema ID before fetching, so that it is exposed on the message metadata even when
194
+ the schema cannot be fetched or the payload cannot be decoded.
195
+ */
196
+ trackConsumedSchemaId(message, type, schemaId);
123
197
  // The schema is already fetch
124
198
  if (registry.get(schemaId)) {
125
199
  callback(null);
@@ -128,28 +202,86 @@ export class ConfluentSchemaRegistry extends AbstractSchemaRegistry {
128
202
  registry.fetchSchema(schemaId, callback);
129
203
  };
130
204
  }
131
- async #fetchSchema(id) {
132
- const requestInit = {};
205
+ async #requestHeaders() {
206
+ let headers;
207
+ if (this.#headers) {
208
+ headers = { ...(typeof this.#headers === 'function' ? await this.#headers() : this.#headers) };
209
+ }
210
+ // Authentication headers are applied last so that auth options always win over custom headers
133
211
  if (this.#auth) {
212
+ headers ??= {};
134
213
  if (this.#auth.token) {
135
214
  const token = await getCredential('token', this.#auth.token);
136
- requestInit.headers = {
137
- Authorization: `Bearer ${token}`
138
- };
215
+ headers.Authorization = `Bearer ${token}`;
139
216
  }
140
217
  else {
141
218
  const username = await getCredential('username', this.#auth.username);
142
219
  const password = await getCredential('password', this.#auth.password);
143
- requestInit.headers = {
144
- Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
145
- };
220
+ headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
221
+ }
222
+ }
223
+ return headers;
224
+ }
225
+ async #requestSchema(id) {
226
+ const requestInit = {};
227
+ const headers = await this.#requestHeaders();
228
+ if (headers) {
229
+ requestInit.headers = headers;
230
+ }
231
+ if (this.#tls) {
232
+ // Created once per registry, so that connections are pooled across schema fetches
233
+ this.#tlsDispatcher ??= createUndiciAgent({ connect: this.#tls });
234
+ requestInit.dispatcher = this.#tlsDispatcher;
235
+ }
236
+ if (this.#timeout > 0) {
237
+ requestInit.signal = AbortSignal.timeout(this.#timeout);
238
+ }
239
+ let response;
240
+ try {
241
+ response = await fetch(`${this.#url}/schemas/ids/${id}`, requestInit);
242
+ }
243
+ catch (error) {
244
+ if (error.name === 'TimeoutError') {
245
+ throw new TimeoutError(`Fetching a schema timed out after ${this.#timeout} ms.`, {
246
+ canRetry: true,
247
+ cause: error
248
+ });
146
249
  }
250
+ throw new NetworkError('Failed to fetch a schema.', { cause: error });
147
251
  }
148
- const response = await fetch(`${this.#url}/schemas/ids/${id}`, requestInit);
149
252
  if (!response.ok) {
150
- throw new UserError(`Failed to fetch a schema: [HTTP ${response.status}]`, { response: await response.text() });
253
+ throw new UserError(`Failed to fetch a schema: [HTTP ${response.status}]`, {
254
+ response: await response.text(),
255
+ canRetry: retriableSchemaRegistryStatuses.includes(response.status)
256
+ });
151
257
  }
152
- const responseBody = (await response.json());
258
+ return (await response.json());
259
+ }
260
+ async #requestSchemaWithRetries(id) {
261
+ const errors = [];
262
+ for (let attempt = 0;; attempt++) {
263
+ try {
264
+ return await this.#requestSchema(id);
265
+ }
266
+ catch (error) {
267
+ errors.push(error);
268
+ if (attempt >= this.#retries || error.canRetry !== true) {
269
+ if (errors.length === 1) {
270
+ throw errors[0];
271
+ }
272
+ throw new MultipleErrors(`Failed to fetch a schema after ${errors.length} attempts.`, errors);
273
+ }
274
+ const delay = typeof this.#retryDelay === 'function'
275
+ ? this.#retryDelay(attempt + 1, this.#retries, error)
276
+ : this.#retryDelay;
277
+ if (delay > 0) {
278
+ await sleep(delay);
279
+ }
280
+ }
281
+ }
282
+ }
283
+ async #fetchSchema(id) {
284
+ const responseBody = await this.#requestSchemaWithRetries(id);
153
285
  const { schema } = responseBody;
154
286
  const schemaType = responseBody.schemaType ?? 'AVRO';
155
287
  switch (schemaType) {
@@ -232,17 +364,32 @@ export class ConfluentSchemaRegistry extends AbstractSchemaRegistry {
232
364
  return Buffer.concat([header, encodedMessage]);
233
365
  }
234
366
  #schemaDeserializer(type, fallbackDeserializer, data, headers, message) {
367
+ /*
368
+ Deserializing replaces the original payload, so record its size beforehand: it is the only
369
+ chance callers have to account for the actual ingress size of the message.
370
+ */
371
+ if ((type === 'key' || type === 'value') && Buffer.isBuffer(data)) {
372
+ trackConsumedPayloadLength(message, type, data);
373
+ }
235
374
  /* c8 ignore next 3 - Hard to test */
236
375
  if (typeof data === 'undefined' || data.length === 0) {
237
376
  return EMPTY_BUFFER;
238
377
  }
239
- if (type === 'headerKey' || type === 'headerValue') {
378
+ const isHeader = type === 'headerKey' || type === 'headerValue';
379
+ if (isHeader) {
240
380
  message = headers;
241
381
  }
242
382
  const schemaId = this.getSchemaId(data, type);
243
383
  if (!schemaId) {
244
384
  return fallbackDeserializer(data);
245
385
  }
386
+ /*
387
+ Also track the schema ID here, so that it is exposed on the message metadata even when the
388
+ deserializers are used without the before-deserialization hook of this registry.
389
+ */
390
+ if (!isHeader) {
391
+ trackConsumedSchemaId(message, type, schemaId);
392
+ }
246
393
  const schema = this.#schemas.get(schemaId);
247
394
  if (!schema) {
248
395
  throw new UserError(`Schema with ID ${schemaId} not found.`, { missingSchema: schemaId });
@@ -35,6 +35,7 @@ export interface PreferredReadReplica {
35
35
  export declare const DeserializationErrorActions: {
36
36
  readonly FAIL: "fail";
37
37
  readonly SKIP: "skip";
38
+ readonly CONTINUE: "continue";
38
39
  };
39
40
  export type DeserializationErrorAction = (typeof DeserializationErrorActions)[keyof typeof DeserializationErrorActions];
40
41
  export interface DeserializationErrorContext {
@@ -47,6 +48,10 @@ export interface DeserializationErrorContext {
47
48
  timestamp: bigint;
48
49
  commit: Message['commit'];
49
50
  }
51
+ export interface MessageDeserializationError {
52
+ error: unknown;
53
+ payloadType: BeforeHookPayloadType;
54
+ }
50
55
  export type DeserializationErrorHandler = (context: DeserializationErrorContext) => DeserializationErrorAction;
51
56
  /**
52
57
  * @deprecated Use `DeserializationErrorHandler` instead.
@@ -34,6 +34,7 @@ export declare class MultipleErrors extends AggregateError {
34
34
  findBy<ErrorType extends GenericError | MultipleErrors = MultipleErrors>(property: string, value: unknown): ErrorType | null;
35
35
  toString(): string;
36
36
  }
37
+ export declare function findErrorBy<ErrorType extends GenericError = GenericError>(error: Error | null | undefined, property: string, value: unknown): ErrorType | null;
37
38
  export * from "./protocol/errors";
38
39
  export declare class AuthenticationError extends GenericError {
39
40
  static code: ErrorCode;
@@ -71,6 +71,7 @@ export interface KafkaRecord {
71
71
  export interface MessageToConsume extends KafkaRecord {
72
72
  topic: string;
73
73
  partition: number;
74
+ metadata?: Record<string, unknown>;
74
75
  }
75
76
  export interface RecordsBatch {
76
77
  firstOffset: bigint;
@@ -1,5 +1,6 @@
1
1
  import { type ErrorObject, type Options, type ValidateFunction } from 'ajv';
2
2
  import { type Type } from 'avsc';
3
+ import { type ConnectionOptions as TLSConnectionOptions } from 'node:tls';
3
4
  import { type Root } from 'protobufjs';
4
5
  import { type Callback } from "../apis/definitions";
5
6
  import { type BeforeDeserializationHook, type BeforeHookPayloadType, type BeforeSerializationHook, type Deserializers, type Serializers } from "../clients/serde";
@@ -7,10 +8,25 @@ import { type CredentialProvider, UserError } from "../index";
7
8
  import { type MessageToConsume, type MessageToProduce } from "../protocol/records";
8
9
  import { AbstractSchemaRegistry } from "./abstract";
9
10
  type ConfluentSchemaRegistryMessageToProduce = MessageToProduce<unknown, unknown, unknown, unknown>;
11
+ export type UndiciDispatcher = NonNullable<RequestInit['dispatcher']>;
12
+ export interface UndiciAgentOptions {
13
+ connect: TLSConnectionOptions;
14
+ }
15
+ export type UndiciAgentConstructor = new (options: UndiciAgentOptions) => UndiciDispatcher;
16
+ export declare function createUndiciAgent(options: UndiciAgentOptions): UndiciDispatcher;
10
17
  export interface ConfluentSchemaRegistryMetadata {
11
- schemas?: Record<BeforeHookPayloadType, number>;
18
+ schemas?: Partial<Record<BeforeHookPayloadType, number>>;
19
+ }
20
+ export interface ConfluentSchemaRegistryConsumedMetadata {
21
+ lengths?: {
22
+ key?: number;
23
+ value?: number;
24
+ };
12
25
  }
13
26
  export type ConfluentSchemaRegistryProtobufTypeMapper = (id: number, type: BeforeHookPayloadType, context: ConfluentSchemaRegistryMessageToProduce | MessageToConsume) => string;
27
+ export type ConfluentSchemaRegistryHeaders = Record<string, string>;
28
+ export type ConfluentSchemaRegistryHeadersProvider = () => ConfluentSchemaRegistryHeaders | Promise<ConfluentSchemaRegistryHeaders>;
29
+ export type ConfluentSchemaRegistryRetryDelayGetter = (attempt: number, retries: number, error: Error) => number;
14
30
  export interface ConfluentSchemaRegistryOptions {
15
31
  url: string;
16
32
  auth?: {
@@ -18,10 +34,22 @@ export interface ConfluentSchemaRegistryOptions {
18
34
  password?: string | CredentialProvider;
19
35
  token?: string | CredentialProvider;
20
36
  };
37
+ headers?: ConfluentSchemaRegistryHeaders | ConfluentSchemaRegistryHeadersProvider;
38
+ timeout?: number;
39
+ retries?: number;
40
+ retryDelay?: number | ConfluentSchemaRegistryRetryDelayGetter;
41
+ tls?: TLSConnectionOptions;
42
+ ssl?: TLSConnectionOptions;
21
43
  protobufTypeMapper?: ConfluentSchemaRegistryProtobufTypeMapper;
22
44
  jsonValidateSend?: boolean;
23
45
  jsonAjvOptions?: Options;
24
46
  }
47
+ export declare const defaultConfluentSchemaRegistryOptions: {
48
+ timeout: number;
49
+ retries: number;
50
+ retryDelay: number;
51
+ };
52
+ export declare const retriableSchemaRegistryStatuses: number[];
25
53
  export type SchemaValidationPhase = 'serialization' | 'deserialization';
26
54
  export interface SchemaValidationErrorProperties {
27
55
  schemaId: number;
@@ -49,6 +77,8 @@ export interface Schema {
49
77
  type: 'avro' | 'protobuf' | 'json';
50
78
  schema: Type | Root | ValidateFunction;
51
79
  }
80
+ export declare function trackConsumedSchemaId(message: MessageToConsume | undefined, type: BeforeHookPayloadType, schemaId: number): void;
81
+ export declare function trackConsumedPayloadLength(message: MessageToConsume | undefined, type: 'key' | 'value', data: Buffer): void;
52
82
  export declare function defaultProtobufTypeMapper(_: number, type: BeforeHookPayloadType, context: ConfluentSchemaRegistryMessageToProduce | MessageToConsume): string;
53
83
  export declare class ConfluentSchemaRegistry<Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> extends AbstractSchemaRegistry<number | undefined, Schema, Key, Value, HeaderKey, HeaderValue> {
54
84
  #private;
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export const name = "@platformatic/kafka";
2
- export const version = "2.8.0";
2
+ export const version = "2.9.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/kafka",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "Modern and performant client for Apache Kafka",
5
5
  "homepage": "https://github.com/platformatic/kafka",
6
6
  "author": "Platformatic Inc. <oss@platformatic.dev> (https://platformatic.dev)",