tina4-nodejs 3.13.43 → 3.13.44

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.
@@ -193,6 +193,13 @@ export class KafkaBackend implements QueueBackend {
193
193
 
194
194
  /**
195
195
  * Execute a Kafka operation synchronously via a child process.
196
+ *
197
+ * The wire protocol is hand-rolled (Tina4 is zero-dependency — no npm Kafka
198
+ * library). Produce uses Produce **v3** carrying a Kafka **v2 RecordBatch**
199
+ * (magic byte 2) with a **CRC-32C** (Castagnoli) checksum; Fetch uses Fetch
200
+ * **v4** and parses the v2 RecordBatch out of the response. Both formats are
201
+ * what a modern KRaft broker (apache/kafka 3.7.0) requires — the old
202
+ * Produce-v0 / message-format-v0 / CRC=0 batch is rejected by such brokers.
196
203
  */
197
204
  private execSync(operation: string, topic: string, data?: string): string {
198
205
  // execFileSync imported at top level
@@ -206,213 +213,342 @@ export class KafkaBackend implements QueueBackend {
206
213
  const topic = ${JSON.stringify(topic)};
207
214
  const groupId = ${JSON.stringify(this.groupId)};
208
215
  const data = ${JSON.stringify(data ?? "")};
216
+ const API_PRODUCE = ${API_PRODUCE};
217
+ const API_FETCH = ${API_FETCH};
218
+ const PRODUCE_VERSION = 3; // v3+ requires the v2 RecordBatch format
219
+ const FETCH_VERSION = 4; // v4 returns v2 RecordBatches + isolation_level
209
220
  let correlationId = 0;
210
221
 
211
- // Kafka wire protocol helpers
212
- function writeInt32(buf, offset, val) {
213
- buf.writeInt32BE(val, offset);
214
- return offset + 4;
222
+ // ── CRC-32C (Castagnoli, polynomial 0x1EDC6F41), table-based ──────────
223
+ // Node has no built-in CRC-32C; the v2 RecordBatch mandates it over the
224
+ // bytes from \`attributes\` to the end of the batch. A wrong CRC makes the
225
+ // broker drop the connection, so this must be exact.
226
+ const CRC32C_TABLE = (() => {
227
+ const table = new Uint32Array(256);
228
+ for (let n = 0; n < 256; n++) {
229
+ let c = n;
230
+ for (let k = 0; k < 8; k++) {
231
+ c = (c & 1) ? (0x82f63b78 ^ (c >>> 1)) : (c >>> 1);
232
+ }
233
+ table[n] = c >>> 0;
234
+ }
235
+ return table;
236
+ })();
237
+ function crc32c(buf) {
238
+ let crc = 0xffffffff;
239
+ for (let i = 0; i < buf.length; i++) {
240
+ crc = (CRC32C_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8)) >>> 0;
241
+ }
242
+ return (crc ^ 0xffffffff) >>> 0;
215
243
  }
216
- function writeInt16(buf, offset, val) {
217
- buf.writeInt16BE(val, offset);
218
- return offset + 2;
244
+
245
+ // ── Kafka protocol varints (zigzag-encoded signed varints) ────────────
246
+ function encodeZigZag(n) {
247
+ // 32-bit zigzag: (n << 1) ^ (n >> 31)
248
+ return ((n << 1) ^ (n >> 31)) >>> 0;
219
249
  }
220
- function writeString(buf, offset, str) {
221
- if (str === null) {
222
- buf.writeInt16BE(-1, offset);
223
- return offset + 2;
250
+ function encodeVarintUnsigned(u) {
251
+ const bytes = [];
252
+ let v = u >>> 0;
253
+ while (true) {
254
+ if ((v & ~0x7f) === 0) { bytes.push(v); break; }
255
+ bytes.push((v & 0x7f) | 0x80);
256
+ v >>>= 7;
224
257
  }
225
- const len = Buffer.byteLength(str, "utf-8");
226
- buf.writeInt16BE(len, offset);
227
- buf.write(str, offset + 2, len, "utf-8");
228
- return offset + 2 + len;
258
+ return Buffer.from(bytes);
229
259
  }
230
- function writeBytes(buf, offset, bytes) {
231
- if (bytes === null) {
232
- buf.writeInt32BE(-1, offset);
233
- return offset + 4;
234
- }
235
- buf.writeInt32BE(bytes.length, offset);
236
- bytes.copy(buf, offset + 4);
237
- return offset + 4 + bytes.length;
260
+ function encodeVarint(n) {
261
+ // signed varint = zigzag then unsigned-varint
262
+ return encodeVarintUnsigned(encodeZigZag(n));
263
+ }
264
+ function decodeVarint(buf, pos) {
265
+ // returns { value, next } — signed (zigzag-decoded)
266
+ let result = 0, shift = 0, b;
267
+ do {
268
+ b = buf[pos++];
269
+ result |= (b & 0x7f) << shift;
270
+ shift += 7;
271
+ } while (b & 0x80);
272
+ result = result >>> 0;
273
+ // zigzag decode
274
+ const value = (result >>> 1) ^ -(result & 1);
275
+ return { value, next: pos };
238
276
  }
239
277
 
240
- function buildProduceRequest(topicName, messageBytes) {
278
+ // ── v2 RecordBatch builder ────────────────────────────────────────────
279
+ function buildRecordBatch(valueBytes) {
280
+ const now = BigInt(Date.now());
281
+
282
+ // Build the single record body.
283
+ // record = attributes:int8(0), timestampDelta:varint(0),
284
+ // offsetDelta:varint(0), keyLen:varint(-1 null),
285
+ // valueLen:varint(len), value, headerCount:varint(0)
286
+ const recBody = Buffer.concat([
287
+ Buffer.from([0]), // attributes (int8)
288
+ encodeVarint(0), // timestampDelta
289
+ encodeVarint(0), // offsetDelta
290
+ encodeVarint(-1), // keyLen (-1 = null key)
291
+ encodeVarint(valueBytes.length), // valueLen
292
+ valueBytes, // value (JSON payload bytes)
293
+ encodeVarint(0), // headerCount
294
+ ]);
295
+ // record length prefix = signed varint of recBody length
296
+ const record = Buffer.concat([encodeVarint(recBody.length), recBody]);
297
+ const recordsCount = 1;
298
+
299
+ // The portion the CRC covers starts at \`attributes\` and runs to end.
300
+ // crcBody = attributes:int16(0), lastOffsetDelta:int32(count-1),
301
+ // firstTimestamp:int64, maxTimestamp:int64,
302
+ // producerId:int64(-1), producerEpoch:int16(-1),
303
+ // baseSequence:int32(-1), recordsCount:int32, records
304
+ const crcBody = Buffer.alloc(2 + 4 + 8 + 8 + 8 + 2 + 4 + 4 + record.length);
305
+ let c = 0;
306
+ crcBody.writeInt16BE(0, c); c += 2; // attributes
307
+ crcBody.writeInt32BE(recordsCount - 1, c); c += 4; // lastOffsetDelta
308
+ crcBody.writeBigInt64BE(now, c); c += 8; // firstTimestamp
309
+ crcBody.writeBigInt64BE(now, c); c += 8; // maxTimestamp
310
+ crcBody.writeBigInt64BE(-1n, c); c += 8; // producerId
311
+ crcBody.writeInt16BE(-1, c); c += 2; // producerEpoch
312
+ crcBody.writeInt32BE(-1, c); c += 4; // baseSequence
313
+ crcBody.writeInt32BE(recordsCount, c); c += 4; // recordsCount
314
+ record.copy(crcBody, c);
315
+
316
+ const crc = crc32c(crcBody);
317
+
318
+ // Header before the CRC-covered region:
319
+ // baseOffset:int64(0), batchLength:int32, partitionLeaderEpoch:int32(-1),
320
+ // magic:int8(2), crc:uint32. batchLength counts everything AFTER itself
321
+ // (partitionLeaderEpoch through end of records) = 4 + 1 + 4 + crcBody.
322
+ const batchLength = 4 + 1 + 4 + crcBody.length;
323
+ const head = Buffer.alloc(8 + 4 + 4 + 1 + 4);
324
+ let h = 0;
325
+ head.writeBigInt64BE(0n, h); h += 8; // baseOffset
326
+ head.writeInt32BE(batchLength, h); h += 4; // batchLength
327
+ head.writeInt32BE(-1, h); h += 4; // partitionLeaderEpoch
328
+ head.writeInt8(2, h); h += 1; // magic = 2
329
+ head.writeUInt32BE(crc, h); h += 4; // crc (CRC-32C)
330
+
331
+ return Buffer.concat([head, crcBody]);
332
+ }
333
+
334
+ // ── Produce v3 request ─────────────────────────────────────────────────
335
+ function buildProduceRequest(topicName, valueBytes) {
241
336
  correlationId++;
242
- const clientId = "tina4";
337
+ const clientBuf = Buffer.from("tina4", "utf-8");
243
338
  const topicBuf = Buffer.from(topicName, "utf-8");
244
- const clientBuf = Buffer.from(clientId, "utf-8");
245
-
246
- // Build message set (MessageV0)
247
- const msgSize = 4 + 1 + 1 + 4 + 4 + messageBytes.length; // crc + magic + attrs + key(-1) + value
248
- const msgBuf = Buffer.alloc(12 + msgSize); // offset(8) + size(4) + message
249
- let o = 0;
250
- // Offset (8 bytes, 0 for produce)
251
- msgBuf.writeBigInt64BE(0n, o); o += 8;
252
- // Message size
253
- msgBuf.writeInt32BE(msgSize, o); o += 4;
254
- // CRC placeholder (will be 0 Kafka accepts for some versions)
255
- msgBuf.writeInt32BE(0, o); o += 4;
256
- // Magic byte
257
- msgBuf.writeInt8(0, o); o += 1;
258
- // Attributes
259
- msgBuf.writeInt8(0, o); o += 1;
260
- // Key (null = -1)
261
- msgBuf.writeInt32BE(-1, o); o += 4;
262
- // Value
263
- msgBuf.writeInt32BE(messageBytes.length, o); o += 4;
264
- messageBytes.copy(msgBuf, o); o += messageBytes.length;
265
-
266
- // Build request
267
- const reqSize = 2 + 2 + 4 + 2 + clientBuf.length + 2 + 4 + 4 + 2 + topicBuf.length + 4 + 4 + 4 + msgBuf.length;
268
- const req = Buffer.alloc(4 + reqSize);
269
- let pos = 0;
270
- req.writeInt32BE(reqSize, pos); pos += 4;
271
- // API key (Produce = 0)
272
- req.writeInt16BE(API_PRODUCE, pos); pos += 2;
273
- // API version
274
- req.writeInt16BE(0, pos); pos += 2;
275
- // Correlation ID
276
- req.writeInt32BE(correlationId, pos); pos += 4;
277
- // Client ID
278
- req.writeInt16BE(clientBuf.length, pos); pos += 2;
279
- clientBuf.copy(req, pos); pos += clientBuf.length;
280
- // Required acks
281
- req.writeInt16BE(1, pos); pos += 2;
282
- // Timeout
283
- req.writeInt32BE(5000, pos); pos += 4;
284
- // Topic count
285
- req.writeInt32BE(1, pos); pos += 4;
286
- // Topic name
287
- req.writeInt16BE(topicBuf.length, pos); pos += 2;
288
- topicBuf.copy(req, pos); pos += topicBuf.length;
289
- // Partition count
290
- req.writeInt32BE(1, pos); pos += 4;
291
- // Partition index
292
- req.writeInt32BE(0, pos); pos += 4;
293
- // Message set size
294
- req.writeInt32BE(msgBuf.length, pos); pos += 4;
295
- msgBuf.copy(req, pos);
296
-
297
- return req;
339
+ const recordBatch = buildRecordBatch(valueBytes);
340
+
341
+ const body = Buffer.alloc(
342
+ 2 + // transactionalId (-1 null nullable_string)
343
+ 2 + // acks
344
+ 4 + // timeoutMs
345
+ 4 + // topics array count
346
+ 2 + topicBuf.length + // topic name
347
+ 4 + // partitions array count
348
+ 4 + // partition index
349
+ 4 + recordBatch.length // recordSetBytes (int32 size) + batch
350
+ );
351
+ let p = 0;
352
+ body.writeInt16BE(-1, p); p += 2; // transactionalId = null
353
+ body.writeInt16BE(1, p); p += 2; // acks = 1
354
+ body.writeInt32BE(10000, p); p += 4; // timeoutMs
355
+ body.writeInt32BE(1, p); p += 4; // topics count
356
+ body.writeInt16BE(topicBuf.length, p); p += 2;
357
+ topicBuf.copy(body, p); p += topicBuf.length;
358
+ body.writeInt32BE(1, p); p += 4; // partitions count
359
+ body.writeInt32BE(0, p); p += 4; // partition index 0
360
+ body.writeInt32BE(recordBatch.length, p); p += 4; // recordSetBytes size
361
+ recordBatch.copy(body, p);
362
+
363
+ return frameRequest(API_PRODUCE, PRODUCE_VERSION, clientBuf, body);
298
364
  }
299
365
 
366
+ // ── Fetch v4 request ────────────────────────────────────────────────────
300
367
  function buildFetchRequest(topicName, fetchOffset) {
301
368
  correlationId++;
302
- const clientId = "tina4";
369
+ const clientBuf = Buffer.from("tina4", "utf-8");
303
370
  const topicBuf = Buffer.from(topicName, "utf-8");
304
- const clientBuf = Buffer.from(clientId, "utf-8");
305
-
306
- const reqSize = 2 + 2 + 4 + 2 + clientBuf.length + 4 + 4 + 4 + 4 + 2 + topicBuf.length + 4 + 4 + 8 + 4;
307
- const req = Buffer.alloc(4 + reqSize);
308
- let pos = 0;
309
- req.writeInt32BE(reqSize, pos); pos += 4;
310
- req.writeInt16BE(API_FETCH, pos); pos += 2;
311
- req.writeInt16BE(0, pos); pos += 2;
312
- req.writeInt32BE(correlationId, pos); pos += 4;
313
- req.writeInt16BE(clientBuf.length, pos); pos += 2;
314
- clientBuf.copy(req, pos); pos += clientBuf.length;
315
- // Replica ID (-1 for consumer)
316
- req.writeInt32BE(-1, pos); pos += 4;
317
- // Max wait time
318
- req.writeInt32BE(1000, pos); pos += 4;
319
- // Min bytes
320
- req.writeInt32BE(1, pos); pos += 4;
321
- // Topic count
322
- req.writeInt32BE(1, pos); pos += 4;
323
- // Topic name
324
- req.writeInt16BE(topicBuf.length, pos); pos += 2;
325
- topicBuf.copy(req, pos); pos += topicBuf.length;
326
- // Partition count
327
- req.writeInt32BE(1, pos); pos += 4;
328
- // Partition
329
- req.writeInt32BE(0, pos); pos += 4;
330
- // Fetch offset
331
- req.writeBigInt64BE(BigInt(fetchOffset), pos); pos += 8;
332
- // Max bytes
333
- req.writeInt32BE(1048576, pos); pos += 4;
334
-
335
- return req;
371
+
372
+ // Fetch v4 request body (NO logStartOffset — that arrives in v5+):
373
+ // replicaId(4), maxWaitMs(4), minBytes(4), maxBytes(4 v3+),
374
+ // isolationLevel(1 v4+), topics[count]: name, partitions[count]:
375
+ // partition(4), fetchOffset(8), partitionMaxBytes(4).
376
+ const buf = Buffer.alloc(4 + 4 + 4 + 4 + 1 + 4 + (2 + topicBuf.length) + 4 + 4 + 8 + 4);
377
+ let p = 0;
378
+ buf.writeInt32BE(-1, p); p += 4; // replicaId (-1 consumer)
379
+ buf.writeInt32BE(1000, p); p += 4; // maxWaitMs
380
+ buf.writeInt32BE(1, p); p += 4; // minBytes
381
+ buf.writeInt32BE(1048576, p); p += 4; // maxBytes
382
+ buf.writeInt8(0, p); p += 1; // isolationLevel = READ_UNCOMMITTED
383
+ buf.writeInt32BE(1, p); p += 4; // topics count
384
+ buf.writeInt16BE(topicBuf.length, p); p += 2;
385
+ topicBuf.copy(buf, p); p += topicBuf.length;
386
+ buf.writeInt32BE(1, p); p += 4; // partitions count
387
+ buf.writeInt32BE(0, p); p += 4; // partition 0
388
+ buf.writeBigInt64BE(BigInt(fetchOffset), p); p += 8; // fetchOffset
389
+ buf.writeInt32BE(1048576, p); p += 4; // partitionMaxBytes
390
+
391
+ return frameRequest(API_FETCH, FETCH_VERSION, clientBuf, buf);
392
+ }
393
+
394
+ // Frame a request with header v1: apiKey, apiVersion, correlationId,
395
+ // clientId (nullable string) + body.
396
+ function frameRequest(apiKey, apiVersion, clientBuf, body) {
397
+ const header = Buffer.alloc(2 + 2 + 4 + 2 + clientBuf.length);
398
+ let p = 0;
399
+ header.writeInt16BE(apiKey, p); p += 2;
400
+ header.writeInt16BE(apiVersion, p); p += 2;
401
+ header.writeInt32BE(correlationId, p); p += 4;
402
+ header.writeInt16BE(clientBuf.length, p); p += 2;
403
+ clientBuf.copy(header, p);
404
+ const payload = Buffer.concat([header, body]);
405
+ const framed = Buffer.alloc(4 + payload.length);
406
+ framed.writeInt32BE(payload.length, 0);
407
+ payload.copy(framed, 4);
408
+ return framed;
409
+ }
410
+
411
+ // ── Parse a v2 RecordBatch region, return the FIRST record value ───────
412
+ // \`buf\` is the whole fetch response; [start, end) bounds the partition's
413
+ // record-set bytes (may contain one or more concatenated batches).
414
+ function firstRecordValue(buf, start, end) {
415
+ let pos = start;
416
+ while (pos + 12 <= end) {
417
+ // baseOffset(8) + batchLength(4) then batchLength bytes follow.
418
+ const batchLength = buf.readInt32BE(pos + 8);
419
+ const batchStart = pos + 12; // first byte of partitionLeaderEpoch
420
+ const batchEnd = batchStart + batchLength;
421
+ if (batchLength <= 0 || batchEnd > end) break;
422
+ // partitionLeaderEpoch(4), magic(1), crc(4), attributes(2),
423
+ // lastOffsetDelta(4), firstTimestamp(8), maxTimestamp(8),
424
+ // producerId(8), producerEpoch(2), baseSequence(4), recordsCount(4)
425
+ const magic = buf.readInt8(batchStart + 4);
426
+ if (magic !== 2) { pos = batchEnd; continue; }
427
+ let r = batchStart + 4 + 1 + 4 + 2 + 4 + 8 + 8 + 8 + 2 + 4;
428
+ const recordsCount = buf.readInt32BE(r); r += 4;
429
+ for (let i = 0; i < recordsCount && r < batchEnd; i++) {
430
+ const recLen = decodeVarint(buf, r); // record length (signed varint)
431
+ let rp = recLen.next;
432
+ const recordEnd = rp + recLen.value;
433
+ rp += 1; // attributes (int8)
434
+ const tsDelta = decodeVarint(buf, rp); rp = tsDelta.next;
435
+ const offDelta = decodeVarint(buf, rp); rp = offDelta.next;
436
+ const keyLen = decodeVarint(buf, rp); rp = keyLen.next;
437
+ if (keyLen.value >= 0) rp += keyLen.value;
438
+ const valLen = decodeVarint(buf, rp); rp = valLen.next;
439
+ if (valLen.value >= 0) {
440
+ return buf.subarray(rp, rp + valLen.value).toString("utf-8");
441
+ }
442
+ r = recordEnd;
443
+ }
444
+ pos = batchEnd;
445
+ }
446
+ return null;
447
+ }
448
+
449
+ let finished = false;
450
+ function finish(out, code) {
451
+ if (finished) return;
452
+ finished = true;
453
+ clearTimeout(timer);
454
+ try { sock.destroy(); } catch (e) { /* ignore */ }
455
+ if (out) process.stdout.write(out);
456
+ // Flush stdout before exiting so execFileSync captures it.
457
+ process.stdout.write("", () => process.exit(code));
336
458
  }
337
459
 
338
460
  const sock = net.createConnection({ host, port }, () => {
339
461
  if (operation === "publish") {
340
- const msgBytes = Buffer.from(data, "utf-8");
341
- const req = buildProduceRequest(topic, msgBytes);
462
+ const req = buildProduceRequest(topic, Buffer.from(data, "utf-8"));
342
463
  sock.write(req);
343
464
  } else if (operation === "get") {
344
- const req = buildFetchRequest(topic, 0);
345
- sock.write(req);
465
+ sock.write(buildFetchRequest(topic, 0));
346
466
  } else {
347
- process.stdout.write("__UNSUPPORTED__");
348
- sock.destroy();
467
+ finish("__UNSUPPORTED__", 0);
349
468
  }
350
469
  });
351
470
 
352
471
  let buffer = Buffer.alloc(0);
353
472
  sock.on("data", (chunk) => {
354
473
  buffer = Buffer.concat([buffer, chunk]);
474
+ if (buffer.length < 4) return;
475
+ const respSize = buffer.readInt32BE(0);
476
+ if (buffer.length < 4 + respSize) return;
355
477
 
356
- if (buffer.length >= 4) {
357
- const respSize = buffer.readInt32BE(0);
358
- if (buffer.length >= 4 + respSize) {
359
- if (operation === "publish") {
360
- process.stdout.write("__PUBLISHED__");
361
- } else if (operation === "get") {
362
- // Parse fetch response to extract message value
363
- try {
364
- // Skip response header and topic metadata to find message
365
- let pos = 4 + 4; // size + correlation_id
366
- const topicCount = buffer.readInt32BE(pos); pos += 4;
367
- if (topicCount > 0) {
368
- const topicLen = buffer.readInt16BE(pos); pos += 2 + topicLen;
369
- const partCount = buffer.readInt32BE(pos); pos += 4;
370
- if (partCount > 0) {
371
- const partId = buffer.readInt32BE(pos); pos += 4;
372
- const errCode = buffer.readInt16BE(pos); pos += 2;
373
- const hwm = buffer.readBigInt64BE(pos); pos += 8;
374
- const msgSetSize = buffer.readInt32BE(pos); pos += 4;
375
-
376
- if (msgSetSize > 0 && errCode === 0) {
377
- // Parse first message in message set
378
- const msgOffset = buffer.readBigInt64BE(pos); pos += 8;
379
- const msgSize = buffer.readInt32BE(pos); pos += 4;
380
- const crc = buffer.readInt32BE(pos); pos += 4;
381
- const magic = buffer.readInt8(pos); pos += 1;
382
- const attrs = buffer.readInt8(pos); pos += 1;
383
- const keyLen = buffer.readInt32BE(pos); pos += 4;
384
- if (keyLen > 0) pos += keyLen;
385
- const valLen = buffer.readInt32BE(pos); pos += 4;
386
- if (valLen > 0) {
387
- const val = buffer.subarray(pos, pos + valLen).toString("utf-8");
388
- process.stdout.write(val);
389
- } else {
390
- process.stdout.write("__EMPTY__");
391
- }
392
- } else {
393
- process.stdout.write("__EMPTY__");
394
- }
395
- } else {
396
- process.stdout.write("__EMPTY__");
397
- }
398
- } else {
399
- process.stdout.write("__EMPTY__");
478
+ if (operation === "publish") {
479
+ // Produce v3 response (after the 4-byte frame size): correlationId(4),
480
+ // topics[count]: name, partitions[count]: partition(4), errorCode(2),
481
+ // baseOffset(8), logAppendTime(8); then throttleTimeMs(4) at the END.
482
+ // Only the per-partition error code matters here.
483
+ try {
484
+ let pos = 4 + 4; // skip frame size + correlationId
485
+ const topicCount = buffer.readInt32BE(pos); pos += 4;
486
+ let errCode = 0;
487
+ for (let t = 0; t < topicCount; t++) {
488
+ const tl = buffer.readInt16BE(pos); pos += 2 + tl;
489
+ const pc = buffer.readInt32BE(pos); pos += 4;
490
+ for (let pi = 0; pi < pc; pi++) {
491
+ pos += 4; // partition index
492
+ errCode = buffer.readInt16BE(pos); pos += 2; // error code
493
+ pos += 8; // baseOffset
494
+ pos += 8; // logAppendTime (v2+)
495
+ }
496
+ }
497
+ if (errCode === 0) {
498
+ finish("__PUBLISHED__", 0);
499
+ } else {
500
+ process.stderr.write("Produce error code " + errCode);
501
+ finish("__ERROR__" + errCode, 0);
502
+ }
503
+ } catch (e) {
504
+ process.stderr.write("produce parse: " + e.message);
505
+ finish("__ERROR__", 0);
506
+ }
507
+ return;
508
+ } else if (operation === "get") {
509
+ // Fetch v4 response (after the 4-byte frame size): correlationId(4),
510
+ // throttleTimeMs(4), topics[count]: name, partitions[count]:
511
+ // partition(4), errorCode(2), highWatermark(8), lastStableOffset(8),
512
+ // abortedTxns[count](producerId(8)+firstOffset(8)),
513
+ // recordSetBytes(int32)+batch.
514
+ try {
515
+ let pos = 4 + 4; // frame size + correlationId
516
+ pos += 4; // throttleTimeMs (v1+)
517
+ const topicCount = buffer.readInt32BE(pos); pos += 4;
518
+ let out = "__EMPTY__";
519
+ for (let t = 0; t < topicCount; t++) {
520
+ const tl = buffer.readInt16BE(pos); pos += 2 + tl;
521
+ const pc = buffer.readInt32BE(pos); pos += 4;
522
+ for (let pi = 0; pi < pc; pi++) {
523
+ pos += 4; // partition index
524
+ const errCode = buffer.readInt16BE(pos); pos += 2;
525
+ pos += 8; // highWatermark
526
+ pos += 8; // lastStableOffset (v4+)
527
+ const abortedCount = buffer.readInt32BE(pos); pos += 4;
528
+ if (abortedCount > 0) pos += abortedCount * 16; // (-1 => none, skip)
529
+ const recSetSize = buffer.readInt32BE(pos); pos += 4;
530
+ if (errCode === 0 && recSetSize > 0) {
531
+ const val = firstRecordValue(buffer, pos, pos + recSetSize);
532
+ if (val !== null) out = val;
400
533
  }
401
- } catch (e) {
402
- process.stdout.write("__EMPTY__");
534
+ pos += recSetSize > 0 ? recSetSize : 0;
403
535
  }
404
536
  }
405
- sock.destroy();
537
+ finish(out, 0);
538
+ } catch (e) {
539
+ process.stderr.write("fetch parse: " + e.message);
540
+ finish("__EMPTY__", 0);
406
541
  }
542
+ return;
407
543
  }
408
544
  });
409
545
 
410
546
  sock.on("error", (err) => {
411
547
  process.stderr.write(err.message);
412
- process.exit(1);
548
+ finish("", 1);
413
549
  });
414
550
 
415
- setTimeout(() => { sock.destroy(); process.exit(1); }, 10000);
551
+ var timer = setTimeout(() => { finish("", 1); }, 10000);
416
552
  `;
417
553
 
418
554
  try {