@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/types.ts
CHANGED
|
@@ -1,23 +1,190 @@
|
|
|
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 { batchFromColumns, isBatch, type VgiBatch, type VgiSchema } from "./arrow/index.js";
|
|
5
5
|
import { AuthContext } from "./auth.js";
|
|
6
6
|
import { buildLogBatch, coerceInt64 } from "./wire/response.js";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Whether an RPC method is request/response or streaming. Mirrors Python's
|
|
10
|
+
* `MethodType` and is carried in the `__describe__` payload.
|
|
11
|
+
*/
|
|
8
12
|
export enum MethodType {
|
|
13
|
+
/** Single request batch in, single result batch out. */
|
|
9
14
|
UNARY = "unary",
|
|
15
|
+
/** Streamed batches — either a producer or an exchange stream. */
|
|
10
16
|
STREAM = "stream",
|
|
11
17
|
}
|
|
12
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Coarse identifier of the transport binding a {@link VgiRpcServer} or
|
|
21
|
+
* HTTP handler. Workers (RPC implementations) read this via
|
|
22
|
+
* {@link CallContext.kind} or the {@link ServeStartHook} lifecycle hook
|
|
23
|
+
* to tailor startup behaviour (skip HTTP-only caching, enable
|
|
24
|
+
* transport-specific metrics, etc.).
|
|
25
|
+
*
|
|
26
|
+
* Values are wire/log-friendly strings to match Python's `TransportKind`
|
|
27
|
+
* StrEnum byte-for-byte across language boundaries.
|
|
28
|
+
*
|
|
29
|
+
* - `PIPE` — Stdio worker (the standalone {@link VgiRpcServer} loop).
|
|
30
|
+
* - `HTTP` — Fetch-style HTTP handler (`createHttpHandler`).
|
|
31
|
+
* - `UNIX` — AF_UNIX socket handler (the launcher path).
|
|
32
|
+
*/
|
|
33
|
+
export enum TransportKind {
|
|
34
|
+
/** Stdio worker — the standalone {@link VgiRpcServer} loop. */
|
|
35
|
+
PIPE = "pipe",
|
|
36
|
+
/** Fetch-style HTTP handler (`createHttpHandler`). */
|
|
37
|
+
HTTP = "http",
|
|
38
|
+
/** AF_UNIX socket handler (the launcher path). */
|
|
39
|
+
UNIX = "unix",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Optional lifecycle hook fired once per process before the first
|
|
44
|
+
* dispatched request.
|
|
45
|
+
*
|
|
46
|
+
* For the stdio server, fires inside `VgiRpcServer.run()` before the
|
|
47
|
+
* first read. For HTTP, fires lazily on the first request handled
|
|
48
|
+
* (fork-safe for pre-fork servers).
|
|
49
|
+
*
|
|
50
|
+
* If the hook raises, the server logs the exception and propagates it,
|
|
51
|
+
* leaving the bind state unset so the next attempt re-fires the hook
|
|
52
|
+
* rather than silently skipping it.
|
|
53
|
+
*/
|
|
54
|
+
export type ServeStartHook = (kind: TransportKind) => void | Promise<void>;
|
|
55
|
+
|
|
13
56
|
/** Logging interface available to handlers. */
|
|
14
57
|
export interface LogContext {
|
|
58
|
+
/** Emit a client-directed log message (sent as a zero-row log batch on the
|
|
59
|
+
* wire). `level` is a severity label such as `"info"`, `"warning"`, or
|
|
60
|
+
* `"error"`; `extra` carries optional structured string key/value pairs. */
|
|
15
61
|
clientLog(level: string, message: string, extra?: Record<string, string>): void;
|
|
16
62
|
}
|
|
17
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Attributes for a Set-Cookie directive queued via {@link CallContext.setCookie}.
|
|
66
|
+
* All fields are optional; omitted attributes are not serialized onto the header.
|
|
67
|
+
*/
|
|
68
|
+
export interface CookieAttrs {
|
|
69
|
+
expires?: Date;
|
|
70
|
+
maxAge?: number;
|
|
71
|
+
domain?: string;
|
|
72
|
+
path?: string;
|
|
73
|
+
secure?: boolean;
|
|
74
|
+
httpOnly?: boolean;
|
|
75
|
+
sameSite?: "Strict" | "Lax" | "None";
|
|
76
|
+
partitioned?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A queued cookie mutation for the HTTP response. Internal — callers
|
|
81
|
+
* interact through {@link CallContext.setCookie} / {@link CallContext.deleteCookie}.
|
|
82
|
+
*/
|
|
83
|
+
export interface CookieSpec extends CookieAttrs {
|
|
84
|
+
name: string;
|
|
85
|
+
value: string;
|
|
86
|
+
delete: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Per-request sticky-session sink. Internal — populated by the HTTP handler
|
|
90
|
+
* when sticky sessions are enabled, read/mutated by {@link CallContext}'s
|
|
91
|
+
* `openSession` / `closeSession` / `session` getters. */
|
|
92
|
+
export interface StickyContext {
|
|
93
|
+
readonly acceptOpens: boolean;
|
|
94
|
+
state: unknown | null;
|
|
95
|
+
sessionId: string | null;
|
|
96
|
+
mintToken: string | null;
|
|
97
|
+
closed: boolean;
|
|
98
|
+
action: "none" | "resume" | "open" | "close";
|
|
99
|
+
_open(state: unknown, ttl: number | undefined): void;
|
|
100
|
+
_close(): void;
|
|
101
|
+
}
|
|
102
|
+
|
|
18
103
|
/** Extended context with authentication info, available to handlers. */
|
|
19
104
|
export interface CallContext extends LogContext {
|
|
105
|
+
/** Authenticated principal for this call; {@link AuthContext.anonymous} when
|
|
106
|
+
* the request was not authenticated. */
|
|
20
107
|
readonly auth: AuthContext;
|
|
108
|
+
/** Coarse identifier of the bound transport, or `undefined` until the
|
|
109
|
+
* server begins serving (the value is committed by the lifecycle hook
|
|
110
|
+
* on the very first request). */
|
|
111
|
+
readonly kind?: TransportKind;
|
|
112
|
+
/**
|
|
113
|
+
* Wire body bytes the framework will accept this iteration before
|
|
114
|
+
* triggering a continuation token (producer streams) or strict-fail
|
|
115
|
+
* with an EXCEPTION batch (unary / stream-exchange). Snapshot at
|
|
116
|
+
* collector construction; not live. `undefined` when no cap is
|
|
117
|
+
* configured or the transport doesn't expose one (stdio).
|
|
118
|
+
*/
|
|
119
|
+
readonly remainingResponseBytes?: number;
|
|
120
|
+
/**
|
|
121
|
+
* External-channel bytes left this iteration. Always a hard cap —
|
|
122
|
+
* externalised uploads have no escape valve like producer
|
|
123
|
+
* continuation tokens. Undefined when no cap is configured or
|
|
124
|
+
* externalisation is disabled.
|
|
125
|
+
*/
|
|
126
|
+
readonly remainingExternalizedResponseBytes?: number;
|
|
127
|
+
/** True iff the server has an externalisation backend wired up. */
|
|
128
|
+
readonly externalizationEnabled?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Incoming request cookies. Empty for non-HTTP transports.
|
|
131
|
+
*/
|
|
132
|
+
readonly cookies: ReadonlyMap<string, string>;
|
|
133
|
+
/**
|
|
134
|
+
* Queue a Set-Cookie header on the HTTP response. Only valid inside a
|
|
135
|
+
* unary RPC method served over HTTP; throws otherwise.
|
|
136
|
+
*/
|
|
137
|
+
setCookie(name: string, value: string, attrs?: CookieAttrs): void;
|
|
138
|
+
/**
|
|
139
|
+
* Queue an unset-cookie directive on the HTTP response. Only valid
|
|
140
|
+
* inside a unary RPC method served over HTTP; throws otherwise.
|
|
141
|
+
*/
|
|
142
|
+
deleteCookie(name: string, opts?: { path?: string; domain?: string }): void;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Live sticky-session state object, or `null` when no session is bound to
|
|
146
|
+
* this request. HTTP-only — other transports always return `null`.
|
|
147
|
+
*/
|
|
148
|
+
readonly session: unknown | null;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Opaque 24-char-hex session ID, or `null` when no session is bound.
|
|
152
|
+
* Survives {@link closeSession} so post-close access-log records still
|
|
153
|
+
* carry the id.
|
|
154
|
+
*/
|
|
155
|
+
readonly sessionId: string | null;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Register a sticky session holding *state* for subsequent requests on
|
|
159
|
+
* this transport. HTTP-only — throws on other transports, on calls
|
|
160
|
+
* without the `VGI-Session-Accept: true` opt-in header, or when a
|
|
161
|
+
* session is already bound to this request.
|
|
162
|
+
*/
|
|
163
|
+
openSession(state: unknown, ttl?: number): void;
|
|
164
|
+
|
|
165
|
+
/** Invalidate the sticky session bound to this request. Idempotent. */
|
|
166
|
+
closeSession(): void;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const EMPTY_COOKIES: ReadonlyMap<string, string> = new Map();
|
|
170
|
+
|
|
171
|
+
function cookieNotUnaryHttpError(): Error {
|
|
172
|
+
return new Error("setCookie/deleteCookie is only supported inside unary RPC methods served over HTTP");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Surface as `exception_type: "RuntimeError"` on the EXCEPTION batch — the
|
|
176
|
+
* wire serializer reads `error.constructor.name`, so we need a real subclass
|
|
177
|
+
* rather than just `err.name = "RuntimeError"`. Matches Python's pattern of
|
|
178
|
+
* raising `RuntimeError` from the runtime API methods. */
|
|
179
|
+
class RuntimeError extends Error {
|
|
180
|
+
constructor(message: string) {
|
|
181
|
+
super(message);
|
|
182
|
+
this.name = "RuntimeError";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function runtimeError(message: string): Error {
|
|
187
|
+
return new RuntimeError(message);
|
|
21
188
|
}
|
|
22
189
|
|
|
23
190
|
/** Handler for unary (request-response) RPC methods. */
|
|
@@ -34,27 +201,60 @@ export type ProducerFn<S = any> = (state: S, out: OutputCollector) => Promise<vo
|
|
|
34
201
|
/** Initialization function for exchange streams. Returns the initial state object. */
|
|
35
202
|
export type ExchangeInit<S = any> = (params: Record<string, any>) => Promise<S> | S;
|
|
36
203
|
/** Called once per input batch. Must emit exactly one output batch per call. */
|
|
37
|
-
export type ExchangeFn<S = any> = (state: S, input:
|
|
204
|
+
export type ExchangeFn<S = any> = (state: S, input: VgiBatch, out: OutputCollector) => Promise<void> | void;
|
|
38
205
|
|
|
39
206
|
/** Produces a header batch sent before the first output batch in a stream. */
|
|
40
207
|
export type HeaderInit = (params: Record<string, any>, state: any, ctx: LogContext) => Record<string, any>;
|
|
41
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Optional handler invoked when the client signals cancellation by writing an
|
|
211
|
+
* input batch carrying the ``vgi_rpc.cancel`` metadata key. The server runs
|
|
212
|
+
* this hook once, before breaking out of the streaming loop, giving state
|
|
213
|
+
* objects a chance to release resources. Errors are logged and swallowed.
|
|
214
|
+
*/
|
|
215
|
+
export type OnCancelFn<S = any> = (state: S) => Promise<void> | void;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* In-memory definition of one registered RPC method, produced by the
|
|
219
|
+
* {@link Protocol} builder and consumed by the dispatch layer. Which optional
|
|
220
|
+
* fields are populated depends on the method {@link type}: `handler` for unary
|
|
221
|
+
* methods, `producerInit`/`producerFn` for producer streams, and
|
|
222
|
+
* `exchangeInit`/`exchangeFn` for exchange streams.
|
|
223
|
+
*/
|
|
42
224
|
export interface MethodDefinition {
|
|
225
|
+
/** Method name as registered on the protocol. */
|
|
43
226
|
name: string;
|
|
227
|
+
/** Whether the method is unary or streaming. */
|
|
44
228
|
type: MethodType;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
229
|
+
/** Schema of the request parameters batch. */
|
|
230
|
+
paramsSchema: VgiSchema;
|
|
231
|
+
/** Schema of the unary result batch (unused for streams). */
|
|
232
|
+
resultSchema: VgiSchema;
|
|
233
|
+
/** Schema of streamed output batches (producer and exchange streams). */
|
|
234
|
+
outputSchema?: VgiSchema;
|
|
235
|
+
/** Schema of streamed input batches (exchange streams only). */
|
|
236
|
+
inputSchema?: VgiSchema;
|
|
237
|
+
/** Implementation for unary methods. */
|
|
49
238
|
handler?: UnaryHandler;
|
|
239
|
+
/** Builds the initial state object for a producer stream. */
|
|
50
240
|
producerInit?: ProducerInit;
|
|
241
|
+
/** Produces output batches for a producer stream. */
|
|
51
242
|
producerFn?: ProducerFn;
|
|
243
|
+
/** Builds the initial state object for an exchange stream. */
|
|
52
244
|
exchangeInit?: ExchangeInit;
|
|
245
|
+
/** Handles each input batch of an exchange stream. */
|
|
53
246
|
exchangeFn?: ExchangeFn;
|
|
54
|
-
|
|
247
|
+
/** Schema of the optional per-stream header batch. */
|
|
248
|
+
headerSchema?: VgiSchema;
|
|
249
|
+
/** Builds the optional header batch emitted before the first output batch. */
|
|
55
250
|
headerInit?: HeaderInit;
|
|
251
|
+
/** Optional hook run when the client cancels a stream. */
|
|
252
|
+
onCancel?: OnCancelFn;
|
|
253
|
+
/** Human-readable method documentation, surfaced via introspection. */
|
|
56
254
|
doc?: string;
|
|
255
|
+
/** Default values applied to omitted request parameters. */
|
|
57
256
|
defaults?: Record<string, any>;
|
|
257
|
+
/** Human-readable parameter type names, surfaced via introspection. */
|
|
58
258
|
paramTypes?: Record<string, string>;
|
|
59
259
|
}
|
|
60
260
|
|
|
@@ -68,15 +268,51 @@ export interface DispatchInfo {
|
|
|
68
268
|
serverId: string;
|
|
69
269
|
/** Client-supplied request identifier, or null. */
|
|
70
270
|
requestId: string | null;
|
|
271
|
+
/** Coarse transport identifier — `pipe` for stdio, `http` for fetch
|
|
272
|
+
* handlers, `unix` for AF_UNIX. */
|
|
273
|
+
kind?: TransportKind;
|
|
274
|
+
/** Logical service / protocol name. */
|
|
275
|
+
protocol?: string;
|
|
276
|
+
/** SHA-256 hex of the canonical __describe__ payload (always required in access log). */
|
|
277
|
+
protocolHash?: string;
|
|
278
|
+
/** Operator-supplied protocol-contract version label (optional). */
|
|
279
|
+
protocolVersion?: string;
|
|
280
|
+
/** Authenticated principal, empty string when anonymous. */
|
|
281
|
+
principal?: string;
|
|
282
|
+
/** Authentication domain, empty string when anonymous. */
|
|
283
|
+
authDomain?: string;
|
|
284
|
+
/** True when the call was authenticated. */
|
|
285
|
+
authenticated?: boolean;
|
|
286
|
+
/** HTTP transport: remote IP:port. */
|
|
287
|
+
remoteAddr?: string;
|
|
288
|
+
/** Self-contained Arrow IPC stream of the request batch (unary + stream init only). */
|
|
289
|
+
requestData?: Uint8Array;
|
|
290
|
+
/** Stream lifecycle identifier (32-char lowercase hex); empty on unary. */
|
|
291
|
+
streamId?: string;
|
|
292
|
+
/** True when a stream was cancelled by the client. */
|
|
293
|
+
cancelled?: boolean;
|
|
294
|
+
/** Sticky session ID (24-char hex). Present only when the request was bound
|
|
295
|
+
* to a sticky session or the method opened/closed one. */
|
|
296
|
+
sessionId?: string;
|
|
297
|
+
/** Sticky-session lifecycle action observed during dispatch — one of
|
|
298
|
+
* `"none"` / `"resume"` / `"open"` / `"close"`. Omitted when sticky is
|
|
299
|
+
* disabled or the request never touched the sticky middleware. */
|
|
300
|
+
sessionAction?: "none" | "resume" | "open" | "close";
|
|
71
301
|
}
|
|
72
302
|
|
|
73
303
|
/** Per-call I/O counters, matching Python's CallStatistics. */
|
|
74
304
|
export interface CallStatistics {
|
|
305
|
+
/** Number of input batches read from the client. */
|
|
75
306
|
inputBatches: number;
|
|
307
|
+
/** Number of output batches written to the client. */
|
|
76
308
|
outputBatches: number;
|
|
309
|
+
/** Total rows across all input batches. */
|
|
77
310
|
inputRows: number;
|
|
311
|
+
/** Total rows across all output batches. */
|
|
78
312
|
outputRows: number;
|
|
313
|
+
/** Total serialized bytes of all input batches. */
|
|
79
314
|
inputBytes: number;
|
|
315
|
+
/** Total serialized bytes of all output batches. */
|
|
80
316
|
outputBytes: number;
|
|
81
317
|
}
|
|
82
318
|
|
|
@@ -88,12 +324,16 @@ export type HookToken = unknown;
|
|
|
88
324
|
* Implementations must be safe for concurrent use (HTTP transport is concurrent).
|
|
89
325
|
*/
|
|
90
326
|
export interface DispatchHook {
|
|
327
|
+
/** Invoked before the method runs. The returned {@link HookToken} is opaque
|
|
328
|
+
* to the framework and passed back to {@link onDispatchEnd}. */
|
|
91
329
|
onDispatchStart(info: DispatchInfo): HookToken;
|
|
330
|
+
/** Invoked after the method completes or throws. `stats` carries the per-call
|
|
331
|
+
* I/O counters; `error` is set only when the dispatch failed. */
|
|
92
332
|
onDispatchEnd(token: HookToken, info: DispatchInfo, stats: CallStatistics, error?: Error): void;
|
|
93
333
|
}
|
|
94
334
|
|
|
95
335
|
export interface EmittedBatch {
|
|
96
|
-
batch:
|
|
336
|
+
batch: VgiBatch;
|
|
97
337
|
metadata?: Map<string, string>;
|
|
98
338
|
}
|
|
99
339
|
|
|
@@ -106,48 +346,166 @@ export class OutputCollector implements CallContext {
|
|
|
106
346
|
private _dataBatchIdx: number | null = null;
|
|
107
347
|
private _finished = false;
|
|
108
348
|
private _producerMode: boolean;
|
|
109
|
-
private _outputSchema:
|
|
349
|
+
private _outputSchema: VgiSchema;
|
|
110
350
|
private _serverId: string;
|
|
111
351
|
private _requestId: string | null;
|
|
352
|
+
private _cookieSinkEnabled = false;
|
|
353
|
+
private _responseCookies: CookieSpec[] = [];
|
|
354
|
+
private _stickyContext: StickyContext | null = null;
|
|
355
|
+
/** Authenticated principal for this call; {@link AuthContext.anonymous} when
|
|
356
|
+
* the request was not authenticated. */
|
|
112
357
|
readonly auth: AuthContext;
|
|
358
|
+
readonly cookies: ReadonlyMap<string, string>;
|
|
359
|
+
readonly kind?: TransportKind;
|
|
360
|
+
readonly remainingResponseBytes?: number;
|
|
361
|
+
readonly remainingExternalizedResponseBytes?: number;
|
|
362
|
+
readonly externalizationEnabled?: boolean;
|
|
113
363
|
|
|
114
364
|
constructor(
|
|
115
|
-
outputSchema:
|
|
365
|
+
outputSchema: VgiSchema,
|
|
116
366
|
producerMode = true,
|
|
117
367
|
serverId = "",
|
|
118
368
|
requestId: string | null = null,
|
|
119
369
|
authContext?: AuthContext,
|
|
370
|
+
cookies?: ReadonlyMap<string, string>,
|
|
371
|
+
kind?: TransportKind,
|
|
372
|
+
/** Snapshot budget fields exposed to worker code via {@link CallContext}.
|
|
373
|
+
* Optional — non-HTTP transports omit them and existing call sites
|
|
374
|
+
* remain source-compatible. */
|
|
375
|
+
budgets?: {
|
|
376
|
+
remainingResponseBytes?: number;
|
|
377
|
+
remainingExternalizedResponseBytes?: number;
|
|
378
|
+
externalizationEnabled?: boolean;
|
|
379
|
+
},
|
|
120
380
|
) {
|
|
121
381
|
this._outputSchema = outputSchema;
|
|
122
382
|
this._producerMode = producerMode;
|
|
123
383
|
this._serverId = serverId;
|
|
124
384
|
this._requestId = requestId;
|
|
125
385
|
this.auth = authContext ?? AuthContext.anonymous();
|
|
386
|
+
this.cookies = cookies ?? EMPTY_COOKIES;
|
|
387
|
+
this.kind = kind;
|
|
388
|
+
this.remainingResponseBytes = budgets?.remainingResponseBytes;
|
|
389
|
+
this.remainingExternalizedResponseBytes = budgets?.remainingExternalizedResponseBytes;
|
|
390
|
+
this.externalizationEnabled = budgets?.externalizationEnabled;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Mark this collector as able to accept Set-Cookie directives. Called
|
|
395
|
+
* by the unary HTTP dispatcher only; streaming and non-HTTP paths leave
|
|
396
|
+
* the sink disabled so setCookie/deleteCookie throw.
|
|
397
|
+
* @internal
|
|
398
|
+
*/
|
|
399
|
+
enableCookieSink(): void {
|
|
400
|
+
this._cookieSinkEnabled = true;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Return and clear all queued cookie mutations.
|
|
405
|
+
* @internal
|
|
406
|
+
*/
|
|
407
|
+
drainResponseCookies(): CookieSpec[] {
|
|
408
|
+
const cookies = this._responseCookies;
|
|
409
|
+
this._responseCookies = [];
|
|
410
|
+
return cookies;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
setCookie(name: string, value: string, attrs?: CookieAttrs): void {
|
|
414
|
+
if (!this._cookieSinkEnabled) throw cookieNotUnaryHttpError();
|
|
415
|
+
this._responseCookies.push({
|
|
416
|
+
name,
|
|
417
|
+
value,
|
|
418
|
+
delete: false,
|
|
419
|
+
...(attrs ?? {}),
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
deleteCookie(name: string, opts?: { path?: string; domain?: string }): void {
|
|
424
|
+
if (!this._cookieSinkEnabled) throw cookieNotUnaryHttpError();
|
|
425
|
+
this._responseCookies.push({
|
|
426
|
+
name,
|
|
427
|
+
value: "",
|
|
428
|
+
delete: true,
|
|
429
|
+
path: opts?.path,
|
|
430
|
+
domain: opts?.domain,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Attach the sticky-session sink the HTTP handler built for this request.
|
|
435
|
+
* @internal */
|
|
436
|
+
attachStickyContext(ctx: StickyContext): void {
|
|
437
|
+
this._stickyContext = ctx;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
get session(): unknown | null {
|
|
441
|
+
return this._stickyContext?.state ?? null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
get sessionId(): string | null {
|
|
445
|
+
return this._stickyContext?.sessionId ?? null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
openSession(state: unknown, ttl?: number): void {
|
|
449
|
+
const sink = this._stickyContext;
|
|
450
|
+
if (!sink) {
|
|
451
|
+
throw runtimeError("sticky sessions not available on this transport");
|
|
452
|
+
}
|
|
453
|
+
if (!sink.acceptOpens) {
|
|
454
|
+
throw runtimeError(
|
|
455
|
+
"client did not opt in to sticky sessions " +
|
|
456
|
+
"(missing VGI-Session-Accept: true header — open the call inside " +
|
|
457
|
+
"an HttpConnection.with_session_token() block)",
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
if (sink.state !== null) {
|
|
461
|
+
throw runtimeError("a sticky session is already active for this request");
|
|
462
|
+
}
|
|
463
|
+
sink._open(state, ttl);
|
|
464
|
+
sink.action = "open";
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
closeSession(): void {
|
|
468
|
+
const sink = this._stickyContext;
|
|
469
|
+
if (!sink) {
|
|
470
|
+
throw runtimeError("sticky sessions not available on this transport");
|
|
471
|
+
}
|
|
472
|
+
sink._close();
|
|
473
|
+
sink.action = "close";
|
|
126
474
|
}
|
|
127
475
|
|
|
128
|
-
|
|
476
|
+
/** Schema of the data batches this collector emits. */
|
|
477
|
+
get outputSchema(): VgiSchema {
|
|
129
478
|
return this._outputSchema;
|
|
130
479
|
}
|
|
131
480
|
|
|
481
|
+
/** True once {@link finish} has been called (producer streams only). */
|
|
132
482
|
get finished(): boolean {
|
|
133
483
|
return this._finished;
|
|
134
484
|
}
|
|
135
485
|
|
|
486
|
+
/** Batches emitted so far this call — the single data batch plus any log
|
|
487
|
+
* batches, in emission order. Consumed by the dispatch layer. */
|
|
136
488
|
get batches(): EmittedBatch[] {
|
|
137
489
|
return this._batches;
|
|
138
490
|
}
|
|
139
491
|
|
|
140
|
-
/** Emit a pre-built
|
|
141
|
-
emit(batch:
|
|
492
|
+
/** Emit a pre-built batch as the data batch for this call. */
|
|
493
|
+
emit(batch: VgiBatch, metadata?: Map<string, string>): void;
|
|
142
494
|
/** Emit a data batch from column arrays keyed by field name. Int64 Number values are coerced to BigInt. */
|
|
143
495
|
emit(columns: Record<string, any[]>): void;
|
|
144
|
-
emit(batchOrColumns:
|
|
145
|
-
let batch:
|
|
146
|
-
if (batchOrColumns
|
|
496
|
+
emit(batchOrColumns: VgiBatch | Record<string, any[]>, metadata?: Map<string, string>): void {
|
|
497
|
+
let batch: VgiBatch;
|
|
498
|
+
if (isBatch(batchOrColumns)) {
|
|
147
499
|
batch = batchOrColumns;
|
|
148
500
|
} else {
|
|
149
|
-
const coerced = coerceInt64(this._outputSchema, batchOrColumns);
|
|
150
|
-
|
|
501
|
+
const coerced = coerceInt64(this._outputSchema, batchOrColumns as Record<string, any[]>);
|
|
502
|
+
// Build columns dict ensuring each field has an array (vectorFromArray-equivalent under the hood).
|
|
503
|
+
const cols: Record<string, any[]> = {};
|
|
504
|
+
for (const f of this._outputSchema.fields) {
|
|
505
|
+
const v = coerced[f.name];
|
|
506
|
+
cols[f.name] = Array.isArray(v) ? v : [v];
|
|
507
|
+
}
|
|
508
|
+
batch = batchFromColumns(this._outputSchema, cols);
|
|
151
509
|
}
|
|
152
510
|
if (this._dataBatchIdx !== null) {
|
|
153
511
|
throw new Error("Only one data batch may be emitted per call");
|
package/src/util/gzip.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cross-runtime gzip compression/decompression.
|
|
6
|
+
*
|
|
7
|
+
* The Python `vgi-rpc[http]` client now defaults to `Content-Encoding: gzip`
|
|
8
|
+
* on every request, so every HTTP server in the framework must decode it.
|
|
9
|
+
* We use the Web platform `DecompressionStream`/`CompressionStream` APIs,
|
|
10
|
+
* which Bun, Node 18+, Deno, and Cloudflare workerd all expose.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
async function streamThrough(
|
|
14
|
+
data: Uint8Array,
|
|
15
|
+
transform: ReadableWritablePair<Uint8Array, BufferSource>,
|
|
16
|
+
maxOutputSize?: number,
|
|
17
|
+
): Promise<Uint8Array<ArrayBuffer>> {
|
|
18
|
+
const ws = transform.writable.getWriter();
|
|
19
|
+
const rs = transform.readable.getReader();
|
|
20
|
+
// Copy into a freshly-allocated ArrayBuffer-backed view to satisfy TS lib
|
|
21
|
+
// BufferSource narrowing (rules out SharedArrayBuffer-backed Uint8Array).
|
|
22
|
+
const view = new Uint8Array(data.byteLength);
|
|
23
|
+
view.set(data);
|
|
24
|
+
const writePromise = (async () => {
|
|
25
|
+
await ws.write(view as BufferSource);
|
|
26
|
+
await ws.close();
|
|
27
|
+
})();
|
|
28
|
+
const chunks: Uint8Array[] = [];
|
|
29
|
+
let total = 0;
|
|
30
|
+
while (true) {
|
|
31
|
+
const { value, done } = await rs.read();
|
|
32
|
+
if (done) break;
|
|
33
|
+
const chunk = value as Uint8Array;
|
|
34
|
+
total += chunk.byteLength;
|
|
35
|
+
if (maxOutputSize != null && total > maxOutputSize) {
|
|
36
|
+
throw new Error(`gzip decompressed size (${total}) exceeds cap (${maxOutputSize})`);
|
|
37
|
+
}
|
|
38
|
+
chunks.push(chunk);
|
|
39
|
+
}
|
|
40
|
+
await writePromise;
|
|
41
|
+
const out = new Uint8Array(total);
|
|
42
|
+
let offset = 0;
|
|
43
|
+
for (const c of chunks) {
|
|
44
|
+
out.set(c, offset);
|
|
45
|
+
offset += c.byteLength;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Decompress gzip-encoded data, optionally bounded by `maxOutputSize`.
|
|
52
|
+
*
|
|
53
|
+
* The gzip footer's ISIZE field is mod 2^32 so it can't be trusted for a
|
|
54
|
+
* pre-check — we bound output incrementally during streaming decode.
|
|
55
|
+
*/
|
|
56
|
+
export async function gzipDecompress(data: Uint8Array, maxOutputSize?: number): Promise<Uint8Array<ArrayBuffer>> {
|
|
57
|
+
return streamThrough(data, new DecompressionStream("gzip"), maxOutputSize);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Compress data with gzip. `level` is accepted for API parity with zstd but ignored — the Web API doesn't expose a level. */
|
|
61
|
+
export async function gzipCompress(data: Uint8Array, _level?: number): Promise<Uint8Array<ArrayBuffer>> {
|
|
62
|
+
return streamThrough(data, new CompressionStream("gzip"));
|
|
63
|
+
}
|
package/src/util/schema.ts
CHANGED
|
@@ -1,31 +1,13 @@
|
|
|
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 { serializeSchema as facadeSerializeSchema, type VgiSchema } from "../arrow/index.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Serialize a Schema to the Arrow IPC Schema message format.
|
|
8
8
|
* This produces bytes compatible with Python's `pa.ipc.read_schema()`.
|
|
9
|
-
*
|
|
10
|
-
* We serialize by writing an empty-batch IPC stream and extracting
|
|
11
|
-
* the bytes, which includes the schema message. Python's read_schema()
|
|
12
|
-
* uses `pa.ipc.read_schema(pa.py_buffer(bytes))` which expects
|
|
13
|
-
* the schema flatbuffer message bytes directly — but the Python side
|
|
14
|
-
* actually uses `schema.serialize()` which produces Schema message bytes.
|
|
15
|
-
*
|
|
16
|
-
* In arrow-js, we can get the equivalent by using Message.from(schema)
|
|
17
|
-
* and encoding it, or by serializing a zero-batch stream.
|
|
18
|
-
*
|
|
19
|
-
* The Python `schema.serialize()` produces the Schema flatbuffer message bytes,
|
|
20
|
-
* and `pa.ipc.read_schema()` expects an IPC stream containing a schema message.
|
|
21
|
-
* The actual format is: continuation marker (0xFFFFFFFF) + length + flatbuffer bytes.
|
|
9
|
+
* Equivalent to writing an empty-batch IPC stream — schema message + EOS marker.
|
|
22
10
|
*/
|
|
23
|
-
export function serializeSchema(schema:
|
|
24
|
-
|
|
25
|
-
// This writes: Schema message + EOS marker.
|
|
26
|
-
// Python's pa.ipc.read_schema() can read this format.
|
|
27
|
-
const writer = new RecordBatchStreamWriter();
|
|
28
|
-
writer.reset(undefined, schema);
|
|
29
|
-
writer.close();
|
|
30
|
-
return writer.toUint8Array(true);
|
|
11
|
+
export function serializeSchema(schema: VgiSchema): Uint8Array {
|
|
12
|
+
return facadeSerializeSchema(schema);
|
|
31
13
|
}
|