@certik/skynet 0.10.4 → 0.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/kafka.js CHANGED
@@ -1,442 +1,443 @@
1
- const meow = require("meow");
2
- const { getEnvironment, getEnvOrThrow } = require("./env");
3
- const { wait } = require("./availability");
4
- const { Kafka, logLevel } = require("kafkajs");
5
- const { getSelectorFlags, getSelectorDesc, toSelectorString } = require("./selector");
6
- const { createRecord, getRecordByKey, deleteRecordsByHashKey } = require("./dynamodb");
7
- const { exponentialRetry } = require("./availability");
8
- const { getBinaryName } = require("./cli");
9
-
10
- const STATE_TABLE_NAME = "skynet-" + getEnvironment() + "-indexer-state";
11
-
12
- async function getProducerLatestId(name, selectorFlags) {
13
- const record = await getRecordByKey(STATE_TABLE_NAME, {
14
- name: `${name}At(${toSelectorString(selectorFlags)})`,
15
- });
16
-
17
- if (record) {
18
- return record.value;
19
- }
20
-
21
- return 0;
22
- }
23
-
24
- async function setProducerLatestId(name, selectorFlags, value) {
25
- await createRecord(STATE_TABLE_NAME, {
26
- name: `${name}At(${toSelectorString(selectorFlags)})`,
27
- value,
28
- });
29
- }
30
-
31
- async function deleteProducerLatestId(name, selectorFlags, verbose) {
32
- await deleteRecordsByHashKey(STATE_TABLE_NAME, null, `${name}At(${toSelectorString(selectorFlags)})`, verbose);
33
- }
34
-
35
- function sendToTopic(producer, topic, verbose) {
36
- async function send(records) {
37
- if (verbose) {
38
- console.log(`sending ${records.length} records to topic`, records);
39
- }
40
-
41
- await producer.send({
42
- topic,
43
- messages: records.map((r) => ({
44
- value: Buffer.from(typeof r === "string" ? r : JSON.stringify(r)),
45
- })),
46
- });
47
- }
48
-
49
- return send;
50
- }
51
-
52
- function getDefaultKafkaCredential() {
53
- return {
54
- server: getEnvOrThrow("SKYNET_KAFKA_SERVER"),
55
- username: getEnvOrThrow("SKYNET_KAFKA_USERNAME"),
56
- password: getEnvOrThrow("SKYNET_KAFKA_PASSWORD"),
57
- };
58
- }
59
-
60
- async function initProducer(clientId, customKafkaCredential = undefined) {
61
- const kafkaCredential = customKafkaCredential ?? getDefaultKafkaCredential();
62
- const kafka = new Kafka({
63
- clientId,
64
- brokers: [kafkaCredential.server],
65
- logLevel: logLevel.ERROR,
66
- ssl: true,
67
- sasl: {
68
- mechanism: "plain",
69
- username: kafkaCredential.username,
70
- password: kafkaCredential.password,
71
- },
72
- });
73
- const producerInstance = kafka.producer();
74
- await producerInstance.connect();
75
-
76
- return producerInstance;
77
- }
78
-
79
- async function closeProducer(producerInstance) {
80
- await producerInstance.disconnect();
81
- }
82
-
83
- async function initConsumer(clientId, customKafkaCredential = undefined) {
84
- const kafkaCredential = customKafkaCredential ? customKafkaCredential : getDefaultKafkaCredential();
85
- const kafka = new Kafka({
86
- clientId,
87
- brokers: [kafkaCredential.server],
88
- logLevel: logLevel.ERROR,
89
- ssl: true,
90
- sasl: {
91
- mechanism: "plain",
92
- username: kafkaCredential.username,
93
- password: kafkaCredential.password,
94
- },
95
- });
96
- const consumerInstance = kafka.consumer({ groupId: clientId });
97
-
98
- await consumerInstance.connect();
99
-
100
- return consumerInstance;
101
- }
102
-
103
- async function closeConsumer(consumerInstance) {
104
- await consumerInstance.disconnect();
105
- }
106
-
107
- async function produceMessages(producerId, callback) {
108
- const producerInstance = await initProducer(producerId);
109
- const send = async (topic, messages) => {
110
- let records;
111
- if (Array.isArray(messages)) {
112
- records = messages;
113
- } else {
114
- records = [messages];
115
- }
116
- return sendToTopic(producerInstance, topic, false)(records);
117
- };
118
- await callback(send);
119
- await closeProducer(producerInstance);
120
- }
121
-
122
- async function consumeMessages(consumerId, topic, callback) {
123
- const consumerInstance = await initConsumer(consumerId);
124
-
125
- await consumerInstance.subscribe({ topic });
126
-
127
- const stopConsumeMessages = async () => {
128
- await closeConsumer(consumerInstance);
129
- };
130
- await consumerInstance.run({
131
- eachMessage: async ({ topic: receivedTopic, message }) => {
132
- if (topic === receivedTopic) {
133
- await callback(message, stopConsumeMessages);
134
- }
135
- },
136
- });
137
- return stopConsumeMessages;
138
- }
139
-
140
- function createProducerApp({ binaryName, name, selector = {}, producer, state }) {
141
- function run() {
142
- if (!binaryName) {
143
- binaryName = getBinaryName();
144
- }
145
-
146
- const finalState = {
147
- type: "block",
148
- updateInterval: 5000,
149
- getMinId: async () => 1,
150
- getMaxId: async () => {
151
- throw new Error("must implement state.getMaxId");
152
- },
153
- ...state,
154
- };
155
-
156
- const cli = meow(
157
- `
158
- Usage
159
- $ ${binaryName} <options>
160
-
161
- Options
162
- ${getSelectorDesc(selector)}
163
- --from min ${finalState.type} to produce
164
- --to max ${finalState.type} to produce
165
- --status print current status and exit
166
- --verbose Output debug messages
167
- --reset restart producer
168
- `,
169
- {
170
- description: false,
171
- version: false,
172
- flags: {
173
- ...getSelectorFlags(selector),
174
- from: {
175
- alias: "since",
176
- type: "number",
177
- default: 0,
178
- },
179
- to: {
180
- alias: "until",
181
- type: "number",
182
- default: 0,
183
- },
184
- verbose: {
185
- type: "boolean",
186
- default: false,
187
- },
188
- },
189
- }
190
- );
191
-
192
- const { topic, deadLetterTopic, produce, batchSize, maxRetry } = {
193
- batchSize: 50,
194
- maxRetry: 2,
195
- ...producer,
196
- };
197
-
198
- const kafkaCredential = (producer && producer.kafkaCredential) || getDefaultKafkaCredential();
199
-
200
- async function build({ from, to, status, reset, verbose, ...selectorFlags }) {
201
- if (reset) {
202
- await deleteProducerLatestId(name, selectorFlags);
203
- if (finalState.onReset) {
204
- await finalState.onReset(selectorFlags, verbose);
205
- }
206
-
207
- console.log("producer reset");
208
-
209
- process.exit(0);
210
- }
211
-
212
- if (status) {
213
- const latestId = await getProducerLatestId(name, selectorFlags);
214
-
215
- console.log(`LatestID=${latestId}`);
216
-
217
- process.exit(0);
218
- }
219
-
220
- if (!from) {
221
- const prevId = await getProducerLatestId(name, selectorFlags);
222
-
223
- if (prevId) {
224
- from = prevId + 1;
225
- } else {
226
- // initial build
227
- from = await finalState.getMinId(selectorFlags);
228
- }
229
- }
230
-
231
- const finalTopic = typeof topic === "function" ? topic(selectorFlags) : topic;
232
- const finalDeadLetterTopic =
233
- typeof deadLetterTopic === "function" ? deadLetterTopic(selectorFlags) : deadLetterTopic;
234
-
235
- const updateInterval =
236
- typeof finalState.updateInterval === "function"
237
- ? finalState.updateInterval(selectorFlags)
238
- : finalState.updateInterval;
239
-
240
- const producerInstance = await initProducer(name, kafkaCredential);
241
-
242
- console.log("producing to topic", finalTopic);
243
-
244
- // if to flag was not provided, run this program indefinitely
245
- const isLongRunning = to === 0;
246
-
247
- let lastId = to > 0 ? to : await finalState.getMaxId(selectorFlags);
248
-
249
- let pollHandle;
250
-
251
- async function cleanUpForLongRunningMode() {
252
- clearInterval(pollHandle);
253
-
254
- await closeProducer(producerInstance);
255
-
256
- process.exit(0);
257
- }
258
-
259
- if (isLongRunning) {
260
- pollHandle = setInterval(async () => {
261
- lastId = await finalState.getMaxId(selectorFlags);
262
- }, updateInterval);
263
-
264
- // clean up before exit
265
- process.on("SIGINT", cleanUpForLongRunningMode);
266
- }
267
-
268
- // the main loop
269
- for (let i = from; isLongRunning || i <= lastId; ) {
270
- if (i > lastId) {
271
- console.log("no more items to process, sleep for", updateInterval, "ms");
272
-
273
- await wait(updateInterval);
274
- } else {
275
- const batchStart = i;
276
- const batchEnd = Math.min(batchStart + batchSize - 1, lastId);
277
-
278
- console.log(`building batch ${batchStart}~${batchEnd}`);
279
-
280
- // add a retry for errors
281
- const result = await exponentialRetry(
282
- async () => {
283
- try {
284
- const failed = await produce({
285
- ...selectorFlags,
286
- from: batchStart,
287
- to: batchEnd,
288
- verbose,
289
- send: sendToTopic(producerInstance, finalTopic, verbose),
290
- });
291
-
292
- if (Array.isArray(failed) && failed.length > 0) {
293
- await sendToTopic(producerInstance, finalDeadLetterTopic, verbose)(failed);
294
- }
295
-
296
- return true;
297
- } catch (err) {
298
- console.error(`Critical error processing batch ${batchStart}~${batchEnd}`, err);
299
-
300
- return false;
301
- }
302
- },
303
- {
304
- maxRetry,
305
- test: (r) => r,
306
- verbose,
307
- }
308
- );
309
-
310
- if (!result) {
311
- if (isLongRunning) {
312
- cleanUpForLongRunningMode();
313
- }
314
-
315
- throw new Error(`Terminate producer due to critical errors, batch ${batchStart}~${batchEnd}`);
316
- }
317
-
318
- await setProducerLatestId(name, selectorFlags, batchEnd);
319
-
320
- if (i + batchSize <= lastId) i += batchSize;
321
- else i = batchEnd + 1;
322
- }
323
-
324
- if (isLongRunning) {
325
- lastId = await finalState.getMaxId(selectorFlags);
326
- }
327
- }
328
-
329
- await closeProducer(producerInstance);
330
- }
331
-
332
- return build(cli.flags).catch((err) => {
333
- console.error(err);
334
- process.exit(1);
335
- });
336
- }
337
-
338
- return { run };
339
- }
340
-
341
- function createConsumerApp({ binaryName, name, selector = {}, consumer }) {
342
- function run() {
343
- if (!binaryName) {
344
- binaryName = getBinaryName();
345
- }
346
-
347
- const cli = meow(
348
- `
349
- Usage
350
- $ ${binaryName} <options>
351
-
352
- Options
353
- ${getSelectorDesc(selector)}
354
- --verbose Output debug messages
355
- `,
356
- {
357
- description: false,
358
- version: false,
359
- flags: {
360
- ...getSelectorFlags(selector),
361
- verbose: {
362
- type: "boolean",
363
- default: false,
364
- },
365
- },
366
- }
367
- );
368
-
369
- const { topic, consume, maxRetry } = {
370
- maxRetry: 2,
371
- ...consumer,
372
- };
373
-
374
- const kafkaCredential = (consume && consumer.kafkaCredential) || getDefaultKafkaCredential();
375
-
376
- async function build({ verbose, ...selectorFlags }) {
377
- const consumerInstance = await initConsumer(name, kafkaCredential);
378
-
379
- try {
380
- const finalTopic = typeof topic === "function" ? topic(selectorFlags) : topic;
381
-
382
- await consumerInstance.subscribe({ topic: finalTopic });
383
-
384
- console.log("subscribed to topic", finalTopic);
385
-
386
- await consumerInstance.run({
387
- eachBatch: async ({ batch }) => {
388
- if (verbose) {
389
- console.log("received batch, number of items =", batch.messages.length);
390
- }
391
-
392
- const messages = batch.messages.map((m) => JSON.parse(m.value));
393
-
394
- // add a retry for errors
395
- const result = await exponentialRetry(
396
- async () => {
397
- try {
398
- await consume({ ...selectorFlags, messages, verbose });
399
-
400
- return true;
401
- } catch (err) {
402
- console.error(`got critical error`, err);
403
-
404
- return false;
405
- }
406
- },
407
- {
408
- maxRetry,
409
- test: (r) => r,
410
- verbose,
411
- }
412
- );
413
-
414
- if (!result) {
415
- console.error("Terminate consumer due to consume errors, likely bug");
416
-
417
- process.exit(1);
418
- }
419
- },
420
- });
421
- } catch (err) {
422
- console.error("Terminate consumer due to critical kafka connection error", err);
423
-
424
- process.exit(1);
425
- }
426
- }
427
-
428
- return build(cli.flags).catch((err) => {
429
- console.error(err);
430
- process.exit(1);
431
- });
432
- }
433
-
434
- return { run };
435
- }
436
-
437
- module.exports = {
438
- createProducerApp,
439
- createConsumerApp,
440
- produceMessages,
441
- consumeMessages,
442
- };
1
+ const meow = require("meow");
2
+ const { getEnvironment, getEnvOrThrow } = require("./env");
3
+ const { wait } = require("./availability");
4
+ const { Kafka, logLevel } = require("kafkajs");
5
+ const { getSelectorFlags, getSelectorDesc, toSelectorString } = require("./selector");
6
+ const { createRecord, getRecordByKey, deleteRecordsByHashKey } = require("./dynamodb");
7
+ const { exponentialRetry } = require("./availability");
8
+ const { getBinaryName } = require("./cli");
9
+
10
+ const STATE_TABLE_NAME = "skynet-" + getEnvironment() + "-indexer-state";
11
+
12
+ async function getProducerLatestId(name, selectorFlags) {
13
+ const record = await getRecordByKey(STATE_TABLE_NAME, {
14
+ name: `${name}At(${toSelectorString(selectorFlags)})`,
15
+ });
16
+
17
+ if (record) {
18
+ return record.value;
19
+ }
20
+
21
+ return 0;
22
+ }
23
+
24
+ async function setProducerLatestId(name, selectorFlags, value) {
25
+ await createRecord(STATE_TABLE_NAME, {
26
+ name: `${name}At(${toSelectorString(selectorFlags)})`,
27
+ value,
28
+ });
29
+ }
30
+
31
+ async function deleteProducerLatestId(name, selectorFlags, verbose) {
32
+ await deleteRecordsByHashKey(STATE_TABLE_NAME, null, `${name}At(${toSelectorString(selectorFlags)})`, verbose);
33
+ }
34
+
35
+ function sendToTopic(producer, topic, verbose) {
36
+ async function send(records) {
37
+ if (verbose) {
38
+ console.log(`sending ${records.length} records to topic`, records);
39
+ }
40
+
41
+ await producer.send({
42
+ topic,
43
+ messages: records.map((r) => ({
44
+ value: Buffer.from(typeof r === "string" ? r : JSON.stringify(r)),
45
+ })),
46
+ });
47
+ }
48
+
49
+ return send;
50
+ }
51
+
52
+ function getDefaultKafkaCredential() {
53
+ return {
54
+ server: getEnvOrThrow("SKYNET_KAFKA_SERVER"),
55
+ username: getEnvOrThrow("SKYNET_KAFKA_USERNAME"),
56
+ password: getEnvOrThrow("SKYNET_KAFKA_PASSWORD"),
57
+ };
58
+ }
59
+
60
+ async function initProducer(clientId, customKafkaCredential = undefined) {
61
+ const kafkaCredential = customKafkaCredential ?? getDefaultKafkaCredential();
62
+ const kafka = new Kafka({
63
+ clientId,
64
+ brokers: [kafkaCredential.server],
65
+ logLevel: logLevel.ERROR,
66
+ ssl: true,
67
+ sasl: {
68
+ mechanism: "plain",
69
+ username: kafkaCredential.username,
70
+ password: kafkaCredential.password,
71
+ },
72
+ });
73
+ const producerInstance = kafka.producer();
74
+ await producerInstance.connect();
75
+
76
+ return producerInstance;
77
+ }
78
+
79
+ async function closeProducer(producerInstance) {
80
+ await producerInstance.disconnect();
81
+ }
82
+
83
+ async function initConsumer(clientId, customKafkaCredential = undefined) {
84
+ const kafkaCredential = customKafkaCredential ? customKafkaCredential : getDefaultKafkaCredential();
85
+ const kafka = new Kafka({
86
+ clientId,
87
+ brokers: [kafkaCredential.server],
88
+ logLevel: logLevel.ERROR,
89
+ ssl: true,
90
+ sasl: {
91
+ mechanism: "plain",
92
+ username: kafkaCredential.username,
93
+ password: kafkaCredential.password,
94
+ },
95
+ });
96
+ const consumerInstance = kafka.consumer({ groupId: clientId });
97
+
98
+ await consumerInstance.connect();
99
+
100
+ return consumerInstance;
101
+ }
102
+
103
+ async function closeConsumer(consumerInstance) {
104
+ await consumerInstance.disconnect();
105
+ }
106
+
107
+ async function produceMessages(producerId, callback) {
108
+ const producerInstance = await initProducer(producerId);
109
+ const send = async (topic, messages) => {
110
+ let records;
111
+ if (Array.isArray(messages)) {
112
+ records = messages;
113
+ } else {
114
+ records = [messages];
115
+ }
116
+ return sendToTopic(producerInstance, topic, false)(records);
117
+ };
118
+ await callback(send);
119
+ await closeProducer(producerInstance);
120
+ }
121
+
122
+ async function consumeMessages(consumerId, topic, callback) {
123
+ const consumerInstance = await initConsumer(consumerId);
124
+
125
+ await consumerInstance.subscribe({ topic });
126
+
127
+ const stopConsumeMessages = async () => {
128
+ await closeConsumer(consumerInstance);
129
+ };
130
+ await consumerInstance.run({
131
+ eachMessage: async ({ topic: receivedTopic, message }) => {
132
+ if (topic === receivedTopic) {
133
+ await callback(message, stopConsumeMessages);
134
+ }
135
+ },
136
+ });
137
+ return stopConsumeMessages;
138
+ }
139
+
140
+ function createProducerApp({ binaryName, name, selector = {}, producer, state }) {
141
+ function run() {
142
+ if (!binaryName) {
143
+ binaryName = getBinaryName();
144
+ }
145
+
146
+ const finalState = {
147
+ type: "block",
148
+ updateInterval: 5000,
149
+ getMinId: async () => 1,
150
+ getMaxId: async () => {
151
+ throw new Error("must implement state.getMaxId");
152
+ },
153
+ ...state,
154
+ };
155
+
156
+ const cli = meow(
157
+ `
158
+ Usage
159
+ $ ${binaryName} <options>
160
+
161
+ Options
162
+ ${getSelectorDesc(selector)}
163
+ --from min ${finalState.type} to produce
164
+ --to max ${finalState.type} to produce
165
+ --status print current status and exit
166
+ --verbose Output debug messages
167
+ --reset restart producer
168
+ `,
169
+ {
170
+ description: false,
171
+ version: false,
172
+ flags: {
173
+ ...getSelectorFlags(selector),
174
+ from: {
175
+ alias: "since",
176
+ type: "number",
177
+ default: 0,
178
+ },
179
+ to: {
180
+ alias: "until",
181
+ type: "number",
182
+ default: 0,
183
+ },
184
+ verbose: {
185
+ type: "boolean",
186
+ default: false,
187
+ },
188
+ },
189
+ }
190
+ );
191
+
192
+ const { topic, deadLetterTopic, produce, batchSize, maxRetry } = {
193
+ batchSize: 50,
194
+ maxRetry: 2,
195
+ ...producer,
196
+ };
197
+
198
+ const kafkaCredential = (producer && producer.kafkaCredential) || getDefaultKafkaCredential();
199
+
200
+ async function build({ from, to, status, reset, verbose, ...selectorFlags }) {
201
+ if (reset) {
202
+ await deleteProducerLatestId(name, selectorFlags);
203
+ if (finalState.onReset) {
204
+ await finalState.onReset(selectorFlags, verbose);
205
+ }
206
+
207
+ console.log("producer reset");
208
+
209
+ process.exit(0);
210
+ }
211
+
212
+ if (status) {
213
+ const latestId = await getProducerLatestId(name, selectorFlags);
214
+
215
+ console.log(`LatestID=${latestId}`);
216
+
217
+ process.exit(0);
218
+ }
219
+
220
+ if (!from) {
221
+ const prevId = await getProducerLatestId(name, selectorFlags);
222
+
223
+ if (prevId) {
224
+ from = prevId + 1;
225
+ } else {
226
+ // initial build
227
+ from = await finalState.getMinId(selectorFlags);
228
+ }
229
+ }
230
+
231
+ const finalTopic = typeof topic === "function" ? topic(selectorFlags) : topic;
232
+ const finalDeadLetterTopic =
233
+ typeof deadLetterTopic === "function" ? deadLetterTopic(selectorFlags) : deadLetterTopic;
234
+
235
+ const updateInterval =
236
+ typeof finalState.updateInterval === "function"
237
+ ? finalState.updateInterval(selectorFlags)
238
+ : finalState.updateInterval;
239
+
240
+ const producerInstance = await initProducer(name, kafkaCredential);
241
+
242
+ console.log("producing to topic", finalTopic);
243
+
244
+ // if to flag was not provided, run this program indefinitely
245
+ const isLongRunning = to === 0;
246
+
247
+ let lastId = to > 0 ? to : await finalState.getMaxId(selectorFlags);
248
+
249
+ let pollHandle;
250
+
251
+ async function cleanUpForLongRunningMode() {
252
+ clearInterval(pollHandle);
253
+
254
+ await closeProducer(producerInstance);
255
+
256
+ process.exit(0);
257
+ }
258
+
259
+ if (isLongRunning) {
260
+ pollHandle = setInterval(async () => {
261
+ lastId = await finalState.getMaxId(selectorFlags);
262
+ }, updateInterval);
263
+
264
+ // clean up before exit
265
+ process.on("SIGINT", cleanUpForLongRunningMode);
266
+ }
267
+
268
+ // the main loop
269
+ for (let i = from; isLongRunning || i <= lastId; ) {
270
+ if (i > lastId) {
271
+ console.log("no more items to process, sleep for", updateInterval, "ms");
272
+
273
+ await wait(updateInterval);
274
+ } else {
275
+ const batchStart = i;
276
+ const batchEnd = Math.min(batchStart + batchSize - 1, lastId);
277
+
278
+ console.log(`building batch ${batchStart}~${batchEnd}`);
279
+
280
+ // add a retry for errors
281
+ const result = await exponentialRetry(
282
+ async () => {
283
+ try {
284
+ const failed = await produce({
285
+ ...selectorFlags,
286
+ from: batchStart,
287
+ to: batchEnd,
288
+ verbose,
289
+ send: sendToTopic(producerInstance, finalTopic, verbose),
290
+ });
291
+
292
+ if (Array.isArray(failed) && failed.length > 0) {
293
+ await sendToTopic(producerInstance, finalDeadLetterTopic, verbose)(failed);
294
+ }
295
+
296
+ return true;
297
+ } catch (err) {
298
+ console.error(`Critical error processing batch ${batchStart}~${batchEnd}`, err);
299
+
300
+ return false;
301
+ }
302
+ },
303
+ {
304
+ maxRetry,
305
+ test: (r) => r,
306
+ verbose,
307
+ }
308
+ );
309
+
310
+ if (!result) {
311
+ if (isLongRunning) {
312
+ cleanUpForLongRunningMode();
313
+ }
314
+
315
+ throw new Error(`Terminate producer due to critical errors, batch ${batchStart}~${batchEnd}`);
316
+ }
317
+
318
+ await setProducerLatestId(name, selectorFlags, batchEnd);
319
+
320
+ if (i + batchSize <= lastId) i += batchSize;
321
+ else i = batchEnd + 1;
322
+ }
323
+
324
+ if (isLongRunning) {
325
+ lastId = await finalState.getMaxId(selectorFlags);
326
+ }
327
+ }
328
+
329
+ await closeProducer(producerInstance);
330
+ }
331
+
332
+ return build(cli.flags).catch((err) => {
333
+ console.error(err);
334
+ process.exit(1);
335
+ });
336
+ }
337
+
338
+ return { run };
339
+ }
340
+
341
+ function createConsumerApp({ binaryName, name, selector = {}, consumer }) {
342
+ function run() {
343
+ if (!binaryName) {
344
+ binaryName = getBinaryName();
345
+ }
346
+
347
+ const cli = meow(
348
+ `
349
+ Usage
350
+ $ ${binaryName} <options>
351
+
352
+ Options
353
+ ${getSelectorDesc(selector)}
354
+ --verbose Output debug messages
355
+ `,
356
+ {
357
+ description: false,
358
+ version: false,
359
+ flags: {
360
+ ...getSelectorFlags(selector),
361
+ verbose: {
362
+ type: "boolean",
363
+ default: false,
364
+ },
365
+ },
366
+ }
367
+ );
368
+
369
+ const { topic, consume, maxRetry } = {
370
+ maxRetry: 2,
371
+ ...consumer,
372
+ };
373
+
374
+ const kafkaCredential = (consume && consumer.kafkaCredential) || getDefaultKafkaCredential();
375
+
376
+ async function build({ verbose, ...selectorFlags }) {
377
+ const consumerInstance = await initConsumer(name, kafkaCredential);
378
+
379
+ try {
380
+ const finalTopic = typeof topic === "function" ? topic(selectorFlags) : topic;
381
+
382
+ await consumerInstance.subscribe({ topic: finalTopic });
383
+
384
+ console.log("subscribed to topic", finalTopic);
385
+
386
+ await consumerInstance.run({
387
+ eachBatch: async ({ batch }) => {
388
+ if (verbose) {
389
+ console.log("received batch, number of items =", batch.messages.length);
390
+ }
391
+
392
+ const messages = batch.messages.map((m) => JSON.parse(m.value));
393
+
394
+ // add a retry for errors
395
+ const result = await exponentialRetry(
396
+ async () => {
397
+ try {
398
+ await consume({ ...selectorFlags, messages, verbose });
399
+
400
+ return true;
401
+ } catch (err) {
402
+ console.error(`got critical error`, err);
403
+
404
+ return false;
405
+ }
406
+ },
407
+ {
408
+ maxRetry,
409
+ test: (r) => r,
410
+ verbose,
411
+ }
412
+ );
413
+
414
+ if (!result) {
415
+ console.error("Terminate consumer due to consume errors, likely bug");
416
+
417
+ process.exit(1);
418
+ }
419
+ },
420
+ });
421
+ } catch (err) {
422
+ console.error("Terminate consumer due to critical kafka connection error", err);
423
+
424
+ process.exit(1);
425
+ }
426
+ }
427
+
428
+ return build(cli.flags).catch((err) => {
429
+ console.error(err);
430
+ process.exit(1);
431
+ });
432
+ }
433
+
434
+ return { run };
435
+ }
436
+
437
+ module.exports = {
438
+ createProducerApp,
439
+ createConsumerApp,
440
+ produceMessages,
441
+ consumeMessages,
442
+ getProducerLatestId,
443
+ };