@platformatic/kafka 1.34.0 → 2.0.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
@@ -353,8 +353,7 @@ Many of the methods accept the same options as the client's constructors. The co
353
353
 
354
354
  Node.js LTS versions:
355
355
 
356
- - `20.19.4` or above
357
- - `22.18.0` or above
356
+ - `22.22.0` or above
358
357
  - `24.6.0` or above
359
358
 
360
359
  ## Testing
@@ -1,5 +1,4 @@
1
1
  import { MultipleErrors } from "../errors.js";
2
- import { promiseWithResolvers } from "../utils.js";
3
2
  export const kCallbackPromise = Symbol('plt.kafka.callbackPromise');
4
3
  // This is only meaningful for testing
5
4
  export const kNoopCallbackReturnValue = Symbol('plt.kafka.noopCallbackReturnValue');
@@ -7,7 +6,7 @@ export const noopCallback = () => {
7
6
  return Promise.resolve(kNoopCallbackReturnValue);
8
7
  };
9
8
  export function createPromisifiedCallback() {
10
- const { promise, resolve, reject } = promiseWithResolvers();
9
+ const { promise, resolve, reject } = Promise.withResolvers();
11
10
  function callback(error, payload) {
12
11
  if (error) {
13
12
  reject(error);
@@ -641,7 +641,7 @@ export class Admin extends Base {
641
641
  const topic = r.readString(false);
642
642
  return [topic, { topic, partitions: reader.readArray(r => r.readInt32(), false, false) }];
643
643
  }, false, false);
644
- reader.readBytes(); // Ignore the user data
644
+ // Ignore the user data
645
645
  }
646
646
  group.members.set(member.memberId, {
647
647
  id: member.memberId,
@@ -66,6 +66,7 @@ export declare class Base<OptionsType extends BaseOptions = BaseOptions, EventsT
66
66
  get closed(): boolean;
67
67
  get type(): ClientType;
68
68
  get context(): unknown;
69
+ get currentMetadata(): ClusterMetadata | undefined;
69
70
  get connections(): ConnectionPool;
70
71
  emitWithDebug(section: string | null, name: string, ...args: any[]): boolean;
71
72
  close(callback: CallbackWithPromise<void>): void;
@@ -90,6 +90,9 @@ export class Base extends TypedEventEmitter {
90
90
  get context() {
91
91
  return this[kContext];
92
92
  }
93
+ get currentMetadata() {
94
+ return this.#metadata;
95
+ }
93
96
  get connections() {
94
97
  return this[kConnections];
95
98
  }
@@ -172,6 +172,7 @@ export class Consumer extends Base {
172
172
  }
173
173
  options.autocommit ??= this[kOptions].autocommit ?? true;
174
174
  options.maxBytes ??= this[kOptions].maxBytes;
175
+ options.maxBytesPerPartition ??= this[kOptions].maxBytesPerPartition ?? options.maxBytes;
175
176
  options.highWaterMark ??= this[kOptions].highWaterMark;
176
177
  options.registry ??= this[kOptions].registry;
177
178
  options.beforeDeserialization ??= this[kOptions].beforeDeserialization;
@@ -618,7 +619,7 @@ export class Consumer extends Base {
618
619
  });
619
620
  });
620
621
  }
621
- #listCommittedOffsets(options, callback) {
622
+ #listCommittedOffsets(options, callback, staleMemberEpochRetries = 0) {
622
623
  const topics = [];
623
624
  for (const { topic: name, partitions } of options.topics) {
624
625
  topics.push({ name, partitionIndexes: partitions });
@@ -640,6 +641,18 @@ export class Consumer extends Base {
640
641
  });
641
642
  }, (error, response) => {
642
643
  if (error) {
644
+ if (this.#useConsumerGroupProtocol &&
645
+ staleMemberEpochRetries < this[kOptions].retries &&
646
+ error.findBy?.('apiId', 'STALE_MEMBER_EPOCH')) {
647
+ this.#consumerGroupHeartbeat(this[kOptions], heartbeatError => {
648
+ if (heartbeatError) {
649
+ callback(this.#handleMetadataError(heartbeatError));
650
+ return;
651
+ }
652
+ this.#listCommittedOffsets(options, callback, staleMemberEpochRetries + 1);
653
+ });
654
+ return;
655
+ }
643
656
  callback(this.#handleMetadataError(error));
644
657
  return;
645
658
  }
@@ -1229,12 +1242,16 @@ export class Consumer extends Base {
1229
1242
  https://github.com/apache/kafka/blob/trunk/clients/src/main/resources/common/message/ConsumerProtocolAssignment.json
1230
1243
  */
1231
1244
  #encodeProtocolAssignment(assignments) {
1232
- return Writer.create()
1245
+ const userData = this[kOptions].assignmentUserData;
1246
+ const writer = Writer.create()
1233
1247
  .appendInt16(0) // Version information
1234
1248
  .appendArray(assignments, (w, { topic, partitions }) => {
1235
1249
  w.appendString(topic, false).appendArray(partitions, (w, a) => w.appendInt32(a), false, false);
1236
- }, false, false)
1237
- .appendInt32(0).buffer; // No user data
1250
+ }, false, false);
1251
+ if (userData) {
1252
+ writer.append(userData);
1253
+ }
1254
+ return writer.buffer;
1238
1255
  }
1239
1256
  #decodeProtocolAssignment(buffer) {
1240
1257
  const reader = Reader.from(buffer);
@@ -18,7 +18,6 @@ export declare class MessagesStream<Key, Value, HeaderKey, HeaderValue> extends
18
18
  get context(): unknown;
19
19
  get offsetsToCommit(): Map<string, CommitOptionsPartition>;
20
20
  get offsetsCommitted(): Map<string, bigint>;
21
- get committedOffsets(): Map<string, bigint>;
22
21
  get connections(): ConnectionPool;
23
22
  close(callback: CallbackWithPromise<void>): void;
24
23
  close(): Promise<void>;
@@ -187,11 +187,6 @@ export class MessagesStream extends Readable {
187
187
  get offsetsCommitted() {
188
188
  return this.#offsetsCommitted;
189
189
  }
190
- // TODO: This is deprecated alias, remove in future major version
191
- /* c8 ignore next 3 - Simple getter */
192
- get committedOffsets() {
193
- return this.#offsetsCommitted;
194
- }
195
190
  /* c8 ignore next 3 - Simple getter */
196
191
  get connections() {
197
192
  return this[kConnections];
@@ -389,7 +384,7 @@ export class MessagesStream extends Readable {
389
384
  {
390
385
  partition,
391
386
  fetchOffset,
392
- partitionMaxBytes: this.#options.maxBytes,
387
+ partitionMaxBytes: this.#options.maxBytesPerPartition,
393
388
  currentLeaderEpoch: leaderEpoch,
394
389
  lastFetchedEpoch: leaderEpoch
395
390
  }
@@ -97,6 +97,10 @@ export declare const consumeOptionsProperties: {
97
97
  type: string;
98
98
  minimum: number;
99
99
  };
100
+ maxBytesPerPartition: {
101
+ type: string;
102
+ minimum: number;
103
+ };
100
104
  maxWaitTime: {
101
105
  type: string;
102
106
  minimum: number;
@@ -217,6 +221,10 @@ export declare const consumeOptionsSchema: {
217
221
  type: string;
218
222
  minimum: number;
219
223
  };
224
+ maxBytesPerPartition: {
225
+ type: string;
226
+ minimum: number;
227
+ };
220
228
  maxWaitTime: {
221
229
  type: string;
222
230
  minimum: number;
@@ -379,6 +387,10 @@ export declare const consumerOptionsSchema: {
379
387
  type: string;
380
388
  minimum: number;
381
389
  };
390
+ maxBytesPerPartition: {
391
+ type: string;
392
+ minimum: number;
393
+ };
382
394
  maxWaitTime: {
383
395
  type: string;
384
396
  minimum: number;
@@ -502,6 +514,10 @@ export declare const fetchOptionsSchema: {
502
514
  type: string;
503
515
  minimum: number;
504
516
  };
517
+ maxBytesPerPartition: {
518
+ type: string;
519
+ minimum: number;
520
+ };
505
521
  maxWaitTime: {
506
522
  type: string;
507
523
  minimum: number;
@@ -58,6 +58,7 @@ export const consumeOptionsProperties = {
58
58
  autocommit: { oneOf: [{ type: 'boolean' }, { type: 'number', minimum: 100 }] },
59
59
  minBytes: { type: 'number', minimum: 0 },
60
60
  maxBytes: { type: 'number', minimum: 0 },
61
+ maxBytesPerPartition: { type: 'number', minimum: 0 },
61
62
  maxWaitTime: { type: 'number', minimum: 0 },
62
63
  isolationLevel: { type: 'number', enum: allowedFetchIsolationLevels },
63
64
  deserializers: serdeProperties,
@@ -245,7 +246,7 @@ export const defaultConsumerOptions = {
245
246
  heartbeatInterval: 3000,
246
247
  protocols: [{ name: 'roundrobin', version: 1 }],
247
248
  minBytes: 1,
248
- maxBytes: 1_048_576 * 10, // 10 MB
249
+ maxBytes: 1_048_576 * 50, // 50 MB
249
250
  maxWaitTime: 5_000,
250
251
  isolationLevel: FetchIsolationLevels.READ_COMMITTED,
251
252
  highWaterMark: 1024
@@ -54,17 +54,20 @@ export interface GroupOptions {
54
54
  heartbeatInterval?: number;
55
55
  protocols?: GroupProtocolSubscription[];
56
56
  partitionAssigner?: GroupPartitionsAssigner;
57
+ assignmentUserData?: Buffer;
57
58
  }
58
59
  export interface ConsumerGroupOptions {
59
60
  groupProtocol: typeof GroupProtocols.CONSUMER;
60
61
  groupInstanceId?: string;
61
62
  groupRemoteAssignor?: string;
62
63
  rebalanceTimeout?: number;
64
+ assignmentUserData?: Buffer;
63
65
  }
64
66
  export interface ConsumeBaseOptions<Key, Value, HeaderKey, HeaderValue> {
65
67
  autocommit?: boolean | number;
66
68
  minBytes?: number;
67
69
  maxBytes?: number;
70
+ maxBytesPerPartition?: number;
68
71
  maxWaitTime?: number;
69
72
  isolationLevel?: number;
70
73
  deserializers?: Partial<Deserializers<Key, Value, HeaderKey, HeaderValue>>;
@@ -838,7 +838,7 @@ export class Producer extends Base {
838
838
  let partition = 0;
839
839
  if (typeof message.partition !== 'number') {
840
840
  if (partitioner) {
841
- partition = partitioner(message, key);
841
+ partition = partitioner(message, key, { metadata: this.currentMetadata });
842
842
  }
843
843
  else if (key) {
844
844
  partition = defaultPartitioner(message, key);
@@ -1,7 +1,7 @@
1
1
  import { type CompressionAlgorithmValue } from '../../protocol/compression.ts';
2
2
  import { type MessageToProduce } from '../../protocol/records.ts';
3
3
  import { type SchemaRegistry } from '../../registries/abstract.ts';
4
- import { type BaseOptions, type TopicWithPartitionAndOffset } from '../base/types.ts';
4
+ import { type BaseOptions, type ClusterMetadata, type TopicWithPartitionAndOffset } from '../base/types.ts';
5
5
  import { type BeforeSerializationHook, type Serializers } from '../serde.ts';
6
6
  export interface ProducerInfo {
7
7
  producerId: bigint;
@@ -12,7 +12,10 @@ export interface ProduceResult {
12
12
  offsets?: TopicWithPartitionAndOffset[];
13
13
  unwritableNodes?: number[];
14
14
  }
15
- export type Partitioner<Key, Value, HeaderKey, HeaderValue> = (message: MessageToProduce<Key, Value, HeaderKey, HeaderValue>, key?: Buffer | undefined) => number;
15
+ export interface PartitionerContext {
16
+ metadata: ClusterMetadata | undefined;
17
+ }
18
+ export type Partitioner<Key, Value, HeaderKey, HeaderValue> = (message: MessageToProduce<Key, Value, HeaderKey, HeaderValue>, key: Buffer | undefined, context: PartitionerContext) => number;
16
19
  export interface ProducerStreamReport {
17
20
  batchId: number;
18
21
  count: number;
@@ -15,9 +15,31 @@ function ensureBuffer(data) {
15
15
  return DynamicBuffer.isDynamicBuffer(data) ? data.buffer : data;
16
16
  }
17
17
  const snappyCompressSync = snappyCompress;
18
- const snappyDecompressSync = snappyDecompress;
19
18
  const lz4CompressFrameSync = lz4Compress;
20
19
  const lz4DecompressFrameSync = lz4Decompress;
20
+ const xerialSnappyHeader = Buffer.from([0x82, 0x53, 0x4e, 0x41, 0x50, 0x50, 0x59, 0x00]);
21
+ function snappyDecompressSync(data) {
22
+ if (!data.subarray(0, xerialSnappyHeader.length).equals(xerialSnappyHeader)) {
23
+ return snappyDecompress(data);
24
+ }
25
+ const decompressed = new DynamicBuffer();
26
+ let offset = 16;
27
+ while (offset < data.length) {
28
+ /* c8 ignore next 3 - Hard to test */
29
+ if (offset + 4 > data.length) {
30
+ throw new Error('Invalid xerial snappy chunk length');
31
+ }
32
+ const chunkLength = data.readUInt32BE(offset);
33
+ offset += 4;
34
+ /* c8 ignore next 3 - Hard to test */
35
+ if (offset + chunkLength > data.length) {
36
+ throw new Error('Invalid xerial snappy chunk data');
37
+ }
38
+ decompressed.append(snappyDecompress(data.subarray(offset, offset + chunkLength)));
39
+ offset += chunkLength;
40
+ }
41
+ return decompressed.buffer;
42
+ }
21
43
  export const compressionsAlgorithms = {
22
44
  /* c8 ignore next 8 - 'none' is actually never used but this is to please Typescript */
23
45
  none: {
@@ -66,6 +66,7 @@ export declare class Base<OptionsType extends BaseOptions = BaseOptions, EventsT
66
66
  get closed(): boolean;
67
67
  get type(): ClientType;
68
68
  get context(): unknown;
69
+ get currentMetadata(): ClusterMetadata | undefined;
69
70
  get connections(): ConnectionPool;
70
71
  emitWithDebug(section: string | null, name: string, ...args: any[]): boolean;
71
72
  close(callback: CallbackWithPromise<void>): void;
@@ -18,7 +18,6 @@ export declare class MessagesStream<Key, Value, HeaderKey, HeaderValue> extends
18
18
  get context(): unknown;
19
19
  get offsetsToCommit(): Map<string, CommitOptionsPartition>;
20
20
  get offsetsCommitted(): Map<string, bigint>;
21
- get committedOffsets(): Map<string, bigint>;
22
21
  get connections(): ConnectionPool;
23
22
  close(callback: CallbackWithPromise<void>): void;
24
23
  close(): Promise<void>;
@@ -97,6 +97,10 @@ export declare const consumeOptionsProperties: {
97
97
  type: string;
98
98
  minimum: number;
99
99
  };
100
+ maxBytesPerPartition: {
101
+ type: string;
102
+ minimum: number;
103
+ };
100
104
  maxWaitTime: {
101
105
  type: string;
102
106
  minimum: number;
@@ -217,6 +221,10 @@ export declare const consumeOptionsSchema: {
217
221
  type: string;
218
222
  minimum: number;
219
223
  };
224
+ maxBytesPerPartition: {
225
+ type: string;
226
+ minimum: number;
227
+ };
220
228
  maxWaitTime: {
221
229
  type: string;
222
230
  minimum: number;
@@ -379,6 +387,10 @@ export declare const consumerOptionsSchema: {
379
387
  type: string;
380
388
  minimum: number;
381
389
  };
390
+ maxBytesPerPartition: {
391
+ type: string;
392
+ minimum: number;
393
+ };
382
394
  maxWaitTime: {
383
395
  type: string;
384
396
  minimum: number;
@@ -502,6 +514,10 @@ export declare const fetchOptionsSchema: {
502
514
  type: string;
503
515
  minimum: number;
504
516
  };
517
+ maxBytesPerPartition: {
518
+ type: string;
519
+ minimum: number;
520
+ };
505
521
  maxWaitTime: {
506
522
  type: string;
507
523
  minimum: number;
@@ -54,17 +54,20 @@ export interface GroupOptions {
54
54
  heartbeatInterval?: number;
55
55
  protocols?: GroupProtocolSubscription[];
56
56
  partitionAssigner?: GroupPartitionsAssigner;
57
+ assignmentUserData?: Buffer;
57
58
  }
58
59
  export interface ConsumerGroupOptions {
59
60
  groupProtocol: typeof GroupProtocols.CONSUMER;
60
61
  groupInstanceId?: string;
61
62
  groupRemoteAssignor?: string;
62
63
  rebalanceTimeout?: number;
64
+ assignmentUserData?: Buffer;
63
65
  }
64
66
  export interface ConsumeBaseOptions<Key, Value, HeaderKey, HeaderValue> {
65
67
  autocommit?: boolean | number;
66
68
  minBytes?: number;
67
69
  maxBytes?: number;
70
+ maxBytesPerPartition?: number;
68
71
  maxWaitTime?: number;
69
72
  isolationLevel?: number;
70
73
  deserializers?: Partial<Deserializers<Key, Value, HeaderKey, HeaderValue>>;
@@ -1,7 +1,7 @@
1
1
  import { type CompressionAlgorithmValue } from "../../protocol/compression";
2
2
  import { type MessageToProduce } from "../../protocol/records";
3
3
  import { type SchemaRegistry } from "../../registries/abstract";
4
- import { type BaseOptions, type TopicWithPartitionAndOffset } from "../base/types";
4
+ import { type BaseOptions, type ClusterMetadata, type TopicWithPartitionAndOffset } from "../base/types";
5
5
  import { type BeforeSerializationHook, type Serializers } from "../serde";
6
6
  export interface ProducerInfo {
7
7
  producerId: bigint;
@@ -12,7 +12,10 @@ export interface ProduceResult {
12
12
  offsets?: TopicWithPartitionAndOffset[];
13
13
  unwritableNodes?: number[];
14
14
  }
15
- export type Partitioner<Key, Value, HeaderKey, HeaderValue> = (message: MessageToProduce<Key, Value, HeaderKey, HeaderValue>, key?: Buffer | undefined) => number;
15
+ export interface PartitionerContext {
16
+ metadata: ClusterMetadata | undefined;
17
+ }
18
+ export type Partitioner<Key, Value, HeaderKey, HeaderValue> = (message: MessageToProduce<Key, Value, HeaderKey, HeaderValue>, key: Buffer | undefined, context: PartitionerContext) => number;
16
19
  export interface ProducerStreamReport {
17
20
  batchId: number;
18
21
  count: number;
@@ -15,12 +15,6 @@ export interface DataValidationContext {
15
15
  }
16
16
  export type DebugDumpLogger = (...args: any[]) => void;
17
17
  export { setTimeout as sleep } from 'node:timers/promises';
18
- interface PromiseWithResolvers<T> {
19
- promise: Promise<T>;
20
- resolve: (value: T | PromiseLike<T>) => void;
21
- reject: (reason?: any) => void;
22
- }
23
- export declare const promiseWithResolvers: <T>() => PromiseWithResolvers<T>;
24
18
  export declare const ajv: Ajv2020;
25
19
  export declare const loggers: Record<string, debug.Debugger>;
26
20
  export declare class NumericMap extends Map<string, number> {
package/dist/utils.d.ts CHANGED
@@ -15,12 +15,6 @@ export interface DataValidationContext {
15
15
  }
16
16
  export type DebugDumpLogger = (...args: any[]) => void;
17
17
  export { setTimeout as sleep } from 'node:timers/promises';
18
- interface PromiseWithResolvers<T> {
19
- promise: Promise<T>;
20
- resolve: (value: T | PromiseLike<T>) => void;
21
- reject: (reason?: any) => void;
22
- }
23
- export declare const promiseWithResolvers: <T>() => PromiseWithResolvers<T>;
24
18
  export declare const ajv: Ajv2020;
25
19
  export declare const loggers: Record<string, debug.Debugger>;
26
20
  export declare class NumericMap extends Map<string, number> {
package/dist/utils.js CHANGED
@@ -2,20 +2,6 @@ import { Ajv2020 } from 'ajv/dist/2020.js';
2
2
  import debug from 'debug';
3
3
  import { inspect } from 'node:util';
4
4
  export { setTimeout as sleep } from 'node:timers/promises';
5
- function promiseWithResolversPolyfill() {
6
- let resolve;
7
- let reject;
8
- const promise = new Promise((_resolve, _reject) => {
9
- resolve = _resolve;
10
- reject = _reject;
11
- });
12
- // @ts-expect-error - resolve and reject are assigned in the promise constructor
13
- return { promise, resolve, reject };
14
- }
15
- export const promiseWithResolvers = Promise.withResolvers
16
- ? Promise.withResolvers.bind(Promise)
17
- : promiseWithResolversPolyfill;
18
- /* c8 ignore end */
19
5
  export const ajv = new Ajv2020({ allErrors: true, coerceTypes: false, strict: true });
20
6
  export const loggers = {
21
7
  protocol: debug('plt:kafka:protocol'),
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export const name = "@platformatic/kafka";
2
- export const version = "1.34.0";
2
+ export const version = "2.0.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/kafka",
3
- "version": "1.34.0",
3
+ "version": "2.0.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)",
@@ -59,7 +59,6 @@
59
59
  "cronometro": "^5.3.0",
60
60
  "eslint": "^9.35.0",
61
61
  "fast-jwt": "^6.0.2",
62
- "glob": "^13.0.0",
63
62
  "hwp": "^0.4.1",
64
63
  "json5": "^2.2.3",
65
64
  "kafkajs": "^2.2.4",
@@ -76,7 +75,7 @@
76
75
  "typescript": "^5.9.2"
77
76
  },
78
77
  "engines": {
79
- "node": ">= 20.19.4 || >= 22.18.0 || >= 24.6.0"
78
+ "node": ">= 22.22.0 || >= 24.6.0"
80
79
  },
81
80
  "typesVersions": {
82
81
  "<5.0": {