@qualithm/arrow-flight-sql-js 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @qualithm/arrow-flight-sql-js might be problematic. Click here for more details.

package/dist/client.js CHANGED
@@ -10,6 +10,7 @@ import { collectToTable, parseFlightData, tryParseSchema } from "./arrow";
10
10
  import { AuthenticationError, ConnectionError, FlightSqlError } from "./errors";
11
11
  import { getFlightServiceDefinition } from "./generated";
12
12
  import { encodeActionClosePreparedStatementRequest, encodeActionCreatePreparedStatementRequest, encodeCommandGetCatalogs, encodeCommandGetDbSchemas, encodeCommandGetExportedKeys, encodeCommandGetImportedKeys, encodeCommandGetPrimaryKeys, encodeCommandGetTables, encodeCommandGetTableTypes, encodeCommandPreparedStatementQuery, encodeCommandStatementQuery, encodeCommandStatementUpdate, getBytesField, parseProtoFields } from "./proto";
13
+ import { SubscriptionMessageType, SubscriptionMode } from "./types";
13
14
  // Default configuration values
14
15
  const defaultConnectTimeoutMs = 30_000;
15
16
  const defaultRequestTimeoutMs = 60_000;
@@ -72,7 +73,7 @@ export class FlightSqlClient {
72
73
  const protocolPackage = flightPackage.protocol;
73
74
  this.flightService = protocolPackage.FlightService;
74
75
  // Create channel credentials
75
- const credentials = this.options.credentials !== undefined ? this.options.credentials : this.createCredentials();
76
+ const credentials = this.options.credentials ?? this.createCredentials();
76
77
  // Create the gRPC client
77
78
  const address = `${this.options.host}:${String(this.options.port)}`;
78
79
  this.grpcClient = new this.flightService(address, credentials, {
@@ -261,7 +262,7 @@ export class FlightSqlClient {
261
262
  };
262
263
  const flightInfo = await this.getFlightInfo(descriptor);
263
264
  return this.fetchCatalogResults(flightInfo, (row) => ({
264
- catalogName: row["catalog_name"]
265
+ catalogName: row.catalog_name
265
266
  }));
266
267
  }
267
268
  /**
@@ -292,8 +293,8 @@ export class FlightSqlClient {
292
293
  };
293
294
  const flightInfo = await this.getFlightInfo(descriptor);
294
295
  return this.fetchCatalogResults(flightInfo, (row) => ({
295
- catalogName: row["catalog_name"],
296
- schemaName: row["db_schema_name"]
296
+ catalogName: row.catalog_name,
297
+ schemaName: row.db_schema_name
297
298
  }));
298
299
  }
299
300
  /**
@@ -333,13 +334,13 @@ export class FlightSqlClient {
333
334
  const flightInfo = await this.getFlightInfo(descriptor);
334
335
  return this.fetchCatalogResults(flightInfo, (row) => {
335
336
  const info = {
336
- catalogName: row["catalog_name"],
337
- schemaName: row["db_schema_name"],
338
- tableName: row["table_name"],
339
- tableType: row["table_type"]
337
+ catalogName: row.catalog_name,
338
+ schemaName: row.db_schema_name,
339
+ tableName: row.table_name,
340
+ tableType: row.table_type
340
341
  };
341
342
  // If includeSchema was requested, parse the table schema
342
- const schemaBytes = row["table_schema"];
343
+ const schemaBytes = row.table_schema;
343
344
  if (schemaBytes && schemaBytes.length > 0) {
344
345
  info.schema = tryParseSchema(schemaBytes) ?? undefined;
345
346
  }
@@ -366,7 +367,7 @@ export class FlightSqlClient {
366
367
  };
367
368
  const flightInfo = await this.getFlightInfo(descriptor);
368
369
  return this.fetchCatalogResults(flightInfo, (row) => ({
369
- tableType: row["table_type"]
370
+ tableType: row.table_type
370
371
  }));
371
372
  }
372
373
  /**
@@ -394,12 +395,12 @@ export class FlightSqlClient {
394
395
  };
395
396
  const flightInfo = await this.getFlightInfo(descriptor);
396
397
  return this.fetchCatalogResults(flightInfo, (row) => ({
397
- catalogName: row["catalog_name"],
398
- schemaName: row["db_schema_name"],
399
- tableName: row["table_name"],
400
- columnName: row["column_name"],
401
- keySequence: row["key_sequence"],
402
- keyName: row["key_name"]
398
+ catalogName: row.catalog_name,
399
+ schemaName: row.db_schema_name,
400
+ tableName: row.table_name,
401
+ columnName: row.column_name,
402
+ keySequence: row.key_sequence,
403
+ keyName: row.key_name
403
404
  }));
404
405
  }
405
406
  /**
@@ -462,42 +463,50 @@ export class FlightSqlClient {
462
463
  }
463
464
  for (const endpoint of flightInfo.endpoints) {
464
465
  for await (const flightData of this.doGet(endpoint.ticket)) {
465
- if (flightData.dataHeader && flightData.dataBody) {
466
- const batch = parseFlightData(flightData.dataHeader, flightData.dataBody, schema);
467
- if (batch) {
468
- // Convert batch rows to objects
469
- for (let i = 0; i < batch.numRows; i++) {
470
- const row = {};
471
- for (const field of schema.fields) {
472
- const column = batch.getChild(field.name);
473
- row[field.name] = column?.get(i);
474
- }
475
- results.push(mapper(row));
476
- }
477
- }
466
+ if (!flightData.dataHeader || !flightData.dataBody) {
467
+ continue;
468
+ }
469
+ const batch = parseFlightData(flightData.dataHeader, flightData.dataBody, schema);
470
+ if (!batch) {
471
+ continue;
478
472
  }
473
+ // Convert batch rows to objects
474
+ this.extractRowsFromBatch(batch, schema, mapper, results);
479
475
  }
480
476
  }
481
477
  return results;
482
478
  }
479
+ /**
480
+ * Helper to extract rows from a batch and map them
481
+ */
482
+ extractRowsFromBatch(batch, schema, mapper, results) {
483
+ for (let i = 0; i < batch.numRows; i++) {
484
+ const row = {};
485
+ for (const field of schema.fields) {
486
+ const column = batch.getChild(field.name);
487
+ row[field.name] = column?.get(i);
488
+ }
489
+ results.push(mapper(row));
490
+ }
491
+ }
483
492
  /**
484
493
  * Helper to fetch foreign key results with the complex schema
485
494
  */
486
495
  async fetchForeignKeyResults(flightInfo) {
487
496
  return this.fetchCatalogResults(flightInfo, (row) => ({
488
- pkCatalogName: row["pk_catalog_name"],
489
- pkSchemaName: row["pk_db_schema_name"],
490
- pkTableName: row["pk_table_name"],
491
- pkColumnName: row["pk_column_name"],
492
- fkCatalogName: row["fk_catalog_name"],
493
- fkSchemaName: row["fk_db_schema_name"],
494
- fkTableName: row["fk_table_name"],
495
- fkColumnName: row["fk_column_name"],
496
- keySequence: row["key_sequence"],
497
- fkKeyName: row["fk_key_name"],
498
- pkKeyName: row["pk_key_name"],
499
- updateRule: row["update_rule"],
500
- deleteRule: row["delete_rule"]
497
+ pkCatalogName: row.pk_catalog_name,
498
+ pkSchemaName: row.pk_db_schema_name,
499
+ pkTableName: row.pk_table_name,
500
+ pkColumnName: row.pk_column_name,
501
+ fkCatalogName: row.fk_catalog_name,
502
+ fkSchemaName: row.fk_db_schema_name,
503
+ fkTableName: row.fk_table_name,
504
+ fkColumnName: row.fk_column_name,
505
+ keySequence: row.key_sequence,
506
+ fkKeyName: row.fk_key_name,
507
+ pkKeyName: row.pk_key_name,
508
+ updateRule: row.update_rule,
509
+ deleteRule: row.delete_rule
501
510
  }));
502
511
  }
503
512
  // ===========================================================================
@@ -622,13 +631,146 @@ export class FlightSqlClient {
622
631
  throw error;
623
632
  }
624
633
  }
634
+ /**
635
+ * Open a bidirectional data exchange with the server.
636
+ *
637
+ * This is the low-level API for DoExchange. For real-time subscriptions,
638
+ * use the higher-level `subscribe()` method instead.
639
+ *
640
+ * @param descriptor - Flight descriptor for the exchange
641
+ * @returns An exchange handle for sending and receiving FlightData
642
+ *
643
+ * @example
644
+ * ```typescript
645
+ * const exchange = client.doExchange({
646
+ * type: DescriptorType.CMD,
647
+ * cmd: new TextEncoder().encode('SUBSCRIBE:my_topic')
648
+ * })
649
+ *
650
+ * // Send initial request
651
+ * await exchange.send({ appMetadata: subscribeCommand })
652
+ *
653
+ * // Receive responses
654
+ * for await (const data of exchange) {
655
+ * if (data.dataHeader) {
656
+ * // Process record batch
657
+ * }
658
+ * }
659
+ *
660
+ * // Clean up
661
+ * await exchange.end()
662
+ * ```
663
+ */
664
+ doExchange(descriptor) {
665
+ this.ensureConnected();
666
+ const client = this.grpcClient;
667
+ const metadata = this.createRequestMetadata();
668
+ const stream = client.doExchange(metadata);
669
+ // Use object wrapper to track errors
670
+ const errorState = { error: null };
671
+ stream.on("error", (err) => {
672
+ errorState.error = err;
673
+ });
674
+ // Capture references for closure
675
+ const wrapGrpcError = this.wrapGrpcError.bind(this);
676
+ const serializeFlightDescriptor = this.serializeFlightDescriptor.bind(this);
677
+ const wrapStream = this.wrapStream.bind(this);
678
+ // Create the exchange handle
679
+ const exchange = {
680
+ async send(data) {
681
+ if (errorState.error) {
682
+ throw wrapGrpcError(errorState.error);
683
+ }
684
+ stream.write({
685
+ flightDescriptor: data.flightDescriptor
686
+ ? serializeFlightDescriptor(data.flightDescriptor)
687
+ : undefined,
688
+ dataHeader: data.dataHeader,
689
+ dataBody: data.dataBody,
690
+ appMetadata: data.appMetadata
691
+ });
692
+ return Promise.resolve();
693
+ },
694
+ async end() {
695
+ stream.end();
696
+ return Promise.resolve();
697
+ },
698
+ cancel() {
699
+ stream.cancel();
700
+ },
701
+ async *[Symbol.asyncIterator]() {
702
+ try {
703
+ for await (const data of wrapStream(stream)) {
704
+ const flightData = data;
705
+ yield {
706
+ flightDescriptor: flightData.flightDescriptor
707
+ ? {
708
+ type: flightData.flightDescriptor.type,
709
+ cmd: flightData.flightDescriptor.cmd,
710
+ path: flightData.flightDescriptor.path
711
+ }
712
+ : undefined,
713
+ dataHeader: flightData.dataHeader ?? new Uint8Array(),
714
+ dataBody: flightData.dataBody ?? new Uint8Array(),
715
+ appMetadata: flightData.appMetadata
716
+ };
717
+ }
718
+ }
719
+ catch (error) {
720
+ if (errorState.error) {
721
+ throw wrapGrpcError(errorState.error);
722
+ }
723
+ throw error;
724
+ }
725
+ }
726
+ };
727
+ // Send descriptor in first message (empty data)
728
+ stream.write({
729
+ flightDescriptor: this.serializeFlightDescriptor(descriptor)
730
+ });
731
+ return exchange;
732
+ }
733
+ /**
734
+ * Subscribe to real-time data updates from a query.
735
+ *
736
+ * Returns a Subscription that yields RecordBatches as they arrive from the server.
737
+ * Automatically handles heartbeats and can reconnect on connection loss.
738
+ *
739
+ * @param query - SQL query to subscribe to
740
+ * @param options - Subscription options
741
+ * @returns Subscription handle for receiving batches and control
742
+ *
743
+ * @example
744
+ * ```typescript
745
+ * const subscription = await client.subscribe(
746
+ * "SELECT * FROM events WHERE status = 'pending'",
747
+ * { mode: 'CHANGES_ONLY', heartbeatMs: 30000 }
748
+ * )
749
+ *
750
+ * for await (const batch of subscription) {
751
+ * console.log(`Received ${batch.numRows} rows`)
752
+ * }
753
+ *
754
+ * // Or with AbortController
755
+ * const controller = new AbortController()
756
+ * const subscription = await client.subscribe(query, {
757
+ * signal: controller.signal
758
+ * })
759
+ *
760
+ * // Later: cancel the subscription
761
+ * controller.abort()
762
+ * ```
763
+ */
764
+ subscribe(query, options = {}) {
765
+ return new Subscription(this, query, options);
766
+ }
625
767
  /**
626
768
  * Helper to get the first item from an async iterable without consuming the rest
627
769
  */
628
770
  async getFirstFromIterable(iterable) {
629
771
  const iterator = iterable[Symbol.asyncIterator]();
630
772
  const result = await iterator.next();
631
- return result.done ? undefined : result.value;
773
+ return result.done === true ? undefined : result.value;
632
774
  }
633
775
  /**
634
776
  * Execute an action on the server.
@@ -752,7 +894,7 @@ export class FlightSqlClient {
752
894
  createRequestMetadata() {
753
895
  const metadata = new grpc.Metadata();
754
896
  // Add auth token if present
755
- if (this.authToken) {
897
+ if (this.authToken !== null && this.authToken !== "") {
756
898
  metadata.set("authorization", `Bearer ${this.authToken}`);
757
899
  }
758
900
  // Add custom metadata from options
@@ -785,7 +927,20 @@ export class FlightSqlClient {
785
927
  case grpc.status.UNAVAILABLE:
786
928
  case grpc.status.DEADLINE_EXCEEDED:
787
929
  return new ConnectionError(message, { cause: error });
788
- default:
930
+ case grpc.status.OK:
931
+ case grpc.status.CANCELLED:
932
+ case grpc.status.UNKNOWN:
933
+ case grpc.status.INVALID_ARGUMENT:
934
+ case grpc.status.NOT_FOUND:
935
+ case grpc.status.ALREADY_EXISTS:
936
+ case grpc.status.PERMISSION_DENIED:
937
+ case grpc.status.RESOURCE_EXHAUSTED:
938
+ case grpc.status.FAILED_PRECONDITION:
939
+ case grpc.status.ABORTED:
940
+ case grpc.status.OUT_OF_RANGE:
941
+ case grpc.status.UNIMPLEMENTED:
942
+ case grpc.status.INTERNAL:
943
+ case grpc.status.DATA_LOSS:
789
944
  return new FlightSqlError(message, { cause: error });
790
945
  }
791
946
  }
@@ -916,7 +1071,7 @@ export class QueryResult {
916
1071
  const framedParts = [];
917
1072
  for await (const flightData of this.client.doGet(endpoint.ticket)) {
918
1073
  if (flightData.dataHeader && flightData.dataHeader.length > 0) {
919
- const framed = this.frameAsIPC(flightData.dataHeader, flightData.dataBody || new Uint8Array(0));
1074
+ const framed = this.frameAsIPC(flightData.dataHeader, flightData.dataBody ?? new Uint8Array(0));
920
1075
  framedParts.push(framed);
921
1076
  }
922
1077
  }
@@ -1084,4 +1239,289 @@ export class PreparedStatement {
1084
1239
  this.closed = true;
1085
1240
  }
1086
1241
  }
1242
+ // ============================================================================
1243
+ // Subscription
1244
+ // ============================================================================
1245
+ /**
1246
+ * Real-time subscription to query results via DoExchange.
1247
+ *
1248
+ * Yields RecordBatches as they arrive from the server. Handles heartbeats
1249
+ * automatically and can reconnect on transient connection failures.
1250
+ *
1251
+ * @example
1252
+ * ```typescript
1253
+ * const subscription = client.subscribe("SELECT * FROM events")
1254
+ *
1255
+ * for await (const batch of subscription) {
1256
+ * console.log(`Received ${batch.numRows} rows`)
1257
+ * }
1258
+ *
1259
+ * await subscription.unsubscribe()
1260
+ * ```
1261
+ */
1262
+ export class Subscription {
1263
+ client;
1264
+ query;
1265
+ options;
1266
+ exchange = null;
1267
+ subscriptionId = null;
1268
+ connectedState = false;
1269
+ batchesReceivedCount = 0;
1270
+ reconnectAttempts = 0;
1271
+ abortedFlag = false;
1272
+ lastHeartbeat = Date.now();
1273
+ iterating = false;
1274
+ /** Check aborted state - method prevents ESLint from static flow analysis across async boundaries */
1275
+ isAborted() {
1276
+ return this.abortedFlag;
1277
+ }
1278
+ constructor(client, query, options = {}) {
1279
+ this.client = client;
1280
+ this.query = query;
1281
+ this.options = {
1282
+ ...options,
1283
+ mode: options.mode ?? SubscriptionMode.ChangesOnly,
1284
+ heartbeatMs: options.heartbeatMs ?? 30_000,
1285
+ autoReconnect: options.autoReconnect ?? true,
1286
+ maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
1287
+ reconnectDelayMs: options.reconnectDelayMs ?? 1_000,
1288
+ maxReconnectDelayMs: options.maxReconnectDelayMs ?? 30_000
1289
+ };
1290
+ // Handle abort signal
1291
+ if (this.options.signal) {
1292
+ this.options.signal.addEventListener("abort", () => {
1293
+ this.abortedFlag = true;
1294
+ this.exchange?.cancel();
1295
+ });
1296
+ }
1297
+ }
1298
+ /**
1299
+ * Unique ID for this subscription (assigned by server after connect)
1300
+ */
1301
+ get id() {
1302
+ return this.subscriptionId ?? "";
1303
+ }
1304
+ /**
1305
+ * Whether the subscription is currently connected
1306
+ */
1307
+ get connected() {
1308
+ return this.connectedState;
1309
+ }
1310
+ /**
1311
+ * Number of batches received
1312
+ */
1313
+ get batchesReceived() {
1314
+ return this.batchesReceivedCount;
1315
+ }
1316
+ /**
1317
+ * Number of reconnection attempts
1318
+ */
1319
+ get reconnectCount() {
1320
+ return this.reconnectAttempts;
1321
+ }
1322
+ /**
1323
+ * Timestamp of the last heartbeat received from the server
1324
+ */
1325
+ get lastHeartbeatTime() {
1326
+ return this.lastHeartbeat;
1327
+ }
1328
+ /**
1329
+ * Time in milliseconds since the last heartbeat
1330
+ */
1331
+ get timeSinceLastHeartbeat() {
1332
+ return Date.now() - this.lastHeartbeat;
1333
+ }
1334
+ /**
1335
+ * Start the subscription and iterate over incoming batches
1336
+ */
1337
+ async *[Symbol.asyncIterator]() {
1338
+ if (this.iterating) {
1339
+ throw new FlightSqlError("Subscription is already being iterated");
1340
+ }
1341
+ this.iterating = true;
1342
+ try {
1343
+ while (!this.isAborted()) {
1344
+ try {
1345
+ yield* this.streamBatches();
1346
+ // Stream completed normally
1347
+ break;
1348
+ }
1349
+ catch (error) {
1350
+ if (this.isAborted()) {
1351
+ break;
1352
+ }
1353
+ if (!this.options.autoReconnect) {
1354
+ throw error;
1355
+ }
1356
+ if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
1357
+ throw new FlightSqlError(`Max reconnection attempts (${String(this.options.maxReconnectAttempts)}) exceeded`, { cause: error instanceof Error ? error : new Error(String(error)) });
1358
+ }
1359
+ await this.reconnect();
1360
+ }
1361
+ }
1362
+ }
1363
+ finally {
1364
+ this.iterating = false;
1365
+ await this.cleanup();
1366
+ }
1367
+ }
1368
+ /**
1369
+ * Unsubscribe and close the connection
1370
+ */
1371
+ async unsubscribe() {
1372
+ this.abortedFlag = true;
1373
+ await this.cleanup();
1374
+ }
1375
+ async *streamBatches() {
1376
+ this.initConnection();
1377
+ if (!this.exchange) {
1378
+ throw new FlightSqlError("Exchange not initialized");
1379
+ }
1380
+ for await (const flightData of this.exchange) {
1381
+ if (this.isAborted()) {
1382
+ break;
1383
+ }
1384
+ // Check for metadata messages (heartbeat, control messages)
1385
+ if (flightData.appMetadata && flightData.appMetadata.length > 0) {
1386
+ const metadata = this.parseMetadata(flightData.appMetadata);
1387
+ switch (metadata.type) {
1388
+ case SubscriptionMessageType.HEARTBEAT:
1389
+ this.lastHeartbeat = Date.now();
1390
+ continue;
1391
+ case SubscriptionMessageType.COMPLETE:
1392
+ return;
1393
+ case SubscriptionMessageType.ERROR:
1394
+ throw new FlightSqlError(metadata.error ?? "Subscription error from server");
1395
+ case SubscriptionMessageType.DATA:
1396
+ // Continue to parse data
1397
+ if (metadata.subscriptionId !== undefined && this.subscriptionId === null) {
1398
+ this.subscriptionId = metadata.subscriptionId;
1399
+ }
1400
+ break;
1401
+ case SubscriptionMessageType.SUBSCRIBE:
1402
+ case SubscriptionMessageType.UNSUBSCRIBE:
1403
+ // These are client-to-server messages, ignore if received
1404
+ break;
1405
+ }
1406
+ }
1407
+ // Parse data if present
1408
+ if (flightData.dataHeader && flightData.dataHeader.length > 0) {
1409
+ const batches = this.parseFlightDataToBatches(flightData);
1410
+ for (const batch of batches) {
1411
+ this.batchesReceivedCount++;
1412
+ yield batch;
1413
+ }
1414
+ }
1415
+ }
1416
+ }
1417
+ initConnection() {
1418
+ const descriptor = {
1419
+ type: 2, // CMD
1420
+ cmd: this.encodeSubscribeCommand()
1421
+ };
1422
+ this.exchange = this.client.doExchange(descriptor);
1423
+ this.connectedState = true;
1424
+ this.lastHeartbeat = Date.now();
1425
+ }
1426
+ async reconnect() {
1427
+ this.reconnectAttempts++;
1428
+ this.connectedState = false;
1429
+ // Calculate backoff delay with jitter
1430
+ const baseDelay = Math.min(this.options.reconnectDelayMs * Math.pow(2, this.reconnectAttempts - 1), this.options.maxReconnectDelayMs);
1431
+ const jitter = Math.random() * baseDelay * 0.1;
1432
+ const delay = baseDelay + jitter;
1433
+ await this.sleep(delay);
1434
+ this.initConnection();
1435
+ }
1436
+ async cleanup() {
1437
+ if (this.exchange && this.connectedState) {
1438
+ try {
1439
+ // Send unsubscribe message
1440
+ await this.exchange.send({
1441
+ dataHeader: new Uint8Array(),
1442
+ dataBody: new Uint8Array(),
1443
+ appMetadata: this.encodeMetadata({
1444
+ type: SubscriptionMessageType.UNSUBSCRIBE,
1445
+ subscriptionId: this.subscriptionId ?? undefined,
1446
+ timestamp: Date.now()
1447
+ })
1448
+ });
1449
+ await this.exchange.end();
1450
+ }
1451
+ catch {
1452
+ // Ignore cleanup errors
1453
+ }
1454
+ }
1455
+ this.connectedState = false;
1456
+ this.exchange = null;
1457
+ }
1458
+ encodeSubscribeCommand() {
1459
+ // Encode subscription request as JSON in the command
1460
+ const request = {
1461
+ type: SubscriptionMessageType.SUBSCRIBE,
1462
+ query: this.query,
1463
+ mode: this.options.mode,
1464
+ heartbeatMs: this.options.heartbeatMs,
1465
+ metadata: this.options.metadata
1466
+ };
1467
+ return new TextEncoder().encode(JSON.stringify(request));
1468
+ }
1469
+ encodeMetadata(metadata) {
1470
+ return new TextEncoder().encode(JSON.stringify(metadata));
1471
+ }
1472
+ parseMetadata(data) {
1473
+ try {
1474
+ const text = new TextDecoder().decode(data);
1475
+ return JSON.parse(text);
1476
+ }
1477
+ catch {
1478
+ return { type: SubscriptionMessageType.DATA };
1479
+ }
1480
+ }
1481
+ parseFlightDataToBatches(flightData) {
1482
+ const batches = [];
1483
+ if (!flightData.dataHeader || flightData.dataHeader.length === 0) {
1484
+ return batches;
1485
+ }
1486
+ try {
1487
+ // Frame the raw flatbuffer with IPC continuation marker
1488
+ const framed = this.frameAsIPC(flightData.dataHeader, flightData.dataBody ?? new Uint8Array(0));
1489
+ const reader = RecordBatchReader.from(framed);
1490
+ for (const batch of reader) {
1491
+ batches.push(batch);
1492
+ }
1493
+ }
1494
+ catch {
1495
+ // If parsing fails, return empty
1496
+ }
1497
+ return batches;
1498
+ }
1499
+ frameAsIPC(header, body) {
1500
+ const continuationToken = new Uint8Array([0xff, 0xff, 0xff, 0xff]);
1501
+ const metadataLength = new Uint8Array(4);
1502
+ new DataView(metadataLength.buffer).setInt32(0, header.length, true);
1503
+ const headerPadding = (8 - ((header.length + 4 + 4) % 8)) % 8;
1504
+ const padding = new Uint8Array(headerPadding);
1505
+ const totalLength = continuationToken.length +
1506
+ metadataLength.length +
1507
+ header.length +
1508
+ padding.length +
1509
+ body.length;
1510
+ const result = new Uint8Array(totalLength);
1511
+ let offset = 0;
1512
+ result.set(continuationToken, offset);
1513
+ offset += continuationToken.length;
1514
+ result.set(metadataLength, offset);
1515
+ offset += metadataLength.length;
1516
+ result.set(header, offset);
1517
+ offset += header.length;
1518
+ result.set(padding, offset);
1519
+ offset += padding.length;
1520
+ result.set(body, offset);
1521
+ return result;
1522
+ }
1523
+ async sleep(ms) {
1524
+ return new Promise((resolve) => setTimeout(resolve, ms));
1525
+ }
1526
+ }
1087
1527
  //# sourceMappingURL=client.js.map