@qualithm/arrow-flight-sql-js 0.2.0 → 0.4.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.
- package/README.md +203 -7
- package/dist/client.d.ts +68 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +112 -1
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/proto.d.ts +40 -0
- package/dist/proto.d.ts.map +1 -1
- package/dist/proto.js +68 -0
- package/dist/proto.js.map +1 -1
- package/dist/transport-grpc-web.d.ts +66 -0
- package/dist/transport-grpc-web.d.ts.map +1 -0
- package/dist/transport-grpc-web.js +752 -0
- package/dist/transport-grpc-web.js.map +1 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +15 -6
- package/proto/Flight.proto +38 -5
- package/proto/FlightSql.proto +60 -56
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gRPC-Web Transport
|
|
3
|
+
*
|
|
4
|
+
* Transport implementation using the gRPC-web protocol for browser and
|
|
5
|
+
* Cloudflare Workers environments. Uses the native Fetch API.
|
|
6
|
+
*
|
|
7
|
+
* Limitations:
|
|
8
|
+
* - Client streaming (DoPut) and bidirectional streaming (DoExchange, Handshake)
|
|
9
|
+
* are not fully supported in gRPC-web. These methods will throw an error.
|
|
10
|
+
* - Requires a gRPC-web proxy (e.g., Envoy, grpcwebproxy) in front of the
|
|
11
|
+
* Flight SQL server.
|
|
12
|
+
*
|
|
13
|
+
* @see https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
|
|
14
|
+
*/
|
|
15
|
+
import { Runtime } from "./runtime.js";
|
|
16
|
+
import { registerTransport } from "./transport.js";
|
|
17
|
+
// gRPC-web content types
|
|
18
|
+
const GRPC_WEB_CONTENT_TYPE = "application/grpc-web+proto";
|
|
19
|
+
// const GRPC_WEB_TEXT_CONTENT_TYPE = "application/grpc-web-text+proto" // For base64 mode
|
|
20
|
+
// gRPC-web frame flags
|
|
21
|
+
const DATA_FRAME = 0x00;
|
|
22
|
+
const TRAILER_FRAME = 0x80;
|
|
23
|
+
// gRPC status codes
|
|
24
|
+
const GRPC_STATUS = {
|
|
25
|
+
OK: 0,
|
|
26
|
+
CANCELLED: 1,
|
|
27
|
+
UNKNOWN: 2,
|
|
28
|
+
INVALID_ARGUMENT: 3,
|
|
29
|
+
DEADLINE_EXCEEDED: 4,
|
|
30
|
+
NOT_FOUND: 5,
|
|
31
|
+
ALREADY_EXISTS: 6,
|
|
32
|
+
PERMISSION_DENIED: 7,
|
|
33
|
+
RESOURCE_EXHAUSTED: 8,
|
|
34
|
+
FAILED_PRECONDITION: 9,
|
|
35
|
+
ABORTED: 10,
|
|
36
|
+
OUT_OF_RANGE: 11,
|
|
37
|
+
UNIMPLEMENTED: 12,
|
|
38
|
+
INTERNAL: 13,
|
|
39
|
+
UNAVAILABLE: 14,
|
|
40
|
+
DATA_LOSS: 15,
|
|
41
|
+
UNAUTHENTICATED: 16
|
|
42
|
+
};
|
|
43
|
+
// Default values
|
|
44
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 30_000;
|
|
45
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
46
|
+
// Flight service path
|
|
47
|
+
const FLIGHT_SERVICE_PATH = "/arrow.flight.protocol.FlightService";
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Protobuf Wire Format Helpers
|
|
50
|
+
// ============================================================================
|
|
51
|
+
/**
|
|
52
|
+
* Encode a varint (variable-length integer)
|
|
53
|
+
*/
|
|
54
|
+
function encodeVarint(value) {
|
|
55
|
+
const bytes = [];
|
|
56
|
+
let v = value >>> 0;
|
|
57
|
+
while (v > 0x7f) {
|
|
58
|
+
bytes.push((v & 0x7f) | 0x80);
|
|
59
|
+
v >>>= 7;
|
|
60
|
+
}
|
|
61
|
+
bytes.push(v);
|
|
62
|
+
return new Uint8Array(bytes);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Decode a varint from a buffer at a given offset
|
|
66
|
+
* Returns [value, bytesRead]
|
|
67
|
+
*/
|
|
68
|
+
function decodeVarint(buffer, offset) {
|
|
69
|
+
let result = 0;
|
|
70
|
+
let shift = 0;
|
|
71
|
+
let bytesRead = 0;
|
|
72
|
+
while (offset + bytesRead < buffer.length) {
|
|
73
|
+
const byte = buffer[offset + bytesRead];
|
|
74
|
+
result |= (byte & 0x7f) << shift;
|
|
75
|
+
bytesRead++;
|
|
76
|
+
if ((byte & 0x80) === 0) {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
shift += 7;
|
|
80
|
+
}
|
|
81
|
+
return [result >>> 0, bytesRead];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Encode a field tag
|
|
85
|
+
*/
|
|
86
|
+
function encodeTag(fieldNumber, wireType) {
|
|
87
|
+
return encodeVarint((fieldNumber << 3) | wireType);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Encode a string field
|
|
91
|
+
*/
|
|
92
|
+
function encodeStringField(fieldNumber, value) {
|
|
93
|
+
const encoder = new TextEncoder();
|
|
94
|
+
const bytes = encoder.encode(value);
|
|
95
|
+
return encodeBytesField(fieldNumber, bytes);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Encode a bytes field
|
|
99
|
+
*/
|
|
100
|
+
function encodeBytesField(fieldNumber, value) {
|
|
101
|
+
const tag = encodeTag(fieldNumber, 2); // wire type 2 = length-delimited
|
|
102
|
+
const length = encodeVarint(value.length);
|
|
103
|
+
const result = new Uint8Array(tag.length + length.length + value.length);
|
|
104
|
+
result.set(tag, 0);
|
|
105
|
+
result.set(length, tag.length);
|
|
106
|
+
result.set(value, tag.length + length.length);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Encode a varint field
|
|
111
|
+
*/
|
|
112
|
+
function encodeVarintField(fieldNumber, value) {
|
|
113
|
+
const tag = encodeTag(fieldNumber, 0); // wire type 0 = varint
|
|
114
|
+
const varint = encodeVarint(value);
|
|
115
|
+
const result = new Uint8Array(tag.length + varint.length);
|
|
116
|
+
result.set(tag, 0);
|
|
117
|
+
result.set(varint, tag.length);
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Concatenate multiple Uint8Arrays
|
|
122
|
+
*/
|
|
123
|
+
function concat(...arrays) {
|
|
124
|
+
const filtered = arrays.filter((a) => a.length > 0);
|
|
125
|
+
if (filtered.length === 0) {
|
|
126
|
+
return new Uint8Array(0);
|
|
127
|
+
}
|
|
128
|
+
if (filtered.length === 1) {
|
|
129
|
+
return filtered[0];
|
|
130
|
+
}
|
|
131
|
+
const totalLength = filtered.reduce((sum, arr) => sum + arr.length, 0);
|
|
132
|
+
const result = new Uint8Array(totalLength);
|
|
133
|
+
let offset = 0;
|
|
134
|
+
for (const arr of filtered) {
|
|
135
|
+
result.set(arr, offset);
|
|
136
|
+
offset += arr.length;
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Protobuf Message Encoders
|
|
142
|
+
// ============================================================================
|
|
143
|
+
/**
|
|
144
|
+
* Encode a FlightDescriptor message
|
|
145
|
+
*/
|
|
146
|
+
function encodeFlightDescriptor(descriptor) {
|
|
147
|
+
const parts = [];
|
|
148
|
+
// field 1: type (enum as varint)
|
|
149
|
+
parts.push(encodeVarintField(1, descriptor.type));
|
|
150
|
+
// field 2: cmd (bytes)
|
|
151
|
+
if (descriptor.cmd && descriptor.cmd.length > 0) {
|
|
152
|
+
parts.push(encodeBytesField(2, descriptor.cmd));
|
|
153
|
+
}
|
|
154
|
+
// field 3: path (repeated string)
|
|
155
|
+
if (descriptor.path) {
|
|
156
|
+
for (const p of descriptor.path) {
|
|
157
|
+
parts.push(encodeStringField(3, p));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return concat(...parts);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Encode a Ticket message
|
|
164
|
+
*/
|
|
165
|
+
function encodeTicket(ticket) {
|
|
166
|
+
return encodeBytesField(1, ticket.ticket);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Encode an Action message
|
|
170
|
+
*/
|
|
171
|
+
function encodeAction(action) {
|
|
172
|
+
const parts = [];
|
|
173
|
+
// field 1: type (string)
|
|
174
|
+
parts.push(encodeStringField(1, action.type));
|
|
175
|
+
// field 2: body (bytes)
|
|
176
|
+
if (action.body && action.body.length > 0) {
|
|
177
|
+
parts.push(encodeBytesField(2, action.body));
|
|
178
|
+
}
|
|
179
|
+
return concat(...parts);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Encode a Criteria message
|
|
183
|
+
*/
|
|
184
|
+
function encodeCriteria(expression) {
|
|
185
|
+
if (!expression || expression.length === 0) {
|
|
186
|
+
return new Uint8Array(0);
|
|
187
|
+
}
|
|
188
|
+
return encodeBytesField(1, expression);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Parse protobuf fields from a buffer
|
|
192
|
+
*/
|
|
193
|
+
function parseProtoFields(buffer) {
|
|
194
|
+
const fields = [];
|
|
195
|
+
let offset = 0;
|
|
196
|
+
while (offset < buffer.length) {
|
|
197
|
+
const [tag, tagBytes] = decodeVarint(buffer, offset);
|
|
198
|
+
offset += tagBytes;
|
|
199
|
+
const fieldNumber = tag >>> 3;
|
|
200
|
+
const wireType = tag & 0x07;
|
|
201
|
+
if (wireType === 0) {
|
|
202
|
+
// Varint
|
|
203
|
+
const [value, valueBytes] = decodeVarint(buffer, offset);
|
|
204
|
+
offset += valueBytes;
|
|
205
|
+
fields.push({ fieldNumber, wireType, data: value });
|
|
206
|
+
}
|
|
207
|
+
else if (wireType === 2) {
|
|
208
|
+
// Length-delimited
|
|
209
|
+
const [length, lengthBytes] = decodeVarint(buffer, offset);
|
|
210
|
+
offset += lengthBytes;
|
|
211
|
+
const data = buffer.slice(offset, offset + length);
|
|
212
|
+
offset += length;
|
|
213
|
+
fields.push({ fieldNumber, wireType, data });
|
|
214
|
+
}
|
|
215
|
+
else if (wireType === 1) {
|
|
216
|
+
// 64-bit fixed
|
|
217
|
+
const data = buffer.slice(offset, offset + 8);
|
|
218
|
+
offset += 8;
|
|
219
|
+
fields.push({ fieldNumber, wireType, data });
|
|
220
|
+
}
|
|
221
|
+
else if (wireType === 5) {
|
|
222
|
+
// 32-bit fixed
|
|
223
|
+
const data = buffer.slice(offset, offset + 4);
|
|
224
|
+
offset += 4;
|
|
225
|
+
fields.push({ fieldNumber, wireType, data });
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// Unknown wire type, skip
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return fields;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Get a bytes field value
|
|
236
|
+
*/
|
|
237
|
+
function getBytesField(fields, fieldNumber) {
|
|
238
|
+
const field = fields.find((f) => f.fieldNumber === fieldNumber && f.wireType === 2);
|
|
239
|
+
return field?.data instanceof Uint8Array ? field.data : undefined;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Get a string field value
|
|
243
|
+
*/
|
|
244
|
+
function getStringField(fields, fieldNumber) {
|
|
245
|
+
const bytes = getBytesField(fields, fieldNumber);
|
|
246
|
+
if (!bytes) {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
return new TextDecoder().decode(bytes);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get a varint field value
|
|
253
|
+
*/
|
|
254
|
+
function getVarintField(fields, fieldNumber) {
|
|
255
|
+
const field = fields.find((f) => f.fieldNumber === fieldNumber && f.wireType === 0);
|
|
256
|
+
return typeof field?.data === "number" ? field.data : undefined;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Get all repeated bytes fields
|
|
260
|
+
*/
|
|
261
|
+
function getRepeatedBytesField(fields, fieldNumber) {
|
|
262
|
+
return fields
|
|
263
|
+
.filter((f) => f.fieldNumber === fieldNumber && f.wireType === 2 && f.data instanceof Uint8Array)
|
|
264
|
+
.map((f) => f.data);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Decode a FlightInfo message
|
|
268
|
+
*/
|
|
269
|
+
function decodeFlightInfo(buffer) {
|
|
270
|
+
const fields = parseProtoFields(buffer);
|
|
271
|
+
const result = {};
|
|
272
|
+
// field 1: schema
|
|
273
|
+
const schema = getBytesField(fields, 1);
|
|
274
|
+
if (schema) {
|
|
275
|
+
result.schema = schema;
|
|
276
|
+
}
|
|
277
|
+
// field 2: flight_descriptor (nested message)
|
|
278
|
+
const descriptorBytes = getBytesField(fields, 2);
|
|
279
|
+
if (descriptorBytes) {
|
|
280
|
+
const descFields = parseProtoFields(descriptorBytes);
|
|
281
|
+
result.flightDescriptor = {
|
|
282
|
+
type: getVarintField(descFields, 1) ?? 0,
|
|
283
|
+
cmd: getBytesField(descFields, 2),
|
|
284
|
+
path: getRepeatedBytesField(descFields, 3).map((b) => new TextDecoder().decode(b))
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
// field 3: endpoint (repeated, nested message)
|
|
288
|
+
const endpointMessages = getRepeatedBytesField(fields, 3);
|
|
289
|
+
if (endpointMessages.length > 0) {
|
|
290
|
+
result.endpoint = endpointMessages.map((epBytes) => {
|
|
291
|
+
const epFields = parseProtoFields(epBytes);
|
|
292
|
+
const ep = {};
|
|
293
|
+
// field 1: ticket
|
|
294
|
+
const ticketBytes = getBytesField(epFields, 1);
|
|
295
|
+
if (ticketBytes) {
|
|
296
|
+
const ticketFields = parseProtoFields(ticketBytes);
|
|
297
|
+
const ticket = getBytesField(ticketFields, 1);
|
|
298
|
+
if (ticket) {
|
|
299
|
+
ep.ticket = { ticket };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// field 2: location (repeated)
|
|
303
|
+
const locationMessages = getRepeatedBytesField(epFields, 2);
|
|
304
|
+
if (locationMessages.length > 0) {
|
|
305
|
+
ep.location = locationMessages.map((locBytes) => {
|
|
306
|
+
const locFields = parseProtoFields(locBytes);
|
|
307
|
+
return { uri: getStringField(locFields, 1) ?? "" };
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return ep;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
// field 4: total_records
|
|
314
|
+
const totalRecords = getVarintField(fields, 4);
|
|
315
|
+
if (totalRecords !== undefined) {
|
|
316
|
+
result.totalRecords = totalRecords;
|
|
317
|
+
}
|
|
318
|
+
// field 5: total_bytes
|
|
319
|
+
const totalBytes = getVarintField(fields, 5);
|
|
320
|
+
if (totalBytes !== undefined) {
|
|
321
|
+
result.totalBytes = totalBytes;
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Decode a FlightData message
|
|
327
|
+
*/
|
|
328
|
+
function decodeFlightData(buffer) {
|
|
329
|
+
const fields = parseProtoFields(buffer);
|
|
330
|
+
const result = {};
|
|
331
|
+
// field 1: flight_descriptor
|
|
332
|
+
const descriptorBytes = getBytesField(fields, 1);
|
|
333
|
+
if (descriptorBytes) {
|
|
334
|
+
const descFields = parseProtoFields(descriptorBytes);
|
|
335
|
+
result.flightDescriptor = {
|
|
336
|
+
type: getVarintField(descFields, 1) ?? 0,
|
|
337
|
+
cmd: getBytesField(descFields, 2),
|
|
338
|
+
path: getRepeatedBytesField(descFields, 3).map((b) => new TextDecoder().decode(b))
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
// field 2: data_header
|
|
342
|
+
result.dataHeader = getBytesField(fields, 2);
|
|
343
|
+
// field 1000: data_body
|
|
344
|
+
result.dataBody = getBytesField(fields, 1000);
|
|
345
|
+
// field 3: app_metadata
|
|
346
|
+
result.appMetadata = getBytesField(fields, 3);
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Decode a Result message (from DoAction)
|
|
351
|
+
*/
|
|
352
|
+
function decodeActionResult(buffer) {
|
|
353
|
+
const fields = parseProtoFields(buffer);
|
|
354
|
+
return {
|
|
355
|
+
body: getBytesField(fields, 1)
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Decode an ActionType message
|
|
360
|
+
*/
|
|
361
|
+
function decodeActionType(buffer) {
|
|
362
|
+
const fields = parseProtoFields(buffer);
|
|
363
|
+
return {
|
|
364
|
+
type: getStringField(fields, 1) ?? "",
|
|
365
|
+
description: getStringField(fields, 2)
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Decode a SchemaResult message
|
|
370
|
+
*/
|
|
371
|
+
function decodeSchemaResult(buffer) {
|
|
372
|
+
const fields = parseProtoFields(buffer);
|
|
373
|
+
return {
|
|
374
|
+
schema: getBytesField(fields, 1) ?? new Uint8Array(0)
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
// ============================================================================
|
|
378
|
+
// gRPC-Web Framing
|
|
379
|
+
// ============================================================================
|
|
380
|
+
/**
|
|
381
|
+
* Frame a message for gRPC-web (5-byte header + payload)
|
|
382
|
+
*/
|
|
383
|
+
function frameMessage(data) {
|
|
384
|
+
const { length } = data;
|
|
385
|
+
const frame = new Uint8Array(5 + length);
|
|
386
|
+
// Byte 0: flags (0x00 = data frame)
|
|
387
|
+
frame[0] = DATA_FRAME;
|
|
388
|
+
// Bytes 1-4: length (big-endian)
|
|
389
|
+
frame[1] = (length >>> 24) & 0xff;
|
|
390
|
+
frame[2] = (length >>> 16) & 0xff;
|
|
391
|
+
frame[3] = (length >>> 8) & 0xff;
|
|
392
|
+
frame[4] = length & 0xff;
|
|
393
|
+
// Payload
|
|
394
|
+
frame.set(data, 5);
|
|
395
|
+
return frame.buffer;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Parse gRPC-web frames from response body
|
|
399
|
+
*/
|
|
400
|
+
async function* parseGrpcWebFrames(reader) {
|
|
401
|
+
let buffer = new Uint8Array(0);
|
|
402
|
+
let streamDone = false;
|
|
403
|
+
while (!streamDone) {
|
|
404
|
+
const { done, value } = await reader.read();
|
|
405
|
+
if (value) {
|
|
406
|
+
// Append new data to buffer
|
|
407
|
+
const newBuffer = new Uint8Array(buffer.length + value.length);
|
|
408
|
+
newBuffer.set(buffer, 0);
|
|
409
|
+
newBuffer.set(value, buffer.length);
|
|
410
|
+
buffer = newBuffer;
|
|
411
|
+
}
|
|
412
|
+
// Parse complete frames
|
|
413
|
+
while (buffer.length >= 5) {
|
|
414
|
+
const flags = buffer[0];
|
|
415
|
+
const length = (buffer[1] << 24) | (buffer[2] << 16) | (buffer[3] << 8) | buffer[4];
|
|
416
|
+
if (buffer.length < 5 + length) {
|
|
417
|
+
// Not enough data for complete frame
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
const frameData = buffer.slice(5, 5 + length);
|
|
421
|
+
buffer = buffer.slice(5 + length);
|
|
422
|
+
const isTrailer = (flags & TRAILER_FRAME) !== 0;
|
|
423
|
+
yield { isTrailer, data: frameData };
|
|
424
|
+
}
|
|
425
|
+
if (done) {
|
|
426
|
+
streamDone = true;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Parse gRPC trailers from trailer frame
|
|
432
|
+
*/
|
|
433
|
+
function parseTrailers(data) {
|
|
434
|
+
const text = new TextDecoder().decode(data);
|
|
435
|
+
const lines = text.split("\r\n");
|
|
436
|
+
let status = GRPC_STATUS.UNKNOWN;
|
|
437
|
+
let message;
|
|
438
|
+
for (const line of lines) {
|
|
439
|
+
const colonIndex = line.indexOf(":");
|
|
440
|
+
if (colonIndex === -1) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const key = line.slice(0, colonIndex).trim().toLowerCase();
|
|
444
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
445
|
+
if (key === "grpc-status") {
|
|
446
|
+
const parsed = parseInt(value, 10);
|
|
447
|
+
if (!Number.isNaN(parsed)) {
|
|
448
|
+
status = parsed;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else if (key === "grpc-message") {
|
|
452
|
+
message = decodeURIComponent(value);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return { status, message };
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Create a TransportError from gRPC status
|
|
459
|
+
*/
|
|
460
|
+
function createTransportError(status, message) {
|
|
461
|
+
const error = new Error(message ?? `gRPC error: status ${String(status)}`);
|
|
462
|
+
error.code = status;
|
|
463
|
+
error.details = message;
|
|
464
|
+
return error;
|
|
465
|
+
}
|
|
466
|
+
// ============================================================================
|
|
467
|
+
// GrpcWebTransport
|
|
468
|
+
// ============================================================================
|
|
469
|
+
/**
|
|
470
|
+
* Transport implementation using gRPC-web protocol.
|
|
471
|
+
* Works with browsers and Cloudflare Workers.
|
|
472
|
+
*
|
|
473
|
+
* Requires a gRPC-web proxy in front of the Flight SQL server.
|
|
474
|
+
*/
|
|
475
|
+
export class GrpcWebTransport {
|
|
476
|
+
options;
|
|
477
|
+
authToken = null;
|
|
478
|
+
connected = false;
|
|
479
|
+
baseUrl;
|
|
480
|
+
constructor(options) {
|
|
481
|
+
this.options = {
|
|
482
|
+
...options,
|
|
483
|
+
tls: options.tls,
|
|
484
|
+
connectTimeoutMs: options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS,
|
|
485
|
+
requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
|
|
486
|
+
};
|
|
487
|
+
const protocol = this.options.tls ? "https" : "http";
|
|
488
|
+
this.baseUrl = `${protocol}://${this.options.host}:${String(this.options.port)}`;
|
|
489
|
+
}
|
|
490
|
+
async connect() {
|
|
491
|
+
if (this.connected) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
// For gRPC-web, we don't establish a persistent connection
|
|
495
|
+
// Just verify the server is reachable with a simple request
|
|
496
|
+
try {
|
|
497
|
+
const controller = new AbortController();
|
|
498
|
+
const timeoutId = setTimeout(() => {
|
|
499
|
+
controller.abort();
|
|
500
|
+
}, this.options.connectTimeoutMs);
|
|
501
|
+
// Send an empty OPTIONS request to check connectivity
|
|
502
|
+
const response = await fetch(this.baseUrl, {
|
|
503
|
+
method: "OPTIONS",
|
|
504
|
+
signal: controller.signal
|
|
505
|
+
}).catch(() => null);
|
|
506
|
+
clearTimeout(timeoutId);
|
|
507
|
+
// Even if OPTIONS fails (which is expected), if we got here
|
|
508
|
+
// without aborting, the server is reachable
|
|
509
|
+
this.connected = true;
|
|
510
|
+
// Check for CORS errors
|
|
511
|
+
if (response === null) {
|
|
512
|
+
// Connection was aborted or failed
|
|
513
|
+
throw new Error(`Failed to connect to ${this.baseUrl}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
if (error.name === "AbortError") {
|
|
518
|
+
throw new Error(`Connection timeout after ${String(this.options.connectTimeoutMs)}ms`);
|
|
519
|
+
}
|
|
520
|
+
// Allow connection even if OPTIONS fails - the actual RPC calls will fail if needed
|
|
521
|
+
this.connected = true;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
close() {
|
|
525
|
+
this.connected = false;
|
|
526
|
+
this.authToken = null;
|
|
527
|
+
}
|
|
528
|
+
isConnected() {
|
|
529
|
+
return this.connected;
|
|
530
|
+
}
|
|
531
|
+
setAuthToken(token) {
|
|
532
|
+
this.authToken = token;
|
|
533
|
+
}
|
|
534
|
+
// --------------------------------------------------------------------------
|
|
535
|
+
// Unary Calls
|
|
536
|
+
// --------------------------------------------------------------------------
|
|
537
|
+
async getFlightInfo(descriptor, metadata) {
|
|
538
|
+
const requestData = encodeFlightDescriptor(descriptor);
|
|
539
|
+
const responseData = await this.unaryCall(`${FLIGHT_SERVICE_PATH}/GetFlightInfo`, requestData, metadata);
|
|
540
|
+
return decodeFlightInfo(responseData);
|
|
541
|
+
}
|
|
542
|
+
async getSchema(descriptor, metadata) {
|
|
543
|
+
const requestData = encodeFlightDescriptor(descriptor);
|
|
544
|
+
const responseData = await this.unaryCall(`${FLIGHT_SERVICE_PATH}/GetSchema`, requestData, metadata);
|
|
545
|
+
return decodeSchemaResult(responseData);
|
|
546
|
+
}
|
|
547
|
+
// --------------------------------------------------------------------------
|
|
548
|
+
// Server Streaming
|
|
549
|
+
// --------------------------------------------------------------------------
|
|
550
|
+
doGet(ticket, metadata) {
|
|
551
|
+
const requestData = encodeTicket(ticket);
|
|
552
|
+
return this.serverStreamingCall(`${FLIGHT_SERVICE_PATH}/DoGet`, requestData, decodeFlightData, metadata);
|
|
553
|
+
}
|
|
554
|
+
doAction(action, metadata) {
|
|
555
|
+
const requestData = encodeAction(action);
|
|
556
|
+
return this.serverStreamingCall(`${FLIGHT_SERVICE_PATH}/DoAction`, requestData, decodeActionResult, metadata);
|
|
557
|
+
}
|
|
558
|
+
listActions(metadata) {
|
|
559
|
+
// Empty message for ListActions
|
|
560
|
+
const requestData = new Uint8Array(0);
|
|
561
|
+
return this.serverStreamingCall(`${FLIGHT_SERVICE_PATH}/ListActions`, requestData, decodeActionType, metadata);
|
|
562
|
+
}
|
|
563
|
+
listFlights(criteria, metadata) {
|
|
564
|
+
const requestData = encodeCriteria(criteria);
|
|
565
|
+
return this.serverStreamingCall(`${FLIGHT_SERVICE_PATH}/ListFlights`, requestData, decodeFlightInfo, metadata);
|
|
566
|
+
}
|
|
567
|
+
// --------------------------------------------------------------------------
|
|
568
|
+
// Bidirectional Streaming (Not Supported)
|
|
569
|
+
// --------------------------------------------------------------------------
|
|
570
|
+
doPut(_metadata) {
|
|
571
|
+
throw new Error("DoPut (client streaming) is not supported in gRPC-web. " +
|
|
572
|
+
"Use a server-side runtime (Node.js/Bun) or a different protocol for uploads.");
|
|
573
|
+
}
|
|
574
|
+
doExchange(_metadata) {
|
|
575
|
+
throw new Error("DoExchange (bidirectional streaming) is not supported in gRPC-web. " +
|
|
576
|
+
"Use a server-side runtime (Node.js/Bun) or WebSocket-based transport for bidirectional communication.");
|
|
577
|
+
}
|
|
578
|
+
handshake(_metadata) {
|
|
579
|
+
throw new Error("Handshake (bidirectional streaming) is not supported in gRPC-web. " +
|
|
580
|
+
"Use bearer token authentication via setAuthToken() instead.");
|
|
581
|
+
}
|
|
582
|
+
// --------------------------------------------------------------------------
|
|
583
|
+
// Private Helpers
|
|
584
|
+
// --------------------------------------------------------------------------
|
|
585
|
+
createHeaders(metadata) {
|
|
586
|
+
const headers = new Headers();
|
|
587
|
+
headers.set("Content-Type", GRPC_WEB_CONTENT_TYPE);
|
|
588
|
+
headers.set("Accept", GRPC_WEB_CONTENT_TYPE);
|
|
589
|
+
headers.set("X-Grpc-Web", "1");
|
|
590
|
+
headers.set("X-User-Agent", "grpc-web-js/0.1");
|
|
591
|
+
// Add auth token
|
|
592
|
+
if (this.authToken !== null && this.authToken !== "") {
|
|
593
|
+
headers.set("Authorization", `Bearer ${this.authToken}`);
|
|
594
|
+
}
|
|
595
|
+
// Add custom metadata
|
|
596
|
+
if (metadata) {
|
|
597
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
598
|
+
if (Array.isArray(value)) {
|
|
599
|
+
headers.set(key, value.join(", "));
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
headers.set(key, value);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return headers;
|
|
607
|
+
}
|
|
608
|
+
async unaryCall(path, requestData, metadata) {
|
|
609
|
+
this.ensureConnected();
|
|
610
|
+
const controller = new AbortController();
|
|
611
|
+
const timeoutId = setTimeout(() => {
|
|
612
|
+
controller.abort();
|
|
613
|
+
}, this.options.requestTimeoutMs);
|
|
614
|
+
try {
|
|
615
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
616
|
+
method: "POST",
|
|
617
|
+
headers: this.createHeaders(metadata),
|
|
618
|
+
body: frameMessage(requestData),
|
|
619
|
+
signal: controller.signal
|
|
620
|
+
});
|
|
621
|
+
clearTimeout(timeoutId);
|
|
622
|
+
if (!response.ok) {
|
|
623
|
+
throw createTransportError(GRPC_STATUS.INTERNAL, `HTTP error: ${String(response.status)} ${response.statusText}`);
|
|
624
|
+
}
|
|
625
|
+
const reader = response.body?.getReader();
|
|
626
|
+
if (!reader) {
|
|
627
|
+
throw createTransportError(GRPC_STATUS.INTERNAL, "No response body");
|
|
628
|
+
}
|
|
629
|
+
let responseData = null;
|
|
630
|
+
let grpcStatus = null;
|
|
631
|
+
for await (const frame of parseGrpcWebFrames(reader)) {
|
|
632
|
+
if (frame.isTrailer) {
|
|
633
|
+
grpcStatus = parseTrailers(frame.data);
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
responseData = frame.data;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// Check gRPC status
|
|
640
|
+
if (grpcStatus && grpcStatus.status !== GRPC_STATUS.OK) {
|
|
641
|
+
throw createTransportError(grpcStatus.status, grpcStatus.message);
|
|
642
|
+
}
|
|
643
|
+
if (!responseData) {
|
|
644
|
+
throw createTransportError(GRPC_STATUS.INTERNAL, "No response data");
|
|
645
|
+
}
|
|
646
|
+
return responseData;
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
clearTimeout(timeoutId);
|
|
650
|
+
if (error.name === "AbortError") {
|
|
651
|
+
throw createTransportError(GRPC_STATUS.DEADLINE_EXCEEDED, "Request timeout");
|
|
652
|
+
}
|
|
653
|
+
throw error;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
serverStreamingCall(path, requestData, decoder, metadata) {
|
|
657
|
+
this.ensureConnected();
|
|
658
|
+
// Use an object wrapper so closures can share mutable state
|
|
659
|
+
const state = { cancelled: false };
|
|
660
|
+
let controller = null;
|
|
661
|
+
let reader = null;
|
|
662
|
+
const asyncIterable = {
|
|
663
|
+
[Symbol.asyncIterator]: () => {
|
|
664
|
+
return {
|
|
665
|
+
next: async () => {
|
|
666
|
+
if (state.cancelled) {
|
|
667
|
+
return { done: true, value: undefined };
|
|
668
|
+
}
|
|
669
|
+
// Initialize on first call
|
|
670
|
+
if (!controller) {
|
|
671
|
+
controller = new AbortController();
|
|
672
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
673
|
+
method: "POST",
|
|
674
|
+
headers: this.createHeaders(metadata),
|
|
675
|
+
body: frameMessage(requestData),
|
|
676
|
+
signal: controller.signal
|
|
677
|
+
});
|
|
678
|
+
if (!response.ok) {
|
|
679
|
+
throw createTransportError(GRPC_STATUS.INTERNAL, `HTTP error: ${String(response.status)} ${response.statusText}`);
|
|
680
|
+
}
|
|
681
|
+
if (!response.body) {
|
|
682
|
+
throw createTransportError(GRPC_STATUS.INTERNAL, "No response body");
|
|
683
|
+
}
|
|
684
|
+
reader = response.body.getReader();
|
|
685
|
+
}
|
|
686
|
+
// Read next frame
|
|
687
|
+
if (!reader) {
|
|
688
|
+
return { done: true, value: undefined };
|
|
689
|
+
}
|
|
690
|
+
// We need to maintain state across next() calls, so we use a
|
|
691
|
+
// frame generator that we consume incrementally
|
|
692
|
+
const frameIterator = parseGrpcWebFrames(reader);
|
|
693
|
+
for await (const frame of frameIterator) {
|
|
694
|
+
// This can be set by cancel() or return() from another async context
|
|
695
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
696
|
+
if (state.cancelled) {
|
|
697
|
+
return { done: true, value: undefined };
|
|
698
|
+
}
|
|
699
|
+
if (frame.isTrailer) {
|
|
700
|
+
const status = parseTrailers(frame.data);
|
|
701
|
+
if (status.status !== GRPC_STATUS.OK) {
|
|
702
|
+
throw createTransportError(status.status, status.message);
|
|
703
|
+
}
|
|
704
|
+
return { done: true, value: undefined };
|
|
705
|
+
}
|
|
706
|
+
return { done: false, value: decoder(frame.data) };
|
|
707
|
+
}
|
|
708
|
+
return { done: true, value: undefined };
|
|
709
|
+
},
|
|
710
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
711
|
+
return: async () => {
|
|
712
|
+
state.cancelled = true;
|
|
713
|
+
controller?.abort();
|
|
714
|
+
return { done: true, value: undefined };
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
return {
|
|
720
|
+
...asyncIterable,
|
|
721
|
+
[Symbol.asyncIterator]: asyncIterable[Symbol.asyncIterator],
|
|
722
|
+
cancel: () => {
|
|
723
|
+
state.cancelled = true;
|
|
724
|
+
controller?.abort();
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
ensureConnected() {
|
|
729
|
+
if (!this.connected) {
|
|
730
|
+
throw new Error("Transport not connected. Call connect() first.");
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Create a GrpcWebTransport instance
|
|
736
|
+
*/
|
|
737
|
+
export function createGrpcWebTransport(options) {
|
|
738
|
+
return new GrpcWebTransport(options);
|
|
739
|
+
}
|
|
740
|
+
// Register for browser and worker runtimes
|
|
741
|
+
registerTransport(Runtime.Browser, createGrpcWebTransport);
|
|
742
|
+
// Deno can also use gRPC-web since it has fetch support
|
|
743
|
+
registerTransport(Runtime.Deno, createGrpcWebTransport);
|
|
744
|
+
/**
|
|
745
|
+
* Get the appropriate web transport for browser environments.
|
|
746
|
+
*
|
|
747
|
+
* @throws Error if called in a non-browser environment
|
|
748
|
+
*/
|
|
749
|
+
export function getWebTransport(options) {
|
|
750
|
+
return new GrpcWebTransport(options);
|
|
751
|
+
}
|
|
752
|
+
//# sourceMappingURL=transport-grpc-web.js.map
|