Skip to content

Commit 21b16c5

Browse files
committed
Improve the gRPC initialization code
1 parent 71ef999 commit 21b16c5

1 file changed

Lines changed: 118 additions & 120 deletions

File tree

src/client.ts

Lines changed: 118 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@ import {
1717
getIpcMessage,
1818
} from './flight';
1919
import {
20-
AsyncQueryRequest,
21-
AsyncQueryResponse,
22-
QueryCompleteNotification,
23-
QueryResultsResponse,
2420
RefreshOverrides,
2521
type SpiceClientConfig,
2622
} from './interfaces';
@@ -109,10 +105,9 @@ async function loadProtoFile(): Promise<string | null> {
109105

110106
let flightProto: any = null;
111107
let grpcAvailable = false;
112-
let protoInitPromise: Promise<void> | null = null;
113108

114109
/**
115-
* Initialize the proto file (sync attempt, async fallback)
110+
* Initialize the proto file (sync attempt)
116111
*/
117112
function initializeProto(): void {
118113
try {
@@ -129,15 +124,7 @@ function initializeProto(): void {
129124
flightProto = arrow.flight.protocol;
130125
grpcAvailable = true;
131126
} catch (error: any) {
132-
if (error.code === 'ENOENT') {
133-
console.warn(
134-
'[spice.js] Local Flight.proto not found. Will attempt to download on first query.',
135-
);
136-
} else {
137-
console.warn(
138-
`[spice.js] Failed to load gRPC Flight proto: ${error.message}`,
139-
);
140-
}
127+
// Silent failure - will attempt download during client initialization
141128
grpcAvailable = false;
142129
}
143130
}
@@ -153,7 +140,7 @@ class SpiceClient {
153140
private _flightTlsEnabled: boolean = true;
154141
private _maxRetries: number = retry.FLIGHT_QUERY_MAX_RETRIES;
155142
private _useGrpc: boolean = grpcAvailable;
156-
private _grpcInitAttempted: boolean = false;
143+
private _initPromise: Promise<void>;
157144

158145
public constructor(params: string | SpiceClientConfig = {}) {
159146
// support legacy constructor with api_key as first agument
@@ -172,42 +159,34 @@ class SpiceClient {
172159
this._flightTlsEnabled =
173160
flightTlsEnabled !== undefined
174161
? flightTlsEnabled
175-
: this._flightUrl.includes('127.0.0.1')
176-
? false
177-
: true;
162+
: !this._flightUrl.includes('127.0.0.1');
178163
// Prepend the user-supplied user agent (if any) with the default user agent
179164
this._userAgent = userAgent
180165
? `${userAgent} ${getUserAgent()}`
181166
: getUserAgent();
182167
}
168+
169+
// Initialize gRPC during construction
170+
this._initPromise = this.initializeGrpc();
183171
}
184172

185173
/**
186-
* Attempts to initialize gRPC by downloading the proto file if needed.
187-
* Returns true if gRPC is available, false otherwise.
174+
* Initializes gRPC by downloading the proto file if needed.
175+
* Called during SpiceClient construction.
188176
*/
189-
private async ensureGrpcAvailable(): Promise<boolean> {
177+
private async initializeGrpc(): Promise<void> {
190178
// If already available, return immediately
191-
if (this._useGrpc && grpcAvailable) {
192-
return true;
179+
if (this._useGrpc && grpcAvailable && flightProto) {
180+
return;
193181
}
194182

195-
// If we've already tried and failed, don't try again
196-
if (this._grpcInitAttempted) {
197-
return false;
198-
}
199-
200-
this._grpcInitAttempted = true;
201-
202183
try {
203184
// Try to load proto file (download if needed)
204185
const protoPath = await loadProtoFile();
205186

206187
if (!protoPath) {
207-
console.warn(
208-
'[spice.js] Unable to initialize gRPC. Falling back to HTTP endpoint.',
209-
);
210-
return false;
188+
this._useGrpc = false;
189+
return;
211190
}
212191

213192
// Load the proto file
@@ -220,21 +199,36 @@ class SpiceClient {
220199
});
221200

222201
const arrow = grpc.loadPackageDefinition(packageDefinition).arrow as any;
202+
203+
if (!arrow?.flight?.protocol?.FlightService) {
204+
throw new Error('Invalid proto file structure');
205+
}
206+
223207
flightProto = arrow.flight.protocol;
224208
grpcAvailable = true;
225209
this._useGrpc = true;
226-
227-
console.log('[spice.js] gRPC Flight protocol initialized successfully');
228-
return true;
229210
} catch (error: any) {
230211
console.warn(
231-
`[spice.js] Failed to initialize gRPC: ${error.message}. Falling back to HTTP.`,
212+
`[spice.js] gRPC initialization failed: ${error.message}. Using HTTP endpoint.`,
232213
);
233-
return false;
214+
this._useGrpc = false;
234215
}
235216
}
236217

237-
private createClient(meta: any): any {
218+
/**
219+
* Ensures the client is fully initialized before use.
220+
* @returns true if gRPC is available, false otherwise.
221+
*/
222+
private async ensureInitialized(): Promise<boolean> {
223+
await this._initPromise;
224+
return this._useGrpc && grpcAvailable && flightProto !== null;
225+
}
226+
227+
private createClient(meta: grpc.Metadata): FlightClient {
228+
if (!flightProto?.FlightService) {
229+
throw new Error('gRPC Flight protocol not initialized');
230+
}
231+
238232
// gRPC channel options
239233
// Compression support is advertised via metadata (grpc-accept-encoding header)
240234
// The server will use compression if it supports it
@@ -270,17 +264,17 @@ class SpiceClient {
270264
getFlightClient: ((client: FlightClient) => void) | undefined = undefined,
271265
): Promise<EventEmitter> {
272266
const meta = new grpc.Metadata();
273-
const client: FlightClient = this.createClient(meta);
274-
meta.set('authorization', 'Bearer ' + this._apiKey);
267+
meta.set('authorization', `Bearer ${this._apiKey || ''}`);
275268
meta.set('User-Agent', this._userAgent);
276-
277269
// Advertise that we accept compressed responses
278270
// The server can choose to compress if it supports it
279271
meta.set('grpc-accept-encoding', 'gzip,deflate');
280272

281-
let queryBuff = Buffer.from(queryText, 'utf8');
273+
const client: FlightClient = this.createClient(meta);
274+
275+
const queryBuff = Buffer.from(queryText, 'utf8');
282276

283-
let flightTicket = await new Promise<Ticket>((resolve, reject) => {
277+
const flightTicket = await new Promise<Ticket>((resolve, reject) => {
284278
// GetFlightInfo returns FlightInfo that have endpoints with ticket to call DoGet with
285279
client.GetFlightInfo(
286280
{ type: DescriptorType.CMD, cmd: queryBuff },
@@ -289,6 +283,10 @@ class SpiceClient {
289283
reject(err);
290284
return;
291285
}
286+
if (!result?.endpoint?.[0]?.ticket) {
287+
reject(new Error('Invalid FlightInfo response: missing ticket'));
288+
return;
289+
}
292290
resolve(result.endpoint[0].ticket);
293291
},
294292
);
@@ -314,59 +312,59 @@ class SpiceClient {
314312
queryText: string,
315313
onData: ((data: Table) => void) | undefined = undefined,
316314
): Promise<Table> {
317-
// Try to ensure gRPC is available (will download proto if needed)
318-
if (!this._useGrpc) {
319-
const grpcReady = await this.ensureGrpcAvailable();
320-
if (!grpcReady) {
321-
console.log('[spice.js] Using HTTP endpoint for query');
322-
return this.doHttpQueryRequest(queryText, onData);
323-
}
324-
}
315+
// Wait for initialization to complete
316+
const useGrpc = await this.ensureInitialized();
325317

326-
// If gRPC is still not available after initialization attempt, fall back to HTTP
327-
if (!this._useGrpc) {
318+
if (!useGrpc) {
328319
return this.doHttpQueryRequest(queryText, onData);
329320
}
330321

331-
let client: FlightClient;
332-
333-
const resultStream = await this.getResultStream(
334-
queryText,
335-
(c: FlightClient) => {
336-
client = c;
337-
},
338-
);
322+
let client: FlightClient | undefined;
339323

340-
// indicates that data has been partially or fully sent
341-
let isDataAlreadySent = false;
342-
343-
let schema: Buffer | undefined;
344-
let chunks: Buffer[] = [];
345-
resultStream.on('data', (response: FlightData) => {
346-
let ipcMessage = getIpcMessage(response);
347-
chunks.push(ipcMessage);
348-
if (!schema) {
349-
schema = ipcMessage;
350-
} else if (onData) {
351-
isDataAlreadySent = true;
352-
onData(tableFromIPC([schema, ipcMessage]));
353-
}
354-
});
324+
try {
325+
const resultStream = await this.getResultStream(
326+
queryText,
327+
(c: FlightClient) => {
328+
client = c;
329+
},
330+
);
355331

356-
return new Promise((resolve, reject) => {
357-
resultStream.on('status', (response: FlightStatus) => {
358-
const table = tableFromIPC(chunks);
359-
client.close();
360-
resolve(table);
332+
// indicates that data has been partially or fully sent
333+
let isDataAlreadySent = false;
334+
335+
let schema: Buffer | undefined;
336+
const chunks: Buffer[] = [];
337+
338+
resultStream.on('data', (response: FlightData) => {
339+
const ipcMessage = getIpcMessage(response);
340+
chunks.push(ipcMessage);
341+
if (!schema) {
342+
schema = ipcMessage;
343+
} else if (onData) {
344+
isDataAlreadySent = true;
345+
onData(tableFromIPC([schema, ipcMessage]));
346+
}
361347
});
362348

363-
resultStream.on('error', (err: any) => {
364-
client.close();
365-
if (isDataAlreadySent) retry.dontRetry(err);
366-
367-
reject(err);
349+
return new Promise((resolve, reject) => {
350+
resultStream.on('status', (_response: FlightStatus) => {
351+
const table = tableFromIPC(chunks);
352+
client?.close();
353+
resolve(table);
354+
});
355+
356+
resultStream.on('error', (err: any) => {
357+
client?.close();
358+
if (isDataAlreadySent) {
359+
retry.dontRetry(err);
360+
}
361+
reject(err);
362+
});
368363
});
369-
});
364+
} catch (error) {
365+
client?.close();
366+
throw error;
367+
}
370368
}
371369

372370
private async doHttpQueryRequest(
@@ -446,7 +444,6 @@ class SpiceClient {
446444

447445
private jsonToArrowTable(schema: any[], rows: any[]): Table {
448446
// Convert JSON response to Arrow Table format
449-
// Create a simple object representation that Arrow can understand
450447
const columns: { [key: string]: any[] } = {};
451448

452449
// Initialize columns
@@ -471,7 +468,7 @@ class SpiceClient {
471468
* Sets the maximum number of times to retry Query calls. The default is 3
472469
* @param maxRetries Num of max retries. Setting to 0 will disable retries
473470
*/
474-
public setMaxRetries(maxRetries: number) {
471+
public setMaxRetries(maxRetries: number): void {
475472
if (maxRetries < 0) {
476473
throw new Error('maxRetries must be greater than or equal to 0');
477474
}
@@ -482,28 +479,22 @@ class SpiceClient {
482479
public async refreshDataset(
483480
dataset: string,
484481
refresh_overrides?: RefreshOverrides,
485-
) {
486-
if (!refresh_overrides) {
487-
refresh_overrides = {
488-
refresh_sql: null,
489-
refresh_mode: null,
490-
refresh_jitter_max: null,
491-
};
492-
}
493-
494-
refresh_overrides.refresh_sql = refresh_overrides.refresh_sql || null;
495-
refresh_overrides.refresh_mode = refresh_overrides.refresh_mode || null;
496-
refresh_overrides.refresh_jitter_max =
497-
refresh_overrides.refresh_jitter_max || null;
482+
): Promise<void> {
483+
const overrides: RefreshOverrides = {
484+
refresh_sql: refresh_overrides?.refresh_sql || null,
485+
refresh_mode: refresh_overrides?.refresh_mode || null,
486+
refresh_jitter_max: refresh_overrides?.refresh_jitter_max || null,
487+
};
498488

499-
const body = JSON.stringify(refresh_overrides);
489+
const body = JSON.stringify(overrides);
500490

501491
const response = await this.fetchInternal(
502492
'POST',
503493
`/v1/datasets/${dataset}/acceleration/refresh`,
504494
undefined,
505495
body,
506496
);
497+
507498
if (response.status !== 201) {
508499
const responseText = await response.text();
509500
throw new Error(
@@ -519,35 +510,42 @@ class SpiceClient {
519510
body?: string,
520511
customHeaders?: { [key: string]: string },
521512
) {
522-
let url;
523-
if (params && Object.keys(params).length) {
524-
url = `${this._httpUrl}${path}?${new URLSearchParams(params)}`;
525-
} else {
526-
url = `${this._httpUrl}${path}`;
527-
}
513+
const url = params && Object.keys(params).length
514+
? `${this._httpUrl}${path}?${new URLSearchParams(params)}`
515+
: `${this._httpUrl}${path}`;
528516

529-
const headers = [
517+
const headers = new Headers([
530518
['Content-Type', 'application/json'],
531519
['Accept-Encoding', 'zstd, br, gzip, deflate'],
532520
['User-Agent', this._userAgent],
533-
];
521+
]);
534522

535523
// Add custom headers
536524
if (customHeaders) {
537525
Object.entries(customHeaders).forEach(([key, value]) => {
538-
headers.push([key, value]);
526+
headers.set(key, value);
539527
});
540528
}
541529

542530
if (this._apiKey) {
543-
headers.push(['X-API-Key', this._apiKey || '']);
531+
headers.set('X-API-Key', this._apiKey);
544532
}
545533

534+
const fetchOptions: RequestInit = {
535+
headers,
536+
method,
537+
body,
538+
};
539+
546540
if (this._httpUrl.startsWith('https://')) {
547-
return fetch(url, {
548-
headers: new Headers(headers),
549-
agent: httpsAgent,
550-
method,
541+
fetchOptions.agent = httpsAgent;
542+
}
543+
544+
return fetch(url, fetchOptions);
545+
}
546+
}
547+
548+
export { SpiceClient };
551549
body,
552550
});
553551
} else {

0 commit comments

Comments
 (0)