@query-farm/vgi-rpc 0.6.4 → 0.7.1
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/dist/access-log.d.ts +55 -0
- package/dist/access-log.d.ts.map +1 -0
- package/dist/arrow/impl-arrowjs/index.d.ts +96 -0
- package/dist/arrow/impl-arrowjs/index.d.ts.map +1 -0
- package/dist/arrow/impl-flechette/index.d.ts +102 -0
- package/dist/arrow/impl-flechette/index.d.ts.map +1 -0
- package/dist/arrow/impl-flechette/message-meta.d.ts +11 -0
- package/dist/arrow/impl-flechette/message-meta.d.ts.map +1 -0
- package/dist/arrow/index.d.ts +4 -0
- package/dist/arrow/index.d.ts.map +1 -0
- package/dist/arrow/predicates.d.ts +44 -0
- package/dist/arrow/predicates.d.ts.map +1 -0
- package/dist/arrow/types.d.ts +62 -0
- package/dist/arrow/types.d.ts.map +1 -0
- package/dist/auth.d.ts +5 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/client/capabilities.d.ts +25 -0
- package/dist/client/capabilities.d.ts.map +1 -0
- package/dist/client/connect.d.ts +10 -0
- package/dist/client/connect.d.ts.map +1 -1
- package/dist/client/introspect.d.ts +21 -0
- package/dist/client/introspect.d.ts.map +1 -1
- package/dist/client/ipc.d.ts +8 -2
- package/dist/client/ipc.d.ts.map +1 -1
- package/dist/client/oauth.d.ts +9 -0
- package/dist/client/oauth.d.ts.map +1 -1
- package/dist/client/pipe.d.ts +24 -0
- package/dist/client/pipe.d.ts.map +1 -1
- package/dist/client/stream.d.ts +19 -2
- package/dist/client/stream.d.ts.map +1 -1
- package/dist/client/types.d.ts +23 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/uploadUrl.d.ts +25 -0
- package/dist/client/uploadUrl.d.ts.map +1 -0
- package/dist/constants.d.ts +30 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/crypto.d.ts +22 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/dispatch/describe.d.ts +10 -6
- package/dist/dispatch/describe.d.ts.map +1 -1
- package/dist/dispatch/stream.d.ts +2 -2
- package/dist/dispatch/stream.d.ts.map +1 -1
- package/dist/dispatch/unary.d.ts +2 -2
- package/dist/dispatch/unary.d.ts.map +1 -1
- package/dist/errors.d.ts +64 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/external.d.ts +27 -5
- package/dist/external.d.ts.map +1 -1
- package/dist/http/auth.d.ts +13 -0
- package/dist/http/auth.d.ts.map +1 -1
- package/dist/http/bearer.d.ts.map +1 -1
- package/dist/http/common.d.ts +43 -7
- package/dist/http/common.d.ts.map +1 -1
- package/dist/http/dispatch.d.ts +20 -2
- package/dist/http/dispatch.d.ts.map +1 -1
- package/dist/http/handler.d.ts.map +1 -1
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/jwt.d.ts +1 -0
- package/dist/http/jwt.d.ts.map +1 -1
- package/dist/http/mtls.d.ts +9 -1
- package/dist/http/mtls.d.ts.map +1 -1
- package/dist/http/oauth-pkce.d.ts +141 -0
- package/dist/http/oauth-pkce.d.ts.map +1 -0
- package/dist/http/pages.d.ts +3 -0
- package/dist/http/pages.d.ts.map +1 -1
- package/dist/http/sticky.d.ts +124 -0
- package/dist/http/sticky.d.ts.map +1 -0
- package/dist/http/token.d.ts +43 -12
- package/dist/http/token.d.ts.map +1 -1
- package/dist/http/types.d.ts +68 -5
- package/dist/http/types.d.ts.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1275 -3511
- package/dist/index.js.map +20 -38
- package/dist/launcher/hash.d.ts +22 -0
- package/dist/launcher/hash.d.ts.map +1 -0
- package/dist/launcher/index.d.ts +23 -0
- package/dist/launcher/index.d.ts.map +1 -0
- package/dist/launcher/launch.d.ts +27 -0
- package/dist/launcher/launch.d.ts.map +1 -0
- package/dist/launcher/lock.d.ts +19 -0
- package/dist/launcher/lock.d.ts.map +1 -0
- package/dist/launcher/serve-unix.d.ts +55 -0
- package/dist/launcher/serve-unix.d.ts.map +1 -0
- package/dist/launcher/state.d.ts +71 -0
- package/dist/launcher/state.d.ts.map +1 -0
- package/dist/otel.d.ts.map +1 -1
- package/dist/protocol.d.ts +19 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/schema.d.ts +45 -18
- package/dist/schema.d.ts.map +1 -1
- package/dist/server.d.ts +23 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/types.d.ts +270 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/util/gzip.d.ts +10 -0
- package/dist/util/gzip.d.ts.map +1 -0
- package/dist/util/schema.d.ts +3 -15
- package/dist/util/schema.d.ts.map +1 -1
- package/dist/util/web-crypto.d.ts +22 -0
- package/dist/util/web-crypto.d.ts.map +1 -0
- package/dist/util/zstd.d.ts +26 -3
- package/dist/util/zstd.d.ts.map +1 -1
- package/dist/wire/opaque.d.ts +11 -0
- package/dist/wire/opaque.d.ts.map +1 -0
- package/dist/wire/reader.d.ts +5 -5
- package/dist/wire/reader.d.ts.map +1 -1
- package/dist/wire/request.d.ts +11 -3
- package/dist/wire/request.d.ts.map +1 -1
- package/dist/wire/response.d.ts +6 -6
- package/dist/wire/response.d.ts.map +1 -1
- package/dist/wire/writer.d.ts +49 -39
- package/dist/wire/writer.d.ts.map +1 -1
- package/package.json +35 -21
- package/src/access-log.ts +200 -0
- package/src/arrow/impl-arrowjs/index.ts +433 -0
- package/src/arrow/impl-flechette/index.ts +414 -0
- package/src/arrow/impl-flechette/message-meta.ts +174 -0
- package/src/arrow/index.ts +89 -0
- package/src/arrow/predicates.ts +56 -0
- package/src/arrow/types.ts +73 -0
- package/src/auth.ts +5 -0
- package/src/client/capabilities.ts +84 -0
- package/src/client/connect.ts +113 -26
- package/src/client/introspect.ts +74 -38
- package/src/client/ipc.ts +37 -27
- package/src/client/oauth.ts +9 -0
- package/src/client/pipe.ts +36 -9
- package/src/client/stream.ts +43 -20
- package/src/client/types.ts +23 -0
- package/src/client/uploadUrl.ts +169 -0
- package/src/constants.ts +34 -2
- package/src/crypto.ts +95 -0
- package/src/dispatch/describe.ts +146 -107
- package/src/dispatch/stream.ts +53 -24
- package/src/dispatch/unary.ts +5 -4
- package/src/errors.ts +87 -0
- package/src/external.ts +49 -30
- package/src/http/auth.ts +13 -0
- package/src/http/bearer.ts +2 -5
- package/src/http/common.ts +91 -23
- package/src/http/dispatch.ts +373 -46
- package/src/http/handler.ts +790 -68
- package/src/http/index.ts +1 -0
- package/src/http/jwt.ts +1 -0
- package/src/http/mtls.ts +25 -3
- package/src/http/oauth-pkce.ts +1035 -0
- package/src/http/pages.ts +30 -15
- package/src/http/sticky.ts +429 -0
- package/src/http/token.ts +170 -75
- package/src/http/types.ts +69 -5
- package/src/index.ts +40 -1
- package/src/launcher/hash.ts +104 -0
- package/src/launcher/index.ts +35 -0
- package/src/launcher/launch.ts +284 -0
- package/src/launcher/lock.ts +171 -0
- package/src/launcher/serve-unix.ts +386 -0
- package/src/launcher/state.ts +257 -0
- package/src/otel.ts +39 -33
- package/src/protocol.ts +30 -3
- package/src/schema.ts +107 -56
- package/src/server.ts +196 -20
- package/src/types.ts +376 -18
- package/src/util/gzip.ts +63 -0
- package/src/util/schema.ts +4 -22
- package/src/util/web-crypto.ts +98 -0
- package/src/util/zstd.ts +133 -14
- package/src/wire/opaque.ts +37 -0
- package/src/wire/reader.ts +5 -4
- package/src/wire/request.ts +67 -8
- package/src/wire/response.ts +51 -85
- package/src/wire/writer.ts +165 -69
- package/dist/util/conform.d.ts +0 -18
- package/dist/util/conform.d.ts.map +0 -1
- package/src/util/conform.ts +0 -94
package/src/client/ipc.ts
CHANGED
|
@@ -7,18 +7,17 @@ import {
|
|
|
7
7
|
DataType,
|
|
8
8
|
Float64,
|
|
9
9
|
Int64,
|
|
10
|
-
|
|
11
|
-
RecordBatch,
|
|
10
|
+
type RecordBatch,
|
|
12
11
|
RecordBatchReader,
|
|
13
12
|
type Schema,
|
|
14
|
-
Struct,
|
|
15
13
|
Utf8,
|
|
16
|
-
vectorFromArray,
|
|
17
14
|
} from "@query-farm/apache-arrow";
|
|
15
|
+
import { emptyBatchWithMetadata, singleRowBatchWithMetadata } from "#vgi-rpc-arrow";
|
|
18
16
|
import {
|
|
19
17
|
LOG_EXTRA_KEY,
|
|
20
18
|
LOG_LEVEL_KEY,
|
|
21
19
|
LOG_MESSAGE_KEY,
|
|
20
|
+
PROTOCOL_VERSION_KEY,
|
|
22
21
|
REQUEST_VERSION,
|
|
23
22
|
REQUEST_VERSION_KEY,
|
|
24
23
|
RPC_METHOD_KEY,
|
|
@@ -79,38 +78,49 @@ function coerceForArrow(type: DataType, value: any): any {
|
|
|
79
78
|
|
|
80
79
|
/**
|
|
81
80
|
* Build a 1-row Arrow IPC request batch with method metadata.
|
|
81
|
+
*
|
|
82
|
+
* When `options.protocolVersion` is non-empty, the value is emitted as
|
|
83
|
+
* `vgi_rpc.protocol_version` so servers that declare a Protocol-level
|
|
84
|
+
* version validate the request at the dispatch boundary.
|
|
82
85
|
*/
|
|
83
|
-
export function buildRequestIpc(
|
|
86
|
+
export function buildRequestIpc(
|
|
87
|
+
schema: Schema,
|
|
88
|
+
params: Record<string, any>,
|
|
89
|
+
method: string,
|
|
90
|
+
options?: { protocolVersion?: string },
|
|
91
|
+
): Uint8Array {
|
|
84
92
|
const metadata = new Map<string, string>();
|
|
85
93
|
metadata.set(RPC_METHOD_KEY, method);
|
|
86
94
|
metadata.set(REQUEST_VERSION_KEY, REQUEST_VERSION);
|
|
95
|
+
if (options?.protocolVersion) {
|
|
96
|
+
metadata.set(PROTOCOL_VERSION_KEY, options.protocolVersion);
|
|
97
|
+
}
|
|
87
98
|
|
|
99
|
+
// Build the batch through the impl-agnostic #vgi-rpc-arrow layer so this
|
|
100
|
+
// works under both backends. `buildRequestIpc` previously constructed an
|
|
101
|
+
// apache-arrow RecordBatch directly and handed it to `serializeIpcStream`,
|
|
102
|
+
// which then dispatched to the backend's `serializeBatches`. Under the
|
|
103
|
+
// browser/worker condition that backend is flechette, and flechette's
|
|
104
|
+
// `tablesToIPC` cannot read apache-arrow's RecordBatch shape — the cross-
|
|
105
|
+
// impl mixing was silently broken (no browser tests cover this path; see
|
|
106
|
+
// test/client/ipc-cross-impl.test.ts). The abstract helpers produce a
|
|
107
|
+
// 0-row metadata-bearing batch for the empty-schema case (which servers
|
|
108
|
+
// accept identically to a 1-row × 0-col batch) and a 1-row batch with
|
|
109
|
+
// coerced field values otherwise.
|
|
88
110
|
if (schema.fields.length === 0) {
|
|
89
|
-
const
|
|
90
|
-
const data = makeData({
|
|
91
|
-
type: structType,
|
|
92
|
-
length: 1,
|
|
93
|
-
children: [],
|
|
94
|
-
nullCount: 0,
|
|
95
|
-
});
|
|
96
|
-
const batch = new RecordBatch(schema, data, metadata);
|
|
111
|
+
const batch = emptyBatchWithMetadata(schema, metadata);
|
|
97
112
|
return serializeIpcStream(schema, [batch]);
|
|
98
113
|
}
|
|
99
114
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
children,
|
|
110
|
-
nullCount: 0,
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const batch = new RecordBatch(schema, data, metadata);
|
|
115
|
+
const coerced: Record<string, any> = {};
|
|
116
|
+
for (const f of schema.fields) {
|
|
117
|
+
const raw = params[f.name];
|
|
118
|
+
// Missing values must be sent as null. arrow-js's typed-array builders
|
|
119
|
+
// throw "Invalid argument type in ToBigInt" when handed `undefined` for
|
|
120
|
+
// an Int64 column; null builds a proper validity bitmap entry instead.
|
|
121
|
+
coerced[f.name] = raw === undefined ? null : coerceForArrow(f.type, raw);
|
|
122
|
+
}
|
|
123
|
+
const batch = singleRowBatchWithMetadata(schema, coerced, metadata);
|
|
114
124
|
return serializeIpcStream(schema, [batch]);
|
|
115
125
|
}
|
|
116
126
|
|
package/src/client/oauth.ts
CHANGED
|
@@ -3,14 +3,23 @@
|
|
|
3
3
|
|
|
4
4
|
/** RFC 9728 OAuth Protected Resource Metadata (client-side response). */
|
|
5
5
|
export interface OAuthResourceMetadataResponse {
|
|
6
|
+
/** The protected resource's canonical URL (`resource`). */
|
|
6
7
|
resource: string;
|
|
8
|
+
/** Authorization-server issuer URLs; the first is used for OIDC discovery (`authorization_servers`). */
|
|
7
9
|
authorizationServers: string[];
|
|
10
|
+
/** Scopes the resource advertises (`scopes_supported`). */
|
|
8
11
|
scopesSupported?: string[];
|
|
12
|
+
/** Advertised bearer methods, e.g. `["header"]` (`bearer_methods_supported`). */
|
|
9
13
|
bearerMethodsSupported?: string[];
|
|
14
|
+
/** JWS algorithms the resource accepts (`resource_signing_alg_values_supported`). */
|
|
10
15
|
resourceSigningAlgValuesSupported?: string[];
|
|
16
|
+
/** Human-readable resource name (`resource_name`). */
|
|
11
17
|
resourceName?: string;
|
|
18
|
+
/** Documentation URL for the resource (`resource_documentation`). */
|
|
12
19
|
resourceDocumentation?: string;
|
|
20
|
+
/** Policy URL for the resource (`resource_policy_uri`). */
|
|
13
21
|
resourcePolicyUri?: string;
|
|
22
|
+
/** Terms-of-service URL for the resource (`resource_tos_uri`). */
|
|
14
23
|
resourceTosUri?: string;
|
|
15
24
|
/** OAuth client_id advertised by the server. */
|
|
16
25
|
clientId?: string;
|
package/src/client/pipe.ts
CHANGED
|
@@ -75,6 +75,13 @@ class PipeIncrementalWriter {
|
|
|
75
75
|
// PipeStreamSession — lockstep streaming over pipes
|
|
76
76
|
// ---------------------------------------------------------------------------
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* {@link StreamSession} implementation for the pipe/subprocess transport.
|
|
80
|
+
* Drives lockstep streaming over a single bidirectional pipe: each
|
|
81
|
+
* {@link PipeStreamSession.exchange} or iteration step writes one input batch
|
|
82
|
+
* and reads one output batch. Holds the connection's single-threaded busy lock
|
|
83
|
+
* until closed.
|
|
84
|
+
*/
|
|
78
85
|
export class PipeStreamSession implements StreamSession {
|
|
79
86
|
private _reader: IpcStreamReader;
|
|
80
87
|
private _writeFn: WriteFn;
|
|
@@ -109,6 +116,7 @@ export class PipeStreamSession implements StreamSession {
|
|
|
109
116
|
this._externalConfig = opts.externalConfig;
|
|
110
117
|
}
|
|
111
118
|
|
|
119
|
+
/** The stream's one-time header row, or `null` if the method declares no header. */
|
|
112
120
|
get header(): Record<string, any> | null {
|
|
113
121
|
return this._header;
|
|
114
122
|
}
|
|
@@ -125,17 +133,17 @@ export class PipeStreamSession implements StreamSession {
|
|
|
125
133
|
|
|
126
134
|
if (batch.numRows === 0) {
|
|
127
135
|
// Check for external location pointer batch
|
|
128
|
-
if (isExternalLocationBatch(batch)) {
|
|
129
|
-
return await resolveExternalLocation(batch, this._externalConfig);
|
|
136
|
+
if (isExternalLocationBatch(batch as any)) {
|
|
137
|
+
return (await resolveExternalLocation(batch as any, this._externalConfig)) as any;
|
|
130
138
|
}
|
|
131
139
|
// Check if it's a log/error batch. If so, dispatch and continue.
|
|
132
140
|
// Otherwise it's a zero-row data batch — return it.
|
|
133
|
-
if (dispatchLogOrError(batch, this._onLog)) {
|
|
141
|
+
if (dispatchLogOrError(batch as any, this._onLog)) {
|
|
134
142
|
continue;
|
|
135
143
|
}
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
return batch;
|
|
146
|
+
return batch as any;
|
|
139
147
|
}
|
|
140
148
|
}
|
|
141
149
|
|
|
@@ -335,6 +343,11 @@ export class PipeStreamSession implements StreamSession {
|
|
|
335
343
|
}
|
|
336
344
|
}
|
|
337
345
|
|
|
346
|
+
/**
|
|
347
|
+
* End the stream: close the input side (or send an empty stream if nothing
|
|
348
|
+
* was sent yet) and drain the server's remaining output in the background,
|
|
349
|
+
* releasing the connection's busy lock once the drain completes.
|
|
350
|
+
*/
|
|
338
351
|
close(): void {
|
|
339
352
|
if (this._closed) return;
|
|
340
353
|
this._closed = true;
|
|
@@ -377,6 +390,12 @@ export class PipeStreamSession implements StreamSession {
|
|
|
377
390
|
// pipeConnect — create an RpcClient over raw readable/writable streams
|
|
378
391
|
// ---------------------------------------------------------------------------
|
|
379
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Connect to a vgi-rpc server over a raw bidirectional pipe (a readable stream
|
|
395
|
+
* of server output plus a writable for client input). The connection is
|
|
396
|
+
* single-threaded: only one call or stream may be in flight at a time. The
|
|
397
|
+
* `__describe__` handshake is sent before the reader is opened to avoid deadlock.
|
|
398
|
+
*/
|
|
380
399
|
export function pipeConnect(
|
|
381
400
|
readable: ReadableStream<Uint8Array>,
|
|
382
401
|
writable: PipeWritable,
|
|
@@ -389,6 +408,7 @@ export function pipeConnect(
|
|
|
389
408
|
let readerPromise: Promise<IpcStreamReader> | null = null;
|
|
390
409
|
let methodCache: Map<string, MethodInfo> | null = null;
|
|
391
410
|
let protocolName = "";
|
|
411
|
+
let serverProtocolVersion = "";
|
|
392
412
|
let _busy = false;
|
|
393
413
|
let _drainPromise: Promise<void> | null = null;
|
|
394
414
|
let closed = false;
|
|
@@ -458,8 +478,9 @@ export function pipeConnect(
|
|
|
458
478
|
throw new Error("EOF reading __describe__ response");
|
|
459
479
|
}
|
|
460
480
|
|
|
461
|
-
const desc = await parseDescribeResponse(response.batches, onLog);
|
|
481
|
+
const desc = await parseDescribeResponse(response.batches as any, onLog);
|
|
462
482
|
protocolName = desc.protocolName;
|
|
483
|
+
serverProtocolVersion = desc.protocolVersion;
|
|
463
484
|
methodCache = new Map(desc.methods.map((m) => [m.name, m]));
|
|
464
485
|
return methodCache;
|
|
465
486
|
} finally {
|
|
@@ -483,7 +504,7 @@ export function pipeConnect(
|
|
|
483
504
|
const fullParams = { ...(info.defaults ?? {}), ...(params ?? {}) };
|
|
484
505
|
|
|
485
506
|
// Send request
|
|
486
|
-
const body = buildRequestIpc(info.paramsSchema, fullParams, method);
|
|
507
|
+
const body = buildRequestIpc(info.paramsSchema, fullParams, method, { protocolVersion: serverProtocolVersion });
|
|
487
508
|
writeFn(body);
|
|
488
509
|
|
|
489
510
|
// Read response
|
|
@@ -494,7 +515,7 @@ export function pipeConnect(
|
|
|
494
515
|
|
|
495
516
|
// Process batches: dispatch logs, resolve external pointers, find result
|
|
496
517
|
let resultBatch: RecordBatch | null = null;
|
|
497
|
-
for (let batch of response.batches) {
|
|
518
|
+
for (let batch of response.batches as any[]) {
|
|
498
519
|
if (batch.numRows === 0) {
|
|
499
520
|
if (isExternalLocationBatch(batch)) {
|
|
500
521
|
batch = await resolveExternalLocation(batch, externalConfig);
|
|
@@ -537,7 +558,7 @@ export function pipeConnect(
|
|
|
537
558
|
const fullParams = { ...(info.defaults ?? {}), ...(params ?? {}) };
|
|
538
559
|
|
|
539
560
|
// Send init request (params as a complete IPC stream)
|
|
540
|
-
const body = buildRequestIpc(info.paramsSchema, fullParams, method);
|
|
561
|
+
const body = buildRequestIpc(info.paramsSchema, fullParams, method, { protocolVersion: serverProtocolVersion });
|
|
541
562
|
writeFn(body);
|
|
542
563
|
|
|
543
564
|
// Read header if method has headerSchema
|
|
@@ -545,7 +566,7 @@ export function pipeConnect(
|
|
|
545
566
|
if (info.headerSchema) {
|
|
546
567
|
const headerStream = await r.readStream();
|
|
547
568
|
if (headerStream) {
|
|
548
|
-
for (const batch of headerStream.batches) {
|
|
569
|
+
for (const batch of headerStream.batches as any[]) {
|
|
549
570
|
if (batch.numRows === 0) {
|
|
550
571
|
dispatchLogOrError(batch, onLog);
|
|
551
572
|
continue;
|
|
@@ -597,6 +618,7 @@ export function pipeConnect(
|
|
|
597
618
|
const methods = await ensureMethodCache();
|
|
598
619
|
return {
|
|
599
620
|
protocolName,
|
|
621
|
+
protocolVersion: serverProtocolVersion,
|
|
600
622
|
methods: [...methods.values()],
|
|
601
623
|
};
|
|
602
624
|
},
|
|
@@ -613,6 +635,11 @@ export function pipeConnect(
|
|
|
613
635
|
// subprocessConnect — spawn a process and wrap with pipeConnect
|
|
614
636
|
// ---------------------------------------------------------------------------
|
|
615
637
|
|
|
638
|
+
/**
|
|
639
|
+
* Spawn a server process (via `Bun.spawn`) and connect to it over its
|
|
640
|
+
* stdin/stdout using {@link pipeConnect}. The returned client's
|
|
641
|
+
* {@link RpcClient.close} also kills the subprocess.
|
|
642
|
+
*/
|
|
616
643
|
export function subprocessConnect(cmd: string[], options?: SubprocessConnectOptions): RpcClient {
|
|
617
644
|
const proc = Bun.spawn(cmd, {
|
|
618
645
|
stdin: "pipe",
|
package/src/client/stream.ts
CHANGED
|
@@ -9,9 +9,22 @@ import { ARROW_CONTENT_TYPE, serializeIpcStream } from "../http/common.js";
|
|
|
9
9
|
import { dispatchLogOrError, extractBatchRows, inferArrowType, readResponseBatches } from "./ipc.js";
|
|
10
10
|
import type { LogMessage, StreamSession } from "./types.js";
|
|
11
11
|
|
|
12
|
-
type CompressFn = (data: Uint8Array, level: number) => Uint8Array
|
|
13
|
-
type DecompressFn = (data: Uint8Array) => Uint8Array
|
|
14
|
-
|
|
12
|
+
type CompressFn = (data: Uint8Array, level: number) => Promise<Uint8Array>;
|
|
13
|
+
type DecompressFn = (data: Uint8Array) => Promise<Uint8Array>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Posts an Arrow IPC request body to *url*, transparently handling
|
|
17
|
+
* client-vended request externalization. Provided by the parent connection
|
|
18
|
+
* so a single capability cache can drive both unary and stream call paths.
|
|
19
|
+
*/
|
|
20
|
+
export type PostFn = (url: string, body: Uint8Array) => Promise<Response>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* {@link StreamSession} implementation for the HTTP transport. Stream state is
|
|
24
|
+
* carried statelessly across requests via an HMAC state token: each
|
|
25
|
+
* {@link HttpStreamSession.exchange} or producer-continuation POST sends the
|
|
26
|
+
* current token and receives the next one in the response metadata.
|
|
27
|
+
*/
|
|
15
28
|
export class HttpStreamSession implements StreamSession {
|
|
16
29
|
private _baseUrl: string;
|
|
17
30
|
private _prefix: string;
|
|
@@ -28,6 +41,7 @@ export class HttpStreamSession implements StreamSession {
|
|
|
28
41
|
private _decompressFn?: DecompressFn;
|
|
29
42
|
private _authorization?: string;
|
|
30
43
|
private _externalConfig?: ExternalLocationConfig;
|
|
44
|
+
private _postFn?: PostFn;
|
|
31
45
|
|
|
32
46
|
constructor(opts: {
|
|
33
47
|
baseUrl: string;
|
|
@@ -45,6 +59,7 @@ export class HttpStreamSession implements StreamSession {
|
|
|
45
59
|
decompressFn?: DecompressFn;
|
|
46
60
|
authorization?: string;
|
|
47
61
|
externalConfig?: ExternalLocationConfig;
|
|
62
|
+
postFn?: PostFn;
|
|
48
63
|
}) {
|
|
49
64
|
this._baseUrl = opts.baseUrl;
|
|
50
65
|
this._prefix = opts.prefix;
|
|
@@ -61,8 +76,19 @@ export class HttpStreamSession implements StreamSession {
|
|
|
61
76
|
this._decompressFn = opts.decompressFn;
|
|
62
77
|
this._authorization = opts.authorization;
|
|
63
78
|
this._externalConfig = opts.externalConfig;
|
|
79
|
+
this._postFn = opts.postFn;
|
|
64
80
|
}
|
|
65
81
|
|
|
82
|
+
private async _post(url: string, body: Uint8Array): Promise<Response> {
|
|
83
|
+
if (this._postFn) return this._postFn(url, body);
|
|
84
|
+
return fetch(url, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: this._buildHeaders(),
|
|
87
|
+
body: (await this._prepareBody(body)) as unknown as BodyInit,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The stream's one-time header row, or `null` if the method declares no header. */
|
|
66
92
|
get header(): Record<string, any> | null {
|
|
67
93
|
return this._header;
|
|
68
94
|
}
|
|
@@ -71,8 +97,10 @@ export class HttpStreamSession implements StreamSession {
|
|
|
71
97
|
const headers: Record<string, string> = {
|
|
72
98
|
"Content-Type": ARROW_CONTENT_TYPE,
|
|
73
99
|
};
|
|
74
|
-
if (this._compressionLevel != null) {
|
|
100
|
+
if (this._compressionLevel != null && this._compressFn) {
|
|
75
101
|
headers["Content-Encoding"] = "zstd";
|
|
102
|
+
}
|
|
103
|
+
if (this._compressionLevel != null && this._decompressFn) {
|
|
76
104
|
headers["Accept-Encoding"] = "zstd";
|
|
77
105
|
}
|
|
78
106
|
if (this._authorization) {
|
|
@@ -81,9 +109,9 @@ export class HttpStreamSession implements StreamSession {
|
|
|
81
109
|
return headers;
|
|
82
110
|
}
|
|
83
111
|
|
|
84
|
-
private _prepareBody(content: Uint8Array): Uint8Array {
|
|
112
|
+
private async _prepareBody(content: Uint8Array): Promise<Uint8Array> {
|
|
85
113
|
if (this._compressionLevel != null && this._compressFn) {
|
|
86
|
-
return this._compressFn(content, this._compressionLevel);
|
|
114
|
+
return await this._compressFn(content, this._compressionLevel);
|
|
87
115
|
}
|
|
88
116
|
return content;
|
|
89
117
|
}
|
|
@@ -91,7 +119,7 @@ export class HttpStreamSession implements StreamSession {
|
|
|
91
119
|
private async _readResponse(resp: Response): Promise<Uint8Array<ArrayBuffer>> {
|
|
92
120
|
let body = new Uint8Array(await resp.arrayBuffer());
|
|
93
121
|
if (resp.headers.get("Content-Encoding") === "zstd" && this._decompressFn) {
|
|
94
|
-
body = new Uint8Array(this._decompressFn(body));
|
|
122
|
+
body = new Uint8Array(await this._decompressFn(body));
|
|
95
123
|
}
|
|
96
124
|
return body;
|
|
97
125
|
}
|
|
@@ -159,11 +187,7 @@ export class HttpStreamSession implements StreamSession {
|
|
|
159
187
|
|
|
160
188
|
private async _doExchange(schema: Schema, batches: RecordBatch[]): Promise<Record<string, any>[]> {
|
|
161
189
|
const body = serializeIpcStream(schema, batches);
|
|
162
|
-
const resp = await
|
|
163
|
-
method: "POST",
|
|
164
|
-
headers: this._buildHeaders(),
|
|
165
|
-
body: this._prepareBody(body) as unknown as BodyInit,
|
|
166
|
-
});
|
|
190
|
+
const resp = await this._post(`${this._baseUrl}${this._prefix}/${this._method}/exchange`, body);
|
|
167
191
|
if (resp.status === 401) {
|
|
168
192
|
throw new RpcError("AuthenticationError", "Authentication required", "");
|
|
169
193
|
}
|
|
@@ -218,7 +242,7 @@ export class HttpStreamSession implements StreamSession {
|
|
|
218
242
|
for (let batch of this._pendingBatches) {
|
|
219
243
|
if (batch.numRows === 0) {
|
|
220
244
|
if (isExternalLocationBatch(batch)) {
|
|
221
|
-
batch = await resolveExternalLocation(batch, this._externalConfig);
|
|
245
|
+
batch = (await resolveExternalLocation(batch as any, this._externalConfig)) as any;
|
|
222
246
|
} else {
|
|
223
247
|
dispatchLogOrError(batch, this._onLog);
|
|
224
248
|
continue;
|
|
@@ -233,7 +257,9 @@ export class HttpStreamSession implements StreamSession {
|
|
|
233
257
|
|
|
234
258
|
// Follow continuation tokens
|
|
235
259
|
while (true) {
|
|
236
|
-
const
|
|
260
|
+
const stateToken = this._stateToken;
|
|
261
|
+
if (stateToken === null) return;
|
|
262
|
+
const responseBody = await this._sendContinuation(stateToken);
|
|
237
263
|
const { batches } = await readResponseBatches(responseBody);
|
|
238
264
|
|
|
239
265
|
let gotContinuation = false;
|
|
@@ -248,7 +274,7 @@ export class HttpStreamSession implements StreamSession {
|
|
|
248
274
|
}
|
|
249
275
|
// Check for external location pointer
|
|
250
276
|
if (isExternalLocationBatch(batch)) {
|
|
251
|
-
batch = await resolveExternalLocation(batch, this._externalConfig);
|
|
277
|
+
batch = (await resolveExternalLocation(batch as any, this._externalConfig)) as any;
|
|
252
278
|
} else {
|
|
253
279
|
// Log/error batch
|
|
254
280
|
dispatchLogOrError(batch, this._onLog);
|
|
@@ -278,11 +304,7 @@ export class HttpStreamSession implements StreamSession {
|
|
|
278
304
|
const batch = new RecordBatch(emptySchema, data, metadata);
|
|
279
305
|
const body = serializeIpcStream(emptySchema, [batch]);
|
|
280
306
|
|
|
281
|
-
const resp = await
|
|
282
|
-
method: "POST",
|
|
283
|
-
headers: this._buildHeaders(),
|
|
284
|
-
body: this._prepareBody(body) as unknown as BodyInit,
|
|
285
|
-
});
|
|
307
|
+
const resp = await this._post(`${this._baseUrl}${this._prefix}/${this._method}/exchange`, body);
|
|
286
308
|
if (resp.status === 401) {
|
|
287
309
|
throw new RpcError("AuthenticationError", "Authentication required", "");
|
|
288
310
|
}
|
|
@@ -290,6 +312,7 @@ export class HttpStreamSession implements StreamSession {
|
|
|
290
312
|
return this._readResponse(resp);
|
|
291
313
|
}
|
|
292
314
|
|
|
315
|
+
/** No-op: the HTTP transport is stateless, so there is nothing to tear down. */
|
|
293
316
|
close(): void {
|
|
294
317
|
// No-op for HTTP (stateless)
|
|
295
318
|
}
|
package/src/client/types.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
+
/** Options for {@link httpConnect}, the HTTP-transport RPC client. */
|
|
4
5
|
export interface HttpConnectOptions {
|
|
6
|
+
/** Route prefix the server mounts its methods under (e.g. `/api`). Trailing slashes are stripped. Defaults to no prefix. */
|
|
5
7
|
prefix?: string;
|
|
8
|
+
/** Callback invoked for each log/error message the server emits during a request. */
|
|
6
9
|
onLog?: (msg: LogMessage) => void;
|
|
10
|
+
/** When set, request bodies are zstd-compressed at this level and `Accept-Encoding: zstd` is sent. Omit to disable compression. */
|
|
7
11
|
compressionLevel?: number;
|
|
8
12
|
/** Authorization header value (e.g. "Bearer <token>"). Sent with every request. */
|
|
9
13
|
authorization?: string;
|
|
@@ -11,27 +15,46 @@ export interface HttpConnectOptions {
|
|
|
11
15
|
externalLocation?: import("../external.js").ExternalLocationConfig;
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
/** A log or error message delivered to an {@link HttpConnectOptions.onLog} callback. */
|
|
14
19
|
export interface LogMessage {
|
|
20
|
+
/** Severity, mirroring the server's log level (e.g. `INFO`, `WARNING`, `EXCEPTION`). */
|
|
15
21
|
level: string;
|
|
22
|
+
/** The human-readable log text. */
|
|
16
23
|
message: string;
|
|
24
|
+
/** Optional structured fields attached to the log record. */
|
|
17
25
|
extra?: Record<string, any>;
|
|
18
26
|
}
|
|
19
27
|
|
|
28
|
+
/**
|
|
29
|
+
* A live streaming method call. Exchange methods drive the server with
|
|
30
|
+
* {@link StreamSession.exchange}; producer methods are consumed by async
|
|
31
|
+
* iteration. Always {@link StreamSession.close} when done.
|
|
32
|
+
*/
|
|
20
33
|
export interface StreamSession {
|
|
34
|
+
/** The method's header row (returned once at stream start), or `null` if the method declares no header. */
|
|
21
35
|
readonly header: Record<string, any> | null;
|
|
36
|
+
/** Send one batch of input rows and receive the server's corresponding output rows (exchange streams). */
|
|
22
37
|
exchange(input: Record<string, any>[]): Promise<Record<string, any>[]>;
|
|
38
|
+
/** Iterate the server-produced output batches one row-array at a time (producer streams). */
|
|
23
39
|
[Symbol.asyncIterator](): AsyncIterableIterator<Record<string, any>[]>;
|
|
40
|
+
/** Tear down the stream, flushing/draining the underlying transport. */
|
|
24
41
|
close(): void;
|
|
25
42
|
}
|
|
26
43
|
|
|
44
|
+
/** Options for {@link pipeConnect}, the client over raw readable/writable streams. */
|
|
27
45
|
export interface PipeConnectOptions {
|
|
46
|
+
/** Callback invoked for each log/error message the server emits during a request. */
|
|
28
47
|
onLog?: (msg: LogMessage) => void;
|
|
29
48
|
/** External storage config for resolving externalized batches. */
|
|
30
49
|
externalLocation?: import("../external.js").ExternalLocationConfig;
|
|
31
50
|
}
|
|
32
51
|
|
|
52
|
+
/** Options for {@link subprocessConnect}, which spawns a server process and pipes to it. */
|
|
33
53
|
export interface SubprocessConnectOptions extends PipeConnectOptions {
|
|
54
|
+
/** Working directory for the spawned process. Defaults to the current directory. */
|
|
34
55
|
cwd?: string;
|
|
56
|
+
/** Extra environment variables, merged over the current `process.env`. */
|
|
35
57
|
env?: Record<string, string>;
|
|
58
|
+
/** How to handle the child's stderr. Defaults to `"ignore"`. */
|
|
36
59
|
stderr?: "inherit" | "pipe" | "ignore";
|
|
37
60
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Client-side request-body externalization.
|
|
6
|
+
*
|
|
7
|
+
* When a request body would exceed the server-advertised `maxRequestBytes`,
|
|
8
|
+
* we (1) call `{prefix}/__upload_url__/init` to get a pre-signed upload URL,
|
|
9
|
+
* (2) PUT the body to that URL, and (3) replace the inline body with a
|
|
10
|
+
* zero-row "pointer" IPC stream that carries the original RPC dispatch
|
|
11
|
+
* metadata plus `vgi_rpc.location: <download-url>`. The server then resolves
|
|
12
|
+
* the pointer and dispatches normally.
|
|
13
|
+
*
|
|
14
|
+
* Mirrors Python's `_externalize_via_upload_url()` and `request_upload_urls()`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Field, Int64, RecordBatchReader, Schema } from "@query-farm/apache-arrow";
|
|
18
|
+
import { REQUEST_VERSION, REQUEST_VERSION_KEY, RPC_METHOD_KEY } from "../constants.js";
|
|
19
|
+
import { RpcError } from "../errors.js";
|
|
20
|
+
import { makeExternalLocationBatch } from "../external.js";
|
|
21
|
+
import { ARROW_CONTENT_TYPE, serializeIpcStream } from "../http/common.js";
|
|
22
|
+
import { buildRequestIpc } from "./ipc.js";
|
|
23
|
+
|
|
24
|
+
const UPLOAD_URL_METHOD = "__upload_url__";
|
|
25
|
+
const UPLOAD_URL_PARAMS_SCHEMA = new Schema([new Field("count", new Int64(), false)]);
|
|
26
|
+
|
|
27
|
+
export interface UploadUrlPair {
|
|
28
|
+
uploadUrl: string;
|
|
29
|
+
downloadUrl: string;
|
|
30
|
+
expiresAt: Date;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* POST `__upload_url__/init` and return the requested number of pre-signed
|
|
35
|
+
* URL pairs. Server must have an `uploadUrlProvider` configured; otherwise
|
|
36
|
+
* the route returns 404 and we surface that as `RpcError("NotSupported")`.
|
|
37
|
+
*/
|
|
38
|
+
export async function requestUploadUrls(
|
|
39
|
+
baseUrl: string,
|
|
40
|
+
prefix: string,
|
|
41
|
+
count: number,
|
|
42
|
+
authorization?: string,
|
|
43
|
+
): Promise<UploadUrlPair[]> {
|
|
44
|
+
const body = buildRequestIpc(UPLOAD_URL_PARAMS_SCHEMA, { count: BigInt(count) }, UPLOAD_URL_METHOD);
|
|
45
|
+
const headers: Record<string, string> = { "Content-Type": ARROW_CONTENT_TYPE };
|
|
46
|
+
if (authorization) headers.Authorization = authorization;
|
|
47
|
+
|
|
48
|
+
const resp = await fetch(`${baseUrl}${prefix}/${UPLOAD_URL_METHOD}/init`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers,
|
|
51
|
+
body: body as unknown as BodyInit,
|
|
52
|
+
});
|
|
53
|
+
if (resp.status === 404) {
|
|
54
|
+
throw new RpcError("NotSupported", "Server does not support upload URLs", "");
|
|
55
|
+
}
|
|
56
|
+
if (resp.status === 401) {
|
|
57
|
+
throw new RpcError("AuthenticationError", "Authentication required", "");
|
|
58
|
+
}
|
|
59
|
+
if (!resp.ok) {
|
|
60
|
+
throw new RpcError("HttpError", `__upload_url__/init failed: HTTP ${resp.status}`, "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const respBody = new Uint8Array(await resp.arrayBuffer());
|
|
64
|
+
const reader = await RecordBatchReader.from(respBody);
|
|
65
|
+
await reader.open();
|
|
66
|
+
|
|
67
|
+
const pairs: UploadUrlPair[] = [];
|
|
68
|
+
for (const batch of reader.readAll()) {
|
|
69
|
+
if (batch.numRows === 0) continue;
|
|
70
|
+
for (let r = 0; r < batch.numRows; r++) {
|
|
71
|
+
const uploadUrl = batch.getChildAt(0)?.get(r) as string;
|
|
72
|
+
const downloadUrl = batch.getChildAt(1)?.get(r) as string;
|
|
73
|
+
const expiresRaw = batch.getChildAt(2)?.get(r);
|
|
74
|
+
// Timestamp(us) → either a Date, a number (ms), or a bigint (us)
|
|
75
|
+
let expiresAt: Date;
|
|
76
|
+
if (expiresRaw instanceof Date) {
|
|
77
|
+
expiresAt = expiresRaw;
|
|
78
|
+
} else if (typeof expiresRaw === "bigint") {
|
|
79
|
+
expiresAt = new Date(Number(expiresRaw / 1000n));
|
|
80
|
+
} else if (typeof expiresRaw === "number") {
|
|
81
|
+
expiresAt = new Date(expiresRaw);
|
|
82
|
+
} else {
|
|
83
|
+
expiresAt = new Date();
|
|
84
|
+
}
|
|
85
|
+
pairs.push({ uploadUrl, downloadUrl, expiresAt });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (pairs.length === 0) {
|
|
90
|
+
throw new RpcError("ProtocolError", "Server returned no upload URLs", "");
|
|
91
|
+
}
|
|
92
|
+
return pairs;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build the externalized pointer body to send in place of *originalBody*.
|
|
97
|
+
*
|
|
98
|
+
* The pointer is a zero-row IPC stream whose first batch carries:
|
|
99
|
+
* - same schema as the original request batch
|
|
100
|
+
* - merged custom_metadata: original RPC dispatch keys + `vgi_rpc.location`
|
|
101
|
+
*
|
|
102
|
+
* The server's request reader honours the dispatch keys for routing, then
|
|
103
|
+
* resolves the pointer to fetch the inline batch for parameter extraction.
|
|
104
|
+
*/
|
|
105
|
+
async function buildPointerRequestBody(originalBody: Uint8Array, downloadUrl: string): Promise<Uint8Array> {
|
|
106
|
+
const reader = await RecordBatchReader.from(originalBody);
|
|
107
|
+
await reader.open();
|
|
108
|
+
const schema = reader.schema;
|
|
109
|
+
if (!schema) {
|
|
110
|
+
throw new RpcError("ProtocolError", "Original request body has no schema", "");
|
|
111
|
+
}
|
|
112
|
+
const batches = reader.readAll();
|
|
113
|
+
if (batches.length === 0) {
|
|
114
|
+
throw new RpcError("ProtocolError", "Original request body has no batches", "");
|
|
115
|
+
}
|
|
116
|
+
const original = batches[0];
|
|
117
|
+
const originalMeta = original.metadata ?? new Map<string, string>();
|
|
118
|
+
|
|
119
|
+
const pointer = makeExternalLocationBatch(schema, downloadUrl);
|
|
120
|
+
const merged = new Map<string, string>(pointer.metadata ?? new Map());
|
|
121
|
+
// Preserve the original RPC dispatch metadata so the server can route.
|
|
122
|
+
const method = originalMeta.get(RPC_METHOD_KEY);
|
|
123
|
+
const version = originalMeta.get(REQUEST_VERSION_KEY) ?? REQUEST_VERSION;
|
|
124
|
+
if (method) merged.set(RPC_METHOD_KEY, method);
|
|
125
|
+
merged.set(REQUEST_VERSION_KEY, version);
|
|
126
|
+
// Carry over any other keys (request id, state token for exchange, etc).
|
|
127
|
+
for (const [k, v] of originalMeta) {
|
|
128
|
+
if (!merged.has(k)) merged.set(k, v);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Re-emit the pointer batch with merged metadata.
|
|
132
|
+
const { RecordBatch } = await import("@query-farm/apache-arrow");
|
|
133
|
+
const pointerWithMeta = new RecordBatch(schema as any, (pointer as any).data, merged);
|
|
134
|
+
return serializeIpcStream(schema, [pointerWithMeta]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ExternalizeOptions {
|
|
138
|
+
baseUrl: string;
|
|
139
|
+
prefix: string;
|
|
140
|
+
authorization?: string;
|
|
141
|
+
/** Optional per-URL validator; throw to reject. */
|
|
142
|
+
urlValidator?: ((url: string) => void) | null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Upload *body* via a server-vended URL and return the pointer-batch body
|
|
147
|
+
* that should be sent in place of the original. Throws if the server does
|
|
148
|
+
* not advertise upload-URL support or the upload fails.
|
|
149
|
+
*/
|
|
150
|
+
export async function externalizeRequestBody(body: Uint8Array, opts: ExternalizeOptions): Promise<Uint8Array> {
|
|
151
|
+
const pairs = await requestUploadUrls(opts.baseUrl, opts.prefix, 1, opts.authorization);
|
|
152
|
+
const pair = pairs[0];
|
|
153
|
+
|
|
154
|
+
if (opts.urlValidator) {
|
|
155
|
+
opts.urlValidator(pair.uploadUrl);
|
|
156
|
+
opts.urlValidator(pair.downloadUrl);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const putResp = await fetch(pair.uploadUrl, {
|
|
160
|
+
method: "PUT",
|
|
161
|
+
headers: { "Content-Type": ARROW_CONTENT_TYPE },
|
|
162
|
+
body: body as unknown as BodyInit,
|
|
163
|
+
});
|
|
164
|
+
if (!putResp.ok) {
|
|
165
|
+
throw new RpcError("ExternalUploadFailed", `PUT to upload URL failed: HTTP ${putResp.status}`, "");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return buildPointerRequestBody(body, pair.downloadUrl);
|
|
169
|
+
}
|