@query-farm/vgi-rpc 0.3.4 → 0.6.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 (87) hide show
  1. package/README.md +47 -0
  2. package/dist/auth.d.ts +13 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/client/connect.d.ts.map +1 -1
  5. package/dist/client/index.d.ts +2 -0
  6. package/dist/client/index.d.ts.map +1 -1
  7. package/dist/client/introspect.d.ts +1 -0
  8. package/dist/client/introspect.d.ts.map +1 -1
  9. package/dist/client/oauth.d.ts +62 -0
  10. package/dist/client/oauth.d.ts.map +1 -0
  11. package/dist/client/pipe.d.ts +3 -0
  12. package/dist/client/pipe.d.ts.map +1 -1
  13. package/dist/client/stream.d.ts +5 -0
  14. package/dist/client/stream.d.ts.map +1 -1
  15. package/dist/client/types.d.ts +6 -0
  16. package/dist/client/types.d.ts.map +1 -1
  17. package/dist/constants.d.ts +3 -1
  18. package/dist/constants.d.ts.map +1 -1
  19. package/dist/dispatch/describe.d.ts.map +1 -1
  20. package/dist/dispatch/stream.d.ts +2 -1
  21. package/dist/dispatch/stream.d.ts.map +1 -1
  22. package/dist/dispatch/unary.d.ts +2 -1
  23. package/dist/dispatch/unary.d.ts.map +1 -1
  24. package/dist/external.d.ts +45 -0
  25. package/dist/external.d.ts.map +1 -0
  26. package/dist/gcs.d.ts +38 -0
  27. package/dist/gcs.d.ts.map +1 -0
  28. package/dist/http/auth.d.ts +32 -0
  29. package/dist/http/auth.d.ts.map +1 -0
  30. package/dist/http/bearer.d.ts +34 -0
  31. package/dist/http/bearer.d.ts.map +1 -0
  32. package/dist/http/dispatch.d.ts +4 -0
  33. package/dist/http/dispatch.d.ts.map +1 -1
  34. package/dist/http/handler.d.ts.map +1 -1
  35. package/dist/http/index.d.ts +8 -0
  36. package/dist/http/index.d.ts.map +1 -1
  37. package/dist/http/jwt.d.ts +21 -0
  38. package/dist/http/jwt.d.ts.map +1 -0
  39. package/dist/http/mtls.d.ts +78 -0
  40. package/dist/http/mtls.d.ts.map +1 -0
  41. package/dist/http/pages.d.ts +9 -0
  42. package/dist/http/pages.d.ts.map +1 -0
  43. package/dist/http/types.d.ts +22 -1
  44. package/dist/http/types.d.ts.map +1 -1
  45. package/dist/index.d.ts +4 -2
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +2576 -317
  48. package/dist/index.js.map +27 -18
  49. package/dist/otel.d.ts +47 -0
  50. package/dist/otel.d.ts.map +1 -0
  51. package/dist/s3.d.ts +43 -0
  52. package/dist/s3.d.ts.map +1 -0
  53. package/dist/server.d.ts +6 -0
  54. package/dist/server.d.ts.map +1 -1
  55. package/dist/types.d.ts +38 -2
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/wire/response.d.ts.map +1 -1
  58. package/package.json +46 -2
  59. package/src/auth.ts +31 -0
  60. package/src/client/connect.ts +28 -6
  61. package/src/client/index.ts +11 -0
  62. package/src/client/introspect.ts +15 -3
  63. package/src/client/oauth.ts +167 -0
  64. package/src/client/pipe.ts +19 -4
  65. package/src/client/stream.ts +32 -7
  66. package/src/client/types.ts +6 -0
  67. package/src/constants.ts +4 -1
  68. package/src/dispatch/describe.ts +20 -0
  69. package/src/dispatch/stream.ts +18 -4
  70. package/src/dispatch/unary.ts +6 -1
  71. package/src/external.ts +209 -0
  72. package/src/gcs.ts +86 -0
  73. package/src/http/auth.ts +110 -0
  74. package/src/http/bearer.ts +107 -0
  75. package/src/http/dispatch.ts +32 -10
  76. package/src/http/handler.ts +120 -3
  77. package/src/http/index.ts +14 -0
  78. package/src/http/jwt.ts +80 -0
  79. package/src/http/mtls.ts +298 -0
  80. package/src/http/pages.ts +298 -0
  81. package/src/http/types.ts +23 -1
  82. package/src/index.ts +32 -0
  83. package/src/otel.ts +161 -0
  84. package/src/s3.ts +94 -0
  85. package/src/server.ts +42 -8
  86. package/src/types.ts +51 -3
  87. package/src/wire/response.ts +28 -14
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // © Copyright 2025-2026, Query.Farm LLC - https://query.farm
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
+ export { AuthContext } from "./auth.js";
4
5
  export * from "./client/index.js";
5
6
  export {
6
7
  DESCRIBE_METHOD_NAME,
@@ -18,14 +19,40 @@ export {
18
19
  STATE_KEY,
19
20
  } from "./constants.js";
20
21
  export { RpcError, VersionError } from "./errors.js";
22
+ export {
23
+ type ExternalLocationConfig,
24
+ type ExternalStorage,
25
+ httpsOnlyValidator,
26
+ isExternalLocationBatch,
27
+ makeExternalLocationBatch,
28
+ maybeExternalizeBatch,
29
+ resolveExternalLocation,
30
+ } from "./external.js";
21
31
  export {
22
32
  ARROW_CONTENT_TYPE,
33
+ type AuthenticateFn,
34
+ type BearerValidateFn,
35
+ bearerAuthenticate,
36
+ bearerAuthenticateStatic,
37
+ type CertValidateFn,
38
+ chainAuthenticate,
23
39
  createHttpHandler,
24
40
  type HttpHandlerOptions,
41
+ type JwtAuthenticateOptions,
25
42
  jsonStateSerializer,
43
+ jwtAuthenticate,
44
+ mtlsAuthenticate,
45
+ mtlsAuthenticateFingerprint,
46
+ mtlsAuthenticateSubject,
47
+ mtlsAuthenticateXfcc,
48
+ type OAuthResourceMetadata,
49
+ oauthResourceMetadataToJson,
50
+ parseXfcc,
26
51
  type StateSerializer,
27
52
  type UnpackedToken,
28
53
  unpackStateToken,
54
+ type XfccElement,
55
+ type XfccValidateFn,
29
56
  } from "./http/index.js";
30
57
  export { Protocol } from "./protocol.js";
31
58
  export {
@@ -42,9 +69,14 @@ export {
42
69
  } from "./schema.js";
43
70
  export { VgiRpcServer } from "./server.js";
44
71
  export {
72
+ type CallContext,
73
+ type CallStatistics,
74
+ type DispatchHook,
75
+ type DispatchInfo,
45
76
  type ExchangeFn,
46
77
  type ExchangeInit,
47
78
  type HeaderInit,
79
+ type HookToken,
48
80
  type LogContext,
49
81
  type MethodDefinition,
50
82
  MethodType,
package/src/otel.ts ADDED
@@ -0,0 +1,161 @@
1
+ // © Copyright 2025-2026, Query.Farm LLC - https://query.farm
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * OpenTelemetry instrumentation for vgi-rpc TypeScript servers.
6
+ *
7
+ * Implements {@link DispatchHook} to add distributed tracing (spans) and
8
+ * metrics (request counter, duration histogram) to RPC dispatch.
9
+ *
10
+ * Requires `@opentelemetry/api` as a peer dependency.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { createOtelHook } from "vgi-rpc/otel";
15
+ * import { createHttpHandler } from "vgi-rpc";
16
+ *
17
+ * const handler = createHttpHandler(protocol, {
18
+ * dispatchHook: createOtelHook(),
19
+ * });
20
+ * ```
21
+ */
22
+
23
+ import {
24
+ type Attributes,
25
+ type Counter,
26
+ type Histogram,
27
+ type Meter,
28
+ metrics,
29
+ type Span,
30
+ SpanKind,
31
+ SpanStatusCode,
32
+ type Tracer,
33
+ trace,
34
+ } from "@opentelemetry/api";
35
+ import type { CallStatistics, DispatchHook, DispatchInfo, HookToken } from "./types.js";
36
+
37
+ const INSTRUMENTATION_NAME = "vgi_rpc";
38
+
39
+ /** Configuration for OpenTelemetry instrumentation. */
40
+ export interface OtelConfig {
41
+ /** Custom TracerProvider; uses the global provider when omitted. */
42
+ tracerProvider?: { getTracer(name: string): Tracer };
43
+ /** Custom MeterProvider; uses the global provider when omitted. */
44
+ meterProvider?: { getMeter(name: string): Meter };
45
+ /** Enable span creation. Default: true. */
46
+ enableTracing?: boolean;
47
+ /** Enable counter/histogram recording. Default: true. */
48
+ enableMetrics?: boolean;
49
+ /** Record exceptions on error spans. Default: true. */
50
+ recordExceptions?: boolean;
51
+ /** Service name for the rpc.service attribute. Default: "TypeScriptRpcServer". */
52
+ serviceName?: string;
53
+ }
54
+
55
+ interface OtelHookToken {
56
+ span: Span | null;
57
+ startTime: number;
58
+ }
59
+
60
+ /**
61
+ * Create a {@link DispatchHook} that instruments RPC calls with OpenTelemetry.
62
+ *
63
+ * Creates a span for each RPC call with method attributes, and records
64
+ * request count and duration metrics.
65
+ */
66
+ export function createOtelHook(config?: OtelConfig): DispatchHook {
67
+ const enableTracing = config?.enableTracing ?? true;
68
+ const enableMetrics = config?.enableMetrics ?? true;
69
+ const recordExceptions = config?.recordExceptions ?? true;
70
+ const serviceName = config?.serviceName ?? "TypeScriptRpcServer";
71
+
72
+ const tracer = (config?.tracerProvider ?? trace).getTracer(INSTRUMENTATION_NAME);
73
+
74
+ let requestCounter: Counter | null = null;
75
+ let durationHistogram: Histogram | null = null;
76
+
77
+ if (enableMetrics) {
78
+ const meter = (config?.meterProvider ?? metrics).getMeter(INSTRUMENTATION_NAME);
79
+ requestCounter = meter.createCounter("rpc.server.requests", {
80
+ unit: "{request}",
81
+ description: "Number of RPC requests handled",
82
+ });
83
+ durationHistogram = meter.createHistogram("rpc.server.duration", {
84
+ unit: "s",
85
+ description: "Duration of RPC requests",
86
+ });
87
+ }
88
+
89
+ return {
90
+ onDispatchStart(info: DispatchInfo): HookToken {
91
+ const startTime = performance.now();
92
+
93
+ if (!enableTracing) {
94
+ return { span: null, startTime } satisfies OtelHookToken;
95
+ }
96
+
97
+ const spanName = `vgi_rpc/${info.method}`;
98
+ const attrs: Attributes = {
99
+ "rpc.system": "vgi_rpc",
100
+ "rpc.service": serviceName,
101
+ "rpc.method": info.method,
102
+ "rpc.vgi_rpc.method_type": info.methodType,
103
+ "rpc.vgi_rpc.server_id": info.serverId,
104
+ };
105
+ if (info.requestId) {
106
+ attrs["rpc.vgi_rpc.request_id"] = info.requestId;
107
+ }
108
+
109
+ const span = tracer.startSpan(spanName, {
110
+ kind: SpanKind.SERVER,
111
+ attributes: attrs,
112
+ });
113
+
114
+ return { span, startTime } satisfies OtelHookToken;
115
+ },
116
+
117
+ onDispatchEnd(token: HookToken, info: DispatchInfo, stats: CallStatistics, error?: Error): void {
118
+ const t = token as OtelHookToken;
119
+ const durationS = (performance.now() - t.startTime) / 1000;
120
+ const status = error ? "error" : "ok";
121
+
122
+ // Finalize span
123
+ if (t.span) {
124
+ if (stats) {
125
+ t.span.setAttributes({
126
+ "rpc.vgi_rpc.input_batches": stats.inputBatches,
127
+ "rpc.vgi_rpc.output_batches": stats.outputBatches,
128
+ "rpc.vgi_rpc.input_rows": stats.inputRows,
129
+ "rpc.vgi_rpc.output_rows": stats.outputRows,
130
+ "rpc.vgi_rpc.input_bytes": stats.inputBytes,
131
+ "rpc.vgi_rpc.output_bytes": stats.outputBytes,
132
+ });
133
+ }
134
+
135
+ if (error) {
136
+ t.span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
137
+ t.span.setAttribute("rpc.vgi_rpc.error_type", error.constructor.name);
138
+ if (recordExceptions) {
139
+ t.span.recordException(error);
140
+ }
141
+ } else {
142
+ t.span.setStatus({ code: SpanStatusCode.OK });
143
+ }
144
+ t.span.end();
145
+ }
146
+
147
+ // Record metrics
148
+ if (enableMetrics) {
149
+ const metricAttrs: Attributes = {
150
+ "rpc.system": "vgi_rpc",
151
+ "rpc.service": serviceName,
152
+ "rpc.method": info.method,
153
+ "rpc.vgi_rpc.method_type": info.methodType,
154
+ status,
155
+ };
156
+ requestCounter?.add(1, metricAttrs);
157
+ durationHistogram?.record(durationS, metricAttrs);
158
+ }
159
+ },
160
+ };
161
+ }
package/src/s3.ts ADDED
@@ -0,0 +1,94 @@
1
+ // © Copyright 2025-2026, Query.Farm LLC - https://query.farm
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * S3 storage backend for external storage of large Arrow IPC batches.
6
+ *
7
+ * Requires `@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner`
8
+ * as peer dependencies.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { createS3Storage } from "@query-farm/vgi-rpc/s3";
13
+ *
14
+ * const storage = createS3Storage({
15
+ * bucket: "my-bucket",
16
+ * prefix: "vgi-rpc/",
17
+ * });
18
+ * const handler = createHttpHandler(protocol, {
19
+ * externalLocation: { storage, externalizeThresholdBytes: 1_048_576 },
20
+ * });
21
+ * ```
22
+ */
23
+
24
+ import type { ExternalStorage } from "./external.js";
25
+
26
+ /** Configuration for the S3 storage backend. */
27
+ export interface S3StorageConfig {
28
+ /** S3 bucket name. */
29
+ bucket: string;
30
+ /** Key prefix for uploaded objects. Default: "vgi-rpc/". */
31
+ prefix?: string;
32
+ /** Lifetime of pre-signed GET URLs in seconds. Default: 3600 (1 hour). */
33
+ presignExpirySeconds?: number;
34
+ /** AWS region. If omitted, uses default SDK config. */
35
+ region?: string;
36
+ /** Custom S3 endpoint URL (for MinIO, LocalStack, etc.). */
37
+ endpointUrl?: string;
38
+ /** Force path-style addressing (required for some S3-compatible services). */
39
+ forcePathStyle?: boolean;
40
+ }
41
+
42
+ /**
43
+ * Create an S3-backed ExternalStorage.
44
+ *
45
+ * Lazily imports `@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner`
46
+ * on first upload to avoid loading the AWS SDK unless needed.
47
+ */
48
+ export function createS3Storage(config: S3StorageConfig): ExternalStorage {
49
+ const bucket = config.bucket;
50
+ const prefix = config.prefix ?? "vgi-rpc/";
51
+ const presignExpiry = config.presignExpirySeconds ?? 3600;
52
+
53
+ // Lazy-loaded AWS SDK clients
54
+ let s3Client: any = null;
55
+
56
+ async function ensureClient(): Promise<any> {
57
+ if (s3Client) return s3Client;
58
+ const { S3Client } = await import("@aws-sdk/client-s3");
59
+ const clientConfig: Record<string, any> = {};
60
+ if (config.region) clientConfig.region = config.region;
61
+ if (config.endpointUrl) {
62
+ clientConfig.endpoint = config.endpointUrl;
63
+ clientConfig.forcePathStyle = config.forcePathStyle ?? true;
64
+ }
65
+ s3Client = new S3Client(clientConfig);
66
+ return s3Client;
67
+ }
68
+
69
+ return {
70
+ async upload(data: Uint8Array, contentEncoding: string): Promise<string> {
71
+ const client = await ensureClient();
72
+ const { PutObjectCommand } = await import("@aws-sdk/client-s3");
73
+ const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
74
+ const { GetObjectCommand } = await import("@aws-sdk/client-s3");
75
+
76
+ const key = `${prefix}${crypto.randomUUID()}${contentEncoding === "zstd" ? ".arrow.zst" : ".arrow"}`;
77
+
78
+ const putCommand = new PutObjectCommand({
79
+ Bucket: bucket,
80
+ Key: key,
81
+ Body: data,
82
+ ContentType: "application/vnd.apache.arrow.stream",
83
+ ...(contentEncoding ? { ContentEncoding: contentEncoding } : {}),
84
+ });
85
+
86
+ await client.send(putCommand);
87
+
88
+ // Generate pre-signed GET URL
89
+ const getCommand = new GetObjectCommand({ Bucket: bucket, Key: key });
90
+ const url = await getSignedUrl(client, getCommand, { expiresIn: presignExpiry });
91
+ return url;
92
+ },
93
+ };
94
+ }
package/src/server.ts CHANGED
@@ -7,8 +7,9 @@ import { buildDescribeBatch } from "./dispatch/describe.js";
7
7
  import { dispatchStream } from "./dispatch/stream.js";
8
8
  import { dispatchUnary } from "./dispatch/unary.js";
9
9
  import { RpcError, VersionError } from "./errors.js";
10
+ import type { ExternalLocationConfig } from "./external.js";
10
11
  import type { Protocol } from "./protocol.js";
11
- import { MethodType } from "./types.js";
12
+ import { type CallStatistics, type DispatchHook, type DispatchInfo, MethodType } from "./types.js";
12
13
  import { IpcStreamReader } from "./wire/reader.js";
13
14
  import { parseRequest } from "./wire/request.js";
14
15
  import { buildErrorBatch } from "./wire/response.js";
@@ -25,11 +26,23 @@ export class VgiRpcServer {
25
26
  private enableDescribe: boolean;
26
27
  private serverId: string;
27
28
  private describeBatch: import("@query-farm/apache-arrow").RecordBatch | null = null;
28
-
29
- constructor(protocol: Protocol, options?: { enableDescribe?: boolean; serverId?: string }) {
29
+ private dispatchHook: DispatchHook | null = null;
30
+ private externalConfig: ExternalLocationConfig | undefined;
31
+
32
+ constructor(
33
+ protocol: Protocol,
34
+ options?: {
35
+ enableDescribe?: boolean;
36
+ serverId?: string;
37
+ dispatchHook?: DispatchHook;
38
+ externalLocation?: ExternalLocationConfig;
39
+ },
40
+ ) {
30
41
  this.protocol = protocol;
31
42
  this.enableDescribe = options?.enableDescribe ?? true;
32
43
  this.serverId = options?.serverId ?? crypto.randomUUID().replace(/-/g, "").slice(0, 12);
44
+ this.dispatchHook = options?.dispatchHook ?? null;
45
+ this.externalConfig = options?.externalLocation;
33
46
 
34
47
  if (this.enableDescribe) {
35
48
  const { batch } = buildDescribeBatch(protocol.name, protocol.getMethods(), this.serverId);
@@ -129,11 +142,32 @@ export class VgiRpcServer {
129
142
  return;
130
143
  }
131
144
 
132
- // Dispatch based on method type
133
- if (method.type === MethodType.UNARY) {
134
- await dispatchUnary(method, params, writer, this.serverId, requestId);
135
- } else {
136
- await dispatchStream(method, params, writer, reader, this.serverId, requestId);
145
+ // Dispatch based on method type, with optional hook
146
+ const methodType = method.type === MethodType.UNARY ? "unary" : "stream";
147
+ const info: DispatchInfo = { method: methodName, methodType, serverId: this.serverId, requestId };
148
+ const stats: CallStatistics = {
149
+ inputBatches: 0,
150
+ outputBatches: 0,
151
+ inputRows: 0,
152
+ outputRows: 0,
153
+ inputBytes: 0,
154
+ outputBytes: 0,
155
+ };
156
+
157
+ const token = this.dispatchHook?.onDispatchStart(info);
158
+ let dispatchError: Error | undefined;
159
+
160
+ try {
161
+ if (method.type === MethodType.UNARY) {
162
+ await dispatchUnary(method, params, writer, this.serverId, requestId, this.externalConfig);
163
+ } else {
164
+ await dispatchStream(method, params, writer, reader, this.serverId, requestId, this.externalConfig);
165
+ }
166
+ } catch (e) {
167
+ dispatchError = e instanceof Error ? e : new Error(String(e));
168
+ throw e;
169
+ } finally {
170
+ this.dispatchHook?.onDispatchEnd(token, info, stats, dispatchError);
137
171
  }
138
172
  }
139
173
  }
package/src/types.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import { RecordBatch, recordBatchFromArrays, type Schema } from "@query-farm/apache-arrow";
5
+ import { AuthContext } from "./auth.js";
5
6
  import { buildLogBatch, coerceInt64 } from "./wire/response.js";
6
7
 
7
8
  export enum MethodType {
@@ -14,6 +15,11 @@ export interface LogContext {
14
15
  clientLog(level: string, message: string, extra?: Record<string, string>): void;
15
16
  }
16
17
 
18
+ /** Extended context with authentication info, available to handlers. */
19
+ export interface CallContext extends LogContext {
20
+ readonly auth: AuthContext;
21
+ }
22
+
17
23
  /** Handler for unary (request-response) RPC methods. */
18
24
  export type UnaryHandler = (
19
25
  params: Record<string, any>,
@@ -52,6 +58,40 @@ export interface MethodDefinition {
52
58
  paramTypes?: Record<string, string>;
53
59
  }
54
60
 
61
+ /** Metadata passed to dispatch hooks before and after RPC method execution. */
62
+ export interface DispatchInfo {
63
+ /** RPC method name. */
64
+ method: string;
65
+ /** "unary" or "stream". */
66
+ methodType: string;
67
+ /** Server identifier. */
68
+ serverId: string;
69
+ /** Client-supplied request identifier, or null. */
70
+ requestId: string | null;
71
+ }
72
+
73
+ /** Per-call I/O counters, matching Python's CallStatistics. */
74
+ export interface CallStatistics {
75
+ inputBatches: number;
76
+ outputBatches: number;
77
+ inputRows: number;
78
+ outputRows: number;
79
+ inputBytes: number;
80
+ outputBytes: number;
81
+ }
82
+
83
+ /** Opaque token returned by onDispatchStart, passed back to onDispatchEnd. */
84
+ export type HookToken = unknown;
85
+
86
+ /**
87
+ * Observability hook called around RPC dispatch.
88
+ * Implementations must be safe for concurrent use (HTTP transport is concurrent).
89
+ */
90
+ export interface DispatchHook {
91
+ onDispatchStart(info: DispatchInfo): HookToken;
92
+ onDispatchEnd(token: HookToken, info: DispatchInfo, stats: CallStatistics, error?: Error): void;
93
+ }
94
+
55
95
  export interface EmittedBatch {
56
96
  batch: RecordBatch;
57
97
  metadata?: Map<string, string>;
@@ -61,7 +101,7 @@ export interface EmittedBatch {
61
101
  * Accumulates output batches during a produce/exchange call.
62
102
  * Enforces that exactly one data batch is emitted per call (plus any number of log batches).
63
103
  */
64
- export class OutputCollector implements LogContext {
104
+ export class OutputCollector implements CallContext {
65
105
  private _batches: EmittedBatch[] = [];
66
106
  private _dataBatchIdx: number | null = null;
67
107
  private _finished = false;
@@ -69,12 +109,20 @@ export class OutputCollector implements LogContext {
69
109
  private _outputSchema: Schema;
70
110
  private _serverId: string;
71
111
  private _requestId: string | null;
72
-
73
- constructor(outputSchema: Schema, producerMode = true, serverId = "", requestId: string | null = null) {
112
+ readonly auth: AuthContext;
113
+
114
+ constructor(
115
+ outputSchema: Schema,
116
+ producerMode = true,
117
+ serverId = "",
118
+ requestId: string | null = null,
119
+ authContext?: AuthContext,
120
+ ) {
74
121
  this._outputSchema = outputSchema;
75
122
  this._producerMode = producerMode;
76
123
  this._serverId = serverId;
77
124
  this._requestId = requestId;
125
+ this.auth = authContext ?? AuthContext.anonymous();
78
126
  }
79
127
 
80
128
  get outputSchema(): Schema {
@@ -133,25 +133,39 @@ export function buildLogBatch(
133
133
  return buildEmptyBatch(schema, metadata);
134
134
  }
135
135
 
136
+ /**
137
+ * Recursively create empty (0-row) Data for any Arrow type,
138
+ * including complex types (Struct, List, FixedSizeList, Map).
139
+ */
140
+ function makeEmptyData(type: DataType): Data {
141
+ if (DataType.isStruct(type)) {
142
+ const children = type.children.map((f: Field) => makeEmptyData(f.type));
143
+ return makeData({ type, length: 0, children, nullCount: 0 });
144
+ }
145
+ if (DataType.isList(type)) {
146
+ const childData = makeEmptyData(type.children[0].type);
147
+ return makeData({ type, length: 0, children: [childData], nullCount: 0, valueOffsets: new Int32Array([0]) } as any);
148
+ }
149
+ if (DataType.isFixedSizeList(type)) {
150
+ const childData = makeEmptyData(type.children[0].type);
151
+ return makeData({ type, length: 0, child: childData, nullCount: 0 } as any);
152
+ }
153
+ if (DataType.isMap(type)) {
154
+ const entryType = type.children[0]?.type;
155
+ const entryData = entryType
156
+ ? makeEmptyData(entryType)
157
+ : makeData({ type: new Struct([]), length: 0, children: [], nullCount: 0 });
158
+ return makeData({ type, length: 0, children: [entryData], nullCount: 0, valueOffsets: new Int32Array([0]) } as any);
159
+ }
160
+ return makeData({ type, length: 0, nullCount: 0 });
161
+ }
162
+
136
163
  /**
137
164
  * Build a 0-row batch from a schema with metadata.
138
165
  * Used for error/log batches.
139
166
  */
140
167
  export function buildEmptyBatch(schema: Schema, metadata?: Map<string, string>): RecordBatch {
141
- const children = schema.fields.map((f: Field) => {
142
- return makeData({ type: f.type, length: 0, nullCount: 0 });
143
- });
144
-
145
- if (schema.fields.length === 0) {
146
- const structType = new Struct(schema.fields);
147
- const data = makeData({
148
- type: structType,
149
- length: 0,
150
- children: [],
151
- nullCount: 0,
152
- });
153
- return new RecordBatch(schema, data, metadata);
154
- }
168
+ const children = schema.fields.map((f: Field) => makeEmptyData(f.type));
155
169
 
156
170
  const structType = new Struct(schema.fields);
157
171
  const data = makeData({