@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/README.md +80 -0
- package/dist/arrow.js +1 -1
- package/dist/arrow.js.map +1 -1
- package/dist/client.d.ts +165 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +486 -46
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/metrics.d.ts +20 -16
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +12 -7
- package/dist/metrics.js.map +1 -1
- package/dist/pool.d.ts +1 -1
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +1 -1
- package/dist/pool.js.map +1 -1
- package/dist/proto.d.ts +2 -2
- package/dist/proto.d.ts.map +1 -1
- package/dist/proto.js +1 -1
- package/dist/proto.js.map +1 -1
- package/dist/query-builder.d.ts +362 -0
- package/dist/query-builder.d.ts.map +1 -1
- package/dist/query-builder.js +732 -2
- package/dist/query-builder.js.map +1 -1
- package/dist/retry.d.ts +2 -2
- package/dist/retry.d.ts.map +1 -1
- package/dist/retry.js +3 -3
- package/dist/retry.js.map +1 -1
- package/dist/types.d.ts +137 -58
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +25 -0
- package/dist/types.js.map +1 -1
- package/package.json +3 -2
- package/proto/Flight.proto +0 -645
- package/proto/FlightSql.proto +0 -1925
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
|
|
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
|
|
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
|
|
296
|
-
schemaName: row
|
|
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
|
|
337
|
-
schemaName: row
|
|
338
|
-
tableName: row
|
|
339
|
-
tableType: row
|
|
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
|
|
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
|
|
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
|
|
398
|
-
schemaName: row
|
|
399
|
-
tableName: row
|
|
400
|
-
columnName: row
|
|
401
|
-
keySequence: row
|
|
402
|
-
keyName: row
|
|
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
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
489
|
-
pkSchemaName: row
|
|
490
|
-
pkTableName: row
|
|
491
|
-
pkColumnName: row
|
|
492
|
-
fkCatalogName: row
|
|
493
|
-
fkSchemaName: row
|
|
494
|
-
fkTableName: row
|
|
495
|
-
fkColumnName: row
|
|
496
|
-
keySequence: row
|
|
497
|
-
fkKeyName: row
|
|
498
|
-
pkKeyName: row
|
|
499
|
-
updateRule: row
|
|
500
|
-
deleteRule: row
|
|
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
|
-
|
|
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
|
|
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
|