kafka-ts 1.0.1 → 1.0.3
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/README.md +0 -1
- package/dist/consumer/consumer-group.js +6 -2
- package/dist/consumer/consumer.d.ts +0 -1
- package/dist/consumer/consumer.js +32 -13
- package/dist/consumer/fetch-manager.d.ts +2 -15
- package/dist/consumer/fetch-manager.js +3 -85
- package/dist/consumer/fetcher.d.ts +2 -3
- package/dist/consumer/fetcher.js +4 -7
- package/dist/metadata.d.ts +2 -2
- package/dist/metadata.js +2 -2
- package/dist/producer/producer.js +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -167,7 +167,6 @@ Custom SASL mechanisms can be implemented following the `SASLProvider` interface
|
|
|
167
167
|
| partitionMaxBytes | number | false | 1_048_576 | Maximum number of bytes to return per partition in the fetch response |
|
|
168
168
|
| allowTopicAutoCreation | boolean | false | false | Allow kafka to auto-create topic when it doesn't exist |
|
|
169
169
|
| fromTimestamp | bigint | false | -1 | Start consuming messages from timestamp (-1 = latest offsets, -2 = earliest offsets) |
|
|
170
|
-
| batchSize | number | false | null | Maximum number of records called `onBatch` |
|
|
171
170
|
| onBatch | (batch: Message[]) => Promise<unknown> | true | | Callback executed when a batch of messages is received |
|
|
172
171
|
|
|
173
172
|
### `kafka.createProducer()`
|
|
@@ -157,9 +157,13 @@ class ConsumerGroup {
|
|
|
157
157
|
groupInstanceId,
|
|
158
158
|
memberId: this.memberId,
|
|
159
159
|
generationIdOrMemberEpoch: this.generationId,
|
|
160
|
-
topics: Object.entries(topicPartitions)
|
|
160
|
+
topics: Object.entries(topicPartitions)
|
|
161
|
+
.filter(([topic]) => topic in offsetManager.pendingOffsets)
|
|
162
|
+
.map(([topic, partitions]) => ({
|
|
161
163
|
name: topic,
|
|
162
|
-
partitions: [...partitions]
|
|
164
|
+
partitions: [...partitions]
|
|
165
|
+
.filter((partition) => partition in offsetManager.pendingOffsets[topic])
|
|
166
|
+
.map((partitionIndex) => ({
|
|
163
167
|
partitionIndex,
|
|
164
168
|
committedOffset: offsetManager.pendingOffsets[topic][partitionIndex],
|
|
165
169
|
committedLeaderEpoch: -1,
|
|
@@ -48,11 +48,10 @@ class Consumer extends events_1.default {
|
|
|
48
48
|
minBytes: options.minBytes ?? 1,
|
|
49
49
|
maxBytes: options.maxBytes ?? 1_048_576,
|
|
50
50
|
partitionMaxBytes: options.partitionMaxBytes ?? 1_048_576,
|
|
51
|
-
isolationLevel: options.isolationLevel ??
|
|
51
|
+
isolationLevel: options.isolationLevel ?? 1 /* IsolationLevel.READ_COMMITTED */,
|
|
52
52
|
allowTopicAutoCreation: options.allowTopicAutoCreation ?? false,
|
|
53
53
|
fromBeginning: options.fromBeginning ?? false,
|
|
54
54
|
fromTimestamp: options.fromTimestamp ?? (options.fromBeginning ? -2n : -1n),
|
|
55
|
-
batchSize: options.batchSize ?? null,
|
|
56
55
|
retrier: options.retrier ?? retrier_1.defaultRetrier,
|
|
57
56
|
};
|
|
58
57
|
this.metadata = new consumer_metadata_1.ConsumerMetadata({ cluster: this.cluster });
|
|
@@ -106,7 +105,7 @@ class Consumer extends events_1.default {
|
|
|
106
105
|
await this.cluster.disconnect().catch((error) => logger_1.log.debug(`Failed to disconnect: ${error.message}`));
|
|
107
106
|
}
|
|
108
107
|
async startFetchManager() {
|
|
109
|
-
const { groupId
|
|
108
|
+
const { groupId } = this.options;
|
|
110
109
|
while (!this.stopHook) {
|
|
111
110
|
try {
|
|
112
111
|
await this.consumerGroup?.join();
|
|
@@ -121,9 +120,6 @@ class Consumer extends events_1.default {
|
|
|
121
120
|
this.fetchManager = new fetch_manager_1.FetchManager({
|
|
122
121
|
fetch: this.fetch.bind(this),
|
|
123
122
|
process: this.process.bind(this),
|
|
124
|
-
metadata: this.metadata,
|
|
125
|
-
consumerGroup: this.consumerGroup,
|
|
126
|
-
batchSize,
|
|
127
123
|
nodeAssignments,
|
|
128
124
|
});
|
|
129
125
|
await this.fetchManager.start();
|
|
@@ -165,21 +161,44 @@ class Consumer extends events_1.default {
|
|
|
165
161
|
this.consumerGroup?.handleLastHeartbeat();
|
|
166
162
|
}
|
|
167
163
|
}
|
|
168
|
-
async process(
|
|
164
|
+
async process(response) {
|
|
169
165
|
const { options } = this;
|
|
170
166
|
const { retrier } = options;
|
|
167
|
+
this.consumerGroup?.handleLastHeartbeat();
|
|
171
168
|
const topicPartitions = {};
|
|
172
|
-
|
|
169
|
+
const messages = response.responses.flatMap(({ topicId, partitions }) => {
|
|
170
|
+
const topic = this.metadata.getTopicNameById(topicId);
|
|
173
171
|
topicPartitions[topic] ??= new Set();
|
|
174
|
-
|
|
172
|
+
return partitions.flatMap(({ partitionIndex, records }) => {
|
|
173
|
+
topicPartitions[topic].add(partitionIndex);
|
|
174
|
+
return records.flatMap(({ baseTimestamp, baseOffset, records }) => records.flatMap((message) => ({
|
|
175
|
+
topic,
|
|
176
|
+
partition: partitionIndex,
|
|
177
|
+
key: message.key ?? null,
|
|
178
|
+
value: message.value ?? null,
|
|
179
|
+
headers: Object.fromEntries(message.headers.map(({ key, value }) => [key, value])),
|
|
180
|
+
timestamp: baseTimestamp + BigInt(message.timestampDelta),
|
|
181
|
+
offset: baseOffset + BigInt(message.offsetDelta),
|
|
182
|
+
})));
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
if (!messages.length) {
|
|
186
|
+
return;
|
|
175
187
|
}
|
|
176
188
|
await retrier(() => options.onBatch(messages));
|
|
177
|
-
|
|
189
|
+
response.responses.forEach(({ topicId, partitions }) => {
|
|
190
|
+
partitions.forEach(({ partitionIndex, records }) => {
|
|
191
|
+
records.forEach(({ baseOffset, lastOffsetDelta }) => {
|
|
192
|
+
this.offsetManager.resolve(this.metadata.getTopicNameById(topicId), partitionIndex, baseOffset + BigInt(lastOffsetDelta) + 1n);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
178
196
|
await this.consumerGroup?.offsetCommit(topicPartitions);
|
|
179
197
|
this.offsetManager.flush(topicPartitions);
|
|
180
198
|
}
|
|
181
199
|
fetch(nodeId, assignment) {
|
|
182
200
|
const { rackId, maxWaitMs, minBytes, maxBytes, partitionMaxBytes, isolationLevel } = this.options;
|
|
201
|
+
this.consumerGroup?.handleLastHeartbeat();
|
|
183
202
|
return this.cluster.sendRequestToNode(nodeId)(api_1.API.FETCH, {
|
|
184
203
|
maxWaitMs,
|
|
185
204
|
minBytes,
|
|
@@ -194,7 +213,7 @@ class Consumer extends events_1.default {
|
|
|
194
213
|
currentLeaderEpoch: -1,
|
|
195
214
|
fetchOffset: this.offsetManager.getCurrentOffset(topic, partition),
|
|
196
215
|
lastFetchedEpoch: -1,
|
|
197
|
-
logStartOffset:
|
|
216
|
+
logStartOffset: -1n,
|
|
198
217
|
partitionMaxBytes,
|
|
199
218
|
})),
|
|
200
219
|
})),
|
|
@@ -217,8 +236,8 @@ __decorate([
|
|
|
217
236
|
__metadata("design:returntype", Promise)
|
|
218
237
|
], Consumer.prototype, "close", null);
|
|
219
238
|
__decorate([
|
|
220
|
-
trace(
|
|
239
|
+
trace(),
|
|
221
240
|
__metadata("design:type", Function),
|
|
222
|
-
__metadata("design:paramtypes", [
|
|
241
|
+
__metadata("design:paramtypes", [Object]),
|
|
223
242
|
__metadata("design:returntype", Promise)
|
|
224
243
|
], Consumer.prototype, "process", null);
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import { FetchResponse } from '../api/fetch';
|
|
2
2
|
import { Assignment } from '../api/sync-group';
|
|
3
|
-
import { Metadata } from '../metadata';
|
|
4
|
-
import { Batch, Message } from '../types';
|
|
5
|
-
import { ConsumerGroup } from './consumer-group';
|
|
6
3
|
type FetchManagerOptions = {
|
|
7
4
|
fetch: (nodeId: number, assignment: Assignment) => Promise<FetchResponse>;
|
|
8
|
-
process: (
|
|
9
|
-
batchSize?: number | null;
|
|
10
|
-
metadata: Metadata;
|
|
11
|
-
consumerGroup?: ConsumerGroup;
|
|
5
|
+
process: (response: FetchResponse) => Promise<void>;
|
|
12
6
|
nodeAssignments: {
|
|
13
7
|
nodeId: number;
|
|
14
8
|
assignment: Assignment;
|
|
@@ -16,16 +10,9 @@ type FetchManagerOptions = {
|
|
|
16
10
|
};
|
|
17
11
|
export declare class FetchManager {
|
|
18
12
|
private options;
|
|
19
|
-
private queue;
|
|
20
|
-
private isRunning;
|
|
21
13
|
private fetchers;
|
|
22
|
-
private processor;
|
|
23
|
-
private pollCallback;
|
|
24
|
-
private fetcherCallbacks;
|
|
25
14
|
constructor(options: FetchManagerOptions);
|
|
26
15
|
start(): Promise<void>;
|
|
27
|
-
stop(): Promise<void>;
|
|
28
|
-
poll(): Promise<Required<Message>[]>;
|
|
29
|
-
private onResponse;
|
|
16
|
+
stop(): Promise<void[]>;
|
|
30
17
|
}
|
|
31
18
|
export {};
|
|
@@ -12,82 +12,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.FetchManager = void 0;
|
|
13
13
|
const tracer_1 = require("../utils/tracer");
|
|
14
14
|
const fetcher_1 = require("./fetcher");
|
|
15
|
-
const processor_1 = require("./processor");
|
|
16
15
|
const trace = (0, tracer_1.createTracer)('FetchManager');
|
|
17
16
|
class FetchManager {
|
|
18
17
|
options;
|
|
19
|
-
queue = [];
|
|
20
|
-
isRunning = false;
|
|
21
18
|
fetchers;
|
|
22
|
-
processor;
|
|
23
|
-
pollCallback;
|
|
24
|
-
fetcherCallbacks = {};
|
|
25
19
|
constructor(options) {
|
|
26
20
|
this.options = options;
|
|
27
21
|
const { fetch, process, nodeAssignments } = this.options;
|
|
28
|
-
this.fetchers = nodeAssignments.map(({ nodeId, assignment }
|
|
29
|
-
nodeId,
|
|
30
|
-
assignment,
|
|
31
|
-
fetch,
|
|
32
|
-
onResponse: this.onResponse.bind(this),
|
|
33
|
-
}));
|
|
34
|
-
this.processor = new processor_1.Processor({ process, poll: this.poll.bind(this) });
|
|
22
|
+
this.fetchers = nodeAssignments.map(({ nodeId, assignment }) => new fetcher_1.Fetcher({ nodeId, assignment, fetch, process }));
|
|
35
23
|
}
|
|
36
24
|
async start() {
|
|
37
|
-
this.queue = [];
|
|
38
|
-
this.isRunning = true;
|
|
39
25
|
try {
|
|
40
|
-
await Promise.all(
|
|
26
|
+
await Promise.all(this.fetchers.map((fetcher) => fetcher.loop()));
|
|
41
27
|
}
|
|
42
28
|
finally {
|
|
43
29
|
await this.stop();
|
|
44
30
|
}
|
|
45
31
|
}
|
|
46
32
|
async stop() {
|
|
47
|
-
this.
|
|
48
|
-
const stopPromise = Promise.all([...this.fetchers.map((fetcher) => fetcher.stop()), this.processor.stop()]);
|
|
49
|
-
this.pollCallback?.();
|
|
50
|
-
Object.values(this.fetcherCallbacks).forEach((callback) => callback());
|
|
51
|
-
this.fetcherCallbacks = {};
|
|
52
|
-
await stopPromise;
|
|
53
|
-
}
|
|
54
|
-
async poll() {
|
|
55
|
-
if (!this.isRunning) {
|
|
56
|
-
return [];
|
|
57
|
-
}
|
|
58
|
-
const { consumerGroup, batchSize } = this.options;
|
|
59
|
-
consumerGroup?.handleLastHeartbeat();
|
|
60
|
-
const batch = batchSize ? this.queue.splice(0, batchSize) : this.queue.splice(0);
|
|
61
|
-
if (!batch.length) {
|
|
62
|
-
await new Promise((resolve) => (this.pollCallback = resolve));
|
|
63
|
-
return this.poll();
|
|
64
|
-
}
|
|
65
|
-
const [checkpoints, messages] = partition(batch, (entry) => 'kind' in entry && entry.kind === 'checkpoint');
|
|
66
|
-
checkpoints.forEach(({ fetcherId }) => this.fetcherCallbacks[fetcherId]?.());
|
|
67
|
-
return messages;
|
|
68
|
-
}
|
|
69
|
-
async onResponse(fetcherId, response) {
|
|
70
|
-
const { metadata, consumerGroup } = this.options;
|
|
71
|
-
consumerGroup?.handleLastHeartbeat();
|
|
72
|
-
const messages = response.responses.flatMap(({ topicId, partitions }) => partitions.flatMap(({ partitionIndex, records }) => records.flatMap(({ baseTimestamp, baseOffset, records }) => records.flatMap((message) => ({
|
|
73
|
-
topic: metadata.getTopicNameById(topicId),
|
|
74
|
-
partition: partitionIndex,
|
|
75
|
-
key: message.key ?? null,
|
|
76
|
-
value: message.value ?? null,
|
|
77
|
-
headers: Object.fromEntries(message.headers.map(({ key, value }) => [key, value])),
|
|
78
|
-
timestamp: baseTimestamp + BigInt(message.timestampDelta),
|
|
79
|
-
offset: baseOffset + BigInt(message.offsetDelta),
|
|
80
|
-
})))));
|
|
81
|
-
if (!messages.length) {
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
// wait until all broker batches have been processed or fetch manager is requested to stop
|
|
85
|
-
await new Promise((resolve) => {
|
|
86
|
-
this.fetcherCallbacks[fetcherId] = resolve;
|
|
87
|
-
this.queue.push(...messages, { kind: 'checkpoint', fetcherId });
|
|
88
|
-
this.pollCallback?.();
|
|
89
|
-
});
|
|
90
|
-
consumerGroup?.handleLastHeartbeat();
|
|
33
|
+
return Promise.all(this.fetchers.map((fetcher) => fetcher.stop()));
|
|
91
34
|
}
|
|
92
35
|
}
|
|
93
36
|
exports.FetchManager = FetchManager;
|
|
@@ -97,28 +40,3 @@ __decorate([
|
|
|
97
40
|
__metadata("design:paramtypes", []),
|
|
98
41
|
__metadata("design:returntype", Promise)
|
|
99
42
|
], FetchManager.prototype, "start", null);
|
|
100
|
-
__decorate([
|
|
101
|
-
trace(),
|
|
102
|
-
__metadata("design:type", Function),
|
|
103
|
-
__metadata("design:paramtypes", []),
|
|
104
|
-
__metadata("design:returntype", Promise)
|
|
105
|
-
], FetchManager.prototype, "poll", null);
|
|
106
|
-
__decorate([
|
|
107
|
-
trace(),
|
|
108
|
-
__metadata("design:type", Function),
|
|
109
|
-
__metadata("design:paramtypes", [Number, Object]),
|
|
110
|
-
__metadata("design:returntype", Promise)
|
|
111
|
-
], FetchManager.prototype, "onResponse", null);
|
|
112
|
-
const partition = (batch, predicate) => {
|
|
113
|
-
const checkpoints = [];
|
|
114
|
-
const messages = [];
|
|
115
|
-
for (const entry of batch) {
|
|
116
|
-
if (predicate(entry)) {
|
|
117
|
-
checkpoints.push(entry);
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
messages.push(entry);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return [checkpoints, messages];
|
|
124
|
-
};
|
|
@@ -7,15 +7,14 @@ type FetcherOptions = {
|
|
|
7
7
|
nodeId: number;
|
|
8
8
|
assignment: Assignment;
|
|
9
9
|
fetch: (nodeId: number, assignment: Assignment) => Promise<FetchResponse>;
|
|
10
|
-
|
|
10
|
+
process: (response: FetchResponse) => Promise<void>;
|
|
11
11
|
};
|
|
12
12
|
export declare class Fetcher extends EventEmitter<{
|
|
13
13
|
stopped: [];
|
|
14
14
|
}> {
|
|
15
|
-
private fetcherId;
|
|
16
15
|
private options;
|
|
17
16
|
private isRunning;
|
|
18
|
-
constructor(
|
|
17
|
+
constructor(options: FetcherOptions);
|
|
19
18
|
loop(): Promise<void>;
|
|
20
19
|
private step;
|
|
21
20
|
stop(): Promise<void>;
|
package/dist/consumer/fetcher.js
CHANGED
|
@@ -14,12 +14,10 @@ const stream_1 = require("stream");
|
|
|
14
14
|
const tracer_1 = require("../utils/tracer");
|
|
15
15
|
const trace = (0, tracer_1.createTracer)('Fetcher');
|
|
16
16
|
class Fetcher extends stream_1.EventEmitter {
|
|
17
|
-
fetcherId;
|
|
18
17
|
options;
|
|
19
18
|
isRunning = false;
|
|
20
|
-
constructor(
|
|
19
|
+
constructor(options) {
|
|
21
20
|
super();
|
|
22
|
-
this.fetcherId = fetcherId;
|
|
23
21
|
this.options = options;
|
|
24
22
|
}
|
|
25
23
|
async loop() {
|
|
@@ -35,12 +33,11 @@ class Fetcher extends stream_1.EventEmitter {
|
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
35
|
async step() {
|
|
38
|
-
const { nodeId, assignment, fetch,
|
|
36
|
+
const { nodeId, assignment, fetch, process } = this.options;
|
|
39
37
|
const response = await fetch(nodeId, assignment);
|
|
40
|
-
if (!this.isRunning)
|
|
38
|
+
if (!this.isRunning)
|
|
41
39
|
return;
|
|
42
|
-
|
|
43
|
-
await onResponse(this.fetcherId, response);
|
|
40
|
+
await process(response);
|
|
44
41
|
}
|
|
45
42
|
async stop() {
|
|
46
43
|
if (!this.isRunning) {
|
package/dist/metadata.d.ts
CHANGED
|
@@ -16,11 +16,11 @@ export declare class Metadata {
|
|
|
16
16
|
getTopicIdByName(name: string): string;
|
|
17
17
|
getTopicNameById(id: string): string;
|
|
18
18
|
fetchMetadataIfNecessary({ topics, allowTopicAutoCreation, }: {
|
|
19
|
-
topics: string[]
|
|
19
|
+
topics: string[] | Set<string>;
|
|
20
20
|
allowTopicAutoCreation: boolean;
|
|
21
21
|
}): Promise<void>;
|
|
22
22
|
fetchMetadata({ topics, allowTopicAutoCreation, }: {
|
|
23
|
-
topics: string[] | null;
|
|
23
|
+
topics: string[] | Set<string> | null;
|
|
24
24
|
allowTopicAutoCreation: boolean;
|
|
25
25
|
}): Promise<void>;
|
|
26
26
|
}
|
package/dist/metadata.js
CHANGED
|
@@ -41,7 +41,7 @@ class Metadata {
|
|
|
41
41
|
return this.topicNameById[id];
|
|
42
42
|
}
|
|
43
43
|
async fetchMetadataIfNecessary({ topics, allowTopicAutoCreation, }) {
|
|
44
|
-
const missingTopics = topics.filter((topic) => !this.topicPartitions[topic]);
|
|
44
|
+
const missingTopics = Array.from(topics).filter((topic) => !this.topicPartitions[topic]);
|
|
45
45
|
if (!missingTopics.length) {
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
@@ -63,7 +63,7 @@ class Metadata {
|
|
|
63
63
|
const { cluster } = this.options;
|
|
64
64
|
const response = await cluster.sendRequest(api_1.API.METADATA, {
|
|
65
65
|
allowTopicAutoCreation,
|
|
66
|
-
topics: topics
|
|
66
|
+
topics: topics ? Array.from(topics).map((name) => ({ id: null, name })) : null,
|
|
67
67
|
});
|
|
68
68
|
this.topicPartitions = {
|
|
69
69
|
...this.topicPartitions,
|
|
@@ -40,9 +40,13 @@ class Producer {
|
|
|
40
40
|
await this.ensureConnected();
|
|
41
41
|
const { allowTopicAutoCreation } = this.options;
|
|
42
42
|
const defaultTimestamp = BigInt(Date.now());
|
|
43
|
-
const topics =
|
|
43
|
+
const topics = new Set(messages.map((message) => message.topic));
|
|
44
44
|
await this.metadata.fetchMetadataIfNecessary({ topics, allowTopicAutoCreation });
|
|
45
|
-
const
|
|
45
|
+
const partitionedMessages = messages.map((message) => {
|
|
46
|
+
message.partition = this.partition(message);
|
|
47
|
+
return message;
|
|
48
|
+
});
|
|
49
|
+
const nodeTopicPartitionMessages = (0, messages_to_topic_partition_leaders_1.distributeMessagesToTopicPartitionLeaders)(partitionedMessages, this.metadata.getTopicPartitionLeaderIds());
|
|
46
50
|
try {
|
|
47
51
|
await Promise.all(Object.entries(nodeTopicPartitionMessages).map(async ([nodeId, topicPartitionMessages]) => {
|
|
48
52
|
const topicData = Object.entries(topicPartitionMessages).map(([topic, partitionMessages]) => ({
|