@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.
Files changed (160) hide show
  1. package/dist/access-log.d.ts +50 -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/client/capabilities.d.ts +25 -0
  16. package/dist/client/capabilities.d.ts.map +1 -0
  17. package/dist/client/connect.d.ts.map +1 -1
  18. package/dist/client/introspect.d.ts +7 -0
  19. package/dist/client/introspect.d.ts.map +1 -1
  20. package/dist/client/ipc.d.ts +8 -2
  21. package/dist/client/ipc.d.ts.map +1 -1
  22. package/dist/client/pipe.d.ts.map +1 -1
  23. package/dist/client/stream.d.ts +11 -2
  24. package/dist/client/stream.d.ts.map +1 -1
  25. package/dist/client/uploadUrl.d.ts +25 -0
  26. package/dist/client/uploadUrl.d.ts.map +1 -0
  27. package/dist/constants.d.ts +15 -1
  28. package/dist/constants.d.ts.map +1 -1
  29. package/dist/crypto.d.ts +22 -0
  30. package/dist/crypto.d.ts.map +1 -0
  31. package/dist/dispatch/describe.d.ts +10 -6
  32. package/dist/dispatch/describe.d.ts.map +1 -1
  33. package/dist/dispatch/stream.d.ts +2 -2
  34. package/dist/dispatch/stream.d.ts.map +1 -1
  35. package/dist/dispatch/unary.d.ts +2 -2
  36. package/dist/dispatch/unary.d.ts.map +1 -1
  37. package/dist/errors.d.ts +46 -0
  38. package/dist/errors.d.ts.map +1 -1
  39. package/dist/external.d.ts +25 -5
  40. package/dist/external.d.ts.map +1 -1
  41. package/dist/http/bearer.d.ts.map +1 -1
  42. package/dist/http/common.d.ts +42 -7
  43. package/dist/http/common.d.ts.map +1 -1
  44. package/dist/http/dispatch.d.ts +20 -2
  45. package/dist/http/dispatch.d.ts.map +1 -1
  46. package/dist/http/handler.d.ts.map +1 -1
  47. package/dist/http/index.d.ts +1 -0
  48. package/dist/http/index.d.ts.map +1 -1
  49. package/dist/http/mtls.d.ts +2 -1
  50. package/dist/http/mtls.d.ts.map +1 -1
  51. package/dist/http/oauth-pkce.d.ts +141 -0
  52. package/dist/http/oauth-pkce.d.ts.map +1 -0
  53. package/dist/http/pages.d.ts +3 -0
  54. package/dist/http/pages.d.ts.map +1 -1
  55. package/dist/http/sticky.d.ts +124 -0
  56. package/dist/http/sticky.d.ts.map +1 -0
  57. package/dist/http/token.d.ts +38 -12
  58. package/dist/http/token.d.ts.map +1 -1
  59. package/dist/http/types.d.ts +66 -5
  60. package/dist/http/types.d.ts.map +1 -1
  61. package/dist/index.d.ts +6 -4
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1275 -3511
  64. package/dist/index.js.map +19 -37
  65. package/dist/launcher/hash.d.ts +22 -0
  66. package/dist/launcher/hash.d.ts.map +1 -0
  67. package/dist/launcher/index.d.ts +23 -0
  68. package/dist/launcher/index.d.ts.map +1 -0
  69. package/dist/launcher/launch.d.ts +27 -0
  70. package/dist/launcher/launch.d.ts.map +1 -0
  71. package/dist/launcher/lock.d.ts +19 -0
  72. package/dist/launcher/lock.d.ts.map +1 -0
  73. package/dist/launcher/serve-unix.d.ts +54 -0
  74. package/dist/launcher/serve-unix.d.ts.map +1 -0
  75. package/dist/launcher/state.d.ts +59 -0
  76. package/dist/launcher/state.d.ts.map +1 -0
  77. package/dist/otel.d.ts.map +1 -1
  78. package/dist/protocol.d.ts +16 -2
  79. package/dist/protocol.d.ts.map +1 -1
  80. package/dist/schema.d.ts +45 -18
  81. package/dist/schema.d.ts.map +1 -1
  82. package/dist/server.d.ts +23 -2
  83. package/dist/server.d.ts.map +1 -1
  84. package/dist/types.d.ts +216 -12
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/util/gzip.d.ts +10 -0
  87. package/dist/util/gzip.d.ts.map +1 -0
  88. package/dist/util/schema.d.ts +3 -15
  89. package/dist/util/schema.d.ts.map +1 -1
  90. package/dist/util/web-crypto.d.ts +22 -0
  91. package/dist/util/web-crypto.d.ts.map +1 -0
  92. package/dist/util/zstd.d.ts +26 -3
  93. package/dist/util/zstd.d.ts.map +1 -1
  94. package/dist/wire/opaque.d.ts +11 -0
  95. package/dist/wire/opaque.d.ts.map +1 -0
  96. package/dist/wire/reader.d.ts +5 -5
  97. package/dist/wire/reader.d.ts.map +1 -1
  98. package/dist/wire/request.d.ts +11 -3
  99. package/dist/wire/request.d.ts.map +1 -1
  100. package/dist/wire/response.d.ts +6 -6
  101. package/dist/wire/response.d.ts.map +1 -1
  102. package/dist/wire/writer.d.ts +49 -39
  103. package/dist/wire/writer.d.ts.map +1 -1
  104. package/package.json +24 -10
  105. package/src/access-log.ts +195 -0
  106. package/src/arrow/impl-arrowjs/index.ts +433 -0
  107. package/src/arrow/impl-flechette/index.ts +414 -0
  108. package/src/arrow/impl-flechette/message-meta.ts +174 -0
  109. package/src/arrow/index.ts +89 -0
  110. package/src/arrow/predicates.ts +56 -0
  111. package/src/arrow/types.ts +73 -0
  112. package/src/client/capabilities.ts +84 -0
  113. package/src/client/connect.ts +103 -26
  114. package/src/client/introspect.ts +60 -38
  115. package/src/client/ipc.ts +37 -27
  116. package/src/client/pipe.ts +12 -9
  117. package/src/client/stream.ts +34 -19
  118. package/src/client/uploadUrl.ts +169 -0
  119. package/src/constants.ts +18 -1
  120. package/src/crypto.ts +95 -0
  121. package/src/dispatch/describe.ts +146 -107
  122. package/src/dispatch/stream.ts +53 -24
  123. package/src/dispatch/unary.ts +5 -4
  124. package/src/errors.ts +76 -0
  125. package/src/external.ts +43 -29
  126. package/src/http/bearer.ts +2 -5
  127. package/src/http/common.ts +90 -23
  128. package/src/http/dispatch.ts +373 -46
  129. package/src/http/handler.ts +790 -68
  130. package/src/http/index.ts +1 -0
  131. package/src/http/mtls.ts +18 -3
  132. package/src/http/oauth-pkce.ts +1035 -0
  133. package/src/http/pages.ts +30 -15
  134. package/src/http/sticky.ts +429 -0
  135. package/src/http/token.ts +165 -75
  136. package/src/http/types.ts +67 -5
  137. package/src/index.ts +40 -1
  138. package/src/launcher/hash.ts +104 -0
  139. package/src/launcher/index.ts +35 -0
  140. package/src/launcher/launch.ts +284 -0
  141. package/src/launcher/lock.ts +171 -0
  142. package/src/launcher/serve-unix.ts +385 -0
  143. package/src/launcher/state.ts +245 -0
  144. package/src/otel.ts +39 -33
  145. package/src/protocol.ts +27 -3
  146. package/src/schema.ts +107 -56
  147. package/src/server.ts +196 -20
  148. package/src/types.ts +322 -18
  149. package/src/util/gzip.ts +63 -0
  150. package/src/util/schema.ts +4 -22
  151. package/src/util/web-crypto.ts +98 -0
  152. package/src/util/zstd.ts +133 -14
  153. package/src/wire/opaque.ts +37 -0
  154. package/src/wire/reader.ts +5 -4
  155. package/src/wire/request.ts +67 -8
  156. package/src/wire/response.ts +51 -85
  157. package/src/wire/writer.ts +165 -69
  158. package/dist/util/conform.d.ts +0 -18
  159. package/dist/util/conform.d.ts.map +0 -1
  160. package/src/util/conform.ts +0 -94
package/src/server.ts CHANGED
@@ -1,21 +1,44 @@
1
1
  // © Copyright 2025-2026, Query.Farm LLC - https://query.farm
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import { Schema } from "@query-farm/apache-arrow";
5
- import { DESCRIBE_METHOD_NAME } from "./constants.js";
4
+ import { schema as makeSchema, serializeBatch } from "./arrow/index.js";
5
+ import { DESCRIBE_METHOD_NAME, PROTOCOL_VERSION_KEY } from "./constants.js";
6
6
  import { buildDescribeBatch } from "./dispatch/describe.js";
7
7
  import { dispatchStream } from "./dispatch/stream.js";
8
8
  import { dispatchUnary } from "./dispatch/unary.js";
9
- import { RpcError, VersionError } from "./errors.js";
9
+ import {
10
+ MethodNotImplementedError,
11
+ ProtocolVersionError,
12
+ parseProtocolVersion,
13
+ RpcError,
14
+ VersionError,
15
+ } from "./errors.js";
10
16
  import type { ExternalLocationConfig } from "./external.js";
11
17
  import type { Protocol } from "./protocol.js";
12
- import { type CallStatistics, type DispatchHook, type DispatchInfo, MethodType } from "./types.js";
18
+ import {
19
+ type CallStatistics,
20
+ type DispatchHook,
21
+ type DispatchInfo,
22
+ MethodType,
23
+ type ServeStartHook,
24
+ TransportKind,
25
+ } from "./types.js";
13
26
  import { IpcStreamReader } from "./wire/reader.js";
14
- import { parseRequest } from "./wire/request.js";
27
+ import { applyDefaults, parseRequest } from "./wire/request.js";
15
28
  import { buildErrorBatch } from "./wire/response.js";
16
29
  import { IpcStreamWriter } from "./wire/writer.js";
17
30
 
18
- const EMPTY_SCHEMA = new Schema([]);
31
+ const EMPTY_SCHEMA = makeSchema([]);
32
+
33
+ function randomStreamId(): string {
34
+ const bytes = new Uint8Array(16);
35
+ crypto.getRandomValues(bytes);
36
+ let out = "";
37
+ for (let i = 0; i < bytes.length; i++) {
38
+ out += bytes[i].toString(16).padStart(2, "0");
39
+ }
40
+ return out;
41
+ }
19
42
 
20
43
  /**
21
44
  * RPC server that reads Arrow IPC requests from stdin and writes responses to stdout.
@@ -25,9 +48,22 @@ export class VgiRpcServer {
25
48
  private protocol: Protocol;
26
49
  private enableDescribe: boolean;
27
50
  private serverId: string;
28
- private describeBatch: import("@query-farm/apache-arrow").RecordBatch | null = null;
51
+ // Lazily initialized `buildDescribeBatch` is async because the protocol
52
+ // hash is computed via `crypto.subtle.digest` (Web Crypto). The dispatch
53
+ // path awaits the cached promise on first use.
54
+ private _describePromise: Promise<{
55
+ batch: import("./arrow/index.js").VgiBatch;
56
+ protocolHash: string;
57
+ }> | null = null;
58
+ private protocolVersion: string;
29
59
  private dispatchHook: DispatchHook | null = null;
30
60
  private externalConfig: ExternalLocationConfig | undefined;
61
+ private onServeStart: ServeStartHook | null = null;
62
+ /** True once the on_serve_start hook has fired successfully. The bind
63
+ * state is committed only after the hook returns, so a transient
64
+ * failure on first request leaves it `false` and the next request
65
+ * re-fires rather than silently skipping. Mirrors Python 7b3999c. */
66
+ private serveStartFired = false;
31
67
 
32
68
  constructor(
33
69
  protocol: Protocol,
@@ -36,6 +72,9 @@ export class VgiRpcServer {
36
72
  serverId?: string;
37
73
  dispatchHook?: DispatchHook;
38
74
  externalLocation?: ExternalLocationConfig;
75
+ protocolVersion?: string;
76
+ /** Lifecycle hook fired once before the first dispatched request. */
77
+ onServeStart?: ServeStartHook;
39
78
  },
40
79
  ) {
41
80
  this.protocol = protocol;
@@ -43,11 +82,83 @@ export class VgiRpcServer {
43
82
  this.serverId = options?.serverId ?? crypto.randomUUID().replace(/-/g, "").slice(0, 12);
44
83
  this.dispatchHook = options?.dispatchHook ?? null;
45
84
  this.externalConfig = options?.externalLocation;
85
+ this.protocolVersion = options?.protocolVersion ?? "";
86
+ this.onServeStart = options?.onServeStart ?? null;
87
+ }
46
88
 
47
- if (this.enableDescribe) {
48
- const { batch } = buildDescribeBatch(protocol.name, protocol.getMethods(), this.serverId);
49
- this.describeBatch = batch;
89
+ /** Fire the on_serve_start hook once for this transport. Idempotent
90
+ * on success re-throws on failure without committing the bind. */
91
+ private async notifyTransport(kind: TransportKind): Promise<void> {
92
+ if (this.serveStartFired) return;
93
+ if (this.onServeStart) {
94
+ await this.onServeStart(kind);
50
95
  }
96
+ this.serveStartFired = true;
97
+ }
98
+
99
+ /** Build (or retrieve cached) describe batch + protocol hash. */
100
+ private async describeInfo(): Promise<{
101
+ batch: import("./arrow/index.js").VgiBatch;
102
+ protocolHash: string;
103
+ }> {
104
+ if (!this._describePromise) {
105
+ this._describePromise = buildDescribeBatch(
106
+ this.protocol.name,
107
+ this.protocol.getMethods(),
108
+ this.serverId,
109
+ this.protocol.protocolVersion || undefined,
110
+ ).then(({ batch, metadata }) => ({
111
+ batch,
112
+ protocolHash: metadata.get("vgi_rpc.protocol_hash") ?? "",
113
+ }));
114
+ }
115
+ return this._describePromise;
116
+ }
117
+
118
+ /** Validate a client's declared protocol_version against the Protocol's
119
+ * declared version. Caller invokes only when
120
+ * `protocol.protocolVersionParts` is non-null. Mirrors Python's
121
+ * `RpcServer._check_protocol_version`: exact major+minor match, patch
122
+ * ignored; directional error message names which side is older. */
123
+ private checkProtocolVersion(clientVersion: string | undefined): void {
124
+ const serverParts = this.protocol.protocolVersionParts!;
125
+ const serverVersion = this.protocol.protocolVersion;
126
+ if (clientVersion === undefined) {
127
+ throw new ProtocolVersionError(
128
+ "VGI client/worker protocol_version mismatch.\n" +
129
+ " Client: <not declared>\n" +
130
+ ` Server: ${serverVersion}\n` +
131
+ " Direction: the client did not send a vgi_rpc.protocol_version " +
132
+ "metadata key. This is either a vgi-rpc framework bug or a " +
133
+ "non-VGI client connecting to a VGI worker.",
134
+ );
135
+ }
136
+ let clientParts: readonly [number, number, number];
137
+ try {
138
+ clientParts = parseProtocolVersion(clientVersion);
139
+ } catch {
140
+ throw new ProtocolVersionError(
141
+ "VGI client/worker protocol_version mismatch.\n" +
142
+ ` Client: ${clientVersion}\n` +
143
+ ` Server: ${serverVersion}\n` +
144
+ " Direction: client sent a malformed protocol_version. " +
145
+ "Expected canonical semver MAJOR.MINOR.PATCH.",
146
+ );
147
+ }
148
+ if (clientParts[0] === serverParts[0] && clientParts[1] === serverParts[1]) {
149
+ return;
150
+ }
151
+ const clientOlder =
152
+ clientParts[0] < serverParts[0] || (clientParts[0] === serverParts[0] && clientParts[1] < serverParts[1]);
153
+ const direction = clientOlder
154
+ ? `client is too old; upgrade the VGI extension/client to a version supporting protocol_version ${serverVersion}.`
155
+ : `server is too old; upgrade the VGI worker to a version supporting protocol_version ${clientVersion}.`;
156
+ throw new ProtocolVersionError(
157
+ "VGI client/worker protocol_version mismatch.\n" +
158
+ ` Client: ${clientVersion}\n` +
159
+ ` Server: ${serverVersion}\n` +
160
+ ` Direction: ${direction}`,
161
+ );
51
162
  }
52
163
 
53
164
  /** Start the server loop. Reads requests until stdin closes. */
@@ -69,6 +180,11 @@ export class VgiRpcServer {
69
180
 
70
181
  try {
71
182
  while (true) {
183
+ // Fire on_serve_start lazily so the hook can do work that
184
+ // depends on the transport binding (matches Python which fires
185
+ // it inside serve()). Inside the loop so a failure on the very
186
+ // first request can be retried.
187
+ await this.notifyTransport(TransportKind.PIPE);
72
188
  await this.serveOne(reader, writer);
73
189
  }
74
190
  } catch (e: any) {
@@ -101,7 +217,7 @@ export class VgiRpcServer {
101
217
  if (batches.length === 0) {
102
218
  const err = new RpcError("ProtocolError", "Request stream contains no batches", "");
103
219
  const errBatch = buildErrorBatch(EMPTY_SCHEMA, err, this.serverId, null);
104
- writer.writeStream(EMPTY_SCHEMA, [errBatch]);
220
+ await writer.writeStream(EMPTY_SCHEMA, [errBatch]);
105
221
  return;
106
222
  }
107
223
 
@@ -118,16 +234,17 @@ export class VgiRpcServer {
118
234
  } catch (e: any) {
119
235
  // Write error response for protocol/version errors
120
236
  const errBatch = buildErrorBatch(EMPTY_SCHEMA, e, this.serverId, null);
121
- writer.writeStream(EMPTY_SCHEMA, [errBatch]);
237
+ await writer.writeStream(EMPTY_SCHEMA, [errBatch]);
122
238
  if (e instanceof VersionError || e instanceof RpcError) {
123
239
  return; // Continue serving
124
240
  }
125
241
  throw e;
126
242
  }
127
243
 
128
- // Handle __describe__
129
- if (methodName === DESCRIBE_METHOD_NAME && this.describeBatch) {
130
- writer.writeStream(this.describeBatch.schema, [this.describeBatch]);
244
+ // Handle __describe__ — lazy-build on first request.
245
+ if (methodName === DESCRIBE_METHOD_NAME && this.enableDescribe) {
246
+ const { batch } = await this.describeInfo();
247
+ await writer.writeStream(batch.schema, [batch]);
131
248
  return;
132
249
  }
133
250
 
@@ -136,15 +253,63 @@ export class VgiRpcServer {
136
253
  const method = methods.get(methodName);
137
254
  if (!method) {
138
255
  const available = [...methods.keys()].sort();
139
- const err = new Error(`Unknown method: '${methodName}'. Available methods: [${available.join(", ")}]`);
256
+ const err = new MethodNotImplementedError(
257
+ `Unknown method: '${methodName}'. Available methods: [${available.join(", ")}]`,
258
+ );
140
259
  const errBatch = buildErrorBatch(EMPTY_SCHEMA, err, this.serverId, requestId);
141
- writer.writeStream(EMPTY_SCHEMA, [errBatch]);
260
+ await writer.writeStream(EMPTY_SCHEMA, [errBatch]);
142
261
  return;
143
262
  }
144
263
 
264
+ // Application-protocol-version gate. Fires only when the Protocol
265
+ // declared a `protocolVersion`. `__describe__` is exempt — it is the
266
+ // diagnostic path a mismatched client uses to introspect the server's
267
+ // version. Mirrors Python's serve_one dispatch-boundary check.
268
+ if (this.protocol.protocolVersionParts !== null) {
269
+ try {
270
+ const md = batch.metadata;
271
+ this.checkProtocolVersion(md?.get(PROTOCOL_VERSION_KEY));
272
+ } catch (exc) {
273
+ const errSchema = method.type === MethodType.UNARY ? method.resultSchema : EMPTY_SCHEMA;
274
+ const errBatch = buildErrorBatch(errSchema, exc as Error, this.serverId, requestId);
275
+ await writer.writeStream(errSchema, [errBatch]);
276
+ return;
277
+ }
278
+ }
279
+
145
280
  // Dispatch based on method type, with optional hook
146
281
  const methodType = method.type === MethodType.UNARY ? "unary" : "stream";
147
- const info: DispatchInfo = { method: methodName, methodType, serverId: this.serverId, requestId };
282
+
283
+ // Capture self-contained IPC bytes of the request batch for the access log.
284
+ let requestData: Uint8Array | undefined;
285
+ try {
286
+ requestData = serializeBatch(batch as any);
287
+ } catch {
288
+ // best-effort; observability must not fail dispatch
289
+ }
290
+
291
+ let streamId: string | undefined;
292
+ if (methodType === "stream") {
293
+ streamId = randomStreamId();
294
+ }
295
+
296
+ const { protocolHash } = await this.describeInfo();
297
+ const info: DispatchInfo = {
298
+ method: methodName,
299
+ methodType,
300
+ serverId: this.serverId,
301
+ requestId,
302
+ protocol: this.protocol.name,
303
+ protocolHash,
304
+ protocolVersion: this.protocolVersion,
305
+ kind: TransportKind.PIPE,
306
+ principal: "",
307
+ authDomain: "",
308
+ authenticated: false,
309
+ remoteAddr: "",
310
+ requestData,
311
+ streamId,
312
+ };
148
313
  const stats: CallStatistics = {
149
314
  inputBatches: 0,
150
315
  outputBatches: 0,
@@ -157,11 +322,22 @@ export class VgiRpcServer {
157
322
  const token = this.dispatchHook?.onDispatchStart(info);
158
323
  let dispatchError: Error | undefined;
159
324
 
325
+ applyDefaults(params, method.defaults);
326
+
160
327
  try {
161
328
  if (method.type === MethodType.UNARY) {
162
- await dispatchUnary(method, params, writer, this.serverId, requestId, this.externalConfig);
329
+ await dispatchUnary(method, params, writer, this.serverId, requestId, this.externalConfig, TransportKind.PIPE);
163
330
  } else {
164
- await dispatchStream(method, params, writer, reader, this.serverId, requestId, this.externalConfig);
331
+ await dispatchStream(
332
+ method,
333
+ params,
334
+ writer,
335
+ reader,
336
+ this.serverId,
337
+ requestId,
338
+ this.externalConfig,
339
+ TransportKind.PIPE,
340
+ );
165
341
  }
166
342
  } catch (e) {
167
343
  dispatchError = e instanceof Error ? e : new Error(String(e));