@platformatic/kafka 1.7.0 → 1.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/apis/callbacks.js +3 -0
- package/dist/apis/enumerations.d.ts +1 -1
- package/dist/apis/enumerations.js +1 -1
- package/dist/clients/admin/admin.js +7 -4
- package/dist/clients/admin/types.d.ts +2 -0
- package/dist/clients/base/base.d.ts +4 -0
- package/dist/clients/base/base.js +112 -19
- package/dist/clients/base/options.d.ts +4 -1
- package/dist/clients/base/options.js +6 -6
- package/dist/clients/base/types.d.ts +1 -0
- package/dist/clients/consumer/consumer.d.ts +2 -0
- package/dist/clients/consumer/consumer.js +52 -21
- package/dist/clients/consumer/messages-stream.d.ts +2 -0
- package/dist/clients/consumer/messages-stream.js +32 -3
- package/dist/clients/consumer/options.d.ts +5 -0
- package/dist/clients/consumer/options.js +1 -0
- package/dist/clients/consumer/types.d.ts +1 -0
- package/dist/clients/producer/producer.js +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +7 -2
- package/dist/network/connection-pool.d.ts +2 -0
- package/dist/network/connection-pool.js +26 -1
- package/dist/network/connection.d.ts +4 -2
- package/dist/network/connection.js +8 -2
- package/dist/protocol/crc32c.d.ts +1 -1
- package/dist/protocol/crc32c.js +7 -11
- package/dist/protocol/index.d.ts +1 -0
- package/dist/protocol/index.js +1 -0
- package/dist/protocol/sasl/oauth-bearer.d.ts +5 -0
- package/dist/protocol/sasl/oauth-bearer.js +8 -0
- package/dist/protocol/writer.js +2 -2
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +5 -1
package/dist/apis/callbacks.js
CHANGED
|
@@ -37,6 +37,9 @@ export function runConcurrentCallbacks(errorMessage, collection, operation, call
|
|
|
37
37
|
callback(hasErrors ? new MultipleErrors(errorMessage, errors) : null, results);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
if (remaining === 0) {
|
|
41
|
+
callback(null, results);
|
|
42
|
+
}
|
|
40
43
|
for (const item of collection) {
|
|
41
44
|
operation(item, operationCallback.bind(null, i++));
|
|
42
45
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const SASLMechanisms: readonly ["PLAIN", "SCRAM-SHA-256", "SCRAM-SHA-512"];
|
|
1
|
+
export declare const SASLMechanisms: readonly ["PLAIN", "SCRAM-SHA-256", "SCRAM-SHA-512", "OAUTHBEARER"];
|
|
2
2
|
export type SASLMechanism = (typeof SASLMechanisms)[number];
|
|
3
3
|
export declare const FindCoordinatorKeyTypes: {
|
|
4
4
|
readonly GROUP: 0;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SASL Authentication
|
|
2
|
-
export const SASLMechanisms = ['PLAIN', 'SCRAM-SHA-256', 'SCRAM-SHA-512'];
|
|
2
|
+
export const SASLMechanisms = ['PLAIN', 'SCRAM-SHA-256', 'SCRAM-SHA-512', 'OAUTHBEARER'];
|
|
3
3
|
// Metadata API
|
|
4
4
|
// ./metadata/find-coordinator.ts
|
|
5
5
|
export const FindCoordinatorKeyTypes = { GROUP: 0, TRANSACTION: 1, SHARE: 2 };
|
|
@@ -144,6 +144,7 @@ export class Admin extends Base {
|
|
|
144
144
|
const numPartitions = options.partitions ?? 1;
|
|
145
145
|
const replicationFactor = options.replicas ?? 1;
|
|
146
146
|
const assignments = [];
|
|
147
|
+
const configs = options.configs ?? [];
|
|
147
148
|
for (const { partition, brokers } of options.assignments ?? []) {
|
|
148
149
|
assignments.push({ partitionIndex: partition, brokerIds: brokers });
|
|
149
150
|
}
|
|
@@ -154,7 +155,7 @@ export class Admin extends Base {
|
|
|
154
155
|
numPartitions,
|
|
155
156
|
replicationFactor,
|
|
156
157
|
assignments,
|
|
157
|
-
configs
|
|
158
|
+
configs
|
|
158
159
|
});
|
|
159
160
|
}
|
|
160
161
|
this[kPerformDeduplicated]('createTopics', deduplicateCallback => {
|
|
@@ -322,10 +323,12 @@ export class Admin extends Base {
|
|
|
322
323
|
metadata: reader.readBytes(false)
|
|
323
324
|
};
|
|
324
325
|
reader.reset(member.memberAssignment);
|
|
326
|
+
reader.skip(2); // Ignore Version information
|
|
325
327
|
const memberAssignments = reader.readMap(r => {
|
|
326
|
-
const topic = r.readString();
|
|
327
|
-
return [topic, { topic, partitions: reader.readArray(r => r.readInt32(),
|
|
328
|
-
},
|
|
328
|
+
const topic = r.readString(false);
|
|
329
|
+
return [topic, { topic, partitions: reader.readArray(r => r.readInt32(), false, false) }];
|
|
330
|
+
}, false, false);
|
|
331
|
+
reader.readBytes(); // Ignore the user data
|
|
329
332
|
group.members.set(member.memberId, {
|
|
330
333
|
id: member.memberId,
|
|
331
334
|
groupInstanceId: member.groupInstanceId,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CreateTopicsRequestTopicConfig } from '../../apis/admin/create-topics-v7.ts';
|
|
1
2
|
import { type ConsumerGroupState } from '../../apis/enumerations.ts';
|
|
2
3
|
import { type NullableString } from '../../protocol/definitions.ts';
|
|
3
4
|
import { type BaseOptions } from '../base/types.ts';
|
|
@@ -39,6 +40,7 @@ export interface CreateTopicsOptions {
|
|
|
39
40
|
partitions?: number;
|
|
40
41
|
replicas?: number;
|
|
41
42
|
assignments?: BrokerAssignment[];
|
|
43
|
+
configs?: CreateTopicsRequestTopicConfig[];
|
|
42
44
|
}
|
|
43
45
|
export interface ListTopicsOptions {
|
|
44
46
|
includeInternals?: boolean;
|
|
@@ -56,6 +56,10 @@ export declare class Base<OptionsType extends BaseOptions = BaseOptions> extends
|
|
|
56
56
|
listApis(): Promise<ApiVersionsResponseApi[]>;
|
|
57
57
|
metadata(options: MetadataOptions, callback: CallbackWithPromise<ClusterMetadata>): void;
|
|
58
58
|
metadata(options: MetadataOptions): Promise<ClusterMetadata>;
|
|
59
|
+
connectToBrokers(nodeIds: number[] | null, callback: CallbackWithPromise<Map<number, Connection>>): void;
|
|
60
|
+
connectToBrokers(nodeIds?: number[] | null): Promise<Map<number, Connection>>;
|
|
61
|
+
isActive(): boolean;
|
|
62
|
+
isConnected(): boolean;
|
|
59
63
|
[kCreateConnectionPool](): ConnectionPool;
|
|
60
64
|
[kListApis](callback: CallbackWithPromise<ApiVersionsResponseApi[]>): void;
|
|
61
65
|
[kMetadata](options: MetadataOptions, callback: CallbackWithPromise<ClusterMetadata>): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import { createPromisifiedCallback, kCallbackPromise } from "../../apis/callbacks.js";
|
|
2
|
+
import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../../apis/callbacks.js";
|
|
3
3
|
import * as apis from "../../apis/index.js";
|
|
4
4
|
import { api as apiVersionsV3 } from "../../apis/metadata/api-versions-v3.js";
|
|
5
5
|
import { baseApisChannel, baseMetadataChannel, createDiagnosticContext, notifyCreation } from "../../diagnostic.js";
|
|
@@ -49,6 +49,7 @@ export class Base extends EventEmitter {
|
|
|
49
49
|
#inflightDeduplications;
|
|
50
50
|
constructor(options) {
|
|
51
51
|
super();
|
|
52
|
+
this.setMaxListeners(0);
|
|
52
53
|
this[kClientType] = 'base';
|
|
53
54
|
this[kInstance] = currentInstance++;
|
|
54
55
|
this[kApis] = [];
|
|
@@ -97,6 +98,7 @@ export class Base extends EventEmitter {
|
|
|
97
98
|
callback = createPromisifiedCallback();
|
|
98
99
|
}
|
|
99
100
|
this[kClosed] = true;
|
|
101
|
+
this.emitWithDebug('client', 'close');
|
|
100
102
|
this[kConnections].close(callback);
|
|
101
103
|
return callback[kCallbackPromise];
|
|
102
104
|
}
|
|
@@ -119,6 +121,57 @@ export class Base extends EventEmitter {
|
|
|
119
121
|
baseMetadataChannel.traceCallback(this[kMetadata], 1, createDiagnosticContext({ client: this, operation: 'metadata' }), this, options, callback);
|
|
120
122
|
return callback[kCallbackPromise];
|
|
121
123
|
}
|
|
124
|
+
connectToBrokers(nodeIds, callback) {
|
|
125
|
+
if (!callback) {
|
|
126
|
+
callback = createPromisifiedCallback();
|
|
127
|
+
}
|
|
128
|
+
// Fetch the metadata
|
|
129
|
+
this[kMetadata]({ topics: [] }, (error, metadata) => {
|
|
130
|
+
if (error) {
|
|
131
|
+
callback(error, undefined);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
let nodes = [];
|
|
135
|
+
if (nodeIds?.length) {
|
|
136
|
+
for (const node of nodeIds) {
|
|
137
|
+
if (metadata.brokers.has(node)) {
|
|
138
|
+
nodes.push(node);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
nodes = Array.from(metadata.brokers.keys());
|
|
144
|
+
}
|
|
145
|
+
runConcurrentCallbacks('Connecting to brokers failed.', nodes, (nodeId, concurrentCallback) => {
|
|
146
|
+
this[kGetConnection](metadata.brokers.get(nodeId), (error, connection) => {
|
|
147
|
+
if (error) {
|
|
148
|
+
concurrentCallback(error, undefined);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
concurrentCallback(null, [nodeId, connection]);
|
|
152
|
+
});
|
|
153
|
+
}, (error, connections) => {
|
|
154
|
+
if (error) {
|
|
155
|
+
callback(error, undefined);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
return callback(null, new Map(connections));
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
return callback[kCallbackPromise];
|
|
162
|
+
}
|
|
163
|
+
isActive() {
|
|
164
|
+
if (this[kClosed]) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
isConnected() {
|
|
170
|
+
if (this[kClosed]) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
return this[kConnections].isConnected();
|
|
174
|
+
}
|
|
122
175
|
[kCreateConnectionPool]() {
|
|
123
176
|
const pool = new ConnectionPool(this[kClientId], {
|
|
124
177
|
ownerId: this[kInstance],
|
|
@@ -148,13 +201,26 @@ export class Base extends EventEmitter {
|
|
|
148
201
|
}, callback);
|
|
149
202
|
}
|
|
150
203
|
[kMetadata](options, callback) {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
options.topics
|
|
156
|
-
|
|
157
|
-
|
|
204
|
+
const expiralDate = Date.now() - (options.metadataMaxAge ?? this[kOptions].metadataMaxAge);
|
|
205
|
+
let topicsToFetch = [];
|
|
206
|
+
// Determine which topics we need to fetch
|
|
207
|
+
if (!this.#metadata || options.forceUpdate) {
|
|
208
|
+
topicsToFetch = options.topics;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
for (const topic of options.topics) {
|
|
212
|
+
const existingTopic = this.#metadata.topics.get(topic);
|
|
213
|
+
if (!existingTopic || existingTopic.lastUpdate < expiralDate) {
|
|
214
|
+
topicsToFetch.push(topic);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// All topics are already up-to-date, simply return them
|
|
219
|
+
if (this.#metadata && !topicsToFetch.length) {
|
|
220
|
+
callback(null, {
|
|
221
|
+
...this.#metadata,
|
|
222
|
+
topics: new Map(options.topics.map(topic => [topic, this.#metadata.topics.get(topic)]))
|
|
223
|
+
});
|
|
158
224
|
return;
|
|
159
225
|
}
|
|
160
226
|
const autocreateTopics = options.autocreateTopics ?? this[kOptions].autocreateTopics;
|
|
@@ -170,20 +236,40 @@ export class Base extends EventEmitter {
|
|
|
170
236
|
retryCallback(error, undefined);
|
|
171
237
|
return;
|
|
172
238
|
}
|
|
173
|
-
api(connection,
|
|
239
|
+
api(connection, topicsToFetch, autocreateTopics, true, retryCallback);
|
|
174
240
|
});
|
|
175
241
|
});
|
|
176
242
|
}, (error, metadata) => {
|
|
177
243
|
if (error) {
|
|
244
|
+
const hasStaleMetadata = error.findBy('hasStaleMetadata', true);
|
|
245
|
+
// Stale metadata, we need to fetch everything again
|
|
246
|
+
if (hasStaleMetadata) {
|
|
247
|
+
this[kClearMetadata]();
|
|
248
|
+
topicsToFetch = options.topics;
|
|
249
|
+
}
|
|
178
250
|
deduplicateCallback(error, undefined);
|
|
179
251
|
return;
|
|
180
252
|
}
|
|
253
|
+
const lastUpdate = Date.now();
|
|
254
|
+
if (!this.#metadata) {
|
|
255
|
+
this.#metadata = {
|
|
256
|
+
id: metadata.clusterId,
|
|
257
|
+
brokers: new Map(),
|
|
258
|
+
topics: new Map(),
|
|
259
|
+
lastUpdate
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
this.#metadata.lastUpdate = lastUpdate;
|
|
264
|
+
}
|
|
181
265
|
const brokers = new Map();
|
|
182
|
-
|
|
266
|
+
// This should never change, but we act defensively here
|
|
183
267
|
for (const broker of metadata.brokers) {
|
|
184
268
|
const { host, port } = broker;
|
|
185
269
|
brokers.set(broker.nodeId, { host, port });
|
|
186
270
|
}
|
|
271
|
+
this.#metadata.brokers = brokers;
|
|
272
|
+
// Update all the topics in the cache
|
|
187
273
|
for (const { name, topicId: id, partitions: rawPartitions, isInternal } of metadata.topics) {
|
|
188
274
|
/* c8 ignore next 3 - Sometimes internal topics might be returned by Kafka */
|
|
189
275
|
if (isInternal) {
|
|
@@ -197,16 +283,15 @@ export class Base extends EventEmitter {
|
|
|
197
283
|
replicas: rawPartition.replicaNodes
|
|
198
284
|
};
|
|
199
285
|
}
|
|
200
|
-
topics.set(name, { id, partitions, partitionsCount: rawPartitions.length });
|
|
286
|
+
this.#metadata.topics.set(name, { id, partitions, partitionsCount: rawPartitions.length, lastUpdate });
|
|
201
287
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
topics,
|
|
206
|
-
lastUpdate: Date.now()
|
|
288
|
+
// Now build the object to return
|
|
289
|
+
const updatedMetadata = {
|
|
290
|
+
...this.#metadata,
|
|
291
|
+
topics: new Map(options.topics.map(topic => [topic, this.#metadata.topics.get(topic)]))
|
|
207
292
|
};
|
|
208
|
-
this.emitWithDebug('client', 'metadata',
|
|
209
|
-
deduplicateCallback(null,
|
|
293
|
+
this.emitWithDebug('client', 'metadata', updatedMetadata);
|
|
294
|
+
deduplicateCallback(null, updatedMetadata);
|
|
210
295
|
}, 0);
|
|
211
296
|
}, callback);
|
|
212
297
|
}
|
|
@@ -242,9 +327,17 @@ export class Base extends EventEmitter {
|
|
|
242
327
|
const retriable = genericError.findBy?.('code', NetworkError.code) || genericError.findBy?.('canRetry', true);
|
|
243
328
|
errors.push(error);
|
|
244
329
|
if (attempt < retries && retriable && !shouldSkipRetry?.(error)) {
|
|
245
|
-
|
|
330
|
+
this.emitWithDebug('client', 'performWithRetry:retry', operationId, attempt, retries);
|
|
331
|
+
function onClose() {
|
|
332
|
+
clearTimeout(timeout);
|
|
333
|
+
errors.push(new UserError(`Client closed while retrying ${operationId}.`));
|
|
334
|
+
callback(new MultipleErrors(`${operationId} failed ${attempt + 1} times.`, errors), undefined);
|
|
335
|
+
}
|
|
336
|
+
const timeout = setTimeout(() => {
|
|
337
|
+
this.removeListener('client:close', onClose);
|
|
246
338
|
this[kPerformWithRetry](operationId, operation, callback, attempt + 1, errors, shouldSkipRetry);
|
|
247
339
|
}, this[kOptions].retryDelay);
|
|
340
|
+
this.once('client:close', onClose);
|
|
248
341
|
}
|
|
249
342
|
else {
|
|
250
343
|
if (attempt === 0) {
|
|
@@ -88,7 +88,7 @@ export declare const baseOptionsSchema: {
|
|
|
88
88
|
properties: {
|
|
89
89
|
mechanism: {
|
|
90
90
|
type: string;
|
|
91
|
-
enum: readonly ["PLAIN", "SCRAM-SHA-256", "SCRAM-SHA-512"];
|
|
91
|
+
enum: readonly ["PLAIN", "SCRAM-SHA-256", "SCRAM-SHA-512", "OAUTHBEARER"];
|
|
92
92
|
};
|
|
93
93
|
username: {
|
|
94
94
|
type: string;
|
|
@@ -96,6 +96,9 @@ export declare const baseOptionsSchema: {
|
|
|
96
96
|
password: {
|
|
97
97
|
type: string;
|
|
98
98
|
};
|
|
99
|
+
token: {
|
|
100
|
+
type: string;
|
|
101
|
+
};
|
|
99
102
|
};
|
|
100
103
|
required: string[];
|
|
101
104
|
additionalProperties: boolean;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
1
|
import { SASLMechanisms } from "../../apis/enumerations.js";
|
|
3
2
|
import { ajv } from "../../utils.js";
|
|
4
|
-
|
|
3
|
+
import { version } from "../../version.js";
|
|
5
4
|
// Note: clientSoftwareName can only contain alphanumeric characters, hyphens and dots
|
|
6
5
|
export const clientSoftwareName = 'platformatic-kafka';
|
|
7
|
-
export const clientSoftwareVersion =
|
|
6
|
+
export const clientSoftwareVersion = version;
|
|
8
7
|
export const idProperty = { type: 'string', pattern: '^\\S+$' };
|
|
9
8
|
export const topicWithPartitionAndOffsetProperties = {
|
|
10
9
|
topic: idProperty,
|
|
@@ -39,9 +38,10 @@ export const baseOptionsSchema = {
|
|
|
39
38
|
properties: {
|
|
40
39
|
mechanism: { type: 'string', enum: SASLMechanisms },
|
|
41
40
|
username: { type: 'string' },
|
|
42
|
-
password: { type: 'string' }
|
|
41
|
+
password: { type: 'string' },
|
|
42
|
+
token: { type: 'string' }
|
|
43
43
|
},
|
|
44
|
-
required: ['mechanism'
|
|
44
|
+
required: ['mechanism'],
|
|
45
45
|
additionalProperties: false
|
|
46
46
|
},
|
|
47
47
|
metadataMaxAge: { type: 'number', minimum: 0 },
|
|
@@ -72,7 +72,7 @@ export const defaultBaseOptions = {
|
|
|
72
72
|
timeout: 5000,
|
|
73
73
|
retries: 3,
|
|
74
74
|
retryDelay: 1000,
|
|
75
|
-
metadataMaxAge: 5000, // 5
|
|
75
|
+
metadataMaxAge: 5000, // 5 seconds
|
|
76
76
|
autocreateTopics: false,
|
|
77
77
|
strict: false
|
|
78
78
|
};
|
|
@@ -15,8 +15,10 @@ export declare class Consumer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
|
|
|
15
15
|
[kFetchConnections]: ConnectionPool;
|
|
16
16
|
constructor(options: ConsumerOptions<Key, Value, HeaderKey, HeaderValue>);
|
|
17
17
|
get streamsCount(): number;
|
|
18
|
+
get lastHeartbeat(): Date | null;
|
|
18
19
|
close(force: boolean | CallbackWithPromise<void>, callback?: CallbackWithPromise<void>): void;
|
|
19
20
|
close(force?: boolean): Promise<void>;
|
|
21
|
+
isActive(): boolean;
|
|
20
22
|
consume(options: ConsumeOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<MessagesStream<Key, Value, HeaderKey, HeaderValue>>): void;
|
|
21
23
|
consume(options: ConsumeOptions<Key, Value, HeaderKey, HeaderValue>): Promise<MessagesStream<Key, Value, HeaderKey, HeaderValue>>;
|
|
22
24
|
fetch(options: FetchOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<FetchResponse>): void;
|
|
@@ -4,7 +4,7 @@ import { consumerCommitsChannel, consumerConsumesChannel, consumerFetchesChannel
|
|
|
4
4
|
import { UserError } from "../../errors.js";
|
|
5
5
|
import { Reader } from "../../protocol/reader.js";
|
|
6
6
|
import { Writer } from "../../protocol/writer.js";
|
|
7
|
-
import { Base, kAfterCreate, kCheckNotClosed, kClosed, kCreateConnectionPool, kFetchConnections, kFormatValidationErrors, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kPrometheus, kValidateOptions } from "../base/base.js";
|
|
7
|
+
import { Base, kAfterCreate, kCheckNotClosed, kClearMetadata, kClosed, kCreateConnectionPool, kFetchConnections, kFormatValidationErrors, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kPrometheus, kValidateOptions } from "../base/base.js";
|
|
8
8
|
import { defaultBaseOptions } from "../base/options.js";
|
|
9
9
|
import { ensureMetric } from "../metrics.js";
|
|
10
10
|
import { MessagesStream } from "./messages-stream.js";
|
|
@@ -23,6 +23,7 @@ export class Consumer extends Base {
|
|
|
23
23
|
#protocol;
|
|
24
24
|
#coordinatorId;
|
|
25
25
|
#heartbeatInterval;
|
|
26
|
+
#lastHeartbeat;
|
|
26
27
|
#streams;
|
|
27
28
|
#partitionsAssigner;
|
|
28
29
|
/*
|
|
@@ -57,6 +58,7 @@ export class Consumer extends Base {
|
|
|
57
58
|
this.#protocol = null;
|
|
58
59
|
this.#coordinatorId = null;
|
|
59
60
|
this.#heartbeatInterval = null;
|
|
61
|
+
this.#lastHeartbeat = null;
|
|
60
62
|
this.#streams = new Set();
|
|
61
63
|
this.#partitionsAssigner = this[kOptions].partitionAssigner ?? roundRobinAssigner;
|
|
62
64
|
this.#validateGroupOptions(this[kOptions], groupIdAndOptionsValidator);
|
|
@@ -72,6 +74,9 @@ export class Consumer extends Base {
|
|
|
72
74
|
get streamsCount() {
|
|
73
75
|
return this.#streams.size;
|
|
74
76
|
}
|
|
77
|
+
get lastHeartbeat() {
|
|
78
|
+
return this.#lastHeartbeat;
|
|
79
|
+
}
|
|
75
80
|
close(force, callback) {
|
|
76
81
|
if (typeof force === 'function') {
|
|
77
82
|
callback = force;
|
|
@@ -118,6 +123,14 @@ export class Consumer extends Base {
|
|
|
118
123
|
});
|
|
119
124
|
return callback[kCallbackPromise];
|
|
120
125
|
}
|
|
126
|
+
isActive() {
|
|
127
|
+
const baseReady = super.isActive();
|
|
128
|
+
if (!baseReady) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
// We consider the group ready if we have a groupId, a memberId and heartbeat interval
|
|
132
|
+
return this.#membershipActive && Boolean(this.groupId) && Boolean(this.memberId) && this.#heartbeatInterval !== null;
|
|
133
|
+
}
|
|
121
134
|
consume(options, callback) {
|
|
122
135
|
if (!callback) {
|
|
123
136
|
callback = createPromisifiedCallback();
|
|
@@ -265,6 +278,7 @@ export class Consumer extends Base {
|
|
|
265
278
|
callback(error);
|
|
266
279
|
return;
|
|
267
280
|
}
|
|
281
|
+
this.#lastHeartbeat = null;
|
|
268
282
|
callback(null);
|
|
269
283
|
});
|
|
270
284
|
return callback[kCallbackPromise];
|
|
@@ -336,10 +350,10 @@ export class Consumer extends Base {
|
|
|
336
350
|
const requests = new Map();
|
|
337
351
|
for (const name of options.topics) {
|
|
338
352
|
const topic = metadata.topics.get(name);
|
|
339
|
-
const toInclude = options.partitions?.[name] ?? [];
|
|
340
|
-
const hasPartitionsFilter = toInclude.
|
|
353
|
+
const toInclude = new Set(options.partitions?.[name] ?? []);
|
|
354
|
+
const hasPartitionsFilter = toInclude.size > 0;
|
|
341
355
|
for (let i = 0; i < topic.partitionsCount; i++) {
|
|
342
|
-
if (hasPartitionsFilter && !toInclude.
|
|
356
|
+
if (hasPartitionsFilter && !toInclude.delete(i)) {
|
|
343
357
|
continue;
|
|
344
358
|
}
|
|
345
359
|
const partition = topic.partitions[i];
|
|
@@ -360,6 +374,10 @@ export class Consumer extends Base {
|
|
|
360
374
|
timestamp: options.timestamp ?? -1n
|
|
361
375
|
});
|
|
362
376
|
}
|
|
377
|
+
if (toInclude.size > 0) {
|
|
378
|
+
callback(new UserError(`Specified partition(s) not found in topic ${name}`), undefined);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
363
381
|
}
|
|
364
382
|
runConcurrentCallbacks('Listing offsets failed.', requests, ([leader, requests], concurrentCallback) => {
|
|
365
383
|
this[kPerformWithRetry]('listOffsets', retryCallback => {
|
|
@@ -379,7 +397,7 @@ export class Consumer extends Base {
|
|
|
379
397
|
}, concurrentCallback, 0);
|
|
380
398
|
}, (error, responses) => {
|
|
381
399
|
if (error) {
|
|
382
|
-
callback(error, undefined);
|
|
400
|
+
callback(this.#handleMetadataError(error), undefined);
|
|
383
401
|
return;
|
|
384
402
|
}
|
|
385
403
|
let offsets = new Map();
|
|
@@ -434,7 +452,7 @@ export class Consumer extends Base {
|
|
|
434
452
|
});
|
|
435
453
|
}, (error, response) => {
|
|
436
454
|
if (error) {
|
|
437
|
-
callback(error, undefined);
|
|
455
|
+
callback(this.#handleMetadataError(error), undefined);
|
|
438
456
|
return;
|
|
439
457
|
}
|
|
440
458
|
const committed = new Map();
|
|
@@ -506,6 +524,7 @@ export class Consumer extends Base {
|
|
|
506
524
|
// Note that here we purposely do not return, since it was not a group related problem we schedule another heartbeat
|
|
507
525
|
}
|
|
508
526
|
else {
|
|
527
|
+
this.#lastHeartbeat = new Date();
|
|
509
528
|
this.emitWithDebug('consumer:heartbeat', 'end', eventPayload);
|
|
510
529
|
}
|
|
511
530
|
this.#heartbeatInterval?.refresh();
|
|
@@ -542,6 +561,7 @@ export class Consumer extends Base {
|
|
|
542
561
|
this.#metricActiveStreams?.inc();
|
|
543
562
|
stream.once('close', () => {
|
|
544
563
|
this.#metricActiveStreams?.dec();
|
|
564
|
+
this.topics.untrackAll(...options.topics);
|
|
545
565
|
this.#streams.delete(stream);
|
|
546
566
|
});
|
|
547
567
|
callback(null, stream);
|
|
@@ -722,7 +742,7 @@ export class Consumer extends Base {
|
|
|
722
742
|
}
|
|
723
743
|
this[kMetadata]({ topics: Array.from(topicsSubscriptions.keys()) }, (error, metadata) => {
|
|
724
744
|
if (error) {
|
|
725
|
-
callback(error, undefined);
|
|
745
|
+
callback(this.#handleMetadataError(error), undefined);
|
|
726
746
|
return;
|
|
727
747
|
}
|
|
728
748
|
this.#performSyncGroup(this.#createAssignments(metadata), callback);
|
|
@@ -751,15 +771,7 @@ export class Consumer extends Base {
|
|
|
751
771
|
callback(error, undefined);
|
|
752
772
|
return;
|
|
753
773
|
}
|
|
754
|
-
|
|
755
|
-
const reader = Reader.from(response.assignment);
|
|
756
|
-
const assignments = reader.readArray(r => {
|
|
757
|
-
return {
|
|
758
|
-
topic: r.readString(),
|
|
759
|
-
partitions: r.readArray(r => r.readInt32(), true, false)
|
|
760
|
-
};
|
|
761
|
-
}, true, false);
|
|
762
|
-
callback(error, assignments);
|
|
774
|
+
callback(error, this.#decodeProtocolAssignment(response.assignment));
|
|
763
775
|
});
|
|
764
776
|
}
|
|
765
777
|
#performDeduplicateGroupOperaton(operationId, operation, callback) {
|
|
@@ -775,7 +787,7 @@ export class Consumer extends Base {
|
|
|
775
787
|
}
|
|
776
788
|
this[kMetadata]({ topics: this.topics.current }, (error, metadata) => {
|
|
777
789
|
if (error) {
|
|
778
|
-
callback(error, undefined);
|
|
790
|
+
callback(this.#handleMetadataError(error), undefined);
|
|
779
791
|
return;
|
|
780
792
|
}
|
|
781
793
|
this[kPerformWithRetry](operationId, retryCallback => {
|
|
@@ -818,13 +830,26 @@ export class Consumer extends Base {
|
|
|
818
830
|
};
|
|
819
831
|
}
|
|
820
832
|
/*
|
|
821
|
-
|
|
833
|
+
The following two methods follow:
|
|
822
834
|
https://github.com/apache/kafka/blob/trunk/clients/src/main/resources/common/message/ConsumerProtocolAssignment.json
|
|
823
835
|
*/
|
|
824
836
|
#encodeProtocolAssignment(assignments) {
|
|
825
|
-
return Writer.create()
|
|
826
|
-
|
|
827
|
-
|
|
837
|
+
return Writer.create()
|
|
838
|
+
.appendInt16(0) // Version information
|
|
839
|
+
.appendArray(assignments, (w, { topic, partitions }) => {
|
|
840
|
+
w.appendString(topic, false).appendArray(partitions, (w, a) => w.appendInt32(a), false, false);
|
|
841
|
+
}, false, false)
|
|
842
|
+
.appendInt32(0).buffer; // No user data
|
|
843
|
+
}
|
|
844
|
+
#decodeProtocolAssignment(buffer) {
|
|
845
|
+
const reader = Reader.from(buffer);
|
|
846
|
+
reader.skip(2); // Ignore Version information
|
|
847
|
+
return reader.readArray(r => {
|
|
848
|
+
return {
|
|
849
|
+
topic: r.readString(false),
|
|
850
|
+
partitions: r.readArray(r => r.readInt32(), false, false)
|
|
851
|
+
};
|
|
852
|
+
}, false, false);
|
|
828
853
|
}
|
|
829
854
|
#createAssignments(metadata) {
|
|
830
855
|
const partitionTracker = new Map();
|
|
@@ -875,4 +900,10 @@ export class Consumer extends Base {
|
|
|
875
900
|
}
|
|
876
901
|
return protocolError;
|
|
877
902
|
}
|
|
903
|
+
#handleMetadataError(error) {
|
|
904
|
+
if (error && error?.findBy('hasStaleMetadata', true)) {
|
|
905
|
+
this[kClearMetadata]();
|
|
906
|
+
}
|
|
907
|
+
return error;
|
|
908
|
+
}
|
|
878
909
|
}
|
|
@@ -11,6 +11,8 @@ export declare class MessagesStream<Key, Value, HeaderKey, HeaderValue> extends
|
|
|
11
11
|
constructor(consumer: Consumer<Key, Value, HeaderKey, HeaderValue>, options: ConsumeOptions<Key, Value, HeaderKey, HeaderValue>);
|
|
12
12
|
close(callback?: CallbackWithPromise<void>): void;
|
|
13
13
|
close(): Promise<void>;
|
|
14
|
+
isActive(): boolean;
|
|
15
|
+
isConnected(): boolean;
|
|
14
16
|
addListener(event: 'autocommit', listener: (err: Error, offsets: CommitOptionsPartition[]) => void): this;
|
|
15
17
|
addListener(event: 'data', listener: (message: Message<Key, Value, HeaderKey, HeaderValue>) => void): this;
|
|
16
18
|
addListener(event: 'close', listener: () => void): this;
|
|
@@ -20,6 +20,8 @@ export class MessagesStream extends Readable {
|
|
|
20
20
|
#consumer;
|
|
21
21
|
#mode;
|
|
22
22
|
#fallbackMode;
|
|
23
|
+
#fetches;
|
|
24
|
+
#maxFetches;
|
|
23
25
|
#options;
|
|
24
26
|
#topics;
|
|
25
27
|
#offsetsToFetch;
|
|
@@ -37,7 +39,7 @@ export class MessagesStream extends Readable {
|
|
|
37
39
|
#metricsConsumedMessages;
|
|
38
40
|
#corruptedMessageHandler;
|
|
39
41
|
constructor(consumer, options) {
|
|
40
|
-
const { autocommit, mode, fallbackMode, offsets, deserializers, onCorruptedMessage, ...otherOptions } = options;
|
|
42
|
+
const { autocommit, mode, fallbackMode, maxFetches, offsets, deserializers, onCorruptedMessage, ...otherOptions } = options;
|
|
41
43
|
if (offsets && mode !== MessagesStreamModes.MANUAL) {
|
|
42
44
|
throw new UserError('Cannot specify offsets when the stream mode is not MANUAL.');
|
|
43
45
|
}
|
|
@@ -50,6 +52,8 @@ export class MessagesStream extends Readable {
|
|
|
50
52
|
this.#mode = mode ?? MessagesStreamModes.LATEST;
|
|
51
53
|
this.#fallbackMode = fallbackMode ?? MessagesStreamFallbackModes.LATEST;
|
|
52
54
|
this.#offsetsToCommit = new Map();
|
|
55
|
+
this.#fetches = 0;
|
|
56
|
+
this.#maxFetches = maxFetches ?? 0;
|
|
53
57
|
this.#topics = structuredClone(options.topics);
|
|
54
58
|
this.#inflightNodes = new Set();
|
|
55
59
|
this.#keyDeserializer = deserializers?.key ?? noopDeserializer;
|
|
@@ -138,6 +142,18 @@ export class MessagesStream extends Readable {
|
|
|
138
142
|
});
|
|
139
143
|
return callback[kCallbackPromise];
|
|
140
144
|
}
|
|
145
|
+
isActive() {
|
|
146
|
+
if (this.#shouldClose || this.closed || this.destroyed) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
return this.#consumer.isActive();
|
|
150
|
+
}
|
|
151
|
+
isConnected() {
|
|
152
|
+
if (this.#shouldClose || this.closed || this.destroyed) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
return this.#consumer.isConnected();
|
|
156
|
+
}
|
|
141
157
|
/* c8 ignore next 3 - Only forwards to Node.js implementation - Inserted here to please Typescript */
|
|
142
158
|
addListener(event, listener) {
|
|
143
159
|
return super.addListener(event, listener);
|
|
@@ -183,7 +199,7 @@ export class MessagesStream extends Readable {
|
|
|
183
199
|
if (error) {
|
|
184
200
|
// The stream has been closed, ignore any error
|
|
185
201
|
/* c8 ignore next 4 - Hard to test */
|
|
186
|
-
if (this.#shouldClose) {
|
|
202
|
+
if (this.#shouldClose || this.closed || this.destroyed) {
|
|
187
203
|
this.push(null);
|
|
188
204
|
return;
|
|
189
205
|
}
|
|
@@ -241,7 +257,7 @@ export class MessagesStream extends Readable {
|
|
|
241
257
|
if (error) {
|
|
242
258
|
// The stream has been closed, ignore the error
|
|
243
259
|
/* c8 ignore next 4 - Hard to test */
|
|
244
|
-
if (this.#shouldClose) {
|
|
260
|
+
if (this.#shouldClose || this.closed || this.destroyed) {
|
|
245
261
|
this.push(null);
|
|
246
262
|
return;
|
|
247
263
|
}
|
|
@@ -257,6 +273,9 @@ export class MessagesStream extends Readable {
|
|
|
257
273
|
return;
|
|
258
274
|
}
|
|
259
275
|
this.#pushRecords(metadata, topicIds, response, requestedOffsets);
|
|
276
|
+
if (this.#maxFetches > 0 && ++this.#fetches >= this.#maxFetches) {
|
|
277
|
+
this.push(null);
|
|
278
|
+
}
|
|
260
279
|
});
|
|
261
280
|
}
|
|
262
281
|
});
|
|
@@ -387,6 +406,11 @@ export class MessagesStream extends Readable {
|
|
|
387
406
|
: ListOffsetTimestamps.LATEST
|
|
388
407
|
}, (error, offsets) => {
|
|
389
408
|
if (error) {
|
|
409
|
+
/* c8 ignore next 4 - Hard to test */
|
|
410
|
+
if (this.#shouldClose || this.closed || this.destroyed) {
|
|
411
|
+
callback(null);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
390
414
|
callback(error);
|
|
391
415
|
return;
|
|
392
416
|
}
|
|
@@ -409,6 +433,11 @@ export class MessagesStream extends Readable {
|
|
|
409
433
|
}
|
|
410
434
|
this.#consumer.listCommittedOffsets({ topics }, (error, commits) => {
|
|
411
435
|
if (error) {
|
|
436
|
+
/* c8 ignore next 4 - Hard to test */
|
|
437
|
+
if (this.#shouldClose || this.closed || this.destroyed) {
|
|
438
|
+
callback(null);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
412
441
|
callback(error);
|
|
413
442
|
return;
|
|
414
443
|
}
|
|
@@ -281,6 +281,11 @@ export declare const consumeOptionsSchema: {
|
|
|
281
281
|
type: string;
|
|
282
282
|
enum: ("latest" | "earliest" | "fail")[];
|
|
283
283
|
};
|
|
284
|
+
maxFetches: {
|
|
285
|
+
type: string;
|
|
286
|
+
minimum: number;
|
|
287
|
+
default: number;
|
|
288
|
+
};
|
|
284
289
|
offsets: {
|
|
285
290
|
type: string;
|
|
286
291
|
items: {
|
|
@@ -71,6 +71,7 @@ export const consumeOptionsSchema = {
|
|
|
71
71
|
topics: { type: 'array', items: idProperty },
|
|
72
72
|
mode: { type: 'string', enum: Object.values(MessagesStreamModes) },
|
|
73
73
|
fallbackMode: { type: 'string', enum: Object.values(MessagesStreamFallbackModes) },
|
|
74
|
+
maxFetches: { type: 'number', minimum: 0, default: 0 },
|
|
74
75
|
offsets: {
|
|
75
76
|
type: 'array',
|
|
76
77
|
items: {
|
package/dist/errors.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export declare class NetworkError extends GenericError {
|
|
|
35
35
|
constructor(message: string, properties?: ErrorProperties);
|
|
36
36
|
}
|
|
37
37
|
export declare class ProtocolError extends GenericError {
|
|
38
|
+
static code: ErrorCode;
|
|
38
39
|
constructor(codeOrId: string | number, properties?: ErrorProperties, response?: unknown);
|
|
39
40
|
}
|
|
40
41
|
export declare class ResponseError extends MultipleErrors {
|
package/dist/errors.js
CHANGED
|
@@ -76,9 +76,13 @@ export class MultipleErrors extends AggregateError {
|
|
|
76
76
|
return this;
|
|
77
77
|
}
|
|
78
78
|
for (const error of this.errors) {
|
|
79
|
-
if (error[
|
|
79
|
+
if (error[property] === value) {
|
|
80
80
|
return error;
|
|
81
81
|
}
|
|
82
|
+
const found = error[kGenericError] ? error.findBy(property, value) : undefined;
|
|
83
|
+
if (found) {
|
|
84
|
+
return found;
|
|
85
|
+
}
|
|
82
86
|
}
|
|
83
87
|
return null;
|
|
84
88
|
}
|
|
@@ -97,9 +101,10 @@ export class NetworkError extends GenericError {
|
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
103
|
export class ProtocolError extends GenericError {
|
|
104
|
+
static code = 'PLT_KFK_PROTOCOL';
|
|
100
105
|
constructor(codeOrId, properties = {}, response = undefined) {
|
|
101
106
|
const { id, code, message, canRetry } = protocolErrors[typeof codeOrId === 'number' ? protocolErrorsCodesById[codeOrId] : codeOrId];
|
|
102
|
-
super(
|
|
107
|
+
super(ProtocolError.code, message, {
|
|
103
108
|
apiId: id,
|
|
104
109
|
apiCode: code,
|
|
105
110
|
canRetry,
|
|
@@ -7,12 +7,14 @@ let currentInstance = 0;
|
|
|
7
7
|
export class ConnectionPool extends EventEmitter {
|
|
8
8
|
#instanceId;
|
|
9
9
|
#clientId;
|
|
10
|
+
#closed;
|
|
10
11
|
// @ts-ignore This is used just for debugging
|
|
11
12
|
#ownerId;
|
|
12
13
|
#connections;
|
|
13
14
|
#connectionOptions;
|
|
14
15
|
constructor(clientId, connectionOptions = {}) {
|
|
15
16
|
super();
|
|
17
|
+
this.#closed = false;
|
|
16
18
|
this.#instanceId = currentInstance++;
|
|
17
19
|
this.#clientId = clientId;
|
|
18
20
|
this.#ownerId = connectionOptions.ownerId;
|
|
@@ -41,17 +43,40 @@ export class ConnectionPool extends EventEmitter {
|
|
|
41
43
|
if (!callback) {
|
|
42
44
|
callback = createPromisifiedCallback();
|
|
43
45
|
}
|
|
44
|
-
if (this.#connections.size === 0) {
|
|
46
|
+
if (this.#closed || this.#connections.size === 0) {
|
|
47
|
+
this.#closed = true;
|
|
45
48
|
callback(null);
|
|
46
49
|
return callback[kCallbackPromise];
|
|
47
50
|
}
|
|
51
|
+
this.#closed = true;
|
|
48
52
|
runConcurrentCallbacks('Closing connections failed.', this.#connections, ([key, connection], cb) => {
|
|
49
53
|
connection.close(cb);
|
|
50
54
|
this.#connections.delete(key);
|
|
51
55
|
}, error => callback(error));
|
|
52
56
|
return callback[kCallbackPromise];
|
|
53
57
|
}
|
|
58
|
+
isActive() {
|
|
59
|
+
if (this.#connections.size === 0) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
isConnected() {
|
|
65
|
+
if (this.#connections.size === 0) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
for (const connection of this.#connections.values()) {
|
|
69
|
+
if (!connection.isConnected()) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
54
75
|
#get(broker, callback) {
|
|
76
|
+
if (this.#closed) {
|
|
77
|
+
callback(new Error('Connection pool is closed.'), undefined);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
55
80
|
const key = `${broker.host}:${broker.port}`;
|
|
56
81
|
const existing = this.#connections.get(key);
|
|
57
82
|
if (existing) {
|
|
@@ -11,8 +11,9 @@ export interface Broker {
|
|
|
11
11
|
}
|
|
12
12
|
export interface SASLOptions {
|
|
13
13
|
mechanism: SASLMechanism;
|
|
14
|
-
username
|
|
15
|
-
password
|
|
14
|
+
username?: string;
|
|
15
|
+
password?: string;
|
|
16
|
+
token?: string;
|
|
16
17
|
}
|
|
17
18
|
export interface ConnectionOptions {
|
|
18
19
|
connectTimeout?: number;
|
|
@@ -53,6 +54,7 @@ export declare class Connection extends EventEmitter {
|
|
|
53
54
|
get instanceId(): number;
|
|
54
55
|
get status(): ConnectionStatusValue;
|
|
55
56
|
get socket(): Socket;
|
|
57
|
+
isConnected(): boolean;
|
|
56
58
|
connect(host: string, port: number, callback?: CallbackWithPromise<void>): void | Promise<void>;
|
|
57
59
|
ready(callback: CallbackWithPromise<void>): void;
|
|
58
60
|
ready(): Promise<void>;
|
|
@@ -10,7 +10,7 @@ import { AuthenticationError, NetworkError, TimeoutError, UnexpectedCorrelationI
|
|
|
10
10
|
import { protocolAPIsById } from "../protocol/apis.js";
|
|
11
11
|
import { EMPTY_OR_SINGLE_COMPACT_LENGTH_SIZE, INT32_SIZE } from "../protocol/definitions.js";
|
|
12
12
|
import { DynamicBuffer } from "../protocol/dynamic-buffer.js";
|
|
13
|
-
import { saslPlain, saslScramSha } from "../protocol/index.js";
|
|
13
|
+
import { saslOAuthBearer, saslPlain, saslScramSha } from "../protocol/index.js";
|
|
14
14
|
import { Reader } from "../protocol/reader.js";
|
|
15
15
|
import { defaultCrypto } from "../protocol/sasl/scram-sha.js";
|
|
16
16
|
import { Writer } from "../protocol/writer.js";
|
|
@@ -80,6 +80,9 @@ export class Connection extends EventEmitter {
|
|
|
80
80
|
get socket() {
|
|
81
81
|
return this.#socket;
|
|
82
82
|
}
|
|
83
|
+
isConnected() {
|
|
84
|
+
return this.#status === ConnectionStatuses.CONNECTED;
|
|
85
|
+
}
|
|
83
86
|
connect(host, port, callback) {
|
|
84
87
|
if (!callback) {
|
|
85
88
|
callback = createPromisifiedCallback();
|
|
@@ -229,7 +232,7 @@ export class Connection extends EventEmitter {
|
|
|
229
232
|
}
|
|
230
233
|
#authenticate(host, port, diagnosticContext) {
|
|
231
234
|
this.#status = ConnectionStatuses.AUTHENTICATING;
|
|
232
|
-
const { mechanism, username, password } = this.#options.sasl;
|
|
235
|
+
const { mechanism, username, password, token } = this.#options.sasl;
|
|
233
236
|
if (!SASLMechanisms.includes(mechanism)) {
|
|
234
237
|
this.#onConnectionError(host, port, diagnosticContext, new UserError(`SASL mechanism ${mechanism} not supported.`));
|
|
235
238
|
return;
|
|
@@ -243,6 +246,9 @@ export class Connection extends EventEmitter {
|
|
|
243
246
|
if (mechanism === 'PLAIN') {
|
|
244
247
|
saslPlain.authenticate(saslAuthenticateV2.api, this, username, password, this.#onSaslAuthenticate.bind(this, host, port, diagnosticContext));
|
|
245
248
|
}
|
|
249
|
+
else if (mechanism === 'OAUTHBEARER') {
|
|
250
|
+
saslOAuthBearer.authenticate(saslAuthenticateV2.api, this, token, this.#onSaslAuthenticate.bind(this, host, port, diagnosticContext));
|
|
251
|
+
}
|
|
246
252
|
else {
|
|
247
253
|
saslScramSha.authenticate(saslAuthenticateV2.api, this, mechanism.substring(6), username, password, defaultCrypto, this.#onSaslAuthenticate.bind(this, host, port, diagnosticContext));
|
|
248
254
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { DynamicBuffer } from './dynamic-buffer.ts';
|
|
2
|
-
export declare function crc32c(data: Buffer | DynamicBuffer): number;
|
|
2
|
+
export declare function crc32c(data: Buffer | Uint8Array | DynamicBuffer): number;
|
package/dist/protocol/crc32c.js
CHANGED
|
@@ -68,16 +68,12 @@ const CRC = [
|
|
|
68
68
|
0xbe2da0a5, 0x4c4623a6, 0x5f16d052, 0xad7d5351
|
|
69
69
|
];
|
|
70
70
|
export function crc32c(data) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
const bytes = DynamicBuffer.isDynamicBuffer(data)
|
|
72
|
+
? data.buffer
|
|
73
|
+
: new Uint8Array(data);
|
|
74
|
+
let crc = 0xffffffff;
|
|
75
|
+
for (let i = 0, len = bytes.length; i < len; ++i) {
|
|
76
|
+
crc = CRC[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
-
for (let i = 0; i < data.length; i++) {
|
|
79
|
-
crc = CRC[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return Uint32Array.from([(crc ^ -1) >>> 0])[0];
|
|
78
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
83
79
|
}
|
package/dist/protocol/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './index.ts';
|
|
|
8
8
|
export * from './murmur2.ts';
|
|
9
9
|
export * from './reader.ts';
|
|
10
10
|
export * from './records.ts';
|
|
11
|
+
export * as saslOAuthBearer from './sasl/oauth-bearer.ts';
|
|
11
12
|
export * as saslPlain from './sasl/plain.ts';
|
|
12
13
|
export * as saslScramSha from './sasl/scram-sha.ts';
|
|
13
14
|
export * from './varint.ts';
|
package/dist/protocol/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./index.js";
|
|
|
8
8
|
export * from "./murmur2.js";
|
|
9
9
|
export * from "./reader.js";
|
|
10
10
|
export * from "./records.js";
|
|
11
|
+
export * as saslOAuthBearer from "./sasl/oauth-bearer.js";
|
|
11
12
|
export * as saslPlain from "./sasl/plain.js";
|
|
12
13
|
export * as saslScramSha from "./sasl/scram-sha.js";
|
|
13
14
|
export * from "./varint.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type CallbackWithPromise } from '../../apis/callbacks.ts';
|
|
2
|
+
import { type SASLAuthenticationAPI, type SaslAuthenticateResponse } from '../../apis/security/sasl-authenticate-v2.ts';
|
|
3
|
+
import { type Connection } from '../../network/connection.ts';
|
|
4
|
+
export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, token: string, callback: CallbackWithPromise<SaslAuthenticateResponse>): void;
|
|
5
|
+
export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, token: string): Promise<SaslAuthenticateResponse>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createPromisifiedCallback, kCallbackPromise } from "../../apis/callbacks.js";
|
|
2
|
+
export function authenticate(authenticateAPI, connection, token, callback) {
|
|
3
|
+
if (!callback) {
|
|
4
|
+
callback = createPromisifiedCallback();
|
|
5
|
+
}
|
|
6
|
+
authenticateAPI(connection, Buffer.from(`n,,\x01auth=Bearer ${token}\x01\x01`), callback);
|
|
7
|
+
return callback[kCallbackPromise];
|
|
8
|
+
}
|
package/dist/protocol/writer.js
CHANGED
|
@@ -152,7 +152,7 @@ export class Writer {
|
|
|
152
152
|
}
|
|
153
153
|
appendArray(value, entryWriter, compact = true, appendTrailingTaggedFields = true) {
|
|
154
154
|
if (value == null) {
|
|
155
|
-
return compact ? this.appendUnsignedVarInt(0) : this.appendInt32(
|
|
155
|
+
return compact ? this.appendUnsignedVarInt(0) : this.appendInt32(-1);
|
|
156
156
|
}
|
|
157
157
|
const length = value.length;
|
|
158
158
|
if (compact) {
|
|
@@ -171,7 +171,7 @@ export class Writer {
|
|
|
171
171
|
}
|
|
172
172
|
appendMap(value, entryWriter, compact = true, appendTrailingTaggedFields = true) {
|
|
173
173
|
if (value == null) {
|
|
174
|
-
return compact ? this.appendUnsignedVarInt(0) : this.appendInt32(
|
|
174
|
+
return compact ? this.appendUnsignedVarInt(0) : this.appendInt32(-1);
|
|
175
175
|
}
|
|
176
176
|
const length = value.size;
|
|
177
177
|
if (compact) {
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/kafka",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.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)",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"cleaner-spec-reporter": "^0.5.0",
|
|
47
47
|
"cronometro": "^5.3.0",
|
|
48
48
|
"eslint": "^9.21.0",
|
|
49
|
+
"fast-jwt": "^6.0.2",
|
|
49
50
|
"hwp": "^0.4.1",
|
|
50
51
|
"json5": "^2.2.3",
|
|
51
52
|
"kafkajs": "^2.2.4",
|
|
@@ -64,11 +65,14 @@
|
|
|
64
65
|
},
|
|
65
66
|
"scripts": {
|
|
66
67
|
"build": "rm -rf dist && tsc -p tsconfig.base.json",
|
|
68
|
+
"postbuild": "node --experimental-strip-types scripts/postbuild.ts",
|
|
67
69
|
"lint": "eslint --cache",
|
|
68
70
|
"typecheck": "tsc -p . --noEmit",
|
|
69
71
|
"format": "prettier -w benchmarks playground src test",
|
|
70
72
|
"test": "c8 -c test/config/c8-local.json node --env-file=test/config/env --no-warnings --test --test-timeout=300000 'test/**/*.test.ts'",
|
|
71
73
|
"test:ci": "c8 -c test/config/c8-ci.json node --env-file=test/config/env --no-warnings --test --test-timeout=300000 'test/**/*.test.ts'",
|
|
74
|
+
"test:docker:up": "node scripts/docker.ts up -d --wait",
|
|
75
|
+
"test:docker:down": "node scripts/docker.ts down",
|
|
72
76
|
"ci": "npm run build && npm run lint && npm run test:ci",
|
|
73
77
|
"generate:apis": "node --experimental-strip-types scripts/generate-apis.ts",
|
|
74
78
|
"generate:errors": "node --experimental-strip-types scripts/generate-errors.ts",
|