@query-farm/vgi-rpc 0.6.4 → 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 +66 -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 +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 +790 -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 +67 -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
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
|
}
|
package/src/errors.ts
CHANGED
|
@@ -20,3 +20,79 @@ export class VersionError extends Error {
|
|
|
20
20
|
this.name = "VersionError";
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
/** Well-known values for the `vgi_rpc.error_kind` batch metadata key. Mirrors
|
|
25
|
+
* Python's `vgi_rpc.metadata.ERROR_KIND_*` constants. */
|
|
26
|
+
export const ERROR_KIND_METHOD_NOT_IMPLEMENTED = "method_not_implemented";
|
|
27
|
+
export const ERROR_KIND_SESSION_LOST = "session_lost";
|
|
28
|
+
export const ERROR_KIND_SERVER_DRAINING = "server_draining";
|
|
29
|
+
export const ERROR_KIND_PROTOCOL_VERSION_MISMATCH = "protocol_version_mismatch";
|
|
30
|
+
|
|
31
|
+
/** Raised when the client's declared `vgi_rpc.protocol_version` is
|
|
32
|
+
* incompatible with the server's. Subclass of `VersionError` so existing
|
|
33
|
+
* catch sites continue to write a typed error stream and keep serving.
|
|
34
|
+
* Carries a directional message that tells the reader which side to
|
|
35
|
+
* upgrade. Mirrors Python's `vgi_rpc.rpc.ProtocolVersionError`. */
|
|
36
|
+
export class ProtocolVersionError extends VersionError {
|
|
37
|
+
static readonly errorKind = ERROR_KIND_PROTOCOL_VERSION_MISMATCH;
|
|
38
|
+
readonly errorKind = ERROR_KIND_PROTOCOL_VERSION_MISMATCH;
|
|
39
|
+
constructor(message: string) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "ProtocolVersionError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SEMVER_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
46
|
+
|
|
47
|
+
/** Parse a canonical semver string into `[major, minor, patch]`. Throws on
|
|
48
|
+
* any input that isn't `MAJOR.MINOR.PATCH` with non-negative integers and
|
|
49
|
+
* no leading zeros (except literal `0`). No prereleases, no build metadata.
|
|
50
|
+
* Mirrors Python's `vgi_rpc.metadata.parse_version`. */
|
|
51
|
+
export function parseProtocolVersion(value: string): [number, number, number] {
|
|
52
|
+
const m = SEMVER_REGEX.exec(value);
|
|
53
|
+
if (!m) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Invalid protocol version '${value}': expected canonical semver ` +
|
|
56
|
+
"MAJOR.MINOR.PATCH with non-negative integers and no leading zeros " +
|
|
57
|
+
"(no prereleases or build metadata).",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Raised when a client invokes a method the server does not implement.
|
|
64
|
+
*
|
|
65
|
+
* Mirrors Python's `vgi_rpc.rpc.MethodNotImplementedError`. The static
|
|
66
|
+
* `errorKind` is hoisted onto the error batch metadata as
|
|
67
|
+
* `vgi_rpc.error_kind` so clients can branch on the typed marker without
|
|
68
|
+
* string-matching the message.
|
|
69
|
+
*/
|
|
70
|
+
export class MethodNotImplementedError extends Error {
|
|
71
|
+
static readonly errorKind = ERROR_KIND_METHOD_NOT_IMPLEMENTED;
|
|
72
|
+
readonly errorKind = ERROR_KIND_METHOD_NOT_IMPLEMENTED;
|
|
73
|
+
constructor(message: string) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = "MethodNotImplementedError";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Raised when a sticky session token is malformed, expired, evicted, or
|
|
80
|
+
* bound to a different worker / principal. HTTP-only. */
|
|
81
|
+
export class SessionLostError extends Error {
|
|
82
|
+
static readonly errorKind = ERROR_KIND_SESSION_LOST;
|
|
83
|
+
readonly errorKind = ERROR_KIND_SESSION_LOST;
|
|
84
|
+
constructor(message: string) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "SessionLostError";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Raised when `ctx.openSession` is called while the server is draining. */
|
|
91
|
+
export class ServerDrainingError extends Error {
|
|
92
|
+
static readonly errorKind = ERROR_KIND_SERVER_DRAINING;
|
|
93
|
+
readonly errorKind = ERROR_KIND_SERVER_DRAINING;
|
|
94
|
+
constructor(message: string) {
|
|
95
|
+
super(message);
|
|
96
|
+
this.name = "ServerDrainingError";
|
|
97
|
+
}
|
|
98
|
+
}
|