@platformatic/kafka 0.1.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.
Files changed (223) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +270 -0
  3. package/dist/apis/admin/alter-client-quotas.d.ts +33 -0
  4. package/dist/apis/admin/alter-client-quotas.js +64 -0
  5. package/dist/apis/admin/alter-configs.d.ts +26 -0
  6. package/dist/apis/admin/alter-configs.js +57 -0
  7. package/dist/apis/admin/alter-partition-reassignments.d.ts +30 -0
  8. package/dist/apis/admin/alter-partition-reassignments.js +68 -0
  9. package/dist/apis/admin/alter-partition.d.ts +39 -0
  10. package/dist/apis/admin/alter-partition.js +87 -0
  11. package/dist/apis/admin/alter-replica-log-dirs.d.ts +26 -0
  12. package/dist/apis/admin/alter-replica-log-dirs.js +55 -0
  13. package/dist/apis/admin/alter-user-scram-credentials.d.ts +27 -0
  14. package/dist/apis/admin/alter-user-scram-credentials.js +60 -0
  15. package/dist/apis/admin/consumer-group-describe.d.ts +41 -0
  16. package/dist/apis/admin/consumer-group-describe.js +103 -0
  17. package/dist/apis/admin/create-acls.d.ts +24 -0
  18. package/dist/apis/admin/create-acls.js +55 -0
  19. package/dist/apis/admin/create-delegation-token.d.ts +24 -0
  20. package/dist/apis/admin/create-delegation-token.js +54 -0
  21. package/dist/apis/admin/create-partitions.d.ts +24 -0
  22. package/dist/apis/admin/create-partitions.js +54 -0
  23. package/dist/apis/admin/create-topics.d.ts +42 -0
  24. package/dist/apis/admin/create-topics.js +86 -0
  25. package/dist/apis/admin/delete-acls.d.ts +36 -0
  26. package/dist/apis/admin/delete-acls.js +82 -0
  27. package/dist/apis/admin/delete-groups.d.ts +14 -0
  28. package/dist/apis/admin/delete-groups.js +40 -0
  29. package/dist/apis/admin/delete-records.d.ts +27 -0
  30. package/dist/apis/admin/delete-records.js +59 -0
  31. package/dist/apis/admin/delete-topics.d.ts +21 -0
  32. package/dist/apis/admin/delete-topics.js +50 -0
  33. package/dist/apis/admin/describe-acls.d.ts +25 -0
  34. package/dist/apis/admin/describe-acls.js +66 -0
  35. package/dist/apis/admin/describe-client-quotas.d.ts +30 -0
  36. package/dist/apis/admin/describe-client-quotas.js +57 -0
  37. package/dist/apis/admin/describe-cluster.d.ts +23 -0
  38. package/dist/apis/admin/describe-cluster.js +50 -0
  39. package/dist/apis/admin/describe-configs.d.ts +38 -0
  40. package/dist/apis/admin/describe-configs.js +85 -0
  41. package/dist/apis/admin/describe-delegation-token.d.ts +31 -0
  42. package/dist/apis/admin/describe-delegation-token.js +62 -0
  43. package/dist/apis/admin/describe-groups.d.ts +28 -0
  44. package/dist/apis/admin/describe-groups.js +67 -0
  45. package/dist/apis/admin/describe-log-dirs.d.ts +32 -0
  46. package/dist/apis/admin/describe-log-dirs.js +75 -0
  47. package/dist/apis/admin/describe-producers.d.ts +33 -0
  48. package/dist/apis/admin/describe-producers.js +70 -0
  49. package/dist/apis/admin/describe-quorum.d.ts +50 -0
  50. package/dist/apis/admin/describe-quorum.js +116 -0
  51. package/dist/apis/admin/describe-topic-partitions.d.ts +42 -0
  52. package/dist/apis/admin/describe-topic-partitions.js +94 -0
  53. package/dist/apis/admin/describe-transactions.d.ts +24 -0
  54. package/dist/apis/admin/describe-transactions.js +59 -0
  55. package/dist/apis/admin/describe-user-scram-credentials.d.ts +26 -0
  56. package/dist/apis/admin/describe-user-scram-credentials.js +62 -0
  57. package/dist/apis/admin/envelope.d.ts +10 -0
  58. package/dist/apis/admin/envelope.js +32 -0
  59. package/dist/apis/admin/expire-delegation-token.d.ts +11 -0
  60. package/dist/apis/admin/expire-delegation-token.js +29 -0
  61. package/dist/apis/admin/incremental-alter-configs.d.ts +27 -0
  62. package/dist/apis/admin/incremental-alter-configs.js +58 -0
  63. package/dist/apis/admin/index.d.ts +37 -0
  64. package/dist/apis/admin/index.js +37 -0
  65. package/dist/apis/admin/list-groups.d.ts +18 -0
  66. package/dist/apis/admin/list-groups.js +43 -0
  67. package/dist/apis/admin/list-partition-reassignments.d.ts +27 -0
  68. package/dist/apis/admin/list-partition-reassignments.js +56 -0
  69. package/dist/apis/admin/list-transactions.d.ts +18 -0
  70. package/dist/apis/admin/list-transactions.js +45 -0
  71. package/dist/apis/admin/offset-delete.d.ts +26 -0
  72. package/dist/apis/admin/offset-delete.js +59 -0
  73. package/dist/apis/admin/renew-delegation-token.d.ts +11 -0
  74. package/dist/apis/admin/renew-delegation-token.js +29 -0
  75. package/dist/apis/admin/unregister-broker.d.ts +12 -0
  76. package/dist/apis/admin/unregister-broker.js +28 -0
  77. package/dist/apis/admin/update-features.d.ts +23 -0
  78. package/dist/apis/admin/update-features.js +60 -0
  79. package/dist/apis/consumer/consumer-group-heartbeat.d.ts +27 -0
  80. package/dist/apis/consumer/consumer-group-heartbeat.js +70 -0
  81. package/dist/apis/consumer/fetch.d.ts +46 -0
  82. package/dist/apis/consumer/fetch.js +121 -0
  83. package/dist/apis/consumer/heartbeat.d.ts +11 -0
  84. package/dist/apis/consumer/heartbeat.js +34 -0
  85. package/dist/apis/consumer/index.d.ts +9 -0
  86. package/dist/apis/consumer/index.js +9 -0
  87. package/dist/apis/consumer/join-group.d.ts +27 -0
  88. package/dist/apis/consumer/join-group.js +71 -0
  89. package/dist/apis/consumer/leave-group.d.ts +22 -0
  90. package/dist/apis/consumer/leave-group.js +57 -0
  91. package/dist/apis/consumer/list-offsets.d.ts +30 -0
  92. package/dist/apis/consumer/list-offsets.js +68 -0
  93. package/dist/apis/consumer/offset-commit.d.ts +29 -0
  94. package/dist/apis/consumer/offset-commit.js +68 -0
  95. package/dist/apis/consumer/offset-fetch.d.ts +37 -0
  96. package/dist/apis/consumer/offset-fetch.js +81 -0
  97. package/dist/apis/consumer/sync-group.d.ts +18 -0
  98. package/dist/apis/consumer/sync-group.js +49 -0
  99. package/dist/apis/definitions.d.ts +16 -0
  100. package/dist/apis/definitions.js +12 -0
  101. package/dist/apis/enumerations.d.ts +114 -0
  102. package/dist/apis/enumerations.js +78 -0
  103. package/dist/apis/index.d.ts +8 -0
  104. package/dist/apis/index.js +10 -0
  105. package/dist/apis/metadata/api-versions.d.ts +17 -0
  106. package/dist/apis/metadata/api-versions.js +41 -0
  107. package/dist/apis/metadata/find-coordinator.d.ts +19 -0
  108. package/dist/apis/metadata/find-coordinator.js +50 -0
  109. package/dist/apis/metadata/index.d.ts +3 -0
  110. package/dist/apis/metadata/index.js +3 -0
  111. package/dist/apis/metadata/metadata.d.ts +37 -0
  112. package/dist/apis/metadata/metadata.js +92 -0
  113. package/dist/apis/producer/add-offsets-to-txn.d.ts +10 -0
  114. package/dist/apis/producer/add-offsets-to-txn.js +34 -0
  115. package/dist/apis/producer/add-partitions-to-txn.d.ts +34 -0
  116. package/dist/apis/producer/add-partitions-to-txn.js +79 -0
  117. package/dist/apis/producer/end-txn.d.ts +10 -0
  118. package/dist/apis/producer/end-txn.js +34 -0
  119. package/dist/apis/producer/index.d.ts +6 -0
  120. package/dist/apis/producer/index.js +6 -0
  121. package/dist/apis/producer/init-producer-id.d.ts +21 -0
  122. package/dist/apis/producer/init-producer-id.js +38 -0
  123. package/dist/apis/producer/produce.d.ts +29 -0
  124. package/dist/apis/producer/produce.js +104 -0
  125. package/dist/apis/producer/txn-offset-commit.d.ts +29 -0
  126. package/dist/apis/producer/txn-offset-commit.js +77 -0
  127. package/dist/apis/security/index.d.ts +2 -0
  128. package/dist/apis/security/index.js +2 -0
  129. package/dist/apis/security/sasl-authenticate.d.ts +15 -0
  130. package/dist/apis/security/sasl-authenticate.js +30 -0
  131. package/dist/apis/security/sasl-handshake.d.ts +10 -0
  132. package/dist/apis/security/sasl-handshake.js +28 -0
  133. package/dist/apis/telemetry/get-telemetry-subscriptions.d.ts +18 -0
  134. package/dist/apis/telemetry/get-telemetry-subscriptions.js +46 -0
  135. package/dist/apis/telemetry/index.d.ts +3 -0
  136. package/dist/apis/telemetry/index.js +3 -0
  137. package/dist/apis/telemetry/list-client-metrics-resources.d.ts +14 -0
  138. package/dist/apis/telemetry/list-client-metrics-resources.js +32 -0
  139. package/dist/apis/telemetry/push-telemetry.d.ts +10 -0
  140. package/dist/apis/telemetry/push-telemetry.js +36 -0
  141. package/dist/clients/admin/admin.d.ts +18 -0
  142. package/dist/clients/admin/admin.js +322 -0
  143. package/dist/clients/admin/index.d.ts +3 -0
  144. package/dist/clients/admin/index.js +3 -0
  145. package/dist/clients/admin/options.d.ts +135 -0
  146. package/dist/clients/admin/options.js +81 -0
  147. package/dist/clients/admin/types.d.ts +56 -0
  148. package/dist/clients/admin/types.js +1 -0
  149. package/dist/clients/base/base.d.ts +48 -0
  150. package/dist/clients/base/base.js +242 -0
  151. package/dist/clients/base/index.d.ts +3 -0
  152. package/dist/clients/base/index.js +3 -0
  153. package/dist/clients/base/options.d.ts +115 -0
  154. package/dist/clients/base/options.js +59 -0
  155. package/dist/clients/base/types.d.ts +38 -0
  156. package/dist/clients/base/types.js +1 -0
  157. package/dist/clients/callbacks.d.ts +8 -0
  158. package/dist/clients/callbacks.js +42 -0
  159. package/dist/clients/consumer/consumer.d.ts +33 -0
  160. package/dist/clients/consumer/consumer.js +767 -0
  161. package/dist/clients/consumer/index.d.ts +5 -0
  162. package/dist/clients/consumer/index.js +5 -0
  163. package/dist/clients/consumer/messages-stream.d.ts +56 -0
  164. package/dist/clients/consumer/messages-stream.js +404 -0
  165. package/dist/clients/consumer/options.d.ts +521 -0
  166. package/dist/clients/consumer/options.js +177 -0
  167. package/dist/clients/consumer/topics-map.d.ts +8 -0
  168. package/dist/clients/consumer/topics-map.js +48 -0
  169. package/dist/clients/consumer/types.d.ts +74 -0
  170. package/dist/clients/consumer/types.js +11 -0
  171. package/dist/clients/index.d.ts +6 -0
  172. package/dist/clients/index.js +6 -0
  173. package/dist/clients/producer/index.d.ts +3 -0
  174. package/dist/clients/producer/index.js +3 -0
  175. package/dist/clients/producer/options.d.ts +122 -0
  176. package/dist/clients/producer/options.js +47 -0
  177. package/dist/clients/producer/producer.d.ts +13 -0
  178. package/dist/clients/producer/producer.js +272 -0
  179. package/dist/clients/producer/types.d.ts +29 -0
  180. package/dist/clients/producer/types.js +1 -0
  181. package/dist/clients/serde.d.ts +40 -0
  182. package/dist/clients/serde.js +49 -0
  183. package/dist/errors.d.ts +67 -0
  184. package/dist/errors.js +160 -0
  185. package/dist/index.d.ts +6 -0
  186. package/dist/index.js +10 -0
  187. package/dist/network/connection-pool.d.ts +11 -0
  188. package/dist/network/connection-pool.js +101 -0
  189. package/dist/network/connection.d.ts +49 -0
  190. package/dist/network/connection.js +319 -0
  191. package/dist/network/index.d.ts +2 -0
  192. package/dist/network/index.js +2 -0
  193. package/dist/protocol/apis.d.ts +2 -0
  194. package/dist/protocol/apis.js +191 -0
  195. package/dist/protocol/compression.d.ts +70 -0
  196. package/dist/protocol/compression.js +114 -0
  197. package/dist/protocol/crc32c.d.ts +2 -0
  198. package/dist/protocol/crc32c.js +83 -0
  199. package/dist/protocol/definitions.d.ts +12 -0
  200. package/dist/protocol/definitions.js +11 -0
  201. package/dist/protocol/dynamic-buffer.d.ts +65 -0
  202. package/dist/protocol/dynamic-buffer.js +557 -0
  203. package/dist/protocol/errors.d.ts +8 -0
  204. package/dist/protocol/errors.js +908 -0
  205. package/dist/protocol/index.d.ts +14 -0
  206. package/dist/protocol/index.js +14 -0
  207. package/dist/protocol/murmur2.d.ts +1 -0
  208. package/dist/protocol/murmur2.js +55 -0
  209. package/dist/protocol/reader.d.ts +58 -0
  210. package/dist/protocol/reader.js +296 -0
  211. package/dist/protocol/records.d.ts +110 -0
  212. package/dist/protocol/records.js +149 -0
  213. package/dist/protocol/sasl/plain.d.ts +3 -0
  214. package/dist/protocol/sasl/plain.js +3 -0
  215. package/dist/protocol/sasl/scram-sha.d.ts +28 -0
  216. package/dist/protocol/sasl/scram-sha.js +104 -0
  217. package/dist/protocol/varint.d.ts +12 -0
  218. package/dist/protocol/varint.js +36 -0
  219. package/dist/protocol/writer.d.ts +48 -0
  220. package/dist/protocol/writer.js +223 -0
  221. package/dist/utils.d.ts +20 -0
  222. package/dist/utils.js +127 -0
  223. package/package.json +75 -0
@@ -0,0 +1,767 @@
1
+ import { api as fetchV17 } from "../../apis/consumer/fetch.js";
2
+ import { api as heartbeatV4 } from "../../apis/consumer/heartbeat.js";
3
+ import { api as joinGroupV9 } from "../../apis/consumer/join-group.js";
4
+ import { api as leaveGroupV5 } from "../../apis/consumer/leave-group.js";
5
+ import { api as listOffsetsV9 } from "../../apis/consumer/list-offsets.js";
6
+ import { api as offsetCommitV9 } from "../../apis/consumer/offset-commit.js";
7
+ import { api as offsetFetchV9 } from "../../apis/consumer/offset-fetch.js";
8
+ import { api as syncGroupV5 } from "../../apis/consumer/sync-group.js";
9
+ import { FetchIsolationLevels, FindCoordinatorKeyTypes } from "../../apis/enumerations.js";
10
+ import { api as findCoordinatorV6 } from "../../apis/metadata/find-coordinator.js";
11
+ import { UserError } from "../../errors.js";
12
+ import { Reader } from "../../protocol/reader.js";
13
+ import { Writer } from "../../protocol/writer.js";
14
+ import { Base, kBootstrapBrokers, kCheckNotClosed, kClosed, kConnections, kCreateConnectionPool, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from "../base/base.js";
15
+ import { defaultBaseOptions } from "../base/options.js";
16
+ import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../callbacks.js";
17
+ import { MessagesStream } from "./messages-stream.js";
18
+ import { commitOptionsValidator, consumeOptionsValidator, consumerOptionsValidator, defaultConsumerOptions, fetchOptionsValidator, groupOptionsValidator, listCommitsOptionsValidator, listOffsetsOptionsValidator } from "./options.js";
19
+ import { TopicsMap } from "./topics-map.js";
20
+ export class Consumer extends Base {
21
+ groupId;
22
+ generationId;
23
+ memberId;
24
+ topics;
25
+ assignments;
26
+ #members;
27
+ #membershipActive;
28
+ #isLeader;
29
+ #protocol;
30
+ #coordinatorId;
31
+ #heartbeatInterval;
32
+ #streams;
33
+ /*
34
+ The following requests are blocking in Kafka:
35
+
36
+ FetchRequest (soprattutto con maxWaitMs)
37
+ JoinGroupRequest
38
+ SyncGroupRequest
39
+ OffsetCommitRequest
40
+ ProduceRequest
41
+ ListOffsetsRequest
42
+ ListGroupsRequest
43
+ DescribeGroupsRequest
44
+
45
+ In order to avoid consumer group problems, we separate FetchRequest only on a separate connection.
46
+ */
47
+ #fetchConnectionPool;
48
+ constructor(options) {
49
+ super(options);
50
+ this[kOptions] = Object.assign({}, defaultBaseOptions, defaultConsumerOptions, options);
51
+ this[kValidateOptions](options, consumerOptionsValidator, '/options');
52
+ this.groupId = options.groupId;
53
+ this.generationId = 0;
54
+ this.memberId = null;
55
+ this.topics = new TopicsMap();
56
+ this.assignments = null;
57
+ this.#members = new Map();
58
+ this.#membershipActive = false;
59
+ this.#isLeader = false;
60
+ this.#protocol = null;
61
+ this.#coordinatorId = null;
62
+ this.#heartbeatInterval = null;
63
+ this.#streams = new Set();
64
+ this.#validateGroupOptions(this[kOptions]);
65
+ // Initialize connection pool
66
+ this.#fetchConnectionPool = this[kCreateConnectionPool]();
67
+ }
68
+ close(force, callback) {
69
+ if (typeof force === 'function') {
70
+ callback = force;
71
+ force = false;
72
+ }
73
+ if (!callback) {
74
+ callback = createPromisifiedCallback();
75
+ }
76
+ if (this[kClosed]) {
77
+ callback(null);
78
+ return callback[kCallbackPromise];
79
+ }
80
+ this[kClosed] = true;
81
+ const closer = this.#membershipActive
82
+ ? this.#leaveGroup.bind(this)
83
+ : function (_, callback) {
84
+ callback(null);
85
+ };
86
+ closer(force, error => {
87
+ if (error) {
88
+ callback(error);
89
+ return;
90
+ }
91
+ this.#fetchConnectionPool.close(error => {
92
+ if (error) {
93
+ callback(error);
94
+ return;
95
+ }
96
+ super.close(callback);
97
+ });
98
+ });
99
+ return callback[kCallbackPromise];
100
+ }
101
+ consume(options, callback) {
102
+ if (!callback) {
103
+ callback = createPromisifiedCallback();
104
+ }
105
+ if (this[kCheckNotClosed](callback)) {
106
+ return callback[kCallbackPromise];
107
+ }
108
+ const validationError = this[kValidateOptions](options, consumeOptionsValidator, '/options', false);
109
+ if (validationError) {
110
+ callback(validationError, undefined);
111
+ return callback[kCallbackPromise];
112
+ }
113
+ options.autocommit ??= this[kOptions].autocommit ?? true;
114
+ options.maxBytes ??= this[kOptions].maxBytes;
115
+ options.deserializers = Object.assign({}, options.deserializers, this[kOptions].deserializers);
116
+ options.highWaterMark ??= this[kOptions].highWaterMark;
117
+ this.#consume(options, callback);
118
+ return callback[kCallbackPromise];
119
+ }
120
+ fetch(options, callback) {
121
+ if (!callback) {
122
+ callback = createPromisifiedCallback();
123
+ }
124
+ if (this[kCheckNotClosed](callback)) {
125
+ return callback[kCallbackPromise];
126
+ }
127
+ const validationError = this[kValidateOptions](options, fetchOptionsValidator, '/options', false);
128
+ if (validationError) {
129
+ callback(validationError, undefined);
130
+ return callback[kCallbackPromise];
131
+ }
132
+ this.#fetch(options, callback);
133
+ return callback[kCallbackPromise];
134
+ }
135
+ commit(options, callback) {
136
+ if (!callback) {
137
+ callback = createPromisifiedCallback();
138
+ }
139
+ if (this[kCheckNotClosed](callback)) {
140
+ return callback[kCallbackPromise];
141
+ }
142
+ const validationError = this[kValidateOptions](options, commitOptionsValidator, '/options', false);
143
+ if (validationError) {
144
+ callback(validationError);
145
+ return callback[kCallbackPromise];
146
+ }
147
+ this.#commit(options, callback);
148
+ return callback[kCallbackPromise];
149
+ }
150
+ listOffsets(options, callback) {
151
+ if (!callback) {
152
+ callback = createPromisifiedCallback();
153
+ }
154
+ if (this[kCheckNotClosed](callback)) {
155
+ return callback[kCallbackPromise];
156
+ }
157
+ const validationError = this[kValidateOptions](options, listOffsetsOptionsValidator, '/options', false);
158
+ if (validationError) {
159
+ callback(validationError, undefined);
160
+ return callback[kCallbackPromise];
161
+ }
162
+ this.#listOffsets(options, callback);
163
+ return callback[kCallbackPromise];
164
+ }
165
+ listCommittedOffsets(options, callback) {
166
+ if (!callback) {
167
+ callback = createPromisifiedCallback();
168
+ }
169
+ if (this[kCheckNotClosed](callback)) {
170
+ return callback[kCallbackPromise];
171
+ }
172
+ const validationError = this[kValidateOptions](options, listCommitsOptionsValidator, '/options', false);
173
+ if (validationError) {
174
+ callback(validationError, undefined);
175
+ return callback[kCallbackPromise];
176
+ }
177
+ this.#listCommittedOffsets(options, callback);
178
+ return callback[kCallbackPromise];
179
+ }
180
+ findGroupCoordinator(callback) {
181
+ if (!callback) {
182
+ callback = createPromisifiedCallback();
183
+ }
184
+ if (this[kCheckNotClosed](callback)) {
185
+ return callback[kCallbackPromise];
186
+ }
187
+ if (this.#coordinatorId) {
188
+ callback(null, this.#coordinatorId);
189
+ return callback[kCallbackPromise];
190
+ }
191
+ this.#findGroupCoordinator(callback);
192
+ return callback[kCallbackPromise];
193
+ }
194
+ joinGroup(options, callback) {
195
+ if (!callback) {
196
+ callback = createPromisifiedCallback();
197
+ }
198
+ if (this[kCheckNotClosed](callback)) {
199
+ return callback[kCallbackPromise];
200
+ }
201
+ const validationError = this[kValidateOptions](options, groupOptionsValidator, '/options', false);
202
+ if (validationError) {
203
+ callback(validationError, undefined);
204
+ return callback[kCallbackPromise];
205
+ }
206
+ options.sessionTimeout ??= this[kOptions].sessionTimeout;
207
+ options.rebalanceTimeout ??= this[kOptions].rebalanceTimeout;
208
+ options.heartbeatInterval ??= this[kOptions].heartbeatInterval;
209
+ options.protocols ??= this[kOptions].protocols;
210
+ this.#validateGroupOptions(options);
211
+ this.#membershipActive = true;
212
+ this.#joinGroup(options, callback);
213
+ return callback[kCallbackPromise];
214
+ }
215
+ leaveGroup(force, callback) {
216
+ if (typeof force === 'function') {
217
+ callback = force;
218
+ force = false;
219
+ }
220
+ if (!callback) {
221
+ callback = createPromisifiedCallback();
222
+ }
223
+ if (this[kCheckNotClosed](callback)) {
224
+ return callback[kCallbackPromise];
225
+ }
226
+ this.#membershipActive = false;
227
+ this.#leaveGroup(force, callback);
228
+ return callback[kCallbackPromise];
229
+ }
230
+ #consume(options, callback) {
231
+ // Subscribe all topics
232
+ let joinNeeded = this.memberId === null;
233
+ for (const topic of options.topics) {
234
+ if (this.topics.track(topic)) {
235
+ joinNeeded = true;
236
+ }
237
+ }
238
+ // If we need to (re)join the group, do that first and then try again
239
+ if (joinNeeded) {
240
+ this.joinGroup(options, error => {
241
+ if (error) {
242
+ callback(error, undefined);
243
+ return;
244
+ }
245
+ this.#consume(options, callback);
246
+ });
247
+ return callback[kCallbackPromise];
248
+ }
249
+ // Create the stream and start consuming
250
+ const stream = new MessagesStream(this, options);
251
+ this.#streams.add(stream);
252
+ stream.once('close', () => {
253
+ this.#streams.delete(stream);
254
+ });
255
+ callback(null, stream);
256
+ return callback[kCallbackPromise];
257
+ }
258
+ #fetch(options, callback) {
259
+ this[kPerformWithRetry]('fetch', retryCallback => {
260
+ this[kMetadata]({ topics: this.topics.current }, (error, metadata) => {
261
+ if (error) {
262
+ retryCallback(error, undefined);
263
+ return;
264
+ }
265
+ const broker = metadata.brokers.get(options.node);
266
+ if (!broker) {
267
+ retryCallback(new UserError(`Cannot find broker with node id ${options.node}`), undefined);
268
+ return;
269
+ }
270
+ this.#fetchConnectionPool.get(broker, (error, connection) => {
271
+ if (error) {
272
+ retryCallback(error, undefined);
273
+ return;
274
+ }
275
+ fetchV17(connection, options.maxWaitTime ?? this[kOptions].maxWaitTime, options.minBytes ?? this[kOptions].minBytes, options.maxBytes ?? this[kOptions].maxBytes, FetchIsolationLevels[options.isolationLevel ?? this[kOptions].isolationLevel], 0, 0, options.topics, [], '', retryCallback);
276
+ });
277
+ });
278
+ }, callback, 0);
279
+ }
280
+ #commit(options, callback) {
281
+ this.#performGroupOperation('commit', (connection, groupCallback) => {
282
+ const topics = new Map();
283
+ for (const { topic, partition, offset, leaderEpoch } of options.offsets) {
284
+ let topicOffsets = topics.get(topic);
285
+ if (!topicOffsets) {
286
+ topicOffsets = { name: topic, partitions: [] };
287
+ topics.set(topic, topicOffsets);
288
+ }
289
+ topicOffsets.partitions.push({
290
+ partitionIndex: partition,
291
+ committedOffset: offset,
292
+ committedLeaderEpoch: leaderEpoch,
293
+ committedMetadata: null
294
+ });
295
+ }
296
+ offsetCommitV9(connection, this.groupId, this.generationId, this.memberId, null, Array.from(topics.values()), groupCallback);
297
+ }, error => {
298
+ callback(error);
299
+ });
300
+ }
301
+ #listOffsets(options, callback) {
302
+ this[kMetadata]({ topics: options.topics }, (error, metadata) => {
303
+ if (error) {
304
+ callback(error, undefined);
305
+ return;
306
+ }
307
+ const requests = new Map();
308
+ for (const name of options.topics) {
309
+ const topic = metadata.topics.get(name);
310
+ for (let i = 0; i < topic.partitionsCount; i++) {
311
+ const partition = topic.partitions[i];
312
+ const { leader, leaderEpoch } = partition;
313
+ let leaderRequests = requests.get(leader);
314
+ if (!leaderRequests) {
315
+ leaderRequests = new Map();
316
+ requests.set(leader, leaderRequests);
317
+ }
318
+ let topicRequests = leaderRequests.get(name);
319
+ if (!topicRequests) {
320
+ topicRequests = { name, partitions: [] };
321
+ leaderRequests.set(name, topicRequests);
322
+ }
323
+ topicRequests.partitions.push({
324
+ partitionIndex: i,
325
+ currentLeaderEpoch: leaderEpoch,
326
+ timestamp: options.timestamp ?? -1n
327
+ });
328
+ }
329
+ }
330
+ runConcurrentCallbacks('Listing offsets failed.', requests, ([leader, requests], concurrentCallback) => {
331
+ this[kPerformWithRetry]('listOffsets', retryCallback => {
332
+ this[kConnections].get(metadata.brokers.get(leader), (error, connection) => {
333
+ if (error) {
334
+ retryCallback(error, undefined);
335
+ return;
336
+ }
337
+ listOffsetsV9(connection, -1, FetchIsolationLevels[options.isolationLevel ?? this[kOptions].isolationLevel], Array.from(requests.values()), retryCallback);
338
+ });
339
+ }, concurrentCallback, 0);
340
+ }, (error, responses) => {
341
+ if (error) {
342
+ callback(error, undefined);
343
+ return;
344
+ }
345
+ const offsets = new Map();
346
+ for (const response of responses) {
347
+ for (const { name: topic, partitions } of response.topics) {
348
+ let topicOffsets = offsets.get(topic);
349
+ if (!topicOffsets) {
350
+ topicOffsets = Array(metadata.topics.get(topic).partitionsCount);
351
+ offsets.set(topic, topicOffsets);
352
+ }
353
+ for (const { partitionIndex: index, offset } of partitions) {
354
+ topicOffsets[index] = offset;
355
+ }
356
+ }
357
+ }
358
+ callback(null, offsets);
359
+ });
360
+ });
361
+ }
362
+ #listCommittedOffsets(options, callback) {
363
+ const topics = [];
364
+ for (const { topic: name, partitions } of options.topics) {
365
+ topics.push({ name, partitionIndexes: partitions });
366
+ }
367
+ this.#performGroupOperation('listCommits', (connection, groupCallback) => {
368
+ offsetFetchV9(connection,
369
+ // Note: once we start implementing KIP-848, the memberEpoch must be obtained
370
+ [{ groupId: this.groupId, memberId: this.memberId, memberEpoch: -1, topics }], false, groupCallback);
371
+ }, (error, response) => {
372
+ if (error) {
373
+ callback(error, undefined);
374
+ return;
375
+ }
376
+ const committed = new Map();
377
+ for (const responseGroup of response.groups) {
378
+ for (const responseTopic of responseGroup.topics) {
379
+ const topic = responseTopic.name;
380
+ const partitions = Array(responseTopic.partitions.length);
381
+ for (const { partitionIndex: index, committedOffset } of responseTopic.partitions) {
382
+ partitions[index] = committedOffset;
383
+ }
384
+ committed.set(topic, partitions);
385
+ }
386
+ }
387
+ callback(null, committed);
388
+ });
389
+ }
390
+ #findGroupCoordinator(callback) {
391
+ if (this.#coordinatorId) {
392
+ callback(null, this.#coordinatorId);
393
+ return;
394
+ }
395
+ this[kPerformDeduplicated]('findGroupCoordinator', deduplicateCallback => {
396
+ this[kPerformWithRetry]('findGroupCoordinator', retryCallback => {
397
+ this[kConnections].getFirstAvailable(this[kBootstrapBrokers], (error, connection) => {
398
+ if (error) {
399
+ retryCallback(error, undefined);
400
+ return;
401
+ }
402
+ findCoordinatorV6(connection, FindCoordinatorKeyTypes.GROUP, [this.groupId], retryCallback);
403
+ });
404
+ }, (error, response) => {
405
+ if (error) {
406
+ deduplicateCallback(error, undefined);
407
+ return;
408
+ }
409
+ const groupInfo = response.coordinators.find(coordinator => coordinator.key === this.groupId);
410
+ this.#coordinatorId = groupInfo.nodeId;
411
+ deduplicateCallback(null, this.#coordinatorId);
412
+ }, 0);
413
+ }, callback);
414
+ }
415
+ #joinGroup(options, callback) {
416
+ if (!this.#membershipActive) {
417
+ callback(null, undefined);
418
+ return;
419
+ }
420
+ this.#cancelHeartbeat();
421
+ const protocols = [];
422
+ for (const protocol of options.protocols) {
423
+ protocols.push({
424
+ name: protocol.name,
425
+ metadata: this.#encodeProtocolSubscriptionMetadata(protocol, this.topics.current)
426
+ });
427
+ }
428
+ this.#performDeduplicateGroupOperaton('joinGroup', (connection, groupCallback) => {
429
+ joinGroupV9(connection, this.groupId, options.sessionTimeout, options.rebalanceTimeout, this.memberId ?? '', null, 'consumer', protocols, '', groupCallback);
430
+ }, (error, response) => {
431
+ if (!this.#membershipActive) {
432
+ callback(null, undefined);
433
+ return;
434
+ }
435
+ if (error) {
436
+ if (this.#getRejoinError(error)) {
437
+ this.#joinGroup(options, callback);
438
+ return;
439
+ }
440
+ callback(error, undefined);
441
+ return;
442
+ }
443
+ this.generationId = response.generationId;
444
+ this.#isLeader = response.leader === this.memberId;
445
+ this.#protocol = response.protocolName;
446
+ this.#members = new Map();
447
+ for (const member of response.members) {
448
+ this.#members.set(member.memberId, this.#decodeProtocolSubscriptionMetadata(member.memberId, member.metadata));
449
+ }
450
+ // Send a syncGroup request
451
+ this.#syncGroup(null, (error, response) => {
452
+ if (!this.#membershipActive) {
453
+ callback(null, undefined);
454
+ return;
455
+ }
456
+ if (error) {
457
+ if (this.#getRejoinError(error)) {
458
+ this.#joinGroup(options, callback);
459
+ return;
460
+ }
461
+ callback(error, undefined);
462
+ return;
463
+ }
464
+ this.assignments = response;
465
+ this.#cancelHeartbeat();
466
+ this.#heartbeatInterval = setTimeout(() => {
467
+ this.#heartbeat(options);
468
+ }, options.heartbeatInterval);
469
+ this.emitWithDebug('consumer', 'group:join', {
470
+ groupId: this.groupId,
471
+ memberId: this.memberId,
472
+ generationId: this.generationId,
473
+ isLeader: this.#isLeader,
474
+ assignments: this.assignments
475
+ });
476
+ callback(null, this.memberId);
477
+ });
478
+ });
479
+ }
480
+ #leaveGroup(force, callback) {
481
+ if (!this.memberId) {
482
+ callback(null);
483
+ return;
484
+ }
485
+ // Remove streams that might have been exited in the meanwhile
486
+ for (const stream of this.#streams) {
487
+ if (stream.closed || stream.destroyed) {
488
+ this.#streams.delete(stream);
489
+ }
490
+ }
491
+ if (this.#streams.size) {
492
+ if (!force) {
493
+ callback(new UserError('Cannot leave group while consuming messages.'));
494
+ return;
495
+ }
496
+ runConcurrentCallbacks('Closing streams failed.', this.#streams, (stream, concurrentCallback) => {
497
+ stream.close(concurrentCallback);
498
+ }, error => {
499
+ if (error) {
500
+ callback(error);
501
+ return;
502
+ }
503
+ // All streams are closed, try the operation again without force
504
+ this.#leaveGroup(false, callback);
505
+ });
506
+ return;
507
+ }
508
+ this.#cancelHeartbeat();
509
+ this.#performDeduplicateGroupOperaton('leaveGroup', (connection, groupCallback) => {
510
+ leaveGroupV5(connection, this.groupId, [{ memberId: this.memberId }], groupCallback);
511
+ }, error => {
512
+ if (error) {
513
+ const unknownMemberError = error.findBy?.('unknownMemberId', true);
514
+ // This is to avoid throwing an error if a group join was cancelled.
515
+ if (!unknownMemberError) {
516
+ callback(error);
517
+ return;
518
+ }
519
+ }
520
+ this.emitWithDebug('consumer', 'group:leave', {
521
+ groupId: this.groupId,
522
+ memberId: this.memberId,
523
+ generationId: this.generationId
524
+ });
525
+ callback(null);
526
+ });
527
+ }
528
+ #syncGroup(assignments, callback) {
529
+ if (!this.#membershipActive) {
530
+ callback(null, undefined);
531
+ return;
532
+ }
533
+ if (!Array.isArray(assignments)) {
534
+ if (this.#isLeader) {
535
+ // Get all the metadata for the topics the consumer are listening to, then compute the assignments
536
+ const topicsSubscriptions = new Map();
537
+ for (const subscription of this.#members.values()) {
538
+ for (const topic of subscription.topics) {
539
+ let topicSubscriptions = topicsSubscriptions.get(topic);
540
+ if (!topicSubscriptions) {
541
+ topicSubscriptions = [];
542
+ topicsSubscriptions.set(topic, topicSubscriptions);
543
+ }
544
+ topicSubscriptions.push(subscription);
545
+ }
546
+ }
547
+ this[kMetadata]({ topics: Array.from(topicsSubscriptions.keys()) }, (error, metadata) => {
548
+ if (error) {
549
+ callback(error, undefined);
550
+ return;
551
+ }
552
+ if (!this.#membershipActive) {
553
+ callback(null, undefined);
554
+ return;
555
+ }
556
+ this.#syncGroup(this.#roundRobinAssignments(metadata), callback);
557
+ });
558
+ return;
559
+ }
560
+ else {
561
+ // Non leader simply do not send any assignments and wait
562
+ assignments = [];
563
+ }
564
+ }
565
+ this.#performDeduplicateGroupOperaton('syncGroup', (connection, groupCallback) => {
566
+ syncGroupV5(connection, this.groupId, this.generationId, this.memberId, null, 'consumer', this.#protocol, assignments, groupCallback);
567
+ }, (error, response) => {
568
+ if (!this.#membershipActive) {
569
+ callback(null, undefined);
570
+ return;
571
+ }
572
+ if (error) {
573
+ callback(error, undefined);
574
+ return;
575
+ }
576
+ else if (response.assignment.length === 0) {
577
+ callback(null, []);
578
+ return;
579
+ }
580
+ // Read the assignment back
581
+ const reader = Reader.from(response.assignment);
582
+ const assignments = reader.readArray(r => {
583
+ return {
584
+ topic: r.readString(),
585
+ partitions: r.readArray(r => r.readInt32(), true, false)
586
+ };
587
+ }, true, false);
588
+ callback(error, assignments);
589
+ });
590
+ }
591
+ #heartbeat(options) {
592
+ const eventPayload = { groupId: this.groupId, memberId: this.memberId, generationId: this.generationId };
593
+ this.#performDeduplicateGroupOperaton('heartbeat', (connection, groupCallback) => {
594
+ // We have left the group in the meanwhile, abort
595
+ if (!this.#membershipActive) {
596
+ return;
597
+ }
598
+ this.emitWithDebug('consumer:heartbeat', 'start');
599
+ heartbeatV4(connection, this.groupId, this.generationId, this.memberId, null, groupCallback);
600
+ }, error => {
601
+ // The heartbeat has been aborted elsewhere, ignore the response
602
+ if (this.#heartbeatInterval === null || !this.#membershipActive) {
603
+ return;
604
+ }
605
+ if (error) {
606
+ this.#cancelHeartbeat();
607
+ if (this.#getRejoinError(error)) {
608
+ this[kPerformWithRetry]('rejoinGroup', retryCallback => {
609
+ this.#joinGroup(options, retryCallback);
610
+ }, error => {
611
+ if (error) {
612
+ this.emitWithDebug(null, 'error', error);
613
+ }
614
+ this.emitWithDebug('consumer', 'rejoin');
615
+ }, 0);
616
+ return;
617
+ }
618
+ this.emitWithDebug('consumer:heartbeat', 'error', { ...eventPayload, error });
619
+ // Note that here we purposely do not return, since it was not a group related problem we schedule another heartbeat
620
+ }
621
+ else {
622
+ this.emitWithDebug('consumer:heartbeat', 'end', eventPayload);
623
+ }
624
+ this.#heartbeatInterval?.refresh();
625
+ });
626
+ }
627
+ #cancelHeartbeat() {
628
+ clearTimeout(this.#heartbeatInterval);
629
+ this.#heartbeatInterval = null;
630
+ }
631
+ #performDeduplicateGroupOperaton(operationId, operation, callback) {
632
+ return this[kPerformDeduplicated](operationId, deduplicateCallback => {
633
+ this.#performGroupOperation(operationId, operation, deduplicateCallback);
634
+ }, callback);
635
+ }
636
+ #performGroupOperation(operationId, operation, callback) {
637
+ this.#findGroupCoordinator((error, coordinatorId) => {
638
+ if (error) {
639
+ callback(error, undefined);
640
+ return;
641
+ }
642
+ this[kMetadata]({ topics: this.topics.current }, (error, metadata) => {
643
+ if (error) {
644
+ callback(error, undefined);
645
+ return;
646
+ }
647
+ this[kPerformWithRetry](operationId, retryCallback => {
648
+ this[kConnections].get(metadata.brokers.get(coordinatorId), (error, connection) => {
649
+ if (error) {
650
+ retryCallback(error, undefined);
651
+ return;
652
+ }
653
+ operation(connection, retryCallback);
654
+ });
655
+ }, callback);
656
+ });
657
+ });
658
+ }
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.');
668
+ }
669
+ }
670
+ /*
671
+ The following two methods follow:
672
+ https://github.com/apache/kafka/blob/trunk/clients/src/main/resources/common/message/ConsumerProtocolSubscription.json
673
+ */
674
+ #encodeProtocolSubscriptionMetadata(metadata, topics) {
675
+ return Writer.create()
676
+ .appendInt16(metadata.version)
677
+ .appendArray(topics, (w, t) => w.appendString(t, false), false, false)
678
+ .appendBytes(typeof metadata.metadata === 'string' ? Buffer.from(metadata.metadata) : metadata.metadata, false)
679
+ .buffer;
680
+ }
681
+ #decodeProtocolSubscriptionMetadata(memberId, buffer) {
682
+ const reader = Reader.from(buffer);
683
+ return {
684
+ memberId,
685
+ version: reader.readInt16(),
686
+ topics: reader.readArray(r => r.readString(false), false, false),
687
+ metadata: reader.readBytes(false)
688
+ };
689
+ }
690
+ /*
691
+ This follows:
692
+ https://github.com/apache/kafka/blob/trunk/clients/src/main/resources/common/message/ConsumerProtocolAssignment.json
693
+ */
694
+ #encodeProtocolAssignment(assignments) {
695
+ return Writer.create().appendArray(assignments, (w, { topic, partitions }) => {
696
+ w.appendString(topic).appendArray(partitions, (w, a) => w.appendInt32(a), true, false);
697
+ }, true, false).buffer;
698
+ }
699
+ #roundRobinAssignments(metadata) {
700
+ const partitionTracker = new Map();
701
+ // First of all, layout topics-partitions in a list
702
+ for (const [topic, partitions] of metadata.topics) {
703
+ partitionTracker.set(topic, { next: 0, max: partitions.partitionsCount });
704
+ }
705
+ // We are the only member of the group, assign all partitions to us
706
+ const membersSize = this.#members.size;
707
+ if (membersSize === 1) {
708
+ const assignments = [];
709
+ for (const topic of this.topics.current) {
710
+ const partitionsCount = metadata.topics.get(topic).partitionsCount;
711
+ const partitions = [];
712
+ for (let i = 0; i < partitionsCount; i++) {
713
+ partitions.push(i);
714
+ }
715
+ assignments.push({ topic, partitions });
716
+ }
717
+ return [{ memberId: this.memberId, assignment: this.#encodeProtocolAssignment(assignments) }];
718
+ }
719
+ // Flat the list of members and subscribed topics
720
+ const members = [];
721
+ const subscribedTopics = new Set();
722
+ for (const [memberId, subscription] of this.#members) {
723
+ members.push({ memberId, assignments: new Map() });
724
+ for (const topic of subscription.topics) {
725
+ subscribedTopics.add(topic);
726
+ }
727
+ }
728
+ // Assign topic-partitions in round robin
729
+ let currentMember = 0;
730
+ for (const topic of subscribedTopics) {
731
+ const partitionsCount = metadata.topics.get(topic).partitionsCount;
732
+ for (let i = 0; i < partitionsCount; i++) {
733
+ const member = members[currentMember++ % membersSize];
734
+ let topicAssignments = member.assignments.get(topic);
735
+ if (!topicAssignments) {
736
+ topicAssignments = { topic, partitions: [] };
737
+ member.assignments.set(topic, topicAssignments);
738
+ }
739
+ topicAssignments?.partitions.push(i);
740
+ }
741
+ }
742
+ const assignments = [];
743
+ for (const member of members) {
744
+ assignments.push({
745
+ memberId: member.memberId,
746
+ assignment: this.#encodeProtocolAssignment(Array.from(member.assignments.values()))
747
+ });
748
+ }
749
+ return assignments;
750
+ }
751
+ #getRejoinError(error) {
752
+ const protocolError = error.findBy?.('needsRejoin', true);
753
+ if (!protocolError) {
754
+ return null;
755
+ }
756
+ if (protocolError.rebalanceInProgress) {
757
+ this.emitWithDebug('consumer', 'group:rebalance', { groupId: this.groupId });
758
+ }
759
+ if (protocolError.unknownMemberId) {
760
+ this.memberId = null;
761
+ }
762
+ else if (protocolError.memberId && !this.memberId) {
763
+ this.memberId = protocolError.memberId;
764
+ }
765
+ return protocolError;
766
+ }
767
+ }