@platformatic/kafka 1.27.0 → 1.28.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
@@ -51,6 +51,7 @@ await producer.send({
51
51
  })
52
52
 
53
53
  // Close the producer when done
54
+ // If you created producer streams with producer.asStream(), either close them first or use producer.close(true)
54
55
  await producer.close()
55
56
  ```
56
57
 
@@ -37,6 +37,9 @@ export class MessagesStream extends Readable {
37
37
  #mode;
38
38
  #fallbackMode;
39
39
  #paused;
40
+ #refreshOffsetsInflight;
41
+ #refreshOffsetsPending;
42
+ #refreshOffsetsDestroyOnError;
40
43
  #fetches;
41
44
  #maxFetches;
42
45
  #options;
@@ -99,6 +102,9 @@ export class MessagesStream extends Readable {
99
102
  this.#offsetsCommitted = new Map();
100
103
  this.#partitionsEpochs = new Map();
101
104
  this.#paused = false;
105
+ this.#refreshOffsetsInflight = false;
106
+ this.#refreshOffsetsPending = false;
107
+ this.#refreshOffsetsDestroyOnError = false;
102
108
  this.#fetches = 0;
103
109
  this.#maxFetches = maxFetches ?? 0;
104
110
  this.#topics = structuredClone(options.topics);
@@ -144,14 +150,7 @@ export class MessagesStream extends Readable {
144
150
  // having some.
145
151
  this.#consumer.on('consumer:group:join', () => {
146
152
  this.#offsetsCommitted.clear();
147
- this.#refreshOffsets(error => {
148
- /* c8 ignore next 4 - Hard to test */
149
- if (error) {
150
- this.destroy(error);
151
- return;
152
- }
153
- this.#fetch();
154
- });
153
+ this.#scheduleRefreshOffsetsAndFetch();
155
154
  });
156
155
  if (consumer[kPrometheus]) {
157
156
  this.#metricsConsumedMessages = ensureMetric(consumer[kPrometheus], 'Counter', 'kafka_consumed_messages', 'Number of consumed Kafka messages');
@@ -291,7 +290,7 @@ export class MessagesStream extends Readable {
291
290
  return;
292
291
  }
293
292
  // No need to fetch if nobody is consuming the data
294
- if (this.readableFlowing === null || this.#paused) {
293
+ if (this.readableFlowing === null || this.#paused || this.#refreshOffsetsInflight || this.#refreshOffsetsPending) {
295
294
  return;
296
295
  }
297
296
  this.#consumer.metadata({ topics: this.#consumer.topics.current }, (error, metadata) => {
@@ -448,8 +447,11 @@ export class MessagesStream extends Readable {
448
447
  try {
449
448
  const headers = new Map();
450
449
  for (const [headerKey, headerValue] of record.headers) {
451
- headers.set(headerKeyDeserializer(headerKey ?? undefined, messageToConsume), headerValueDeserializer(headerValue ?? undefined, messageToConsume));
450
+ headers.set(
451
+ /* c8 ignore next 2 - Hard to test */
452
+ headerKeyDeserializer(headerKey ?? undefined, messageToConsume), headerValueDeserializer(headerValue ?? undefined, messageToConsume));
452
453
  }
454
+ /* c8 ignore next 2 - Hard to test */
453
455
  const key = keyDeserializer(record.key ?? undefined, headers, messageToConsume);
454
456
  const value = valueDeserializer(record.value ?? undefined, headers, messageToConsume);
455
457
  this.#metricsConsumedMessages?.inc();
@@ -595,11 +597,37 @@ export class MessagesStream extends Readable {
595
597
  });
596
598
  });
597
599
  }
598
- [kRefreshOffsetsAndFetch]() {
599
- this.#refreshOffsets(() => {
600
+ #scheduleRefreshOffsetsAndFetch(destroyOnError = true) {
601
+ this.#refreshOffsetsDestroyOnError ||= destroyOnError;
602
+ if (this.#refreshOffsetsInflight) {
603
+ this.#refreshOffsetsPending = true;
604
+ return;
605
+ }
606
+ this.#refreshOffsetsInflight = true;
607
+ this.#refreshOffsets(error => {
608
+ const shouldDestroyOnError = this.#refreshOffsetsDestroyOnError;
609
+ this.#refreshOffsetsInflight = false;
610
+ this.#refreshOffsetsDestroyOnError = false;
611
+ this.#refreshOffsetsPending = false;
612
+ /* c8 ignore next 9 - Hard to test */
613
+ if (error) {
614
+ if (shouldDestroyOnError) {
615
+ this.destroy(error);
616
+ }
617
+ return;
618
+ }
619
+ // A new one was scheduled while the previous one was inflight, we need to run it immediately
620
+ /* c8 ignore next 4 - Hard to test */
621
+ if (this.#refreshOffsetsPending) {
622
+ this.#scheduleRefreshOffsetsAndFetch(shouldDestroyOnError);
623
+ return;
624
+ }
600
625
  this.#fetch();
601
626
  });
602
627
  }
628
+ [kRefreshOffsetsAndFetch]() {
629
+ this.#scheduleRefreshOffsetsAndFetch(false);
630
+ }
603
631
  #assignOffsets(offsets, commits, callback) {
604
632
  for (const [topic, partitions] of offsets) {
605
633
  for (let i = 0; i < partitions.length; i++) {
@@ -1,3 +1,4 @@
1
1
  export * from './options.ts';
2
+ export * from './producer-stream.ts';
2
3
  export * from './producer.ts';
3
4
  export * from './types.ts';
@@ -1,3 +1,4 @@
1
1
  export * from "./options.js";
2
+ export * from "./producer-stream.js";
2
3
  export * from "./producer.js";
3
4
  export * from "./types.js";
@@ -155,3 +155,66 @@ export declare const sendOptionsSchema: {
155
155
  export declare const sendOptionsValidator: import("ajv").ValidateFunction<{
156
156
  [x: string]: {};
157
157
  }>;
158
+ export declare const defaultProducerStreamOptions: {
159
+ batchSize: number;
160
+ batchTime: number;
161
+ reportMode: "none";
162
+ };
163
+ export declare const producerStreamOptionsSchema: {
164
+ type: string;
165
+ properties: {
166
+ producerId: {
167
+ bigint: boolean;
168
+ };
169
+ producerEpoch: {
170
+ type: string;
171
+ };
172
+ idempotent: {
173
+ type: string;
174
+ };
175
+ transactionalId: {
176
+ type: string;
177
+ };
178
+ acks: {
179
+ type: string;
180
+ enumeration: {
181
+ allowed: (0 | 1 | -1)[];
182
+ errorMessage: string;
183
+ };
184
+ };
185
+ compression: {
186
+ type: string;
187
+ enumeration: {
188
+ allowed: import("../../protocol/compression.ts").CompressionAlgorithmValue[];
189
+ errorMessage: string;
190
+ };
191
+ };
192
+ partitioner: {
193
+ function: boolean;
194
+ };
195
+ autocreateTopics: {
196
+ type: string;
197
+ };
198
+ repeatOnStaleMetadata: {
199
+ type: string;
200
+ };
201
+ highWaterMark: {
202
+ type: string;
203
+ minimum: number;
204
+ };
205
+ batchSize: {
206
+ type: string;
207
+ minimum: number;
208
+ };
209
+ batchTime: {
210
+ type: string;
211
+ minimum: number;
212
+ };
213
+ reportMode: {
214
+ type: string;
215
+ enum: import("./types.ts").ProducerStreamReportModeValue[];
216
+ };
217
+ };
218
+ additionalProperties: boolean;
219
+ };
220
+ export declare const producerStreamOptionsValidator: import("ajv").ValidateFunction<unknown>;
@@ -3,6 +3,7 @@ import { allowedCompressionsAlgorithms, compressionsAlgorithms } from "../../pro
3
3
  import { messageSchema } from "../../protocol/records.js";
4
4
  import { ajv, enumErrorMessage } from "../../utils.js";
5
5
  import { serdeProperties } from "../serde.js";
6
+ import { allowedProducerStreamReportModes, ProducerStreamReportModes } from "./types.js";
6
7
  export const produceOptionsProperties = {
7
8
  producerId: { bigint: true },
8
9
  producerEpoch: { type: 'number' },
@@ -52,3 +53,20 @@ export const sendOptionsSchema = {
52
53
  additionalProperties: false
53
54
  };
54
55
  export const sendOptionsValidator = ajv.compile(sendOptionsSchema);
56
+ export const defaultProducerStreamOptions = {
57
+ batchSize: 100,
58
+ batchTime: 10,
59
+ reportMode: ProducerStreamReportModes.NONE
60
+ };
61
+ export const producerStreamOptionsSchema = {
62
+ type: 'object',
63
+ properties: {
64
+ highWaterMark: { type: 'integer', minimum: 1 },
65
+ batchSize: { type: 'integer', minimum: 1 },
66
+ batchTime: { type: 'integer', minimum: 0 },
67
+ reportMode: { type: 'string', enum: allowedProducerStreamReportModes },
68
+ ...produceOptionsProperties
69
+ },
70
+ additionalProperties: false
71
+ };
72
+ export const producerStreamOptionsValidator = ajv.compile(producerStreamOptionsSchema);
@@ -0,0 +1,19 @@
1
+ import { Writable } from 'node:stream';
2
+ import { type CallbackWithPromise } from '../../apis/callbacks.ts';
3
+ import type { MessageToProduce } from '../../protocol/records.ts';
4
+ import type { Producer } from './producer.ts';
5
+ import { type ProducerStreamOptions } from './types.ts';
6
+ export declare class ProducerStream<Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> extends Writable {
7
+ #private;
8
+ instance: number;
9
+ constructor(producer: Producer<Key, Value, HeaderKey, HeaderValue>, options: ProducerStreamOptions<Key, Value, HeaderKey, HeaderValue>);
10
+ get producer(): Producer<Key, Value, HeaderKey, HeaderValue>;
11
+ close(callback: CallbackWithPromise<void>): void;
12
+ close(): Promise<void>;
13
+ _write(message: MessageToProduce<Key, Value, HeaderKey, HeaderValue>, _: BufferEncoding, callback: (error?: Error | null) => void): void;
14
+ _writev(chunks: Array<{
15
+ chunk: MessageToProduce<Key, Value, HeaderKey, HeaderValue>;
16
+ }>, callback: (error?: Error | null) => void): void;
17
+ _final(callback: (error?: Error | null) => void): void;
18
+ _destroy(error: Error | null, callback: (error?: Error | null) => void): void;
19
+ }
@@ -0,0 +1,187 @@
1
+ import { Writable } from 'node:stream';
2
+ import { createPromisifiedCallback, kCallbackPromise } from "../../apis/callbacks.js";
3
+ import { notifyCreation } from "../../diagnostic.js";
4
+ import { defaultProducerStreamOptions } from "./options.js";
5
+ import { ProducerStreamReportModes } from "./types.js";
6
+ let currentInstance = 0;
7
+ export class ProducerStream extends Writable {
8
+ #producer;
9
+ #batchSize;
10
+ #batchTime;
11
+ #reportMode;
12
+ #produceOptions;
13
+ #buffer;
14
+ #timer;
15
+ #flushing;
16
+ #batchId;
17
+ #pendingWriteCallback;
18
+ instance;
19
+ constructor(producer, options) {
20
+ super({
21
+ objectMode: true,
22
+ highWaterMark: options.highWaterMark
23
+ });
24
+ const { batchSize, batchTime, reportMode, highWaterMark: _highWaterMark, ...sendOptions } = options;
25
+ this.#producer = producer;
26
+ this.#batchSize = batchSize ?? defaultProducerStreamOptions.batchSize;
27
+ this.#batchTime = batchTime ?? defaultProducerStreamOptions.batchTime;
28
+ this.#reportMode = reportMode ?? defaultProducerStreamOptions.reportMode;
29
+ this.#produceOptions = sendOptions;
30
+ this.#buffer = [];
31
+ this.#timer = null;
32
+ this.#flushing = false;
33
+ this.#batchId = 0;
34
+ this.#pendingWriteCallback = null;
35
+ this.instance = currentInstance++;
36
+ notifyCreation('producer-stream', this);
37
+ }
38
+ get producer() {
39
+ return this.#producer;
40
+ }
41
+ close(callback) {
42
+ if (!callback) {
43
+ callback = createPromisifiedCallback();
44
+ }
45
+ this.end((error) => callback(error ?? null));
46
+ return callback[kCallbackPromise];
47
+ }
48
+ _write(message, _, callback) {
49
+ this.#buffer.push(message);
50
+ const finalizeAfterFlush = this.#buffer.length >= this.writableHighWaterMark;
51
+ if (this.#buffer.length >= this.#batchSize) {
52
+ this.#clearTimer();
53
+ this.#flush();
54
+ }
55
+ else {
56
+ this.#scheduleFlush();
57
+ }
58
+ if (finalizeAfterFlush) {
59
+ this.#pendingWriteCallback = callback;
60
+ return;
61
+ }
62
+ callback(null);
63
+ }
64
+ _writev(chunks, callback) {
65
+ for (const { chunk } of chunks) {
66
+ this.#buffer.push(chunk);
67
+ }
68
+ const finalizeAfterFlush = this.#buffer.length >= this.writableHighWaterMark;
69
+ if (this.#buffer.length >= this.#batchSize) {
70
+ this.#clearTimer();
71
+ this.#flush();
72
+ }
73
+ else {
74
+ this.#scheduleFlush();
75
+ }
76
+ if (finalizeAfterFlush) {
77
+ this.#pendingWriteCallback = callback;
78
+ return;
79
+ }
80
+ callback(null);
81
+ }
82
+ _final(callback) {
83
+ this.#clearTimer();
84
+ if (!this.#buffer.length && !this.#flushing) {
85
+ callback(null);
86
+ return;
87
+ }
88
+ const self = this;
89
+ function onError(error) {
90
+ self.removeListener('error', onError);
91
+ self.removeListener('flush', onFlush);
92
+ callback(error);
93
+ }
94
+ function onFlush() {
95
+ if (self.#buffer.length || self.#flushing) {
96
+ return;
97
+ }
98
+ self.removeListener('error', onError);
99
+ self.removeListener('flush', onFlush);
100
+ callback(null);
101
+ }
102
+ this.on('error', onError);
103
+ this.on('flush', onFlush);
104
+ this.#flush();
105
+ }
106
+ _destroy(error, callback) {
107
+ this.#clearTimer();
108
+ this.#buffer = [];
109
+ this.#flushWriteCallbacks(error);
110
+ callback(error);
111
+ }
112
+ #scheduleFlush() {
113
+ if (this.#timer || this.#batchTime < 0) {
114
+ return;
115
+ }
116
+ this.#timer = setTimeout(() => {
117
+ this.#timer = null;
118
+ this.#flush();
119
+ }, this.#batchTime);
120
+ }
121
+ #clearTimer() {
122
+ if (this.#timer) {
123
+ clearTimeout(this.#timer);
124
+ this.#timer = null;
125
+ }
126
+ }
127
+ #flush() {
128
+ if (this.#flushing || this.destroyed) {
129
+ return;
130
+ }
131
+ /* c8 ignore next 3 - Hard to test */
132
+ if (this.#buffer.length === 0) {
133
+ return;
134
+ }
135
+ this.#flushing = true;
136
+ const pending = this.#buffer.splice(0, this.#batchSize);
137
+ const batchId = this.#batchId++;
138
+ const startedAt = Date.now();
139
+ this.#producer.send({ messages: pending, ...this.#produceOptions }, (error, result) => {
140
+ this.#flushing = false;
141
+ if (error) {
142
+ this.#flushWriteCallbacks(error);
143
+ this.destroy(error);
144
+ return;
145
+ }
146
+ this.#emitReports(this.#reportMode, batchId, pending, result);
147
+ this.emit('flush', { batchId, count: pending.length, duration: Date.now() - startedAt, result: result });
148
+ if (this.#buffer.length >= this.#batchSize) {
149
+ this.#flush();
150
+ }
151
+ else if (this.#buffer.length > 0) {
152
+ this.#scheduleFlush();
153
+ }
154
+ this.#flushWriteCallbacks();
155
+ });
156
+ }
157
+ #flushWriteCallbacks(error) {
158
+ if (!this.#pendingWriteCallback || this.#buffer.length >= this.writableHighWaterMark) {
159
+ return;
160
+ }
161
+ const callback = this.#pendingWriteCallback;
162
+ this.#pendingWriteCallback = null;
163
+ callback(error ?? null);
164
+ }
165
+ #emitReports(mode, batchId, pending, result) {
166
+ if (mode === ProducerStreamReportModes.NONE) {
167
+ return;
168
+ }
169
+ if (mode === ProducerStreamReportModes.BATCH) {
170
+ const report = {
171
+ batchId,
172
+ count: pending.length,
173
+ result
174
+ };
175
+ this.emit('delivery-report', report);
176
+ return;
177
+ }
178
+ for (let i = 0; i < pending.length; i++) {
179
+ const report = {
180
+ batchId,
181
+ index: i,
182
+ message: pending[i]
183
+ };
184
+ this.emit('delivery-report', report);
185
+ }
186
+ }
187
+ }
@@ -2,8 +2,9 @@ import { type CallbackWithPromise } from '../../apis/callbacks.ts';
2
2
  import { type Message } from '../../protocol/records.ts';
3
3
  import { kTransaction, kTransactionAddOffsets, kTransactionAddPartitions, kTransactionCancel, kTransactionCommitOffset, kTransactionEnd, kTransactionFindCoordinator } from '../../symbols.ts';
4
4
  import { Base } from '../base/base.ts';
5
+ import { ProducerStream } from './producer-stream.ts';
5
6
  import { Transaction } from './transaction.ts';
6
- import { type ProduceOptions, type ProduceResult, type ProducerInfo, type ProducerOptions, type SendOptions } from './types.ts';
7
+ import { type ProduceOptions, type ProduceResult, type ProducerInfo, type ProducerOptions, type ProducerStreamOptions, type SendOptions } from './types.ts';
7
8
  export declare function noopSerializer(data?: Buffer): Buffer | undefined;
8
9
  export declare class Producer<Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> extends Base<ProducerOptions<Key, Value, HeaderKey, HeaderValue>> {
9
10
  #private;
@@ -12,8 +13,9 @@ export declare class Producer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
12
13
  get producerEpoch(): number | undefined;
13
14
  get transaction(): Transaction<Key, Value, HeaderKey, HeaderValue> | undefined;
14
15
  get coordinatorId(): number;
15
- close(callback: CallbackWithPromise<void>): void;
16
- close(): Promise<void>;
16
+ get streamsCount(): number;
17
+ close(force: boolean | CallbackWithPromise<void>, callback?: CallbackWithPromise<void>): void;
18
+ close(force?: boolean): Promise<void>;
17
19
  initIdempotentProducer(options: ProduceOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<ProducerInfo>): void;
18
20
  initIdempotentProducer(options: ProduceOptions<Key, Value, HeaderKey, HeaderValue>): Promise<ProducerInfo>;
19
21
  send(options: SendOptions<Key, Value, HeaderKey, HeaderValue> & {
@@ -22,6 +24,7 @@ export declare class Producer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
22
24
  send(options: SendOptions<Key, Value, HeaderKey, HeaderValue> & {
23
25
  [kTransaction]?: number;
24
26
  }): Promise<ProduceResult>;
27
+ asStream(options?: ProducerStreamOptions<Key, Value, HeaderKey, HeaderValue>): ProducerStream<Key, Value, HeaderKey, HeaderValue>;
25
28
  beginTransaction(options: ProduceOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<Transaction<Key, Value, HeaderKey, HeaderValue>>): void;
26
29
  beginTransaction(options?: ProduceOptions<Key, Value, HeaderKey, HeaderValue>): Promise<Transaction<Key, Value, HeaderKey, HeaderValue>>;
27
30
  [kTransactionFindCoordinator](callback: CallbackWithPromise<void>): void;
@@ -9,7 +9,8 @@ import { kInstance, kTransaction, kTransactionAddOffsets, kTransactionAddPartiti
9
9
  import { emitExperimentalApiWarning, NumericMap } from "../../utils.js";
10
10
  import { Base, kAfterCreate, kCheckNotClosed, kClosed, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kPrometheus, kValidateOptions } from "../base/base.js";
11
11
  import { ensureMetric } from "../metrics.js";
12
- import { produceOptionsValidator, producerOptionsValidator, sendOptionsValidator } from "./options.js";
12
+ import { produceOptionsValidator, producerOptionsValidator, producerStreamOptionsValidator, sendOptionsValidator } from "./options.js";
13
+ import { ProducerStream } from "./producer-stream.js";
13
14
  import { Transaction } from "./transaction.js";
14
15
  // Don't move this function as being in the same file will enable V8 to remove.
15
16
  // For futher info, ask Matteo.
@@ -29,6 +30,7 @@ export class Producer extends Base {
29
30
  #metricsProducedMessages;
30
31
  #coordinatorId;
31
32
  #transaction;
33
+ #streams;
32
34
  #sendOperation;
33
35
  constructor(options) {
34
36
  if (options.idempotent) {
@@ -64,6 +66,7 @@ export class Producer extends Base {
64
66
  this.#valueSerializer = serializers?.value ?? noopSerializer;
65
67
  this.#headerKeySerializer = serializers?.headerKey ?? noopSerializer;
66
68
  this.#headerValueSerializer = serializers?.headerValue ?? noopSerializer;
69
+ this.#streams = new Set();
67
70
  this[kOptions].transactionalId ??= randomUUID();
68
71
  if (this[kPrometheus]) {
69
72
  ensureMetric(this[kPrometheus], 'Gauge', 'kafka_producers', 'Number of active Kafka producers').inc();
@@ -92,7 +95,14 @@ export class Producer extends Base {
92
95
  get coordinatorId() {
93
96
  return this.#coordinatorId;
94
97
  }
95
- close(callback) {
98
+ get streamsCount() {
99
+ return this.#streams.size;
100
+ }
101
+ close(force, callback) {
102
+ if (typeof force === 'function') {
103
+ callback = force;
104
+ force = false;
105
+ }
96
106
  if (!callback) {
97
107
  callback = createPromisifiedCallback();
98
108
  }
@@ -101,6 +111,36 @@ export class Producer extends Base {
101
111
  return callback[kCallbackPromise];
102
112
  }
103
113
  this[kClosed] = true;
114
+ this.#performClose(force, callback);
115
+ return callback[kCallbackPromise];
116
+ }
117
+ #performClose(force, callback) {
118
+ for (const stream of this.#streams) {
119
+ /* c8 ignore next 3 - Hard to test */
120
+ if (stream.closed || stream.destroyed) {
121
+ this.#streams.delete(stream);
122
+ }
123
+ }
124
+ if (this.#streams.size) {
125
+ if (!force) {
126
+ this[kClosed] = false;
127
+ callback(new UserError('Cannot close producer while producing messages.'));
128
+ return;
129
+ }
130
+ runConcurrentCallbacks('Closing streams failed.', this.#streams, (stream, concurrentCallback) => {
131
+ stream.close(concurrentCallback);
132
+ }, error => {
133
+ /* c8 ignore next 5 - Hard to test */
134
+ if (error) {
135
+ this[kClosed] = false;
136
+ callback(error);
137
+ return;
138
+ }
139
+ this.#streams.clear();
140
+ this.#performClose(false, callback);
141
+ });
142
+ return;
143
+ }
104
144
  super.close(error => {
105
145
  if (error) {
106
146
  this[kClosed] = false;
@@ -112,7 +152,6 @@ export class Producer extends Base {
112
152
  }
113
153
  callback(null);
114
154
  });
115
- return callback[kCallbackPromise];
116
155
  }
117
156
  initIdempotentProducer(options, callback) {
118
157
  if (!callback) {
@@ -162,6 +201,19 @@ export class Producer extends Base {
162
201
  producerSendsChannel.traceCallback(this.#sendOperation, 1, createDiagnosticContext({ client: this, operation: 'send', options }), this, options, callback);
163
202
  return callback[kCallbackPromise];
164
203
  }
204
+ asStream(options) {
205
+ options ??= {};
206
+ const validationError = this[kValidateOptions](options, producerStreamOptionsValidator, '/options', false);
207
+ if (validationError) {
208
+ throw validationError;
209
+ }
210
+ const stream = new ProducerStream(this, options);
211
+ this.#streams.add(stream);
212
+ stream.once('close', () => {
213
+ this.#streams.delete(stream);
214
+ });
215
+ return stream;
216
+ }
165
217
  beginTransaction(options, callback) {
166
218
  if (!callback) {
167
219
  callback = createPromisifiedCallback();
@@ -13,6 +13,16 @@ export interface ProduceResult {
13
13
  unwritableNodes?: number[];
14
14
  }
15
15
  export type Partitioner<Key, Value, HeaderKey, HeaderValue> = (message: MessageToProduce<Key, Value, HeaderKey, HeaderValue>) => number;
16
+ export interface ProducerStreamReport {
17
+ batchId: number;
18
+ count: number;
19
+ result: ProduceResult;
20
+ }
21
+ export interface ProducerStreamMessageReport<Key, Value, HeaderKey, HeaderValue> {
22
+ batchId: number;
23
+ index: number;
24
+ message: MessageToProduce<Key, Value, HeaderKey, HeaderValue>;
25
+ }
16
26
  export interface ProduceOptions<Key, Value, HeaderKey, HeaderValue> {
17
27
  producerId?: bigint;
18
28
  producerEpoch?: number;
@@ -32,3 +42,17 @@ export type ProducerOptions<Key, Value, HeaderKey, HeaderValue> = BaseOptions &
32
42
  export type SendOptions<Key, Value, HeaderKey, HeaderValue> = {
33
43
  messages: MessageToProduce<Key, Value, HeaderKey, HeaderValue>[];
34
44
  } & ProduceOptions<Key, Value, HeaderKey, HeaderValue>;
45
+ export declare const ProducerStreamReportModes: {
46
+ readonly NONE: "none";
47
+ readonly BATCH: "batch";
48
+ readonly MESSAGE: "message";
49
+ };
50
+ export declare const allowedProducerStreamReportModes: ProducerStreamReportModeValue[];
51
+ export type ProducerStreamReportMode = keyof typeof ProducerStreamReportModes;
52
+ export type ProducerStreamReportModeValue = (typeof ProducerStreamReportModes)[keyof typeof ProducerStreamReportModes];
53
+ export type ProducerStreamOptions<Key, Value, HeaderKey, HeaderValue> = Omit<SendOptions<Key, Value, HeaderKey, HeaderValue>, 'messages'> & {
54
+ highWaterMark?: number;
55
+ batchSize?: number;
56
+ batchTime?: number;
57
+ reportMode?: ProducerStreamReportModeValue;
58
+ };
@@ -1 +1,6 @@
1
- export {};
1
+ export const ProducerStreamReportModes = {
2
+ NONE: 'none',
3
+ BATCH: 'batch',
4
+ MESSAGE: 'message'
5
+ };
6
+ export const allowedProducerStreamReportModes = Object.values(ProducerStreamReportModes);
@@ -4,7 +4,7 @@ import { type ConnectionPool } from './network/connection-pool.ts';
4
4
  import { type Connection } from './network/connection.ts';
5
5
  export type ClientType = 'base' | 'producer' | 'consumer' | 'admin';
6
6
  export interface CreationEvent<InstanceType> {
7
- type: ClientType | 'connection' | 'connectionPool';
7
+ type: ClientType | 'connection' | 'connection-pool' | 'messages-stream' | 'producer-stream';
8
8
  instance: InstanceType;
9
9
  }
10
10
  export type ConnectionDiagnosticEvent<Attributes = Record<string, unknown>> = {
@@ -29,7 +29,7 @@ export type DiagnosticContext<BaseContext = {}> = BaseContext & {
29
29
  };
30
30
  export declare const channelsNamespace: "plt:kafka";
31
31
  export declare function createDiagnosticContext<BaseContext = {}>(context: BaseContext): DiagnosticContext<BaseContext>;
32
- export declare function notifyCreation<InstanceType>(type: ClientType | 'connection' | 'connection-pool' | 'messages-stream', instance: InstanceType): void;
32
+ export declare function notifyCreation<InstanceType>(type: ClientType | 'connection' | 'connection-pool' | 'messages-stream' | 'producer-stream', instance: InstanceType): void;
33
33
  export declare function createChannel<DiagnosticEvent extends object>(name: string): ChannelWithName<DiagnosticEvent>;
34
34
  export declare function createTracingChannel<DiagnosticEvent extends object>(name: string): TracingChannelWithName<DiagnosticEvent>;
35
35
  export declare const instancesChannel: ChannelWithName<object>;
@@ -1,3 +1,4 @@
1
1
  export * from "./options";
2
+ export * from "./producer-stream";
2
3
  export * from "./producer";
3
4
  export * from "./types";
@@ -154,4 +154,67 @@ export declare const sendOptionsSchema: {
154
154
  };
155
155
  export declare const sendOptionsValidator: import("ajv").ValidateFunction<{
156
156
  [x: string]: {};
157
- }>;
157
+ }>;
158
+ export declare const defaultProducerStreamOptions: {
159
+ batchSize: number;
160
+ batchTime: number;
161
+ reportMode: "none";
162
+ };
163
+ export declare const producerStreamOptionsSchema: {
164
+ type: string;
165
+ properties: {
166
+ producerId: {
167
+ bigint: boolean;
168
+ };
169
+ producerEpoch: {
170
+ type: string;
171
+ };
172
+ idempotent: {
173
+ type: string;
174
+ };
175
+ transactionalId: {
176
+ type: string;
177
+ };
178
+ acks: {
179
+ type: string;
180
+ enumeration: {
181
+ allowed: (0 | 1 | -1)[];
182
+ errorMessage: string;
183
+ };
184
+ };
185
+ compression: {
186
+ type: string;
187
+ enumeration: {
188
+ allowed: import("../../protocol/compression").CompressionAlgorithmValue[];
189
+ errorMessage: string;
190
+ };
191
+ };
192
+ partitioner: {
193
+ function: boolean;
194
+ };
195
+ autocreateTopics: {
196
+ type: string;
197
+ };
198
+ repeatOnStaleMetadata: {
199
+ type: string;
200
+ };
201
+ highWaterMark: {
202
+ type: string;
203
+ minimum: number;
204
+ };
205
+ batchSize: {
206
+ type: string;
207
+ minimum: number;
208
+ };
209
+ batchTime: {
210
+ type: string;
211
+ minimum: number;
212
+ };
213
+ reportMode: {
214
+ type: string;
215
+ enum: import("./types").ProducerStreamReportModeValue[];
216
+ };
217
+ };
218
+ additionalProperties: boolean;
219
+ };
220
+ export declare const producerStreamOptionsValidator: import("ajv").ValidateFunction<unknown>;
@@ -0,0 +1,19 @@
1
+ import { Writable } from 'node:stream';
2
+ import { type CallbackWithPromise } from "../../apis/callbacks";
3
+ import type { MessageToProduce } from "../../protocol/records";
4
+ import type { Producer } from "./producer";
5
+ import { type ProducerStreamOptions } from "./types";
6
+ export declare class ProducerStream<Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> extends Writable {
7
+ #private;
8
+ instance: number;
9
+ constructor(producer: Producer<Key, Value, HeaderKey, HeaderValue>, options: ProducerStreamOptions<Key, Value, HeaderKey, HeaderValue>);
10
+ get producer(): Producer<Key, Value, HeaderKey, HeaderValue>;
11
+ close(callback: CallbackWithPromise<void>): void;
12
+ close(): Promise<void>;
13
+ _write(message: MessageToProduce<Key, Value, HeaderKey, HeaderValue>, _: BufferEncoding, callback: (error?: Error | null) => void): void;
14
+ _writev(chunks: Array<{
15
+ chunk: MessageToProduce<Key, Value, HeaderKey, HeaderValue>;
16
+ }>, callback: (error?: Error | null) => void): void;
17
+ _final(callback: (error?: Error | null) => void): void;
18
+ _destroy(error: Error | null, callback: (error?: Error | null) => void): void;
19
+ }
@@ -2,8 +2,9 @@ import { type CallbackWithPromise } from "../../apis/callbacks";
2
2
  import { type Message } from "../../protocol/records";
3
3
  import { kTransaction, kTransactionAddOffsets, kTransactionAddPartitions, kTransactionCancel, kTransactionCommitOffset, kTransactionEnd, kTransactionFindCoordinator } from "../../symbols";
4
4
  import { Base } from "../base/base";
5
+ import { ProducerStream } from "./producer-stream";
5
6
  import { Transaction } from "./transaction";
6
- import { type ProduceOptions, type ProduceResult, type ProducerInfo, type ProducerOptions, type SendOptions } from "./types";
7
+ import { type ProduceOptions, type ProduceResult, type ProducerInfo, type ProducerOptions, type ProducerStreamOptions, type SendOptions } from "./types";
7
8
  export declare function noopSerializer(data?: Buffer): Buffer | undefined;
8
9
  export declare class Producer<Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> extends Base<ProducerOptions<Key, Value, HeaderKey, HeaderValue>> {
9
10
  #private;
@@ -12,8 +13,9 @@ export declare class Producer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
12
13
  get producerEpoch(): number | undefined;
13
14
  get transaction(): Transaction<Key, Value, HeaderKey, HeaderValue> | undefined;
14
15
  get coordinatorId(): number;
15
- close(callback: CallbackWithPromise<void>): void;
16
- close(): Promise<void>;
16
+ get streamsCount(): number;
17
+ close(force: boolean | CallbackWithPromise<void>, callback?: CallbackWithPromise<void>): void;
18
+ close(force?: boolean): Promise<void>;
17
19
  initIdempotentProducer(options: ProduceOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<ProducerInfo>): void;
18
20
  initIdempotentProducer(options: ProduceOptions<Key, Value, HeaderKey, HeaderValue>): Promise<ProducerInfo>;
19
21
  send(options: SendOptions<Key, Value, HeaderKey, HeaderValue> & {
@@ -22,6 +24,7 @@ export declare class Producer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
22
24
  send(options: SendOptions<Key, Value, HeaderKey, HeaderValue> & {
23
25
  [kTransaction]?: number;
24
26
  }): Promise<ProduceResult>;
27
+ asStream(options?: ProducerStreamOptions<Key, Value, HeaderKey, HeaderValue>): ProducerStream<Key, Value, HeaderKey, HeaderValue>;
25
28
  beginTransaction(options: ProduceOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<Transaction<Key, Value, HeaderKey, HeaderValue>>): void;
26
29
  beginTransaction(options?: ProduceOptions<Key, Value, HeaderKey, HeaderValue>): Promise<Transaction<Key, Value, HeaderKey, HeaderValue>>;
27
30
  [kTransactionFindCoordinator](callback: CallbackWithPromise<void>): void;
@@ -13,6 +13,16 @@ export interface ProduceResult {
13
13
  unwritableNodes?: number[];
14
14
  }
15
15
  export type Partitioner<Key, Value, HeaderKey, HeaderValue> = (message: MessageToProduce<Key, Value, HeaderKey, HeaderValue>) => number;
16
+ export interface ProducerStreamReport {
17
+ batchId: number;
18
+ count: number;
19
+ result: ProduceResult;
20
+ }
21
+ export interface ProducerStreamMessageReport<Key, Value, HeaderKey, HeaderValue> {
22
+ batchId: number;
23
+ index: number;
24
+ message: MessageToProduce<Key, Value, HeaderKey, HeaderValue>;
25
+ }
16
26
  export interface ProduceOptions<Key, Value, HeaderKey, HeaderValue> {
17
27
  producerId?: bigint;
18
28
  producerEpoch?: number;
@@ -31,4 +41,18 @@ export type ProducerOptions<Key, Value, HeaderKey, HeaderValue> = BaseOptions &
31
41
  };
32
42
  export type SendOptions<Key, Value, HeaderKey, HeaderValue> = {
33
43
  messages: MessageToProduce<Key, Value, HeaderKey, HeaderValue>[];
34
- } & ProduceOptions<Key, Value, HeaderKey, HeaderValue>;
44
+ } & ProduceOptions<Key, Value, HeaderKey, HeaderValue>;
45
+ export declare const ProducerStreamReportModes: {
46
+ readonly NONE: "none";
47
+ readonly BATCH: "batch";
48
+ readonly MESSAGE: "message";
49
+ };
50
+ export declare const allowedProducerStreamReportModes: ProducerStreamReportModeValue[];
51
+ export type ProducerStreamReportMode = keyof typeof ProducerStreamReportModes;
52
+ export type ProducerStreamReportModeValue = (typeof ProducerStreamReportModes)[keyof typeof ProducerStreamReportModes];
53
+ export type ProducerStreamOptions<Key, Value, HeaderKey, HeaderValue> = Omit<SendOptions<Key, Value, HeaderKey, HeaderValue>, 'messages'> & {
54
+ highWaterMark?: number;
55
+ batchSize?: number;
56
+ batchTime?: number;
57
+ reportMode?: ProducerStreamReportModeValue;
58
+ };
@@ -4,7 +4,7 @@ import { type ConnectionPool } from "./network/connection-pool";
4
4
  import { type Connection } from "./network/connection";
5
5
  export type ClientType = 'base' | 'producer' | 'consumer' | 'admin';
6
6
  export interface CreationEvent<InstanceType> {
7
- type: ClientType | 'connection' | 'connectionPool';
7
+ type: ClientType | 'connection' | 'connection-pool' | 'messages-stream' | 'producer-stream';
8
8
  instance: InstanceType;
9
9
  }
10
10
  export type ConnectionDiagnosticEvent<Attributes = Record<string, unknown>> = {
@@ -29,7 +29,7 @@ export type DiagnosticContext<BaseContext = {}> = BaseContext & {
29
29
  };
30
30
  export declare const channelsNamespace: "plt:kafka";
31
31
  export declare function createDiagnosticContext<BaseContext = {}>(context: BaseContext): DiagnosticContext<BaseContext>;
32
- export declare function notifyCreation<InstanceType>(type: ClientType | 'connection' | 'connection-pool' | 'messages-stream', instance: InstanceType): void;
32
+ export declare function notifyCreation<InstanceType>(type: ClientType | 'connection' | 'connection-pool' | 'messages-stream' | 'producer-stream', instance: InstanceType): void;
33
33
  export declare function createChannel<DiagnosticEvent extends object>(name: string): ChannelWithName<DiagnosticEvent>;
34
34
  export declare function createTracingChannel<DiagnosticEvent extends object>(name: string): TracingChannelWithName<DiagnosticEvent>;
35
35
  export declare const instancesChannel: ChannelWithName<object>;
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export const name = "@platformatic/kafka";
2
- export const version = "1.27.0";
2
+ export const version = "1.28.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/kafka",
3
- "version": "1.27.0",
3
+ "version": "1.28.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)",
@@ -28,6 +28,7 @@
28
28
  "@platformatic/dynamic-buffer": "^0.3.0",
29
29
  "@platformatic/wasm-utils": "^0.1.0",
30
30
  "ajv": "^8.17.1",
31
+ "avsc": "^5.7.9",
31
32
  "debug": "^4.4.3",
32
33
  "fastq": "^1.19.1",
33
34
  "mnemonist": "^0.40.3",
@@ -53,7 +54,6 @@
53
54
  "@types/node": "^22.18.8",
54
55
  "@types/semver": "^7.7.1",
55
56
  "@watchable/unpromise": "^1.0.2",
56
- "avsc": "^5.7.9",
57
57
  "c8": "^10.1.3",
58
58
  "cleaner-spec-reporter": "^0.5.0",
59
59
  "cronometro": "^5.3.0",