@platformatic/kafka 2.9.0-alpha.1 → 2.10.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apis/enumerations.d.ts +1 -1
- package/dist/clients/admin/admin.js +2 -2
- package/dist/clients/admin/options.d.ts +3 -3
- 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/reader.js +6 -2
- 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/apis/enumerations.d.ts +1 -1
- package/dist/typescript-4/dist/clients/admin/options.d.ts +3 -3
- 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
|
@@ -105,7 +105,7 @@ export declare const AclOperations: {
|
|
|
105
105
|
readonly DESCRIBE_TOKENS: 14;
|
|
106
106
|
readonly TWO_PHASE_COMMIT: 15;
|
|
107
107
|
};
|
|
108
|
-
export declare const allowedAclOperations: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 3 | 5 | 6 | 9 | 11 |
|
|
108
|
+
export declare const allowedAclOperations: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 12 | 3 | 5 | 6 | 9 | 11 | 13 | 14 | 15)[];
|
|
109
109
|
export type AclOperationLabel = keyof typeof AclOperations;
|
|
110
110
|
export type AclOperationValue = (typeof AclOperations)[AclOperationLabel];
|
|
111
111
|
/** @deprecated Use AclOperationLabel */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../../apis/callbacks.js";
|
|
2
2
|
import { ConfigResourceTypes, FetchIsolationLevels, FindCoordinatorKeyTypes, legacyConsumerGroupStates } 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) {
|
|
@@ -708,7 +708,7 @@ export declare const createAclsOptionsSchema: {
|
|
|
708
708
|
};
|
|
709
709
|
operation: {
|
|
710
710
|
type: string;
|
|
711
|
-
enum: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 3 | 5 | 6 | 9 | 11 |
|
|
711
|
+
enum: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 12 | 3 | 5 | 6 | 9 | 11 | 13 | 14 | 15)[];
|
|
712
712
|
};
|
|
713
713
|
permissionType: {
|
|
714
714
|
type: string;
|
|
@@ -767,7 +767,7 @@ export declare const describeAclsOptionsSchema: {
|
|
|
767
767
|
};
|
|
768
768
|
operation: {
|
|
769
769
|
type: string;
|
|
770
|
-
enum: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 3 | 5 | 6 | 9 | 11 |
|
|
770
|
+
enum: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 12 | 3 | 5 | 6 | 9 | 11 | 13 | 14 | 15)[];
|
|
771
771
|
};
|
|
772
772
|
permissionType: {
|
|
773
773
|
type: string;
|
|
@@ -826,7 +826,7 @@ export declare const deleteAclsOptionsSchema: {
|
|
|
826
826
|
};
|
|
827
827
|
operation: {
|
|
828
828
|
type: string;
|
|
829
|
-
enum: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 3 | 5 | 6 | 9 | 11 |
|
|
829
|
+
enum: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 12 | 3 | 5 | 6 | 9 | 11 | 13 | 14 | 15)[];
|
|
830
830
|
};
|
|
831
831
|
permissionType: {
|
|
832
832
|
type: string;
|
|
@@ -2,7 +2,7 @@ import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } f
|
|
|
2
2
|
import * as apis from "../../apis/index.js";
|
|
3
3
|
import { api as apiVersionsV1 } from "../../apis/metadata/api-versions-v1.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";
|
|
@@ -235,7 +235,12 @@ export class Base extends TypedEventEmitter {
|
|
|
235
235
|
}
|
|
236
236
|
[kCheckNotClosed](callback) {
|
|
237
237
|
if (this[kClosed]) {
|
|
238
|
-
|
|
238
|
+
// A closed client never reopens, so this must not be retried.
|
|
239
|
+
const error = new NetworkError('Client is closed.', {
|
|
240
|
+
canRetry: false,
|
|
241
|
+
closed: true,
|
|
242
|
+
instance: this[kInstance]
|
|
243
|
+
});
|
|
239
244
|
callback(error, undefined);
|
|
240
245
|
return true;
|
|
241
246
|
}
|
|
@@ -249,9 +254,8 @@ export class Base extends TypedEventEmitter {
|
|
|
249
254
|
this.emitWithDebug('client', 'performWithRetry', operationId, attempt, retries);
|
|
250
255
|
operation((error, result) => {
|
|
251
256
|
if (error) {
|
|
252
|
-
const genericError = error;
|
|
253
257
|
// Only retry if all the errors in the chain are retriable
|
|
254
|
-
const retriable = !
|
|
258
|
+
const retriable = !findErrorBy(error, 'canRetry', false);
|
|
255
259
|
errors.push(error);
|
|
256
260
|
if (attempt < retries && retriable && !shouldSkipRetry?.(error)) {
|
|
257
261
|
function onClose() {
|
|
@@ -445,7 +449,7 @@ export class Base extends TypedEventEmitter {
|
|
|
445
449
|
}, attempt);
|
|
446
450
|
}, (error, metadata) => {
|
|
447
451
|
if (error) {
|
|
448
|
-
const unknownTopicError = error
|
|
452
|
+
const unknownTopicError = findErrorBy(error, 'apiCode', 3);
|
|
449
453
|
if (unknownTopicError) {
|
|
450
454
|
const topicIndexMatch = unknownTopicError.path?.match(/\/topics\/(\d+)/);
|
|
451
455
|
/* c8 ignore next 3 - Hard to test */
|
|
@@ -454,7 +458,7 @@ export class Base extends TypedEventEmitter {
|
|
|
454
458
|
deduplicateCallback(new UserError(`Unknown topic ${topicName}.`));
|
|
455
459
|
return;
|
|
456
460
|
}
|
|
457
|
-
const hasStaleMetadata = error
|
|
461
|
+
const hasStaleMetadata = findErrorBy(error, 'hasStaleMetadata', true);
|
|
458
462
|
// Stale metadata, we need to fetch everything again
|
|
459
463
|
/* c8 ignore next 4 - Hard to test */
|
|
460
464
|
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";
|
|
@@ -609,13 +609,12 @@ export class Consumer extends Base {
|
|
|
609
609
|
}
|
|
610
610
|
if (error) {
|
|
611
611
|
this.#clearPreferredReadReplicas(options.topics, topicIds);
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
genericError.findBy?.('apiId', 'FETCH_SESSION_TOPIC_ID_ERROR')) {
|
|
612
|
+
if (findErrorBy(error, 'apiId', 'FETCH_SESSION_ID_NOT_FOUND') ||
|
|
613
|
+
findErrorBy(error, 'apiId', 'INVALID_FETCH_SESSION_EPOCH') ||
|
|
614
|
+
findErrorBy(error, 'apiId', 'FETCH_SESSION_TOPIC_ID_ERROR')) {
|
|
616
615
|
this.#fetchSessions.delete(node);
|
|
617
616
|
}
|
|
618
|
-
if (
|
|
617
|
+
if (findErrorBy(error, 'apiId', 'FENCED_LEADER_EPOCH')) {
|
|
619
618
|
this.#fetchSessions.delete(node);
|
|
620
619
|
this.clearMetadata();
|
|
621
620
|
for (const topic of options.topics) {
|
|
@@ -830,7 +829,7 @@ export class Consumer extends Base {
|
|
|
830
829
|
if (error) {
|
|
831
830
|
if (this.#useConsumerGroupProtocol &&
|
|
832
831
|
staleMemberEpochRetries < this[kOptions].retries &&
|
|
833
|
-
error
|
|
832
|
+
findErrorBy(error, 'apiId', 'STALE_MEMBER_EPOCH')) {
|
|
834
833
|
this.#consumerGroupHeartbeat(this[kOptions], heartbeatError => {
|
|
835
834
|
if (heartbeatError) {
|
|
836
835
|
callback(this.#handleError(heartbeatError));
|
|
@@ -1080,7 +1079,35 @@ export class Consumer extends Base {
|
|
|
1080
1079
|
callback(err);
|
|
1081
1080
|
});
|
|
1082
1081
|
}
|
|
1083
|
-
#leaveGroupConsumerProtocol(
|
|
1082
|
+
#leaveGroupConsumerProtocol(force, callback) {
|
|
1083
|
+
// Remove streams that might have been exited in the meanwhile
|
|
1084
|
+
for (const stream of this.#streams) {
|
|
1085
|
+
if (stream.closed || stream.destroyed) {
|
|
1086
|
+
this.#streams.delete(stream);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
// Active streams must be closed before leaving the group, exactly like in the classic protocol.
|
|
1090
|
+
// Their fetch loop keeps running until they are closed and, since Consumer.close marks the client
|
|
1091
|
+
// as closed before leaving, any fetch scheduled in the meanwhile would fail with "Client is closed."
|
|
1092
|
+
// and destroy the stream with an error the user never asked for.
|
|
1093
|
+
if (this.#streams.size) {
|
|
1094
|
+
if (!force) {
|
|
1095
|
+
callback(new UserError('Cannot leave group while consuming messages.'));
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
runConcurrentCallbacks('Closing streams failed.', this.#streams, (stream, concurrentCallback) => {
|
|
1099
|
+
stream.close(concurrentCallback);
|
|
1100
|
+
}, error => {
|
|
1101
|
+
/* c8 ignore next 4 - Hard to test */
|
|
1102
|
+
if (error) {
|
|
1103
|
+
callback(error);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
// All streams are closed, try the operation again without force
|
|
1107
|
+
this.#leaveGroupConsumerProtocol(false, callback);
|
|
1108
|
+
});
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1084
1111
|
// Leave by sending a heartbeat with memberEpoch = -1
|
|
1085
1112
|
this.#cancelHeartbeat();
|
|
1086
1113
|
this.#performDeduplicateGroupOperaton('leaveGroupConsumerProtocol', (connection, groupCallback) => {
|
|
@@ -1304,7 +1331,7 @@ export class Consumer extends Base {
|
|
|
1304
1331
|
});
|
|
1305
1332
|
}, error => {
|
|
1306
1333
|
if (error) {
|
|
1307
|
-
const unknownMemberError = error
|
|
1334
|
+
const unknownMemberError = findErrorBy(error, 'unknownMemberId', true);
|
|
1308
1335
|
// This is to avoid throwing an error if a group join was cancelled.
|
|
1309
1336
|
if (!unknownMemberError) {
|
|
1310
1337
|
callback(error);
|
|
@@ -1586,7 +1613,7 @@ export class Consumer extends Base {
|
|
|
1586
1613
|
}
|
|
1587
1614
|
}
|
|
1588
1615
|
#getRejoinError(error) {
|
|
1589
|
-
const protocolError = error
|
|
1616
|
+
const protocolError = findErrorBy(error, 'needsRejoin', true);
|
|
1590
1617
|
if (!protocolError) {
|
|
1591
1618
|
return null;
|
|
1592
1619
|
}
|
|
@@ -1672,17 +1699,14 @@ export class Consumer extends Base {
|
|
|
1672
1699
|
}
|
|
1673
1700
|
}
|
|
1674
1701
|
#handleError(error) {
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
kafkaError.findBy('apiId', 'NOT_COORDINATOR')) {
|
|
1684
|
-
this.#coordinatorId = null;
|
|
1685
|
-
}
|
|
1702
|
+
if (findErrorBy(error, 'hasStaleMetadata', true)) {
|
|
1703
|
+
this.clearMetadata();
|
|
1704
|
+
}
|
|
1705
|
+
else if (findErrorBy(error, 'code', 'PLT_KFK_NETWORK') ||
|
|
1706
|
+
findErrorBy(error, 'apiId', 'COORDINATOR_LOAD_IN_PROGRESS') ||
|
|
1707
|
+
findErrorBy(error, 'apiId', 'COORDINATOR_NOT_AVAILABLE') ||
|
|
1708
|
+
findErrorBy(error, 'apiId', 'NOT_COORDINATOR')) {
|
|
1709
|
+
this.#coordinatorId = null;
|
|
1686
1710
|
}
|
|
1687
1711
|
return error;
|
|
1688
1712
|
}
|
|
@@ -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";
|
|
@@ -775,11 +775,8 @@ export class Producer extends Base {
|
|
|
775
775
|
if (error) {
|
|
776
776
|
// If the last error was due to stale metadata, we retry the operation with this set of messages
|
|
777
777
|
// since the partition is already set, it should attempt on the new destination
|
|
778
|
-
/* c8 ignore next
|
|
779
|
-
const
|
|
780
|
-
? error
|
|
781
|
-
: null;
|
|
782
|
-
const hasStaleMetadata = kafkaError?.findBy('hasStaleMetadata', true);
|
|
778
|
+
/* c8 ignore next - Hard to test */
|
|
779
|
+
const hasStaleMetadata = findErrorBy(error, 'hasStaleMetadata', true);
|
|
783
780
|
if (hasStaleMetadata && repeatOnStaleMetadata) {
|
|
784
781
|
this.clearMetadata();
|
|
785
782
|
this.#performSingleDestinationSend(topics, messages, timeout, acks, autocreateTopics, false, produceOptions, recoverOnProducerStateLoss, callback);
|
|
@@ -801,17 +798,16 @@ export class Producer extends Base {
|
|
|
801
798
|
callback(error, results);
|
|
802
799
|
}, 0, [], error => {
|
|
803
800
|
/* c8 ignore next 3 - Hard to test */
|
|
804
|
-
if (!repeatOnStaleMetadata
|
|
801
|
+
if (!repeatOnStaleMetadata) {
|
|
805
802
|
return false;
|
|
806
803
|
}
|
|
807
|
-
return !!error
|
|
804
|
+
return !!findErrorBy(error, 'hasStaleMetadata', true);
|
|
808
805
|
});
|
|
809
806
|
});
|
|
810
807
|
}
|
|
811
808
|
#isProducerStateLost(error) {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
(kafkaError.findBy('apiId', 'UNKNOWN_PRODUCER_ID') || kafkaError.findBy('apiId', 'OUT_OF_ORDER_SEQUENCE_NUMBER')));
|
|
809
|
+
return !!(findErrorBy(error, 'apiId', 'UNKNOWN_PRODUCER_ID') ||
|
|
810
|
+
findErrorBy(error, 'apiId', 'OUT_OF_ORDER_SEQUENCE_NUMBER'));
|
|
815
811
|
}
|
|
816
812
|
#recoverIdempotentProducer(messages, produceOptions, callback) {
|
|
817
813
|
this.#producerInfo = undefined;
|
|
@@ -864,20 +860,17 @@ export class Producer extends Base {
|
|
|
864
860
|
});
|
|
865
861
|
}
|
|
866
862
|
#handleFencingError(error) {
|
|
867
|
-
if (error
|
|
863
|
+
if (findErrorBy(error, 'producerFenced', true)) {
|
|
868
864
|
this.#producerInfo = undefined;
|
|
869
865
|
this.#transaction = undefined;
|
|
870
866
|
}
|
|
871
867
|
}
|
|
872
868
|
#handleError(error) {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
kafkaError.findBy('apiId', 'NOT_COORDINATOR')) {
|
|
879
|
-
this.#coordinatorId = undefined;
|
|
880
|
-
}
|
|
869
|
+
if (findErrorBy(error, 'code', 'PLT_KFK_NETWORK') ||
|
|
870
|
+
findErrorBy(error, 'apiId', 'COORDINATOR_LOAD_IN_PROGRESS') ||
|
|
871
|
+
findErrorBy(error, 'apiId', 'COORDINATOR_NOT_AVAILABLE') ||
|
|
872
|
+
findErrorBy(error, 'apiId', 'NOT_COORDINATOR')) {
|
|
873
|
+
this.#coordinatorId = undefined;
|
|
881
874
|
}
|
|
882
875
|
return error;
|
|
883
876
|
}
|
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) {
|
package/dist/protocol/reader.js
CHANGED
|
@@ -187,9 +187,13 @@ export class Reader {
|
|
|
187
187
|
return this.readNullableString(compact, encoding) || '';
|
|
188
188
|
}
|
|
189
189
|
readUUID() {
|
|
190
|
-
const
|
|
190
|
+
const hex = this.peekUUID();
|
|
191
191
|
this.position += UUID_SIZE;
|
|
192
|
-
|
|
192
|
+
// Hyphenated by slicing rather than by a capture group regex. This runs once per topic per
|
|
193
|
+
// Fetch response from v13 up, and the regex form measured about fourteen times the cost, which
|
|
194
|
+
// made the newest Fetch versions measurably slower to decode than the ones identifying topics
|
|
195
|
+
// by name.
|
|
196
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
193
197
|
}
|
|
194
198
|
readNullableBytes(compact = true) {
|
|
195
199
|
let length;
|
|
@@ -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 });
|
|
@@ -105,7 +105,7 @@ export declare const AclOperations: {
|
|
|
105
105
|
readonly DESCRIBE_TOKENS: 14;
|
|
106
106
|
readonly TWO_PHASE_COMMIT: 15;
|
|
107
107
|
};
|
|
108
|
-
export declare const allowedAclOperations: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 3 | 5 | 6 | 9 | 11 |
|
|
108
|
+
export declare const allowedAclOperations: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 12 | 3 | 5 | 6 | 9 | 11 | 13 | 14 | 15)[];
|
|
109
109
|
export type AclOperationLabel = keyof typeof AclOperations;
|
|
110
110
|
export type AclOperationValue = (typeof AclOperations)[AclOperationLabel];
|
|
111
111
|
/** @deprecated Use AclOperationLabel */
|
|
@@ -708,7 +708,7 @@ export declare const createAclsOptionsSchema: {
|
|
|
708
708
|
};
|
|
709
709
|
operation: {
|
|
710
710
|
type: string;
|
|
711
|
-
enum: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 3 | 5 | 6 | 9 | 11 |
|
|
711
|
+
enum: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 12 | 3 | 5 | 6 | 9 | 11 | 13 | 14 | 15)[];
|
|
712
712
|
};
|
|
713
713
|
permissionType: {
|
|
714
714
|
type: string;
|
|
@@ -767,7 +767,7 @@ export declare const describeAclsOptionsSchema: {
|
|
|
767
767
|
};
|
|
768
768
|
operation: {
|
|
769
769
|
type: string;
|
|
770
|
-
enum: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 3 | 5 | 6 | 9 | 11 |
|
|
770
|
+
enum: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 12 | 3 | 5 | 6 | 9 | 11 | 13 | 14 | 15)[];
|
|
771
771
|
};
|
|
772
772
|
permissionType: {
|
|
773
773
|
type: string;
|
|
@@ -826,7 +826,7 @@ export declare const deleteAclsOptionsSchema: {
|
|
|
826
826
|
};
|
|
827
827
|
operation: {
|
|
828
828
|
type: string;
|
|
829
|
-
enum: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 3 | 5 | 6 | 9 | 11 |
|
|
829
|
+
enum: (0 | 1 | 2 | 4 | 8 | 7 | 10 | 12 | 3 | 5 | 6 | 9 | 11 | 13 | 14 | 15)[];
|
|
830
830
|
};
|
|
831
831
|
permissionType: {
|
|
832
832
|
type: string;
|
|
@@ -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.10.0-alpha.1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/kafka",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0-alpha.1",
|
|
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)",
|