castrum 0.7.0 → 0.9.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 (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -368
  3. package/castrum.linux-x64-gnu.node +0 -0
  4. package/dist/index.d.ts +2763 -0
  5. package/dist/index.js +8707 -0
  6. package/docs/API.md +337 -0
  7. package/docs/ARCHITECTURE.md +507 -0
  8. package/docs/BENCHMARKS.md +343 -0
  9. package/docs/CASE_STUDY.md +240 -0
  10. package/docs/ENVIRONMENT.md +81 -0
  11. package/docs/FFI_BUN_GUIDE.md +327 -0
  12. package/docs/GETTING_STARTED.md +192 -0
  13. package/docs/INGRESS-ROUTER.md +81 -0
  14. package/docs/INGRESS.md +746 -0
  15. package/docs/NATIVE-ROUTE.md +53 -0
  16. package/docs/REPO_MAP.md +279 -0
  17. package/docs/adr/0001-two-wire-formats.md +39 -0
  18. package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
  19. package/docs/adr/0003-bun-builtins-delegation.md +43 -0
  20. package/docs/bun-builtins-decision-matrix.md +115 -0
  21. package/index.ts +44 -8
  22. package/package.json +98 -33
  23. package/src/ingress/body.ts +289 -0
  24. package/src/ingress/constants.ts +220 -48
  25. package/src/ingress/context.ts +136 -0
  26. package/src/ingress/decode/baked-result.ts +121 -0
  27. package/src/ingress/decode/fast-result.ts +160 -0
  28. package/src/ingress/decode/packed-sections.ts +56 -0
  29. package/src/ingress/decode/result-base.ts +172 -0
  30. package/src/ingress/errors.ts +72 -0
  31. package/src/ingress/fast.ts +152 -1021
  32. package/src/ingress/handlers.ts +639 -1001
  33. package/src/ingress/headers/baked-templates.ts +102 -0
  34. package/src/ingress/headers/cors.ts +44 -0
  35. package/src/ingress/headers/fast-templates.ts +238 -0
  36. package/src/ingress/headers/hsts.ts +83 -0
  37. package/src/ingress/health.ts +68 -0
  38. package/src/ingress/index.ts +142 -405
  39. package/src/ingress/metrics.ts +132 -0
  40. package/src/ingress/options.ts +197 -0
  41. package/src/ingress/packing/gather-raw-headers.ts +139 -0
  42. package/src/ingress/packing/header-packing.ts +75 -0
  43. package/src/ingress/packing/input-packer.ts +164 -0
  44. package/src/ingress/packing/scratch.ts +50 -0
  45. package/src/ingress/packing/select-headers.ts +102 -0
  46. package/src/ingress/response/error-bodies.ts +75 -0
  47. package/src/ingress/response/terminal.ts +59 -0
  48. package/src/ingress/router.ts +237 -0
  49. package/src/ingress/routes/common.ts +79 -0
  50. package/src/ingress/routes/delete.ts +14 -0
  51. package/src/ingress/routes/echo.ts +107 -0
  52. package/src/ingress/routes/fallback.ts +23 -0
  53. package/src/ingress/routes/head.ts +27 -0
  54. package/src/ingress/routes/index.ts +11 -0
  55. package/src/ingress/routes/json-write.ts +129 -0
  56. package/src/ingress/routes/options.ts +26 -0
  57. package/src/ingress/routes/read.ts +35 -0
  58. package/src/ingress/routes/responder.ts +159 -0
  59. package/src/ingress/server-node.ts +376 -0
  60. package/src/ingress/server.ts +475 -0
  61. package/src/ingress/shared.ts +132 -0
  62. package/src/ingress/status.ts +97 -0
  63. package/src/ingress/types.ts +191 -0
  64. package/src/integration/batch.ts +58 -0
  65. package/src/integration/index.ts +29 -0
  66. package/src/integration/pipeline.ts +220 -0
  67. package/src/integration/streaming.ts +78 -0
  68. package/src/integration/websocket.ts +93 -0
  69. package/src/loader/batch.ts +137 -0
  70. package/src/loader/cost.ts +105 -0
  71. package/src/loader/index.ts +745 -0
  72. package/src/loader/ops.ts +657 -0
  73. package/src/loader/state.ts +84 -0
  74. package/src/loader/types.ts +145 -0
  75. package/src/native/ffi/constants.ts +58 -0
  76. package/src/native/ffi/selftest.ts +729 -0
  77. package/src/native/ffi/types.ts +474 -0
  78. package/src/native/ffi.ts +1775 -0
  79. package/src/native/index.ts +8 -207
  80. package/src/native/loader.ts +240 -0
  81. package/src/native/types/instances.ts +274 -0
  82. package/src/native/types.ts +401 -0
  83. package/src/runtime/builtins.ts +131 -0
  84. package/src/runtime/codec.ts +147 -0
  85. package/src/runtime/detect.ts +36 -0
  86. package/src/runtime/index.ts +28 -0
  87. package/src/runtime/native.ts +35 -0
  88. package/src/runtime/server.ts +31 -0
  89. package/src/runtime/transport.ts +64 -0
  90. package/src/runtime/types.ts +173 -0
  91. package/src/runtime/uuid.ts +45 -0
  92. package/src/runtime/websocket.ts +30 -0
  93. package/src/rust-ffi/addon.ts +10 -0
  94. package/src/rust-ffi/batch/build.ts +155 -0
  95. package/src/rust-ffi/batch/index.ts +8 -0
  96. package/src/rust-ffi/batch/types.ts +92 -0
  97. package/src/rust-ffi/client.ts +87 -0
  98. package/src/rust-ffi/context.ts +193 -0
  99. package/src/rust-ffi/index.ts +51 -541
  100. package/src/rust-ffi/into.ts +21 -0
  101. package/src/rust-ffi/options.ts +113 -0
  102. package/src/rust-ffi/packed.ts +253 -0
  103. package/src/rust-ffi/proven.ts +28 -0
  104. package/src/rust-ffi/scalar/crypto.ts +346 -0
  105. package/src/rust-ffi/scalar/factories.ts +459 -0
  106. package/src/rust-ffi/scalar/hashing.ts +74 -0
  107. package/src/rust-ffi/scalar/http.ts +337 -0
  108. package/src/rust-ffi/scalar/index.ts +31 -0
  109. package/src/rust-ffi/scalar/interface.ts +325 -0
  110. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  111. package/src/rust-ffi/scalar/json.ts +61 -0
  112. package/src/rust-ffi/scalar/payload.ts +241 -0
  113. package/src/rust-ffi/text.ts +121 -0
  114. package/src/selection.json +194 -0
  115. package/src/selection.ts +73 -0
  116. package/src/shared/adaptive.ts +67 -0
  117. package/src/shared/buffer-pool.ts +264 -0
  118. package/src/shared/bytes.ts +108 -9
  119. package/src/shared/codec.ts +78 -0
  120. package/src/shared/env.ts +20 -0
  121. package/src/shared/log.ts +75 -0
  122. package/src/shared/metrics.ts +247 -0
  123. package/src/shared/packed/index.ts +10 -0
  124. package/src/shared/packed/parsers.ts +31 -0
  125. package/src/shared/packed/schema.ts +23 -0
  126. package/src/shared/packed/wire.ts +504 -0
  127. package/src/shared/proven.ts +289 -0
  128. package/src/shared/request-id.ts +67 -0
  129. package/src/shared/response.ts +104 -0
  130. package/src/shared/runtime.ts +29 -0
  131. package/src/shared/trace.ts +81 -0
  132. package/src/shared/uuid.ts +15 -0
  133. package/Cargo.toml +0 -73
  134. package/bench.ts +0 -3
  135. package/rust/batch.rs +0 -195
  136. package/rust/cookie_parser.rs +0 -70
  137. package/rust/cors.rs +0 -263
  138. package/rust/hashing.rs +0 -164
  139. package/rust/headers.rs +0 -234
  140. package/rust/hmac_sha256.rs +0 -113
  141. package/rust/http_parser.rs +0 -132
  142. package/rust/ingress.rs +0 -616
  143. package/rust/ingress_constants.rs +0 -89
  144. package/rust/ip_trust.rs +0 -185
  145. package/rust/json_ops.rs +0 -66
  146. package/rust/json_patch_ops.rs +0 -25
  147. package/rust/json_schema.rs +0 -151
  148. package/rust/json_ser.rs +0 -429
  149. package/rust/lib.rs +0 -48
  150. package/rust/method.rs +0 -141
  151. package/rust/mime_lookup.rs +0 -76
  152. package/rust/output.rs +0 -238
  153. package/rust/proxy.rs +0 -90
  154. package/rust/query_parser.rs +0 -141
  155. package/rust/random_token.rs +0 -52
  156. package/rust/rate_limit.rs +0 -203
  157. package/rust/terminal.rs +0 -95
  158. package/rust/unit_tests.rs +0 -843
  159. package/rust/url_codec.rs +0 -133
  160. package/rust/util.rs +0 -537
  161. package/rust/validation.rs +0 -107
  162. package/rust/websocket.rs +0 -23
  163. package/src/baseline/index.ts +0 -12
  164. package/src/baseline/tasks/cookie.ts +0 -22
  165. package/src/baseline/tasks/hashing.ts +0 -18
  166. package/src/baseline/tasks/hmac.ts +0 -38
  167. package/src/baseline/tasks/http.ts +0 -62
  168. package/src/baseline/tasks/json-patch.ts +0 -12
  169. package/src/baseline/tasks/json.ts +0 -50
  170. package/src/baseline/tasks/mime.ts +0 -5
  171. package/src/baseline/tasks/query.ts +0 -14
  172. package/src/baseline/tasks/token.ts +0 -15
  173. package/src/baseline/tasks/url.ts +0 -11
  174. package/src/baseline/tasks/validation.ts +0 -19
  175. package/src/baseline/tasks/websocket.ts +0 -14
  176. package/src/bench/assert.ts +0 -70
  177. package/src/bench/checks.ts +0 -227
  178. package/src/bench/checksum.ts +0 -27
  179. package/src/bench/comparisons.ts +0 -70
  180. package/src/bench/concurrent-worker.ts +0 -135
  181. package/src/bench/fixtures.ts +0 -233
  182. package/src/bench/index.ts +0 -2
  183. package/src/bench/measure.ts +0 -177
  184. package/src/bench/now.ts +0 -3
  185. package/src/bench/report.ts +0 -79
  186. package/src/bench/run.ts +0 -57
  187. package/src/bench/tasks/complex.ts +0 -232
  188. package/src/bench/tasks/concurrent.ts +0 -213
  189. package/src/bench/tasks/cookie.ts +0 -44
  190. package/src/bench/tasks/hashing.ts +0 -33
  191. package/src/bench/tasks/hmac.ts +0 -36
  192. package/src/bench/tasks/http.ts +0 -50
  193. package/src/bench/tasks/index.ts +0 -52
  194. package/src/bench/tasks/json-patch.ts +0 -21
  195. package/src/bench/tasks/json.ts +0 -33
  196. package/src/bench/tasks/mime.ts +0 -21
  197. package/src/bench/tasks/query.ts +0 -44
  198. package/src/bench/tasks/stress.ts +0 -142
  199. package/src/bench/tasks/token.ts +0 -20
  200. package/src/bench/tasks/url.ts +0 -34
  201. package/src/bench/tasks/validation.ts +0 -57
  202. package/src/bench/tasks/websocket.ts +0 -21
  203. package/src/bench/types.ts +0 -60
  204. package/src/data/json-rows.ts +0 -33
  205. package/src/ingress/packed-input.ts +0 -59
  206. package/src/shared/json.ts +0 -18
  207. package/src/shared/packed.ts +0 -316
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "castrum",
3
- "version": "0.7.0",
3
+ "version": "0.9.1",
4
4
  "private": false,
5
5
  "description": "Enterprise-grade Bun + Rust runtime: Rust-accelerated FFI primitives, a production-grade HTTP ingress pipeline, validation, hashing, and JSON processing with benchmark-grade performance.",
6
6
  "type": "module",
7
- "main": "index.ts",
8
- "module": "index.ts",
9
- "types": "index.ts",
7
+ "main": "dist/index.js",
8
+ "module": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
10
  "license": "MIT",
11
11
  "keywords": [
12
12
  "bun",
@@ -25,70 +25,135 @@
25
25
  "type": "git",
26
26
  "url": "git+https://github.com/Axylabs/flux-rs.git"
27
27
  },
28
+ "homepage": "https://github.com/Axylabs/flux-rs#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/Axylabs/flux-rs/issues"
31
+ },
28
32
  "publishConfig": {
29
33
  "access": "public"
30
34
  },
31
35
  "exports": {
32
- ".": "./index.ts"
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "bun": "./index.ts",
39
+ "node": "./dist/index.js",
40
+ "default": "./dist/index.js"
41
+ },
42
+ "./package.json": "./package.json"
33
43
  },
34
44
  "files": [
35
45
  "index.ts",
36
- "bench.ts",
46
+ "dist",
37
47
  "src",
38
- "rust",
39
- "Cargo.toml",
48
+ "docs",
40
49
  "README.md",
41
- "*.node"
50
+ "*.node",
51
+ "!src/bench",
52
+ "!src/baseline",
53
+ "!src/data"
42
54
  ],
43
55
  "scripts": {
44
56
  "build": "napi build --release --platform",
57
+ "build:perf": "bash scripts/build-perf.sh",
58
+ "build:v3": "bash scripts/build-v3.sh",
59
+ "build:all": "bun run build && bun run build:v3",
60
+ "build:js": "bun run build:js:bundle && bun run build:js:types",
61
+ "build:js:bundle": "bun build ./index.ts --target=node --format=esm --outdir=dist",
62
+ "build:js:types": "dts-bundle-generator -o dist/index.d.ts index.ts",
45
63
  "bench": "bun run build && bun bench.ts",
46
64
  "check": "bun bench.ts",
47
65
  "test": "bun test",
66
+ "test:all": "bun test && bun run typecheck && bun run typecheck:test && bun run lint:ci && bun run check:clean && cargo test --lib",
67
+ "test:coverage": "node scripts/check-coverage.mjs",
68
+ "setup": "bun install && bun run build",
69
+ "test:node": "node --test test/integration/node-smoke.test.mjs test/integration/node-enterprise.test.mjs",
70
+ "verify:install": "node scripts/verify-install.mjs",
48
71
  "build:debug": "napi build --platform",
49
- "artifacts": "napi artifacts",
72
+ "lint": "bunx biome check .",
73
+ "lint:fix": "bunx biome check --write .",
74
+ "lint:ci": "bunx biome check . --formatter-enabled=false --assist-enabled=false --error-on-warnings",
75
+ "format": "bunx biome format --write .",
76
+ "typecheck": "bunx tsc --noEmit",
77
+ "typecheck:test": "bunx tsc --noEmit -p tsconfig.test.json",
78
+ "test:rust": "cargo test",
79
+ "deny": "cargo deny check",
80
+ "check:version": "bun scripts/check-version.mjs",
81
+ "check:jsdoc": "bun scripts/check-jsdoc.ts",
82
+ "check:selection": "bun scripts/select-native.ts --check",
83
+ "check:clean": "bun scripts/check-clean.ts",
84
+ "audit": "bun audit",
50
85
  "publish:manual": "node scripts/publish-manual.mjs",
51
86
  "publish:manual:dry": "node scripts/publish-manual.mjs --dry-run",
52
- "prepublishOnly": "node scripts/prepublish.mjs",
53
- "bench:http": "bun run bench/run-bench.ts",
54
- "bench:http:smoke": "bun run bench/run-bench.ts smoke",
55
- "bench:http:stress": "bun run bench/run-bench.ts stress",
56
- "bench:http:heavy": "bun run bench/run-bench.ts heavy-json",
57
- "bench:http:crud": "bun run bench/run-bench.ts crud",
58
- "bench:http:spike": "bun run bench/run-bench.ts spike",
59
- "bench:http:soak": "bun run bench/run-bench.ts soak",
60
- "bench:http:storm": "bun run bench/run-bench.ts storm",
61
- "bench:http:boundary": "bun run bench/run-bench.ts boundary",
62
- "bench:http:elysia": "bun run bench/run-bench.ts '' elysia",
63
- "bench:http:ingress": "bun run bench/run-bench.ts '' ingress",
64
- "bench:http:bun-only": "bun run bench/run-bench.ts '' bun",
65
- "bench:http:all-heavy": "bun run bench/run-bench.ts 1"
87
+ "prepublishOnly": "bun run build:js && node scripts/prepublish.mjs",
88
+ "bench:http": "BUN_CONFIG_MAX_HTTP_REQUESTS=65536 bun run bench/run-bench.ts",
89
+ "bench:http:smoke": "BUN_CONFIG_MAX_HTTP_REQUESTS=65536 bun run bench/run-bench.ts smoke",
90
+ "bench:http:stress": "BUN_CONFIG_MAX_HTTP_REQUESTS=65536 bun run bench/run-bench.ts stress",
91
+ "bench:http:heavy": "BUN_CONFIG_MAX_HTTP_REQUESTS=65536 bun run bench/run-bench.ts heavy-json",
92
+ "bench:http:crud": "BUN_CONFIG_MAX_HTTP_REQUESTS=65536 bun run bench/run-bench.ts crud",
93
+ "bench:http:spike": "BUN_CONFIG_MAX_HTTP_REQUESTS=65536 bun run bench/run-bench.ts spike",
94
+ "bench:http:soak": "BUN_CONFIG_MAX_HTTP_REQUESTS=65536 bun run bench/run-bench.ts soak",
95
+ "bench:http:storm": "BUN_CONFIG_MAX_HTTP_REQUESTS=65536 bun run bench/run-bench.ts storm",
96
+ "bench:http:boundary": "BUN_CONFIG_MAX_HTTP_REQUESTS=65536 bun run bench/run-bench.ts boundary",
97
+ "bench:http:elysia": "BUN_CONFIG_MAX_HTTP_REQUESTS=65536 bun run bench/run-bench.ts '' elysia",
98
+ "bench:http:ingress": "BUN_CONFIG_MAX_HTTP_REQUESTS=65536 bun run bench/run-bench.ts '' ingress",
99
+ "bench:http:bun-only": "BUN_CONFIG_MAX_HTTP_REQUESTS=65536 bun run bench/run-bench.ts '' bun",
100
+ "bench:http:all-heavy": "BUN_CONFIG_MAX_HTTP_REQUESTS=65536 bun run bench/run-bench.ts 1",
101
+ "bench:http:ac": "bun bench/autocannon-stress.mjs",
102
+ "bench:http:stress:ac": "SCENARIO=03-stress bun bench/autocannon-stress.mjs",
103
+ "bench:startup": "bun run bench/startup.ts",
104
+ "bench:load": "bun run bench/load-phase.ts",
105
+ "bench:ffi": "bun run bench/ffi-all.ts",
106
+ "bench:ffi:load": "bun run bench/ffi-load.ts",
107
+ "bench:ffi:public": "bun run bench/ffi-public.ts",
108
+ "bench:ffi:workers": "bun run bench/ffi-workers.ts",
109
+ "bench:margin": "bun run bench/ffi-margin.ts",
110
+ "bench:router": "bun run bench/router-cost.ts",
111
+ "bench:ingress-cost": "bun run bench/ingress-cost.ts",
112
+ "bench:ingress-cost:post": "bun run bench/ingress-cost-post.ts",
113
+ "verify:native:batch": "bun run scripts/verify-native-batch.ts"
66
114
  },
67
- "dependencies": {
68
- "crc-32": "^1.2.2",
69
- "fast-json-patch": "^3.1.1",
70
- "mime-types": "^3.0.2",
71
- "validator": "^13.15.35"
115
+ "dependencies": {},
116
+ "peerDependencies": {
117
+ "@types/node": "^26.1.2"
118
+ },
119
+ "peerDependenciesMeta": {
120
+ "@types/node": {
121
+ "optional": true
122
+ }
72
123
  },
73
124
  "devDependencies": {
74
125
  "@elysia/cors": "^1.4.2",
75
- "@napi-rs/cli": "^3.7.4",
126
+ "@napi-rs/cli": "^3.8.6",
76
127
  "@types/bun": "^1.3.14",
77
128
  "@types/mime-types": "^3.0.1",
129
+ "autocannon": "^8.0.0",
78
130
  "@types/validator": "^13.15.10",
79
- "elysia": "^1.4.0"
131
+ "ajv": "^8.20.0",
132
+ "crc-32": "^1.2.2",
133
+ "dts-bundle-generator": "^9.5.1",
134
+ "elysia": "^1.4.29",
135
+ "fast-json-patch": "^3.1.1",
136
+ "mime-types": "^3.0.2",
137
+ "typescript": "5.9.3",
138
+ "validator": "^13.15.35"
139
+ },
140
+ "overrides": {
141
+ "typescript": "5.9.3"
80
142
  },
81
143
  "engines": {
82
- "bun": ">=1.1.0"
144
+ "bun": ">=1.1.0",
145
+ "node": ">=20.3"
83
146
  },
84
147
  "napi": {
85
148
  "binaryName": "castrum",
86
149
  "targets": [
87
150
  "aarch64-apple-darwin",
88
151
  "aarch64-unknown-linux-gnu",
152
+ "aarch64-unknown-linux-musl",
89
153
  "x86_64-apple-darwin",
90
154
  "x86_64-pc-windows-msvc",
91
- "x86_64-unknown-linux-gnu"
155
+ "x86_64-unknown-linux-gnu",
156
+ "x86_64-unknown-linux-musl"
92
157
  ]
93
158
  }
94
159
  }
@@ -0,0 +1,289 @@
1
+ // src/ingress/body.ts — Streaming request-body reading for the async ingress
2
+ // API.
3
+
4
+ import { isBun } from '../shared/runtime'
5
+
6
+ const EMPTY_BODY = new Uint8Array(0)
7
+
8
+ /**
9
+ * Read the request body as bytes. Bun's `req.bytes()` returns a `Uint8Array`
10
+ * directly (no `ArrayBuffer` → view wrapper and no copy) — feature-detected so
11
+ * Node (which lacks `Response.bytes()` on some versions) falls back to
12
+ * `arrayBuffer()` + a zero-copy view. This is the hot-path body read for every
13
+ * write route.
14
+ */
15
+ async function readBodyBytes(req: Request): Promise<Uint8Array> {
16
+ const bytes = (req as { bytes?: () => Promise<Uint8Array> }).bytes
17
+ if (typeof bytes === 'function') {
18
+ return bytes.call(req)
19
+ }
20
+ return new Uint8Array(await req.arrayBuffer())
21
+ }
22
+
23
+ /**
24
+ * Bun-only synchronous peek of an already-buffered body.
25
+ *
26
+ * Under Bun a body fully received during HTTP parsing resolves `req.bytes()`
27
+ * IMMEDIATELY — `Bun.peek` hands back the `Uint8Array` without awaiting, so a
28
+ * declared-Content-Length write route can read the body with no deadline race,
29
+ * no watchdog entry and no microtask turn (the common small-body case).
30
+ *
31
+ * Returns:
32
+ * - `{ bytes }` — already buffered; the caller reads it synchronously and
33
+ * must NOT pay the deadline race (the body is already here).
34
+ * - `Promise<Uint8Array>` — the read is still pending (slow/trickling body);
35
+ * the caller MUST still enforce its deadline against this promise.
36
+ * - `false` — no `req.bytes()` on this runtime (Node); the caller falls back
37
+ * to the async `readBodyBytes` + race as before.
38
+ */
39
+ function peekBufferedBody(req: Request): { bytes: Uint8Array } | Promise<Uint8Array> | false {
40
+ const bytes = (req as { bytes?: () => Promise<Uint8Array> }).bytes
41
+ if (typeof bytes !== 'function') return false
42
+ const pending = bytes.call(req)
43
+ if (!isBun()) return pending
44
+ const peeked = Bun.peek(pending)
45
+ return peeked instanceof Promise ? peeked : { bytes: peeked }
46
+ }
47
+
48
+ /** Read the request body once, enforcing the size guard when enabled. */
49
+ export function readRequestBodyOnce(
50
+ req: Request,
51
+ maxBytes: number,
52
+ guard: boolean,
53
+ timeoutMs?: number,
54
+ ): Promise<Uint8Array> {
55
+ if (req.body === null) {
56
+ return Promise.resolve(EMPTY_BODY)
57
+ }
58
+
59
+ return readBodyWithLimit(req, maxBytes, guard, timeoutMs)
60
+ }
61
+
62
+ function bodyTooLargeError(): Error & { code?: string } {
63
+ const err = new Error('BODY_TOO_LARGE') as Error & { code?: string }
64
+ err.code = 'BODY_TOO_LARGE'
65
+ return err
66
+ }
67
+
68
+ // ── Shared body-read deadline watchdog ──────────────────────────────────────
69
+ // One interval timer sweeps ALL in-flight body reads that carry a deadline,
70
+ // replacing a per-request `setTimeout` + `clearTimeout` + `timer.unref` on
71
+ // every write-route body read (that per-request timer alloc is measurable in
72
+ // the POST micro-bench — ~800ns of the ~2.3µs body read). Abort semantics are
73
+ // unchanged: a read that outlives its deadline rejects with REQUEST_TIMEOUT.
74
+ // Entries are removed on completion or when the deadline fires; the interval
75
+ // only runs while at least one read is pending.
76
+ const bodyWatchdog = (() => {
77
+ const inflight = new Set<WatchdogEntry>()
78
+ let timer: ReturnType<typeof setInterval> | null = null
79
+ const SWEEP_MS = 250
80
+
81
+ function sweep() {
82
+ const now = Date.now()
83
+ for (const entry of inflight) {
84
+ if (entry.settled) {
85
+ inflight.delete(entry)
86
+ continue
87
+ }
88
+ if (now >= entry.deadlineWall) {
89
+ entry.settled = true
90
+ inflight.delete(entry)
91
+ entry.reject(timeoutError())
92
+ }
93
+ }
94
+ if (inflight.size === 0 && timer !== null) {
95
+ clearInterval(timer)
96
+ timer = null
97
+ }
98
+ }
99
+
100
+ function track(entry: WatchdogEntry) {
101
+ inflight.add(entry)
102
+ if (timer === null) {
103
+ timer = setInterval(sweep, SWEEP_MS)
104
+ }
105
+ }
106
+
107
+ function untrack(entry: WatchdogEntry) {
108
+ entry.settled = true
109
+ inflight.delete(entry)
110
+ if (inflight.size === 0 && timer !== null) {
111
+ clearInterval(timer)
112
+ timer = null
113
+ }
114
+ }
115
+
116
+ return { track, untrack }
117
+ })()
118
+
119
+ interface WatchdogEntry {
120
+ deadlineWall: number
121
+ settled: boolean
122
+ reject: (err: Error) => void
123
+ }
124
+
125
+ /** Create a deadline tracked by the shared watchdog (no per-read setTimeout). */
126
+ function createDeadline(deadlineMs: number): { promise: Promise<never>; cancel: () => void } {
127
+ const entry: WatchdogEntry = {
128
+ deadlineWall: Date.now() + deadlineMs,
129
+ settled: false,
130
+ reject: () => {},
131
+ }
132
+ const promise = new Promise<never>((_resolve, reject) => {
133
+ entry.reject = reject
134
+ })
135
+ bodyWatchdog.track(entry)
136
+ return { promise, cancel: () => bodyWatchdog.untrack(entry) }
137
+ }
138
+
139
+ /**
140
+ * Stream-read the request body, enforcing the size guard when enabled and an
141
+ * optional overall deadline.
142
+ *
143
+ * Errors carry `err.code`:
144
+ * - `BODY_TOO_LARGE` when `guard` is on and the body exceeds `maxBytes`
145
+ * (thrown as soon as the limit is crossed — the body is never fully
146
+ * buffered first, which is the slowloris/large-body protection).
147
+ * - `REQUEST_TIMEOUT` when `timeoutMs` elapses before the body completes.
148
+ */
149
+ export async function readBodyWithLimit(
150
+ req: Request,
151
+ maxBytes: number,
152
+ guard: boolean,
153
+ timeoutMs?: number,
154
+ ): Promise<Uint8Array> {
155
+ const deadline = timeoutMs && timeoutMs > 0 ? timeoutMs : 0
156
+
157
+ // Fast path: a declared Content-Length lets us PROVE the body fits the
158
+ // guard before reading, so one native `bytes()`/`arrayBuffer()` read
159
+ // replaces the reader + per-chunk `Promise.race` + `concatUint8Arrays`
160
+ // churn (the bench POSTs are single-shot bodies — this removes ~4-6
161
+ // allocations per write request). The deadline (when set) is still enforced
162
+ // with a single race, not one per chunk. The read is bounded by the
163
+ // server's `maxRequestBodySize`, and the post-read length re-check catches
164
+ // a client that lies about Content-Length.
165
+ const declared = req.headers.get('content-length')
166
+ if (declared !== null) {
167
+ const declaredLen = Number(declared)
168
+ if (Number.isFinite(declaredLen)) {
169
+ if (guard && declaredLen > maxBytes) {
170
+ throw bodyTooLargeError()
171
+ }
172
+ // `req.body` (the stream getter) is deliberately NOT touched on this
173
+ // path — `req.bytes()` (Bun) / `arrayBuffer()` (Node) read the buffered
174
+ // body directly, avoiding a lazily-constructed stream per write request.
175
+ if (deadline <= 0) {
176
+ const bytes = await readBodyBytes(req)
177
+ if (guard && bytes.byteLength > maxBytes) {
178
+ throw bodyTooLargeError()
179
+ }
180
+ return bytes
181
+ }
182
+ // Deadline path. Under Bun, a small declared-length body is usually
183
+ // already buffered — `Bun.peek` returns the bytes synchronously, so we
184
+ // skip the deadline race + watchdog + microtask entirely for the common
185
+ // case. Only a genuinely pending read (slow/trickling body) pays the
186
+ // race; non-Bun runtimes keep the previous behavior.
187
+ const buffered = peekBufferedBody(req)
188
+ if (buffered === false) {
189
+ const dl = createDeadline(deadline)
190
+ try {
191
+ const bytes = await Promise.race([readBodyBytes(req), dl.promise])
192
+ if (guard && bytes.byteLength > maxBytes) {
193
+ throw bodyTooLargeError()
194
+ }
195
+ return bytes
196
+ } finally {
197
+ dl.cancel()
198
+ }
199
+ }
200
+ if ('bytes' in buffered) {
201
+ if (guard && buffered.bytes.byteLength > maxBytes) {
202
+ throw bodyTooLargeError()
203
+ }
204
+ return buffered.bytes
205
+ }
206
+ const dl = createDeadline(deadline)
207
+ try {
208
+ const bytes = await Promise.race([buffered, dl.promise])
209
+ if (guard && bytes.byteLength > maxBytes) {
210
+ throw bodyTooLargeError()
211
+ }
212
+ return bytes
213
+ } finally {
214
+ dl.cancel()
215
+ }
216
+ }
217
+ }
218
+
219
+ // No (usable) Content-Length: fall back to the streaming reader. `req.body`
220
+ // is only touched here — the declared-length fast path never needs it.
221
+ const body = req.body
222
+ if (!body) {
223
+ return EMPTY_BODY
224
+ }
225
+
226
+ const reader = body.getReader()
227
+ const chunks: Uint8Array[] = []
228
+ let total = 0
229
+ // True when the stream was fully read to `done` — on that path the reader is
230
+ // already closed, so the `cancel()` in `finally` is a wasted async round-trip
231
+ // per request. It is still required when the loop exits early (timeout/error).
232
+ let completed = false
233
+ const dl = deadline > 0 ? createDeadline(deadline) : null
234
+
235
+ try {
236
+ for (;;) {
237
+ const { done, value } =
238
+ dl !== null ? await Promise.race([reader.read(), dl.promise]) : await reader.read()
239
+
240
+ if (done) {
241
+ completed = true
242
+ break
243
+ }
244
+ if (!value) continue
245
+
246
+ total += value.byteLength
247
+
248
+ if (guard && total > maxBytes) {
249
+ throw bodyTooLargeError()
250
+ }
251
+
252
+ chunks.push(value)
253
+ }
254
+ } finally {
255
+ dl?.cancel()
256
+ // Release the underlying stream. Cancelling while a read() is still
257
+ // pending (the timeout case) can resolve that pending read() as `done`
258
+ // and RACE the timeout rejection — so reject first, cancel only AFTER
259
+ // the loop has settled. The `.catch` keeps the cancel from masking a
260
+ // thrown REQUEST_TIMEOUT / BODY_TOO_LARGE. On the normal `done` path the
261
+ // stream is already closed, so skip the cancel entirely.
262
+ if (!completed) {
263
+ await reader.cancel().catch(() => {})
264
+ }
265
+ }
266
+
267
+ return concatUint8Arrays(chunks, total)
268
+ }
269
+
270
+ function timeoutError(): Error & { code?: string } {
271
+ const err = new Error('REQUEST_TIMEOUT') as Error & { code?: string }
272
+ err.code = 'REQUEST_TIMEOUT'
273
+ return err
274
+ }
275
+
276
+ function concatUint8Arrays(chunks: Uint8Array[], total: number): Uint8Array {
277
+ if (chunks.length === 0) return EMPTY_BODY
278
+ if (chunks.length === 1) return chunks[0] ?? EMPTY_BODY
279
+
280
+ const out = new Uint8Array(total)
281
+ let offset = 0
282
+
283
+ for (const chunk of chunks) {
284
+ out.set(chunk, offset)
285
+ offset += chunk.byteLength
286
+ }
287
+
288
+ return out
289
+ }