@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.
- package/dist/clients/admin/admin.js +2 -2
- package/dist/clients/base/base.js +10 -6
- package/dist/clients/consumer/consumer.js +45 -21
- package/dist/clients/consumer/messages-stream.js +45 -9
- package/dist/clients/consumer/types.d.ts +5 -0
- package/dist/clients/consumer/types.js +2 -1
- package/dist/clients/producer/producer.js +13 -20
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +12 -0
- package/dist/network/connection-pool.js +3 -1
- package/dist/protocol/records.d.ts +1 -0
- package/dist/registries/confluent-schema-registry.d.ts +31 -1
- package/dist/registries/confluent-schema-registry.js +161 -14
- package/dist/typescript-4/dist/clients/consumer/types.d.ts +5 -0
- package/dist/typescript-4/dist/errors.d.ts +1 -0
- package/dist/typescript-4/dist/protocol/records.d.ts +1 -0
- package/dist/typescript-4/dist/registries/confluent-schema-registry.d.ts +31 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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 = !
|
|
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
|
|
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
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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 (
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
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
|
|
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
|
|
644
|
-
? this.#deserializationErrorHandler(context)
|
|
645
|
-
: this.#corruptedMessageHandler(record, topic, partition, firstTimestamp, firstOffset, commit, error)
|
|
646
|
-
|
|
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
|
|
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
|
|
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.
|
|
@@ -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 {
|
|
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
|
|
774
|
-
const
|
|
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
|
|
796
|
+
if (!repeatOnStaleMetadata) {
|
|
800
797
|
return false;
|
|
801
798
|
}
|
|
802
|
-
return !!error
|
|
799
|
+
return !!findErrorBy(error, 'hasStaleMetadata', true);
|
|
803
800
|
});
|
|
804
801
|
});
|
|
805
802
|
}
|
|
806
803
|
#isProducerStateLost(error) {
|
|
807
|
-
|
|
808
|
-
|
|
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
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -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,
|
|
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 #
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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}]`, {
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -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.
|
|
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.
|
|
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)",
|