@query-farm/vgi-rpc 0.2.4 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/client/connect.d.ts.map +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/introspect.d.ts +1 -1
- package/dist/client/introspect.d.ts.map +1 -1
- package/dist/client/ipc.d.ts +1 -1
- package/dist/client/ipc.d.ts.map +1 -1
- package/dist/client/pipe.d.ts +2 -2
- package/dist/client/pipe.d.ts.map +1 -1
- package/dist/client/stream.d.ts +1 -1
- package/dist/client/stream.d.ts.map +1 -1
- package/dist/dispatch/describe.d.ts +1 -1
- package/dist/dispatch/describe.d.ts.map +1 -1
- package/dist/dispatch/stream.d.ts +1 -1
- package/dist/dispatch/stream.d.ts.map +1 -1
- package/dist/dispatch/unary.d.ts.map +1 -1
- package/dist/http/common.d.ts +1 -1
- package/dist/http/common.d.ts.map +1 -1
- package/dist/http/dispatch.d.ts.map +1 -1
- package/dist/http/handler.d.ts.map +1 -1
- package/dist/http/index.d.ts +3 -1
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/token.d.ts.map +1 -1
- package/dist/http/types.d.ts.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2162 -2119
- package/dist/index.js.map +26 -26
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/util/schema.d.ts +1 -1
- package/dist/util/schema.d.ts.map +1 -1
- package/dist/util/zstd.d.ts.map +1 -1
- package/dist/wire/reader.d.ts +1 -1
- package/dist/wire/reader.d.ts.map +1 -1
- package/dist/wire/request.d.ts +1 -1
- package/dist/wire/request.d.ts.map +1 -1
- package/dist/wire/response.d.ts +1 -1
- package/dist/wire/response.d.ts.map +1 -1
- package/dist/wire/writer.d.ts +1 -1
- package/dist/wire/writer.d.ts.map +1 -1
- package/package.json +9 -5
- package/src/client/connect.ts +12 -20
- package/src/client/index.ts +8 -8
- package/src/client/introspect.ts +11 -15
- package/src/client/ipc.ts +18 -32
- package/src/client/pipe.ts +17 -37
- package/src/client/stream.ts +20 -46
- package/src/dispatch/describe.ts +15 -27
- package/src/dispatch/stream.ts +7 -21
- package/src/dispatch/unary.ts +1 -2
- package/src/http/common.ts +9 -19
- package/src/http/dispatch.ts +115 -110
- package/src/http/handler.ts +20 -55
- package/src/http/index.ts +3 -1
- package/src/http/token.ts +2 -7
- package/src/http/types.ts +2 -6
- package/src/index.ts +44 -41
- package/src/protocol.ts +8 -8
- package/src/schema.ts +12 -16
- package/src/server.ts +16 -36
- package/src/types.ts +9 -36
- package/src/util/schema.ts +1 -1
- package/src/util/zstd.ts +2 -8
- package/src/wire/reader.ts +2 -4
- package/src/wire/request.ts +4 -15
- package/src/wire/response.ts +9 -25
- package/src/wire/writer.ts +1 -5
package/src/client/stream.ts
CHANGED
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
RecordBatch,
|
|
6
|
-
Schema,
|
|
7
|
-
Field,
|
|
8
|
-
makeData,
|
|
9
|
-
Struct,
|
|
10
|
-
vectorFromArray,
|
|
11
|
-
} from "apache-arrow";
|
|
4
|
+
import { Field, makeData, RecordBatch, Schema, Struct, vectorFromArray } from "@query-farm/apache-arrow";
|
|
12
5
|
import { STATE_KEY } from "../constants.js";
|
|
13
6
|
import { RpcError } from "../errors.js";
|
|
14
7
|
import { ARROW_CONTENT_TYPE, serializeIpcStream } from "../http/common.js";
|
|
15
|
-
import {
|
|
16
|
-
inferArrowType,
|
|
17
|
-
dispatchLogOrError,
|
|
18
|
-
extractBatchRows,
|
|
19
|
-
readResponseBatches,
|
|
20
|
-
} from "./ipc.js";
|
|
8
|
+
import { dispatchLogOrError, extractBatchRows, inferArrowType, readResponseBatches } from "./ipc.js";
|
|
21
9
|
import type { LogMessage, StreamSession } from "./types.js";
|
|
22
10
|
|
|
23
11
|
type CompressFn = (data: Uint8Array, level: number) => Uint8Array;
|
|
@@ -103,11 +91,7 @@ export class HttpStreamSession implements StreamSession {
|
|
|
103
91
|
*/
|
|
104
92
|
async exchange(input: Record<string, any>[]): Promise<Record<string, any>[]> {
|
|
105
93
|
if (this._stateToken === null) {
|
|
106
|
-
throw new RpcError(
|
|
107
|
-
"ProtocolError",
|
|
108
|
-
"Stream has finished \u2014 no state token available",
|
|
109
|
-
"",
|
|
110
|
-
);
|
|
94
|
+
throw new RpcError("ProtocolError", "Stream has finished \u2014 no state token available", "");
|
|
111
95
|
}
|
|
112
96
|
|
|
113
97
|
// We need to determine the input schema from the data.
|
|
@@ -122,11 +106,7 @@ export class HttpStreamSession implements StreamSession {
|
|
|
122
106
|
const emptyBatch = this._buildEmptyBatch(zeroSchema);
|
|
123
107
|
const metadata = new Map<string, string>();
|
|
124
108
|
metadata.set(STATE_KEY, this._stateToken);
|
|
125
|
-
const batchWithMeta = new RecordBatch(
|
|
126
|
-
zeroSchema,
|
|
127
|
-
emptyBatch.data,
|
|
128
|
-
metadata,
|
|
129
|
-
);
|
|
109
|
+
const batchWithMeta = new RecordBatch(zeroSchema, emptyBatch.data, metadata);
|
|
130
110
|
return this._doExchange(zeroSchema, [batchWithMeta]);
|
|
131
111
|
}
|
|
132
112
|
|
|
@@ -134,9 +114,12 @@ export class HttpStreamSession implements StreamSession {
|
|
|
134
114
|
const keys = Object.keys(input[0]);
|
|
135
115
|
const fields = keys.map((key) => {
|
|
136
116
|
// Find first non-null value to infer type
|
|
137
|
-
let sample: any
|
|
117
|
+
let sample: any;
|
|
138
118
|
for (const row of input) {
|
|
139
|
-
if (row[key] != null) {
|
|
119
|
+
if (row[key] != null) {
|
|
120
|
+
sample = row[key];
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
140
123
|
}
|
|
141
124
|
const arrowType = inferArrowType(sample);
|
|
142
125
|
const nullable = input.some((row) => row[key] == null);
|
|
@@ -164,19 +147,13 @@ export class HttpStreamSession implements StreamSession {
|
|
|
164
147
|
return this._doExchange(inputSchema, [batch]);
|
|
165
148
|
}
|
|
166
149
|
|
|
167
|
-
private async _doExchange(
|
|
168
|
-
schema: Schema,
|
|
169
|
-
batches: RecordBatch[],
|
|
170
|
-
): Promise<Record<string, any>[]> {
|
|
150
|
+
private async _doExchange(schema: Schema, batches: RecordBatch[]): Promise<Record<string, any>[]> {
|
|
171
151
|
const body = serializeIpcStream(schema, batches);
|
|
172
|
-
const resp = await fetch(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
body: this._prepareBody(body) as unknown as BodyInit,
|
|
178
|
-
},
|
|
179
|
-
);
|
|
152
|
+
const resp = await fetch(`${this._baseUrl}${this._prefix}/${this._method}/exchange`, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: this._buildHeaders(),
|
|
155
|
+
body: this._prepareBody(body) as unknown as BodyInit,
|
|
156
|
+
});
|
|
180
157
|
|
|
181
158
|
const responseBody = await this._readResponse(resp);
|
|
182
159
|
const { batches: responseBatches } = await readResponseBatches(responseBody);
|
|
@@ -279,14 +256,11 @@ export class HttpStreamSession implements StreamSession {
|
|
|
279
256
|
const batch = new RecordBatch(emptySchema, data, metadata);
|
|
280
257
|
const body = serializeIpcStream(emptySchema, [batch]);
|
|
281
258
|
|
|
282
|
-
const resp = await fetch(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
body: this._prepareBody(body) as unknown as BodyInit,
|
|
288
|
-
},
|
|
289
|
-
);
|
|
259
|
+
const resp = await fetch(`${this._baseUrl}${this._prefix}/${this._method}/exchange`, {
|
|
260
|
+
method: "POST",
|
|
261
|
+
headers: this._buildHeaders(),
|
|
262
|
+
body: this._prepareBody(body) as unknown as BodyInit,
|
|
263
|
+
});
|
|
290
264
|
|
|
291
265
|
return this._readResponse(resp);
|
|
292
266
|
}
|
package/src/dispatch/describe.ts
CHANGED
|
@@ -2,25 +2,25 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
Binary,
|
|
6
|
+
Bool,
|
|
6
7
|
Field,
|
|
8
|
+
makeData,
|
|
7
9
|
RecordBatch,
|
|
10
|
+
Schema,
|
|
11
|
+
Struct,
|
|
8
12
|
Utf8,
|
|
9
|
-
Bool,
|
|
10
|
-
Binary,
|
|
11
13
|
vectorFromArray,
|
|
12
|
-
|
|
13
|
-
Struct,
|
|
14
|
-
} from "apache-arrow";
|
|
15
|
-
import type { MethodDefinition } from "../types.js";
|
|
14
|
+
} from "@query-farm/apache-arrow";
|
|
16
15
|
import {
|
|
16
|
+
DESCRIBE_VERSION,
|
|
17
|
+
DESCRIBE_VERSION_KEY,
|
|
17
18
|
PROTOCOL_NAME_KEY,
|
|
18
|
-
REQUEST_VERSION_KEY,
|
|
19
19
|
REQUEST_VERSION,
|
|
20
|
-
|
|
21
|
-
DESCRIBE_VERSION,
|
|
20
|
+
REQUEST_VERSION_KEY,
|
|
22
21
|
SERVER_ID_KEY,
|
|
23
22
|
} from "../constants.js";
|
|
23
|
+
import type { MethodDefinition } from "../types.js";
|
|
24
24
|
import { serializeSchema } from "../util/schema.js";
|
|
25
25
|
|
|
26
26
|
/**
|
|
@@ -48,9 +48,7 @@ export function buildDescribeBatch(
|
|
|
48
48
|
serverId: string,
|
|
49
49
|
): { batch: RecordBatch; metadata: Map<string, string> } {
|
|
50
50
|
// Sort methods by name for consistent ordering
|
|
51
|
-
const sortedEntries = [...methods.entries()].sort(([a], [b]) =>
|
|
52
|
-
a.localeCompare(b),
|
|
53
|
-
);
|
|
51
|
+
const sortedEntries = [...methods.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
54
52
|
|
|
55
53
|
const names: (string | null)[] = [];
|
|
56
54
|
const methodTypes: (string | null)[] = [];
|
|
@@ -69,8 +67,7 @@ export function buildDescribeBatch(
|
|
|
69
67
|
docs.push(method.doc ?? null);
|
|
70
68
|
|
|
71
69
|
// Unary methods with non-empty result schema have a return value
|
|
72
|
-
const hasReturn =
|
|
73
|
-
method.type === "unary" && method.resultSchema.fields.length > 0;
|
|
70
|
+
const hasReturn = method.type === "unary" && method.resultSchema.fields.length > 0;
|
|
74
71
|
hasReturns.push(hasReturn);
|
|
75
72
|
|
|
76
73
|
paramsSchemas.push(serializeSchema(method.paramsSchema));
|
|
@@ -87,26 +84,17 @@ export function buildDescribeBatch(
|
|
|
87
84
|
if (method.defaults && Object.keys(method.defaults).length > 0) {
|
|
88
85
|
const safe: Record<string, any> = {};
|
|
89
86
|
for (const [k, v] of Object.entries(method.defaults)) {
|
|
90
|
-
if (
|
|
91
|
-
v === null ||
|
|
92
|
-
typeof v === "string" ||
|
|
93
|
-
typeof v === "number" ||
|
|
94
|
-
typeof v === "boolean"
|
|
95
|
-
) {
|
|
87
|
+
if (v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
96
88
|
safe[k] = v;
|
|
97
89
|
}
|
|
98
90
|
}
|
|
99
|
-
paramDefaultsJsons.push(
|
|
100
|
-
Object.keys(safe).length > 0 ? JSON.stringify(safe) : null,
|
|
101
|
-
);
|
|
91
|
+
paramDefaultsJsons.push(Object.keys(safe).length > 0 ? JSON.stringify(safe) : null);
|
|
102
92
|
} else {
|
|
103
93
|
paramDefaultsJsons.push(null);
|
|
104
94
|
}
|
|
105
95
|
|
|
106
96
|
hasHeaders.push(!!method.headerSchema);
|
|
107
|
-
headerSchemas.push(
|
|
108
|
-
method.headerSchema ? serializeSchema(method.headerSchema) : null,
|
|
109
|
-
);
|
|
97
|
+
headerSchemas.push(method.headerSchema ? serializeSchema(method.headerSchema) : null);
|
|
110
98
|
}
|
|
111
99
|
|
|
112
100
|
// Build the batch using vectorFromArray for each column
|
package/src/dispatch/stream.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import { Schema } from "apache-arrow";
|
|
4
|
+
import { Schema } from "@query-farm/apache-arrow";
|
|
5
5
|
import type { MethodDefinition } from "../types.js";
|
|
6
6
|
import { OutputCollector } from "../types.js";
|
|
7
|
-
import type { IpcStreamWriter } from "../wire/writer.js";
|
|
8
7
|
import type { IpcStreamReader } from "../wire/reader.js";
|
|
9
|
-
import {
|
|
8
|
+
import { buildErrorBatch, buildResultBatch } from "../wire/response.js";
|
|
9
|
+
import type { IpcStreamWriter } from "../wire/writer.js";
|
|
10
10
|
|
|
11
11
|
const EMPTY_SCHEMA = new Schema([]);
|
|
12
12
|
|
|
@@ -33,8 +33,7 @@ export async function dispatchStream(
|
|
|
33
33
|
serverId: string,
|
|
34
34
|
requestId: string | null,
|
|
35
35
|
): Promise<void> {
|
|
36
|
-
const isProducer =
|
|
37
|
-
!method.inputSchema || method.inputSchema.fields.length === 0;
|
|
36
|
+
const isProducer = !!method.producerFn;
|
|
38
37
|
|
|
39
38
|
let state: any;
|
|
40
39
|
try {
|
|
@@ -71,16 +70,8 @@ export async function dispatchStream(
|
|
|
71
70
|
try {
|
|
72
71
|
const headerOut = new OutputCollector(method.headerSchema, true, serverId, requestId);
|
|
73
72
|
const headerValues = method.headerInit(params, state, headerOut);
|
|
74
|
-
const headerBatch = buildResultBatch(
|
|
75
|
-
|
|
76
|
-
headerValues,
|
|
77
|
-
serverId,
|
|
78
|
-
requestId,
|
|
79
|
-
);
|
|
80
|
-
const headerBatches = [
|
|
81
|
-
...headerOut.batches.map((b) => b.batch),
|
|
82
|
-
headerBatch,
|
|
83
|
-
];
|
|
73
|
+
const headerBatch = buildResultBatch(method.headerSchema, headerValues, serverId, requestId);
|
|
74
|
+
const headerBatches = [...headerOut.batches.map((b) => b.batch), headerBatch];
|
|
84
75
|
writer.writeStream(method.headerSchema, headerBatches);
|
|
85
76
|
} catch (error: any) {
|
|
86
77
|
const errBatch = buildErrorBatch(method.headerSchema, error, serverId, requestId);
|
|
@@ -97,12 +88,7 @@ export async function dispatchStream(
|
|
|
97
88
|
// Open the input IPC stream (ticks or data from client)
|
|
98
89
|
const inputSchema = await reader.openNextStream();
|
|
99
90
|
if (!inputSchema) {
|
|
100
|
-
const errBatch = buildErrorBatch(
|
|
101
|
-
outputSchema,
|
|
102
|
-
new Error("Expected input stream but got EOF"),
|
|
103
|
-
serverId,
|
|
104
|
-
requestId,
|
|
105
|
-
);
|
|
91
|
+
const errBatch = buildErrorBatch(outputSchema, new Error("Expected input stream but got EOF"), serverId, requestId);
|
|
106
92
|
writer.writeStream(outputSchema, [errBatch]);
|
|
107
93
|
return;
|
|
108
94
|
}
|
package/src/dispatch/unary.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import { Schema } from "apache-arrow";
|
|
5
4
|
import type { MethodDefinition } from "../types.js";
|
|
6
5
|
import { OutputCollector } from "../types.js";
|
|
6
|
+
import { buildErrorBatch, buildResultBatch } from "../wire/response.js";
|
|
7
7
|
import type { IpcStreamWriter } from "../wire/writer.js";
|
|
8
|
-
import { buildResultBatch, buildErrorBatch } from "../wire/response.js";
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* Dispatch a unary RPC call.
|
package/src/http/common.ts
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
RecordBatchReader,
|
|
5
|
+
makeData,
|
|
7
6
|
RecordBatch,
|
|
8
|
-
|
|
7
|
+
RecordBatchReader,
|
|
8
|
+
RecordBatchStreamWriter,
|
|
9
|
+
type Schema,
|
|
9
10
|
Struct,
|
|
10
|
-
|
|
11
|
-
} from "apache-arrow";
|
|
11
|
+
} from "@query-farm/apache-arrow";
|
|
12
12
|
|
|
13
13
|
export const ARROW_CONTENT_TYPE = "application/vnd.apache.arrow.stream";
|
|
14
14
|
|
|
@@ -31,14 +31,9 @@ export class HttpRpcError extends Error {
|
|
|
31
31
|
* match the writer's schema. Cloning each child Data with the schema's field
|
|
32
32
|
* type fixes the type metadata while preserving the underlying buffers.
|
|
33
33
|
*/
|
|
34
|
-
function conformBatchToSchema(
|
|
35
|
-
batch: RecordBatch,
|
|
36
|
-
schema: Schema,
|
|
37
|
-
): RecordBatch {
|
|
34
|
+
function conformBatchToSchema(batch: RecordBatch, schema: Schema): RecordBatch {
|
|
38
35
|
if (batch.numRows === 0) return batch;
|
|
39
|
-
const children = schema.fields.map((f, i) =>
|
|
40
|
-
batch.data.children[i].clone(f.type),
|
|
41
|
-
);
|
|
36
|
+
const children = schema.fields.map((f, i) => batch.data.children[i].clone(f.type));
|
|
42
37
|
const structType = new Struct(schema.fields);
|
|
43
38
|
const data = makeData({
|
|
44
39
|
type: structType,
|
|
@@ -51,10 +46,7 @@ function conformBatchToSchema(
|
|
|
51
46
|
}
|
|
52
47
|
|
|
53
48
|
/** Serialize a schema + batches into a complete IPC stream as Uint8Array. */
|
|
54
|
-
export function serializeIpcStream(
|
|
55
|
-
schema: Schema,
|
|
56
|
-
batches: RecordBatch[],
|
|
57
|
-
): Uint8Array {
|
|
49
|
+
export function serializeIpcStream(schema: Schema, batches: RecordBatch[]): Uint8Array {
|
|
58
50
|
const writer = new RecordBatchStreamWriter();
|
|
59
51
|
writer.reset(undefined, schema);
|
|
60
52
|
for (const batch of batches) {
|
|
@@ -72,9 +64,7 @@ export function arrowResponse(body: Uint8Array, status = 200, extraHeaders?: Hea
|
|
|
72
64
|
}
|
|
73
65
|
|
|
74
66
|
/** Read schema + first batch from an IPC stream body. */
|
|
75
|
-
export async function readRequestFromBody(
|
|
76
|
-
body: Uint8Array,
|
|
77
|
-
): Promise<{ schema: Schema; batch: RecordBatch }> {
|
|
67
|
+
export async function readRequestFromBody(body: Uint8Array): Promise<{ schema: Schema; batch: RecordBatch }> {
|
|
78
68
|
const reader = await RecordBatchReader.from(body);
|
|
79
69
|
await reader.open();
|
|
80
70
|
const schema = reader.schema;
|
package/src/http/dispatch.ts
CHANGED
|
@@ -1,27 +1,24 @@
|
|
|
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 { RecordBatch, RecordBatchReader, Schema } from "@query-farm/apache-arrow";
|
|
5
|
+
import { STATE_KEY } from "../constants.js";
|
|
6
|
+
import { buildDescribeBatch, DESCRIBE_SCHEMA } from "../dispatch/describe.js";
|
|
5
7
|
import type { MethodDefinition } from "../types.js";
|
|
6
8
|
import { OutputCollector } from "../types.js";
|
|
7
|
-
import { parseRequest } from "../wire/request.js";
|
|
8
|
-
import {
|
|
9
|
-
buildResultBatch,
|
|
10
|
-
buildErrorBatch,
|
|
11
|
-
buildEmptyBatch,
|
|
12
|
-
} from "../wire/response.js";
|
|
13
|
-
import { buildDescribeBatch, DESCRIBE_SCHEMA } from "../dispatch/describe.js";
|
|
14
|
-
import { STATE_KEY } from "../constants.js";
|
|
15
9
|
import { serializeSchema } from "../util/schema.js";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
readRequestFromBody,
|
|
20
|
-
arrowResponse,
|
|
21
|
-
} from "./common.js";
|
|
10
|
+
import { parseRequest } from "../wire/request.js";
|
|
11
|
+
import { buildEmptyBatch, buildErrorBatch, buildResultBatch } from "../wire/response.js";
|
|
12
|
+
import { arrowResponse, HttpRpcError, readRequestFromBody, serializeIpcStream } from "./common.js";
|
|
22
13
|
import { packStateToken, unpackStateToken } from "./token.js";
|
|
23
14
|
import type { StateSerializer } from "./types.js";
|
|
24
15
|
|
|
16
|
+
async function deserializeSchema(bytes: Uint8Array): Promise<Schema> {
|
|
17
|
+
const reader = await RecordBatchReader.from(bytes);
|
|
18
|
+
await reader.open();
|
|
19
|
+
return reader.schema!;
|
|
20
|
+
}
|
|
21
|
+
|
|
25
22
|
const EMPTY_SCHEMA = new Schema([]);
|
|
26
23
|
|
|
27
24
|
export interface DispatchContext {
|
|
@@ -54,10 +51,7 @@ export async function httpDispatchUnary(
|
|
|
54
51
|
const parsed = parseRequest(reqSchema, reqBatch);
|
|
55
52
|
|
|
56
53
|
if (parsed.methodName !== method.name) {
|
|
57
|
-
throw new HttpRpcError(
|
|
58
|
-
`Method name in request '${parsed.methodName}' does not match URL '${method.name}'`,
|
|
59
|
-
400,
|
|
60
|
-
);
|
|
54
|
+
throw new HttpRpcError(`Method name in request '${parsed.methodName}' does not match URL '${method.name}'`, 400);
|
|
61
55
|
}
|
|
62
56
|
|
|
63
57
|
const out = new OutputCollector(schema, true, ctx.serverId, parsed.requestId);
|
|
@@ -79,8 +73,7 @@ export async function httpDispatchStreamInit(
|
|
|
79
73
|
body: Uint8Array,
|
|
80
74
|
ctx: DispatchContext,
|
|
81
75
|
): Promise<Response> {
|
|
82
|
-
const isProducer =
|
|
83
|
-
!method.inputSchema || method.inputSchema.fields.length === 0;
|
|
76
|
+
const isProducer = !!method.producerFn;
|
|
84
77
|
const outputSchema = method.outputSchema!;
|
|
85
78
|
const inputSchema = method.inputSchema ?? EMPTY_SCHEMA;
|
|
86
79
|
|
|
@@ -88,10 +81,7 @@ export async function httpDispatchStreamInit(
|
|
|
88
81
|
const parsed = parseRequest(reqSchema, reqBatch);
|
|
89
82
|
|
|
90
83
|
if (parsed.methodName !== method.name) {
|
|
91
|
-
throw new HttpRpcError(
|
|
92
|
-
`Method name in request '${parsed.methodName}' does not match URL '${method.name}'`,
|
|
93
|
-
400,
|
|
94
|
-
);
|
|
84
|
+
throw new HttpRpcError(`Method name in request '${parsed.methodName}' does not match URL '${method.name}'`, 400);
|
|
95
85
|
}
|
|
96
86
|
|
|
97
87
|
// Init state
|
|
@@ -116,56 +106,28 @@ export async function httpDispatchStreamInit(
|
|
|
116
106
|
let headerBytes: Uint8Array | null = null;
|
|
117
107
|
if (method.headerSchema && method.headerInit) {
|
|
118
108
|
try {
|
|
119
|
-
const headerOut = new OutputCollector(
|
|
120
|
-
method.headerSchema,
|
|
121
|
-
true,
|
|
122
|
-
ctx.serverId,
|
|
123
|
-
parsed.requestId,
|
|
124
|
-
);
|
|
109
|
+
const headerOut = new OutputCollector(method.headerSchema, true, ctx.serverId, parsed.requestId);
|
|
125
110
|
const headerValues = method.headerInit(parsed.params, state, headerOut);
|
|
126
|
-
const headerBatch = buildResultBatch(
|
|
127
|
-
|
|
128
|
-
headerValues,
|
|
129
|
-
ctx.serverId,
|
|
130
|
-
parsed.requestId,
|
|
131
|
-
);
|
|
132
|
-
const headerBatches = [
|
|
133
|
-
...headerOut.batches.map((b) => b.batch),
|
|
134
|
-
headerBatch,
|
|
135
|
-
];
|
|
111
|
+
const headerBatch = buildResultBatch(method.headerSchema, headerValues, ctx.serverId, parsed.requestId);
|
|
112
|
+
const headerBatches = [...headerOut.batches.map((b) => b.batch), headerBatch];
|
|
136
113
|
headerBytes = serializeIpcStream(method.headerSchema, headerBatches);
|
|
137
114
|
} catch (error: any) {
|
|
138
|
-
const errBatch = buildErrorBatch(
|
|
139
|
-
method.headerSchema,
|
|
140
|
-
error,
|
|
141
|
-
ctx.serverId,
|
|
142
|
-
parsed.requestId,
|
|
143
|
-
);
|
|
115
|
+
const errBatch = buildErrorBatch(method.headerSchema, error, ctx.serverId, parsed.requestId);
|
|
144
116
|
return arrowResponse(serializeIpcStream(method.headerSchema, [errBatch]), 500);
|
|
145
117
|
}
|
|
146
118
|
}
|
|
147
119
|
|
|
148
120
|
if (effectiveProducer) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
inputSchema,
|
|
154
|
-
ctx,
|
|
155
|
-
parsed.requestId,
|
|
156
|
-
headerBytes,
|
|
157
|
-
);
|
|
121
|
+
// Producer method — produce data inline in the init response.
|
|
122
|
+
// For exchange-registered methods acting as producers (__isProducer),
|
|
123
|
+
// produceStreamResponse falls back to exchangeFn with tick batches.
|
|
124
|
+
return produceStreamResponse(method, state, resolvedOutputSchema, inputSchema, ctx, parsed.requestId, headerBytes);
|
|
158
125
|
} else {
|
|
159
126
|
// Exchange: serialize state into signed token, return zero-row batch with token
|
|
160
127
|
const stateBytes = ctx.stateSerializer.serialize(state);
|
|
161
128
|
const schemaBytes = serializeSchema(resolvedOutputSchema);
|
|
162
129
|
const inputSchemaBytes = serializeSchema(inputSchema);
|
|
163
|
-
const token = packStateToken(
|
|
164
|
-
stateBytes,
|
|
165
|
-
schemaBytes,
|
|
166
|
-
inputSchemaBytes,
|
|
167
|
-
ctx.signingKey,
|
|
168
|
-
);
|
|
130
|
+
const token = packStateToken(stateBytes, schemaBytes, inputSchemaBytes, ctx.signingKey);
|
|
169
131
|
|
|
170
132
|
const tokenMeta = new Map<string, string>();
|
|
171
133
|
tokenMeta.set(STATE_KEY, token);
|
|
@@ -189,8 +151,7 @@ export async function httpDispatchStreamExchange(
|
|
|
189
151
|
body: Uint8Array,
|
|
190
152
|
ctx: DispatchContext,
|
|
191
153
|
): Promise<Response> {
|
|
192
|
-
const isProducer =
|
|
193
|
-
!method.inputSchema || method.inputSchema.fields.length === 0;
|
|
154
|
+
const isProducer = !!method.producerFn;
|
|
194
155
|
|
|
195
156
|
const { batch: reqBatch } = await readRequestFromBody(body);
|
|
196
157
|
|
|
@@ -200,64 +161,102 @@ export async function httpDispatchStreamExchange(
|
|
|
200
161
|
throw new HttpRpcError("Missing state token in exchange request", 400);
|
|
201
162
|
}
|
|
202
163
|
|
|
203
|
-
let unpacked;
|
|
164
|
+
let unpacked: import("./token.js").UnpackedToken;
|
|
204
165
|
try {
|
|
205
166
|
unpacked = unpackStateToken(tokenBase64, ctx.signingKey, ctx.tokenTtl);
|
|
206
167
|
} catch (error: any) {
|
|
207
168
|
throw new HttpRpcError(`Invalid state token: ${error.message}`, 400);
|
|
208
169
|
}
|
|
209
170
|
|
|
210
|
-
|
|
171
|
+
let state: any;
|
|
172
|
+
try {
|
|
173
|
+
state = ctx.stateSerializer.deserialize(unpacked.stateBytes);
|
|
174
|
+
} catch (error: any) {
|
|
175
|
+
console.error(`[httpDispatchStreamExchange] state deserialize error:`, error.message);
|
|
176
|
+
throw new HttpRpcError(`State deserialization failed: ${error.message}`, 500);
|
|
177
|
+
}
|
|
211
178
|
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
179
|
+
// Recover schemas from the token (the state itself may not contain
|
|
180
|
+
// Schema objects after JSON round-trip — always prefer the token).
|
|
181
|
+
let outputSchema: Schema;
|
|
182
|
+
if (unpacked.schemaBytes.length > 0) {
|
|
183
|
+
outputSchema = await deserializeSchema(unpacked.schemaBytes);
|
|
184
|
+
} else {
|
|
185
|
+
outputSchema = state?.__outputSchema ?? method.outputSchema!;
|
|
186
|
+
}
|
|
187
|
+
let inputSchema: Schema;
|
|
188
|
+
if (unpacked.inputSchemaBytes.length > 0) {
|
|
189
|
+
inputSchema = await deserializeSchema(unpacked.inputSchemaBytes);
|
|
190
|
+
} else {
|
|
191
|
+
inputSchema = method.inputSchema ?? EMPTY_SCHEMA;
|
|
192
|
+
}
|
|
215
193
|
const effectiveProducer = state?.__isProducer ?? isProducer;
|
|
194
|
+
if (process.env.VGI_DISPATCH_DEBUG)
|
|
195
|
+
console.error(
|
|
196
|
+
`[httpDispatchStreamExchange] method=${method.name} effectiveProducer=${effectiveProducer} stateKeys=${Object.keys(state || {})}`,
|
|
197
|
+
);
|
|
216
198
|
|
|
217
199
|
if (effectiveProducer) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
outputSchema,
|
|
222
|
-
inputSchema,
|
|
223
|
-
ctx,
|
|
224
|
-
null,
|
|
225
|
-
null,
|
|
226
|
-
);
|
|
200
|
+
// Producer continuation — produce more data inline.
|
|
201
|
+
// For exchange-registered methods, falls back to exchangeFn with tick batches.
|
|
202
|
+
return produceStreamResponse(method, state, outputSchema, inputSchema, ctx, null, null);
|
|
227
203
|
} else {
|
|
228
|
-
|
|
204
|
+
// Exchange path — also handles exchange-registered methods acting as
|
|
205
|
+
// producers (__isProducer=true). Use producer mode on the OutputCollector
|
|
206
|
+
// when effectiveProducer so finish() is allowed.
|
|
207
|
+
const out = new OutputCollector(outputSchema, effectiveProducer, ctx.serverId, null);
|
|
229
208
|
|
|
230
209
|
try {
|
|
231
|
-
|
|
210
|
+
if (method.exchangeFn) {
|
|
211
|
+
await method.exchangeFn(state, reqBatch, out);
|
|
212
|
+
} else {
|
|
213
|
+
await method.producerFn!(state, out);
|
|
214
|
+
}
|
|
232
215
|
} catch (error: any) {
|
|
216
|
+
if (process.env.VGI_DISPATCH_DEBUG)
|
|
217
|
+
console.error(
|
|
218
|
+
`[httpDispatchStreamExchange] exchange handler error:`,
|
|
219
|
+
error.message,
|
|
220
|
+
error.stack?.split("\n").slice(0, 5).join("\n"),
|
|
221
|
+
);
|
|
233
222
|
const errBatch = buildErrorBatch(outputSchema, error, ctx.serverId, null);
|
|
234
223
|
return arrowResponse(serializeIpcStream(outputSchema, [errBatch]), 500);
|
|
235
224
|
}
|
|
236
225
|
|
|
237
|
-
//
|
|
238
|
-
const stateBytes = ctx.stateSerializer.serialize(state);
|
|
239
|
-
const schemaBytes = serializeSchema(outputSchema);
|
|
240
|
-
const inputSchemaBytes = serializeSchema(inputSchema);
|
|
241
|
-
const token = packStateToken(
|
|
242
|
-
stateBytes,
|
|
243
|
-
schemaBytes,
|
|
244
|
-
inputSchemaBytes,
|
|
245
|
-
ctx.signingKey,
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
// Merge token into the data batch's metadata (matching Python behavior).
|
|
249
|
-
// The Python client expects the token on the data batch itself, not a
|
|
250
|
-
// separate zero-row batch.
|
|
226
|
+
// Collect emitted batches
|
|
251
227
|
const batches: RecordBatch[] = [];
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
228
|
+
|
|
229
|
+
if (out.finished) {
|
|
230
|
+
// Stream is done — return data WITHOUT state token.
|
|
231
|
+
// The absence of a token tells the client there's no more data.
|
|
232
|
+
for (const emitted of out.batches) {
|
|
233
|
+
batches.push(emitted.batch);
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
// More data may follow — repack state into token for next exchange.
|
|
237
|
+
const stateBytes = ctx.stateSerializer.serialize(state);
|
|
238
|
+
const schemaBytes = serializeSchema(outputSchema);
|
|
239
|
+
const inputSchemaBytes = serializeSchema(inputSchema);
|
|
240
|
+
const token = packStateToken(stateBytes, schemaBytes, inputSchemaBytes, ctx.signingKey);
|
|
241
|
+
|
|
242
|
+
for (const emitted of out.batches) {
|
|
243
|
+
const batch = emitted.batch;
|
|
244
|
+
if (batch.numRows > 0) {
|
|
245
|
+
const mergedMeta = new Map<string, string>(batch.metadata ?? []);
|
|
246
|
+
mergedMeta.set(STATE_KEY, token);
|
|
247
|
+
batches.push(new RecordBatch(batch.schema, batch.data, mergedMeta));
|
|
248
|
+
} else {
|
|
249
|
+
batches.push(batch);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Safety net: if no batch carries a state token (e.g. all rows were
|
|
254
|
+
// filtered out by pushdown filters), emit an empty batch with the
|
|
255
|
+
// token so the client knows to continue exchanging.
|
|
256
|
+
if (!batches.some((b) => b.metadata?.get(STATE_KEY))) {
|
|
257
|
+
const tokenMeta = new Map<string, string>();
|
|
258
|
+
tokenMeta.set(STATE_KEY, token);
|
|
259
|
+
batches.push(buildEmptyBatch(outputSchema, tokenMeta));
|
|
261
260
|
}
|
|
262
261
|
}
|
|
263
262
|
|
|
@@ -283,8 +282,19 @@ async function produceStreamResponse(
|
|
|
283
282
|
const out = new OutputCollector(outputSchema, true, ctx.serverId, requestId);
|
|
284
283
|
|
|
285
284
|
try {
|
|
286
|
-
|
|
285
|
+
if (method.producerFn) {
|
|
286
|
+
await method.producerFn(state, out);
|
|
287
|
+
} else {
|
|
288
|
+
// Exchange-registered method acting as producer (e.g. VGI's "init"
|
|
289
|
+
// method which is registered as exchange but may produce based on
|
|
290
|
+
// the __isProducer state flag). Call exchangeFn with an empty tick
|
|
291
|
+
// batch, matching how the subprocess transport dispatches these.
|
|
292
|
+
const tickBatch = buildEmptyBatch(inputSchema);
|
|
293
|
+
await method.exchangeFn!(state, tickBatch, out);
|
|
294
|
+
}
|
|
287
295
|
} catch (error: any) {
|
|
296
|
+
if (process.env.VGI_DISPATCH_DEBUG)
|
|
297
|
+
console.error(`[produceStreamResponse] error:`, error.message, error.stack?.split("\n").slice(0, 3).join("\n"));
|
|
288
298
|
allBatches.push(buildErrorBatch(outputSchema, error, ctx.serverId, requestId));
|
|
289
299
|
break;
|
|
290
300
|
}
|
|
@@ -305,12 +315,7 @@ async function produceStreamResponse(
|
|
|
305
315
|
const stateBytes = ctx.stateSerializer.serialize(state);
|
|
306
316
|
const schemaBytes = serializeSchema(outputSchema);
|
|
307
317
|
const inputSchemaBytes = serializeSchema(inputSchema);
|
|
308
|
-
const token = packStateToken(
|
|
309
|
-
stateBytes,
|
|
310
|
-
schemaBytes,
|
|
311
|
-
inputSchemaBytes,
|
|
312
|
-
ctx.signingKey,
|
|
313
|
-
);
|
|
318
|
+
const token = packStateToken(stateBytes, schemaBytes, inputSchemaBytes, ctx.signingKey);
|
|
314
319
|
const tokenMeta = new Map<string, string>();
|
|
315
320
|
tokenMeta.set(STATE_KEY, token);
|
|
316
321
|
allBatches.push(buildEmptyBatch(outputSchema, tokenMeta));
|