@laserdata/laser-sdk 0.4.1 → 0.5.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 (87) hide show
  1. package/README.md +42 -2
  2. package/dist/agent/reliable-consumer.d.ts.map +1 -1
  3. package/dist/agent/reliable-consumer.js +5 -1
  4. package/dist/agent/reliable-consumer.js.map +1 -1
  5. package/dist/bridges/a2a.d.ts +1 -1
  6. package/dist/bridges/a2a.js +1 -1
  7. package/dist/client/capabilities.d.ts +15 -2
  8. package/dist/client/capabilities.d.ts.map +1 -1
  9. package/dist/client/capabilities.js +22 -1
  10. package/dist/client/capabilities.js.map +1 -1
  11. package/dist/client/errors.d.ts +23 -1
  12. package/dist/client/errors.d.ts.map +1 -1
  13. package/dist/client/errors.js +34 -0
  14. package/dist/client/errors.js.map +1 -1
  15. package/dist/client/laser.d.ts +6 -0
  16. package/dist/client/laser.d.ts.map +1 -1
  17. package/dist/client/laser.js +25 -0
  18. package/dist/client/laser.js.map +1 -1
  19. package/dist/client/managed.d.ts +1 -0
  20. package/dist/client/managed.d.ts.map +1 -1
  21. package/dist/client/managed.js +15 -2
  22. package/dist/client/managed.js.map +1 -1
  23. package/dist/iggy/apache-iggy.d.ts +63 -2
  24. package/dist/iggy/apache-iggy.d.ts.map +1 -1
  25. package/dist/iggy/apache-iggy.js +343 -14
  26. package/dist/iggy/apache-iggy.js.map +1 -1
  27. package/dist/index.d.ts +8 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/managed/filters.d.ts +358 -0
  32. package/dist/managed/filters.d.ts.map +1 -0
  33. package/dist/managed/filters.js +1913 -0
  34. package/dist/managed/filters.js.map +1 -0
  35. package/dist/stream/consumer.d.ts +11 -1
  36. package/dist/stream/consumer.d.ts.map +1 -1
  37. package/dist/stream/consumer.js +82 -9
  38. package/dist/stream/consumer.js.map +1 -1
  39. package/dist/stream/topic.d.ts +2 -0
  40. package/dist/stream/topic.d.ts.map +1 -1
  41. package/dist/stream/topic.js +6 -0
  42. package/dist/stream/topic.js.map +1 -1
  43. package/dist/wire/authz.d.ts +1 -1
  44. package/dist/wire/authz.d.ts.map +1 -1
  45. package/dist/wire/authz.js +12 -3
  46. package/dist/wire/authz.js.map +1 -1
  47. package/dist/wire/codes.d.ts +17 -0
  48. package/dist/wire/codes.d.ts.map +1 -1
  49. package/dist/wire/codes.js +17 -0
  50. package/dist/wire/codes.js.map +1 -1
  51. package/dist/wire/commands.d.ts +12 -1
  52. package/dist/wire/commands.d.ts.map +1 -1
  53. package/dist/wire/commands.js +13 -1
  54. package/dist/wire/commands.js.map +1 -1
  55. package/dist/wire/control.d.ts +4 -0
  56. package/dist/wire/control.d.ts.map +1 -1
  57. package/dist/wire/control.js +5 -0
  58. package/dist/wire/control.js.map +1 -1
  59. package/dist/wire/filter-codecs.d.ts +16 -0
  60. package/dist/wire/filter-codecs.d.ts.map +1 -0
  61. package/dist/wire/filter-codecs.js +519 -0
  62. package/dist/wire/filter-codecs.js.map +1 -0
  63. package/dist/wire/filter-eval.d.ts +75 -0
  64. package/dist/wire/filter-eval.d.ts.map +1 -0
  65. package/dist/wire/filter-eval.js +899 -0
  66. package/dist/wire/filter-eval.js.map +1 -0
  67. package/dist/wire/filter.d.ts +772 -0
  68. package/dist/wire/filter.d.ts.map +1 -0
  69. package/dist/wire/filter.js +2869 -0
  70. package/dist/wire/filter.js.map +1 -0
  71. package/dist/wire/hello.d.ts +18 -0
  72. package/dist/wire/hello.d.ts.map +1 -1
  73. package/dist/wire/hello.js +29 -3
  74. package/dist/wire/hello.js.map +1 -1
  75. package/dist/wire/http.d.ts +22 -0
  76. package/dist/wire/http.d.ts.map +1 -1
  77. package/dist/wire/http.js +54 -47
  78. package/dist/wire/http.js.map +1 -1
  79. package/dist/wire/limits.d.ts +25 -0
  80. package/dist/wire/limits.d.ts.map +1 -1
  81. package/dist/wire/limits.js +25 -0
  82. package/dist/wire/limits.js.map +1 -1
  83. package/dist/wire/result.d.ts +4 -0
  84. package/dist/wire/result.d.ts.map +1 -1
  85. package/dist/wire/result.js +50 -0
  86. package/dist/wire/result.js.map +1 -1
  87. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
1
  import { connectOptions } from "../client/connect-options.js";
2
2
  import { publishOptions, publishWithin } from "../client/publish-options.js";
3
- import { Consumer, DeserializeError, HeaderKeyFactory, HeaderValue as IggyHeaderValueFactory, Partitioning, PollingStrategy as IggyPollingStrategy, SimpleClient, getRawClient } from "apache-iggy";
3
+ import { Consumer, DeserializeError, HeaderKeyFactory, HeaderValue as IggyHeaderValueFactory, Partitioning, PollingStrategy as IggyPollingStrategy, ResponseError, SimpleClient, getRawClient } from "apache-iggy";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { isIP } from "node:net";
6
- import { AmbiguousMutationError, ConfigError, TimeoutError, TransportError } from "../client/errors.js";
6
+ import { AmbiguousMutationError, ConfigError, ProtocolError, TimeoutError, TransportError, UnsupportedError } from "../client/errors.js";
7
7
  import { LASERDATA_ROOT_CA } from "../client/laserdata-ca.js";
8
8
  import { Mutex } from "../runtime/mutex.js";
9
9
  import { mintUlidValue } from "../runtime/ulid.js";
@@ -16,9 +16,16 @@ const LOGIN_STAGE = "Iggy login reply";
16
16
  const VSR_HEARTBEAT_INTERVAL_MS = 5_000;
17
17
  const TRANSIENT_NOT_COMMITTED = 57;
18
18
  const TRANSIENT_NOT_ACCEPTED = 58;
19
+ const POLLED_HEAD_BYTES = 16;
20
+ const BATCH_HEADER_BYTES = 256;
21
+ const FRAME_HEADER_BYTES = 48;
22
+ const STRING_HEADER_KIND = 2;
23
+ const CONTENT_TYPE_HEADER = "agdx.ct";
24
+ const STRICT_UTF8 = new TextDecoder("utf-8", { fatal: true });
19
25
  export function toNodeBuffer(bytes) {
20
26
  return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
21
27
  }
28
+ const SYNC_CONSUMER_GROUP_CODE = 606;
22
29
  function toIggyConsumer(target) {
23
30
  return target.kind === "single"
24
31
  ? target.name === undefined
@@ -189,6 +196,162 @@ function parsedHeadersToMap(entries) {
189
196
  }
190
197
  return map;
191
198
  }
199
+ /** The Iggy error code a server reply carried, through any transport wrapping. */
200
+ export function serverErrorCode(error) {
201
+ for (let current = error; current !== undefined;) {
202
+ if (current instanceof ResponseError)
203
+ return current.errorCode;
204
+ current = current instanceof Error ? current.cause : undefined;
205
+ }
206
+ return undefined;
207
+ }
208
+ /**
209
+ * The records of a standard polled-messages body, decoded exactly as stored:
210
+ * original offsets, per-record microsecond timestamps, payloads, and typed
211
+ * user headers.
212
+ */
213
+ export function decodePolledBody(body) {
214
+ const malformed = (what) => new ProtocolError(`the polled record body has a malformed ${what}`);
215
+ if (body.byteLength < POLLED_HEAD_BYTES)
216
+ throw malformed("head");
217
+ const view = new DataView(body.buffer, body.byteOffset, body.byteLength);
218
+ const partitionId = view.getUint32(0, true);
219
+ const messages = [];
220
+ let position = POLLED_HEAD_BYTES;
221
+ while (position < body.byteLength) {
222
+ if (position + BATCH_HEADER_BYTES > body.byteLength)
223
+ throw malformed("batch header");
224
+ const baseOffset = view.getBigUint64(position + 8, true);
225
+ const baseTimestamp = view.getBigUint64(position + 16, true);
226
+ const batchLength = view.getBigUint64(position + 32, true);
227
+ if (batchLength < BigInt(BATCH_HEADER_BYTES) ||
228
+ batchLength > BigInt(body.byteLength - position))
229
+ throw malformed("batch length");
230
+ const batchEnd = position + Number(batchLength);
231
+ position += BATCH_HEADER_BYTES;
232
+ while (position < batchEnd) {
233
+ if (position + FRAME_HEADER_BYTES > batchEnd)
234
+ throw malformed("message frame");
235
+ const offsetDelta = view.getUint32(position + 24, true);
236
+ const headersLength = view.getUint32(position + 32, true);
237
+ const payloadLength = view.getUint32(position + 36, true);
238
+ const payloadStart = position + FRAME_HEADER_BYTES;
239
+ const headersStart = payloadStart + payloadLength;
240
+ const frameEnd = headersStart + headersLength;
241
+ if (frameEnd > batchEnd)
242
+ throw malformed("message frame");
243
+ const block = body.subarray(headersStart, frameEnd);
244
+ const headers = decodeUserHeaders(block);
245
+ const contentType = headers === "entry" ? decodeUserHeaders(block, true) : headers;
246
+ messages.push({
247
+ payload: body.subarray(payloadStart, headersStart),
248
+ partitionId,
249
+ offset: baseOffset + BigInt(offsetDelta),
250
+ timestampMicros: baseTimestamp,
251
+ ...(typeof headers === "string"
252
+ ? {
253
+ headers: typeof contentType === "string" ? new Map() : contentType,
254
+ headersMalformed: typeof contentType === "string" ? contentType : headers
255
+ }
256
+ : { headers })
257
+ });
258
+ position = frameEnd;
259
+ }
260
+ }
261
+ return messages;
262
+ }
263
+ // The typed user headers as the server reads them for a filter: string keys
264
+ // only, a kind this build does not know kept raw, a value that does not fit
265
+ // its kind faulting the record, and the last of a repeated key kept. A structurally broken
266
+ // block names what did not decode, which the server treats as a malformed
267
+ // record when its filter needs that part.
268
+ function decodeUserHeaders(block, contentTypeOnly = false) {
269
+ const headers = new Map();
270
+ const view = new DataView(block.buffer, block.byteOffset, block.byteLength);
271
+ let position = 0;
272
+ while (position < block.byteLength) {
273
+ if (position + 5 > block.byteLength)
274
+ return "structure";
275
+ const keyKind = view.getUint8(position);
276
+ const keyStart = position + 5;
277
+ const keyEnd = keyStart + view.getUint32(position + 1, true);
278
+ if (keyKind === 0 ||
279
+ keyEnd === keyStart ||
280
+ keyEnd - keyStart > 255 ||
281
+ keyEnd + 5 > block.byteLength)
282
+ return "structure";
283
+ const valueKind = view.getUint8(keyEnd);
284
+ const valueStart = keyEnd + 5;
285
+ const valueEnd = valueStart + view.getUint32(keyEnd + 1, true);
286
+ if (valueKind === 0 ||
287
+ valueEnd === valueStart ||
288
+ valueEnd - valueStart > 255 ||
289
+ valueEnd > block.byteLength)
290
+ return "structure";
291
+ position = valueEnd;
292
+ if (keyKind !== STRING_HEADER_KIND)
293
+ continue;
294
+ if (contentTypeOnly &&
295
+ (keyEnd - keyStart !== CONTENT_TYPE_HEADER.length ||
296
+ CONTENT_TYPE_HEADER.split("").some((character, index) => block[keyStart + index] !== character.charCodeAt(0))))
297
+ continue;
298
+ let key;
299
+ try {
300
+ key = STRICT_UTF8.decode(block.subarray(keyStart, keyEnd));
301
+ }
302
+ catch {
303
+ return "entry";
304
+ }
305
+ const value = headerValueOf(valueKind, block.subarray(valueStart, valueEnd));
306
+ if (value === undefined)
307
+ return key === CONTENT_TYPE_HEADER ? "content_type" : "entry";
308
+ headers.set(key, value);
309
+ }
310
+ return headers;
311
+ }
312
+ function headerValueOf(kind, bytes) {
313
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
314
+ const sized = (size) => bytes.byteLength === size;
315
+ switch (kind) {
316
+ case STRING_HEADER_KIND:
317
+ try {
318
+ return { kind: "string", value: STRICT_UTF8.decode(bytes) };
319
+ }
320
+ catch {
321
+ return undefined;
322
+ }
323
+ case 3:
324
+ return sized(1) && (bytes[0] === 0 || bytes[0] === 1)
325
+ ? { kind: "bool", value: bytes[0] === 1 }
326
+ : undefined;
327
+ case 4:
328
+ return sized(1) ? { kind: "int8", value: view.getInt8(0) } : undefined;
329
+ case 5:
330
+ return sized(2) ? { kind: "int16", value: view.getInt16(0, true) } : undefined;
331
+ case 6:
332
+ return sized(4) ? { kind: "int32", value: view.getInt32(0, true) } : undefined;
333
+ case 7:
334
+ return sized(8) ? { kind: "int64", value: view.getBigInt64(0, true) } : undefined;
335
+ case 8:
336
+ return sized(16) ? { kind: "int128", value: bytes.slice() } : undefined;
337
+ case 9:
338
+ return sized(1) ? { kind: "uint8", value: view.getUint8(0) } : undefined;
339
+ case 10:
340
+ return sized(2) ? { kind: "uint16", value: view.getUint16(0, true) } : undefined;
341
+ case 11:
342
+ return sized(4) ? { kind: "uint32", value: view.getUint32(0, true) } : undefined;
343
+ case 12:
344
+ return sized(8) ? { kind: "uint64", value: view.getBigUint64(0, true) } : undefined;
345
+ case 13:
346
+ return sized(16) ? { kind: "uint128", value: bytes.slice() } : undefined;
347
+ case 14:
348
+ return sized(4) ? { kind: "float", value: view.getFloat32(0, true) } : undefined;
349
+ case 15:
350
+ return sized(8) ? { kind: "double", value: view.getFloat64(0, true) } : undefined;
351
+ default:
352
+ return { kind: "raw", value: bytes.slice() };
353
+ }
354
+ }
192
355
  function laserDataHost(host) {
193
356
  const normalized = host.toLowerCase();
194
357
  return (normalized === "laserdata.cloud" ||
@@ -295,7 +458,7 @@ export function watchConnectionLoss(connection, onLost) {
295
458
  onLost();
296
459
  });
297
460
  }
298
- async function connectSimpleClient(parsed, deadline) {
461
+ async function connectSimpleClient(parsed, deadline, pinned = false) {
299
462
  const config = parsed.tls
300
463
  ? {
301
464
  heartbeatInterval: VSR_HEARTBEAT_INTERVAL_MS,
@@ -303,7 +466,11 @@ async function connectSimpleClient(parsed, deadline) {
303
466
  options: {
304
467
  port: parsed.port,
305
468
  host: parsed.host,
306
- ...(isIP(parsed.host) === 0 ? { servername: parsed.host } : {}),
469
+ ...(parsed.servername !== undefined
470
+ ? { servername: parsed.servername }
471
+ : isIP(parsed.host) === 0
472
+ ? { servername: parsed.host }
473
+ : {}),
307
474
  ...(parsed.ca !== undefined ? { ca: parsed.ca } : {})
308
475
  },
309
476
  credentials: parsed.credentials,
@@ -325,7 +492,7 @@ async function connectSimpleClient(parsed, deadline) {
325
492
  }
326
493
  try {
327
494
  const connection = raw.connection;
328
- const client = new SimpleClient(raw);
495
+ const client = pinned ? pinnedClient(raw, deadline) : new SimpleClient(raw);
329
496
  // The client emits `connect` only once the transport, TLS included, is up, so it
330
497
  // separates a server that never accepts the socket from one that never answers login.
331
498
  let accepted = false;
@@ -338,7 +505,18 @@ async function connectSimpleClient(parsed, deadline) {
338
505
  reject(cause instanceof Error ? cause : new Error(String(cause)));
339
506
  };
340
507
  connection.once("error", failed);
341
- client.client.getMe().then(() => {
508
+ const login = async () => {
509
+ if (pinned) {
510
+ const connection = raw.connection;
511
+ await connection.connect(true);
512
+ if ("token" in parsed.credentials)
513
+ await client.session.loginWithToken(parsed.credentials);
514
+ else
515
+ await client.session.login(parsed.credentials);
516
+ }
517
+ await client.client.getMe();
518
+ };
519
+ login().then(() => {
342
520
  connection.off("error", failed);
343
521
  resolve();
344
522
  }, reject);
@@ -366,13 +544,44 @@ async function connectSimpleClient(parsed, deadline) {
366
544
  }
367
545
  catch (cause) {
368
546
  raw.destroy();
369
- if (cause instanceof TimeoutError)
547
+ if (cause instanceof TimeoutError || cause instanceof UnsupportedError)
370
548
  throw cause;
371
549
  throw new TransportError(`failed to connect to ${parsed.host}:${String(parsed.port)}`, true, {
372
550
  cause
373
551
  });
374
552
  }
375
553
  }
554
+ // Use the same request queue the Apache Iggy SDK uses for its native partition
555
+ // data connections. This bypasses coordinator login settlement on this connection.
556
+ function pinnedClient(raw, loginDeadline) {
557
+ const data = raw;
558
+ const queue = data._queueCommand;
559
+ if (typeof queue !== "function")
560
+ throw new UnsupportedError("this Apache Iggy SDK does not expose the partition data request queue required by filtered Primary reads");
561
+ const facade = {
562
+ sendCommand(code, payload, options) {
563
+ return queue.call(data, code, payload, options?.handleResponse ?? true, options?.last ?? true, false, options?.deadline ??
564
+ (raw.isAuthenticated
565
+ ? Date.now() + connectOptions().timeoutMs
566
+ : (loginDeadline ?? Date.now() + connectOptions().timeoutMs)));
567
+ },
568
+ get isAuthenticated() {
569
+ return raw.isAuthenticated;
570
+ },
571
+ authenticate: (credentials) => raw.authenticate(credentials),
572
+ destroy: () => {
573
+ raw.destroy();
574
+ },
575
+ on: (event, callback) => {
576
+ raw.on(event, callback);
577
+ },
578
+ once: (event, callback) => {
579
+ raw.once(event, callback);
580
+ },
581
+ getReadStream: () => raw.getReadStream()
582
+ };
583
+ return new SimpleClient(facade);
584
+ }
376
585
  function serverResponseError(error) {
377
586
  let current = error;
378
587
  for (let depth = 0; depth < 8; depth += 1) {
@@ -428,6 +637,8 @@ export class ApacheIggyTransport {
428
637
  reconnectLock = new Mutex();
429
638
  publishLane = new Mutex();
430
639
  disconnected = new WeakSet();
640
+ // Groups this transport rejoins after a reconnect. Setup helpers allow
641
+ // recreation, while joining an existing group keeps `create: false`.
431
642
  consumerGroups = new Map();
432
643
  partitionCounts = new Map();
433
644
  balancedCursors = new Map();
@@ -441,6 +652,9 @@ export class ApacheIggyTransport {
441
652
  get iggyClient() {
442
653
  return this.client;
443
654
  }
655
+ get connectsNodes() {
656
+ return this.connection !== undefined;
657
+ }
444
658
  static async connect(connectionString, options, deadline = Date.now() + connectOptions().timeoutMs) {
445
659
  const config = publishOptions(options);
446
660
  const parsed = parseConnectionString(connectionString);
@@ -469,7 +683,12 @@ export class ApacheIggyTransport {
469
683
  const stale = this.client;
470
684
  if (this.disconnected.has(stale)) {
471
685
  await this.reconnect(stale);
472
- return operation(this.client);
686
+ try {
687
+ return await operation(this.client);
688
+ }
689
+ catch (cause) {
690
+ throw new TransportError(message, serverResponseError(cause) === undefined, { cause });
691
+ }
473
692
  }
474
693
  try {
475
694
  return await operation(stale);
@@ -523,8 +742,21 @@ export class ApacheIggyTransport {
523
742
  throw new TimeoutError("Iggy publish reconnect");
524
743
  const connected = await connectSimpleClient(this.connection, deadline);
525
744
  try {
526
- for (const group of this.consumerGroups.values()) {
527
- const join = connected.client.group.ensureAndJoin(group.streamId, group.topicId, group.name);
745
+ for (const [key, group] of [...this.consumerGroups]) {
746
+ const join = group.create && typeof group.name === "string"
747
+ ? connected.client.group.ensureAndJoin(group.streamId, group.topicId, group.name)
748
+ : connected.client.group
749
+ .join({
750
+ streamId: group.streamId,
751
+ topicId: group.topicId,
752
+ groupId: group.name
753
+ })
754
+ .then(() => undefined,
755
+ // A deleted group is not recreated: its member finds out
756
+ // on its next assignment read.
757
+ () => {
758
+ this.consumerGroups.delete(key);
759
+ });
528
760
  await (deadline === undefined
529
761
  ? join
530
762
  : publishWithin(join, Math.max(1, deadline - Date.now())));
@@ -601,6 +833,9 @@ export class ApacheIggyTransport {
601
833
  }
602
834
  return deleted;
603
835
  }
836
+ async ensureConsumerGroup(streamId, topicId, name) {
837
+ await this.execute((client) => client.group.ensure(streamId, topicId, name), `failed to ensure consumer group \`${name}\``);
838
+ }
604
839
  async ensureTopic(streamId, topicId, partitions) {
605
840
  const topic = await this.execute((client) => client.topic.ensure(streamId, topicId, partitions), `failed to ensure topic \`${topicId}\` on stream \`${streamId}\``);
606
841
  this.partitionCounts.set(this.topicKey(streamId, topicId), topic.partitionsCount);
@@ -765,7 +1000,7 @@ export class ApacheIggyTransport {
765
1000
  const reply = await this.execute((client) => client.message.poll({
766
1001
  streamId,
767
1002
  topicId,
768
- partitionId: target.kind === "single" ? target.partitionId : null,
1003
+ partitionId: target.partitionId ?? null,
769
1004
  consumer: toIggyConsumer(target),
770
1005
  pollingStrategy: toIggyPollingStrategy(strategy),
771
1006
  count,
@@ -801,11 +1036,105 @@ export class ApacheIggyTransport {
801
1036
  }
802
1037
  async joinConsumerGroup(streamId, topicId, name) {
803
1038
  await this.execute((client) => client.group.ensureAndJoin(streamId, topicId, name), `failed to join consumer group \`${name}\``);
804
- this.consumerGroups.set(`${streamId}\0${topicId}\0${name}`, { streamId, topicId, name });
1039
+ this.consumerGroups.set(`${streamId}\0${topicId}\0${name}`, {
1040
+ streamId,
1041
+ topicId,
1042
+ name,
1043
+ create: true
1044
+ });
1045
+ }
1046
+ async syncConsumerGroup(streamId, topicId, name) {
1047
+ const identifiers = [streamId, topicId, name].map((value) => new TextEncoder().encode(value));
1048
+ if (identifiers.some((value) => value.byteLength === 0 || value.byteLength > 255))
1049
+ throw new ConfigError("consumer group identifiers must contain 1 to 255 UTF-8 bytes");
1050
+ const payload = new Uint8Array(identifiers.reduce((size, value) => size + 2 + value.byteLength, 0));
1051
+ let offset = 0;
1052
+ for (const value of identifiers) {
1053
+ payload.set([2, value.byteLength], offset);
1054
+ payload.set(value, offset + 2);
1055
+ offset += value.byteLength + 2;
1056
+ }
1057
+ let reply = await this.sendManaged(SYNC_CONSUMER_GROUP_CODE, payload, {
1058
+ retryAfterReconnect: true
1059
+ });
1060
+ let rejoined = false;
1061
+ if (reply.byteLength === 0) {
1062
+ await this.joinConsumerGroup(streamId, topicId, name);
1063
+ rejoined = true;
1064
+ reply = await this.sendManaged(SYNC_CONSUMER_GROUP_CODE, payload, {
1065
+ retryAfterReconnect: true
1066
+ });
1067
+ if (reply.byteLength === 0)
1068
+ return undefined;
1069
+ }
1070
+ if (reply.byteLength < 12)
1071
+ throw new ProtocolError("the consumer group assignment is truncated");
1072
+ const view = new DataView(reply.buffer, reply.byteOffset, reply.byteLength);
1073
+ const count = view.getUint32(8, true);
1074
+ if (reply.byteLength !== 12 + count * 4)
1075
+ throw new ProtocolError("the consumer group assignment length is invalid");
1076
+ return {
1077
+ generation: view.getBigUint64(0, true),
1078
+ rejoined,
1079
+ partitions: Array.from({ length: count }, (_, index) => view.getUint32(12 + index * 4, true))
1080
+ };
805
1081
  }
806
1082
  async leaveConsumerGroup(streamId, topicId, name) {
807
- await this.execute((client) => client.group.leave({ streamId, topicId, groupId: name }), `failed to leave consumer group \`${name}\``);
808
- this.consumerGroups.delete(`${streamId}\0${topicId}\0${name}`);
1083
+ await this.execute((client) => client.group.leave({ streamId, topicId, groupId: name }), `failed to leave consumer group \`${String(name)}\``);
1084
+ this.consumerGroups.delete(`${streamId}\0${topicId}\0${String(name)}`);
1085
+ }
1086
+ async joinExistingConsumerGroup(streamId, topicId, name) {
1087
+ await this.execute((client) => client.group.join({ streamId, topicId, groupId: name }), `failed to join consumer group \`${String(name)}\``);
1088
+ this.consumerGroups.set(`${streamId}\0${topicId}\0${String(name)}`, {
1089
+ streamId,
1090
+ topicId,
1091
+ name,
1092
+ create: false
1093
+ });
1094
+ }
1095
+ async openNodeConnection(ip, port) {
1096
+ if (this.connection === undefined) {
1097
+ throw new ConfigError("a node connection needs a Laser connected from a connection string, not an injected client");
1098
+ }
1099
+ const unspecified = ip.length === 0 || ip === "0.0.0.0" || ip === "::" || ip === "[::]";
1100
+ const host = unspecified ? this.connection.host : ip;
1101
+ const connected = await connectSimpleClient({
1102
+ ...this.connection,
1103
+ host,
1104
+ port,
1105
+ ...(isIP(this.connection.host) === 0 ? { servername: this.connection.host } : {})
1106
+ }, Date.now() + connectOptions().timeoutMs, true);
1107
+ const client = connected.client;
1108
+ return {
1109
+ async send(code, payload) {
1110
+ const reply = await client.sendBinaryRequest(code, toNodeBuffer(payload));
1111
+ return new Uint8Array(reply.buffer, reply.byteOffset, reply.byteLength);
1112
+ },
1113
+ async close() {
1114
+ await client.destroy().catch(() => undefined);
1115
+ }
1116
+ };
1117
+ }
1118
+ async openCoordinator() {
1119
+ if (this.connection === undefined) {
1120
+ throw new ConfigError("a coordinator connection needs a Laser connected from a connection string, not an injected client");
1121
+ }
1122
+ const connected = await connectSimpleClient(this.connection, Date.now() + connectOptions().timeoutMs);
1123
+ const coordinator = new ApacheIggyTransport(connected.client, this.connection, "owned", this.publishConfig);
1124
+ coordinator.watch(connected);
1125
+ return {
1126
+ async send(code, payload) {
1127
+ const reply = await coordinator.execute((client) => client.sendBinaryRequest(code, toNodeBuffer(payload)), `coordinator command ${String(code)} failed`);
1128
+ return new Uint8Array(reply.buffer, reply.byteOffset, reply.byteLength);
1129
+ },
1130
+ async joinConsumerGroup(streamId, topicId, name) {
1131
+ await coordinator.execute((client) => client.group.join({ streamId, topicId, groupId: name }), `failed to join consumer group \`${String(name)}\``);
1132
+ },
1133
+ async leaveConsumerGroup(streamId, topicId, name) {
1134
+ await coordinator.execute((client) => client.group.leave({ streamId, topicId, groupId: name }), `failed to leave consumer group \`${String(name)}\``);
1135
+ },
1136
+ close: () => coordinator.close()
1137
+ };
809
1138
  }
810
1139
  async close() {
811
1140
  this.closed = true;