@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/external.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* download URL and SHA-256 checksum in metadata.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { deserializeBatch, serializeBatch, type VgiBatch, type VgiSchema } from "./arrow/index.js";
|
|
14
14
|
import { LOCATION_KEY, LOCATION_SHA256_KEY, LOG_LEVEL_KEY } from "./constants.js";
|
|
15
15
|
import { zstdCompress, zstdDecompress } from "./util/zstd.js";
|
|
16
16
|
import { buildEmptyBatch } from "./wire/response.js";
|
|
@@ -25,6 +25,28 @@ export interface ExternalStorage {
|
|
|
25
25
|
upload(data: Uint8Array, contentEncoding: string): Promise<string>;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** A pre-signed PUT/GET URL pair for client-side data upload. */
|
|
29
|
+
export interface UploadUrl {
|
|
30
|
+
/** Pre-signed PUT URL the client uploads to. */
|
|
31
|
+
uploadUrl: string;
|
|
32
|
+
/** Pre-signed GET URL the server fetches from. */
|
|
33
|
+
downloadUrl: string;
|
|
34
|
+
/** Expiration time (UTC) for the URL pair. */
|
|
35
|
+
expiresAt: Date;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generates pre-signed upload URL pairs for client-vended externalization.
|
|
40
|
+
*
|
|
41
|
+
* Implementations must be safe to call from multiple concurrent requests.
|
|
42
|
+
* Object lifecycle is the operator's responsibility — uploaded objects are
|
|
43
|
+
* not automatically deleted by vgi-rpc.
|
|
44
|
+
*/
|
|
45
|
+
export interface UploadUrlProvider {
|
|
46
|
+
/** Allocate one upload/download URL pair. */
|
|
47
|
+
generateUploadUrl(): Promise<UploadUrl> | UploadUrl;
|
|
48
|
+
}
|
|
49
|
+
|
|
28
50
|
/** Configuration for external storage of large batches. */
|
|
29
51
|
export interface ExternalLocationConfig {
|
|
30
52
|
/** Storage backend for uploading. */
|
|
@@ -70,7 +92,7 @@ async function sha256Hex(data: Uint8Array): Promise<string> {
|
|
|
70
92
|
// ---------------------------------------------------------------------------
|
|
71
93
|
|
|
72
94
|
/** Returns true if the batch is a zero-row pointer to external data. */
|
|
73
|
-
export function isExternalLocationBatch(batch:
|
|
95
|
+
export function isExternalLocationBatch(batch: VgiBatch): boolean {
|
|
74
96
|
if (batch.numRows !== 0) return false;
|
|
75
97
|
const meta = batch.metadata;
|
|
76
98
|
if (!meta) return false;
|
|
@@ -82,7 +104,7 @@ export function isExternalLocationBatch(batch: RecordBatch): boolean {
|
|
|
82
104
|
// ---------------------------------------------------------------------------
|
|
83
105
|
|
|
84
106
|
/** Create a zero-row pointer batch with location URL and optional SHA-256. */
|
|
85
|
-
export function makeExternalLocationBatch(schema:
|
|
107
|
+
export function makeExternalLocationBatch(schema: VgiSchema, url: string, sha256?: string): VgiBatch {
|
|
86
108
|
const metadata = new Map<string, string>();
|
|
87
109
|
metadata.set(LOCATION_KEY, url);
|
|
88
110
|
if (sha256) {
|
|
@@ -95,22 +117,13 @@ export function makeExternalLocationBatch(schema: Schema, url: string, sha256?:
|
|
|
95
117
|
// IPC serialization helpers
|
|
96
118
|
// ---------------------------------------------------------------------------
|
|
97
119
|
|
|
98
|
-
function serializeBatchToIpc(batch:
|
|
99
|
-
|
|
100
|
-
writer.reset(undefined, batch.schema);
|
|
101
|
-
writer.write(batch);
|
|
102
|
-
writer.close();
|
|
103
|
-
return writer.toUint8Array(true);
|
|
120
|
+
function serializeBatchToIpc(batch: VgiBatch): Uint8Array {
|
|
121
|
+
return serializeBatch(batch);
|
|
104
122
|
}
|
|
105
123
|
|
|
106
|
-
function batchByteSize(batch:
|
|
107
|
-
// Arrow TS data.byteLength doesn't reflect actual data size.
|
|
124
|
+
function batchByteSize(batch: VgiBatch): number {
|
|
108
125
|
// Estimate from IPC serialization size for threshold check.
|
|
109
|
-
|
|
110
|
-
writer.reset(undefined, batch.schema);
|
|
111
|
-
writer.write(batch);
|
|
112
|
-
writer.close();
|
|
113
|
-
return writer.toUint8Array(true).byteLength;
|
|
126
|
+
return serializeBatch(batch).byteLength;
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
// ---------------------------------------------------------------------------
|
|
@@ -122,9 +135,9 @@ function batchByteSize(batch: RecordBatch): number {
|
|
|
122
135
|
* Returns the original batch unchanged if below threshold or no config.
|
|
123
136
|
*/
|
|
124
137
|
export async function maybeExternalizeBatch(
|
|
125
|
-
batch:
|
|
138
|
+
batch: VgiBatch,
|
|
126
139
|
config?: ExternalLocationConfig | null,
|
|
127
|
-
): Promise<
|
|
140
|
+
): Promise<VgiBatch> {
|
|
128
141
|
if (!config?.storage) return batch;
|
|
129
142
|
if (batch.numRows === 0) return batch;
|
|
130
143
|
|
|
@@ -140,7 +153,7 @@ export async function maybeExternalizeBatch(
|
|
|
140
153
|
// Optionally compress
|
|
141
154
|
let contentEncoding = "";
|
|
142
155
|
if (config.compression?.algorithm === "zstd") {
|
|
143
|
-
ipcData = zstdCompress(ipcData, config.compression.level ?? 3) as Uint8Array;
|
|
156
|
+
ipcData = (await zstdCompress(ipcData, config.compression.level ?? 3)) as Uint8Array;
|
|
144
157
|
contentEncoding = "zstd";
|
|
145
158
|
}
|
|
146
159
|
|
|
@@ -160,9 +173,9 @@ export async function maybeExternalizeBatch(
|
|
|
160
173
|
* Returns the original batch unchanged if not a pointer or no config.
|
|
161
174
|
*/
|
|
162
175
|
export async function resolveExternalLocation(
|
|
163
|
-
batch:
|
|
176
|
+
batch: VgiBatch,
|
|
164
177
|
config?: ExternalLocationConfig | null,
|
|
165
|
-
): Promise<
|
|
178
|
+
): Promise<VgiBatch> {
|
|
166
179
|
if (!config) return batch;
|
|
167
180
|
if (!isExternalLocationBatch(batch)) return batch;
|
|
168
181
|
|
|
@@ -182,10 +195,14 @@ export async function resolveExternalLocation(
|
|
|
182
195
|
}
|
|
183
196
|
let data = new Uint8Array(await response.arrayBuffer());
|
|
184
197
|
|
|
185
|
-
// Decompress if needed
|
|
198
|
+
// Decompress if needed. Cap the decompressed size at 16x the
|
|
199
|
+
// compressed body — generous for typical Arrow IPC zstd ratios but
|
|
200
|
+
// tight enough that a tiny response cannot inflate to multi-GB.
|
|
201
|
+
// Mirrors Python's external_fetch.fetch_url.
|
|
186
202
|
const contentEncoding = response.headers.get("Content-Encoding");
|
|
187
203
|
if (contentEncoding === "zstd") {
|
|
188
|
-
|
|
204
|
+
const cap = data.byteLength * 16;
|
|
205
|
+
data = new Uint8Array(await zstdDecompress(data, cap));
|
|
189
206
|
}
|
|
190
207
|
|
|
191
208
|
// Verify SHA-256 if present
|
|
@@ -198,12 +215,9 @@ export async function resolveExternalLocation(
|
|
|
198
215
|
}
|
|
199
216
|
|
|
200
217
|
// Parse IPC stream
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
const resolved = reader.next();
|
|
204
|
-
if (!resolved || resolved.done || !resolved.value) {
|
|
218
|
+
const resolved = deserializeBatch(data);
|
|
219
|
+
if (resolved.numRows === 0 && resolved.schema.fields.length === 0) {
|
|
205
220
|
throw new Error(`No data batch found in external IPC stream from ${url}`);
|
|
206
221
|
}
|
|
207
|
-
|
|
208
|
-
return resolved.value;
|
|
222
|
+
return resolved;
|
|
209
223
|
}
|
package/src/http/bearer.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import { timingSafeEqual } from "node:crypto";
|
|
5
4
|
import type { AuthContext } from "../auth.js";
|
|
5
|
+
import { constantTimeEqual } from "../util/web-crypto.js";
|
|
6
6
|
import type { AuthenticateFn } from "./auth.js";
|
|
7
7
|
|
|
8
8
|
/** Receives the raw bearer token string, returns an AuthContext on success. Must throw on failure. */
|
|
@@ -30,10 +30,7 @@ export function bearerAuthenticate(options: { validate: BearerValidateFn }): Aut
|
|
|
30
30
|
/** Constant-time string comparison to prevent timing attacks on token lookup. */
|
|
31
31
|
function safeEqual(a: string, b: string): boolean {
|
|
32
32
|
const enc = new TextEncoder();
|
|
33
|
-
|
|
34
|
-
const bufB = enc.encode(b);
|
|
35
|
-
if (bufA.byteLength !== bufB.byteLength) return false;
|
|
36
|
-
return timingSafeEqual(bufA, bufB);
|
|
33
|
+
return constantTimeEqual(enc.encode(a), enc.encode(b));
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
/**
|
package/src/http/common.ts
CHANGED
|
@@ -1,11 +1,66 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
conformBatchToSchema,
|
|
6
|
+
deserializeBatch,
|
|
7
|
+
serializeBatches,
|
|
8
|
+
type VgiBatch,
|
|
9
|
+
type VgiSchema,
|
|
10
|
+
} from "../arrow/index.js";
|
|
11
|
+
import { RPC_ERROR_HEADER } from "../constants.js";
|
|
12
|
+
import type { CookieSpec } from "../types.js";
|
|
6
13
|
|
|
7
14
|
export const ARROW_CONTENT_TYPE = "application/vnd.apache.arrow.stream";
|
|
8
15
|
|
|
16
|
+
// Sticky session header conventions (HTTP-only). Mirrors Python's
|
|
17
|
+
// `vgi_rpc.http._common`. Headers — not cookies — so multiple concurrent
|
|
18
|
+
// sessions to one host from a single client multiplex correctly.
|
|
19
|
+
export const SESSION_HEADER = "VGI-Session";
|
|
20
|
+
export const SESSION_ACCEPT_HEADER = "VGI-Session-Accept";
|
|
21
|
+
export const SESSION_CLOSE_HEADER = "VGI-Session-Close";
|
|
22
|
+
export const STICKY_ENABLED_HEADER = "VGI-Sticky-Enabled";
|
|
23
|
+
export const STICKY_DEFAULT_TTL_HEADER = "VGI-Sticky-Default-TTL";
|
|
24
|
+
export const STICKY_ECHO_HEADERS_HEADER = "VGI-Sticky-Echo-Headers";
|
|
25
|
+
|
|
26
|
+
/** Prefix the server uses to tell the client "echo this header on subsequent
|
|
27
|
+
* requests in this session". Clients capture and replay
|
|
28
|
+
* `VGI-Echo-<name>: <value>` as plain `<name>: <value>` for the session
|
|
29
|
+
* lifetime — used for client-driven routing (e.g. `fly-force-instance-id`). */
|
|
30
|
+
export const ECHO_HEADER_PREFIX = "VGI-Echo-";
|
|
31
|
+
|
|
32
|
+
/** Framework-managed sticky session teardown endpoint path component.
|
|
33
|
+
* `DELETE {prefix}/__session__` idempotently closes the session referenced
|
|
34
|
+
* by the request's `VGI-Session` header. */
|
|
35
|
+
export const SESSION_ENDPOINT = "__session__";
|
|
36
|
+
|
|
37
|
+
/** Serialize a CookieSpec into a Set-Cookie header value. */
|
|
38
|
+
export function formatSetCookieHeader(c: CookieSpec): string {
|
|
39
|
+
const parts: string[] = [];
|
|
40
|
+
if (c.delete) {
|
|
41
|
+
parts.push(`${c.name}=`);
|
|
42
|
+
parts.push("Max-Age=0");
|
|
43
|
+
} else {
|
|
44
|
+
parts.push(`${c.name}=${c.value}`);
|
|
45
|
+
if (c.maxAge !== undefined) parts.push(`Max-Age=${c.maxAge}`);
|
|
46
|
+
if (c.expires) parts.push(`Expires=${c.expires.toUTCString()}`);
|
|
47
|
+
}
|
|
48
|
+
if (c.path) parts.push(`Path=${c.path}`);
|
|
49
|
+
if (c.domain) parts.push(`Domain=${c.domain}`);
|
|
50
|
+
if (c.secure) parts.push("Secure");
|
|
51
|
+
if (c.httpOnly) parts.push("HttpOnly");
|
|
52
|
+
if (c.sameSite) parts.push(`SameSite=${c.sameSite}`);
|
|
53
|
+
if (c.partitioned) parts.push("Partitioned");
|
|
54
|
+
return parts.join("; ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Append Set-Cookie headers for each queued CookieSpec onto an existing Headers object. */
|
|
58
|
+
export function appendCookieHeaders(headers: Headers, cookies: readonly CookieSpec[]): void {
|
|
59
|
+
for (const c of cookies) {
|
|
60
|
+
headers.append("Set-Cookie", formatSetCookieHeader(c));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
9
64
|
export class HttpRpcError extends Error {
|
|
10
65
|
constructor(
|
|
11
66
|
message: string,
|
|
@@ -16,35 +71,47 @@ export class HttpRpcError extends Error {
|
|
|
16
71
|
}
|
|
17
72
|
}
|
|
18
73
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Serialize a schema + batches into a complete IPC stream as Uint8Array.
|
|
76
|
+
*
|
|
77
|
+
* A single IPC stream is `[schema_msg, batch_msg, batch_msg, ..., EOS]`.
|
|
78
|
+
* Each backend implements `serializeBatches` to write that atomically —
|
|
79
|
+
* arrow-js via `RecordBatchStreamWriter`, flechette via `tablesToIPC`
|
|
80
|
+
* (added in our flechette fork). Naive concatenation of per-batch streams
|
|
81
|
+
* produces multiple EOS markers and breaks readers.
|
|
82
|
+
*/
|
|
83
|
+
export function serializeIpcStream(schema: VgiSchema, batches: VgiBatch[]): Uint8Array {
|
|
84
|
+
const conformed = batches.map((b) => conformBatchToSchema(b, schema));
|
|
85
|
+
return serializeBatches(schema, conformed);
|
|
28
86
|
}
|
|
29
87
|
|
|
30
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Create a Response with Arrow IPC content type.
|
|
90
|
+
*
|
|
91
|
+
* Server errors (status 500) are translated to HTTP 200 with an
|
|
92
|
+
* ``X-VGI-RPC-Error: true`` header so that clients which discard
|
|
93
|
+
* response bodies on 5xx still receive the Arrow IPC error metadata.
|
|
94
|
+
* Client errors (400, 401, 404, 415) are passed through unchanged.
|
|
95
|
+
*/
|
|
31
96
|
export function arrowResponse(body: Uint8Array, status = 200, extraHeaders?: Headers): Response {
|
|
32
97
|
const headers = extraHeaders ?? new Headers();
|
|
33
98
|
headers.set("Content-Type", ARROW_CONTENT_TYPE);
|
|
99
|
+
if (status === 500) {
|
|
100
|
+
headers.set(RPC_ERROR_HEADER, "true");
|
|
101
|
+
return new Response(body as unknown as BodyInit, { status: 200, headers });
|
|
102
|
+
}
|
|
34
103
|
return new Response(body as unknown as BodyInit, { status, headers });
|
|
35
104
|
}
|
|
36
105
|
|
|
37
|
-
/** Read schema + first batch from an IPC stream body. */
|
|
38
|
-
export async function readRequestFromBody(body: Uint8Array): Promise<{ schema:
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
106
|
+
/** Read schema + first batch from an IPC stream body via the facade. */
|
|
107
|
+
export async function readRequestFromBody(body: Uint8Array): Promise<{ schema: VgiSchema; batch: VgiBatch }> {
|
|
108
|
+
const batch = deserializeBatch(body);
|
|
109
|
+
// Reject only truly empty bodies. A zero-field, zero-row batch with batch
|
|
110
|
+
// metadata is a legal exchange/cancel/continuation signal — the state
|
|
111
|
+
// token rides on `batch.metadata` and downstream code (cancel detection,
|
|
112
|
+
// schema conformance gating) is built to handle it.
|
|
113
|
+
if (batch.schema.fields.length === 0 && batch.numRows === 0 && (batch.metadata?.size ?? 0) === 0) {
|
|
43
114
|
throw new HttpRpcError("Empty IPC stream: no schema", 400);
|
|
44
115
|
}
|
|
45
|
-
|
|
46
|
-
if (batches.length === 0) {
|
|
47
|
-
throw new HttpRpcError("IPC stream contains no batches", 400);
|
|
48
|
-
}
|
|
49
|
-
return { schema, batch: batches[0] };
|
|
116
|
+
return { schema: batch.schema, batch };
|
|
50
117
|
}
|