kafka-ts 0.0.1-beta

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 (196) hide show
  1. package/.prettierrc +7 -0
  2. package/LICENSE +24 -0
  3. package/README.md +88 -0
  4. package/certs/ca.crt +29 -0
  5. package/certs/ca.key +52 -0
  6. package/certs/ca.srl +1 -0
  7. package/certs/kafka.crt +29 -0
  8. package/certs/kafka.csr +26 -0
  9. package/certs/kafka.key +52 -0
  10. package/certs/kafka.keystore.jks +0 -0
  11. package/certs/kafka.truststore.jks +0 -0
  12. package/dist/api/api-versions.d.ts +9 -0
  13. package/dist/api/api-versions.js +24 -0
  14. package/dist/api/create-topics.d.ts +38 -0
  15. package/dist/api/create-topics.js +53 -0
  16. package/dist/api/delete-topics.d.ts +18 -0
  17. package/dist/api/delete-topics.js +33 -0
  18. package/dist/api/fetch.d.ts +77 -0
  19. package/dist/api/fetch.js +106 -0
  20. package/dist/api/find-coordinator.d.ts +21 -0
  21. package/dist/api/find-coordinator.js +39 -0
  22. package/dist/api/heartbeat.d.ts +11 -0
  23. package/dist/api/heartbeat.js +27 -0
  24. package/dist/api/index.d.ts +573 -0
  25. package/dist/api/index.js +164 -0
  26. package/dist/api/init-producer-id.d.ts +13 -0
  27. package/dist/api/init-producer-id.js +29 -0
  28. package/dist/api/join-group.d.ts +34 -0
  29. package/dist/api/join-group.js +51 -0
  30. package/dist/api/leave-group.d.ts +19 -0
  31. package/dist/api/leave-group.js +39 -0
  32. package/dist/api/list-offsets.d.ts +29 -0
  33. package/dist/api/list-offsets.js +48 -0
  34. package/dist/api/metadata.d.ts +40 -0
  35. package/dist/api/metadata.js +58 -0
  36. package/dist/api/offset-commit.d.ts +28 -0
  37. package/dist/api/offset-commit.js +48 -0
  38. package/dist/api/offset-fetch.d.ts +33 -0
  39. package/dist/api/offset-fetch.js +57 -0
  40. package/dist/api/produce.d.ts +53 -0
  41. package/dist/api/produce.js +129 -0
  42. package/dist/api/sasl-authenticate.d.ts +11 -0
  43. package/dist/api/sasl-authenticate.js +23 -0
  44. package/dist/api/sasl-handshake.d.ts +6 -0
  45. package/dist/api/sasl-handshake.js +19 -0
  46. package/dist/api/sync-group.d.ts +24 -0
  47. package/dist/api/sync-group.js +36 -0
  48. package/dist/broker.d.ts +29 -0
  49. package/dist/broker.js +60 -0
  50. package/dist/client.d.ts +23 -0
  51. package/dist/client.js +36 -0
  52. package/dist/cluster.d.ts +24 -0
  53. package/dist/cluster.js +72 -0
  54. package/dist/connection.d.ts +25 -0
  55. package/dist/connection.js +155 -0
  56. package/dist/consumer/consumer-group.d.ts +36 -0
  57. package/dist/consumer/consumer-group.js +182 -0
  58. package/dist/consumer/consumer-metadata.d.ts +7 -0
  59. package/dist/consumer/consumer-metadata.js +14 -0
  60. package/dist/consumer/consumer.d.ts +37 -0
  61. package/dist/consumer/consumer.js +178 -0
  62. package/dist/consumer/metadata.d.ts +24 -0
  63. package/dist/consumer/metadata.js +64 -0
  64. package/dist/consumer/offset-manager.d.ts +22 -0
  65. package/dist/consumer/offset-manager.js +56 -0
  66. package/dist/distributors/assignments-to-replicas.d.ts +17 -0
  67. package/dist/distributors/assignments-to-replicas.js +60 -0
  68. package/dist/distributors/assignments-to-replicas.test.d.ts +1 -0
  69. package/dist/distributors/assignments-to-replicas.test.js +40 -0
  70. package/dist/distributors/messages-to-topic-partition-leaders.d.ts +17 -0
  71. package/dist/distributors/messages-to-topic-partition-leaders.js +15 -0
  72. package/dist/distributors/messages-to-topic-partition-leaders.test.d.ts +1 -0
  73. package/dist/distributors/messages-to-topic-partition-leaders.test.js +30 -0
  74. package/dist/examples/src/replicator.js +34 -0
  75. package/dist/examples/src/utils/json.js +5 -0
  76. package/dist/index.d.ts +3 -0
  77. package/dist/index.js +19 -0
  78. package/dist/metadata.d.ts +24 -0
  79. package/dist/metadata.js +89 -0
  80. package/dist/producer/producer.d.ts +19 -0
  81. package/dist/producer/producer.js +111 -0
  82. package/dist/request-handler.d.ts +16 -0
  83. package/dist/request-handler.js +67 -0
  84. package/dist/request-handler.test.d.ts +1 -0
  85. package/dist/request-handler.test.js +340 -0
  86. package/dist/src/api/api-versions.js +18 -0
  87. package/dist/src/api/create-topics.js +46 -0
  88. package/dist/src/api/delete-topics.js +26 -0
  89. package/dist/src/api/fetch.js +95 -0
  90. package/dist/src/api/find-coordinator.js +34 -0
  91. package/dist/src/api/heartbeat.js +22 -0
  92. package/dist/src/api/index.js +38 -0
  93. package/dist/src/api/init-producer-id.js +24 -0
  94. package/dist/src/api/join-group.js +48 -0
  95. package/dist/src/api/leave-group.js +30 -0
  96. package/dist/src/api/list-offsets.js +39 -0
  97. package/dist/src/api/metadata.js +47 -0
  98. package/dist/src/api/offset-commit.js +39 -0
  99. package/dist/src/api/offset-fetch.js +44 -0
  100. package/dist/src/api/produce.js +119 -0
  101. package/dist/src/api/sync-group.js +31 -0
  102. package/dist/src/broker.js +35 -0
  103. package/dist/src/connection.js +21 -0
  104. package/dist/src/consumer/consumer-group.js +131 -0
  105. package/dist/src/consumer/consumer.js +103 -0
  106. package/dist/src/consumer/metadata.js +52 -0
  107. package/dist/src/consumer/offset-manager.js +23 -0
  108. package/dist/src/index.js +19 -0
  109. package/dist/src/producer/producer.js +84 -0
  110. package/dist/src/request-handler.js +57 -0
  111. package/dist/src/request-handler.test.js +321 -0
  112. package/dist/src/types.js +2 -0
  113. package/dist/src/utils/api.js +5 -0
  114. package/dist/src/utils/decoder.js +161 -0
  115. package/dist/src/utils/encoder.js +137 -0
  116. package/dist/src/utils/error.js +10 -0
  117. package/dist/types.d.ts +9 -0
  118. package/dist/types.js +2 -0
  119. package/dist/utils/api.d.ts +9 -0
  120. package/dist/utils/api.js +5 -0
  121. package/dist/utils/debug.d.ts +2 -0
  122. package/dist/utils/debug.js +11 -0
  123. package/dist/utils/decoder.d.ts +29 -0
  124. package/dist/utils/decoder.js +147 -0
  125. package/dist/utils/delay.d.ts +1 -0
  126. package/dist/utils/delay.js +5 -0
  127. package/dist/utils/encoder.d.ts +28 -0
  128. package/dist/utils/encoder.js +122 -0
  129. package/dist/utils/error.d.ts +11 -0
  130. package/dist/utils/error.js +27 -0
  131. package/dist/utils/memo.d.ts +1 -0
  132. package/dist/utils/memo.js +16 -0
  133. package/dist/utils/retrier.d.ts +10 -0
  134. package/dist/utils/retrier.js +22 -0
  135. package/dist/utils/tracer.d.ts +1 -0
  136. package/dist/utils/tracer.js +26 -0
  137. package/docker-compose.yml +104 -0
  138. package/examples/node_modules/.package-lock.json +22 -0
  139. package/examples/package-lock.json +30 -0
  140. package/examples/package.json +14 -0
  141. package/examples/src/client.ts +9 -0
  142. package/examples/src/consumer.ts +17 -0
  143. package/examples/src/create-topic.ts +37 -0
  144. package/examples/src/producer.ts +24 -0
  145. package/examples/src/replicator.ts +25 -0
  146. package/examples/src/utils/json.ts +1 -0
  147. package/examples/tsconfig.json +7 -0
  148. package/log4j.properties +95 -0
  149. package/package.json +17 -0
  150. package/scripts/generate-certs.sh +24 -0
  151. package/src/__snapshots__/request-handler.test.ts.snap +1687 -0
  152. package/src/api/api-versions.ts +21 -0
  153. package/src/api/create-topics.ts +78 -0
  154. package/src/api/delete-topics.ts +42 -0
  155. package/src/api/fetch.ts +143 -0
  156. package/src/api/find-coordinator.ts +39 -0
  157. package/src/api/heartbeat.ts +33 -0
  158. package/src/api/index.ts +164 -0
  159. package/src/api/init-producer-id.ts +35 -0
  160. package/src/api/join-group.ts +67 -0
  161. package/src/api/leave-group.ts +48 -0
  162. package/src/api/list-offsets.ts +65 -0
  163. package/src/api/metadata.ts +66 -0
  164. package/src/api/offset-commit.ts +67 -0
  165. package/src/api/offset-fetch.ts +74 -0
  166. package/src/api/produce.ts +173 -0
  167. package/src/api/sasl-authenticate.ts +21 -0
  168. package/src/api/sasl-handshake.ts +16 -0
  169. package/src/api/sync-group.ts +54 -0
  170. package/src/broker.ts +74 -0
  171. package/src/client.ts +47 -0
  172. package/src/cluster.ts +87 -0
  173. package/src/connection.ts +141 -0
  174. package/src/consumer/consumer-group.ts +209 -0
  175. package/src/consumer/consumer-metadata.ts +14 -0
  176. package/src/consumer/consumer.ts +229 -0
  177. package/src/consumer/offset-manager.ts +93 -0
  178. package/src/distributors/assignments-to-replicas.test.ts +43 -0
  179. package/src/distributors/assignments-to-replicas.ts +85 -0
  180. package/src/distributors/messages-to-topic-partition-leaders.test.ts +32 -0
  181. package/src/distributors/messages-to-topic-partition-leaders.ts +19 -0
  182. package/src/index.ts +3 -0
  183. package/src/metadata.ts +122 -0
  184. package/src/producer/producer.ts +132 -0
  185. package/src/request-handler.test.ts +366 -0
  186. package/src/types.ts +9 -0
  187. package/src/utils/api.ts +11 -0
  188. package/src/utils/debug.ts +9 -0
  189. package/src/utils/decoder.ts +168 -0
  190. package/src/utils/delay.ts +1 -0
  191. package/src/utils/encoder.ts +141 -0
  192. package/src/utils/error.ts +21 -0
  193. package/src/utils/memo.ts +12 -0
  194. package/src/utils/retrier.ts +39 -0
  195. package/src/utils/tracer.ts +28 -0
  196. package/tsconfig.json +17 -0
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || function (mod) {
25
+ if (mod && mod.__esModule) return mod;
26
+ var result = {};
27
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
28
+ __setModuleDefault(result, mod);
29
+ return result;
30
+ };
31
+ var __metadata = (this && this.__metadata) || function (k, v) {
32
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
33
+ };
34
+ var __importDefault = (this && this.__importDefault) || function (mod) {
35
+ return (mod && mod.__esModule) ? mod : { "default": mod };
36
+ };
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.Connection = void 0;
39
+ const assert_1 = __importDefault(require("assert"));
40
+ const net_1 = __importStar(require("net"));
41
+ const tls_1 = __importDefault(require("tls"));
42
+ const api_1 = require("./api");
43
+ const decoder_1 = require("./utils/decoder");
44
+ const encoder_1 = require("./utils/encoder");
45
+ const error_1 = require("./utils/error");
46
+ const tracer_1 = require("./utils/tracer");
47
+ class Connection {
48
+ options;
49
+ socket = new net_1.Socket();
50
+ queue = {};
51
+ lastCorrelationId = 0;
52
+ buffer = null;
53
+ constructor(options) {
54
+ this.options = options;
55
+ }
56
+ async connect() {
57
+ this.queue = {};
58
+ this.buffer = null;
59
+ await new Promise((resolve, reject) => {
60
+ const { ssl, connection } = this.options;
61
+ this.socket = ssl
62
+ ? tls_1.default.connect({
63
+ ...connection,
64
+ ...ssl,
65
+ ...(connection.host && !(0, net_1.isIP)(connection.host) && { servername: connection.host }),
66
+ }, resolve)
67
+ : net_1.default.connect(connection, resolve);
68
+ this.socket.once("error", reject);
69
+ });
70
+ this.socket.removeAllListeners("error");
71
+ this.socket.on("data", (data) => this.handleData(data));
72
+ this.socket.once("close", async () => {
73
+ Object.values(this.queue).forEach(({ reject }) => {
74
+ reject(new error_1.ConnectionError("Socket closed unexpectedly"));
75
+ });
76
+ this.queue = {};
77
+ });
78
+ }
79
+ disconnect() {
80
+ this.socket.removeAllListeners();
81
+ return new Promise((resolve) => {
82
+ if (this.socket.pending) {
83
+ return resolve();
84
+ }
85
+ this.socket.end(resolve);
86
+ });
87
+ }
88
+ async sendRequest(api, body) {
89
+ const correlationId = this.nextCorrelationId();
90
+ const encoder = new encoder_1.Encoder()
91
+ .writeInt16(api.apiKey)
92
+ .writeInt16(api.apiVersion)
93
+ .writeInt32(correlationId)
94
+ .writeString(this.options.clientId);
95
+ const request = api.request(encoder, body).value();
96
+ const requestEncoder = new encoder_1.Encoder().writeInt32(request.length).write(request);
97
+ const { responseDecoder, responseSize } = await new Promise(async (resolve, reject) => {
98
+ try {
99
+ await this.write(requestEncoder.value());
100
+ this.queue[correlationId] = { resolve, reject };
101
+ }
102
+ catch (error) {
103
+ reject(error);
104
+ }
105
+ });
106
+ const response = api.response(responseDecoder);
107
+ (0, assert_1.default)(responseDecoder.getOffset() - 4 === responseSize, `Buffer not correctly consumed: ${responseDecoder.getOffset() - 4} !== ${responseSize}`);
108
+ return response;
109
+ }
110
+ write(buffer) {
111
+ return new Promise((resolve, reject) => {
112
+ const { stack } = new Error("Write error");
113
+ this.socket.write(buffer, (error) => {
114
+ if (error) {
115
+ const err = new error_1.ConnectionError(error.message);
116
+ err.stack += `\n${stack}`;
117
+ return reject(err);
118
+ }
119
+ resolve();
120
+ });
121
+ });
122
+ }
123
+ handleData(buffer) {
124
+ this.buffer = this.buffer ? Buffer.concat([this.buffer, buffer]) : buffer;
125
+ if (this.buffer.length < 4) {
126
+ return;
127
+ }
128
+ const decoder = new decoder_1.Decoder(this.buffer);
129
+ const size = decoder.readInt32();
130
+ if (size !== decoder.getBufferLength() - 4) {
131
+ return;
132
+ }
133
+ const correlationId = decoder.readInt32();
134
+ const { resolve } = this.queue[correlationId];
135
+ delete this.queue[correlationId];
136
+ resolve({ responseDecoder: decoder, responseSize: size });
137
+ this.buffer = null;
138
+ }
139
+ nextCorrelationId() {
140
+ return (this.lastCorrelationId = (this.lastCorrelationId + 1) % 2 ** 31);
141
+ }
142
+ }
143
+ exports.Connection = Connection;
144
+ __decorate([
145
+ (0, tracer_1.trace)(),
146
+ __metadata("design:type", Function),
147
+ __metadata("design:paramtypes", []),
148
+ __metadata("design:returntype", Promise)
149
+ ], Connection.prototype, "connect", null);
150
+ __decorate([
151
+ (0, tracer_1.trace)((api, body) => ({ apiName: (0, api_1.getApiName)(api), body })),
152
+ __metadata("design:type", Function),
153
+ __metadata("design:paramtypes", [Object, Request]),
154
+ __metadata("design:returntype", Promise)
155
+ ], Connection.prototype, "sendRequest", null);
@@ -0,0 +1,36 @@
1
+ import { Cluster } from "../cluster";
2
+ import { ConsumerMetadata } from "./consumer-metadata";
3
+ import { OffsetManager } from "./offset-manager";
4
+ type ConsumerGroupOptions = {
5
+ cluster: Cluster;
6
+ topics: string[];
7
+ groupId: string;
8
+ groupInstanceId: string | null;
9
+ sessionTimeoutMs: number;
10
+ rebalanceTimeoutMs: number;
11
+ metadata: ConsumerMetadata;
12
+ offsetManager: OffsetManager;
13
+ };
14
+ export declare class ConsumerGroup {
15
+ private options;
16
+ private coordinatorId;
17
+ private memberId;
18
+ private generationId;
19
+ private leaderId;
20
+ private memberIds;
21
+ private heartbeatInterval;
22
+ private heartbeatError;
23
+ constructor(options: ConsumerGroupOptions);
24
+ join(): Promise<void>;
25
+ private startHeartbeater;
26
+ private stopHeartbeater;
27
+ handleLastHeartbeat(): Promise<void>;
28
+ private findCoordinator;
29
+ private joinGroup;
30
+ private syncGroup;
31
+ private offsetFetch;
32
+ offsetCommit(): Promise<void>;
33
+ heartbeat(): Promise<void>;
34
+ leaveGroup(): Promise<void>;
35
+ }
36
+ export {};
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConsumerGroup = void 0;
4
+ const api_1 = require("../api");
5
+ const find_coordinator_1 = require("../api/find-coordinator");
6
+ class ConsumerGroup {
7
+ options;
8
+ coordinatorId = -1;
9
+ memberId = "";
10
+ generationId = -1;
11
+ leaderId = "";
12
+ memberIds = [];
13
+ heartbeatInterval = null;
14
+ heartbeatError = null;
15
+ constructor(options) {
16
+ this.options = options;
17
+ }
18
+ async join() {
19
+ await this.findCoordinator();
20
+ await this.joinGroup();
21
+ await this.syncGroup();
22
+ await this.offsetFetch();
23
+ this.startHeartbeater();
24
+ }
25
+ async startHeartbeater() {
26
+ this.heartbeatInterval = setInterval(async () => {
27
+ try {
28
+ await this.heartbeat();
29
+ }
30
+ catch (error) {
31
+ this.heartbeatError = error;
32
+ }
33
+ }, 5000);
34
+ }
35
+ async stopHeartbeater() {
36
+ if (this.heartbeatInterval) {
37
+ clearInterval(this.heartbeatInterval);
38
+ this.heartbeatInterval = null;
39
+ }
40
+ }
41
+ async handleLastHeartbeat() {
42
+ if (this.heartbeatError) {
43
+ throw this.heartbeatError;
44
+ }
45
+ }
46
+ async findCoordinator() {
47
+ const { coordinators } = await this.options.cluster.sendRequest(api_1.API.FIND_COORDINATOR, {
48
+ keyType: find_coordinator_1.KEY_TYPE.GROUP,
49
+ keys: [this.options.groupId],
50
+ });
51
+ this.coordinatorId = coordinators[0].nodeId;
52
+ }
53
+ async joinGroup() {
54
+ const { cluster, groupId, groupInstanceId, sessionTimeoutMs, rebalanceTimeoutMs, topics } = this.options;
55
+ try {
56
+ const response = await cluster.sendRequestToNode(this.coordinatorId)(api_1.API.JOIN_GROUP, {
57
+ groupId,
58
+ groupInstanceId,
59
+ memberId: this.memberId,
60
+ sessionTimeoutMs,
61
+ rebalanceTimeoutMs,
62
+ protocolType: "consumer",
63
+ protocols: [{ name: "RoundRobinAssigner", metadata: { version: 0, topics } }],
64
+ reason: null,
65
+ });
66
+ this.memberId = response.memberId;
67
+ this.generationId = response.generationId;
68
+ this.leaderId = response.leader;
69
+ this.memberIds = response.members.map((member) => member.memberId);
70
+ }
71
+ catch (error) {
72
+ if (error.errorCode === api_1.API_ERROR.MEMBER_ID_REQUIRED) {
73
+ this.memberId = error.response.memberId;
74
+ return this.joinGroup();
75
+ }
76
+ throw error;
77
+ }
78
+ }
79
+ async syncGroup() {
80
+ const { cluster, metadata, groupId, groupInstanceId } = this.options;
81
+ let assignments = [];
82
+ if (this.memberId === this.leaderId) {
83
+ const memberAssignments = Object.entries(metadata.getTopicPartitions())
84
+ .flatMap(([topic, partitions]) => partitions.map((partition) => ({ topic, partition })))
85
+ .reduce((acc, { topic, partition }, index) => {
86
+ const memberId = this.memberIds[index % this.memberIds.length];
87
+ acc[memberId] ??= {};
88
+ acc[memberId][topic] ??= [];
89
+ acc[memberId][topic].push(partition);
90
+ return acc;
91
+ }, {});
92
+ assignments = Object.entries(memberAssignments).map(([memberId, assignment]) => ({ memberId, assignment }));
93
+ }
94
+ const response = await cluster.sendRequestToNode(this.coordinatorId)(api_1.API.SYNC_GROUP, {
95
+ groupId,
96
+ groupInstanceId,
97
+ memberId: this.memberId,
98
+ generationId: this.generationId,
99
+ protocolType: "consumer",
100
+ protocolName: "RoundRobinAssigner",
101
+ assignments,
102
+ });
103
+ metadata.setAssignment(JSON.parse(response.assignments || "{}"));
104
+ }
105
+ async offsetFetch() {
106
+ const { cluster, groupId, topics, metadata, offsetManager } = this.options;
107
+ const assignment = metadata.getAssignment();
108
+ const request = {
109
+ groups: [
110
+ {
111
+ groupId,
112
+ memberId: this.memberId,
113
+ memberEpoch: -1,
114
+ topics: topics
115
+ .map((topic) => ({ name: topic, partitionIndexes: assignment[topic] ?? [] }))
116
+ .filter(({ partitionIndexes }) => partitionIndexes.length),
117
+ },
118
+ ].filter(({ topics }) => topics.length),
119
+ requireStable: true,
120
+ };
121
+ if (!request.groups.length)
122
+ return;
123
+ const response = await cluster.sendRequestToNode(this.coordinatorId)(api_1.API.OFFSET_FETCH, request);
124
+ response.groups.forEach((group) => {
125
+ group.topics.forEach((topic) => {
126
+ topic.partitions
127
+ .filter(({ committedOffset }) => committedOffset >= 0)
128
+ .forEach(({ partitionIndex, committedOffset }) => offsetManager.resolve(topic.name, partitionIndex, committedOffset));
129
+ });
130
+ });
131
+ offsetManager.flush();
132
+ }
133
+ async offsetCommit() {
134
+ const { cluster, groupId, groupInstanceId, offsetManager } = this.options;
135
+ const request = {
136
+ groupId,
137
+ groupInstanceId,
138
+ memberId: this.memberId,
139
+ generationIdOrMemberEpoch: this.generationId,
140
+ topics: Object.entries(offsetManager.pendingOffsets).map(([topic, partitions]) => ({
141
+ name: topic,
142
+ partitions: Object.entries(partitions).map(([partition, offset]) => ({
143
+ partitionIndex: parseInt(partition),
144
+ committedOffset: offset,
145
+ committedLeaderEpoch: -1,
146
+ committedMetadata: null,
147
+ })),
148
+ })),
149
+ };
150
+ if (!request.topics.length) {
151
+ return;
152
+ }
153
+ await cluster.sendRequestToNode(this.coordinatorId)(api_1.API.OFFSET_COMMIT, request);
154
+ offsetManager.flush();
155
+ }
156
+ async heartbeat() {
157
+ const { cluster, groupId, groupInstanceId } = this.options;
158
+ await cluster.sendRequestToNode(this.coordinatorId)(api_1.API.HEARTBEAT, {
159
+ groupId,
160
+ groupInstanceId,
161
+ memberId: this.memberId,
162
+ generationId: this.generationId,
163
+ });
164
+ }
165
+ async leaveGroup() {
166
+ const { cluster, groupId, groupInstanceId } = this.options;
167
+ this.stopHeartbeater();
168
+ try {
169
+ await cluster.sendRequestToNode(this.coordinatorId)(api_1.API.LEAVE_GROUP, {
170
+ groupId,
171
+ members: [{ memberId: this.memberId, groupInstanceId, reason: null }],
172
+ });
173
+ }
174
+ catch (error) {
175
+ if (error.errorCode === api_1.API_ERROR.FENCED_INSTANCE_ID) {
176
+ return;
177
+ }
178
+ throw error;
179
+ }
180
+ }
181
+ }
182
+ exports.ConsumerGroup = ConsumerGroup;
@@ -0,0 +1,7 @@
1
+ import { Assignment } from "../api/sync-group";
2
+ import { Metadata } from "../metadata";
3
+ export declare class ConsumerMetadata extends Metadata {
4
+ private assignment;
5
+ getAssignment(): Assignment;
6
+ setAssignment(newAssignment: Assignment): void;
7
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConsumerMetadata = void 0;
4
+ const metadata_1 = require("../metadata");
5
+ class ConsumerMetadata extends metadata_1.Metadata {
6
+ assignment = {};
7
+ getAssignment() {
8
+ return this.assignment;
9
+ }
10
+ setAssignment(newAssignment) {
11
+ this.assignment = newAssignment;
12
+ }
13
+ }
14
+ exports.ConsumerMetadata = ConsumerMetadata;
@@ -0,0 +1,37 @@
1
+ import { IsolationLevel } from "../api/fetch";
2
+ import { Cluster } from "../cluster";
3
+ import { Message } from "../types";
4
+ import { Retrier } from "../utils/retrier";
5
+ export type ConsumerOptions = {
6
+ topics: string[];
7
+ groupId?: string | null;
8
+ groupInstanceId?: string | null;
9
+ rackId?: string;
10
+ isolationLevel?: IsolationLevel;
11
+ sessionTimeoutMs?: number;
12
+ rebalanceTimeoutMs?: number;
13
+ maxWaitMs?: number;
14
+ minBytes?: number;
15
+ maxBytes?: number;
16
+ partitionMaxBytes?: number;
17
+ allowTopicAutoCreation?: boolean;
18
+ fromBeginning?: boolean;
19
+ retrier?: Retrier;
20
+ } & ({
21
+ onMessage: (message: Message) => unknown;
22
+ } | {
23
+ onBatch: (messages: Message[]) => unknown;
24
+ });
25
+ export declare class Consumer {
26
+ private cluster;
27
+ private options;
28
+ private metadata;
29
+ private consumerGroup;
30
+ private offsetManager;
31
+ private stopHook;
32
+ constructor(cluster: Cluster, options: ConsumerOptions);
33
+ start(): Promise<void>;
34
+ private fetchLoop;
35
+ close(force?: boolean): Promise<void>;
36
+ private fetch;
37
+ }
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Consumer = void 0;
4
+ const api_1 = require("../api");
5
+ const assignments_to_replicas_1 = require("../distributors/assignments-to-replicas");
6
+ const delay_1 = require("../utils/delay");
7
+ const error_1 = require("../utils/error");
8
+ const retrier_1 = require("../utils/retrier");
9
+ const consumer_group_1 = require("./consumer-group");
10
+ const consumer_metadata_1 = require("./consumer-metadata");
11
+ const offset_manager_1 = require("./offset-manager");
12
+ class Consumer {
13
+ cluster;
14
+ options;
15
+ metadata;
16
+ consumerGroup;
17
+ offsetManager;
18
+ stopHook;
19
+ constructor(cluster, options) {
20
+ this.cluster = cluster;
21
+ this.options = {
22
+ ...options,
23
+ groupId: options.groupId ?? null,
24
+ groupInstanceId: options.groupInstanceId ?? null,
25
+ rackId: options.rackId ?? "",
26
+ sessionTimeoutMs: options.sessionTimeoutMs ?? 30_000,
27
+ rebalanceTimeoutMs: options.rebalanceTimeoutMs ?? 60_000,
28
+ maxWaitMs: options.maxWaitMs ?? 5000,
29
+ minBytes: options.minBytes ?? 1,
30
+ maxBytes: options.maxBytes ?? 1_000_000,
31
+ partitionMaxBytes: options.partitionMaxBytes ?? 1_000_000,
32
+ isolationLevel: options.isolationLevel ?? 0 /* IsolationLevel.READ_UNCOMMITTED */,
33
+ allowTopicAutoCreation: options.allowTopicAutoCreation ?? false,
34
+ fromBeginning: options.fromBeginning ?? false,
35
+ retrier: options.retrier ?? retrier_1.defaultRetrier,
36
+ };
37
+ this.metadata = new consumer_metadata_1.ConsumerMetadata({ cluster: this.cluster });
38
+ this.offsetManager = new offset_manager_1.OffsetManager({
39
+ cluster: this.cluster,
40
+ metadata: this.metadata,
41
+ isolationLevel: this.options.isolationLevel,
42
+ });
43
+ this.consumerGroup = this.options.groupId
44
+ ? new consumer_group_1.ConsumerGroup({
45
+ cluster: this.cluster,
46
+ topics: this.options.topics,
47
+ groupId: this.options.groupId,
48
+ groupInstanceId: this.options.groupInstanceId,
49
+ sessionTimeoutMs: this.options.sessionTimeoutMs,
50
+ rebalanceTimeoutMs: this.options.rebalanceTimeoutMs,
51
+ metadata: this.metadata,
52
+ offsetManager: this.offsetManager,
53
+ })
54
+ : undefined;
55
+ }
56
+ async start() {
57
+ const { topics, allowTopicAutoCreation, fromBeginning } = this.options;
58
+ this.stopHook = undefined;
59
+ try {
60
+ await this.cluster.connect();
61
+ await this.metadata.fetchMetadataIfNecessary({ topics, allowTopicAutoCreation });
62
+ this.metadata.setAssignment(this.metadata.getTopicPartitions());
63
+ await this.offsetManager.fetchOffsets({ fromBeginning });
64
+ await this.consumerGroup?.join();
65
+ }
66
+ catch (error) {
67
+ console.error(error);
68
+ console.debug(`Restarting consumer in 1 second...`);
69
+ await (0, delay_1.delay)(1000);
70
+ if (this.stopHook)
71
+ return this.stopHook();
72
+ return this.close(true).then(() => this.start());
73
+ }
74
+ this.fetchLoop();
75
+ }
76
+ fetchLoop = async () => {
77
+ const { options } = this;
78
+ const { retrier } = options;
79
+ let nodeAssignments = [];
80
+ let shouldReassign = true;
81
+ while (!this.stopHook) {
82
+ if (shouldReassign || !nodeAssignments) {
83
+ nodeAssignments = Object.entries((0, assignments_to_replicas_1.distributeAssignmentsToNodes)(this.metadata.getAssignment(), this.metadata.getTopicPartitionReplicaIds())).map(([nodeId, assignment]) => ({ nodeId: parseInt(nodeId), assignment }));
84
+ shouldReassign = false;
85
+ }
86
+ try {
87
+ for (const { nodeId, assignment } of nodeAssignments) {
88
+ const batch = await this.fetch(nodeId, assignment);
89
+ const messages = batch.responses.flatMap(({ topicId, partitions }) => partitions.flatMap(({ partitionIndex, records }) => records.flatMap(({ baseTimestamp, baseOffset, records }) => records.map((message) => ({
90
+ topic: this.metadata.getTopicNameById(topicId),
91
+ partition: partitionIndex,
92
+ key: message.key ?? null,
93
+ value: message.value ?? null,
94
+ headers: Object.fromEntries(message.headers.map(({ key, value }) => [key, value])),
95
+ timestamp: baseTimestamp + BigInt(message.timestampDelta),
96
+ offset: baseOffset + BigInt(message.offsetDelta),
97
+ })))));
98
+ if ("onBatch" in options) {
99
+ await retrier(() => options.onBatch(messages));
100
+ messages.forEach(({ topic, partition, offset }) => this.offsetManager.resolve(topic, partition, offset + 1n));
101
+ }
102
+ else if ("onMessage" in options) {
103
+ for (const message of messages) {
104
+ await retrier(() => options.onMessage(message));
105
+ const { topic, partition, offset } = message;
106
+ this.offsetManager.resolve(topic, partition, offset + 1n);
107
+ }
108
+ }
109
+ await this.consumerGroup?.offsetCommit();
110
+ await this.consumerGroup?.handleLastHeartbeat();
111
+ }
112
+ if (!nodeAssignments.length) {
113
+ console.debug("No partitions assigned. Waiting for reassignment...");
114
+ await (0, delay_1.delay)(this.options.maxWaitMs);
115
+ await this.consumerGroup?.handleLastHeartbeat();
116
+ }
117
+ }
118
+ catch (error) {
119
+ if (error.errorCode === api_1.API_ERROR.REBALANCE_IN_PROGRESS) {
120
+ console.debug("Rebalance in progress...");
121
+ shouldReassign = true;
122
+ continue;
123
+ }
124
+ if (error.errorCode === api_1.API_ERROR.FENCED_INSTANCE_ID) {
125
+ console.debug("New consumer with the same groupInstanceId joined. Exiting the consumer...");
126
+ this.close();
127
+ break;
128
+ }
129
+ if (error instanceof error_1.ConnectionError ||
130
+ (error instanceof error_1.KafkaTSApiError && error.errorCode === api_1.API_ERROR.NOT_COORDINATOR)) {
131
+ console.debug(`${error.message}. Restarting consumer...`);
132
+ this.close().then(() => this.start());
133
+ break;
134
+ }
135
+ console.error(error);
136
+ await this.consumerGroup?.offsetCommit();
137
+ break;
138
+ }
139
+ }
140
+ this.stopHook?.();
141
+ };
142
+ async close(force = false) {
143
+ if (!force) {
144
+ await new Promise((resolve) => {
145
+ this.stopHook = resolve;
146
+ });
147
+ }
148
+ await this.consumerGroup
149
+ ?.leaveGroup()
150
+ .catch((error) => console.warn(`Failed to leave group: ${error.message}`));
151
+ await this.cluster.disconnect().catch((error) => console.warn(`Failed to disconnect: ${error.message}`));
152
+ }
153
+ fetch(nodeId, assignment) {
154
+ const { rackId, maxWaitMs, minBytes, maxBytes, partitionMaxBytes, isolationLevel } = this.options;
155
+ return this.cluster.sendRequestToNode(nodeId)(api_1.API.FETCH, {
156
+ maxWaitMs,
157
+ minBytes,
158
+ maxBytes,
159
+ isolationLevel,
160
+ sessionId: 0,
161
+ sessionEpoch: -1,
162
+ topics: Object.entries(assignment).map(([topic, partitions]) => ({
163
+ topicId: this.metadata.getTopicIdByName(topic),
164
+ partitions: partitions.map((partition) => ({
165
+ partition,
166
+ currentLeaderEpoch: -1,
167
+ fetchOffset: this.offsetManager.getCurrentOffset(topic, partition),
168
+ lastFetchedEpoch: -1,
169
+ logStartOffset: 0n,
170
+ partitionMaxBytes,
171
+ })),
172
+ })),
173
+ forgottenTopicsData: [],
174
+ rackId,
175
+ });
176
+ }
177
+ }
178
+ exports.Consumer = Consumer;
@@ -0,0 +1,24 @@
1
+ import { IsolationLevel } from "../api/fetch";
2
+ import { Assignment } from "../api/sync-group";
3
+ import { Cluster } from "../cluster";
4
+ import { OffsetManager } from "./offset-manager";
5
+ export type Metadata = ReturnType<typeof createMetadata>;
6
+ type MetadataOptions = {
7
+ cluster: Cluster;
8
+ topics?: string[];
9
+ isolationLevel?: IsolationLevel;
10
+ allowTopicAutoCreation?: boolean;
11
+ fromBeginning?: boolean;
12
+ offsetManager?: OffsetManager;
13
+ };
14
+ export declare const createMetadata: ({ cluster, topics, isolationLevel, allowTopicAutoCreation, fromBeginning, offsetManager, }: MetadataOptions) => {
15
+ init: () => Promise<void>;
16
+ getTopicPartitions: () => Record<string, number[]>;
17
+ getTopicIdByName: (name: string) => string;
18
+ getTopicNameById: (id: string) => string;
19
+ getAssignment: () => Assignment;
20
+ setAssignment: (newAssignment: Assignment) => void;
21
+ getLeaderIdByTopicPartition: (topic: string, partition: number) => number;
22
+ getIsrNodeIdsByTopicPartition: (topic: string, partition: number) => number[];
23
+ };
24
+ export {};