@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
@@ -1,59 +1,69 @@
1
- import { type RecordBatch, type Schema } from "@query-farm/apache-arrow";
1
+ import type { Socket } from "node:net";
2
+ import type { VgiBatch, VgiSchema } from "../arrow/index.js";
3
+ type WriterTarget = {
4
+ kind: "fd";
5
+ fd: number;
6
+ } | {
7
+ kind: "socket";
8
+ socket: Socket;
9
+ };
2
10
  /**
3
- * Writes sequential IPC streams to a file descriptor (e.g., stdout).
4
- * Each call to writeStream() writes a complete IPC stream: schema + batches + EOS.
11
+ * Writes sequential IPC streams to either an fd (stdio subprocess transport)
12
+ * or a Node Socket (AF_UNIX transport). Each call to writeStream() writes a
13
+ * complete IPC stream: schema + batches + EOS.
5
14
  *
6
- * All writes use synchronous I/O (writeSync) to avoid deadlocks when
7
- * interleaving stdout writes with blocking stdin reads.
15
+ * All public methods are async. The fd path resolves immediately after a
16
+ * synchronous writeSync; the socket path awaits real `'drain'` events on
17
+ * backpressure so the event loop stays responsive to other connections.
8
18
  */
9
19
  export declare class IpcStreamWriter {
10
- private readonly fd;
11
- constructor(fd?: number);
20
+ private readonly target;
21
+ /**
22
+ * Construct from a file descriptor (stdio transport) or a Node net.Socket
23
+ * (AF_UNIX transport). The default targets stdout for legacy stdio servers
24
+ * that didn't pass an fd.
25
+ */
26
+ constructor(fdOrSocket?: number | Socket);
12
27
  /**
13
28
  * Write a complete IPC stream with the given schema and batches.
14
29
  * Creates schema message, writes all batches (with their metadata), writes EOS.
15
30
  */
16
- writeStream(schema: Schema, batches: RecordBatch[]): void;
31
+ writeStream(schema: VgiSchema, batches: VgiBatch[]): Promise<void>;
17
32
  /**
18
33
  * Open an incremental IPC stream for writing batches one at a time.
19
- * Used for streaming methods where output batches are produced incrementally.
20
- * Bytes are written synchronously after each batch.
21
34
  */
22
- openStream(schema: Schema): IncrementalStream;
35
+ openStream(schema: VgiSchema): IncrementalStream;
23
36
  }
24
37
  /**
25
38
  * An open IPC stream that supports incremental batch writes.
26
39
  *
27
- * Uses RecordBatchStreamWriter with internal buffering (no pipe to stdout).
28
- * After each operation, drains the writer's internal AsyncByteQueue buffer
29
- * and writes bytes synchronously via writeAll(). This avoids deadlocks
30
- * caused by Node.js async stream piping when stdin reads block before
31
- * stdout writes flush through the event loop.
40
+ * Drives a backend {@link IncrementalEncoder} and flushes its bytes through
41
+ * the same target (fd or socket) as the parent IpcStreamWriter. The write()
42
+ * and close() methods are async so the socket path can yield on backpressure
43
+ * critical under AF_UNIX where the kernel send buffer (~8 KB on macOS)
44
+ * fills quickly and any synchronous busy-wait would starve every other
45
+ * connection sharing this event loop.
46
+ *
47
+ * The encoder is obtained from the Arrow facade, so this file no longer
48
+ * imports arrow-js directly — keeping arrow-js out of the flechette
49
+ * (workerd/browser) bundle. The flechette encoder throws on construction;
50
+ * the stdio exchange protocol is lockstep (the client reads each response
51
+ * batch before sending the next input) which needs an incremental writer
52
+ * flechette doesn't provide. workerd/browser deployments use HTTP (no
53
+ * stdio), so the flechette path is never reached there; `flechette-pipe`
54
+ * conformance is xfailed for streams.
32
55
  */
33
56
  export declare class IncrementalStream {
34
- private writer;
35
- private readonly fd;
57
+ private readonly encoder;
58
+ private readonly target;
36
59
  private closed;
37
- constructor(fd: number, schema: Schema);
38
- /**
39
- * Write a single batch to the stream. Bytes are flushed synchronously.
40
- *
41
- * Uses _writeRecordBatch() directly to bypass the Arrow writer's schema
42
- * comparison in write(). The public write() method calls compareSchemas()
43
- * and auto-closes the writer if the batch's schema differs (e.g., in
44
- * nullability), silently dropping the batch. Since our output schema is
45
- * set at stream open time and all batches are structurally compatible,
46
- * we skip the comparison.
47
- */
48
- write(batch: RecordBatch): void;
49
- /**
50
- * Close the stream (writes EOS marker synchronously).
51
- */
52
- close(): void;
53
- /**
54
- * Drain buffered bytes from the Arrow writer's internal queue
55
- * and write them synchronously to the output fd.
56
- */
57
- private drain;
60
+ private writeChain;
61
+ constructor(target: WriterTarget, schema: VgiSchema);
62
+ /** Write a single batch. Resolves once the bytes are queued/flushed. */
63
+ write(batch: VgiBatch): Promise<void>;
64
+ /** Close the stream (writes EOS marker). */
65
+ close(): Promise<void>;
66
+ private enqueue;
58
67
  }
68
+ export {};
59
69
  //# sourceMappingURL=writer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../src/wire/writer.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,WAAW,EAA2B,KAAK,MAAM,EAAE,MAAM,0BAA0B,CAAC;AA4BlG;;;;;;GAMG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;gBAEhB,EAAE,GAAE,MAAkB;IAIlC;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;IAYzD;;;;OAIG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB;CAG9C;AAED;;;;;;;;GAQG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;IAC5B,OAAO,CAAC,MAAM,CAAS;gBAEX,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAQtC;;;;;;;;;OASG;IACH,KAAK,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAM/B;;OAEG;IACH,KAAK,IAAI,IAAI;IAQb;;;OAGG;IACH,OAAO,CAAC,KAAK;CAOd"}
1
+ {"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../src/wire/writer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,KAAK,EAAsB,QAAQ,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AA2GjF,KAAK,YAAY,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpF;;;;;;;;GAQG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IAEtC;;;;OAIG;gBACS,UAAU,GAAE,MAAM,GAAG,MAAkB;IAQnD;;;OAGG;IACG,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAexE;;OAEG;IACH,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,iBAAiB;CAGjD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,MAAM,CAAS;IAIvB,OAAO,CAAC,UAAU,CAAoC;gBAE1C,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS;IAQnD,wEAAwE;IAClE,KAAK,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAK3C,4CAA4C;IACtC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B,OAAO,CAAC,OAAO;CAchB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@query-farm/vgi-rpc",
3
- "version": "0.6.4",
3
+ "version": "0.7.1",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://vgi-rpc-typescript.query.farm",
6
6
  "repository": {
@@ -8,37 +8,51 @@
8
8
  "url": "https://github.com/Query-farm/vgi-rpc-typescript"
9
9
  },
10
10
  "type": "module",
11
+ "sideEffects": false,
11
12
  "main": "dist/index.js",
12
13
  "types": "dist/index.d.ts",
13
14
  "exports": {
14
15
  ".": {
15
- "import": "./dist/index.js",
16
+ "workerd": "./src/index.ts",
17
+ "worker": "./src/index.ts",
18
+ "browser": "./src/index.ts",
19
+ "bun": "./src/index.ts",
16
20
  "types": "./dist/index.d.ts",
17
- "bun": "./src/index.ts"
21
+ "import": "./dist/index.js"
18
22
  },
19
23
  "./otel": {
20
- "import": "./dist/otel.js",
24
+ "bun": "./src/otel.ts",
21
25
  "types": "./dist/otel.d.ts",
22
- "bun": "./src/otel.ts"
26
+ "import": "./dist/otel.js"
23
27
  },
24
28
  "./s3": {
25
- "import": "./dist/s3.js",
29
+ "bun": "./src/s3.ts",
26
30
  "types": "./dist/s3.d.ts",
27
- "bun": "./src/s3.ts"
31
+ "import": "./dist/s3.js"
28
32
  },
29
33
  "./gcs": {
30
- "import": "./dist/gcs.js",
34
+ "bun": "./src/gcs.ts",
31
35
  "types": "./dist/gcs.d.ts",
32
- "bun": "./src/gcs.ts"
36
+ "import": "./dist/gcs.js"
33
37
  }
34
38
  },
35
39
  "files": [
36
40
  "dist",
37
41
  "src"
38
42
  ],
43
+ "imports": {
44
+ "#vgi-rpc-arrow": {
45
+ "workerd": "./src/arrow/impl-flechette/index.ts",
46
+ "worker": "./src/arrow/impl-flechette/index.ts",
47
+ "default": "./src/arrow/impl-arrowjs/index.ts"
48
+ }
49
+ },
39
50
  "dependencies": {
40
- "@query-farm/apache-arrow": "*",
41
- "oauth4webapi": "^3.8.5"
51
+ "@noble/ciphers": "^2.2.0",
52
+ "@query-farm/apache-arrow": "^21.1.1",
53
+ "@uwdata/flechette": "github:Query-farm/flechette#fix/timestamp-bigint-encode",
54
+ "fzstd": "^0.1.1",
55
+ "oauth4webapi": "^3.8.6"
42
56
  },
43
57
  "peerDependencies": {
44
58
  "@opentelemetry/api": ">=1.4.0",
@@ -61,22 +75,22 @@
61
75
  }
62
76
  },
63
77
  "devDependencies": {
64
- "@biomejs/biome": "^2.4.5",
65
- "@opentelemetry/api": "^1.9.0",
66
- "@opentelemetry/resources": "^2.6.0",
67
- "@opentelemetry/sdk-metrics": "^2.6.0",
68
- "@opentelemetry/sdk-trace-base": "^2.6.0",
69
- "@opentelemetry/semantic-conventions": "^1.40.0",
70
- "@aws-sdk/client-s3": "^3.750.0",
71
- "@aws-sdk/s3-request-presigner": "^3.750.0",
72
- "@google-cloud/storage": "^7.15.0",
78
+ "@biomejs/biome": "^2.4.16",
79
+ "@opentelemetry/api": "^1.9.1",
80
+ "@opentelemetry/resources": "^2.7.1",
81
+ "@opentelemetry/sdk-metrics": "^2.7.1",
82
+ "@opentelemetry/sdk-trace-base": "^2.7.1",
83
+ "@opentelemetry/semantic-conventions": "^1.41.1",
84
+ "@aws-sdk/client-s3": "^3.1059.0",
85
+ "@aws-sdk/s3-request-presigner": "^3.1059.0",
86
+ "@google-cloud/storage": "^7.19.0",
73
87
  "@types/bun": "latest"
74
88
  },
75
89
  "scripts": {
76
90
  "build": "bun run build:types && bun run build:js",
77
91
  "build:types": "bunx tsc -p tsconfig.build.json",
78
92
  "build:js": "bun build ./src/index.ts --outdir dist --target node --format esm --sourcemap=external --external @query-farm/apache-arrow",
79
- "postinstall": "cd node_modules/@query-farm/apache-arrow && node -e \"const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.main='src/Arrow.node.ts';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n')\"",
93
+ "postinstall": "cd node_modules/@query-farm/apache-arrow && node -e \"const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.main='src/Arrow.node.ts';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n')\" && cd ../../.. && bun scripts/patch-flechette.mjs",
80
94
  "test": "bun test",
81
95
  "lint": "biome check .",
82
96
  "lint:fix": "biome check --write .",
@@ -0,0 +1,200 @@
1
+ // © Copyright 2025-2026, Query.Farm LLC - https://query.farm
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Cross-language conformance access-log hook.
6
+ *
7
+ * Emits one JSON record per RPC dispatch to a {@link Sink} (typically a file
8
+ * descriptor opened in append mode). The record shape conforms to the
9
+ * vgi-rpc access-log specification (`docs/access-log-spec.md` and
10
+ * `vgi_rpc/access_log.schema.json` in the Python reference repo).
11
+ *
12
+ * Use {@link AccessLogHook} to align this implementation with `vgi-rpc-test
13
+ * --access-log` so worker behaviour is checked across language ports by the
14
+ * same tool that gates the conformance suite.
15
+ */
16
+
17
+ import type { CallStatistics, DispatchHook, DispatchInfo, HookToken } from "./types.js";
18
+
19
+ /** Where the hook writes formatted JSON lines. */
20
+ export interface AccessLogSink {
21
+ /** Write one access-log line. The trailing newline is included by the caller. */
22
+ write(line: string): void;
23
+ }
24
+
25
+ // Indirect-string require so esbuild can't pull node:fs into the bundle.
26
+ // Workers should use a custom sink (e.g., one backed by `console.log`).
27
+ const _NODE_FS_MOD = "node:fs";
28
+ function _loadWriteSync(): (fd: number, data: Uint8Array, offset?: number, len?: number) => number {
29
+ const req: any = (import.meta as any).require ?? (globalThis as any).require ?? null;
30
+ if (!req) {
31
+ throw new Error(
32
+ "FdSink requires Node.js or Bun (node:fs.writeSync). For other runtimes, " +
33
+ "supply a custom AccessLogSink that wraps console.log or your logger.",
34
+ );
35
+ }
36
+ return req(_NODE_FS_MOD).writeSync;
37
+ }
38
+
39
+ /** A sink backed by a file descriptor; uses synchronous writes for ordering. */
40
+ export class FdSink implements AccessLogSink {
41
+ private readonly _writeSync = _loadWriteSync();
42
+ constructor(private readonly fd: number) {}
43
+ /** Write `line` to the file descriptor, looping until the buffer is fully flushed. */
44
+ write(line: string): void {
45
+ const buf = new TextEncoder().encode(line);
46
+ let offset = 0;
47
+ while (offset < buf.length) {
48
+ const n = this._writeSync(this.fd, buf, offset, buf.length - offset);
49
+ if (n <= 0) throw new Error(`access-log writeSync returned ${n}`);
50
+ offset += n;
51
+ }
52
+ }
53
+ }
54
+
55
+ interface StartToken {
56
+ startNs: bigint;
57
+ }
58
+
59
+ function rfc3339Utc(): string {
60
+ const d = new Date();
61
+ const yyyy = d.getUTCFullYear().toString().padStart(4, "0");
62
+ const mm = (d.getUTCMonth() + 1).toString().padStart(2, "0");
63
+ const dd = d.getUTCDate().toString().padStart(2, "0");
64
+ const hh = d.getUTCHours().toString().padStart(2, "0");
65
+ const mi = d.getUTCMinutes().toString().padStart(2, "0");
66
+ const ss = d.getUTCSeconds().toString().padStart(2, "0");
67
+ const ms = d.getUTCMilliseconds().toString().padStart(3, "0");
68
+ return `${yyyy}-${mm}-${dd}T${hh}:${mi}:${ss}.${ms}Z`;
69
+ }
70
+
71
+ function base64(bytes: Uint8Array): string {
72
+ return Buffer.from(bytes).toString("base64");
73
+ }
74
+
75
+ /** Round to 2 decimal places. */
76
+ function roundTo2(f: number): number {
77
+ return Math.round(f * 100) / 100;
78
+ }
79
+
80
+ /**
81
+ * Options for {@link AccessLogHook}.
82
+ *
83
+ * `level` matches Python's logger-level gating in `_emit_access_log`:
84
+ * at "INFO" the heavy `request_data` field (a base64 of the full
85
+ * request batch — typically 8+ KiB per init RPC) is replaced with a
86
+ * `truncated: true` marker plus `original_request_bytes`, so the
87
+ * access-log schema's "unary requires request_data unless truncated"
88
+ * invariant still holds. Bump to "DEBUG" to capture full payloads for
89
+ * replay/audit.
90
+ */
91
+ export interface AccessLogOptions {
92
+ /** Server version string (optional). */
93
+ serverVersion?: string;
94
+ /** Verbosity for heavy fields. Default: "INFO". */
95
+ level?: "INFO" | "DEBUG";
96
+ }
97
+
98
+ export class AccessLogHook implements DispatchHook {
99
+ private readonly serverVersion: string;
100
+ private readonly level: "INFO" | "DEBUG";
101
+
102
+ constructor(
103
+ private readonly sink: AccessLogSink,
104
+ options: AccessLogOptions | string = {},
105
+ ) {
106
+ // Backward compatibility: the original signature accepted a bare
107
+ // serverVersion string as the second arg.
108
+ if (typeof options === "string") {
109
+ this.serverVersion = options;
110
+ this.level = "INFO";
111
+ } else {
112
+ this.serverVersion = options.serverVersion ?? "";
113
+ this.level = options.level ?? "INFO";
114
+ }
115
+ }
116
+
117
+ /** Capture a high-resolution start timestamp; returned token feeds {@link onDispatchEnd}. */
118
+ onDispatchStart(_info: DispatchInfo): HookToken {
119
+ const token: StartToken = { startNs: process.hrtime.bigint() };
120
+ return token;
121
+ }
122
+
123
+ /** Emit one access-log JSON record for the completed dispatch (best-effort;
124
+ * write errors are swallowed so logging never breaks a request). */
125
+ onDispatchEnd(token: HookToken, info: DispatchInfo, stats: CallStatistics, error?: Error): void {
126
+ const t = token as StartToken | undefined;
127
+ const durationMs = t ? roundTo2(Number(process.hrtime.bigint() - t.startNs) / 1_000_000) : 0;
128
+
129
+ const status = error ? "error" : "ok";
130
+ const errType = error ? ((error as Error & { type?: string }).type ?? error.constructor.name) : "";
131
+ const errMsg = error?.message ?? "";
132
+
133
+ const protocol = info.protocol ?? "";
134
+ const rec: Record<string, unknown> = {
135
+ timestamp: rfc3339Utc(),
136
+ level: "INFO",
137
+ logger: "vgi_rpc.access",
138
+ message: `${protocol}.${info.method} ${status}`,
139
+ server_id: info.serverId,
140
+ protocol,
141
+ protocol_hash: info.protocolHash ?? "",
142
+ method: info.method,
143
+ method_type: info.methodType,
144
+ principal: info.principal ?? "",
145
+ auth_domain: info.authDomain ?? "",
146
+ authenticated: info.authenticated ?? false,
147
+ remote_addr: info.remoteAddr ?? "",
148
+ duration_ms: durationMs,
149
+ status,
150
+ error_type: errType,
151
+ };
152
+
153
+ if (errMsg) rec.error_message = errMsg;
154
+ if (this.serverVersion) rec.server_version = this.serverVersion;
155
+ if (info.protocolVersion) rec.protocol_version = info.protocolVersion;
156
+ if (info.requestId) rec.request_id = info.requestId;
157
+ if (info.requestData && info.requestData.length > 0) {
158
+ // At INFO, the per-request base64 payload dominates record size
159
+ // (an init RPC commonly logs 8+ KiB of base64 per call) and audit
160
+ // consumers rarely need the bytes — they care about who/what/when.
161
+ // Replace with a `truncated: true` marker so the access-log schema's
162
+ // "unary requires request_data unless truncated" invariant holds.
163
+ // Bump level to DEBUG to re-enable the full payload.
164
+ const encoded = base64(info.requestData);
165
+ if (this.level === "DEBUG") {
166
+ rec.request_data = encoded;
167
+ } else {
168
+ rec.original_request_bytes = encoded.length;
169
+ rec.truncated = true;
170
+ }
171
+ }
172
+ if (info.methodType === "stream") {
173
+ rec.stream_id = info.streamId ?? "00000000000000000000000000000000";
174
+ }
175
+ if (info.cancelled) rec.cancelled = true;
176
+
177
+ if (
178
+ stats.inputBatches +
179
+ stats.outputBatches +
180
+ stats.inputRows +
181
+ stats.outputRows +
182
+ stats.inputBytes +
183
+ stats.outputBytes !==
184
+ 0
185
+ ) {
186
+ rec.input_batches = stats.inputBatches;
187
+ rec.output_batches = stats.outputBatches;
188
+ rec.input_rows = stats.inputRows;
189
+ rec.output_rows = stats.outputRows;
190
+ rec.input_bytes = stats.inputBytes;
191
+ rec.output_bytes = stats.outputBytes;
192
+ }
193
+
194
+ try {
195
+ this.sink.write(`${JSON.stringify(rec)}\n`);
196
+ } catch {
197
+ // best-effort
198
+ }
199
+ }
200
+ }