@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/constants.ts
CHANGED
|
@@ -1,25 +1,57 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Well-known metadata keys matching Python's metadata.py.
|
|
5
5
|
|
|
6
|
+
/** Batch-metadata key carrying the invoked RPC method name. */
|
|
6
7
|
export const RPC_METHOD_KEY = "vgi_rpc.method";
|
|
8
|
+
/** Batch-metadata key carrying a log batch's severity level. */
|
|
7
9
|
export const LOG_LEVEL_KEY = "vgi_rpc.log_level";
|
|
10
|
+
/** Batch-metadata key carrying a log batch's message text. */
|
|
8
11
|
export const LOG_MESSAGE_KEY = "vgi_rpc.log_message";
|
|
12
|
+
/** Batch-metadata key carrying a log batch's structured extra fields. */
|
|
9
13
|
export const LOG_EXTRA_KEY = "vgi_rpc.log_extra";
|
|
14
|
+
/** Batch-metadata key carrying the wire request-framing version. */
|
|
10
15
|
export const REQUEST_VERSION_KEY = "vgi_rpc.request_version";
|
|
16
|
+
/** Current wire request-framing version. Distinct from the application-level
|
|
17
|
+
* {@link PROTOCOL_VERSION_KEY protocol version}. */
|
|
11
18
|
export const REQUEST_VERSION = "1";
|
|
12
19
|
|
|
20
|
+
/** Batch-metadata key identifying the server instance that produced a batch. */
|
|
13
21
|
export const SERVER_ID_KEY = "vgi_rpc.server_id";
|
|
22
|
+
/** Batch-metadata key carrying the client-supplied request id. */
|
|
14
23
|
export const REQUEST_ID_KEY = "vgi_rpc.request_id";
|
|
15
24
|
|
|
25
|
+
/** Batch-metadata key carrying the service / protocol name. */
|
|
16
26
|
export const PROTOCOL_NAME_KEY = "vgi_rpc.protocol_name";
|
|
27
|
+
/** Batch-metadata key carrying the `__describe__` response schema version. */
|
|
17
28
|
export const DESCRIBE_VERSION_KEY = "vgi_rpc.describe_version";
|
|
18
|
-
export const
|
|
29
|
+
export const PROTOCOL_HASH_KEY = "vgi_rpc.protocol_hash";
|
|
30
|
+
/** Current `__describe__` response schema version (the slim 8-column schema). */
|
|
31
|
+
export const DESCRIBE_VERSION = "4";
|
|
19
32
|
|
|
33
|
+
/** Application protocol surface version. Carried on every request batch from
|
|
34
|
+
* a client bound to a Protocol that declares `protocolVersion`; also emitted
|
|
35
|
+
* in the __describe__ response metadata. Format: canonical semver
|
|
36
|
+
* MAJOR.MINOR.PATCH. Enforced at the dispatch boundary on the server: exact
|
|
37
|
+
* major+minor match required, patch ignored. Distinct from `REQUEST_VERSION`
|
|
38
|
+
* (wire framing). Mirrors Python's `PROTOCOL_VERSION_KEY`. */
|
|
39
|
+
export const PROTOCOL_VERSION_KEY = "vgi_rpc.protocol_version";
|
|
40
|
+
|
|
41
|
+
/** Reserved method name for the introspection (`__describe__`) call. */
|
|
20
42
|
export const DESCRIBE_METHOD_NAME = "__describe__";
|
|
21
43
|
|
|
44
|
+
/** Batch-metadata key carrying the base64-encoded stream continuation/state token. */
|
|
22
45
|
export const STATE_KEY = "vgi_rpc.stream_state#b64";
|
|
46
|
+
export const CANCEL_KEY = "vgi_rpc.cancel";
|
|
23
47
|
|
|
24
48
|
export const LOCATION_KEY = "vgi_rpc.location";
|
|
25
49
|
export const LOCATION_SHA256_KEY = "vgi_rpc.location.sha256";
|
|
50
|
+
|
|
51
|
+
/** HTTP response header set when an RPC error is returned over the HTTP transport. */
|
|
52
|
+
export const RPC_ERROR_HEADER = "X-VGI-RPC-Error";
|
|
53
|
+
|
|
54
|
+
/** Top-level metadata key on an EXCEPTION batch identifying the error category.
|
|
55
|
+
* Hoisted by `buildErrorBatch` when the thrown error has a static or instance
|
|
56
|
+
* `errorKind` property. Mirrors Python's `vgi_rpc.metadata.ERROR_KIND_KEY`. */
|
|
57
|
+
export const ERROR_KIND_KEY = "vgi_rpc.error_kind";
|
package/src/crypto.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generic AEAD seal/open primitives shared by stream-state and sticky-session
|
|
6
|
+
* tokens. Mirrors Python's `vgi_rpc.crypto` module: a tiny envelope around
|
|
7
|
+
* XChaCha20-Poly1305 with a leading version byte so future format bumps stay
|
|
8
|
+
* self-describing.
|
|
9
|
+
*
|
|
10
|
+
* Wire format (returned by {@link sealBytes}, accepted by {@link openBytes}):
|
|
11
|
+
*
|
|
12
|
+
* ```
|
|
13
|
+
* [1B version (1..255)]
|
|
14
|
+
* [24B nonce (XChaCha20-Poly1305, random per envelope)]
|
|
15
|
+
* [.. ciphertext + 16B Poly1305 tag]
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The plaintext frame is fully up to the caller — only the version + nonce +
|
|
19
|
+
* tag overhead is fixed. AAD (`aad`) is bound at the crypto layer so an
|
|
20
|
+
* envelope sealed for one identity fails decryption when presented by another.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
|
|
24
|
+
import { randomBytes } from "./util/web-crypto.js";
|
|
25
|
+
|
|
26
|
+
const NONCE_LEN = 24;
|
|
27
|
+
const TAG_LEN = 16;
|
|
28
|
+
const VERSION_LEN = 1;
|
|
29
|
+
const MIN_ENVELOPE_LEN = VERSION_LEN + NONCE_LEN + TAG_LEN;
|
|
30
|
+
|
|
31
|
+
/** Thrown by {@link openBytes} for any envelope it cannot open — malformed,
|
|
32
|
+
* tampered, wrong key, wrong AAD, wrong version, truncated, all surface the
|
|
33
|
+
* same way so callers cannot distinguish failure modes via error content. */
|
|
34
|
+
export class SealError extends Error {
|
|
35
|
+
constructor(message: string) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "SealError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Normalise a key to 32 bytes by SHA-256 hashing when it isn't already 32B.
|
|
42
|
+
* Mirrors Python's `normalize_key` so any callers can pass operator-provided
|
|
43
|
+
* keys of arbitrary length without a separate stretching step. */
|
|
44
|
+
export async function normalizeKey(key: Uint8Array): Promise<Uint8Array> {
|
|
45
|
+
if (key.length === 32) return key;
|
|
46
|
+
const digest = await crypto.subtle.digest("SHA-256", key as BufferSource);
|
|
47
|
+
return new Uint8Array(digest);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SealOptions {
|
|
51
|
+
/** Associated data bound at the crypto layer — typically a principal or
|
|
52
|
+
* request-scoped identifier. Must match between seal and open. */
|
|
53
|
+
aad: Uint8Array;
|
|
54
|
+
/** Envelope version byte. Defaults to 1; carry through to {@link openBytes}. */
|
|
55
|
+
version?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Seal `plaintext` under `key` with AEAD, returning the wire envelope. */
|
|
59
|
+
export function sealBytes(plaintext: Uint8Array, key: Uint8Array, opts: SealOptions): Uint8Array {
|
|
60
|
+
if (key.length !== 32) {
|
|
61
|
+
throw new Error("AEAD key must be 32 bytes — call normalizeKey() first");
|
|
62
|
+
}
|
|
63
|
+
const version = opts.version ?? 1;
|
|
64
|
+
if (version < 1 || version > 255) {
|
|
65
|
+
throw new Error(`AEAD envelope version must fit in one byte; got ${version}`);
|
|
66
|
+
}
|
|
67
|
+
const nonce = randomBytes(NONCE_LEN);
|
|
68
|
+
const ciphertext = xchacha20poly1305(key, nonce, opts.aad as Uint8Array).encrypt(plaintext);
|
|
69
|
+
const wire = new Uint8Array(VERSION_LEN + NONCE_LEN + ciphertext.length);
|
|
70
|
+
wire[0] = version;
|
|
71
|
+
wire.set(nonce, VERSION_LEN);
|
|
72
|
+
wire.set(ciphertext, VERSION_LEN + NONCE_LEN);
|
|
73
|
+
return wire;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Open and verify an envelope produced by {@link sealBytes}. */
|
|
77
|
+
export function openBytes(envelope: Uint8Array, key: Uint8Array, opts: SealOptions): Uint8Array {
|
|
78
|
+
if (key.length !== 32) {
|
|
79
|
+
throw new Error("AEAD key must be 32 bytes — call normalizeKey() first");
|
|
80
|
+
}
|
|
81
|
+
if (envelope.length < MIN_ENVELOPE_LEN) {
|
|
82
|
+
throw new SealError("envelope truncated");
|
|
83
|
+
}
|
|
84
|
+
const expectedVersion = opts.version ?? 1;
|
|
85
|
+
if (envelope[0] !== expectedVersion) {
|
|
86
|
+
throw new SealError(`unsupported envelope version: ${envelope[0]}`);
|
|
87
|
+
}
|
|
88
|
+
const nonce = envelope.subarray(VERSION_LEN, VERSION_LEN + NONCE_LEN);
|
|
89
|
+
const ciphertext = envelope.subarray(VERSION_LEN + NONCE_LEN);
|
|
90
|
+
try {
|
|
91
|
+
return xchacha20poly1305(key, nonce, opts.aad as Uint8Array).decrypt(ciphertext);
|
|
92
|
+
} catch {
|
|
93
|
+
throw new SealError("envelope verification failed");
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/dispatch/describe.ts
CHANGED
|
@@ -2,162 +2,201 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from "@query-farm/apache-arrow";
|
|
5
|
+
batchFromColumns,
|
|
6
|
+
binary,
|
|
7
|
+
bool,
|
|
8
|
+
field,
|
|
9
|
+
schema as makeSchema,
|
|
10
|
+
utf8,
|
|
11
|
+
type VgiBatch,
|
|
12
|
+
withBatchMetadata,
|
|
13
|
+
} from "../arrow/index.js";
|
|
15
14
|
import {
|
|
16
15
|
DESCRIBE_VERSION,
|
|
17
16
|
DESCRIBE_VERSION_KEY,
|
|
17
|
+
PROTOCOL_HASH_KEY,
|
|
18
18
|
PROTOCOL_NAME_KEY,
|
|
19
|
+
PROTOCOL_VERSION_KEY,
|
|
19
20
|
REQUEST_VERSION,
|
|
20
21
|
REQUEST_VERSION_KEY,
|
|
21
22
|
SERVER_ID_KEY,
|
|
22
23
|
} from "../constants.js";
|
|
23
24
|
import type { MethodDefinition } from "../types.js";
|
|
24
25
|
import { serializeSchema } from "../util/schema.js";
|
|
26
|
+
import { sha256Hex } from "../util/web-crypto.js";
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
|
-
*
|
|
29
|
+
* Slim DESCRIBE_VERSION 4 schema. Python-flavoured fields (doc,
|
|
30
|
+
* param_types_json, param_defaults_json, param_docs_json) are not on the
|
|
31
|
+
* wire — Arrow IPC schema bytes are the authoritative type information;
|
|
32
|
+
* everything else is source-level metadata that callers should consult the
|
|
33
|
+
* Protocol class for.
|
|
28
34
|
*/
|
|
29
|
-
export const DESCRIBE_SCHEMA =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
new Field("has_header", new Bool(), false),
|
|
39
|
-
new Field("header_schema_ipc", new Binary(), true),
|
|
40
|
-
new Field("is_exchange", new Bool(), true),
|
|
41
|
-
new Field("param_docs_json", new Utf8(), true),
|
|
35
|
+
export const DESCRIBE_SCHEMA = makeSchema([
|
|
36
|
+
field("name", utf8(), false),
|
|
37
|
+
field("method_type", utf8(), false),
|
|
38
|
+
field("has_return", bool(), false),
|
|
39
|
+
field("params_schema_ipc", binary(), false),
|
|
40
|
+
field("result_schema_ipc", binary(), false),
|
|
41
|
+
field("has_header", bool(), false),
|
|
42
|
+
field("header_schema_ipc", binary(), true),
|
|
43
|
+
field("is_exchange", bool(), true),
|
|
42
44
|
]);
|
|
43
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Compute the SHA-256 hex digest of the canonical describe payload. Mirrors
|
|
48
|
+
* Python's vgi_rpc.introspect.compute_protocol_hash byte-for-byte, so two
|
|
49
|
+
* implementations of the same Protocol that produce identical Arrow IPC
|
|
50
|
+
* schema bytes for params/result/header will hash to the same value.
|
|
51
|
+
*/
|
|
52
|
+
async function computeProtocolHash(
|
|
53
|
+
protocolName: string,
|
|
54
|
+
rows: ReadonlyArray<{
|
|
55
|
+
name: string;
|
|
56
|
+
methodType: string;
|
|
57
|
+
hasReturn: boolean;
|
|
58
|
+
hasHeader: boolean;
|
|
59
|
+
isExchange: boolean | null;
|
|
60
|
+
paramsIpc: Uint8Array;
|
|
61
|
+
resultIpc: Uint8Array;
|
|
62
|
+
headerIpc: Uint8Array | null;
|
|
63
|
+
}>,
|
|
64
|
+
): Promise<string> {
|
|
65
|
+
// Web Crypto's `subtle.digest` takes a single buffer, so we accumulate the
|
|
66
|
+
// canonical byte stream into a single Uint8Array and hash once. The byte
|
|
67
|
+
// sequence below must remain byte-for-byte identical to the Python
|
|
68
|
+
// implementation in vgi_rpc.introspect.compute_protocol_hash.
|
|
69
|
+
const enc = new TextEncoder();
|
|
70
|
+
const parts: Uint8Array[] = [];
|
|
71
|
+
const push = (v: string | Uint8Array) => parts.push(typeof v === "string" ? enc.encode(v) : v);
|
|
72
|
+
|
|
73
|
+
push("vgi_rpc.describe.v");
|
|
74
|
+
push(DESCRIBE_VERSION);
|
|
75
|
+
push("|");
|
|
76
|
+
push(REQUEST_VERSION);
|
|
77
|
+
push("|");
|
|
78
|
+
push(protocolName);
|
|
79
|
+
push("|");
|
|
80
|
+
for (const r of rows) {
|
|
81
|
+
push(Uint8Array.of(0x1f));
|
|
82
|
+
push(r.name);
|
|
83
|
+
push(Uint8Array.of(0x1e));
|
|
84
|
+
push(r.methodType);
|
|
85
|
+
push(Uint8Array.of(0x1e));
|
|
86
|
+
push(r.hasReturn ? "1" : "0");
|
|
87
|
+
push(Uint8Array.of(0x1e));
|
|
88
|
+
push(r.hasHeader ? "1" : "0");
|
|
89
|
+
push(Uint8Array.of(0x1e));
|
|
90
|
+
push(r.isExchange === null ? "-" : r.isExchange ? "1" : "0");
|
|
91
|
+
push(Uint8Array.of(0x1e));
|
|
92
|
+
push(r.paramsIpc);
|
|
93
|
+
push(Uint8Array.of(0x1e));
|
|
94
|
+
push(r.resultIpc);
|
|
95
|
+
push(Uint8Array.of(0x1e));
|
|
96
|
+
if (r.headerIpc) push(r.headerIpc);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let total = 0;
|
|
100
|
+
for (const p of parts) total += p.length;
|
|
101
|
+
const buf = new Uint8Array(total);
|
|
102
|
+
let off = 0;
|
|
103
|
+
for (const p of parts) {
|
|
104
|
+
buf.set(p, off);
|
|
105
|
+
off += p.length;
|
|
106
|
+
}
|
|
107
|
+
return sha256Hex(buf);
|
|
108
|
+
}
|
|
109
|
+
|
|
44
110
|
/**
|
|
45
111
|
* Build the __describe__ response batch and metadata.
|
|
46
112
|
*/
|
|
47
|
-
export function buildDescribeBatch(
|
|
113
|
+
export async function buildDescribeBatch(
|
|
48
114
|
protocolName: string,
|
|
49
115
|
methods: Map<string, MethodDefinition>,
|
|
50
116
|
serverId: string,
|
|
51
|
-
|
|
117
|
+
protocolVersion?: string,
|
|
118
|
+
): Promise<{ batch: VgiBatch; metadata: Map<string, string> }> {
|
|
52
119
|
// Sort methods by name for consistent ordering
|
|
53
120
|
const sortedEntries = [...methods.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
54
121
|
|
|
55
122
|
const names: (string | null)[] = [];
|
|
56
123
|
const methodTypes: (string | null)[] = [];
|
|
57
|
-
const docs: (string | null)[] = [];
|
|
58
124
|
const hasReturns: boolean[] = [];
|
|
59
125
|
const paramsSchemas: (Uint8Array | null)[] = [];
|
|
60
126
|
const resultSchemas: (Uint8Array | null)[] = [];
|
|
61
|
-
const paramTypesJsons: (string | null)[] = [];
|
|
62
|
-
const paramDefaultsJsons: (string | null)[] = [];
|
|
63
127
|
const hasHeaders: boolean[] = [];
|
|
64
128
|
const headerSchemas: (Uint8Array | null)[] = [];
|
|
65
129
|
const isExchanges: (boolean | null)[] = [];
|
|
66
|
-
|
|
130
|
+
|
|
131
|
+
const hashRows: Array<{
|
|
132
|
+
name: string;
|
|
133
|
+
methodType: string;
|
|
134
|
+
hasReturn: boolean;
|
|
135
|
+
hasHeader: boolean;
|
|
136
|
+
isExchange: boolean | null;
|
|
137
|
+
paramsIpc: Uint8Array;
|
|
138
|
+
resultIpc: Uint8Array;
|
|
139
|
+
headerIpc: Uint8Array | null;
|
|
140
|
+
}> = [];
|
|
67
141
|
|
|
68
142
|
for (const [name, method] of sortedEntries) {
|
|
69
143
|
names.push(name);
|
|
70
144
|
methodTypes.push(method.type);
|
|
71
|
-
docs.push(method.doc ?? null);
|
|
72
145
|
|
|
73
|
-
// Unary methods with non-empty result schema have a return value
|
|
74
146
|
const hasReturn = method.type === "unary" && method.resultSchema.fields.length > 0;
|
|
75
147
|
hasReturns.push(hasReturn);
|
|
76
148
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (method.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// is_exchange: true for exchange, false for producer, null for unary
|
|
104
|
-
if (method.exchangeFn) {
|
|
105
|
-
isExchanges.push(true);
|
|
106
|
-
} else if (method.producerFn) {
|
|
107
|
-
isExchanges.push(false);
|
|
108
|
-
} else {
|
|
109
|
-
isExchanges.push(null);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// param_docs_json: no docstring source in TypeScript, always null
|
|
113
|
-
paramDocsJsons.push(null);
|
|
149
|
+
const paramsIpc = serializeSchema(method.paramsSchema);
|
|
150
|
+
const resultIpc = serializeSchema(method.resultSchema);
|
|
151
|
+
paramsSchemas.push(paramsIpc);
|
|
152
|
+
resultSchemas.push(resultIpc);
|
|
153
|
+
|
|
154
|
+
const hasHeader = !!method.headerSchema;
|
|
155
|
+
hasHeaders.push(hasHeader);
|
|
156
|
+
const headerIpc = method.headerSchema ? serializeSchema(method.headerSchema) : null;
|
|
157
|
+
headerSchemas.push(headerIpc);
|
|
158
|
+
|
|
159
|
+
let isExchange: boolean | null;
|
|
160
|
+
if (method.exchangeFn) isExchange = true;
|
|
161
|
+
else if (method.producerFn) isExchange = false;
|
|
162
|
+
else isExchange = null;
|
|
163
|
+
isExchanges.push(isExchange);
|
|
164
|
+
|
|
165
|
+
hashRows.push({
|
|
166
|
+
name,
|
|
167
|
+
methodType: method.type,
|
|
168
|
+
hasReturn,
|
|
169
|
+
hasHeader,
|
|
170
|
+
isExchange,
|
|
171
|
+
paramsIpc,
|
|
172
|
+
resultIpc,
|
|
173
|
+
headerIpc,
|
|
174
|
+
});
|
|
114
175
|
}
|
|
115
176
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const hasHeaderArr = vectorFromArray(hasHeaders, new Bool());
|
|
126
|
-
const headerSchemaArr = vectorFromArray(headerSchemas, new Binary());
|
|
127
|
-
const isExchangeArr = vectorFromArray(isExchanges, new Bool());
|
|
128
|
-
const paramDocsArr = vectorFromArray(paramDocsJsons, new Utf8());
|
|
129
|
-
|
|
130
|
-
const children = [
|
|
131
|
-
nameArr.data[0],
|
|
132
|
-
methodTypeArr.data[0],
|
|
133
|
-
docArr.data[0],
|
|
134
|
-
hasReturnArr.data[0],
|
|
135
|
-
paramsSchemaArr.data[0],
|
|
136
|
-
resultSchemaArr.data[0],
|
|
137
|
-
paramTypesArr.data[0],
|
|
138
|
-
paramDefaultsArr.data[0],
|
|
139
|
-
hasHeaderArr.data[0],
|
|
140
|
-
headerSchemaArr.data[0],
|
|
141
|
-
isExchangeArr.data[0],
|
|
142
|
-
paramDocsArr.data[0],
|
|
143
|
-
];
|
|
144
|
-
|
|
145
|
-
const structType = new Struct(DESCRIBE_SCHEMA.fields);
|
|
146
|
-
const data = makeData({
|
|
147
|
-
type: structType,
|
|
148
|
-
length: sortedEntries.length,
|
|
149
|
-
children,
|
|
150
|
-
nullCount: 0,
|
|
177
|
+
const baseBatch = batchFromColumns(DESCRIBE_SCHEMA, {
|
|
178
|
+
name: names,
|
|
179
|
+
method_type: methodTypes,
|
|
180
|
+
has_return: hasReturns,
|
|
181
|
+
params_schema_ipc: paramsSchemas,
|
|
182
|
+
result_schema_ipc: resultSchemas,
|
|
183
|
+
has_header: hasHeaders,
|
|
184
|
+
header_schema_ipc: headerSchemas,
|
|
185
|
+
is_exchange: isExchanges,
|
|
151
186
|
});
|
|
152
187
|
|
|
153
|
-
|
|
188
|
+
const protocolHash = await computeProtocolHash(protocolName, hashRows);
|
|
189
|
+
|
|
154
190
|
const metadata = new Map<string, string>();
|
|
155
191
|
metadata.set(PROTOCOL_NAME_KEY, protocolName);
|
|
156
192
|
metadata.set(REQUEST_VERSION_KEY, REQUEST_VERSION);
|
|
157
193
|
metadata.set(DESCRIBE_VERSION_KEY, DESCRIBE_VERSION);
|
|
194
|
+
metadata.set(PROTOCOL_HASH_KEY, protocolHash);
|
|
158
195
|
metadata.set(SERVER_ID_KEY, serverId);
|
|
196
|
+
if (protocolVersion) {
|
|
197
|
+
metadata.set(PROTOCOL_VERSION_KEY, protocolVersion);
|
|
198
|
+
}
|
|
159
199
|
|
|
160
|
-
const batch =
|
|
161
|
-
|
|
200
|
+
const batch = withBatchMetadata(baseBatch, metadata);
|
|
162
201
|
return { batch, metadata };
|
|
163
202
|
}
|
package/src/dispatch/stream.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { conformBatchToSchema, schema as makeSchema, withBatchMetadata } from "../arrow/index.js";
|
|
5
|
+
import { CANCEL_KEY } from "../constants.js";
|
|
5
6
|
import { type ExternalLocationConfig, maybeExternalizeBatch } from "../external.js";
|
|
6
|
-
import type { MethodDefinition } from "../types.js";
|
|
7
|
+
import type { MethodDefinition, TransportKind } from "../types.js";
|
|
7
8
|
import { OutputCollector } from "../types.js";
|
|
8
|
-
import { conformBatchToSchema } from "../util/conform.js";
|
|
9
9
|
import type { IpcStreamReader } from "../wire/reader.js";
|
|
10
10
|
import { buildErrorBatch, buildResultBatch } from "../wire/response.js";
|
|
11
11
|
import type { IpcStreamWriter } from "../wire/writer.js";
|
|
12
12
|
|
|
13
|
-
const EMPTY_SCHEMA =
|
|
13
|
+
const EMPTY_SCHEMA = makeSchema([]);
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Dispatch a stream RPC call (producer or exchange).
|
|
@@ -35,6 +35,7 @@ export async function dispatchStream(
|
|
|
35
35
|
serverId: string,
|
|
36
36
|
requestId: string | null,
|
|
37
37
|
externalConfig?: ExternalLocationConfig,
|
|
38
|
+
kind?: TransportKind,
|
|
38
39
|
): Promise<void> {
|
|
39
40
|
const isProducer = !!method.producerFn;
|
|
40
41
|
|
|
@@ -48,7 +49,7 @@ export async function dispatchStream(
|
|
|
48
49
|
} catch (error: any) {
|
|
49
50
|
const errSchema = method.headerSchema ?? EMPTY_SCHEMA;
|
|
50
51
|
const errBatch = buildErrorBatch(errSchema, error, serverId, requestId);
|
|
51
|
-
writer.writeStream(errSchema, [errBatch]);
|
|
52
|
+
await writer.writeStream(errSchema, [errBatch]);
|
|
52
53
|
// Still need to consume the input stream from the client
|
|
53
54
|
const inputSchema = await reader.openNextStream();
|
|
54
55
|
if (inputSchema) {
|
|
@@ -71,14 +72,14 @@ export async function dispatchStream(
|
|
|
71
72
|
// Write header IPC stream if method has a header schema
|
|
72
73
|
if (method.headerSchema && method.headerInit) {
|
|
73
74
|
try {
|
|
74
|
-
const headerOut = new OutputCollector(method.headerSchema, true, serverId, requestId);
|
|
75
|
+
const headerOut = new OutputCollector(method.headerSchema, true, serverId, requestId, undefined, undefined, kind);
|
|
75
76
|
const headerValues = method.headerInit(params, state, headerOut);
|
|
76
77
|
const headerBatch = buildResultBatch(method.headerSchema, headerValues, serverId, requestId);
|
|
77
78
|
const headerBatches = [...headerOut.batches.map((b) => b.batch), headerBatch];
|
|
78
|
-
writer.writeStream(method.headerSchema, headerBatches);
|
|
79
|
+
await writer.writeStream(method.headerSchema, headerBatches);
|
|
79
80
|
} catch (error: any) {
|
|
80
81
|
const errBatch = buildErrorBatch(method.headerSchema, error, serverId, requestId);
|
|
81
|
-
writer.writeStream(method.headerSchema, [errBatch]);
|
|
82
|
+
await writer.writeStream(method.headerSchema, [errBatch]);
|
|
82
83
|
// Drain input stream so client doesn't hang
|
|
83
84
|
const inputSchema = await reader.openNextStream();
|
|
84
85
|
if (inputSchema) {
|
|
@@ -92,7 +93,7 @@ export async function dispatchStream(
|
|
|
92
93
|
const inputSchema = await reader.openNextStream();
|
|
93
94
|
if (!inputSchema) {
|
|
94
95
|
const errBatch = buildErrorBatch(outputSchema, new Error("Expected input stream but got EOF"), serverId, requestId);
|
|
95
|
-
writer.writeStream(outputSchema, [errBatch]);
|
|
96
|
+
await writer.writeStream(outputSchema, [errBatch]);
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -101,32 +102,54 @@ export async function dispatchStream(
|
|
|
101
102
|
// same stream. We use IncrementalStream which writes bytes synchronously.
|
|
102
103
|
const stream = writer.openStream(outputSchema);
|
|
103
104
|
|
|
104
|
-
// Expected input schema for casting compatible types (e.g., decimal→double)
|
|
105
|
-
|
|
105
|
+
// Expected input schema for casting compatible types (e.g., decimal→double).
|
|
106
|
+
// State.__inputSchema overrides the method-registration schema per call,
|
|
107
|
+
// mirroring the __outputSchema pattern. Used by dynamic-input exchange
|
|
108
|
+
// methods (e.g., VGI's init, which binds to a user-supplied input shape).
|
|
109
|
+
const expectedInputSchema = state?.__inputSchema ?? method.inputSchema;
|
|
106
110
|
|
|
107
111
|
try {
|
|
108
112
|
while (true) {
|
|
109
113
|
let inputBatch = await reader.readNextBatch();
|
|
110
114
|
if (!inputBatch) break;
|
|
111
115
|
|
|
116
|
+
// Client cancellation: if the input batch carries vgi_rpc.cancel metadata,
|
|
117
|
+
// end the stream cleanly without calling the producer/exchange handler.
|
|
118
|
+
// The onCancel hook (if registered) runs once so state objects can
|
|
119
|
+
// release resources.
|
|
120
|
+
if (inputBatch.metadata?.get(CANCEL_KEY)) {
|
|
121
|
+
if (method.onCancel) {
|
|
122
|
+
try {
|
|
123
|
+
await method.onCancel(state);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.debug?.(`onCancel hook failed: ${err instanceof Error ? err.message : err}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
// Cast compatible input types when schema doesn't match exactly.
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
132
|
+
// Gated on effectiveProducer (not isProducer) so methods that flip to
|
|
133
|
+
// producer mode via state.__isProducer skip the conform entirely — the
|
|
134
|
+
// tick batches they receive have a dummy shape that shouldn't be checked.
|
|
135
|
+
// Any conformance failure falls through with the original batch; the
|
|
136
|
+
// exchange handler owns input-shape validation if it cares.
|
|
137
|
+
if (expectedInputSchema && !effectiveProducer && inputBatch.schema !== expectedInputSchema) {
|
|
117
138
|
try {
|
|
118
139
|
inputBatch = conformBatchToSchema(inputBatch, expectedInputSchema);
|
|
119
140
|
} catch (e) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
//
|
|
141
|
+
// Field name/count mismatch (TypeError) is a hard contract violation —
|
|
142
|
+
// propagate as an RpcError so callers see a structured failure instead
|
|
143
|
+
// of a downstream hang or silent garbage. Other conform failures (e.g.
|
|
144
|
+
// type-cast issues for dynamic-input handlers) fall through with the
|
|
145
|
+
// original batch — handlers that bind their input shape per-call
|
|
146
|
+
// should set state.__inputSchema so the conform doesn't run at all.
|
|
147
|
+
if (e instanceof TypeError) throw e;
|
|
125
148
|
console.debug?.(`Schema conformance skipped: ${e instanceof Error ? e.message : e}`);
|
|
126
149
|
}
|
|
127
150
|
}
|
|
128
151
|
|
|
129
|
-
const out = new OutputCollector(outputSchema, effectiveProducer, serverId, requestId);
|
|
152
|
+
const out = new OutputCollector(outputSchema, effectiveProducer, serverId, requestId, undefined, undefined, kind);
|
|
130
153
|
|
|
131
154
|
if (isProducer) {
|
|
132
155
|
await method.producerFn!(state, out);
|
|
@@ -139,7 +162,13 @@ export async function dispatchStream(
|
|
|
139
162
|
if (externalConfig) {
|
|
140
163
|
batch = await maybeExternalizeBatch(batch, externalConfig);
|
|
141
164
|
}
|
|
142
|
-
|
|
165
|
+
// Attach per-emit metadata (e.g. vgi_batch_index,
|
|
166
|
+
// vgi_partition_values#b64) as the RecordBatch message's
|
|
167
|
+
// custom_metadata so the C++ extension can read it off the wire.
|
|
168
|
+
if (emitted.metadata && emitted.metadata.size > 0) {
|
|
169
|
+
batch = withBatchMetadata(batch, emitted.metadata);
|
|
170
|
+
}
|
|
171
|
+
await stream.write(batch);
|
|
143
172
|
}
|
|
144
173
|
|
|
145
174
|
if (out.finished) {
|
|
@@ -147,10 +176,10 @@ export async function dispatchStream(
|
|
|
147
176
|
}
|
|
148
177
|
}
|
|
149
178
|
} catch (error: any) {
|
|
150
|
-
stream.write(buildErrorBatch(outputSchema, error, serverId, requestId));
|
|
179
|
+
await stream.write(buildErrorBatch(outputSchema, error, serverId, requestId));
|
|
151
180
|
}
|
|
152
181
|
|
|
153
|
-
stream.close();
|
|
182
|
+
await stream.close();
|
|
154
183
|
|
|
155
184
|
// Drain remaining input so transport stays synchronized for next request.
|
|
156
185
|
// Matches Python's _drain_stream() called after every streaming method.
|
package/src/dispatch/unary.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import { type ExternalLocationConfig, maybeExternalizeBatch } from "../external.js";
|
|
5
|
-
import type { MethodDefinition } from "../types.js";
|
|
5
|
+
import type { MethodDefinition, TransportKind } from "../types.js";
|
|
6
6
|
import { OutputCollector } from "../types.js";
|
|
7
7
|
import { buildErrorBatch, buildResultBatch } from "../wire/response.js";
|
|
8
8
|
import type { IpcStreamWriter } from "../wire/writer.js";
|
|
@@ -19,9 +19,10 @@ export async function dispatchUnary(
|
|
|
19
19
|
serverId: string,
|
|
20
20
|
requestId: string | null,
|
|
21
21
|
externalConfig?: ExternalLocationConfig,
|
|
22
|
+
kind?: TransportKind,
|
|
22
23
|
): Promise<void> {
|
|
23
24
|
const schema = method.resultSchema;
|
|
24
|
-
const out = new OutputCollector(schema, true, serverId, requestId);
|
|
25
|
+
const out = new OutputCollector(schema, true, serverId, requestId, undefined, undefined, kind);
|
|
25
26
|
|
|
26
27
|
try {
|
|
27
28
|
const result = await method.handler!(params, out);
|
|
@@ -31,9 +32,9 @@ export async function dispatchUnary(
|
|
|
31
32
|
}
|
|
32
33
|
// Collect log batches (from clientLog) + result batch
|
|
33
34
|
const batches = [...out.batches.map((b) => b.batch), resultBatch];
|
|
34
|
-
writer.writeStream(schema, batches);
|
|
35
|
+
await writer.writeStream(schema, batches);
|
|
35
36
|
} catch (error: any) {
|
|
36
37
|
const batch = buildErrorBatch(schema, error, serverId, requestId);
|
|
37
|
-
writer.writeStream(schema, [batch]);
|
|
38
|
+
await writer.writeStream(schema, [batch]);
|
|
38
39
|
}
|
|
39
40
|
}
|