@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.
Files changed (177) hide show
  1. package/dist/access-log.d.ts +55 -0
  2. package/dist/access-log.d.ts.map +1 -0
  3. package/dist/arrow/impl-arrowjs/index.d.ts +96 -0
  4. package/dist/arrow/impl-arrowjs/index.d.ts.map +1 -0
  5. package/dist/arrow/impl-flechette/index.d.ts +102 -0
  6. package/dist/arrow/impl-flechette/index.d.ts.map +1 -0
  7. package/dist/arrow/impl-flechette/message-meta.d.ts +11 -0
  8. package/dist/arrow/impl-flechette/message-meta.d.ts.map +1 -0
  9. package/dist/arrow/index.d.ts +4 -0
  10. package/dist/arrow/index.d.ts.map +1 -0
  11. package/dist/arrow/predicates.d.ts +44 -0
  12. package/dist/arrow/predicates.d.ts.map +1 -0
  13. package/dist/arrow/types.d.ts +62 -0
  14. package/dist/arrow/types.d.ts.map +1 -0
  15. package/dist/auth.d.ts +5 -0
  16. package/dist/auth.d.ts.map +1 -1
  17. package/dist/client/capabilities.d.ts +25 -0
  18. package/dist/client/capabilities.d.ts.map +1 -0
  19. package/dist/client/connect.d.ts +10 -0
  20. package/dist/client/connect.d.ts.map +1 -1
  21. package/dist/client/introspect.d.ts +21 -0
  22. package/dist/client/introspect.d.ts.map +1 -1
  23. package/dist/client/ipc.d.ts +8 -2
  24. package/dist/client/ipc.d.ts.map +1 -1
  25. package/dist/client/oauth.d.ts +9 -0
  26. package/dist/client/oauth.d.ts.map +1 -1
  27. package/dist/client/pipe.d.ts +24 -0
  28. package/dist/client/pipe.d.ts.map +1 -1
  29. package/dist/client/stream.d.ts +19 -2
  30. package/dist/client/stream.d.ts.map +1 -1
  31. package/dist/client/types.d.ts +23 -0
  32. package/dist/client/types.d.ts.map +1 -1
  33. package/dist/client/uploadUrl.d.ts +25 -0
  34. package/dist/client/uploadUrl.d.ts.map +1 -0
  35. package/dist/constants.d.ts +30 -2
  36. package/dist/constants.d.ts.map +1 -1
  37. package/dist/crypto.d.ts +22 -0
  38. package/dist/crypto.d.ts.map +1 -0
  39. package/dist/dispatch/describe.d.ts +10 -6
  40. package/dist/dispatch/describe.d.ts.map +1 -1
  41. package/dist/dispatch/stream.d.ts +2 -2
  42. package/dist/dispatch/stream.d.ts.map +1 -1
  43. package/dist/dispatch/unary.d.ts +2 -2
  44. package/dist/dispatch/unary.d.ts.map +1 -1
  45. package/dist/errors.d.ts +64 -1
  46. package/dist/errors.d.ts.map +1 -1
  47. package/dist/external.d.ts +27 -5
  48. package/dist/external.d.ts.map +1 -1
  49. package/dist/http/auth.d.ts +13 -0
  50. package/dist/http/auth.d.ts.map +1 -1
  51. package/dist/http/bearer.d.ts.map +1 -1
  52. package/dist/http/common.d.ts +43 -7
  53. package/dist/http/common.d.ts.map +1 -1
  54. package/dist/http/dispatch.d.ts +20 -2
  55. package/dist/http/dispatch.d.ts.map +1 -1
  56. package/dist/http/handler.d.ts.map +1 -1
  57. package/dist/http/index.d.ts +1 -0
  58. package/dist/http/index.d.ts.map +1 -1
  59. package/dist/http/jwt.d.ts +1 -0
  60. package/dist/http/jwt.d.ts.map +1 -1
  61. package/dist/http/mtls.d.ts +9 -1
  62. package/dist/http/mtls.d.ts.map +1 -1
  63. package/dist/http/oauth-pkce.d.ts +141 -0
  64. package/dist/http/oauth-pkce.d.ts.map +1 -0
  65. package/dist/http/pages.d.ts +3 -0
  66. package/dist/http/pages.d.ts.map +1 -1
  67. package/dist/http/sticky.d.ts +124 -0
  68. package/dist/http/sticky.d.ts.map +1 -0
  69. package/dist/http/token.d.ts +43 -12
  70. package/dist/http/token.d.ts.map +1 -1
  71. package/dist/http/types.d.ts +68 -5
  72. package/dist/http/types.d.ts.map +1 -1
  73. package/dist/index.d.ts +6 -4
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +1275 -3511
  76. package/dist/index.js.map +20 -38
  77. package/dist/launcher/hash.d.ts +22 -0
  78. package/dist/launcher/hash.d.ts.map +1 -0
  79. package/dist/launcher/index.d.ts +23 -0
  80. package/dist/launcher/index.d.ts.map +1 -0
  81. package/dist/launcher/launch.d.ts +27 -0
  82. package/dist/launcher/launch.d.ts.map +1 -0
  83. package/dist/launcher/lock.d.ts +19 -0
  84. package/dist/launcher/lock.d.ts.map +1 -0
  85. package/dist/launcher/serve-unix.d.ts +55 -0
  86. package/dist/launcher/serve-unix.d.ts.map +1 -0
  87. package/dist/launcher/state.d.ts +71 -0
  88. package/dist/launcher/state.d.ts.map +1 -0
  89. package/dist/otel.d.ts.map +1 -1
  90. package/dist/protocol.d.ts +19 -2
  91. package/dist/protocol.d.ts.map +1 -1
  92. package/dist/schema.d.ts +45 -18
  93. package/dist/schema.d.ts.map +1 -1
  94. package/dist/server.d.ts +23 -2
  95. package/dist/server.d.ts.map +1 -1
  96. package/dist/types.d.ts +270 -12
  97. package/dist/types.d.ts.map +1 -1
  98. package/dist/util/gzip.d.ts +10 -0
  99. package/dist/util/gzip.d.ts.map +1 -0
  100. package/dist/util/schema.d.ts +3 -15
  101. package/dist/util/schema.d.ts.map +1 -1
  102. package/dist/util/web-crypto.d.ts +22 -0
  103. package/dist/util/web-crypto.d.ts.map +1 -0
  104. package/dist/util/zstd.d.ts +26 -3
  105. package/dist/util/zstd.d.ts.map +1 -1
  106. package/dist/wire/opaque.d.ts +11 -0
  107. package/dist/wire/opaque.d.ts.map +1 -0
  108. package/dist/wire/reader.d.ts +5 -5
  109. package/dist/wire/reader.d.ts.map +1 -1
  110. package/dist/wire/request.d.ts +11 -3
  111. package/dist/wire/request.d.ts.map +1 -1
  112. package/dist/wire/response.d.ts +6 -6
  113. package/dist/wire/response.d.ts.map +1 -1
  114. package/dist/wire/writer.d.ts +49 -39
  115. package/dist/wire/writer.d.ts.map +1 -1
  116. package/package.json +35 -21
  117. package/src/access-log.ts +200 -0
  118. package/src/arrow/impl-arrowjs/index.ts +433 -0
  119. package/src/arrow/impl-flechette/index.ts +414 -0
  120. package/src/arrow/impl-flechette/message-meta.ts +174 -0
  121. package/src/arrow/index.ts +89 -0
  122. package/src/arrow/predicates.ts +56 -0
  123. package/src/arrow/types.ts +73 -0
  124. package/src/auth.ts +5 -0
  125. package/src/client/capabilities.ts +84 -0
  126. package/src/client/connect.ts +113 -26
  127. package/src/client/introspect.ts +74 -38
  128. package/src/client/ipc.ts +37 -27
  129. package/src/client/oauth.ts +9 -0
  130. package/src/client/pipe.ts +36 -9
  131. package/src/client/stream.ts +43 -20
  132. package/src/client/types.ts +23 -0
  133. package/src/client/uploadUrl.ts +169 -0
  134. package/src/constants.ts +34 -2
  135. package/src/crypto.ts +95 -0
  136. package/src/dispatch/describe.ts +146 -107
  137. package/src/dispatch/stream.ts +53 -24
  138. package/src/dispatch/unary.ts +5 -4
  139. package/src/errors.ts +87 -0
  140. package/src/external.ts +49 -30
  141. package/src/http/auth.ts +13 -0
  142. package/src/http/bearer.ts +2 -5
  143. package/src/http/common.ts +91 -23
  144. package/src/http/dispatch.ts +373 -46
  145. package/src/http/handler.ts +790 -68
  146. package/src/http/index.ts +1 -0
  147. package/src/http/jwt.ts +1 -0
  148. package/src/http/mtls.ts +25 -3
  149. package/src/http/oauth-pkce.ts +1035 -0
  150. package/src/http/pages.ts +30 -15
  151. package/src/http/sticky.ts +429 -0
  152. package/src/http/token.ts +170 -75
  153. package/src/http/types.ts +69 -5
  154. package/src/index.ts +40 -1
  155. package/src/launcher/hash.ts +104 -0
  156. package/src/launcher/index.ts +35 -0
  157. package/src/launcher/launch.ts +284 -0
  158. package/src/launcher/lock.ts +171 -0
  159. package/src/launcher/serve-unix.ts +386 -0
  160. package/src/launcher/state.ts +257 -0
  161. package/src/otel.ts +39 -33
  162. package/src/protocol.ts +30 -3
  163. package/src/schema.ts +107 -56
  164. package/src/server.ts +196 -20
  165. package/src/types.ts +376 -18
  166. package/src/util/gzip.ts +63 -0
  167. package/src/util/schema.ts +4 -22
  168. package/src/util/web-crypto.ts +98 -0
  169. package/src/util/zstd.ts +133 -14
  170. package/src/wire/opaque.ts +37 -0
  171. package/src/wire/reader.ts +5 -4
  172. package/src/wire/request.ts +67 -8
  173. package/src/wire/response.ts +51 -85
  174. package/src/wire/writer.ts +165 -69
  175. package/dist/util/conform.d.ts +0 -18
  176. package/dist/util/conform.d.ts.map +0 -1
  177. 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 { RecordBatch, recordBatchFromArrays, type Schema } from "@query-farm/apache-arrow";
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: RecordBatch, out: OutputCollector) => Promise<void> | void;
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
- paramsSchema: Schema;
46
- resultSchema: Schema;
47
- outputSchema?: Schema;
48
- inputSchema?: Schema;
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
- headerSchema?: Schema;
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: RecordBatch;
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: Schema;
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: Schema,
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
- get outputSchema(): Schema {
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 RecordBatch as the data batch for this call. */
141
- emit(batch: RecordBatch, metadata?: Map<string, string>): void;
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: RecordBatch | Record<string, any[]>, metadata?: Map<string, string>): void {
145
- let batch: RecordBatch;
146
- if (batchOrColumns instanceof RecordBatch) {
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
- batch = recordBatchFromArrays(coerced, this._outputSchema);
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");
@@ -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
+ }
@@ -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 { RecordBatchStreamWriter, type Schema } from "@query-farm/apache-arrow";
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: Schema): Uint8Array {
24
- // Write a complete IPC stream with no batches.
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
  }