kafka-ts 0.0.3-beta → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +68 -8
  2. package/dist/api/api-versions.d.ts +9 -0
  3. package/dist/api/api-versions.js +24 -0
  4. package/dist/api/create-topics.d.ts +38 -0
  5. package/dist/api/create-topics.js +53 -0
  6. package/dist/api/delete-topics.d.ts +18 -0
  7. package/dist/api/delete-topics.js +33 -0
  8. package/dist/api/fetch.d.ts +84 -0
  9. package/dist/api/fetch.js +142 -0
  10. package/dist/api/find-coordinator.d.ts +21 -0
  11. package/dist/api/find-coordinator.js +39 -0
  12. package/dist/api/heartbeat.d.ts +11 -0
  13. package/dist/api/heartbeat.js +27 -0
  14. package/dist/api/index.d.ts +576 -0
  15. package/dist/api/index.js +165 -0
  16. package/dist/api/init-producer-id.d.ts +13 -0
  17. package/dist/api/init-producer-id.js +29 -0
  18. package/dist/api/join-group.d.ts +34 -0
  19. package/dist/api/join-group.js +51 -0
  20. package/dist/api/leave-group.d.ts +19 -0
  21. package/dist/api/leave-group.js +39 -0
  22. package/dist/api/list-offsets.d.ts +29 -0
  23. package/dist/api/list-offsets.js +48 -0
  24. package/dist/api/metadata.d.ts +40 -0
  25. package/dist/api/metadata.js +58 -0
  26. package/dist/api/offset-commit.d.ts +28 -0
  27. package/dist/api/offset-commit.js +48 -0
  28. package/dist/api/offset-fetch.d.ts +31 -0
  29. package/dist/api/offset-fetch.js +55 -0
  30. package/dist/api/produce.d.ts +54 -0
  31. package/dist/api/produce.js +126 -0
  32. package/dist/api/sasl-authenticate.d.ts +11 -0
  33. package/dist/api/sasl-authenticate.js +23 -0
  34. package/dist/api/sasl-handshake.d.ts +6 -0
  35. package/dist/api/sasl-handshake.js +19 -0
  36. package/dist/api/sync-group.d.ts +24 -0
  37. package/dist/api/sync-group.js +36 -0
  38. package/dist/auth/index.d.ts +2 -0
  39. package/dist/auth/index.js +8 -0
  40. package/dist/auth/plain.d.ts +5 -0
  41. package/dist/auth/plain.js +12 -0
  42. package/dist/auth/scram.d.ts +9 -0
  43. package/dist/auth/scram.js +40 -0
  44. package/dist/broker.d.ts +30 -0
  45. package/dist/broker.js +55 -0
  46. package/dist/client.d.ts +23 -0
  47. package/dist/client.js +36 -0
  48. package/dist/cluster.d.ts +27 -0
  49. package/dist/cluster.js +70 -0
  50. package/dist/cluster.test.d.ts +1 -0
  51. package/dist/cluster.test.js +343 -0
  52. package/dist/codecs/gzip.d.ts +2 -0
  53. package/dist/codecs/gzip.js +8 -0
  54. package/dist/codecs/index.d.ts +2 -0
  55. package/dist/codecs/index.js +17 -0
  56. package/dist/codecs/none.d.ts +2 -0
  57. package/dist/codecs/none.js +7 -0
  58. package/dist/codecs/types.d.ts +5 -0
  59. package/dist/codecs/types.js +2 -0
  60. package/dist/connection.d.ts +26 -0
  61. package/dist/connection.js +175 -0
  62. package/dist/consumer/consumer-group.d.ts +41 -0
  63. package/dist/consumer/consumer-group.js +215 -0
  64. package/dist/consumer/consumer-metadata.d.ts +7 -0
  65. package/dist/consumer/consumer-metadata.js +14 -0
  66. package/dist/consumer/consumer.d.ts +44 -0
  67. package/dist/consumer/consumer.js +225 -0
  68. package/dist/consumer/fetch-manager.d.ts +33 -0
  69. package/dist/consumer/fetch-manager.js +140 -0
  70. package/dist/consumer/fetcher.d.ts +25 -0
  71. package/dist/consumer/fetcher.js +64 -0
  72. package/dist/consumer/offset-manager.d.ts +22 -0
  73. package/dist/consumer/offset-manager.js +66 -0
  74. package/dist/consumer/processor.d.ts +19 -0
  75. package/dist/consumer/processor.js +59 -0
  76. package/dist/distributors/assignments-to-replicas.d.ts +16 -0
  77. package/dist/distributors/assignments-to-replicas.js +59 -0
  78. package/dist/distributors/assignments-to-replicas.test.d.ts +1 -0
  79. package/dist/distributors/assignments-to-replicas.test.js +40 -0
  80. package/dist/distributors/messages-to-topic-partition-leaders.d.ts +17 -0
  81. package/dist/distributors/messages-to-topic-partition-leaders.js +15 -0
  82. package/dist/distributors/messages-to-topic-partition-leaders.test.d.ts +1 -0
  83. package/dist/distributors/messages-to-topic-partition-leaders.test.js +30 -0
  84. package/dist/distributors/partitioner.d.ts +7 -0
  85. package/dist/distributors/partitioner.js +23 -0
  86. package/dist/index.d.ts +9 -0
  87. package/dist/index.js +26 -0
  88. package/dist/metadata.d.ts +24 -0
  89. package/dist/metadata.js +106 -0
  90. package/dist/producer/producer.d.ts +24 -0
  91. package/dist/producer/producer.js +131 -0
  92. package/dist/types.d.ts +11 -0
  93. package/dist/types.js +2 -0
  94. package/dist/utils/api.d.ts +9 -0
  95. package/dist/utils/api.js +5 -0
  96. package/dist/utils/crypto.d.ts +8 -0
  97. package/dist/utils/crypto.js +18 -0
  98. package/dist/utils/decoder.d.ts +30 -0
  99. package/dist/utils/decoder.js +152 -0
  100. package/dist/utils/delay.d.ts +1 -0
  101. package/dist/utils/delay.js +5 -0
  102. package/dist/utils/encoder.d.ts +28 -0
  103. package/dist/utils/encoder.js +125 -0
  104. package/dist/utils/error.d.ts +11 -0
  105. package/dist/utils/error.js +27 -0
  106. package/dist/utils/logger.d.ts +9 -0
  107. package/dist/utils/logger.js +32 -0
  108. package/dist/utils/memo.d.ts +1 -0
  109. package/dist/utils/memo.js +16 -0
  110. package/dist/utils/murmur2.d.ts +3 -0
  111. package/dist/utils/murmur2.js +40 -0
  112. package/dist/utils/retrier.d.ts +10 -0
  113. package/dist/utils/retrier.js +22 -0
  114. package/dist/utils/tracer.d.ts +5 -0
  115. package/dist/utils/tracer.js +39 -0
  116. package/package.json +11 -2
  117. package/src/__snapshots__/{request-handler.test.ts.snap → cluster.test.ts.snap} +329 -26
  118. package/src/api/fetch.ts +84 -29
  119. package/src/api/index.ts +3 -1
  120. package/src/api/metadata.ts +1 -1
  121. package/src/api/offset-commit.ts +1 -1
  122. package/src/api/offset-fetch.ts +1 -5
  123. package/src/api/produce.ts +15 -18
  124. package/src/auth/index.ts +2 -0
  125. package/src/auth/plain.ts +10 -0
  126. package/src/auth/scram.ts +52 -0
  127. package/src/broker.ts +7 -9
  128. package/src/client.ts +2 -2
  129. package/src/cluster.test.ts +16 -14
  130. package/src/cluster.ts +38 -40
  131. package/src/codecs/gzip.ts +9 -0
  132. package/src/codecs/index.ts +16 -0
  133. package/src/codecs/none.ts +6 -0
  134. package/src/codecs/types.ts +4 -0
  135. package/src/connection.ts +31 -17
  136. package/src/consumer/consumer-group.ts +43 -23
  137. package/src/consumer/consumer.ts +64 -43
  138. package/src/consumer/fetch-manager.ts +43 -53
  139. package/src/consumer/fetcher.ts +20 -13
  140. package/src/consumer/offset-manager.ts +18 -7
  141. package/src/consumer/processor.ts +14 -8
  142. package/src/distributors/assignments-to-replicas.ts +1 -3
  143. package/src/distributors/partitioner.ts +27 -0
  144. package/src/index.ts +7 -2
  145. package/src/metadata.ts +4 -0
  146. package/src/producer/producer.ts +22 -12
  147. package/src/types.ts +3 -3
  148. package/src/utils/api.ts +1 -1
  149. package/src/utils/crypto.ts +15 -0
  150. package/src/utils/decoder.ts +11 -5
  151. package/src/utils/encoder.ts +29 -22
  152. package/src/utils/logger.ts +37 -0
  153. package/src/utils/murmur2.ts +44 -0
  154. package/src/utils/tracer.ts +40 -22
  155. package/.github/workflows/release.yml +0 -17
  156. package/certs/ca.crt +0 -29
  157. package/certs/ca.key +0 -52
  158. package/certs/ca.srl +0 -1
  159. package/certs/kafka.crt +0 -29
  160. package/certs/kafka.csr +0 -26
  161. package/certs/kafka.key +0 -52
  162. package/certs/kafka.keystore.jks +0 -0
  163. package/certs/kafka.truststore.jks +0 -0
  164. package/docker-compose.yml +0 -104
  165. package/examples/package-lock.json +0 -31
  166. package/examples/package.json +0 -14
  167. package/examples/src/client.ts +0 -9
  168. package/examples/src/consumer.ts +0 -18
  169. package/examples/src/create-topic.ts +0 -44
  170. package/examples/src/producer.ts +0 -24
  171. package/examples/src/replicator.ts +0 -25
  172. package/examples/src/utils/delay.ts +0 -1
  173. package/examples/src/utils/json.ts +0 -1
  174. package/examples/tsconfig.json +0 -7
  175. package/log4j.properties +0 -95
  176. package/scripts/generate-certs.sh +0 -24
  177. package/src/utils/debug.ts +0 -9
@@ -3,8 +3,11 @@ import { IsolationLevel } from '../api/fetch';
3
3
  import { Assignment } from '../api/sync-group';
4
4
  import { Cluster } from '../cluster';
5
5
  import { distributeMessagesToTopicPartitionLeaders } from '../distributors/messages-to-topic-partition-leaders';
6
+ import { createTracer } from '../utils/tracer';
6
7
  import { ConsumerMetadata } from './consumer-metadata';
7
8
 
9
+ const trace = createTracer('OffsetManager');
10
+
8
11
  type OffsetManagerOptions = {
9
12
  cluster: Cluster;
10
13
  metadata: ConsumerMetadata;
@@ -24,13 +27,18 @@ export class OffsetManager {
24
27
  public resolve(topic: string, partition: number, offset: bigint) {
25
28
  this.pendingOffsets[topic] ??= {};
26
29
  this.pendingOffsets[topic][partition] = offset;
27
-
28
- this.currentOffsets[topic] ??= {};
29
- this.currentOffsets[topic][partition] = offset;
30
30
  }
31
31
 
32
- public flush() {
33
- this.pendingOffsets = {};
32
+ public flush(topicPartitions: Record<string, Set<number>>) {
33
+ Object.entries(topicPartitions).forEach(([topic, partitions]) => {
34
+ this.currentOffsets[topic] ??= {};
35
+ partitions.forEach((partition) => {
36
+ if (this.pendingOffsets[topic]?.[partition]) {
37
+ this.currentOffsets[topic][partition] = this.pendingOffsets[topic][partition];
38
+ delete this.pendingOffsets[topic][partition];
39
+ }
40
+ });
41
+ });
34
42
  }
35
43
 
36
44
  public async fetchOffsets(options: { fromBeginning: boolean }) {
@@ -58,7 +66,6 @@ export class OffsetManager {
58
66
  }),
59
67
  ),
60
68
  );
61
- this.flush();
62
69
  }
63
70
 
64
71
  private async listOffsets({
@@ -83,11 +90,15 @@ export class OffsetManager {
83
90
  })),
84
91
  });
85
92
 
93
+ const topicPartitions: Record<string, Set<number>> = {};
86
94
  offsets.topics.forEach(({ name, partitions }) => {
95
+ topicPartitions[name] ??= new Set();
87
96
  partitions.forEach(({ partitionIndex, offset }) => {
97
+ topicPartitions[name].add(partitionIndex);
88
98
  this.resolve(name, partitionIndex, fromBeginning ? 0n : offset);
89
99
  });
90
100
  });
91
- this.flush();
101
+
102
+ this.flush(topicPartitions);
92
103
  }
93
104
  }
@@ -9,7 +9,7 @@ type ProcessorOptions = {
9
9
  process: (batch: Batch) => Promise<void>;
10
10
  };
11
11
 
12
- export class Processor extends EventEmitter<{ stop: []; stopped: [] }> {
12
+ export class Processor extends EventEmitter<{ stopped: [] }> {
13
13
  private isRunning = false;
14
14
 
15
15
  constructor(private options: ProcessorOptions) {
@@ -17,15 +17,11 @@ export class Processor extends EventEmitter<{ stop: []; stopped: [] }> {
17
17
  }
18
18
 
19
19
  public async loop() {
20
- const { poll, process } = this.options;
21
-
22
20
  this.isRunning = true;
23
- this.once('stop', () => (this.isRunning = false));
24
21
 
25
22
  try {
26
23
  while (this.isRunning) {
27
- const batch = await poll();
28
- await process(batch);
24
+ await this.step();
29
25
  }
30
26
  } finally {
31
27
  this.isRunning = false;
@@ -34,14 +30,24 @@ export class Processor extends EventEmitter<{ stop: []; stopped: [] }> {
34
30
  }
35
31
 
36
32
  @trace()
33
+ private async step() {
34
+ const { poll, process } = this.options;
35
+
36
+ const batch = await poll();
37
+ if (batch.length) {
38
+ await process(batch);
39
+ }
40
+ }
41
+
37
42
  public async stop() {
38
43
  if (!this.isRunning) {
39
44
  return;
40
45
  }
41
46
 
42
- return new Promise<void>((resolve) => {
47
+ const stopPromise = new Promise<void>((resolve) => {
43
48
  this.once('stopped', resolve);
44
- this.emit('stop');
45
49
  });
50
+ this.isRunning = false;
51
+ return stopPromise;
46
52
  }
47
53
  }
@@ -1,6 +1,6 @@
1
1
  type Assignment = { [topicName: string]: number[] };
2
2
  type TopicPartitionReplicaIds = { [topicName: string]: { [partition: number]: number[] } };
3
- export type NodeAssignment = { [replicaId: number]: Assignment };
3
+ type NodeAssignment = { [replicaId: number]: Assignment };
4
4
 
5
5
  /** From replica ids pick the one with fewest assignments to balance the load across brokers */
6
6
  export const distributeAssignmentsToNodesBalanced = (
@@ -81,5 +81,3 @@ const getPartitionsByReplica = (assignment: Assignment, topicPartitionReplicaIds
81
81
  }
82
82
  return Object.entries(partitionsByReplicaId);
83
83
  };
84
-
85
- export const distributeAssignmentsToNodes = distributeAssignmentsToNodesBalanced;
@@ -0,0 +1,27 @@
1
+ import { Metadata } from '../metadata';
2
+ import { Message } from '../types';
3
+ import { murmur2, toPositive } from '../utils/murmur2';
4
+
5
+ export type Partition = (message: Message) => number;
6
+ export type Partitioner = (context: { metadata: Metadata }) => Partition;
7
+
8
+ export const defaultPartitioner: Partitioner = ({ metadata }) => {
9
+ const topicCounterMap: Record<string, number> = {};
10
+
11
+ const getNextValue = (topic: string) => {
12
+ topicCounterMap[topic] ??= 0;
13
+ return topicCounterMap[topic]++;
14
+ };
15
+
16
+ return ({ topic, partition, key }: Message) => {
17
+ if (partition !== null && partition !== undefined) {
18
+ return partition;
19
+ }
20
+ const partitions = metadata.getTopicPartitions()[topic];
21
+ const numPartitions = partitions.length;
22
+ if (key) {
23
+ return toPositive(murmur2(key)) % numPartitions;
24
+ }
25
+ return toPositive(getNextValue(topic)) % numPartitions;
26
+ };
27
+ };
package/src/index.ts CHANGED
@@ -1,4 +1,9 @@
1
- export * from './utils/error';
2
- export * from './client';
3
1
  export * from './api';
2
+ export * from './auth';
3
+ export { SASLProvider } from './broker';
4
+ export * from './client';
5
+ export * from './distributors/partitioner';
4
6
  export * from './types';
7
+ export * from './utils/error';
8
+ export * from './utils/logger';
9
+ export { Tracer, setTracer } from './utils/tracer';
package/src/metadata.ts CHANGED
@@ -2,6 +2,9 @@ import { API, API_ERROR } from './api';
2
2
  import { Cluster } from './cluster';
3
3
  import { delay } from './utils/delay';
4
4
  import { KafkaTSApiError } from './utils/error';
5
+ import { createTracer } from './utils/tracer';
6
+
7
+ const trace = createTracer('Metadata');
5
8
 
6
9
  type MetadataOptions = {
7
10
  cluster: Cluster;
@@ -36,6 +39,7 @@ export class Metadata {
36
39
  return this.topicNameById[id];
37
40
  }
38
41
 
42
+ @trace()
39
43
  public async fetchMetadataIfNecessary({
40
44
  topics,
41
45
  allowTopicAutoCreation,
@@ -1,14 +1,19 @@
1
1
  import { API, API_ERROR } from '../api';
2
2
  import { Cluster } from '../cluster';
3
3
  import { distributeMessagesToTopicPartitionLeaders } from '../distributors/messages-to-topic-partition-leaders';
4
+ import { defaultPartitioner, Partition, Partitioner } from '../distributors/partitioner';
4
5
  import { Metadata } from '../metadata';
5
6
  import { Message } from '../types';
6
7
  import { delay } from '../utils/delay';
7
8
  import { KafkaTSApiError } from '../utils/error';
8
9
  import { memo } from '../utils/memo';
10
+ import { createTracer } from '../utils/tracer';
11
+
12
+ const trace = createTracer('Producer');
9
13
 
10
14
  export type ProducerOptions = {
11
15
  allowTopicAutoCreation?: boolean;
16
+ partitioner?: Partitioner;
12
17
  };
13
18
 
14
19
  export class Producer {
@@ -17,6 +22,7 @@ export class Producer {
17
22
  private producerId = 0n;
18
23
  private producerEpoch = 0;
19
24
  private sequences: Record<string, Record<number, number>> = {};
25
+ private partition: Partition;
20
26
 
21
27
  constructor(
22
28
  private cluster: Cluster,
@@ -25,11 +31,14 @@ export class Producer {
25
31
  this.options = {
26
32
  ...options,
27
33
  allowTopicAutoCreation: options.allowTopicAutoCreation ?? false,
34
+ partitioner: options.partitioner ?? defaultPartitioner,
28
35
  };
29
36
  this.metadata = new Metadata({ cluster });
37
+ this.partition = this.options.partitioner({ metadata: this.metadata });
30
38
  }
31
39
 
32
- public async send(messages: Message[]) {
40
+ @trace(() => ({ root: true }))
41
+ public async send(messages: Message[], { acks = -1 }: { acks?: -1 | 1 } = {}) {
33
42
  await this.ensureConnected();
34
43
 
35
44
  const { allowTopicAutoCreation } = this.options;
@@ -39,19 +48,20 @@ export class Producer {
39
48
  await this.metadata.fetchMetadataIfNecessary({ topics, allowTopicAutoCreation });
40
49
 
41
50
  const nodeTopicPartitionMessages = distributeMessagesToTopicPartitionLeaders(
42
- messages,
51
+ messages.map((message) => ({ ...message, partition: this.partition(message) })),
43
52
  this.metadata.getTopicPartitionLeaderIds(),
44
53
  );
45
54
 
46
55
  await Promise.all(
47
- Object.entries(nodeTopicPartitionMessages).map(async ([nodeId, topicPartitionMessages]) => {
48
- await this.cluster.sendRequestToNode(parseInt(nodeId))(API.PRODUCE, {
56
+ Object.entries(nodeTopicPartitionMessages).map(([nodeId, topicPartitionMessages]) =>
57
+ this.cluster.sendRequestToNode(parseInt(nodeId))(API.PRODUCE, {
49
58
  transactionalId: null,
50
- acks: 1,
59
+ acks,
51
60
  timeoutMs: 5000,
52
61
  topicData: Object.entries(topicPartitionMessages).map(([topic, partitionMessages]) => ({
53
62
  name: topic,
54
63
  partitionData: Object.entries(partitionMessages).map(([partition, messages]) => {
64
+ const partitionIndex = parseInt(partition);
55
65
  let baseTimestamp: bigint | undefined;
56
66
  let maxTimestamp: bigint | undefined;
57
67
 
@@ -64,9 +74,9 @@ export class Producer {
64
74
  }
65
75
  });
66
76
 
67
- const baseSequence = this.nextSequence(topic, parseInt(partition), messages.length);
77
+ const baseSequence = this.nextSequence(topic, partitionIndex, messages.length);
68
78
  return {
69
- index: parseInt(partition),
79
+ index: partitionIndex,
70
80
  baseOffset: 0n,
71
81
  partitionLeaderEpoch: -1,
72
82
  attributes: 0,
@@ -80,18 +90,18 @@ export class Producer {
80
90
  attributes: 0,
81
91
  timestampDelta: (message.timestamp ?? defaultTimestamp) - (baseTimestamp ?? 0n),
82
92
  offsetDelta: index,
83
- key: message.key,
93
+ key: message.key ?? null,
84
94
  value: message.value,
85
95
  headers: Object.entries(message.headers ?? {}).map(([key, value]) => ({
86
- key,
87
- value,
96
+ key: Buffer.from(key),
97
+ value: Buffer.from(value),
88
98
  })),
89
99
  })),
90
100
  };
91
101
  }),
92
102
  })),
93
- });
94
- }),
103
+ }),
104
+ ),
95
105
  );
96
106
  }
97
107
 
package/src/types.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export type Message = {
2
2
  topic: string;
3
- partition: number;
3
+ partition?: number;
4
4
  offset?: bigint;
5
5
  timestamp?: bigint;
6
- key: string | null;
7
- value: string | null;
6
+ key?: Buffer | null;
7
+ value: Buffer | null;
8
8
  headers?: Record<string, string>;
9
9
  };
10
10
 
package/src/utils/api.ts CHANGED
@@ -5,7 +5,7 @@ export type Api<Request, Response> = {
5
5
  apiKey: number;
6
6
  apiVersion: number;
7
7
  request: (encoder: Encoder, body: Request) => Encoder;
8
- response: (buffer: Decoder) => Response;
8
+ response: (buffer: Decoder) => Promise<Response> | Response;
9
9
  };
10
10
 
11
11
  export const createApi = <Request, Response>(api: Api<Request, Response>) => api;
@@ -0,0 +1,15 @@
1
+ import { createHash, createHmac, pbkdf2, randomBytes } from 'crypto';
2
+
3
+ export const generateNonce = () => randomBytes(16).toString('base64').replace(/[\/=]/g, '');
4
+
5
+ export const saltPassword = (password: string, salt: string, iterations: number, keyLength: number, digest: string) =>
6
+ new Promise<Buffer>((resolve, reject) =>
7
+ pbkdf2(password, salt, iterations, keyLength, digest, (err, key) => (err ? reject(err) : resolve(key))),
8
+ );
9
+
10
+ export const base64Encode = (input: Buffer | string) => Buffer.from(input).toString('base64');
11
+ export const base64Decode = (input: string) => Buffer.from(input, 'base64').toString();
12
+ export const hash = (data: Buffer, digest: string) => createHash(digest).update(data).digest();
13
+ export const hmac = (key: Buffer, data: Buffer | string, digest: string) =>
14
+ createHmac(digest, key).update(data).digest();
15
+ export const xor = (a: Buffer, b: Buffer) => Buffer.from(a.map((byte, i) => byte ^ b[i]));
@@ -97,13 +97,13 @@ export class Decoder {
97
97
  return value;
98
98
  }
99
99
 
100
- public readVarIntString() {
100
+ public readVarIntBuffer() {
101
101
  const length = this.readVarInt();
102
102
  if (length < 0) {
103
103
  return null;
104
104
  }
105
105
 
106
- const value = this.buffer.toString('utf-8', this.offset, this.offset + length);
106
+ const value = this.buffer.subarray(this.offset, this.offset + length);
107
107
  this.offset += length;
108
108
  return value;
109
109
  }
@@ -132,6 +132,12 @@ export class Decoder {
132
132
  return results;
133
133
  }
134
134
 
135
+ public readVarIntArray<T>(callback: (opts: Decoder) => T): T[] {
136
+ const length = this.readVarInt();
137
+ const results = Array.from({ length }).map(() => callback(this));
138
+ return results;
139
+ }
140
+
135
141
  public readRecords<T>(callback: (opts: Decoder) => T): T[] {
136
142
  const length = this.readInt32();
137
143
 
@@ -143,9 +149,9 @@ export class Decoder {
143
149
  });
144
150
  }
145
151
 
146
- public read(length: number) {
147
- const value = this.buffer.subarray(this.offset, this.offset + length);
148
- this.offset += length;
152
+ public read(length?: number) {
153
+ const value = this.buffer.subarray(this.offset, length !== undefined ? this.offset + length : undefined);
154
+ this.offset += Buffer.byteLength(value);
149
155
  return value;
150
156
  }
151
157
 
@@ -1,41 +1,49 @@
1
1
  export class Encoder {
2
- private buffer: Buffer;
2
+ private chunks: Buffer[] = [];
3
3
 
4
- constructor({ buffer = Buffer.alloc(0) }: { buffer?: Buffer } = {}) {
5
- this.buffer = buffer;
4
+ public getChunks() {
5
+ return this.chunks;
6
6
  }
7
7
 
8
- public write(rightBuffer: Buffer) {
9
- this.buffer = Buffer.concat([this.buffer, rightBuffer]);
8
+ public getByteLength() {
9
+ return this.chunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
10
+ }
11
+
12
+ public write(...buffers: Buffer[]) {
13
+ this.chunks.push(...buffers);
10
14
  return this;
11
15
  }
12
16
 
17
+ public writeEncoder(encoder: Encoder) {
18
+ return this.write(...encoder.getChunks());
19
+ }
20
+
13
21
  public writeInt8(value: number) {
14
- const buffer = Buffer.alloc(1);
22
+ const buffer = Buffer.allocUnsafe(1);
15
23
  buffer.writeInt8(value);
16
24
  return this.write(buffer);
17
25
  }
18
26
 
19
27
  public writeInt16(value: number) {
20
- const buffer = Buffer.alloc(2);
28
+ const buffer = Buffer.allocUnsafe(2);
21
29
  buffer.writeInt16BE(value);
22
30
  return this.write(buffer);
23
31
  }
24
32
 
25
33
  public writeInt32(value: number) {
26
- const buffer = Buffer.alloc(4);
34
+ const buffer = Buffer.allocUnsafe(4);
27
35
  buffer.writeInt32BE(value);
28
36
  return this.write(buffer);
29
37
  }
30
38
 
31
39
  public writeUInt32(value: number) {
32
- const buffer = Buffer.alloc(4);
40
+ const buffer = Buffer.allocUnsafe(4);
33
41
  buffer.writeUInt32BE(value);
34
42
  return this.write(buffer);
35
43
  }
36
44
 
37
45
  public writeInt64(value: bigint) {
38
- const buffer = Buffer.alloc(8);
46
+ const buffer = Buffer.allocUnsafe(8);
39
47
  buffer.writeBigInt64BE(value);
40
48
  return this.write(buffer);
41
49
  }
@@ -75,7 +83,7 @@ export class Encoder {
75
83
  return this.writeInt16(-1);
76
84
  }
77
85
  const byteLength = Buffer.byteLength(value, 'utf-8');
78
- const buffer = Buffer.alloc(byteLength);
86
+ const buffer = Buffer.allocUnsafe(byteLength);
79
87
  buffer.write(value, 0, byteLength, 'utf-8');
80
88
  return this.writeInt16(byteLength).write(buffer);
81
89
  }
@@ -86,16 +94,16 @@ export class Encoder {
86
94
  }
87
95
 
88
96
  const byteLength = Buffer.byteLength(value, 'utf-8');
89
- const buffer = Buffer.alloc(byteLength);
97
+ const buffer = Buffer.allocUnsafe(byteLength);
90
98
  buffer.write(value, 0, byteLength, 'utf-8');
91
99
  return this.writeUVarInt(byteLength + 1).write(buffer);
92
100
  }
93
101
 
94
- public writeVarIntString(value: string | null) {
95
- if (value === null) {
102
+ public writeVarIntBuffer(buffer: Buffer | null) {
103
+ if (buffer === null) {
96
104
  return this.writeVarInt(-1);
97
105
  }
98
- return this.writeVarInt(Buffer.byteLength(value, 'utf-8')).write(Buffer.from(value, 'utf-8'));
106
+ return this.writeVarInt(buffer.byteLength).write(buffer);
99
107
  }
100
108
 
101
109
  public writeUUID(value: string | null) {
@@ -110,21 +118,20 @@ export class Encoder {
110
118
  }
111
119
 
112
120
  public writeArray<T>(arr: T[], callback: (encoder: Encoder, item: T) => Encoder) {
113
- const buffers = arr.map((item) => callback(new Encoder(), item).value());
114
- return this.writeInt32(arr.length).write(Buffer.concat(buffers));
121
+ return this.writeInt32(arr.length).write(...arr.flatMap((item) => callback(new Encoder(), item).getChunks()));
115
122
  }
116
123
 
117
124
  public writeCompactArray<T>(arr: T[] | null, callback: (encoder: Encoder, item: T) => Encoder) {
118
125
  if (arr === null) {
119
126
  return this.writeUVarInt(0);
120
127
  }
121
- const buffers = arr.map((item) => callback(new Encoder(), item).value());
122
- return this.writeUVarInt(buffers.length + 1).write(Buffer.concat(buffers));
128
+ return this.writeUVarInt(arr.length + 1).write(
129
+ ...arr.flatMap((item) => callback(new Encoder(), item).getChunks()),
130
+ );
123
131
  }
124
132
 
125
133
  public writeVarIntArray<T>(arr: T[], callback: (encoder: Encoder, item: T) => Encoder) {
126
- const buffers = arr.map((item) => callback(new Encoder(), item).value());
127
- return this.writeVarInt(buffers.length).write(Buffer.concat(buffers));
134
+ return this.writeVarInt(arr.length).write(...arr.flatMap((item) => callback(new Encoder(), item).getChunks()));
128
135
  }
129
136
 
130
137
  public writeBytes(value: Buffer) {
@@ -136,6 +143,6 @@ export class Encoder {
136
143
  }
137
144
 
138
145
  public value() {
139
- return this.buffer;
146
+ return Buffer.concat(this.chunks);
140
147
  }
141
148
  }
@@ -0,0 +1,37 @@
1
+ export interface Logger {
2
+ debug: (message: string, metadata?: unknown) => void;
3
+ info: (message: string, metadata?: unknown) => void;
4
+ warn: (message: string, metadata?: unknown) => void;
5
+ error: (message: string, metadata?: unknown) => void;
6
+ }
7
+
8
+ export const jsonSerializer = (_: unknown, v: unknown) => {
9
+ if (v instanceof Error) {
10
+ return { name: v.name, message: v.message, stack: v.stack, cause: v.cause };
11
+ }
12
+ if (typeof v === 'bigint') {
13
+ return v.toString();
14
+ }
15
+ return v;
16
+ };
17
+
18
+ class JsonLogger implements Logger {
19
+ debug(message: string, metadata?: unknown) {
20
+ console.debug(JSON.stringify({ message, metadata, level: 'debug' }, jsonSerializer));
21
+ }
22
+ info(message: string, metadata?: unknown) {
23
+ console.info(JSON.stringify({ message, metadata, level: 'info' }, jsonSerializer));
24
+ }
25
+ warn(message: string, metadata?: unknown) {
26
+ console.warn(JSON.stringify({ message, metadata, level: 'warning' }, jsonSerializer));
27
+ }
28
+ error(message: string, metadata?: unknown) {
29
+ console.error(JSON.stringify({ message, metadata, level: 'error' }, jsonSerializer));
30
+ }
31
+ }
32
+
33
+ export let log: Logger = new JsonLogger();
34
+
35
+ export const setLogger = (newLogger: Logger) => {
36
+ log = newLogger;
37
+ };
@@ -0,0 +1,44 @@
1
+ /* https://github.com/apache/kafka/blob/0.10.2/clients/src/main/java/org/apache/kafka/common/utils/Utils.java#L364 */
2
+
3
+ export const murmur2 = (data: Buffer): number => {
4
+ const length = data.length;
5
+ const seed = 0x9747b28c;
6
+
7
+ const m = 0x5bd1e995;
8
+ const r = 24;
9
+
10
+ let h = seed ^ length;
11
+ let length4 = Math.floor(length / 4);
12
+
13
+ for (let i = 0; i < length4; i++) {
14
+ const i4 = i * 4;
15
+ let k =
16
+ (data[i4 + 0] & 0xff) +
17
+ ((data[i4 + 1] & 0xff) << 8) +
18
+ ((data[i4 + 2] & 0xff) << 16) +
19
+ ((data[i4 + 3] & 0xff) << 24);
20
+ k *= m;
21
+ k ^= k >> r;
22
+ k *= m;
23
+ h *= m;
24
+ h ^= k;
25
+ }
26
+
27
+ switch (length % 4) {
28
+ case 3:
29
+ h = h ^ ((data[(length & ~3) + 2] & 0xff) << 16);
30
+ case 2:
31
+ h = h ^ ((data[(length & ~3) + 1] & 0xff) << 8);
32
+ case 1:
33
+ h = h ^ (data[length & ~3] & 0xff);
34
+ h *= m;
35
+ }
36
+
37
+ h ^= h >> 13;
38
+ h *= m;
39
+ h ^= h >> 15;
40
+
41
+ return h;
42
+ };
43
+
44
+ export const toPositive = (input: number) => input & 0x7fffffff;
@@ -1,31 +1,49 @@
1
- import { serializer } from './debug';
1
+ import { log } from './logger';
2
+
3
+ export interface Tracer {
4
+ startActiveSpan<T>(module: string, method: string, metadata: Record<string, unknown>, callback: () => T): T;
5
+ }
6
+
7
+ class DebugTracer implements Tracer {
8
+ private isEnabled = process.env.DEBUG?.includes('kafka-ts');
9
+
10
+ startActiveSpan<T>(module: string, method: string, metadata: Record<string, unknown>, callback: () => T): T {
11
+ if (!this.isEnabled) {
12
+ return callback();
13
+ }
14
+
15
+ const startTime = Date.now();
16
+
17
+ const onEnd = <T>(result: T): T => {
18
+ log.debug(`[${module}.${method}] ${metadata?.message ?? ''} +${Date.now() - startTime}ms`, {
19
+ ...metadata,
20
+ ...(!!result && { result }),
21
+ });
22
+ return result;
23
+ };
24
+
25
+ const result = callback();
26
+ if (result instanceof Promise) {
27
+ return result.then(onEnd) as T;
28
+ }
29
+ onEnd(result);
30
+ return result;
31
+ }
32
+ }
33
+
34
+ let tracer: Tracer = new DebugTracer();
35
+
36
+ export const setTracer = <T>(newTracer: Tracer) => {
37
+ tracer = newTracer;
38
+ };
2
39
 
3
40
  export const createTracer =
4
- (module: string, attributes?: Record<string, unknown>) =>
41
+ (module: string) =>
5
42
  (fn?: (...args: any[]) => Record<string, unknown> | undefined) =>
6
43
  (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
7
- if (!process.env.DEBUG?.includes('kafka-ts')) return;
8
-
9
44
  const original = descriptor.value;
10
45
  descriptor.value = function (...args: any[]) {
11
- const startTime = Date.now();
12
46
  const metadata = fn?.(...args);
13
-
14
- const onEnd = <T>(result: T): T => {
15
- console.log(
16
- `[${module}.${propertyKey}] +${Date.now() - startTime}ms ${JSON.stringify({ ...attributes, ...metadata, result }, serializer)}`,
17
- );
18
- return result;
19
- };
20
-
21
- const result = original.apply(this, args);
22
- if (result instanceof Promise) {
23
- return result.then(onEnd);
24
- } else {
25
- onEnd(result);
26
- return result;
27
- }
47
+ return tracer.startActiveSpan(module, propertyKey, { ...metadata }, () => original.apply(this, args));
28
48
  };
29
49
  };
30
-
31
- export const trace = createTracer('GLOBAL');
@@ -1,17 +0,0 @@
1
- name: Publish package
2
- on:
3
- release:
4
- types: [published]
5
- jobs:
6
- build:
7
- runs-on: ubuntu-latest
8
- steps:
9
- - uses: actions/checkout@v4
10
- - uses: actions/setup-node@v4
11
- with:
12
- node-version: '20.x'
13
- registry-url: 'https://registry.npmjs.org'
14
- - run: npm ci
15
- - run: npm publish
16
- env:
17
- NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}