@platformatic/kafka 0.1.0 → 0.2.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/README.md +15 -10
- package/dist/apis/admin/consumer-group-describe.js +2 -2
- package/dist/apis/producer/txn-offset-commit.js +4 -7
- package/dist/clients/base/base.d.ts +6 -0
- package/dist/clients/base/base.js +15 -4
- package/dist/clients/base/options.d.ts +4 -0
- package/dist/clients/base/options.js +2 -1
- package/dist/clients/base/types.d.ts +2 -0
- package/dist/clients/consumer/consumer.d.ts +5 -2
- package/dist/clients/consumer/consumer.js +62 -31
- package/dist/clients/consumer/messages-stream.js +11 -1
- package/dist/clients/consumer/options.js +31 -1
- package/dist/clients/consumer/topics-map.d.ts +3 -0
- package/dist/clients/consumer/topics-map.js +14 -0
- package/dist/clients/metrics.d.ts +54 -0
- package/dist/clients/metrics.js +18 -0
- package/dist/clients/producer/producer.d.ts +2 -0
- package/dist/clients/producer/producer.js +32 -3
- package/dist/utils.d.ts +7 -2
- package/dist/utils.js +27 -3
- package/package.json +20 -21
package/README.md
CHANGED
|
@@ -24,13 +24,13 @@ npm install @platformatic/kafka
|
|
|
24
24
|
### Producer
|
|
25
25
|
|
|
26
26
|
```typescript
|
|
27
|
-
import { Producer,
|
|
27
|
+
import { Producer, stringSerializers } from '@platformatic/kafka'
|
|
28
28
|
|
|
29
29
|
// Create a producer with string serialisers
|
|
30
30
|
const producer = new Producer({
|
|
31
31
|
clientId: 'my-producer',
|
|
32
32
|
bootstrapBrokers: ['localhost:9092'],
|
|
33
|
-
serializers:
|
|
33
|
+
serializers: stringSerializers
|
|
34
34
|
})
|
|
35
35
|
|
|
36
36
|
// Send messages
|
|
@@ -52,7 +52,7 @@ await producer.close()
|
|
|
52
52
|
### Consumer
|
|
53
53
|
|
|
54
54
|
```typescript
|
|
55
|
-
import { Consumer,
|
|
55
|
+
import { Consumer, stringDeserializers } from '@platformatic/kafka'
|
|
56
56
|
import { forEach } from 'hwp'
|
|
57
57
|
|
|
58
58
|
// Create a consumer with string deserialisers
|
|
@@ -60,7 +60,7 @@ const consumer = new Consumer({
|
|
|
60
60
|
groupId: 'my-consumer-group',
|
|
61
61
|
clientId: 'my-consumer',
|
|
62
62
|
bootstrapBrokers: ['localhost:9092'],
|
|
63
|
-
deserializers:
|
|
63
|
+
deserializers: stringDeserializers
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
// Create a consumer stream
|
|
@@ -83,10 +83,14 @@ for await (const message of stream) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// Option 3: Concurrent processing
|
|
86
|
-
await forEach(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
86
|
+
await forEach(
|
|
87
|
+
stream,
|
|
88
|
+
async message => {
|
|
89
|
+
console.log(`Received: ${message.key} -> ${message.value}`)
|
|
90
|
+
// Process message...
|
|
91
|
+
},
|
|
92
|
+
16
|
|
93
|
+
) // 16 is the concurrency level
|
|
90
94
|
|
|
91
95
|
// Close the consumer when done
|
|
92
96
|
await consumer.close()
|
|
@@ -164,7 +168,7 @@ type Strings = string[]
|
|
|
164
168
|
|
|
165
169
|
const producer = new Producer({
|
|
166
170
|
clientId: 'my-producer',
|
|
167
|
-
bootstrapBrokers: ['localhost:
|
|
171
|
+
bootstrapBrokers: ['localhost:9092'],
|
|
168
172
|
serializers: {
|
|
169
173
|
key: stringSerializer,
|
|
170
174
|
value: jsonSerializer<Strings>
|
|
@@ -174,7 +178,7 @@ const producer = new Producer({
|
|
|
174
178
|
const consumer = new Consumer({
|
|
175
179
|
groupId: 'my-consumer-group',
|
|
176
180
|
clientId: 'my-consumer',
|
|
177
|
-
bootstrapBrokers: ['localhost:
|
|
181
|
+
bootstrapBrokers: ['localhost:9092'],
|
|
178
182
|
deserializers: {
|
|
179
183
|
key: stringDeserializer,
|
|
180
184
|
value: jsonDeserializer<Strings>
|
|
@@ -259,6 +263,7 @@ Many of the methods accept the same options as the client's constructors. The co
|
|
|
259
263
|
- [Consumer API](./docs/consumer.md)
|
|
260
264
|
- [Admin API](./docs/admin.md)
|
|
261
265
|
- [Base Client](./docs/base.md)
|
|
266
|
+
- [Metrics](./docs/metrics.md)
|
|
262
267
|
- [Other APIs and Types](./docs/other.md)
|
|
263
268
|
|
|
264
269
|
## Requirements
|
|
@@ -76,7 +76,7 @@ export function parseResponse(_correlationId, apiKey, apiVersion, reader) {
|
|
|
76
76
|
return {
|
|
77
77
|
topicId: r.readUUID(),
|
|
78
78
|
topicName: r.readString(),
|
|
79
|
-
partitions: r.readArray(() => r.readInt32())
|
|
79
|
+
partitions: r.readArray(() => r.readInt32(), true, false)
|
|
80
80
|
};
|
|
81
81
|
})
|
|
82
82
|
},
|
|
@@ -85,7 +85,7 @@ export function parseResponse(_correlationId, apiKey, apiVersion, reader) {
|
|
|
85
85
|
return {
|
|
86
86
|
topicId: r.readUUID(),
|
|
87
87
|
topicName: r.readString(),
|
|
88
|
-
partitions: r.readArray(() => r.readInt32())
|
|
88
|
+
partitions: r.readArray(() => r.readInt32(), true, false)
|
|
89
89
|
};
|
|
90
90
|
})
|
|
91
91
|
}
|
|
@@ -28,16 +28,13 @@ export function createRequest(transactionalId, groupId, producerId, producerEpoc
|
|
|
28
28
|
.appendString(memberId, true)
|
|
29
29
|
.appendString(groupInstanceId, true)
|
|
30
30
|
.appendArray(topics, (w, t) => {
|
|
31
|
-
w.appendString(t.name, true)
|
|
32
|
-
.appendArray(t.partitions, (w, p) => {
|
|
31
|
+
w.appendString(t.name, true).appendArray(t.partitions, (w, p) => {
|
|
33
32
|
w.appendInt32(p.partitionIndex)
|
|
34
33
|
.appendInt64(p.committedOffset)
|
|
35
34
|
.appendInt32(p.committedLeaderEpoch)
|
|
36
|
-
.appendString(p.committedMetadata, true)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.appendTaggedFields(); // Add tagged fields for topics
|
|
40
|
-
}, true, true)
|
|
35
|
+
.appendString(p.committedMetadata, true);
|
|
36
|
+
});
|
|
37
|
+
})
|
|
41
38
|
.appendTaggedFields();
|
|
42
39
|
}
|
|
43
40
|
/*
|
|
@@ -4,11 +4,13 @@ import { type Callback } from '../../apis/definitions.ts';
|
|
|
4
4
|
import { ConnectionPool } from '../../network/connection-pool.ts';
|
|
5
5
|
import { type Broker } from '../../network/connection.ts';
|
|
6
6
|
import { type CallbackWithPromise } from '../callbacks.ts';
|
|
7
|
+
import { type Metrics } from '../metrics.ts';
|
|
7
8
|
import { type BaseOptions, type ClusterMetadata, type MetadataOptions } from './types.ts';
|
|
8
9
|
export declare const kClientId: unique symbol;
|
|
9
10
|
export declare const kBootstrapBrokers: unique symbol;
|
|
10
11
|
export declare const kOptions: unique symbol;
|
|
11
12
|
export declare const kConnections: unique symbol;
|
|
13
|
+
export declare const kFetchConnections: unique symbol;
|
|
12
14
|
export declare const kCreateConnectionPool: unique symbol;
|
|
13
15
|
export declare const kClosed: unique symbol;
|
|
14
16
|
export declare const kMetadata: unique symbol;
|
|
@@ -19,7 +21,9 @@ export declare const kPerformWithRetry: unique symbol;
|
|
|
19
21
|
export declare const kPerformDeduplicated: unique symbol;
|
|
20
22
|
export declare const kValidateOptions: unique symbol;
|
|
21
23
|
export declare const kInspect: unique symbol;
|
|
24
|
+
export declare const kFormatValidationErrors: unique symbol;
|
|
22
25
|
export declare const kInstance: unique symbol;
|
|
26
|
+
export declare const kPrometheus: unique symbol;
|
|
23
27
|
export declare class Base<OptionsType extends BaseOptions> extends EventEmitter {
|
|
24
28
|
#private;
|
|
25
29
|
[kInstance]: number;
|
|
@@ -28,6 +32,7 @@ export declare class Base<OptionsType extends BaseOptions> extends EventEmitter
|
|
|
28
32
|
[kOptions]: OptionsType;
|
|
29
33
|
[kConnections]: ConnectionPool;
|
|
30
34
|
[kClosed]: boolean;
|
|
35
|
+
[kPrometheus]: Metrics | undefined;
|
|
31
36
|
constructor(options: OptionsType);
|
|
32
37
|
get clientId(): string;
|
|
33
38
|
get closed(): boolean;
|
|
@@ -45,4 +50,5 @@ export declare class Base<OptionsType extends BaseOptions> extends EventEmitter
|
|
|
45
50
|
[kPerformDeduplicated]<ReturnType>(operationId: string, operation: (callback: CallbackWithPromise<ReturnType>) => void, callback: CallbackWithPromise<ReturnType>): void | Promise<ReturnType>;
|
|
46
51
|
[kValidateOptions](target: unknown, validator: ValidateFunction<unknown>, targetName: string, throwOnErrors?: boolean): Error | null;
|
|
47
52
|
[kInspect](...args: unknown[]): void;
|
|
53
|
+
[kFormatValidationErrors](validator: ValidateFunction<unknown>, targetName: string): string;
|
|
48
54
|
}
|
|
@@ -8,9 +8,10 @@ import { baseOptionsValidator, defaultBaseOptions, defaultPort, metadataOptionsV
|
|
|
8
8
|
export const kClientId = Symbol('plt.kafka.base.clientId');
|
|
9
9
|
export const kBootstrapBrokers = Symbol('plt.kafka.base.bootstrapBrokers');
|
|
10
10
|
export const kOptions = Symbol('plt.kafka.base.options');
|
|
11
|
-
export const kConnections = Symbol('plt.kafka.base.
|
|
12
|
-
export const
|
|
13
|
-
export const
|
|
11
|
+
export const kConnections = Symbol('plt.kafka.base.connections');
|
|
12
|
+
export const kFetchConnections = Symbol('plt.kafka.base.fetchCnnections');
|
|
13
|
+
export const kCreateConnectionPool = Symbol('plt.kafka.base.createConnectionPool');
|
|
14
|
+
export const kClosed = Symbol('plt.kafka.base.closed');
|
|
14
15
|
export const kMetadata = Symbol('plt.kafka.base.metadata');
|
|
15
16
|
export const kCheckNotClosed = Symbol('plt.kafka.base.checkNotClosed');
|
|
16
17
|
export const kClearMetadata = Symbol('plt.kafka.base.clearMetadata');
|
|
@@ -19,7 +20,9 @@ export const kPerformWithRetry = Symbol('plt.kafka.base.performWithRetry');
|
|
|
19
20
|
export const kPerformDeduplicated = Symbol('plt.kafka.base.performDeduplicated');
|
|
20
21
|
export const kValidateOptions = Symbol('plt.kafka.base.validateOptions');
|
|
21
22
|
export const kInspect = Symbol('plt.kafka.base.inspect');
|
|
23
|
+
export const kFormatValidationErrors = Symbol('plt.kafka.base.formatValidationErrors');
|
|
22
24
|
export const kInstance = Symbol('plt.kafka.base.instance');
|
|
25
|
+
export const kPrometheus = Symbol('plt.kafka.base.prometheus');
|
|
23
26
|
let currentInstance = 0;
|
|
24
27
|
export class Base extends EventEmitter {
|
|
25
28
|
// This is just used for debugging
|
|
@@ -30,6 +33,7 @@ export class Base extends EventEmitter {
|
|
|
30
33
|
[kOptions];
|
|
31
34
|
[kConnections];
|
|
32
35
|
[kClosed];
|
|
36
|
+
[kPrometheus];
|
|
33
37
|
#metadata;
|
|
34
38
|
#inflightDeduplications;
|
|
35
39
|
constructor(options) {
|
|
@@ -48,6 +52,10 @@ export class Base extends EventEmitter {
|
|
|
48
52
|
this[kConnections] = this[kCreateConnectionPool]();
|
|
49
53
|
this[kClosed] = false;
|
|
50
54
|
this.#inflightDeduplications = new Map();
|
|
55
|
+
// Initialize metrics
|
|
56
|
+
if (options.metrics) {
|
|
57
|
+
this[kPrometheus] = options.metrics;
|
|
58
|
+
}
|
|
51
59
|
}
|
|
52
60
|
/* c8 ignore next 3 */
|
|
53
61
|
get clientId() {
|
|
@@ -226,7 +234,7 @@ export class Base extends EventEmitter {
|
|
|
226
234
|
}
|
|
227
235
|
const valid = validator(target);
|
|
228
236
|
if (!valid) {
|
|
229
|
-
const error = new UserError(
|
|
237
|
+
const error = new UserError(this[kFormatValidationErrors](validator, targetName));
|
|
230
238
|
if (throwOnErrors) {
|
|
231
239
|
throw error;
|
|
232
240
|
}
|
|
@@ -239,4 +247,7 @@ export class Base extends EventEmitter {
|
|
|
239
247
|
[kInspect](...args) {
|
|
240
248
|
debugDump(`client:${this[kInstance]}`, ...args);
|
|
241
249
|
}
|
|
250
|
+
[kFormatValidationErrors](validator, targetName) {
|
|
251
|
+
return ajv.errorsText(validator.errors, { dataVar: '$dataVar$' }).replaceAll('$dataVar$', targetName) + '.';
|
|
252
|
+
}
|
|
242
253
|
}
|
|
@@ -28,7 +28,8 @@ export const baseOptionsSchema = {
|
|
|
28
28
|
maxInflights: { type: 'number', minimum: 0 },
|
|
29
29
|
metadataMaxAge: { type: 'number', minimum: 0 },
|
|
30
30
|
autocreateTopics: { type: 'boolean' },
|
|
31
|
-
strict: { type: 'boolean' }
|
|
31
|
+
strict: { type: 'boolean' },
|
|
32
|
+
metrics: { type: 'object', additionalProperties: true }
|
|
32
33
|
},
|
|
33
34
|
required: ['clientId', 'bootstrapBrokers'],
|
|
34
35
|
additionalProperties: true
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Broker, type ConnectionOptions } from '../../network/connection.ts';
|
|
2
|
+
import { type Metrics } from '../metrics.ts';
|
|
2
3
|
export interface TopicWithPartitionAndOffset {
|
|
3
4
|
topic: string;
|
|
4
5
|
partition: number;
|
|
@@ -29,6 +30,7 @@ export interface BaseOptions extends ConnectionOptions {
|
|
|
29
30
|
metadataMaxAge?: number;
|
|
30
31
|
autocreateTopics?: boolean;
|
|
31
32
|
strict?: boolean;
|
|
33
|
+
metrics?: Metrics;
|
|
32
34
|
}
|
|
33
35
|
export interface MetadataOptions {
|
|
34
36
|
topics: string[];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type FetchResponse } from '../../apis/consumer/fetch.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { type ConnectionPool } from '../../network/connection-pool.ts';
|
|
3
|
+
import { Base, kFetchConnections } from '../base/base.ts';
|
|
3
4
|
import { type CallbackWithPromise } from '../callbacks.ts';
|
|
4
5
|
import { MessagesStream } from './messages-stream.ts';
|
|
5
6
|
import { TopicsMap } from './topics-map.ts';
|
|
@@ -11,7 +12,9 @@ export declare class Consumer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
|
|
|
11
12
|
memberId: string | null;
|
|
12
13
|
topics: TopicsMap;
|
|
13
14
|
assignments: GroupAssignment[] | null;
|
|
15
|
+
[kFetchConnections]: ConnectionPool;
|
|
14
16
|
constructor(options: ConsumerOptions<Key, Value, HeaderKey, HeaderValue>);
|
|
17
|
+
get streamsCount(): number;
|
|
15
18
|
close(force: boolean | CallbackWithPromise<void>, callback?: CallbackWithPromise<void>): void;
|
|
16
19
|
close(force?: boolean): Promise<void>;
|
|
17
20
|
consume(options: ConsumeOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<MessagesStream<Key, Value, HeaderKey, HeaderValue>>): void;
|
|
@@ -28,6 +31,6 @@ export declare class Consumer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
|
|
|
28
31
|
findGroupCoordinator(): Promise<number>;
|
|
29
32
|
joinGroup(options: GroupOptions, callback: CallbackWithPromise<string>): void;
|
|
30
33
|
joinGroup(options: GroupOptions): Promise<string>;
|
|
31
|
-
leaveGroup(force
|
|
34
|
+
leaveGroup(force?: boolean | CallbackWithPromise<void>, callback?: CallbackWithPromise<void>): void;
|
|
32
35
|
leaveGroup(force?: boolean): Promise<void>;
|
|
33
36
|
}
|
|
@@ -11,9 +11,10 @@ import { api as findCoordinatorV6 } from "../../apis/metadata/find-coordinator.j
|
|
|
11
11
|
import { UserError } from "../../errors.js";
|
|
12
12
|
import { Reader } from "../../protocol/reader.js";
|
|
13
13
|
import { Writer } from "../../protocol/writer.js";
|
|
14
|
-
import { Base, kBootstrapBrokers, kCheckNotClosed, kClosed, kConnections, kCreateConnectionPool, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from "../base/base.js";
|
|
14
|
+
import { Base, kBootstrapBrokers, kCheckNotClosed, kClosed, kConnections, kCreateConnectionPool, kFetchConnections, kFormatValidationErrors, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kPrometheus, kValidateOptions } from "../base/base.js";
|
|
15
15
|
import { defaultBaseOptions } from "../base/options.js";
|
|
16
16
|
import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../callbacks.js";
|
|
17
|
+
import { ensureMetric } from "../metrics.js";
|
|
17
18
|
import { MessagesStream } from "./messages-stream.js";
|
|
18
19
|
import { commitOptionsValidator, consumeOptionsValidator, consumerOptionsValidator, defaultConsumerOptions, fetchOptionsValidator, groupOptionsValidator, listCommitsOptionsValidator, listOffsetsOptionsValidator } from "./options.js";
|
|
19
20
|
import { TopicsMap } from "./topics-map.js";
|
|
@@ -44,7 +45,9 @@ export class Consumer extends Base {
|
|
|
44
45
|
|
|
45
46
|
In order to avoid consumer group problems, we separate FetchRequest only on a separate connection.
|
|
46
47
|
*/
|
|
47
|
-
|
|
48
|
+
[kFetchConnections];
|
|
49
|
+
// Metrics
|
|
50
|
+
#metricActiveStreams;
|
|
48
51
|
constructor(options) {
|
|
49
52
|
super(options);
|
|
50
53
|
this[kOptions] = Object.assign({}, defaultBaseOptions, defaultConsumerOptions, options);
|
|
@@ -63,7 +66,15 @@ export class Consumer extends Base {
|
|
|
63
66
|
this.#streams = new Set();
|
|
64
67
|
this.#validateGroupOptions(this[kOptions]);
|
|
65
68
|
// Initialize connection pool
|
|
66
|
-
this
|
|
69
|
+
this[kFetchConnections] = this[kCreateConnectionPool]();
|
|
70
|
+
if (this[kPrometheus]) {
|
|
71
|
+
ensureMetric(this[kPrometheus], 'Gauge', 'kafka_consumers', 'Number of active Kafka consumers').inc();
|
|
72
|
+
this.#metricActiveStreams = ensureMetric(this[kPrometheus], 'Gauge', 'kafka_consumers_streams', 'Number of active Kafka consumers streams');
|
|
73
|
+
this.topics.setMetric(ensureMetric(this[kPrometheus], 'Gauge', 'kafka_consumers_topics', 'Number of topics being consumed'));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
get streamsCount() {
|
|
77
|
+
return this.#streams.size;
|
|
67
78
|
}
|
|
68
79
|
close(force, callback) {
|
|
69
80
|
if (typeof force === 'function') {
|
|
@@ -80,20 +91,33 @@ export class Consumer extends Base {
|
|
|
80
91
|
this[kClosed] = true;
|
|
81
92
|
const closer = this.#membershipActive
|
|
82
93
|
? this.#leaveGroup.bind(this)
|
|
83
|
-
: function (_, callback) {
|
|
94
|
+
: function noopCloser(_, callback) {
|
|
84
95
|
callback(null);
|
|
85
96
|
};
|
|
86
97
|
closer(force, error => {
|
|
87
98
|
if (error) {
|
|
99
|
+
this[kClosed] = false;
|
|
88
100
|
callback(error);
|
|
89
101
|
return;
|
|
90
102
|
}
|
|
91
|
-
this
|
|
103
|
+
this[kFetchConnections].close(error => {
|
|
92
104
|
if (error) {
|
|
105
|
+
this[kClosed] = false;
|
|
93
106
|
callback(error);
|
|
94
107
|
return;
|
|
95
108
|
}
|
|
96
|
-
super.close(
|
|
109
|
+
super.close(error => {
|
|
110
|
+
if (error) {
|
|
111
|
+
this[kClosed] = false;
|
|
112
|
+
callback(error);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.topics.clear();
|
|
116
|
+
if (this[kPrometheus]) {
|
|
117
|
+
ensureMetric(this[kPrometheus], 'Gauge', 'kafka_consumers', 'Number of active Kafka consumers').dec();
|
|
118
|
+
}
|
|
119
|
+
callback(null);
|
|
120
|
+
});
|
|
97
121
|
});
|
|
98
122
|
});
|
|
99
123
|
return callback[kCallbackPromise];
|
|
@@ -224,15 +248,24 @@ export class Consumer extends Base {
|
|
|
224
248
|
return callback[kCallbackPromise];
|
|
225
249
|
}
|
|
226
250
|
this.#membershipActive = false;
|
|
227
|
-
this.#leaveGroup(force,
|
|
251
|
+
this.#leaveGroup(force, error => {
|
|
252
|
+
if (error) {
|
|
253
|
+
this.#membershipActive = true;
|
|
254
|
+
callback(error);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
callback(null);
|
|
258
|
+
});
|
|
228
259
|
return callback[kCallbackPromise];
|
|
229
260
|
}
|
|
230
|
-
#consume(options, callback) {
|
|
261
|
+
#consume(options, callback, trackTopics = true) {
|
|
231
262
|
// Subscribe all topics
|
|
232
263
|
let joinNeeded = this.memberId === null;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
264
|
+
if (trackTopics) {
|
|
265
|
+
for (const topic of options.topics) {
|
|
266
|
+
if (this.topics.track(topic)) {
|
|
267
|
+
joinNeeded = true;
|
|
268
|
+
}
|
|
236
269
|
}
|
|
237
270
|
}
|
|
238
271
|
// If we need to (re)join the group, do that first and then try again
|
|
@@ -242,14 +275,16 @@ export class Consumer extends Base {
|
|
|
242
275
|
callback(error, undefined);
|
|
243
276
|
return;
|
|
244
277
|
}
|
|
245
|
-
this.#consume(options, callback);
|
|
278
|
+
this.#consume(options, callback, false);
|
|
246
279
|
});
|
|
247
280
|
return callback[kCallbackPromise];
|
|
248
281
|
}
|
|
249
282
|
// Create the stream and start consuming
|
|
250
283
|
const stream = new MessagesStream(this, options);
|
|
251
284
|
this.#streams.add(stream);
|
|
285
|
+
this.#metricActiveStreams?.inc();
|
|
252
286
|
stream.once('close', () => {
|
|
287
|
+
this.#metricActiveStreams?.dec();
|
|
253
288
|
this.#streams.delete(stream);
|
|
254
289
|
});
|
|
255
290
|
callback(null, stream);
|
|
@@ -267,7 +302,7 @@ export class Consumer extends Base {
|
|
|
267
302
|
retryCallback(new UserError(`Cannot find broker with node id ${options.node}`), undefined);
|
|
268
303
|
return;
|
|
269
304
|
}
|
|
270
|
-
this
|
|
305
|
+
this[kFetchConnections].get(broker, (error, connection) => {
|
|
271
306
|
if (error) {
|
|
272
307
|
retryCallback(error, undefined);
|
|
273
308
|
return;
|
|
@@ -522,12 +557,15 @@ export class Consumer extends Base {
|
|
|
522
557
|
memberId: this.memberId,
|
|
523
558
|
generationId: this.generationId
|
|
524
559
|
});
|
|
560
|
+
this.memberId = null;
|
|
561
|
+
this.generationId = 0;
|
|
562
|
+
this.assignments = null;
|
|
525
563
|
callback(null);
|
|
526
564
|
});
|
|
527
565
|
}
|
|
528
566
|
#syncGroup(assignments, callback) {
|
|
529
567
|
if (!this.#membershipActive) {
|
|
530
|
-
callback(null,
|
|
568
|
+
callback(null, []);
|
|
531
569
|
return;
|
|
532
570
|
}
|
|
533
571
|
if (!Array.isArray(assignments)) {
|
|
@@ -549,10 +587,6 @@ export class Consumer extends Base {
|
|
|
549
587
|
callback(error, undefined);
|
|
550
588
|
return;
|
|
551
589
|
}
|
|
552
|
-
if (!this.#membershipActive) {
|
|
553
|
-
callback(null, undefined);
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
590
|
this.#syncGroup(this.#roundRobinAssignments(metadata), callback);
|
|
557
591
|
});
|
|
558
592
|
return;
|
|
@@ -573,10 +607,6 @@ export class Consumer extends Base {
|
|
|
573
607
|
callback(error, undefined);
|
|
574
608
|
return;
|
|
575
609
|
}
|
|
576
|
-
else if (response.assignment.length === 0) {
|
|
577
|
-
callback(null, []);
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
610
|
// Read the assignment back
|
|
581
611
|
const reader = Reader.from(response.assignment);
|
|
582
612
|
const assignments = reader.readArray(r => {
|
|
@@ -593,13 +623,15 @@ export class Consumer extends Base {
|
|
|
593
623
|
this.#performDeduplicateGroupOperaton('heartbeat', (connection, groupCallback) => {
|
|
594
624
|
// We have left the group in the meanwhile, abort
|
|
595
625
|
if (!this.#membershipActive) {
|
|
626
|
+
this.emitWithDebug('consumer:heartbeat', 'cancel', eventPayload);
|
|
596
627
|
return;
|
|
597
628
|
}
|
|
598
|
-
this.emitWithDebug('consumer:heartbeat', 'start');
|
|
629
|
+
this.emitWithDebug('consumer:heartbeat', 'start', eventPayload);
|
|
599
630
|
heartbeatV4(connection, this.groupId, this.generationId, this.memberId, null, groupCallback);
|
|
600
631
|
}, error => {
|
|
601
632
|
// The heartbeat has been aborted elsewhere, ignore the response
|
|
602
633
|
if (this.#heartbeatInterval === null || !this.#membershipActive) {
|
|
634
|
+
this.emitWithDebug('consumer:heartbeat', 'cancel', eventPayload);
|
|
603
635
|
return;
|
|
604
636
|
}
|
|
605
637
|
if (error) {
|
|
@@ -657,14 +689,9 @@ export class Consumer extends Base {
|
|
|
657
689
|
});
|
|
658
690
|
}
|
|
659
691
|
#validateGroupOptions(options) {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
if (options.heartbeatInterval > options.sessionTimeout) {
|
|
664
|
-
throw new UserError('/options/heartbeatInterval must be less than or equal to /options/sessionTimeout.');
|
|
665
|
-
}
|
|
666
|
-
if (options.heartbeatInterval > options.rebalanceTimeout) {
|
|
667
|
-
throw new UserError('/options/heartbeatInterval must be less than or equal to /options/rebalanceTimeout.');
|
|
692
|
+
const valid = groupOptionsValidator(options);
|
|
693
|
+
if (!valid) {
|
|
694
|
+
throw new UserError(this[kFormatValidationErrors](groupOptionsValidator, '/options'));
|
|
668
695
|
}
|
|
669
696
|
}
|
|
670
697
|
/*
|
|
@@ -762,6 +789,10 @@ export class Consumer extends Base {
|
|
|
762
789
|
else if (protocolError.memberId && !this.memberId) {
|
|
763
790
|
this.memberId = protocolError.memberId;
|
|
764
791
|
}
|
|
792
|
+
// This is only used in testing
|
|
793
|
+
if (protocolError.cancelMembership) {
|
|
794
|
+
this.#membershipActive = false;
|
|
795
|
+
}
|
|
765
796
|
return protocolError;
|
|
766
797
|
}
|
|
767
798
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
2
|
import { ListOffsetTimestamps } from "../../apis/enumerations.js";
|
|
3
3
|
import { UserError } from "../../errors.js";
|
|
4
|
-
import { kInspect } from "../base/base.js";
|
|
4
|
+
import { kInspect, kPrometheus } from "../base/base.js";
|
|
5
5
|
import { createPromisifiedCallback, kCallbackPromise, noopCallback } from "../callbacks.js";
|
|
6
|
+
import { ensureMetric } from "../metrics.js";
|
|
6
7
|
import { MessagesStreamFallbackModes, MessagesStreamModes } from "./types.js";
|
|
7
8
|
// Don't move this function as being in the same file will enable V8 to remove.
|
|
8
9
|
// For futher info, ask Matteo.
|
|
@@ -28,6 +29,7 @@ export class MessagesStream extends Readable {
|
|
|
28
29
|
#autocommitInflight;
|
|
29
30
|
#shouldClose;
|
|
30
31
|
#closeCallbacks;
|
|
32
|
+
#metricsConsumedMessages;
|
|
31
33
|
constructor(consumer, options) {
|
|
32
34
|
const { autocommit, mode, fallbackMode, offsets, deserializers, ..._options } = options;
|
|
33
35
|
if (offsets && mode !== MessagesStreamModes.MANUAL) {
|
|
@@ -82,6 +84,9 @@ export class MessagesStream extends Readable {
|
|
|
82
84
|
this.#fetch();
|
|
83
85
|
});
|
|
84
86
|
});
|
|
87
|
+
if (consumer[kPrometheus]) {
|
|
88
|
+
this.#metricsConsumedMessages = ensureMetric(consumer[kPrometheus], 'Counter', 'kafka_consumers_messages', 'Number of consumed Kafka messages');
|
|
89
|
+
}
|
|
85
90
|
}
|
|
86
91
|
close(callback) {
|
|
87
92
|
if (!callback) {
|
|
@@ -270,6 +275,7 @@ export class MessagesStream extends Readable {
|
|
|
270
275
|
for (const [headerKey, headerValue] of record.headers) {
|
|
271
276
|
headers.set(headerKeyDeserializer(headerKey), headerValueDeserializer(headerValue));
|
|
272
277
|
}
|
|
278
|
+
this.#metricsConsumedMessages?.inc();
|
|
273
279
|
canPush = this.push({
|
|
274
280
|
key,
|
|
275
281
|
value,
|
|
@@ -327,6 +333,10 @@ export class MessagesStream extends Readable {
|
|
|
327
333
|
});
|
|
328
334
|
}
|
|
329
335
|
#refreshOffsets(callback) {
|
|
336
|
+
if (this.#topics.length === 0) {
|
|
337
|
+
callback(null);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
330
340
|
// List topic offsets
|
|
331
341
|
this.#consumer.listOffsets({
|
|
332
342
|
topics: this.#topics,
|
|
@@ -23,6 +23,33 @@ const groupOptionsProperties = {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
|
+
const groupOptionsAdditionalValidations = {
|
|
27
|
+
rebalanceTimeout: {
|
|
28
|
+
properties: {
|
|
29
|
+
rebalanceTimeout: {
|
|
30
|
+
type: 'number',
|
|
31
|
+
minimum: 0,
|
|
32
|
+
gteProperty: 'sessionTimeout'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
heartbeatInterval: {
|
|
37
|
+
properties: {
|
|
38
|
+
heartbeatInterval: {
|
|
39
|
+
type: 'number',
|
|
40
|
+
minimum: 0,
|
|
41
|
+
allOf: [
|
|
42
|
+
{
|
|
43
|
+
lteProperty: 'sessionTimeout'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
lteProperty: 'rebalanceTimeout'
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
26
53
|
const consumeOptionsProperties = {
|
|
27
54
|
autocommit: { oneOf: [{ type: 'boolean' }, { type: 'number', minimum: 100 }] },
|
|
28
55
|
minBytes: { type: 'number', minimum: 0 },
|
|
@@ -156,7 +183,10 @@ export const listOffsetsOptionsSchema = {
|
|
|
156
183
|
required: ['topics'],
|
|
157
184
|
additionalProperties: false
|
|
158
185
|
};
|
|
159
|
-
export const groupOptionsValidator = ajv.compile(
|
|
186
|
+
export const groupOptionsValidator = ajv.compile({
|
|
187
|
+
...groupOptionsSchema,
|
|
188
|
+
dependentSchemas: groupOptionsAdditionalValidations
|
|
189
|
+
});
|
|
160
190
|
export const consumeOptionsValidator = ajv.compile(consumeOptionsSchema);
|
|
161
191
|
export const consumerOptionsValidator = ajv.compile(consumerOptionsSchema);
|
|
162
192
|
export const fetchOptionsValidator = ajv.compile(fetchOptionsSchema);
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { type Gauge } from '../metrics.ts';
|
|
1
2
|
export declare class TopicsMap extends Map<string, number> {
|
|
2
3
|
#private;
|
|
3
4
|
get current(): string[];
|
|
5
|
+
clear(): void;
|
|
4
6
|
track(topic: string): boolean;
|
|
5
7
|
trackAll(...topics: string[]): boolean[];
|
|
6
8
|
untrack(topic: string): boolean;
|
|
7
9
|
untrackAll(...topics: string[]): boolean[];
|
|
10
|
+
setMetric(metric: Gauge): void;
|
|
8
11
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
export class TopicsMap extends Map {
|
|
2
2
|
#current = [];
|
|
3
|
+
#metric;
|
|
3
4
|
get current() {
|
|
4
5
|
return this.#current;
|
|
5
6
|
}
|
|
7
|
+
clear() {
|
|
8
|
+
for (const k of this.keys()) {
|
|
9
|
+
this.untrack(k);
|
|
10
|
+
}
|
|
11
|
+
super.clear();
|
|
12
|
+
}
|
|
6
13
|
track(topic) {
|
|
7
14
|
let updated = false;
|
|
8
15
|
let existing = this.get(topic);
|
|
@@ -11,6 +18,9 @@ export class TopicsMap extends Map {
|
|
|
11
18
|
updated = true;
|
|
12
19
|
}
|
|
13
20
|
this.set(topic, existing + 1);
|
|
21
|
+
if (existing === 0) {
|
|
22
|
+
this.#metric?.inc();
|
|
23
|
+
}
|
|
14
24
|
if (updated) {
|
|
15
25
|
this.#updateCurrentList();
|
|
16
26
|
}
|
|
@@ -28,6 +38,7 @@ export class TopicsMap extends Map {
|
|
|
28
38
|
if (existing === 1) {
|
|
29
39
|
this.delete(topic);
|
|
30
40
|
this.#updateCurrentList();
|
|
41
|
+
this.#metric?.dec();
|
|
31
42
|
return true;
|
|
32
43
|
}
|
|
33
44
|
else if (typeof existing === 'number') {
|
|
@@ -42,6 +53,9 @@ export class TopicsMap extends Map {
|
|
|
42
53
|
}
|
|
43
54
|
return updated;
|
|
44
55
|
}
|
|
56
|
+
setMetric(metric) {
|
|
57
|
+
this.#metric = metric;
|
|
58
|
+
}
|
|
45
59
|
#updateCurrentList() {
|
|
46
60
|
this.#current = Array.from(this.keys());
|
|
47
61
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
type RegistryContentType = 'application/openmetrics-text; version=1.0.0; charset=utf-8' | 'text/plain; version=0.0.4; charset=utf-8';
|
|
2
|
+
export interface Metric {
|
|
3
|
+
name?: string;
|
|
4
|
+
get(): Promise<unknown>;
|
|
5
|
+
reset: () => void;
|
|
6
|
+
labels(labels: any): any;
|
|
7
|
+
}
|
|
8
|
+
export interface Counter extends Metric {
|
|
9
|
+
inc: (value?: number) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface Gauge extends Metric {
|
|
12
|
+
inc: (value?: number) => void;
|
|
13
|
+
dec: (value?: number) => void;
|
|
14
|
+
}
|
|
15
|
+
export interface Histogram {
|
|
16
|
+
}
|
|
17
|
+
export interface Summary {
|
|
18
|
+
}
|
|
19
|
+
export interface Registry {
|
|
20
|
+
getSingleMetric: (name: string) => Counter | Gauge | any;
|
|
21
|
+
metrics(): Promise<string>;
|
|
22
|
+
clear(): void;
|
|
23
|
+
resetMetrics(): void;
|
|
24
|
+
registerMetric(metric: Metric): void;
|
|
25
|
+
getMetricsAsJSON(): Promise<any>;
|
|
26
|
+
getMetricsAsArray(): any[];
|
|
27
|
+
removeSingleMetric(name: string): void;
|
|
28
|
+
setDefaultLabels(labels: object): void;
|
|
29
|
+
getSingleMetricAsString(name: string): Promise<string>;
|
|
30
|
+
readonly contentType: RegistryContentType;
|
|
31
|
+
setContentType(contentType: RegistryContentType): void;
|
|
32
|
+
}
|
|
33
|
+
export interface Prometheus {
|
|
34
|
+
Counter: new (options: {
|
|
35
|
+
name: string;
|
|
36
|
+
help: string;
|
|
37
|
+
registers: Registry[];
|
|
38
|
+
labelNames?: string[];
|
|
39
|
+
}) => Counter;
|
|
40
|
+
Gauge: new (options: {
|
|
41
|
+
name: string;
|
|
42
|
+
help: string;
|
|
43
|
+
registers: Registry[];
|
|
44
|
+
labelNames?: string[];
|
|
45
|
+
}) => Gauge;
|
|
46
|
+
Registry: new (contentType?: string) => Registry;
|
|
47
|
+
}
|
|
48
|
+
export interface Metrics {
|
|
49
|
+
registry: Registry;
|
|
50
|
+
client: Prometheus;
|
|
51
|
+
labels?: Record<string, any>;
|
|
52
|
+
}
|
|
53
|
+
export declare function ensureMetric<MetricType extends Metric>(metrics: Metrics, type: 'Gauge' | 'Counter', name: string, help: string): MetricType;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Interfaces to make the package compatible with prom-client
|
|
2
|
+
export function ensureMetric(metrics, type, name, help) {
|
|
3
|
+
let metric = metrics.registry.getSingleMetric(name);
|
|
4
|
+
const labels = Object.keys(metrics.labels ?? {});
|
|
5
|
+
if (!metric) {
|
|
6
|
+
metric = new metrics.client[type]({
|
|
7
|
+
name,
|
|
8
|
+
help,
|
|
9
|
+
registers: [metrics.registry],
|
|
10
|
+
labelNames: labels
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
// @ts-expect-error Overriding internal API
|
|
15
|
+
metric.labelNames = metric.sortedLabelNames = Array.from(new Set([...metric.labelNames, ...labels])).sort();
|
|
16
|
+
}
|
|
17
|
+
return metric.labels(metrics.labels ?? {});
|
|
18
|
+
}
|
|
@@ -6,6 +6,8 @@ export declare class Producer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
|
|
|
6
6
|
constructor(options: ProducerOptions<Key, Value, HeaderKey, HeaderValue>);
|
|
7
7
|
get producerId(): bigint | undefined;
|
|
8
8
|
get producerEpoch(): number | undefined;
|
|
9
|
+
close(callback: CallbackWithPromise<void>): void;
|
|
10
|
+
close(): Promise<void>;
|
|
9
11
|
initIdempotentProducer(options: ProduceOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<ProducerInfo>): void;
|
|
10
12
|
initIdempotentProducer(options: ProduceOptions<Key, Value, HeaderKey, HeaderValue>): Promise<ProducerInfo>;
|
|
11
13
|
send(options: SendOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<ProduceResult>): void;
|
|
@@ -4,8 +4,9 @@ import { api as produceV11 } from "../../apis/producer/produce.js";
|
|
|
4
4
|
import { UserError } from "../../errors.js";
|
|
5
5
|
import { murmur2 } from "../../protocol/murmur2.js";
|
|
6
6
|
import { NumericMap } from "../../utils.js";
|
|
7
|
-
import { Base, kBootstrapBrokers, kCheckNotClosed, kClearMetadata, kConnections, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from "../base/base.js";
|
|
7
|
+
import { Base, kBootstrapBrokers, kCheckNotClosed, kClearMetadata, kClosed, kConnections, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kPrometheus, kValidateOptions } from "../base/base.js";
|
|
8
8
|
import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../callbacks.js";
|
|
9
|
+
import { ensureMetric } from "../metrics.js";
|
|
9
10
|
import { produceOptionsValidator, producerOptionsValidator, sendOptionsValidator } from "./options.js";
|
|
10
11
|
// Don't move this function as being in the same file will enable V8 to remove.
|
|
11
12
|
// For futher info, ask Matteo.
|
|
@@ -22,6 +23,7 @@ export class Producer extends Base {
|
|
|
22
23
|
#valueSerializer;
|
|
23
24
|
#headerKeySerializer;
|
|
24
25
|
#headerValueSerializer;
|
|
26
|
+
#metricsProducedMessages;
|
|
25
27
|
constructor(options) {
|
|
26
28
|
if (options.idempotent) {
|
|
27
29
|
options.maxInflights = 1;
|
|
@@ -37,6 +39,10 @@ export class Producer extends Base {
|
|
|
37
39
|
this.#headerKeySerializer = options.serializers?.headerKey ?? noopSerializer;
|
|
38
40
|
this.#headerValueSerializer = options.serializers?.headerValue ?? noopSerializer;
|
|
39
41
|
this[kValidateOptions](options, producerOptionsValidator, '/options');
|
|
42
|
+
if (this[kPrometheus]) {
|
|
43
|
+
ensureMetric(this[kPrometheus], 'Gauge', 'kafka_producers', 'Number of active Kafka producers').inc();
|
|
44
|
+
this.#metricsProducedMessages = ensureMetric(this[kPrometheus], 'Counter', 'kafka_produced_messages', 'Number of produced Kafka messages');
|
|
45
|
+
}
|
|
40
46
|
}
|
|
41
47
|
get producerId() {
|
|
42
48
|
return this.#producerInfo?.producerId;
|
|
@@ -44,6 +50,28 @@ export class Producer extends Base {
|
|
|
44
50
|
get producerEpoch() {
|
|
45
51
|
return this.#producerInfo?.producerEpoch;
|
|
46
52
|
}
|
|
53
|
+
close(callback) {
|
|
54
|
+
if (!callback) {
|
|
55
|
+
callback = createPromisifiedCallback();
|
|
56
|
+
}
|
|
57
|
+
if (this[kClosed]) {
|
|
58
|
+
callback(null);
|
|
59
|
+
return callback[kCallbackPromise];
|
|
60
|
+
}
|
|
61
|
+
this[kClosed] = true;
|
|
62
|
+
super.close(error => {
|
|
63
|
+
if (error) {
|
|
64
|
+
this[kClosed] = false;
|
|
65
|
+
callback(error);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (this[kPrometheus]) {
|
|
69
|
+
ensureMetric(this[kPrometheus], 'Gauge', 'kafka_producers', 'Number of active Kafka producers').dec();
|
|
70
|
+
}
|
|
71
|
+
callback(null);
|
|
72
|
+
});
|
|
73
|
+
return callback[kCallbackPromise];
|
|
74
|
+
}
|
|
47
75
|
initIdempotentProducer(options, callback) {
|
|
48
76
|
if (!callback) {
|
|
49
77
|
callback = createPromisifiedCallback();
|
|
@@ -194,14 +222,15 @@ export class Producer extends Base {
|
|
|
194
222
|
}
|
|
195
223
|
// Track nodes so that we can get their ID for delayed write reporting
|
|
196
224
|
const nodes = [];
|
|
197
|
-
runConcurrentCallbacks('Producing messages failed.', messagesByDestination, ([destination,
|
|
225
|
+
runConcurrentCallbacks('Producing messages failed.', messagesByDestination, ([destination, destinationMessages], concurrentCallback) => {
|
|
198
226
|
nodes.push(destination);
|
|
199
|
-
this.#performSingleDestinationSend(topics,
|
|
227
|
+
this.#performSingleDestinationSend(topics, destinationMessages, this[kOptions].timeout, sendOptions.acks, sendOptions.autocreateTopics, sendOptions.repeatOnStaleMetadata, produceOptions, concurrentCallback);
|
|
200
228
|
}, (error, apiResults) => {
|
|
201
229
|
if (error) {
|
|
202
230
|
callback(error, undefined);
|
|
203
231
|
return;
|
|
204
232
|
}
|
|
233
|
+
this.#metricsProducedMessages?.inc(messages.length);
|
|
205
234
|
const results = {};
|
|
206
235
|
if (sendOptions.acks === ProduceAcks.NO_RESPONSE) {
|
|
207
236
|
const unwritableNodes = [];
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
2
2
|
import debug from 'debug';
|
|
3
3
|
import { type DynamicBuffer } from './protocol/dynamic-buffer.ts';
|
|
4
|
+
export interface DataValidationContext {
|
|
5
|
+
parentData: {
|
|
6
|
+
[k: string | number]: any;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
4
9
|
export type DebugDumpLogger = (...args: any[]) => void;
|
|
5
10
|
export { setTimeout as sleep } from 'node:timers/promises';
|
|
6
|
-
export declare const ajv:
|
|
11
|
+
export declare const ajv: Ajv2020;
|
|
7
12
|
export declare const loggers: Record<string, debug.Debugger>;
|
|
8
13
|
export declare class NumericMap extends Map<string, number> {
|
|
9
14
|
getWithDefault(key: string, fallback: number): number;
|
package/dist/utils.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Unpromise } from '@watchable/unpromise';
|
|
2
|
-
import { Ajv } from 'ajv';
|
|
3
2
|
import ajvErrors from 'ajv-errors';
|
|
3
|
+
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
4
|
+
import debug from 'debug';
|
|
4
5
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
6
|
import { inspect } from 'node:util';
|
|
6
|
-
import debug from 'debug';
|
|
7
7
|
export { setTimeout as sleep } from 'node:timers/promises';
|
|
8
|
-
export const ajv = new
|
|
8
|
+
export const ajv = new Ajv2020({ allErrors: true, coerceTypes: false, strict: true });
|
|
9
9
|
export const loggers = {
|
|
10
10
|
protocol: debug('plt:kafka:protocol'),
|
|
11
11
|
client: debug('plt:kafka:client'),
|
|
@@ -53,6 +53,30 @@ ajv.addKeyword({
|
|
|
53
53
|
message: 'must be Buffer'
|
|
54
54
|
}
|
|
55
55
|
});
|
|
56
|
+
ajv.addKeyword({
|
|
57
|
+
keyword: 'gteProperty',
|
|
58
|
+
validate(property, current, _, context) {
|
|
59
|
+
const root = context?.parentData;
|
|
60
|
+
return current >= root[property];
|
|
61
|
+
},
|
|
62
|
+
error: {
|
|
63
|
+
message({ schema }) {
|
|
64
|
+
return `must be greater than or equal to $dataVar$/${schema}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
ajv.addKeyword({
|
|
69
|
+
keyword: 'lteProperty',
|
|
70
|
+
validate(property, current, _, context) {
|
|
71
|
+
const root = context?.parentData;
|
|
72
|
+
return current < root[property];
|
|
73
|
+
},
|
|
74
|
+
error: {
|
|
75
|
+
message({ schema }) {
|
|
76
|
+
return `must be less than or equal to $dataVar$/${schema}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
56
80
|
export class NumericMap extends Map {
|
|
57
81
|
getWithDefault(key, fallback) {
|
|
58
82
|
return this.get(key) ?? fallback;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/kafka",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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)",
|
|
@@ -24,20 +24,6 @@
|
|
|
24
24
|
"type": "module",
|
|
25
25
|
"exports": "./dist/index.js",
|
|
26
26
|
"types": "./dist/index.d.ts",
|
|
27
|
-
"scripts": {
|
|
28
|
-
"build": "rm -rf dist && tsc -p tsconfig.base.json",
|
|
29
|
-
"lint": "eslint --cache",
|
|
30
|
-
"typecheck": "tsc -p . --noEmit",
|
|
31
|
-
"format": "prettier -w benchmarks playground src test",
|
|
32
|
-
"test": "c8 -c test/config/c8-local.json node --env-file=test/config/env --no-warnings --test --test-timeout=180000 'test/**/*.test.ts'",
|
|
33
|
-
"test:ci": "c8 -c test/config/c8-ci.json node --env-file=test/config/env --no-warnings --test --test-timeout=180000 'test/**/*.test.ts'",
|
|
34
|
-
"ci": "npm run build && npm run lint && npm run test:ci",
|
|
35
|
-
"prepublishOnly": "npm run ci",
|
|
36
|
-
"postpublish": "git push origin && git push origin -f --tags",
|
|
37
|
-
"generate:apis": "node --experimental-strip-types scripts/generate-apis.ts",
|
|
38
|
-
"generate:errors": "node --experimental-strip-types scripts/generate-errors.ts",
|
|
39
|
-
"create:api": "node --experimental-strip-types scripts/create-api.ts"
|
|
40
|
-
},
|
|
41
27
|
"dependencies": {
|
|
42
28
|
"@watchable/unpromise": "^1.0.2",
|
|
43
29
|
"ajv": "^8.17.1",
|
|
@@ -48,28 +34,41 @@
|
|
|
48
34
|
"semver": "^7.7.1"
|
|
49
35
|
},
|
|
50
36
|
"optionalDependencies": {
|
|
51
|
-
"@platformatic/rdkafka": "^4.0.0",
|
|
52
|
-
"cronometro": "^5.3.0",
|
|
53
|
-
"kafkajs": "^2.2.4",
|
|
54
37
|
"lz4-napi": "^2.8.0",
|
|
55
|
-
"node-rdkafka": "^3.3.1",
|
|
56
38
|
"snappy": "^7.2.2"
|
|
57
39
|
},
|
|
58
40
|
"devDependencies": {
|
|
41
|
+
"@platformatic/rdkafka": "^4.0.0",
|
|
59
42
|
"@types/debug": "^4.1.12",
|
|
60
43
|
"@types/node": "^22.13.5",
|
|
61
44
|
"c8": "^10.1.3",
|
|
62
|
-
"cleaner-spec-reporter": "^0.
|
|
45
|
+
"cleaner-spec-reporter": "^0.4.0",
|
|
46
|
+
"cronometro": "^5.3.0",
|
|
63
47
|
"eslint": "^9.21.0",
|
|
64
48
|
"hwp": "^0.4.1",
|
|
65
49
|
"json5": "^2.2.3",
|
|
50
|
+
"kafkajs": "^2.2.4",
|
|
66
51
|
"neostandard": "^0.12.1",
|
|
52
|
+
"node-rdkafka": "^3.3.1",
|
|
67
53
|
"parse5": "^7.2.1",
|
|
68
54
|
"prettier": "^3.5.3",
|
|
55
|
+
"prom-client": "^15.1.3",
|
|
69
56
|
"scule": "^1.3.0",
|
|
70
57
|
"typescript": "^5.7.3"
|
|
71
58
|
},
|
|
72
59
|
"engines": {
|
|
73
60
|
"node": ">= 22.14.0"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "rm -rf dist && tsc -p tsconfig.base.json",
|
|
64
|
+
"lint": "eslint --cache",
|
|
65
|
+
"typecheck": "tsc -p . --noEmit",
|
|
66
|
+
"format": "prettier -w benchmarks playground src test",
|
|
67
|
+
"test": "c8 -c test/config/c8-local.json node --env-file=test/config/env --no-warnings --test --test-timeout=300000 'test/**/*.test.ts'",
|
|
68
|
+
"test:ci": "c8 -c test/config/c8-ci.json node --env-file=test/config/env --no-warnings --test --test-timeout=300000 'test/**/*.test.ts'",
|
|
69
|
+
"ci": "npm run build && npm run lint && npm run test:ci",
|
|
70
|
+
"generate:apis": "node --experimental-strip-types scripts/generate-apis.ts",
|
|
71
|
+
"generate:errors": "node --experimental-strip-types scripts/generate-errors.ts",
|
|
72
|
+
"create:api": "node --experimental-strip-types scripts/create-api.ts"
|
|
74
73
|
}
|
|
75
|
-
}
|
|
74
|
+
}
|