@platformatic/kafka 0.1.1 → 0.3.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 CHANGED
@@ -24,13 +24,13 @@ npm install @platformatic/kafka
24
24
  ### Producer
25
25
 
26
26
  ```typescript
27
- import { Producer, stringSerializer } from '@platformatic/kafka'
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: stringSerializer
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, stringDeserializer } from '@platformatic/kafka'
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: stringDeserializer
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(stream, async message => {
87
- console.log(`Received: ${message.key} -> ${message.value}`)
88
- // Process message...
89
- }, 16) // 16 is the concurrency level
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:29092'],
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:29092'],
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
- .appendTaggedFields(); // Add tagged fields for partitions
38
- }, true, true)
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.kConnections');
12
- export const kCreateConnectionPool = Symbol('plt.kafka.base.kCreateConnectionPool');
13
- export const kClosed = Symbol('plt.kafka.base.kClosed');
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(ajv.errorsText(validator.errors, { dataVar: targetName }) + '.');
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
  }
@@ -77,6 +77,10 @@ export declare const baseOptionsSchema: {
77
77
  strict: {
78
78
  type: string;
79
79
  };
80
+ metrics: {
81
+ type: string;
82
+ additionalProperties: boolean;
83
+ };
80
84
  };
81
85
  required: string[];
82
86
  additionalProperties: boolean;
@@ -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 { Base } from '../base/base.ts';
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: boolean | CallbackWithPromise<void>, callback?: CallbackWithPromise<void>): void;
34
+ leaveGroup(force?: boolean | CallbackWithPromise<void>, callback?: CallbackWithPromise<void>): void;
32
35
  leaveGroup(force?: boolean): Promise<void>;
33
36
  }
@@ -11,11 +11,12 @@ 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
- import { commitOptionsValidator, consumeOptionsValidator, consumerOptionsValidator, defaultConsumerOptions, fetchOptionsValidator, groupOptionsValidator, listCommitsOptionsValidator, listOffsetsOptionsValidator } from "./options.js";
19
+ import { commitOptionsValidator, consumeOptionsValidator, consumerOptionsValidator, defaultConsumerOptions, fetchOptionsValidator, groupIdAndOptionsValidator, groupOptionsValidator, listCommitsOptionsValidator, listOffsetsOptionsValidator } from "./options.js";
19
20
  import { TopicsMap } from "./topics-map.js";
20
21
  export class Consumer extends Base {
21
22
  groupId;
@@ -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
- #fetchConnectionPool;
48
+ [kFetchConnections];
49
+ // Metrics
50
+ #metricActiveStreams;
48
51
  constructor(options) {
49
52
  super(options);
50
53
  this[kOptions] = Object.assign({}, defaultBaseOptions, defaultConsumerOptions, options);
@@ -61,9 +64,17 @@ export class Consumer extends Base {
61
64
  this.#coordinatorId = null;
62
65
  this.#heartbeatInterval = null;
63
66
  this.#streams = new Set();
64
- this.#validateGroupOptions(this[kOptions]);
67
+ this.#validateGroupOptions(this[kOptions], groupIdAndOptionsValidator);
65
68
  // Initialize connection pool
66
- this.#fetchConnectionPool = this[kCreateConnectionPool]();
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.#fetchConnectionPool.close(error => {
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(callback);
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, callback);
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
- for (const topic of options.topics) {
234
- if (this.topics.track(topic)) {
235
- joinNeeded = true;
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.#fetchConnectionPool.get(broker, (error, connection) => {
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, undefined);
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) {
@@ -656,15 +688,11 @@ export class Consumer extends Base {
656
688
  });
657
689
  });
658
690
  }
659
- #validateGroupOptions(options) {
660
- if (options.rebalanceTimeout < options.sessionTimeout) {
661
- throw new UserError('/options/rebalanceTimeout must be greater than or equal to /options/sessionTimeout.');
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.');
691
+ #validateGroupOptions(options, validator) {
692
+ validator ??= groupOptionsValidator;
693
+ const valid = validator(options);
694
+ if (!valid) {
695
+ throw new UserError(this[kFormatValidationErrors](validator, '/options'));
668
696
  }
669
697
  }
670
698
  /*
@@ -762,6 +790,10 @@ export class Consumer extends Base {
762
790
  else if (protocolError.memberId && !this.memberId) {
763
791
  this.memberId = protocolError.memberId;
764
792
  }
793
+ // This is only used in testing
794
+ if (protocolError.cancelMembership) {
795
+ this.#membershipActive = false;
796
+ }
765
797
  return protocolError;
766
798
  }
767
799
  }
@@ -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,
@@ -1,4 +1,120 @@
1
1
  import { type ConsumerOptions } from './types.ts';
2
+ export declare const groupOptionsProperties: {
3
+ sessionTimeout: {
4
+ type: string;
5
+ minimum: number;
6
+ };
7
+ rebalanceTimeout: {
8
+ type: string;
9
+ minimum: number;
10
+ };
11
+ heartbeatInterval: {
12
+ type: string;
13
+ minimum: number;
14
+ };
15
+ protocols: {
16
+ type: string;
17
+ items: {
18
+ type: string;
19
+ properties: {
20
+ name: {
21
+ type: string;
22
+ pattern: string;
23
+ };
24
+ version: {
25
+ type: string;
26
+ minimum: number;
27
+ };
28
+ topics: {
29
+ type: string;
30
+ items: {
31
+ type: string;
32
+ };
33
+ };
34
+ metadata: {
35
+ oneOf: ({
36
+ type: string;
37
+ buffer?: undefined;
38
+ } | {
39
+ buffer: boolean;
40
+ type?: undefined;
41
+ })[];
42
+ };
43
+ };
44
+ };
45
+ };
46
+ };
47
+ export declare const groupOptionsAdditionalValidations: {
48
+ rebalanceTimeout: {
49
+ properties: {
50
+ rebalanceTimeout: {
51
+ type: string;
52
+ minimum: number;
53
+ gteProperty: string;
54
+ };
55
+ };
56
+ };
57
+ heartbeatInterval: {
58
+ properties: {
59
+ heartbeatInterval: {
60
+ type: string;
61
+ minimum: number;
62
+ allOf: {
63
+ lteProperty: string;
64
+ }[];
65
+ };
66
+ };
67
+ };
68
+ };
69
+ export declare const consumeOptionsProperties: {
70
+ autocommit: {
71
+ oneOf: ({
72
+ type: string;
73
+ minimum?: undefined;
74
+ } | {
75
+ type: string;
76
+ minimum: number;
77
+ })[];
78
+ };
79
+ minBytes: {
80
+ type: string;
81
+ minimum: number;
82
+ };
83
+ maxBytes: {
84
+ type: string;
85
+ minimum: number;
86
+ };
87
+ maxWaitTime: {
88
+ type: string;
89
+ minimum: number;
90
+ };
91
+ isolationLevel: {
92
+ type: string;
93
+ enum: string[];
94
+ };
95
+ deserializers: {
96
+ type: string;
97
+ properties: {
98
+ key: {
99
+ function: boolean;
100
+ };
101
+ value: {
102
+ function: boolean;
103
+ };
104
+ headerKey: {
105
+ function: boolean;
106
+ };
107
+ headerValue: {
108
+ function: boolean;
109
+ };
110
+ };
111
+ additionalProperties: boolean;
112
+ };
113
+ highWaterMark: {
114
+ type: string;
115
+ minimum: number;
116
+ };
117
+ };
2
118
  export declare const groupOptionsSchema: {
3
119
  type: string;
4
120
  properties: {
@@ -500,6 +616,9 @@ export declare const listOffsetsOptionsSchema: {
500
616
  additionalProperties: boolean;
501
617
  };
502
618
  export declare const groupOptionsValidator: import("ajv").ValidateFunction<unknown>;
619
+ export declare const groupIdAndOptionsValidator: import("ajv").ValidateFunction<{
620
+ groupId: any;
621
+ }>;
503
622
  export declare const consumeOptionsValidator: import("ajv").ValidateFunction<{
504
623
  [x: string]: {};
505
624
  }>;
@@ -3,7 +3,7 @@ import { ajv } from "../../utils.js";
3
3
  import { idProperty, topicWithPartitionAndOffsetProperties } from "../base/options.js";
4
4
  import { serdeProperties } from "../serde.js";
5
5
  import { MessagesStreamFallbackModes, MessagesStreamModes } from "./types.js";
6
- const groupOptionsProperties = {
6
+ export const groupOptionsProperties = {
7
7
  sessionTimeout: { type: 'number', minimum: 0 },
8
8
  rebalanceTimeout: { type: 'number', minimum: 0 },
9
9
  heartbeatInterval: { type: 'number', minimum: 0 },
@@ -23,7 +23,34 @@ const groupOptionsProperties = {
23
23
  }
24
24
  }
25
25
  };
26
- const consumeOptionsProperties = {
26
+ export 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
+ };
53
+ export const consumeOptionsProperties = {
27
54
  autocommit: { oneOf: [{ type: 'boolean' }, { type: 'number', minimum: 100 }] },
28
55
  minBytes: { type: 'number', minimum: 0 },
29
56
  maxBytes: { type: 'number', minimum: 0 },
@@ -156,7 +183,20 @@ export const listOffsetsOptionsSchema = {
156
183
  required: ['topics'],
157
184
  additionalProperties: false
158
185
  };
159
- export const groupOptionsValidator = ajv.compile(groupOptionsSchema);
186
+ export const groupOptionsValidator = ajv.compile({
187
+ ...groupOptionsSchema,
188
+ dependentSchemas: groupOptionsAdditionalValidations
189
+ });
190
+ export const groupIdAndOptionsValidator = ajv.compile({
191
+ type: 'object',
192
+ properties: {
193
+ groupId: idProperty,
194
+ ...groupOptionsProperties
195
+ },
196
+ required: ['groupId'],
197
+ additionalProperties: true,
198
+ dependentSchemas: groupOptionsAdditionalValidations
199
+ });
160
200
  export const consumeOptionsValidator = ajv.compile(consumeOptionsSchema);
161
201
  export const consumerOptionsValidator = ajv.compile(consumerOptionsSchema);
162
202
  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
+ }
@@ -1,3 +1,33 @@
1
+ export declare const produceOptionsProperties: {
2
+ producerId: {
3
+ bigint: boolean;
4
+ };
5
+ producerEpoch: {
6
+ type: string;
7
+ };
8
+ idempotent: {
9
+ type: string;
10
+ };
11
+ acks: {
12
+ type: string;
13
+ enum: (0 | 1 | -1)[];
14
+ errorMessage: string;
15
+ };
16
+ compression: {
17
+ type: string;
18
+ enum: string[];
19
+ errorMessage: string;
20
+ };
21
+ partitioner: {
22
+ function: boolean;
23
+ };
24
+ autocreateTopics: {
25
+ type: string;
26
+ };
27
+ repeatOnStaleMetadata: {
28
+ type: string;
29
+ };
30
+ };
1
31
  export declare const produceOptionsSchema: {
2
32
  type: string;
3
33
  properties: {
@@ -3,7 +3,7 @@ import { compressionsAlgorithms } from "../../protocol/compression.js";
3
3
  import { messageSchema } from "../../protocol/records.js";
4
4
  import { ajv, enumErrorMessage } from "../../utils.js";
5
5
  import { serdeProperties } from "../serde.js";
6
- const produceOptionsProperties = {
6
+ export const produceOptionsProperties = {
7
7
  producerId: { bigint: true },
8
8
  producerEpoch: { type: 'number' },
9
9
  idempotent: { type: 'boolean' },
@@ -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, messages], concurrentCallback) => {
225
+ runConcurrentCallbacks('Producing messages failed.', messagesByDestination, ([destination, destinationMessages], concurrentCallback) => {
198
226
  nodes.push(destination);
199
- this.#performSingleDestinationSend(topics, messages, this[kOptions].timeout, sendOptions.acks, sendOptions.autocreateTopics, sendOptions.repeatOnStaleMetadata, produceOptions, concurrentCallback);
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 { Ajv } from 'ajv';
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: 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 Ajv({ allErrors: true, coerceTypes: false, strict: true });
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.1.1",
3
+ "version": "0.3.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",
@@ -53,23 +39,36 @@
53
39
  },
54
40
  "devDependencies": {
55
41
  "@platformatic/rdkafka": "^4.0.0",
56
- "cronometro": "^5.3.0",
57
- "node-rdkafka": "^3.3.1",
58
- "kafkajs": "^2.2.4",
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.3.1",
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
+ }