castrum 0.7.0 → 0.9.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 (176) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -59
  3. package/castrum.linux-x64-gnu.node +0 -0
  4. package/dist/index.d.ts +3161 -0
  5. package/dist/index.js +7306 -0
  6. package/docs/ARCHITECTURE.md +415 -0
  7. package/docs/BENCHMARKS.md +307 -0
  8. package/docs/ENVIRONMENT.md +79 -0
  9. package/docs/GETTING_STARTED.md +209 -0
  10. package/docs/INGRESS.md +745 -0
  11. package/docs/REPO_MAP.md +259 -0
  12. package/docs/adr/0001-two-wire-formats.md +39 -0
  13. package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
  14. package/docs/adr/0003-bun-builtins-delegation.md +42 -0
  15. package/docs/bun-builtins-decision-matrix.md +108 -0
  16. package/index.ts +51 -8
  17. package/package.json +73 -20
  18. package/src/data/json-rows.ts +13 -13
  19. package/src/ingress/body.ts +149 -0
  20. package/src/ingress/constants.ts +220 -48
  21. package/src/ingress/context.ts +136 -0
  22. package/src/ingress/decode/baked-result.ts +121 -0
  23. package/src/ingress/decode/fast-result.ts +160 -0
  24. package/src/ingress/decode/packed-sections.ts +56 -0
  25. package/src/ingress/decode/result-base.ts +172 -0
  26. package/src/ingress/errors.ts +72 -0
  27. package/src/ingress/fast.ts +152 -1021
  28. package/src/ingress/handlers.ts +563 -1001
  29. package/src/ingress/headers/baked-templates.ts +102 -0
  30. package/src/ingress/headers/cors.ts +44 -0
  31. package/src/ingress/headers/fast-templates.ts +238 -0
  32. package/src/ingress/headers/hsts.ts +83 -0
  33. package/src/ingress/health.ts +67 -0
  34. package/src/ingress/index.ts +130 -405
  35. package/src/ingress/metrics.ts +132 -0
  36. package/src/ingress/options.ts +197 -0
  37. package/src/ingress/packing/gather-raw-headers.ts +75 -0
  38. package/src/ingress/packing/header-packing.ts +75 -0
  39. package/src/ingress/packing/input-packer.ts +150 -0
  40. package/src/ingress/packing/scratch.ts +50 -0
  41. package/src/ingress/packing/select-headers.ts +96 -0
  42. package/src/ingress/response/error-bodies.ts +75 -0
  43. package/src/ingress/response/terminal.ts +59 -0
  44. package/src/ingress/routes/common.ts +38 -0
  45. package/src/ingress/routes/delete.ts +14 -0
  46. package/src/ingress/routes/echo.ts +107 -0
  47. package/src/ingress/routes/fallback.ts +23 -0
  48. package/src/ingress/routes/head.ts +37 -0
  49. package/src/ingress/routes/index.ts +10 -0
  50. package/src/ingress/routes/json-write.ts +153 -0
  51. package/src/ingress/routes/options.ts +36 -0
  52. package/src/ingress/routes/read.ts +45 -0
  53. package/src/ingress/server-node.ts +376 -0
  54. package/src/ingress/server.ts +443 -0
  55. package/src/ingress/shared.ts +132 -0
  56. package/src/ingress/status.ts +97 -0
  57. package/src/ingress/types.ts +157 -0
  58. package/src/integration/batch.ts +58 -0
  59. package/src/integration/index.ts +29 -0
  60. package/src/integration/pipeline.ts +217 -0
  61. package/src/integration/streaming.ts +79 -0
  62. package/src/integration/websocket.ts +92 -0
  63. package/src/loader/batch.ts +133 -0
  64. package/src/loader/cost.ts +104 -0
  65. package/src/loader/index.ts +930 -0
  66. package/src/loader/ops.ts +657 -0
  67. package/src/native/ffi.ts +1541 -0
  68. package/src/native/index.ts +8 -207
  69. package/src/native/loader.ts +194 -0
  70. package/src/native/types.ts +598 -0
  71. package/src/rust-ffi/addon.ts +10 -0
  72. package/src/rust-ffi/batch.ts +431 -0
  73. package/src/rust-ffi/client.ts +88 -0
  74. package/src/rust-ffi/context.ts +164 -0
  75. package/src/rust-ffi/index.ts +40 -541
  76. package/src/rust-ffi/options.ts +113 -0
  77. package/src/rust-ffi/packed.ts +253 -0
  78. package/src/rust-ffi/proven.ts +42 -0
  79. package/src/rust-ffi/scalar/crypto.ts +265 -0
  80. package/src/rust-ffi/scalar/factories.ts +233 -0
  81. package/src/rust-ffi/scalar/hashing.ts +72 -0
  82. package/src/rust-ffi/scalar/http.ts +206 -0
  83. package/src/rust-ffi/scalar/index.ts +31 -0
  84. package/src/rust-ffi/scalar/interface.ts +450 -0
  85. package/src/rust-ffi/scalar/json.ts +45 -0
  86. package/src/rust-ffi/scalar/payload.ts +124 -0
  87. package/src/rust-ffi/text.ts +95 -0
  88. package/src/selection.json +194 -0
  89. package/src/selection.ts +77 -0
  90. package/src/shared/adaptive.ts +68 -0
  91. package/src/shared/bench-classify.ts +209 -0
  92. package/src/shared/buffer-pool.ts +209 -0
  93. package/src/shared/bytes.ts +35 -5
  94. package/src/shared/env.ts +20 -0
  95. package/src/shared/log.ts +74 -0
  96. package/src/shared/metrics.ts +247 -0
  97. package/src/shared/packed.ts +350 -154
  98. package/src/shared/proven.ts +596 -0
  99. package/src/shared/request-id.ts +67 -0
  100. package/src/shared/response.ts +104 -0
  101. package/src/shared/runtime.ts +34 -0
  102. package/src/shared/trace.ts +81 -0
  103. package/src/shared/uuid.ts +31 -0
  104. package/Cargo.toml +0 -73
  105. package/bench.ts +0 -3
  106. package/rust/batch.rs +0 -195
  107. package/rust/cookie_parser.rs +0 -70
  108. package/rust/cors.rs +0 -263
  109. package/rust/hashing.rs +0 -164
  110. package/rust/headers.rs +0 -234
  111. package/rust/hmac_sha256.rs +0 -113
  112. package/rust/http_parser.rs +0 -132
  113. package/rust/ingress.rs +0 -616
  114. package/rust/ingress_constants.rs +0 -89
  115. package/rust/ip_trust.rs +0 -185
  116. package/rust/json_ops.rs +0 -66
  117. package/rust/json_patch_ops.rs +0 -25
  118. package/rust/json_schema.rs +0 -151
  119. package/rust/json_ser.rs +0 -429
  120. package/rust/lib.rs +0 -48
  121. package/rust/method.rs +0 -141
  122. package/rust/mime_lookup.rs +0 -76
  123. package/rust/output.rs +0 -238
  124. package/rust/proxy.rs +0 -90
  125. package/rust/query_parser.rs +0 -141
  126. package/rust/random_token.rs +0 -52
  127. package/rust/rate_limit.rs +0 -203
  128. package/rust/terminal.rs +0 -95
  129. package/rust/unit_tests.rs +0 -843
  130. package/rust/url_codec.rs +0 -133
  131. package/rust/util.rs +0 -537
  132. package/rust/validation.rs +0 -107
  133. package/rust/websocket.rs +0 -23
  134. package/src/baseline/index.ts +0 -12
  135. package/src/baseline/tasks/cookie.ts +0 -22
  136. package/src/baseline/tasks/hashing.ts +0 -18
  137. package/src/baseline/tasks/hmac.ts +0 -38
  138. package/src/baseline/tasks/http.ts +0 -62
  139. package/src/baseline/tasks/json-patch.ts +0 -12
  140. package/src/baseline/tasks/json.ts +0 -50
  141. package/src/baseline/tasks/mime.ts +0 -5
  142. package/src/baseline/tasks/query.ts +0 -14
  143. package/src/baseline/tasks/token.ts +0 -15
  144. package/src/baseline/tasks/url.ts +0 -11
  145. package/src/baseline/tasks/validation.ts +0 -19
  146. package/src/baseline/tasks/websocket.ts +0 -14
  147. package/src/bench/assert.ts +0 -70
  148. package/src/bench/checks.ts +0 -227
  149. package/src/bench/checksum.ts +0 -27
  150. package/src/bench/comparisons.ts +0 -70
  151. package/src/bench/concurrent-worker.ts +0 -135
  152. package/src/bench/fixtures.ts +0 -233
  153. package/src/bench/index.ts +0 -2
  154. package/src/bench/measure.ts +0 -177
  155. package/src/bench/now.ts +0 -3
  156. package/src/bench/report.ts +0 -79
  157. package/src/bench/run.ts +0 -57
  158. package/src/bench/tasks/complex.ts +0 -232
  159. package/src/bench/tasks/concurrent.ts +0 -213
  160. package/src/bench/tasks/cookie.ts +0 -44
  161. package/src/bench/tasks/hashing.ts +0 -33
  162. package/src/bench/tasks/hmac.ts +0 -36
  163. package/src/bench/tasks/http.ts +0 -50
  164. package/src/bench/tasks/index.ts +0 -52
  165. package/src/bench/tasks/json-patch.ts +0 -21
  166. package/src/bench/tasks/json.ts +0 -33
  167. package/src/bench/tasks/mime.ts +0 -21
  168. package/src/bench/tasks/query.ts +0 -44
  169. package/src/bench/tasks/stress.ts +0 -142
  170. package/src/bench/tasks/token.ts +0 -20
  171. package/src/bench/tasks/url.ts +0 -34
  172. package/src/bench/tasks/validation.ts +0 -57
  173. package/src/bench/tasks/websocket.ts +0 -21
  174. package/src/bench/types.ts +0 -60
  175. package/src/ingress/packed-input.ts +0 -59
  176. package/src/shared/json.ts +0 -18
@@ -0,0 +1,307 @@
1
+ # Benchmarks Guide
2
+
3
+ This document describes the benchmark framework, how to run benchmarks, and how to interpret results.
4
+
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ The benchmark system compares the Rust implementations against pure JavaScript/Bun baseline implementations. It measures throughput (ops/sec), latency (mean/percentiles), and memory allocation. Under Bun the `rust:*` side runs through the PRIMARY `bun:ffi` transport (NAPI is the fallback); the CPU bench warns — or hard-fails with `CASTRUM_BENCH_FFI=1` — if the ffi transport is not live.
10
+
11
+ ### Benchmark Types
12
+
13
+ | Type | Description |
14
+ |------|-------------|
15
+ | **Unit benchmarks** | Individual function comparisons (hashing, validation, JSON, etc.) |
16
+ | **HTTP benchmarks** | Full HTTP server comparisons (Bun vs Elysia vs Ingress) |
17
+ | **Batch benchmarks** | Throughput for batch operations |
18
+ | **Stress benchmarks** | Performance under load |
19
+ | **Concurrency benchmarks** | Performance with multiple concurrent requests |
20
+
21
+ ---
22
+
23
+ ## Running Benchmarks
24
+
25
+ ### Unit Benchmarks
26
+
27
+ ```bash
28
+ # Run all unit benchmarks
29
+ bun bench.ts
30
+ ```
31
+
32
+ ### Performance-proven surface & auto-annotated JSDoc
33
+
34
+ The CPU benchmark persists a machine-readable report to `bench/results/cpu/latest.json`
35
+ (+ timestamped snapshots). Three commands consume it; `bun run check:docs` runs all
36
+ three as the one-stop "informed decision" pipeline:
37
+
38
+ | Command | What it does |
39
+ |---------|--------------|
40
+ | `bun run check` | Runs the CPU benchmark and writes `bench/results/cpu/latest.json` (report-only — **never mutates source**). |
41
+ | `bun run check:annotate` | Rewrites the `@performance` / `@deprecated` / `@remarks` JSDoc on the public `rust.*` declarations (`src/rust-ffi/scalar/interface.ts`, `batch.ts`, `client.ts`) from the report. Idempotent — generated lines carry the `[check:annotate]` marker; hand-written JSDoc is preserved. `--dry-run` previews changes. |
42
+ | `bun run check:proven` | Audits the `PROVEN_SURFACE` registry against the report and flags `REGRESSION`, `PARITY-DRIFT`, and `PROMOTABLE`. `--fail` exits 1 on regressions. |
43
+ | `bun run check:docs` | `check` + `check:annotate` + `check:proven` — the full pipeline. |
44
+
45
+ **Automatic annotation (opt-in):** `CASTRUM_BENCH_ANNOTATE=1 bun run check`
46
+ auto-runs the annotator right after the benchmark, so the JSDoc always reflects
47
+ the latest run. It is opt-in because it mutates tracked source files.
48
+
49
+ **How the annotations work (data-driven):**
50
+
51
+ - Each function's **score** is embedded in the `@performance` line, e.g.
52
+ `JSON valid: ~3.2x faster than the JS baseline (8/8 tasks won)` or
53
+ `CRC32: ~4.1x slower than the JS baseline (2/3 tasks failed)`. A function's
54
+ benchmark set is the canonical comparison plus all its `_`-suffixed variants
55
+ (`_large`, `_batch`, `_concurrent_*`, `_stress`, pooled `_into` overloads)
56
+ that no other registry entry claims.
57
+ - Task bands (see `src/shared/bench-classify.ts`): **won** = `ratio >= 1`
58
+ (Rust at least as fast), **failed** = `ratio < 1 / 1.35` (clear loss), the
59
+ middle is a **tie** that counts toward neither.
60
+ - `@deprecated` is **hybrid**:
61
+ - A clear **majority** of a function's benchmarks failing in the latest run
62
+ auto-deprecates it even when the static registry says `proven`/`parity` — a
63
+ `@remarks Auto-deprecated: N/M benchmarks failed ...` note records the drift.
64
+ - A single good run **never** removes an existing `@deprecated` (static
65
+ classifications reflect the shipped release build, not one local run). A
66
+ majority win on a static `not-competitive` function only adds a
67
+ `@remarks Promotion candidate ...` note.
68
+ - Classifications are **build-dependent** (debug/SIMD builds distort ratios). The
69
+ annotator warns when the report carries no `buildFlavor`; confirm on a release
70
+ build (`bun run build`) or local perf build (`bun run build:perf`).
71
+
72
+ ### Bun built-ins diagnostic set ("don't reinvent the wheel")
73
+
74
+ `bun run check` also races castrum ops against **Bun's native built-ins**
75
+ (`Bun.hash`, `Bun.password`, `Bun.CryptoHasher`, `Bun.gzipSync`,
76
+ `Bun.randomUUIDv7`) as a diagnostic set:
77
+
78
+ - Task names use the **`diag:`** prefix (not `native:`/`rust:`) so
79
+ `check-proven`'s aggregation (which matches `PROVEN_SURFACE` rustTask names
80
+ and their `_`-suffixed variants) never picks them up — they are informational.
81
+ - Sources: `src/baseline/tasks/bun-builtins.ts` (Bun-only baselines, guarded
82
+ with `typeof Bun` so the module still loads under Node),
83
+ `src/bench/tasks/bun-builtins.ts`, comparisons in `src/bench/comparisons.ts`.
84
+ - **Runtime honesty guard**: a Bun API must be verified to actually execute real
85
+ work before its result is trusted. The first run produced false "wins"
86
+ (brotli 156x, validators 3-5x) because Bun 1.4 has **no**
87
+ `Bun.brotliCompressSync` and **no** `Bun.validators` — those pairs were
88
+ removed, not reported.
89
+ - Results and the keep/delegate/add decisions live in
90
+ `docs/bun-builtins-decision-matrix.md`.
91
+
92
+ ### HTTP Benchmarks
93
+
94
+ ```bash
95
+ # Run all HTTP scenarios
96
+ bun run bench:http
97
+
98
+ # Run specific scenario
99
+ bun run bench:http:smoke # Light load, quick health check
100
+ bun run bench:http:stress # High concurrency, short duration
101
+ bun run bench:http:heavy # Heavy JSON payloads
102
+ bun run bench:http:crud # CRUD operations
103
+ bun run bench:http:spike # Burst traffic patterns
104
+ bun run bench:http:soak # Long duration (endurance)
105
+ bun run bench:http:storm # Very high concurrency burst
106
+ bun run bench:http:boundary # Edge case payloads
107
+
108
+ # Compare specific frameworks
109
+ bun run bench:http:elysia # Run only Elysia vs baseline
110
+ bun run bench:http:ingress # Run only Ingress vs baseline
111
+ bun run bench:http:bun-only # Run only raw Bun vs baseline
112
+
113
+ # Run all heavy scenarios
114
+ bun run bench:http:all-heavy
115
+ ```
116
+
117
+ ---
118
+
119
+ ## HTTP Benchmark Scenarios
120
+
121
+ Each scenario targets a specific performance aspect:
122
+
123
+ ### 01-smoke
124
+ - **Goal**: Quick health check (health, GET, POST mix)
125
+ - **Duration**: ~10 seconds
126
+ - **Concurrency**: up to 50
127
+
128
+ ### 02-load
129
+ - **Goal**: Sustained mixed load (warm-up → sustain → cool-down)
130
+ - **Duration**: ~120 seconds
131
+ - **Concurrency**: up to 1,000
132
+ - **Traffic**: GET/POST mix with cookies, health
133
+
134
+ ### 03-stress
135
+ - **Goal**: Ramp to maximum throughput
136
+ - **Duration**: ~100 seconds (5 × 20s ramp phases)
137
+ - **Concurrency**: up to 10,000
138
+
139
+ ### 04-spike
140
+ - **Goal**: Burst resilience (baseline → spike → recovery)
141
+ - **Duration**: ~100 seconds
142
+ - **Concurrency**: up to 8,000 (spikes up to 5,000 req/s)
143
+
144
+ ### 05-soak
145
+ - **Goal**: Endurance/stability
146
+ - **Duration**: ~600 seconds (10 minutes)
147
+ - **Concurrency**: up to 1,000
148
+
149
+ ### 06-edge-cases
150
+ - **Goal**: Malformed / edge-case payloads
151
+ - **Duration**: ~30 seconds
152
+ - **Concurrency**: up to 200
153
+ - **Traffic**: invalid JSON, schema failures, 404/405/415/422, Unicode, oversize query
154
+
155
+ ### 07-cors-preflight
156
+ - **Goal**: CORS preflight storm (allowed + disallowed origins)
157
+ - **Duration**: ~30 seconds
158
+ - **Concurrency**: up to 500
159
+
160
+ ### 08-rate-limit
161
+ - **Goal**: Rate limiting (under limit → over limit → recovery)
162
+ - **Duration**: ~40 seconds
163
+ - **Concurrency**: up to 1,000
164
+
165
+ ### 09-large-payload
166
+ - **Goal**: Large payload handling
167
+ - **Duration**: ~30 seconds
168
+ - **Concurrency**: up to 100
169
+ - **Payload**: large binary + large JSON arrays
170
+
171
+ ### 10-mixed-realistic
172
+ - **Goal**: Realistic mixed traffic (origins, cookies, auth)
173
+ - **Duration**: ~60 seconds (ramp)
174
+ - **Concurrency**: up to 5,000
175
+
176
+ ### 11-concurrent-burst
177
+ - **Goal**: High-concurrency bursts with pauses
178
+ - **Duration**: ~35 seconds
179
+ - **Concurrency**: up to 8,000
180
+
181
+ ### 12-slowloris
182
+ - **Goal**: Slow clients
183
+ - **Duration**: ~60 seconds
184
+ - **Concurrency**: up to 500
185
+
186
+ ### Heavy JSON scenarios (`13`–`20`)
187
+
188
+ `13-heavy-json-nested` … `20-validation-storm` stress schema validation with nested, array-heavy, wide, CRUD-mix, spike, soak, large-body, and storm payloads. Run them with `bun run bench:http:heavy`.
189
+
190
+ ---
191
+
192
+ ## Interpreting Results
193
+
194
+ ### Report Format
195
+
196
+ Results are saved to `bench/results/<framework>/<scenario>.bench.md` and include:
197
+
198
+ ```md
199
+ # Benchmark Report: bun / 01-smoke
200
+
201
+ | Metric | Value |
202
+ |--------|-------|
203
+ | Duration | 10.0s |
204
+ | Requests | 125,432 |
205
+ | Throughput | 12,543 req/s |
206
+ | Mean Latency | 0.79ms |
207
+ | p50 Latency | 0.72ms |
208
+ | p95 Latency | 1.13ms |
209
+ | p99 Latency | 2.45ms |
210
+ | Max Latency | 15.67ms |
211
+ | Errors | 0 |
212
+ ```
213
+
214
+ ### Comparison Reports
215
+
216
+ When comparing multiple frameworks, differences are expressed as:
217
+
218
+ - **Speedup**: `2.45x` (Rust is 2.45x faster than JS)
219
+ - **Slowdown**: `0.82x` (Rust is 0.82x the speed of JS, i.e., 18% slower)
220
+ - **% Improvement**: `+145%` throughput increase
221
+
222
+ ### Key Metrics
223
+
224
+ | Metric | What It Measures |
225
+ |--------|-----------------|
226
+ | **Throughput** | Requests per second — higher is better |
227
+ | **Mean Latency** | Average response time — lower is better |
228
+ | **p50/p95/p99** | Percentile latencies — lower is better |
229
+ | **Max Latency** | Worst-case response time — should be bounded |
230
+ | **Error Rate** | Percentage of failed requests — should be 0% |
231
+
232
+ ---
233
+
234
+ ## Benchmark Architecture
235
+
236
+ ```
237
+ bench.ts
238
+ └─ Loads benchmark tasks from src/bench/tasks/ (30 tasks)
239
+ ├── hashing.ts Compare Rust fnv1a64 vs JS TextEncoder + hash
240
+ ├── validation.ts Compare Rust validation vs validator library
241
+ ├── json.ts Compare Rust sonic-rs vs JSON.parse
242
+ ├── http.ts Compare Rust httparse vs manual
243
+ ├── complex.ts Combined operations
244
+ ├── stress.ts Concurrent load tests
245
+ └── ... (aead, accept, compress, cookie-sign, csrf, encoding, etag,
246
+ form, json-schema, jwt, media-type, multipart, password,
247
+ streaming, template, url-join, ... — full list in docs/REPO_MAP.md)
248
+ ```
249
+
250
+ Each task runs:
251
+ 1. **Warmup**: 100ms to JIT-compile JavaScript
252
+ 2. **Measurement**: Fixed duration (default 2000ms) in a tight loop
253
+ 3. **Operations counted**: Number of function calls completed
254
+ 4. **Result**: Ops/sec calculated from wall-clock time
255
+
256
+ ### Benchmark controls
257
+
258
+ | Env / flag | Effect |
259
+ |------------|--------|
260
+ | `CASTRUM_BENCH_BATCH_SIZE` | Batch size for sub-µs operations (default `64`). Batching amortizes the timer/measurement overhead for very fast ops. |
261
+ | `CASTRUM_BENCH_ANNOTATE=1` | After `bun run check`, auto-rewrite the `rust.*` JSDoc annotations from the fresh report (see "Performance-proven surface & auto-annotated JSDoc"). |
262
+ | `HTTP_NO_SHAPE=1` | The HTTP load generator (`bench/load.ts`) skips response-shape `JSON.parse` for pure-throughput runs. |
263
+
264
+ ---
265
+
266
+ ## HTTP Server Architecture
267
+
268
+ ```
269
+ bench/servers/
270
+ ├── bun-server.ts # Raw Bun.serve with manual routing
271
+ ├── elysia-server.ts # Elysia framework
272
+ ├── ingress-server.ts # Ingress (this project) optimized handler
273
+ └── shared.ts # Shared route handlers
274
+ ```
275
+
276
+ Each server implements the same routes for fair comparison:
277
+
278
+ - `GET /` — Plain text hello
279
+ - `GET /json` — JSON response
280
+ - `POST /json` — Echo JSON body
281
+ - `GET /id/:id` — Path parameter
282
+ - `POST /users` — User creation
283
+
284
+ ---
285
+
286
+ ## Environment
287
+
288
+ All benchmarks should be run on a dedicated machine for reproducible results:
289
+
290
+ - **CPU**: Modern x86_64 (Intel/AMD) or Apple Silicon
291
+ - **Memory**: 8GB+ RAM
292
+ - **OS**: Linux (recommended) or macOS
293
+ - **Bun**: Latest stable
294
+ - **Rust**: Latest stable (via rustup)
295
+
296
+ ### Turbo Mode (Linux)
297
+
298
+ For consistent results on Linux, disable CPU frequency scaling:
299
+
300
+ ```bash
301
+ sudo cpupower frequency-set --governor performance
302
+ ```
303
+
304
+ Run benchmarks, then restore:
305
+
306
+ ```bash
307
+ sudo cpupower frequency-set --governor powersave
@@ -0,0 +1,79 @@
1
+ # Environment Variables Reference
2
+
3
+ This document lists every environment variable read by the package and its
4
+ example servers. All values are read once at module load; invalid values are
5
+ warned about and replaced with the documented default.
6
+
7
+ ## Ingress server (`bench/servers/ingress-server.ts`)
8
+
9
+ | Variable | Type | Default | Description |
10
+ |----------|------|---------|-------------|
11
+ | `INGRESS_TRUST_PROXY` | bool | `false` | **Opt-in.** When `true`, honors `X-Forwarded-For` / `X-Real-IP` for client-IP resolution. Keep **off** unless the server sits behind a trusted edge that strips these headers, otherwise clients can spoof arbitrary IPs for IP-based rate limiting. |
12
+ | `INGRESS_REQUEST_ID_HEADER` | bool | `false` | Emit an `x-request-id` response header per request. The request ID is always generated internally; this only controls whether it is echoed to clients. |
13
+ | `INGRESS_UNSAFE_ZERO_COPY` | bool | `false` | Enable zero-copy response bodies. **Not recommended** unless per-response output buffers are implemented; leave off for safety (bodies are copied by default). |
14
+ | `INGRESS_OUTPUT_BUF_BYTES` | int | `131072` | Size of the Rust output buffer (min `65536`). If cookies + query + metadata JSON exceed this, `FLAG_BODY_TRUNCATED` is set and the request fails closed. |
15
+ | `INGRESS_SECURITY_HEADERS` | bool | `true` | Emit the precomputed security headers (CSP, HSTS, X-Frame-Options, etc.). Disable only behind an edge that already sets them. |
16
+ | `INGRESS_REUSE_PORT` | bool | `false` | Enable `SO_REUSEPORT` for multi-process deployments (spawn N processes, each with a distinct `INGRESS_WORKER_ID`). |
17
+ | `INGRESS_LOG_REQUESTS` | bool | `false` | Write one JSON line per request to **stderr** (`{ts, kind, requestId, method, path, status, errorCode?, rateLimited?, ...}`). |
18
+ | `INGRESS_METRICS` | bool | `false` | Write a periodic aggregate summary to **stderr** every 10s (`{ts, kind, total, rps, status, rateLimited, internalErrors}`). |
19
+ | `INGRESS_HOST` | string | `0.0.0.0` | Bind address. |
20
+ | `INGRESS_IDLE_TIMEOUT` | int | `30` | Idle keep-alive timeout in seconds (min `1`). |
21
+ | `INGRESS_WORKER_ID` | int | `0` | Worker identifier (0–65535), mixed into request-ID generation to reduce collision risk across processes. |
22
+
23
+ Boolean flags accept `1`/`true`/`yes`/`on` for true and `0`/`false`/`no`/`off`
24
+ for false; anything else is warned about and falls back to the default.
25
+
26
+ ## Rust FFI thread pool
27
+
28
+ | Variable | Type | Default | Description |
29
+ |----------|------|---------|-------------|
30
+ | `CASTRUM_RAYON_THREADS` / `RUST_RAYON_THREADS` (legacy `RUST_BENCH_RAYON_THREADS`) | int | `max(1, cores-1)` | Rayon thread pool size. The pool is process-wide and initialized **once** — the first use wins. With lazy init, call `rust.configure({ rayonThreads })` (or set this env) before the first batch operation. |
31
+ | `CASTRUM_MAX_RAYON_THREADS` (legacy `RUST_BENCH_MAX_RAYON_THREADS`) | int | `cores` | Hard cap on rayon threads (native). |
32
+ | `CASTRUM_PIN_CORES` (legacy `RUST_BENCH_PIN_CORES`) | (presence) | — | When set (Linux), pin rayon worker threads to distinct cores. |
33
+
34
+ ## Native transport selection (`src/native/ffi.ts`)
35
+
36
+ `bun:ffi` is the PRIMARY transport under Bun; NAPI is the fallback (Node,
37
+ `CASTRUM_FFI_MODE=napi`, or a failed bind-time self-test). `rust.transport()`
38
+ reports the resolved transport (`"ffi"` / `"napi"`); `rust.ffiActive()` reports
39
+ whether the ffi transport is live.
40
+
41
+ | Variable | Type | Default | Description |
42
+ |----------|------|---------|-------------|
43
+ | `CASTRUM_FFI_MODE` (legacy `RUST_FFI_MODE`) | string | `auto` | Native transport selection: `auto` (default) — use `bun:ffi` on Bun, silently fall back to napi on bind/self-test failure (and always on Node); `ffi` — force `bun:ffi` on Bun and THROW if it cannot bind or the self-test fails (for benches/CI that must run the primary transport; invalid on Node); `napi` — never bind, every call goes through the napi addon (exercises the fallback on Bun). |
44
+
45
+ ## Native addon loader (`src/native/loader.ts`)
46
+
47
+ | Variable | Type | Default | Description |
48
+ |----------|------|---------|-------------|
49
+ | `CASTRUM_DEBUG` (legacy `RUST_BENCH_DEBUG`) | (presence) | — | Log the resolved addon path and exported keys at load time. |
50
+ | `CASTRUM_NATIVE_LIBRARY_PATH` / `NAPI_RS_NATIVE_LIBRARY_PATH` | path | — | Explicit path (file or directory) to the native `.node` artifact, overriding normal resolution. Useful for bundled/electron layouts. |
51
+
52
+ ## Library logging (`src/shared/log.ts`)
53
+
54
+ | Variable | Type | Default | Description |
55
+ |----------|------|---------|-------------|
56
+ | `CASTRUM_LOG_LEVEL` | string | `info` | Threshold for the built-in structured logger (`debug`/`info`/`warn`/`error`/`silent`). Enabled per handler via the `structuredLog: true` runtime option; emits one JSON line per request/error to stderr (`{ts, level, event, requestId, method, path, status, durationMs, ...}`). |
57
+
58
+ ## Security guidance
59
+
60
+ - **Proxy trust**: `trustProxy: true` is deprecated — it trusts EVERY hop of
61
+ `X-Forwarded-For`/`X-Real-IP`, so clients can forge their IP and bypass IP-based
62
+ rate limiting. Prefer `trustedProxies: { enabled: true, networks: [...] }` and only
63
+ enable proxy trust behind a trusted edge that strips these headers.
64
+ - **Rate limiting is per-process, not distributed**: with N worker processes
65
+ (`reusePort`), an IP's effective limit is multiplied by N. The shared-limiter config
66
+ registry is bounded (LRU, max 16 distinct configs) to prevent unbounded memory growth.
67
+ - **Request bodies**: `createIngressServer` applies a server-level `maxRequestBodySize`
68
+ default of 16 MiB, and JSON-write routes stream-read with `maxBodyBytes` enforced
69
+ while reading (an oversized body is rejected as soon as the limit is crossed, never
70
+ fully buffered). Route/async handlers apply a non-zero body-read deadline by default
71
+ (`bodyTimeoutMs`, default **30 s** — set `bodyTimeoutMs: 0` to disable) that guards
72
+ against slowloris/trickling bodies.
73
+ - **Secrets (JWT/AEAD)**: keys and secrets are caller-owned buffers; treat them as
74
+ sensitive (never log them, rotate regularly). The AEAD **batch** APIs derive a unique
75
+ nonce per item, but callers MUST use a fresh base nonce for every batch call — never
76
+ reuse a base nonce with the same key.
77
+ - **Multipart**: `multipart_parse` accepts an optional `limits` object
78
+ (`max_parts`, `max_field_count`, `max_part_bytes`, `max_total_bytes`) with generous
79
+ defaults to bound worst-case parse cost.
@@ -0,0 +1,209 @@
1
+ # Getting Started with castrum
2
+
3
+ A walkthrough for a first-time contributor or intern. By the end you'll know
4
+ how to import the package, use the `rust.*` primitives, stand up an HTTP
5
+ server, run the benchmarks, and run the test suite. For the full file map see
6
+ [`REPO_MAP.md`](./REPO_MAP.md).
7
+
8
+ ## Prerequisites
9
+
10
+ - [Bun](https://bun.sh) ≥ 1.1 (primary runtime)
11
+ - [Rust](https://rustup.rs) — stable + `cargo` (to build/test the addon). The
12
+ repo pins a toolchain in `rust-toolchain.toml` (`channel = "1.97.1"`); install
13
+ it with `rustup` so local builds match CI.
14
+ - Node.js ≥ 20.3 (optional — only for the Node compatibility path)
15
+
16
+ > **Building on different OSes**: `bun run build` (napi) works on Linux, macOS,
17
+ > and Windows (WSL recommended on Windows). `bun run build:perf` shells out to
18
+ > `bash scripts/build-perf.sh` and is **Linux-only** — it is a local max-perf
19
+ > build, never for publish. If `bash` is unavailable, use plain `bun run build`.
20
+
21
+ ```bash
22
+ bun install
23
+ ```
24
+
25
+ ## 1. Import the package
26
+
27
+ The entry point is `index.ts` (Bun resolves it via the `bun` export condition).
28
+ Node users get the compiled `dist/index.js`.
29
+
30
+ ```ts
31
+ import { rust } from "castrum";
32
+ ```
33
+
34
+ ## 2. Use the Rust-accelerated primitives
35
+
36
+ The flat `rust.*` namespace wraps every native function:
37
+
38
+ ```ts
39
+ import { rust } from "castrum";
40
+
41
+ rust.crc32(new TextEncoder().encode("hello")); // number
42
+ rust.fnv1a64(bytes); // bigint
43
+ rust.jsonValid(bytes); // boolean
44
+ rust.validateEmail(emailBytes); // boolean
45
+
46
+ // Text helpers (string in / string out)
47
+ rust.text.mimeFromExtension(".js"); // "text/javascript"
48
+ rust.text.urlEncode("a b"); // "a%20b"
49
+
50
+ // Batch helpers (array of byte arrays → packed bitset/count)
51
+ rust.batch.jsonValid([docA, docB, docC]); // Uint8Array bitset
52
+ rust.batch.fnv1a64([a, b]); // BigUint64Array
53
+ rust.batch.etag([a, b]); // Uint8Array[]
54
+ ```
55
+
56
+ Most functions accept `Uint8Array` (or strings via the `text.` namespace).
57
+ Everything is **synchronous** — there is no async/await in the FFI surface.
58
+
59
+ > **Transport**: under Bun these calls run through `bun:ffi` (the PRIMARY
60
+ > transport — see `rust.transport()` / `rust.ffiActive()`); Node and any
61
+ > `CASTRUM_FFI_MODE=napi` fall back to the napi addon. Surfaces with no C-ABI
62
+ > export (instances, `rust.batch.*`/`packed.*`, `text.*`, structured scalars)
63
+ > always use the napi addon.
64
+
65
+ > **Tip**: `rust` is a singleton created with sensible defaults. If you need
66
+ > isolated state (own rayon threads, own HMAC cache), call
67
+ > `createRust({ rayonThreads: 4 })`.
68
+
69
+ ### Higher-order data loader (`loader`)
70
+
71
+ For workloads that call the same op many times, `loader` gives you a hot,
72
+ pre-bound function with automatic bulk-vs-scalar dispatch, DataLoader-style
73
+ coalescing and a bounded LRU cache — over **44 ops** including paired and
74
+ schema-validator ops:
75
+
76
+ ```ts
77
+ import { loader } from "castrum";
78
+
79
+ const isEmail = loader("validateEmail");
80
+ isEmail(emailBytes); // scalar → boolean
81
+ isEmail([a, b, c]); // bulk → ONE packed call
82
+ await isEmail.load(a); // coalesced + cached
83
+
84
+ loader.run("jsonPatch", docs, patches); // paired ops (companion array)
85
+ loader.schema(validator)(docs); // schema bitset (+ .count())
86
+ ```
87
+
88
+ See the README "Higher-order data loader" section for the full op table and
89
+ `createLoader` options.
90
+
91
+ ## 3. The `proven` performance surface
92
+
93
+ Not every `rust.*` function beats the JS baseline on every CPU. `proven`
94
+ exposes the **full** `rust.*` surface, and each function's JSDoc records its
95
+ measured performance vs the JS baseline — functions that are slower carry
96
+ `@deprecated`:
97
+
98
+ ```ts
99
+ import { proven, PROVEN_SURFACE } from "castrum";
100
+
101
+ proven.fnv1a64(bytes); // JSDoc: ~11x faster than the JS baseline
102
+ ```
103
+
104
+ `PROVEN_SURFACE` is the registry (pure data); `scripts/check-proven.ts`
105
+ audits it against the CPU benchmark so it can't drift, and
106
+ `scripts/annotate-performance.ts` (`bun run check:annotate`) refreshes the
107
+ per-function JSDoc from the latest report.
108
+
109
+ ## 4. Stand up an HTTP server (the ingress pipeline)
110
+
111
+ The quickest path is the **pre-baked** route handlers + the server builder:
112
+
113
+ ```ts
114
+ import { createIngressHandler, createIngressServer } from "castrum";
115
+
116
+ const ingress = createIngressHandler({
117
+ parseCookies: true,
118
+ parseQuery: true,
119
+ cors: { allowOrigin: ["https://app.example.com"] },
120
+ });
121
+
122
+ // createIngressServer is SYNCHRONOUS — it wraps Bun.serve over the routes.
123
+ // Each route maps HTTP methods to the pre-baked handlers (see below):
124
+ const server = createIngressServer({
125
+ port: 3000,
126
+ routes: {
127
+ "/health": { read: ingress }, // GET + HEAD
128
+ "/api/users": { read: ingress, write: ingress }, // + POST/PUT/PATCH (+OPTIONS)
129
+ },
130
+ });
131
+
132
+ console.log("listening on", server.port);
133
+ ```
134
+
135
+ - `read: ingress` — GET (+ HEAD) returns the ingress body JSON.
136
+ - `write: ingress` — POST/PUT/PATCH validates + echoes JSON.
137
+ - `echo: ingress`, `cookies: ingress`, and a top-level `fallback` are the other
138
+ route options. The underlying factories (`readHandler`, `jsonWriteHandler`,
139
+ `echoHandler`, `headHandler`, `fallbackHandler`) are also exported directly
140
+ if you'd rather wire methods by hand.
141
+ - **Bun-only**: `createIngressServer`. **Node**: use `createIngressServerNode`
142
+ with the same `routes` spec (and `await srv.ready` for the port).
143
+
144
+ The wire format is `{"ok":true,...,"requestId":...}` on success and
145
+ `{"ok":false,"error":{code,message}}` on errors, with `ratelimit-*` headers.
146
+
147
+ > There is also a lower-level **fast path** (`createIngressFast` +
148
+ > `createIngressSync`/`createIngress`). Prefer it only when you need the
149
+ > absolute lowest overhead and are comfortable calling `run()` yourself with a
150
+ > synchronous callback.
151
+
152
+ ## 5. Run the benchmarks
153
+
154
+ ```bash
155
+ # CPU benchmark (correctness checks + comparisons) — also gates `proven`
156
+ bun run check
157
+
158
+ # HTTP benchmark across all servers (bun / elysia / ingress)
159
+ bun run bench:http
160
+
161
+ # Fast HTTP smoke (the CI wire-format guard)
162
+ bun run bench:http:smoke
163
+ ```
164
+
165
+ Results land in `bench/results/` (gitignored).
166
+
167
+ ## 6. Run the tests
168
+
169
+ ```bash
170
+ # Everything at once (TS unit + Rust unit + typecheck + lint)
171
+ bun run test:all
172
+
173
+ bun test # TypeScript unit tests (test/unit/**)
174
+ bun run test:rust # Rust unit tests (cargo test)
175
+ bun run typecheck # TypeScript typecheck
176
+ bun run lint:ci # Biome lint gate (what CI enforces)
177
+ bun run test:node # Node.js integration tests (node --test) — run `bun run build:js` first
178
+ ```
179
+
180
+ ## 7. Make your first change
181
+
182
+ 1. Find where the code lives with [`REPO_MAP.md`](./REPO_MAP.md).
183
+ 2. Edit the TypeScript (`src/`) and/or Rust (`rust/`).
184
+ 3. If you changed Rust, rebuild the addon: `bun run build:debug`.
185
+ 4. Run `bun test`, `bun run typecheck`, and `cargo test`.
186
+ 5. If you touched any server or `handlers.ts`, run `bun run bench:http:smoke`.
187
+ 6. If you changed a public function, update the `PROVEN_SURFACE` registry
188
+ (`src/shared/proven.ts`) and run `bun run check:proven:fail` on a release
189
+ build (`bun run build`).
190
+ 7. Format + lint: `bun run format` and `bun run lint`.
191
+
192
+ ## Common gotchas
193
+
194
+ - The **two ingress paths** have different wire formats — don't unify them
195
+ (see REPO_MAP §4.2 and AGENTS.md).
196
+ - `tsconfig.json` typechecks `index.ts`, `bench.ts`, and `src/` (so `bench/` IS
197
+ covered by `bun run typecheck`); `test/` + `bench/` are covered by
198
+ `bun run typecheck:test` (`-p tsconfig.test.json`).
199
+ - `src/ingress/constants.ts` dlopens the addon at import time — that's
200
+ intentional.
201
+ - The native addon must be rebuilt (`bun run build:debug`) before tests that
202
+ touch Rust changes will see them.
203
+
204
+ ## Next steps
205
+
206
+ - [`REPO_MAP.md`](./REPO_MAP.md) — where everything is and how it connects
207
+ - [`INGRESS.md`](./INGRESS.md) — the full pre-baked ingress API
208
+ - [`ARCHITECTURE.md`](./ARCHITECTURE.md) — deep-dive internals
209
+ - [`AGENTS.md`](../AGENTS.md) — agent/contributor guidance