@query-farm/vgi-rpc 0.6.3 → 0.7.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/dist/access-log.d.ts +50 -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/client/capabilities.d.ts +25 -0
- package/dist/client/capabilities.d.ts.map +1 -0
- package/dist/client/connect.d.ts.map +1 -1
- package/dist/client/introspect.d.ts +7 -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/pipe.d.ts.map +1 -1
- package/dist/client/stream.d.ts +11 -2
- package/dist/client/stream.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 +15 -1
- 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 +46 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/external.d.ts +25 -5
- package/dist/external.d.ts.map +1 -1
- package/dist/http/bearer.d.ts.map +1 -1
- package/dist/http/common.d.ts +42 -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/mtls.d.ts +2 -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 +38 -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 -3507
- package/dist/index.js.map +19 -37
- 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 +54 -0
- package/dist/launcher/serve-unix.d.ts.map +1 -0
- package/dist/launcher/state.d.ts +59 -0
- package/dist/launcher/state.d.ts.map +1 -0
- package/dist/otel.d.ts.map +1 -1
- package/dist/protocol.d.ts +16 -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 +216 -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 +24 -10
- package/src/access-log.ts +195 -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/client/capabilities.ts +84 -0
- package/src/client/connect.ts +103 -26
- package/src/client/introspect.ts +60 -38
- package/src/client/ipc.ts +37 -27
- package/src/client/pipe.ts +12 -9
- package/src/client/stream.ts +34 -19
- package/src/client/uploadUrl.ts +169 -0
- package/src/constants.ts +18 -1
- 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 +76 -0
- package/src/external.ts +43 -29
- package/src/http/bearer.ts +2 -5
- package/src/http/common.ts +90 -23
- package/src/http/dispatch.ts +373 -46
- package/src/http/handler.ts +794 -68
- package/src/http/index.ts +1 -0
- package/src/http/mtls.ts +18 -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 +165 -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 +385 -0
- package/src/launcher/state.ts +245 -0
- package/src/otel.ts +39 -33
- package/src/protocol.ts +27 -3
- package/src/schema.ts +107 -56
- package/src/server.ts +196 -20
- package/src/types.ts +322 -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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Backend-agnostic Arrow type predicates. typeId values match the Arrow Type
|
|
2
|
+
// enum and agree across arrow-js and flechette.
|
|
3
|
+
|
|
4
|
+
import type { VgiDataType } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export const TypeId = {
|
|
7
|
+
Null: 1,
|
|
8
|
+
Int: 2,
|
|
9
|
+
Float: 3,
|
|
10
|
+
Binary: 4,
|
|
11
|
+
Utf8: 5,
|
|
12
|
+
Bool: 6,
|
|
13
|
+
Decimal: 7,
|
|
14
|
+
Date: 8,
|
|
15
|
+
Time: 9,
|
|
16
|
+
Timestamp: 10,
|
|
17
|
+
Interval: 11,
|
|
18
|
+
List: 12,
|
|
19
|
+
Struct: 13,
|
|
20
|
+
Union: 14,
|
|
21
|
+
FixedSizeBinary: 15,
|
|
22
|
+
FixedSizeList: 16,
|
|
23
|
+
Map: 17,
|
|
24
|
+
Duration: 18,
|
|
25
|
+
LargeBinary: 19,
|
|
26
|
+
LargeUtf8: 20,
|
|
27
|
+
Dictionary: -1,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export const isNull = (t: VgiDataType): boolean => t.typeId === TypeId.Null;
|
|
31
|
+
export const isInt = (t: VgiDataType): boolean => t.typeId === TypeId.Int;
|
|
32
|
+
export const isFloat = (t: VgiDataType): boolean => t.typeId === TypeId.Float;
|
|
33
|
+
export const isBinary = (t: VgiDataType): boolean => t.typeId === TypeId.Binary || t.typeId === TypeId.LargeBinary;
|
|
34
|
+
export const isUtf8 = (t: VgiDataType): boolean => t.typeId === TypeId.Utf8 || t.typeId === TypeId.LargeUtf8;
|
|
35
|
+
export const isLargeUtf8 = (t: VgiDataType): boolean => t.typeId === TypeId.LargeUtf8;
|
|
36
|
+
export const isLargeBinary = (t: VgiDataType): boolean => t.typeId === TypeId.LargeBinary;
|
|
37
|
+
export const isBool = (t: VgiDataType): boolean => t.typeId === TypeId.Bool;
|
|
38
|
+
export const isDecimal = (t: VgiDataType): boolean => t.typeId === TypeId.Decimal;
|
|
39
|
+
export const isDate = (t: VgiDataType): boolean => t.typeId === TypeId.Date;
|
|
40
|
+
export const isTime = (t: VgiDataType): boolean => t.typeId === TypeId.Time;
|
|
41
|
+
export const isTimestamp = (t: VgiDataType): boolean => t.typeId === TypeId.Timestamp;
|
|
42
|
+
export const isDuration = (t: VgiDataType): boolean => t.typeId === TypeId.Duration;
|
|
43
|
+
export const isList = (t: VgiDataType): boolean => t.typeId === TypeId.List;
|
|
44
|
+
export const isStruct = (t: VgiDataType): boolean => t.typeId === TypeId.Struct;
|
|
45
|
+
export const isMap = (t: VgiDataType): boolean => t.typeId === TypeId.Map;
|
|
46
|
+
export const isFixedSizeBinary = (t: VgiDataType): boolean => t.typeId === TypeId.FixedSizeBinary;
|
|
47
|
+
export const isDictionary = (t: VgiDataType): boolean => t.typeId === TypeId.Dictionary;
|
|
48
|
+
|
|
49
|
+
export function isBatch(x: unknown): x is import("./types.js").VgiBatch {
|
|
50
|
+
return (
|
|
51
|
+
x != null &&
|
|
52
|
+
typeof (x as any).numRows === "number" &&
|
|
53
|
+
(x as any).schema != null &&
|
|
54
|
+
Array.isArray((x as any).schema.fields)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Backend-agnostic Arrow type surface used inside vgi-rpc-typescript.
|
|
2
|
+
// Mirrors vgi-typescript's facade so structurally compatible values flow
|
|
3
|
+
// freely between the two packages.
|
|
4
|
+
|
|
5
|
+
export type VgiTypeId = number;
|
|
6
|
+
|
|
7
|
+
export interface VgiDataType {
|
|
8
|
+
readonly typeId: VgiTypeId;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface VgiField {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly type: VgiDataType;
|
|
14
|
+
readonly nullable: boolean;
|
|
15
|
+
readonly metadata: Map<string, string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface VgiSchema {
|
|
19
|
+
readonly fields: readonly VgiField[];
|
|
20
|
+
readonly metadata: Map<string, string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface VgiColumn {
|
|
24
|
+
readonly type: VgiDataType;
|
|
25
|
+
readonly length: number;
|
|
26
|
+
get(index: number): unknown;
|
|
27
|
+
[Symbol.iterator](): Iterator<unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface VgiBatch {
|
|
31
|
+
readonly schema: VgiSchema;
|
|
32
|
+
readonly numRows: number;
|
|
33
|
+
readonly metadata?: Map<string, string> | null;
|
|
34
|
+
getChild(name: string): VgiColumn | null;
|
|
35
|
+
getChildAt(index: number): VgiColumn | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface VgiBackendInfo {
|
|
39
|
+
readonly name: "arrow-js" | "flechette";
|
|
40
|
+
/**
|
|
41
|
+
* Whether this backend's `.get(0)` round-trip is unreliable for opaque
|
|
42
|
+
* column types (Map/Date/Time/Timestamp/Duration/Decimal/LargeUtf8/
|
|
43
|
+
* LargeBinary/FixedSizeBinary/Dictionary), requiring the request parser to
|
|
44
|
+
* pass the raw column data straight through instead of materializing a
|
|
45
|
+
* value. True for arrow-js; false for flechette (which extracts cleanly).
|
|
46
|
+
*/
|
|
47
|
+
readonly opaquePassthrough: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type VgiColumnData = unknown;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Incremental IPC stream encoder for the lockstep stdio transport.
|
|
54
|
+
*
|
|
55
|
+
* The stdio exchange protocol is lockstep — the client reads each response
|
|
56
|
+
* batch (and its framing bytes) before sending the next input — so we
|
|
57
|
+
* cannot buffer-then-emit at close. Each call returns the wire bytes to
|
|
58
|
+
* flush immediately. Only the stdio server uses this; HTTP serializes
|
|
59
|
+
* whole responses via {@link serializeBatches}.
|
|
60
|
+
*
|
|
61
|
+
* The arrow-js backend implements this over `RecordBatchStreamWriter`.
|
|
62
|
+
* The flechette backend has no incremental-writer surface and its
|
|
63
|
+
* factory throws — keeping arrow-js out of the flechette (workerd/
|
|
64
|
+
* browser) bundle, which is HTTP-only anyway.
|
|
65
|
+
*/
|
|
66
|
+
export interface IncrementalEncoder {
|
|
67
|
+
/** Bytes for the schema preamble (continuation + schema message). */
|
|
68
|
+
start(): Uint8Array;
|
|
69
|
+
/** Bytes for one record batch message. */
|
|
70
|
+
writeBatch(batch: VgiBatch): Uint8Array;
|
|
71
|
+
/** Bytes for the end-of-stream marker. */
|
|
72
|
+
finish(): Uint8Array;
|
|
73
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HTTP server capability discovery.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors Python's `http_capabilities()`: probes `OPTIONS {prefix}/health`
|
|
8
|
+
* and reads three response headers:
|
|
9
|
+
* - `VGI-Max-Request-Bytes` — server-enforced inline request cap
|
|
10
|
+
* - `VGI-Upload-URL-Support` — "true" when the server vends upload URLs
|
|
11
|
+
* - `VGI-Max-Upload-Bytes` — cap on out-of-band upload size
|
|
12
|
+
*
|
|
13
|
+
* Honours `Cache-Control: max-age=N` for refresh scheduling.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface HttpServerCapabilities {
|
|
17
|
+
/** Server's advertised max inline request body size (bytes). */
|
|
18
|
+
maxRequestBytes: number | null;
|
|
19
|
+
/** Whether the server vends upload URLs via `__upload_url__/init`. */
|
|
20
|
+
uploadUrlSupport: boolean;
|
|
21
|
+
/** Cap on the size of an externalized upload (bytes). */
|
|
22
|
+
maxUploadBytes: number | null;
|
|
23
|
+
/** Monotonic-time-ish epoch (ms) at which this snapshot should be re-probed. */
|
|
24
|
+
cacheExpiresAt: number | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MAX_REQUEST_BYTES_HEADER = "VGI-Max-Request-Bytes";
|
|
28
|
+
const UPLOAD_URL_HEADER = "VGI-Upload-URL-Support";
|
|
29
|
+
const MAX_UPLOAD_BYTES_HEADER = "VGI-Max-Upload-Bytes";
|
|
30
|
+
|
|
31
|
+
function parseHeaderInt(headers: Headers, name: string): number | null {
|
|
32
|
+
const raw = headers.get(name) ?? headers.get(name.toLowerCase());
|
|
33
|
+
if (raw == null) return null;
|
|
34
|
+
const parsed = Number.parseInt(raw, 10);
|
|
35
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseCapabilitiesFromHeaders(headers: Headers): HttpServerCapabilities {
|
|
39
|
+
const uploadRaw = headers.get(UPLOAD_URL_HEADER) ?? headers.get(UPLOAD_URL_HEADER.toLowerCase());
|
|
40
|
+
const uploadUrlSupport = uploadRaw === "true";
|
|
41
|
+
|
|
42
|
+
let cacheExpiresAt: number | null = null;
|
|
43
|
+
const cc = headers.get("Cache-Control") ?? headers.get("cache-control");
|
|
44
|
+
if (cc) {
|
|
45
|
+
for (const token of cc.split(",")) {
|
|
46
|
+
const t = token.trim().toLowerCase();
|
|
47
|
+
if (t.startsWith("max-age=")) {
|
|
48
|
+
const seconds = Number.parseFloat(t.slice("max-age=".length));
|
|
49
|
+
if (Number.isFinite(seconds)) {
|
|
50
|
+
cacheExpiresAt = Date.now() + seconds * 1000;
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
maxRequestBytes: parseHeaderInt(headers, MAX_REQUEST_BYTES_HEADER),
|
|
59
|
+
uploadUrlSupport,
|
|
60
|
+
maxUploadBytes: parseHeaderInt(headers, MAX_UPLOAD_BYTES_HEADER),
|
|
61
|
+
cacheExpiresAt,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function discoverHttpCapabilities(
|
|
66
|
+
baseUrl: string,
|
|
67
|
+
prefix: string,
|
|
68
|
+
authorization?: string,
|
|
69
|
+
): Promise<HttpServerCapabilities> {
|
|
70
|
+
const headers: Record<string, string> = {};
|
|
71
|
+
if (authorization) headers.Authorization = authorization;
|
|
72
|
+
const resp = await fetch(`${baseUrl}${prefix}/health`, {
|
|
73
|
+
method: "OPTIONS",
|
|
74
|
+
headers,
|
|
75
|
+
});
|
|
76
|
+
// Capability headers are advertised on every response; we don't require 200.
|
|
77
|
+
return parseCapabilitiesFromHeaders(resp.headers);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isCapabilitySnapshotFresh(snapshot: HttpServerCapabilities | null): boolean {
|
|
81
|
+
if (!snapshot) return false;
|
|
82
|
+
if (snapshot.cacheExpiresAt == null) return true;
|
|
83
|
+
return Date.now() < snapshot.cacheExpiresAt;
|
|
84
|
+
}
|
package/src/client/connect.ts
CHANGED
|
@@ -6,6 +6,11 @@ import { LOG_LEVEL_KEY, STATE_KEY } from "../constants.js";
|
|
|
6
6
|
import { RpcError } from "../errors.js";
|
|
7
7
|
import { isExternalLocationBatch, resolveExternalLocation } from "../external.js";
|
|
8
8
|
import { ARROW_CONTENT_TYPE } from "../http/common.js";
|
|
9
|
+
import {
|
|
10
|
+
type HttpServerCapabilities,
|
|
11
|
+
isCapabilitySnapshotFresh,
|
|
12
|
+
parseCapabilitiesFromHeaders,
|
|
13
|
+
} from "./capabilities.js";
|
|
9
14
|
import { httpIntrospect, type MethodInfo, type ServiceDescription } from "./introspect.js";
|
|
10
15
|
import {
|
|
11
16
|
buildRequestIpc,
|
|
@@ -16,9 +21,10 @@ import {
|
|
|
16
21
|
} from "./ipc.js";
|
|
17
22
|
import { HttpStreamSession } from "./stream.js";
|
|
18
23
|
import type { HttpConnectOptions, StreamSession } from "./types.js";
|
|
24
|
+
import { externalizeRequestBody } from "./uploadUrl.js";
|
|
19
25
|
|
|
20
|
-
type CompressFn = (data: Uint8Array, level: number) => Uint8Array
|
|
21
|
-
type DecompressFn = (data: Uint8Array) => Uint8Array
|
|
26
|
+
type CompressFn = (data: Uint8Array, level: number) => Promise<Uint8Array>;
|
|
27
|
+
type DecompressFn = (data: Uint8Array) => Promise<Uint8Array>;
|
|
22
28
|
|
|
23
29
|
export interface RpcClient {
|
|
24
30
|
call(method: string, params?: Record<string, any>): Promise<Record<string, any> | null>;
|
|
@@ -35,13 +41,73 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
35
41
|
const externalConfig = options?.externalLocation;
|
|
36
42
|
|
|
37
43
|
let methodCache: Map<string, MethodInfo> | null = null;
|
|
44
|
+
/** Application protocol surface version discovered via __describe__. When
|
|
45
|
+
* non-empty, the client emits it on every request as
|
|
46
|
+
* `vgi_rpc.protocol_version` so a versioned server can validate at the
|
|
47
|
+
* dispatch boundary. */
|
|
48
|
+
let serverProtocolVersion = "";
|
|
38
49
|
let compressFn: CompressFn | undefined;
|
|
39
50
|
let decompressFn: DecompressFn | undefined;
|
|
40
51
|
let compressionLoaded = false;
|
|
52
|
+
let capabilities: HttpServerCapabilities | null = null;
|
|
53
|
+
|
|
54
|
+
function updateCapabilitiesFromResponse(resp: Response): void {
|
|
55
|
+
const next = parseCapabilitiesFromHeaders(resp.headers);
|
|
56
|
+
// Only treat the snapshot as authoritative when the server actually
|
|
57
|
+
// emitted capability hints. Otherwise leave any prior cache in place.
|
|
58
|
+
if (next.maxRequestBytes != null || next.uploadUrlSupport) {
|
|
59
|
+
capabilities = next;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function maybeExternalize(body: Uint8Array): Promise<Uint8Array> {
|
|
64
|
+
const caps = isCapabilitySnapshotFresh(capabilities) ? capabilities : null;
|
|
65
|
+
if (!caps) return body;
|
|
66
|
+
if (!caps.uploadUrlSupport) return body;
|
|
67
|
+
if (caps.maxRequestBytes == null || body.byteLength <= caps.maxRequestBytes) return body;
|
|
68
|
+
return externalizeRequestBody(body, {
|
|
69
|
+
baseUrl,
|
|
70
|
+
prefix,
|
|
71
|
+
authorization,
|
|
72
|
+
urlValidator: externalConfig?.urlValidator ?? null,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Send a POST request, transparently retrying with externalization if
|
|
78
|
+
* the server returns 413 (Payload Too Large) and advertises upload-URL
|
|
79
|
+
* support. Mirrors Python's 413 fallback in `_HttpProxy._post_with_externalization`.
|
|
80
|
+
*/
|
|
81
|
+
async function postWithExternalization(url: string, body: Uint8Array): Promise<Response> {
|
|
82
|
+
const sendBody = await maybeExternalize(body);
|
|
83
|
+
let resp = await fetch(url, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: buildHeaders(),
|
|
86
|
+
body: (await prepareBody(sendBody)) as unknown as BodyInit,
|
|
87
|
+
});
|
|
88
|
+
updateCapabilitiesFromResponse(resp);
|
|
89
|
+
|
|
90
|
+
if (resp.status === 413 && capabilities?.uploadUrlSupport && body.byteLength > 0) {
|
|
91
|
+
// Refresh-and-retry: caps tell us we can externalize.
|
|
92
|
+
const externalized = await externalizeRequestBody(body, {
|
|
93
|
+
baseUrl,
|
|
94
|
+
prefix,
|
|
95
|
+
authorization,
|
|
96
|
+
urlValidator: externalConfig?.urlValidator ?? null,
|
|
97
|
+
});
|
|
98
|
+
resp = await fetch(url, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: buildHeaders(),
|
|
101
|
+
body: (await prepareBody(externalized)) as unknown as BodyInit,
|
|
102
|
+
});
|
|
103
|
+
updateCapabilitiesFromResponse(resp);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return resp;
|
|
107
|
+
}
|
|
41
108
|
|
|
42
109
|
async function ensureCompression(): Promise<void> {
|
|
43
110
|
if (compressionLoaded || compressionLevel == null) return;
|
|
44
|
-
compressionLoaded = true;
|
|
45
111
|
try {
|
|
46
112
|
const mod = await import("../util/zstd.js");
|
|
47
113
|
compressFn = mod.zstdCompress;
|
|
@@ -49,14 +115,17 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
49
115
|
} catch {
|
|
50
116
|
// zstd not available in this runtime
|
|
51
117
|
}
|
|
118
|
+
compressionLoaded = true;
|
|
52
119
|
}
|
|
53
120
|
|
|
54
121
|
function buildHeaders(): Record<string, string> {
|
|
55
122
|
const headers: Record<string, string> = {
|
|
56
123
|
"Content-Type": ARROW_CONTENT_TYPE,
|
|
57
124
|
};
|
|
58
|
-
if (compressionLevel != null) {
|
|
125
|
+
if (compressionLevel != null && compressFn) {
|
|
59
126
|
headers["Content-Encoding"] = "zstd";
|
|
127
|
+
}
|
|
128
|
+
if (compressionLevel != null && decompressFn) {
|
|
60
129
|
headers["Accept-Encoding"] = "zstd";
|
|
61
130
|
}
|
|
62
131
|
if (authorization) {
|
|
@@ -65,9 +134,9 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
65
134
|
return headers;
|
|
66
135
|
}
|
|
67
136
|
|
|
68
|
-
function prepareBody(content: Uint8Array): Uint8Array {
|
|
137
|
+
async function prepareBody(content: Uint8Array): Promise<Uint8Array> {
|
|
69
138
|
if (compressionLevel != null && compressFn) {
|
|
70
|
-
return compressFn(content, compressionLevel);
|
|
139
|
+
return await compressFn(content, compressionLevel);
|
|
71
140
|
}
|
|
72
141
|
return content;
|
|
73
142
|
}
|
|
@@ -81,15 +150,23 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
81
150
|
async function readResponse(resp: Response): Promise<Uint8Array<ArrayBuffer>> {
|
|
82
151
|
let body = new Uint8Array(await resp.arrayBuffer());
|
|
83
152
|
if (resp.headers.get("Content-Encoding") === "zstd" && decompressFn) {
|
|
84
|
-
body = new Uint8Array(decompressFn(body));
|
|
153
|
+
body = new Uint8Array(await decompressFn(body));
|
|
85
154
|
}
|
|
86
155
|
return body;
|
|
87
156
|
}
|
|
88
157
|
|
|
89
158
|
async function ensureMethodCache(): Promise<Map<string, MethodInfo>> {
|
|
90
159
|
if (methodCache) return methodCache;
|
|
91
|
-
|
|
160
|
+
await ensureCompression();
|
|
161
|
+
const desc = await httpIntrospect(baseUrl, {
|
|
162
|
+
prefix,
|
|
163
|
+
authorization,
|
|
164
|
+
compressionLevel,
|
|
165
|
+
compressFn,
|
|
166
|
+
decompressFn,
|
|
167
|
+
});
|
|
92
168
|
methodCache = new Map(desc.methods.map((m) => [m.name, m]));
|
|
169
|
+
serverProtocolVersion = desc.protocolVersion;
|
|
93
170
|
return methodCache;
|
|
94
171
|
}
|
|
95
172
|
|
|
@@ -105,12 +182,8 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
105
182
|
// Apply defaults
|
|
106
183
|
const fullParams = { ...(info.defaults ?? {}), ...(params ?? {}) };
|
|
107
184
|
|
|
108
|
-
const body = buildRequestIpc(info.paramsSchema, fullParams, method);
|
|
109
|
-
const resp = await
|
|
110
|
-
method: "POST",
|
|
111
|
-
headers: buildHeaders(),
|
|
112
|
-
body: prepareBody(body) as unknown as BodyInit,
|
|
113
|
-
});
|
|
185
|
+
const body = buildRequestIpc(info.paramsSchema, fullParams, method, { protocolVersion: serverProtocolVersion });
|
|
186
|
+
const resp = await postWithExternalization(`${baseUrl}${prefix}/${method}`, body);
|
|
114
187
|
checkAuth(resp);
|
|
115
188
|
|
|
116
189
|
const responseBody = await readResponse(resp);
|
|
@@ -121,8 +194,8 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
121
194
|
for (let batch of batches) {
|
|
122
195
|
if (batch.numRows === 0) {
|
|
123
196
|
// Check for external location pointer batch
|
|
124
|
-
if (isExternalLocationBatch(batch)) {
|
|
125
|
-
batch = await resolveExternalLocation(batch, externalConfig);
|
|
197
|
+
if (isExternalLocationBatch(batch as any)) {
|
|
198
|
+
batch = (await resolveExternalLocation(batch as any, externalConfig)) as any;
|
|
126
199
|
} else {
|
|
127
200
|
dispatchLogOrError(batch, onLog);
|
|
128
201
|
continue;
|
|
@@ -159,12 +232,8 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
159
232
|
// Apply defaults
|
|
160
233
|
const fullParams = { ...(info.defaults ?? {}), ...(params ?? {}) };
|
|
161
234
|
|
|
162
|
-
const body = buildRequestIpc(info.paramsSchema, fullParams, method);
|
|
163
|
-
const resp = await
|
|
164
|
-
method: "POST",
|
|
165
|
-
headers: buildHeaders(),
|
|
166
|
-
body: prepareBody(body) as unknown as BodyInit,
|
|
167
|
-
});
|
|
235
|
+
const body = buildRequestIpc(info.paramsSchema, fullParams, method, { protocolVersion: serverProtocolVersion });
|
|
236
|
+
const resp = await postWithExternalization(`${baseUrl}${prefix}/${method}/init`, body);
|
|
168
237
|
checkAuth(resp);
|
|
169
238
|
|
|
170
239
|
const responseBody = await readResponse(resp);
|
|
@@ -185,7 +254,7 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
185
254
|
// First stream: header
|
|
186
255
|
const headerStream = await reader.readStream();
|
|
187
256
|
if (headerStream) {
|
|
188
|
-
for (const batch of headerStream.batches) {
|
|
257
|
+
for (const batch of headerStream.batches as any[]) {
|
|
189
258
|
if (batch.numRows === 0) {
|
|
190
259
|
dispatchLogOrError(batch, onLog);
|
|
191
260
|
continue;
|
|
@@ -200,11 +269,11 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
200
269
|
// Second stream: data/state
|
|
201
270
|
const dataStream = await reader.readStream();
|
|
202
271
|
if (dataStream) {
|
|
203
|
-
streamSchema = dataStream.schema;
|
|
272
|
+
streamSchema = dataStream.schema as any;
|
|
204
273
|
}
|
|
205
274
|
const headerErrorBatches: RecordBatch[] = [];
|
|
206
275
|
if (dataStream) {
|
|
207
|
-
for (const batch of dataStream.batches) {
|
|
276
|
+
for (const batch of dataStream.batches as any[]) {
|
|
208
277
|
if (batch.numRows === 0) {
|
|
209
278
|
// Check for state token
|
|
210
279
|
const token = batch.metadata?.get(STATE_KEY);
|
|
@@ -310,11 +379,19 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
310
379
|
decompressFn,
|
|
311
380
|
authorization,
|
|
312
381
|
externalConfig,
|
|
382
|
+
postFn: postWithExternalization,
|
|
313
383
|
});
|
|
314
384
|
},
|
|
315
385
|
|
|
316
386
|
async describe(): Promise<ServiceDescription> {
|
|
317
|
-
|
|
387
|
+
await ensureCompression();
|
|
388
|
+
return httpIntrospect(baseUrl, {
|
|
389
|
+
prefix,
|
|
390
|
+
authorization,
|
|
391
|
+
compressionLevel,
|
|
392
|
+
compressFn,
|
|
393
|
+
decompressFn,
|
|
394
|
+
});
|
|
318
395
|
},
|
|
319
396
|
|
|
320
397
|
close(): void {
|
package/src/client/introspect.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import { Schema as ArrowSchema, type RecordBatch,
|
|
5
|
-
import {
|
|
4
|
+
import { Schema as ArrowSchema, type RecordBatch, type Schema } from "@query-farm/apache-arrow";
|
|
5
|
+
import { deserializeSchema as deserializeSchemaImpl } from "#vgi-rpc-arrow";
|
|
6
|
+
import { DESCRIBE_METHOD_NAME, PROTOCOL_NAME_KEY, PROTOCOL_VERSION_KEY } from "../constants.js";
|
|
6
7
|
import { RpcError } from "../errors.js";
|
|
7
8
|
import { ARROW_CONTENT_TYPE } from "../http/common.js";
|
|
8
9
|
import { buildRequestIpc, dispatchLogOrError, readResponseBatches } from "./ipc.js";
|
|
@@ -23,14 +24,29 @@ export interface MethodInfo {
|
|
|
23
24
|
|
|
24
25
|
export interface ServiceDescription {
|
|
25
26
|
protocolName: string;
|
|
27
|
+
/** Application protocol surface version surfaced by the server's
|
|
28
|
+
* __describe__ response. Empty string when the server did not declare
|
|
29
|
+
* a `protocolVersion`. */
|
|
30
|
+
protocolVersion: string;
|
|
26
31
|
methods: MethodInfo[];
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Deserialize a schema from IPC bytes (schema message + EOS).
|
|
36
|
+
*
|
|
37
|
+
* Must dispatch via `#vgi-rpc-arrow` so the resulting type instances are
|
|
38
|
+
* the same impl (apache-arrow / flechette) as the rest of the active
|
|
39
|
+
* backend. Using apache-arrow's `RecordBatchReader` directly here used to
|
|
40
|
+
* silently mix impls: in browser builds the backend is flechette, and a
|
|
41
|
+
* flechette builder receiving an apache-arrow `Binary` type defaults to
|
|
42
|
+
* the wrong offsets buffer (Uint8Array instead of Int32Array) and emits
|
|
43
|
+
* a 0-byte value where a populated binary column was expected. The
|
|
44
|
+
* downstream symptom is "Tried reading schema message, was null or
|
|
45
|
+
* length 0" from the server when it tries to open the (empty) binary
|
|
46
|
+
* column as a nested IPC stream. See test/client/ipc-cross-impl.test.ts.
|
|
47
|
+
*/
|
|
48
|
+
function deserializeSchema(bytes: Uint8Array): Schema {
|
|
49
|
+
return deserializeSchemaImpl(bytes) as unknown as Schema;
|
|
34
50
|
}
|
|
35
51
|
|
|
36
52
|
/**
|
|
@@ -58,45 +74,30 @@ export async function parseDescribeResponse(
|
|
|
58
74
|
// Extract metadata from batch
|
|
59
75
|
const meta = dataBatch.metadata;
|
|
60
76
|
const protocolName = meta?.get(PROTOCOL_NAME_KEY) ?? "";
|
|
77
|
+
const protocolVersion = meta?.get(PROTOCOL_VERSION_KEY) ?? "";
|
|
61
78
|
|
|
79
|
+
// Slim DESCRIBE_VERSION 4 wire format (see dispatch/describe.ts):
|
|
80
|
+
// 0:name 1:method_type 2:has_return 3:params_schema_ipc
|
|
81
|
+
// 4:result_schema_ipc 5:has_header 6:header_schema_ipc 7:is_exchange
|
|
62
82
|
const methods: MethodInfo[] = [];
|
|
63
83
|
for (let i = 0; i < dataBatch.numRows; i++) {
|
|
64
|
-
const name = dataBatch.getChildAt(0)!.get(i) as string;
|
|
65
|
-
const methodType = dataBatch.getChildAt(1)!.get(i) as string;
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
const hasHeader = dataBatch.getChildAt(8)!.get(i) as boolean; // has_header
|
|
73
|
-
const headerIpc = dataBatch.getChildAt(9)?.get(i) as Uint8Array | null; // header_schema_ipc
|
|
84
|
+
const name = dataBatch.getChildAt(0)!.get(i) as string;
|
|
85
|
+
const methodType = dataBatch.getChildAt(1)!.get(i) as string;
|
|
86
|
+
const _hasReturn = dataBatch.getChildAt(2)!.get(i) as boolean;
|
|
87
|
+
const paramsIpc = dataBatch.getChildAt(3)!.get(i) as Uint8Array;
|
|
88
|
+
const resultIpc = dataBatch.getChildAt(4)!.get(i) as Uint8Array;
|
|
89
|
+
const hasHeader = dataBatch.getChildAt(5)!.get(i) as boolean;
|
|
90
|
+
const headerIpc = dataBatch.getChildAt(6)?.get(i) as Uint8Array | null;
|
|
91
|
+
// is_exchange (index 7) currently unused on the client side.
|
|
74
92
|
|
|
75
93
|
const paramsSchema = await deserializeSchema(paramsIpc);
|
|
76
94
|
const resultSchema = await deserializeSchema(resultIpc);
|
|
77
95
|
|
|
78
|
-
let paramTypes: Record<string, string> | undefined;
|
|
79
|
-
if (paramTypesJson) {
|
|
80
|
-
try {
|
|
81
|
-
paramTypes = JSON.parse(paramTypesJson);
|
|
82
|
-
} catch {}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
let defaults: Record<string, any> | undefined;
|
|
86
|
-
if (paramDefaultsJson) {
|
|
87
|
-
try {
|
|
88
|
-
defaults = JSON.parse(paramDefaultsJson);
|
|
89
|
-
} catch {}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
96
|
const info: MethodInfo = {
|
|
93
97
|
name,
|
|
94
98
|
type: methodType as "unary" | "stream",
|
|
95
99
|
paramsSchema,
|
|
96
100
|
resultSchema,
|
|
97
|
-
doc: doc ?? undefined,
|
|
98
|
-
paramTypes,
|
|
99
|
-
defaults,
|
|
100
101
|
};
|
|
101
102
|
|
|
102
103
|
// For stream methods, result_schema_ipc actually holds the output schema
|
|
@@ -111,7 +112,7 @@ export async function parseDescribeResponse(
|
|
|
111
112
|
methods.push(info);
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
return { protocolName, methods };
|
|
115
|
+
return { protocolName, protocolVersion, methods };
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
/**
|
|
@@ -119,7 +120,13 @@ export async function parseDescribeResponse(
|
|
|
119
120
|
*/
|
|
120
121
|
export async function httpIntrospect(
|
|
121
122
|
baseUrl: string,
|
|
122
|
-
options?: {
|
|
123
|
+
options?: {
|
|
124
|
+
prefix?: string;
|
|
125
|
+
authorization?: string;
|
|
126
|
+
compressionLevel?: number;
|
|
127
|
+
compressFn?: (data: Uint8Array, level: number) => Promise<Uint8Array>;
|
|
128
|
+
decompressFn?: (data: Uint8Array) => Promise<Uint8Array>;
|
|
129
|
+
},
|
|
123
130
|
): Promise<ServiceDescription> {
|
|
124
131
|
const prefix = options?.prefix ?? "";
|
|
125
132
|
const emptySchema = new ArrowSchema([]);
|
|
@@ -130,16 +137,31 @@ export async function httpIntrospect(
|
|
|
130
137
|
headers.Authorization = options.authorization;
|
|
131
138
|
}
|
|
132
139
|
|
|
140
|
+
const level = options?.compressionLevel;
|
|
141
|
+
const compressFn = options?.compressFn;
|
|
142
|
+
const decompressFn = options?.decompressFn;
|
|
143
|
+
let sendBody: Uint8Array = body;
|
|
144
|
+
if (level != null && compressFn) {
|
|
145
|
+
headers["Content-Encoding"] = "zstd";
|
|
146
|
+
sendBody = await compressFn(body, level);
|
|
147
|
+
}
|
|
148
|
+
if (level != null && decompressFn) {
|
|
149
|
+
headers["Accept-Encoding"] = "zstd";
|
|
150
|
+
}
|
|
151
|
+
|
|
133
152
|
const response = await fetch(`${baseUrl}${prefix}/${DESCRIBE_METHOD_NAME}`, {
|
|
134
153
|
method: "POST",
|
|
135
154
|
headers,
|
|
136
|
-
body:
|
|
155
|
+
body: sendBody as unknown as BodyInit,
|
|
137
156
|
});
|
|
138
157
|
if (response.status === 401) {
|
|
139
158
|
throw new RpcError("AuthenticationError", "Authentication required", "");
|
|
140
159
|
}
|
|
141
160
|
|
|
142
|
-
|
|
161
|
+
let responseBody = new Uint8Array(await response.arrayBuffer());
|
|
162
|
+
if (response.headers.get("Content-Encoding") === "zstd" && decompressFn) {
|
|
163
|
+
responseBody = new Uint8Array(await decompressFn(responseBody));
|
|
164
|
+
}
|
|
143
165
|
const { batches } = await readResponseBatches(responseBody);
|
|
144
166
|
|
|
145
167
|
return parseDescribeResponse(batches);
|