castrum 0.9.1 → 0.9.2
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.
- package/README.md +1 -1
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +239 -5
- package/dist/index.js +3852 -3375
- package/docs/ARCHITECTURE.md +11 -9
- package/docs/BENCHMARKS.md +70 -38
- package/docs/CASE_STUDY.md +10 -7
- package/docs/ENVIRONMENT.md +16 -1
- package/docs/FFI_BUN_GUIDE.md +18 -10
- package/docs/GETTING_STARTED.md +5 -4
- package/docs/HOW_TO_ADD_AN_OP.md +180 -0
- package/docs/INGRESS-ROUTER.md +3 -2
- package/docs/INGRESS.md +6 -3
- package/docs/NATIVE-ROUTE.md +28 -4
- package/docs/REPO_MAP.md +60 -30
- package/docs/adr/0001-two-wire-formats.md +1 -1
- package/docs/ai/LOCAL_DEV.md +81 -0
- package/docs/ai/TREE.md +1070 -0
- package/package.json +24 -23
- package/src/ingress/decode/result-base.ts +10 -2
- package/src/ingress/handlers.ts +45 -290
- package/src/ingress/headers/security.ts +41 -0
- package/src/ingress/index.ts +12 -148
- package/src/ingress/native-route.ts +182 -0
- package/src/ingress/packing/gather-raw-headers.ts +26 -2
- package/src/ingress/packing/route-wire.ts +197 -0
- package/src/ingress/packing/select-headers.ts +8 -1
- package/src/ingress/path-matcher.ts +115 -0
- package/src/ingress/response/baked-response.ts +302 -0
- package/src/ingress/router.ts +42 -2
- package/src/ingress/routes/common.ts +9 -1
- package/src/ingress/routes/json-write.ts +21 -18
- package/src/ingress/routes/native.ts +130 -0
- package/src/ingress/server-node.ts +1 -1
- package/src/ingress/server.ts +38 -99
- package/src/ingress/sync.ts +151 -0
- package/src/ingress/types.ts +9 -0
- package/src/native/ffi/build/codecs.ts +736 -0
- package/src/native/ffi/build/compress.ts +248 -0
- package/src/native/ffi/build/instances.ts +598 -0
- package/src/native/ffi/build/parse.ts +539 -0
- package/src/native/ffi/build/util.ts +140 -0
- package/src/native/ffi/build.ts +94 -0
- package/src/native/ffi/selftest.ts +11 -718
- package/src/native/ffi/types.ts +25 -0
- package/src/native/ffi.ts +36 -1320
- package/src/native/types/instances.ts +5 -0
- package/src/rust-ffi/packed.ts +38 -0
- package/src/rust-ffi/scalar/factories.ts +4 -0
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -31,9 +31,9 @@ The bridge between TypeScript and Rust has **two transports**, both loading the
|
|
|
31
31
|
cdylib (built with napi-rs):
|
|
32
32
|
|
|
33
33
|
- **`bun:ffi` (PRIMARY on Bun)** — `src/native/ffi.ts` `dlopen`s the addon's
|
|
34
|
-
`extern "C"` exports (`rust/ffi
|
|
34
|
+
`extern "C"` exports (`rust/ffi/`, 85 `castrum_*` symbols — 77 direct + 4
|
|
35
35
|
`validator_c_abi!` + 4 `compress_to_out!`, parity guarded by
|
|
36
|
-
`test/unit/
|
|
36
|
+
`test/unit/native/ffi-symbol-parity.test.ts`) and Bun JIT-calls them
|
|
37
37
|
directly (~10-20ns crossing vs ~100-350ns N-API). A bind-time self-test guards
|
|
38
38
|
correctness: on ANY failure the layer is disabled and calls fall back to napi.
|
|
39
39
|
`CASTRUM_FFI_MODE=auto|ffi|napi` selects the transport (default `auto`).
|
|
@@ -83,7 +83,8 @@ shared-infra names so legacy `crate::util::*` call sites keep working.
|
|
|
83
83
|
rust/
|
|
84
84
|
├── lib.rs ── Crate root: folder declarations + module map, global allocator
|
|
85
85
|
├── test_support.rs ── shared #[cfg(test)] helpers (pack_headers, decode_packed_pairs, Rng)
|
|
86
|
-
├──
|
|
86
|
+
├── panic_safety.rs ── #[cfg(test)] cross-parser fuzz tests (malformed input never panics)
|
|
87
|
+
├── proptest_suite.rs ── #[cfg(test)] property-based adversarial-parser tests
|
|
87
88
|
│
|
|
88
89
|
├── util/ ── Shared infrastructure (mod.rs re-exports crate::util::*)
|
|
89
90
|
│ ├── bytes.rs ── byte primitives (word-compare, hex, %XX decode, cookie_pairs)
|
|
@@ -126,9 +127,10 @@ rust/
|
|
|
126
127
|
│ │ (jsonschema cmp::equal; shared by jwt_verify, json_parse,
|
|
127
128
|
│ │ fast_schema enum/const/uniqueItems)
|
|
128
129
|
│ ├── json_ser.rs ── zero-alloc JSON escaping + cookie/query → JSON writers
|
|
129
|
-
│ ├──
|
|
130
|
-
│ │ add/remove/replace/
|
|
131
|
-
│ │ 1==1.0; json-patch/jsonptr
|
|
130
|
+
│ ├── patch/ ── CUSTOM sonic RFC 6902 engine (pointer.rs + ops.rs + engine.rs +
|
|
131
|
+
│ │ api.rs + tests.rs; Pointer ~0/~1 unescape + add/remove/replace/
|
|
132
|
+
│ │ move/copy/test incl. array `-`/bounds + 1==1.0; json-patch/jsonptr
|
|
133
|
+
│ │ deps REMOVED)
|
|
132
134
|
│ ├── json_schema.rs ── SchemaValidator napi class (fast + jsonschema fallback)
|
|
133
135
|
│ └── fast_schema/ ── zero-DOM draft-07 JSON Schema fast path
|
|
134
136
|
│ └── mod.rs + types.rs / cursor.rs / compile.rs / validate.rs / errors.rs / email.rs (format:"email" replica) / tests.rs
|
|
@@ -265,7 +267,7 @@ src/
|
|
|
265
267
|
> (`handle_request_packed`). Different wire format: success
|
|
266
268
|
> `{"ok":true,...,"requestId":...}`, errors
|
|
267
269
|
> `{"ok":false,"error":{code,message}}`, `ratelimit-*` headers. The
|
|
268
|
-
> benchmark server (`bench/servers/ingress-server.ts`) uses this path and
|
|
270
|
+
> benchmark server (`bench/http/servers/ingress-server.ts`) uses this path and
|
|
269
271
|
> re-exports the pre-baked functions.
|
|
270
272
|
|
|
271
273
|
## Node.js Compatibility
|
|
@@ -458,7 +460,7 @@ Offset (bytes)
|
|
|
458
460
|
- fast_schema enum/const/uniqueItems re-parse the doc into `sonic_rs::Value`
|
|
459
461
|
and compare with `sonic_values_equal` (no `Vec<serde_json::Value>`).
|
|
460
462
|
- minijinja contexts are `sonic_rs::Value` (implements `Serialize`).
|
|
461
|
-
- **Custom RFC 6902 patch engine** (`
|
|
463
|
+
- **Custom RFC 6902 patch engine** (`rust/json/patch/`): no longer round-trips
|
|
462
464
|
through the serde_json-DOM `json-patch` crate. A `Pointer` type (`~0`/`~1`
|
|
463
465
|
unescape, RFC 6901) + `PatchOp` parser + applicator (add incl. array `-` and
|
|
464
466
|
bounds, remove, replace, move, copy, test incl. `1 == 1.0`) runs directly on
|
|
@@ -497,7 +499,7 @@ Offset (bytes)
|
|
|
497
499
|
└─── [cookie_parser]
|
|
498
500
|
[hashing] ──── [hmac_sha256]
|
|
499
501
|
[validation]
|
|
500
|
-
[json_ops] ──── [
|
|
502
|
+
[json_ops] ──── [json/patch]
|
|
501
503
|
[json_schema]
|
|
502
504
|
[url_codec]
|
|
503
505
|
[mime_lookup]
|
package/docs/BENCHMARKS.md
CHANGED
|
@@ -76,17 +76,22 @@ bun run bench:http:all-heavy
|
|
|
76
76
|
bun run bench:http:ac # all servers, 03-stress weighted mix
|
|
77
77
|
bun run bench:http:stress:ac # 03-stress mix only
|
|
78
78
|
|
|
79
|
-
# Single-path static comparison (fastest, most stable signal
|
|
80
|
-
#
|
|
81
|
-
# AC_PATH=/api/users node bench/autocannon-stress.mjs
|
|
79
|
+
# Single-path static comparison (fastest, most stable signal — SERVER-BOUND
|
|
80
|
+
# by default with AC_CONNECTIONS=2000; use this for ceiling comparisons):
|
|
81
|
+
# AC_PATH=/api/users AC_RUNS=3 node bench/autocannon-stress.mjs
|
|
82
82
|
# AC_PATH=/api/users AC_METHOD=POST AC_BODY='{"id":1,"name":"x"}' \
|
|
83
|
-
# AC_CONTENT_TYPE=application/json
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
83
|
+
# AC_CONTENT_TYPE=application/json AC_RUNS=3 \
|
|
84
|
+
# node bench/autocannon-stress.mjs
|
|
85
|
+
# # median-of-N (see §Single-core): run-major interleaving — every server is
|
|
86
|
+
# # measured once per round, so the median compares servers over the same
|
|
87
|
+
# # load windows on a noisy host.
|
|
88
|
+
#
|
|
89
|
+
# The DYNAMIC 03-stress mix (default, no AC_PATH) is CLIENT-bound (~47k here —
|
|
90
|
+
# autocannon's setupRequest generation is the cap, identical across servers);
|
|
91
|
+
# use it only for shape/behavior validation, not ceiling comparisons.
|
|
87
92
|
#
|
|
88
93
|
# Multi-core scaling (SO_REUSEPORT — the way to exceed the single-core ceiling):
|
|
89
|
-
# AC_INSTANCES=4
|
|
94
|
+
# AC_INSTANCES=4 node bench/autocannon-stress.mjs
|
|
90
95
|
# (ingress/router only — they read INGRESS_REUSE_PORT=1)
|
|
91
96
|
```
|
|
92
97
|
|
|
@@ -98,8 +103,8 @@ default** (`BUN_CONFIG_MAX_HTTP_REQUESTS`, max 65,336). When a scenario's
|
|
|
98
103
|
and the run measures the *generator*, not the server — the classic signature is
|
|
99
104
|
a sub-ms `p50` with a multi-second tail and a flat RPS wall across every server.
|
|
100
105
|
|
|
101
|
-
- All `bench:http:*` scripts set `BUN_CONFIG_MAX_HTTP_REQUESTS=
|
|
102
|
-
- `bench/run-bench.ts` warns if a selected scenario exceeds the effective cap.
|
|
106
|
+
- All `bench:http:*` scripts set `BUN_CONFIG_MAX_HTTP_REQUESTS=65535` for you.
|
|
107
|
+
- `bench/http/run-bench.ts` warns if a selected scenario exceeds the effective cap.
|
|
103
108
|
- For **max-throughput** server comparisons use **autocannon**
|
|
104
109
|
(`bench/autocannon-stress.mjs`, run with `node`): it has no such cap, uses a
|
|
105
110
|
real socket pool, scales across worker threads, and reports RPS + latency
|
|
@@ -112,7 +117,7 @@ a sub-ms `p50` with a multi-second tail and a flat RPS wall across every server.
|
|
|
112
117
|
2. `result.workers` is a count (`opts.workers`), not an array — the report no
|
|
113
118
|
longer treats it as a per-worker array.
|
|
114
119
|
|
|
115
|
-
### Single-core ceiling & multi-core scaling (measured 2026-08-
|
|
120
|
+
### Single-core ceiling & multi-core scaling (measured 2026-08-21)
|
|
116
121
|
|
|
117
122
|
All four servers are **single-process `Bun.serve`** (one event loop), so they
|
|
118
123
|
pin at a per-core ceiling on this machine. The host is load-noisy, so use the
|
|
@@ -120,33 +125,60 @@ pin at a per-core ceiling on this machine. The host is load-noisy, so use the
|
|
|
120
125
|
every server is visited once per round, so a single slow window affects all
|
|
121
126
|
servers' run-`i` equally and the median compares servers over the same load
|
|
122
127
|
windows (the old server-major order biased later servers into noisier windows).
|
|
123
|
-
> **Dated snapshot (2026-08-16 ingress-RPS pass).** The host is load-noisy;
|
|
124
|
-
> the newest persisted autocannon medians under `bench/results/autocannon/*/final-*.median.md`
|
|
125
|
-
> supersede these numbers run-to-run. Use the median-of-N runner below for fresh
|
|
126
|
-
> comparisons.
|
|
127
|
-
|
|
128
|
-
Current medians (`AC_DURATION=8 AC_RUNS=3`, 500 connections, single instance):
|
|
129
128
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
> **Methodology (2026-08-21 — replaces the 2026-08-16 snapshot).** Two
|
|
130
|
+
> measurement traps were identified and the numbers below use the config that
|
|
131
|
+
> avoids both:
|
|
132
|
+
>
|
|
133
|
+
> 1. **The dynamic `setupRequest` mix is CLIENT-bound, not server-bound.** The
|
|
134
|
+
> 03-stress weighted mix caps every server at ~47k RPS on this host because
|
|
135
|
+
> autocannon's per-request `setupRequest` (random string generation + JSON
|
|
136
|
+
> stringify) is the bottleneck — all four servers report the identical
|
|
137
|
+
> ~47k. **Use static-path runs** (`AC_PATH=…`, `AC_METHOD=…`, `AC_BODY=…`)
|
|
138
|
+
> for ceiling comparisons; the mix only validates shape/behavior.
|
|
139
|
+
> 2. **`AC_PIPELINING>1` is a client/transport artifact under GET here.**
|
|
140
|
+
> With pipelining 10, even raw Bun collapses to ~8.6k on `GET /health`
|
|
141
|
+
> (autocannon pipelining + Bun's HTTP/1.1 handling), while the same server
|
|
142
|
+
> does 52k at pipelining 1 — so pipelining does NOT expose the ceiling on
|
|
143
|
+
> this stack. The server-bound config is **`AC_PIPELINING=1` + enough
|
|
144
|
+
> connections** (≥2000 on this host) to drive the event loop.
|
|
145
|
+
>
|
|
146
|
+
> The server-bound snapshot below is **static-path, `AC_CONNECTIONS=2000`,
|
|
147
|
+
> `AC_PIPELINING=1`, `AC_RUNS=3`** (median-of-3), single instance.
|
|
148
|
+
|
|
149
|
+
Current medians (static path, 2000 connections, pipelining 1, single instance):
|
|
150
|
+
|
|
151
|
+
- `GET /api/users?q=testquery&page=1` (query + cookies + CORS + metadata
|
|
152
|
+
envelope): **bun 52.5k, elysia 52.4k, ingress 95.9k, router 95.8k RPS** —
|
|
153
|
+
ingress/router ~**+83% vs bun**, with a tighter tail (router p99.9 195ms vs
|
|
154
|
+
bun 800ms).
|
|
133
155
|
- `POST /api/users` (JSON body, fast-path schema validation):
|
|
134
|
-
**bun
|
|
135
|
-
vs bun
|
|
136
|
-
the
|
|
137
|
-
body validation).
|
|
138
|
-
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
156
|
+
**bun 49.5k, elysia 49.5k, ingress 95.8k, router 93.5k RPS** — ~**+89–94%
|
|
157
|
+
vs bun**. The pre-optimization ingress POST gap (≈ −24% vs bun, 2026-08-16)
|
|
158
|
+
is long closed; the current gap is a large WIN (JS hot-path elimination +
|
|
159
|
+
single-pass body validation + copy-mode response default).
|
|
160
|
+
- **Response mode matters more than any remaining pipeline cost**: copy mode
|
|
161
|
+
(default) vs zero-copy on `POST /api/users`: **66.4k vs 45.1k RPS (+47%)** —
|
|
162
|
+
per-request `ReadableStream` construction is the cost, not the native body
|
|
163
|
+
write. Both servers default to copy mode; `INGRESS_ZERO_COPY=1` opts back in
|
|
164
|
+
for large-payload deployments.
|
|
165
|
+
- Per-request cost breakdown (`bun bench/cost/router-cost.ts` +
|
|
166
|
+
`router-minimal-breakdown.ts`, min-of-5): the router's minimal route `run()`
|
|
167
|
+
is ~310ns (native pruned pipeline ~115ns + response/run machinery ~150ns +
|
|
168
|
+
refresh 25ns + rid 12ns + header gather 4ns). A `syncResponder` experiment
|
|
169
|
+
that threaded a known-sync hint through `run()` was REVERTED as **slower**
|
|
170
|
+
(−6 to −94ns across 3 runs) — the per-request `assertSyncCallback` +
|
|
171
|
+
`instanceof Response` checks are already JIT-inlined and free on JSC; the
|
|
172
|
+
extra param/branch deoptimized the shared hot function. Conclusion: the
|
|
173
|
+
`run()` machinery is NOT real overhead.
|
|
144
174
|
- **To exceed the single-core ceiling**, run multiple server processes on the
|
|
145
|
-
same port via SO_REUSEPORT: `AC_INSTANCES=
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
autocannon on a separate host.
|
|
175
|
+
same port via SO_REUSEPORT: `AC_INSTANCES=N` (ingress/router only — they read
|
|
176
|
+
`INGRESS_REUSE_PORT=1`; bun/elysia do not set it). On this noisy host the
|
|
177
|
+
scaling signal is unreliable (4 instances: 52.8k median with one run at
|
|
178
|
+
91.9k; 8 instances: 48.5k — the load-noise band swallows the gain), so for
|
|
179
|
+
clean multi-core numbers run autocannon on a separate host. The mechanism
|
|
180
|
+
(N event loops sharing the accept queue) is the documented way to exceed the
|
|
181
|
+
single-process ceiling.
|
|
150
182
|
|
|
151
183
|
---
|
|
152
184
|
|
|
@@ -271,7 +303,7 @@ When comparing multiple frameworks, differences are expressed as:
|
|
|
271
303
|
|
|
272
304
|
```
|
|
273
305
|
bench.ts
|
|
274
|
-
└─ Loads benchmark tasks from src/bench/tasks/ (
|
|
306
|
+
└─ Loads benchmark tasks from src/bench/tasks/ (34 tasks)
|
|
275
307
|
├── hashing.ts Compare Rust fnv1a64 vs JS TextEncoder + hash
|
|
276
308
|
├── validation.ts Compare Rust validation vs validator library
|
|
277
309
|
├── json.ts Compare Rust sonic-rs vs JSON.parse
|
|
@@ -294,14 +326,14 @@ Each task runs:
|
|
|
294
326
|
| Env / flag | Effect |
|
|
295
327
|
|------------|--------|
|
|
296
328
|
| `CASTRUM_BENCH_BATCH_SIZE` | Batch size for sub-µs operations (default `64`). Batching amortizes the timer/measurement overhead for very fast ops. |
|
|
297
|
-
| `HTTP_NO_SHAPE=1` | The HTTP load generator (`bench/load.ts`) skips response-shape `JSON.parse` for pure-throughput runs. |
|
|
329
|
+
| `HTTP_NO_SHAPE=1` | The HTTP load generator (`bench/http/load.ts`) skips response-shape `JSON.parse` for pure-throughput runs. |
|
|
298
330
|
|
|
299
331
|
---
|
|
300
332
|
|
|
301
333
|
## HTTP Server Architecture
|
|
302
334
|
|
|
303
335
|
```
|
|
304
|
-
bench/servers/
|
|
336
|
+
bench/http/servers/
|
|
305
337
|
├── bun-server.ts # Raw Bun.serve with manual routing
|
|
306
338
|
├── elysia-server.ts # Elysia framework
|
|
307
339
|
├── ingress-server.ts # Ingress (this project) optimized handler
|
package/docs/CASE_STUDY.md
CHANGED
|
@@ -42,7 +42,7 @@ as a pre-baked pipeline, and still be usable from both runtimes.
|
|
|
42
42
|
### 2.1 One cdylib, two transports
|
|
43
43
|
|
|
44
44
|
The addon is built with napi-rs (so Node can load it), but it *also* exports
|
|
45
|
-
`extern "C"` symbols (`rust/ffi
|
|
45
|
+
`extern "C"` symbols (`rust/ffi/`, **85 `castrum_*` symbols** — 77 direct + 4
|
|
46
46
|
`validator_c_abi!` + 4 `compress_to_out!`) so Bun can
|
|
47
47
|
`dlopen` it directly:
|
|
48
48
|
|
|
@@ -56,7 +56,7 @@ The addon is built with napi-rs (so Node can load it), but it *also* exports
|
|
|
56
56
|
|
|
57
57
|
```mermaid
|
|
58
58
|
flowchart LR
|
|
59
|
-
TS["TypeScript (index.ts)"] -->|"bun:ffi ·
|
|
59
|
+
TS["TypeScript (index.ts)"] -->|"bun:ffi · 85 C-ABI symbols · ~10–20 ns"| RUST["Rust cdylib (rust/ffi/)"]
|
|
60
60
|
TS -.->|"NAPI fallback (Node / self-test failure)"| RUST
|
|
61
61
|
RUST --> PIPELINE["8-stage ingress pipeline"]
|
|
62
62
|
TS --> PACKED["Packed, length-prefixed buffers (zero-copy)"]
|
|
@@ -134,18 +134,21 @@ native or JS **once at load time** instead of per call.
|
|
|
134
134
|
|
|
135
135
|
### 3.3 HTTP: the pipeline holds up under load
|
|
136
136
|
|
|
137
|
-
From `bench/results/*/03-stress.bench.md` (ramp to 10k concurrent, ~104 s
|
|
137
|
+
From `bench/results/*/03-stress.bench.md` (ramp to 10k concurrent, ~104 s;
|
|
138
|
+
numbers below are the latest run, 2026-08-21 — re-run
|
|
139
|
+
`bun run bench:http:stress` to refresh):
|
|
138
140
|
|
|
139
141
|
| Server | Achieved RPS | Total requests | Unexpected errors | Shape failures |
|
|
140
142
|
|--------|-------------|----------------|-------------------|----------------|
|
|
141
|
-
| raw `Bun.serve` |
|
|
142
|
-
| **castrum ingress** | **
|
|
143
|
+
| raw `Bun.serve` | 929.26 | 96,362 | 0 | 0 |
|
|
144
|
+
| **castrum ingress** | **928.65** | **96,369** | **0** | **0** |
|
|
143
145
|
|
|
144
146
|
Under extreme concurrency both saturate and show long tail latencies (p99 ~18 s
|
|
145
147
|
on this machine) — that's the load generator over-driving the box, not a bug.
|
|
146
148
|
The meaningful result is that adding the full pipeline (CORS + rate limit +
|
|
147
149
|
body guard + schema + security headers + request IDs) costs **no correctness
|
|
148
|
-
and no measurable throughput** versus a hand-rolled raw server
|
|
150
|
+
and no measurable throughput** versus a hand-rolled raw server (the two are at
|
|
151
|
+
parity, within noise).
|
|
149
152
|
|
|
150
153
|
### 3.4 The bug that almost killed the server: C-ABI panics
|
|
151
154
|
|
|
@@ -155,7 +158,7 @@ The most instructive moment: under `11-concurrent-burst`, the ingress server
|
|
|
155
158
|
exception, but a panic unwinding through raw `extern "C"` is undefined behaviour
|
|
156
159
|
that takes the whole process down.
|
|
157
160
|
|
|
158
|
-
The fix (in `rust/ffi
|
|
161
|
+
The fix (in `rust/ffi/`): every fallible / allocating C-ABI export now routes
|
|
159
162
|
its core through `panic_guard` (`catch_unwind`) and reports a sentinel instead
|
|
160
163
|
of panicking. After the fix, the same scenario completes cleanly:
|
|
161
164
|
|
package/docs/ENVIRONMENT.md
CHANGED
|
@@ -4,7 +4,7 @@ This document lists every environment variable read by the package and its
|
|
|
4
4
|
example servers. All values are read once at module load; invalid values are
|
|
5
5
|
warned about and replaced with the documented default.
|
|
6
6
|
|
|
7
|
-
## Ingress server (`bench/servers/ingress-server.ts`)
|
|
7
|
+
## Ingress server (`bench/http/servers/ingress-server.ts`)
|
|
8
8
|
|
|
9
9
|
| Variable | Type | Default | Description |
|
|
10
10
|
|----------|------|---------|-------------|
|
|
@@ -57,6 +57,21 @@ whether the ffi transport is live.
|
|
|
57
57
|
|----------|------|---------|-------------|
|
|
58
58
|
| `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, ...}`). |
|
|
59
59
|
|
|
60
|
+
## Benchmark controls (`bench/`, `src/bench/`)
|
|
61
|
+
|
|
62
|
+
| Variable | Type | Default | Description |
|
|
63
|
+
|----------|------|---------|-------------|
|
|
64
|
+
| `CASTRUM_BENCH_BATCH_SIZE` | int | `64` | Fast-op batch size for the CPU bench's measurement loop (`src/bench/measure.ts`); for methodology experiments. |
|
|
65
|
+
| `CASTRUM_BENCH_FFI` | bool | — | When `1`, the CPU bench treats an unavailable FFI transport as a hard failure instead of a warning (`src/bench/run.ts`). |
|
|
66
|
+
| `CASTRUM_FFI_LOAD_MS` | int | `400` | Per-window duration (ms) of the `ffi-all` load-run (`bench/ffi/ffi-all.ts`). |
|
|
67
|
+
| `CASTRUM_FFI_LOAD_WINDOWS` | int | `4` | Number of load windows in `ffi-all` (`bench/ffi/ffi-all.ts`). |
|
|
68
|
+
|
|
69
|
+
## Publishing (`scripts/prepublish.mjs`, `publish-manual.mjs`)
|
|
70
|
+
|
|
71
|
+
| Variable | Type | Default | Description |
|
|
72
|
+
|----------|------|---------|-------------|
|
|
73
|
+
| `CASTRUM_PUBLISH_ALLOW_PARTIAL` | bool | — | When `1`, `prepublishOnly` ships a tarball containing only the locally-built platforms instead of failing on the full `napi.targets` set. Set automatically by `bun run publish:manual`; **never** set it for a normal `npm publish`.
|
|
74
|
+
|
|
60
75
|
## Security guidance
|
|
61
76
|
|
|
62
77
|
- **Proxy trust**: `trustProxy: true` is deprecated — it trusts EVERY hop of
|
package/docs/FFI_BUN_GUIDE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Writing Rust for Bun — FFI Optimization Guide
|
|
2
2
|
|
|
3
3
|
> **Who this is for**: anyone (human or agent) writing or reviewing the Rust
|
|
4
|
-
> `extern "C"` surface in `rust/ffi
|
|
4
|
+
> `extern "C"` surface in `rust/ffi/` and its `bun:ffi` binding in
|
|
5
5
|
> `src/native/ffi.ts`. The rules below are the concrete, citable behaviors of
|
|
6
6
|
> Bun's own `bun:ffi` implementation, distilled so that future generated Rust
|
|
7
7
|
> code is **optimized for Bun** out of the box — correct on the first `dlopen`,
|
|
@@ -28,7 +28,9 @@ JS shim:
|
|
|
28
28
|
in-engine … with no per-argument JavaScript shim" (`docs/runtime/ffi.mdx`,
|
|
29
29
|
"Performance"). This is why the crossing is ~10–20 ns vs ~100–350 ns for a
|
|
30
30
|
Node-API call, and why Bun's own bench puts `bun:ffi` at 2–6× faster than
|
|
31
|
-
Node's N-API (`bench/ffi/bun.js
|
|
31
|
+
Node's N-API (upstream bench `bench/ffi/bun.js` in the Bun runtime checkout
|
|
32
|
+
`/home/adeel/poc/bun`, plus `docs/runtime/ffi.mdx`; castrum's own FFI-vs-napi
|
|
33
|
+
margin bench is `bench/ffi/ffi-margin.ts`).
|
|
32
34
|
- The `#[no_mangle] extern "C"` symbols come from the **same cdylib** that
|
|
33
35
|
napi-rs loads under Node — the two transports are two views of one binary.
|
|
34
36
|
|
|
@@ -168,7 +170,7 @@ string into a JS `string` at call time and returns that; `NULL` → `null`
|
|
|
168
170
|
copies **nothing itself** — the C side must own the memory until the call
|
|
169
171
|
returns.
|
|
170
172
|
|
|
171
|
-
- **castrum's application**: the per-thread reused `CSTR_BUF` in `rust/ffi
|
|
173
|
+
- **castrum's application**: the per-thread reused `CSTR_BUF` in `rust/ffi/`
|
|
172
174
|
(`cstring_return`) is exactly right — the clone is synchronous, so reusing
|
|
173
175
|
the buffer across calls is safe.
|
|
174
176
|
- **Aliasing caveat**: a C function that returns a pointer *derived from a
|
|
@@ -204,7 +206,7 @@ all bound symbols are unreachable.
|
|
|
204
206
|
## 8. Thread-safety & per-thread caches
|
|
205
207
|
|
|
206
208
|
`bun:ffi` symbols are shared across Worker threads (same dlopen'd cdylib
|
|
207
|
-
mapping; verified by castrum's `bench/ffi-workers.ts` — 4 workers, no
|
|
209
|
+
mapping; verified by castrum's `bench/ffi/ffi-workers.ts` — 4 workers, no
|
|
208
210
|
thread-safety issue). The Rust side must therefore keep any per-call mutable
|
|
209
211
|
state **thread-local**, never global:
|
|
210
212
|
|
|
@@ -267,7 +269,7 @@ These are not optional — they are what make the C-ABI surface safe on Bun (no
|
|
|
267
269
|
napi-style unwind guard exists for raw `extern "C"`).
|
|
268
270
|
|
|
269
271
|
1. **`panic_guard` every fallible/allocating core** (`std::panic::catch_unwind`
|
|
270
|
-
in `rust/ffi
|
|
272
|
+
in `rust/ffi/`, `AssertUnwindSafe` for captured `&mut [u8]`). An uncaught
|
|
271
273
|
panic unwinding through `extern "C"` kills the whole Bun process — this is
|
|
272
274
|
what happened under `11-concurrent-burst`.
|
|
273
275
|
2. **Bind-time self-test for EVERY new symbol** (`src/native/ffi/selfTest`).
|
|
@@ -307,9 +309,15 @@ that transfer directly to castrum's cdylib:
|
|
|
307
309
|
|
|
308
310
|
## 13. Version anchors & verification
|
|
309
311
|
|
|
310
|
-
|
|
312
|
+
The behavioral claims above are verified against the Rust-based Bun source
|
|
313
|
+
checkout at `/home/adeel/poc/bun` (LATEST file: 1.3.14; `package.json` tracks
|
|
314
|
+
the 1.4 development line). The decision matrix and ADR-0003 measure against
|
|
315
|
+
the installed **Bun 1.4.0** runtime — both anchors describe the same
|
|
316
|
+
Rust-based runtime family. Castrum's own measurements live in `bench/`.
|
|
317
|
+
|
|
318
|
+
| Claim | Source (Bun checkout `/home/adeel/poc/bun`) |
|
|
311
319
|
|---|---|
|
|
312
|
-
| JIT-to-direct-native-call, 2–6× vs NAPI | `docs/runtime/ffi.mdx` → Performance; `bench/ffi/bun.js` |
|
|
320
|
+
| JIT-to-direct-native-call, 2–6× vs NAPI | `docs/runtime/ffi.mdx` → Performance; `bench/ffi/bun.js` (upstream) |
|
|
313
321
|
| `FFIType` numeric tags (`buffer`=20, `buffer_length`=21, `u64_fast`=16) | `src/js/bun/ffi.ts`; static asserts in `src/jsc/bindings/JSCFFIBridge.cpp` |
|
|
314
322
|
| `buffer`/`buffer_length` atomic snapshot, arg-only | `packages/bun-types/ffi.d.ts`; `src/runtime/ffi/ffi_body.rs::reject_cc_unsupported_types_error` |
|
|
315
323
|
| Pointers are `number`; BigInt converted to number | `docs/runtime/ffi.mdx` → Pointers |
|
|
@@ -320,8 +328,8 @@ that transfer directly to castrum's cdylib:
|
|
|
320
328
|
**Verify after any FFI change**:
|
|
321
329
|
|
|
322
330
|
```bash
|
|
323
|
-
nm -D --defined-only <addon> | grep -c castrum_ # =
|
|
331
|
+
nm -D --defined-only <addon> | grep -c castrum_ # = 85
|
|
324
332
|
bun run bench:http:smoke # after touching decoders/handlers
|
|
325
|
-
bun test test/unit/
|
|
326
|
-
bun test test/unit/
|
|
333
|
+
bun test test/unit/native/ffi.test.ts # FFI↔napi parity + self-test
|
|
334
|
+
bun test test/unit/native/ffi-symbol-parity.test.ts
|
|
327
335
|
```
|
package/docs/GETTING_STARTED.md
CHANGED
|
@@ -60,8 +60,10 @@ Everything is **synchronous** — there is no async/await in the FFI surface.
|
|
|
60
60
|
> transport — see `rust.transport()` / `rust.ffiActive()`); Node and any
|
|
61
61
|
> `CASTRUM_FFI_MODE=napi` fall back to the napi addon. Since Phase 6 the
|
|
62
62
|
> compiled-once instances are NOT napi-only — their hot per-call ops route
|
|
63
|
-
> through the C-ABI via opaque-`usize` handles.
|
|
64
|
-
> `
|
|
63
|
+
> through the C-ABI via opaque-`usize` handles. `rust.text.*`
|
|
64
|
+
> (`mimeFromExtension`, `wsAcceptKey`, `validateEmail`/`validateUuid`/…) and
|
|
65
|
+
> the DOM `jsonParse` are FFI-first today; still napi-only:
|
|
66
|
+
> `rust.batch.*` and `rust.packed.*`.
|
|
65
67
|
|
|
66
68
|
> **Tip**: `rust` is a singleton created with sensible defaults. If you need
|
|
67
69
|
> isolated state (own rayon threads, own HMAC cache), call
|
|
@@ -86,8 +88,7 @@ loader.run("jsonPatch", docs, patches); // paired ops (companion array
|
|
|
86
88
|
loader.schema(validator)(docs); // schema bitset (+ .count())
|
|
87
89
|
```
|
|
88
90
|
|
|
89
|
-
See
|
|
90
|
-
`createLoader` options.
|
|
91
|
+
See docs/API.md — [Higher-order data loader](API.md#higher-order-data-loader-loader--createloader) — for the full op table and `createLoader` options.
|
|
91
92
|
|
|
92
93
|
## 3. Stand up an HTTP server (the ingress pipeline)
|
|
93
94
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# HOW TO ADD AN OP (runbook)
|
|
2
|
+
|
|
3
|
+
This is the single runbook for adding a new native operation to `castrum` end
|
|
4
|
+
to end. It consolidates the six places a new op must be wired, so a contributor
|
|
5
|
+
(or AI agent) knows exactly what to touch and in what order. "Op" = a
|
|
6
|
+
performance-critical primitive (`crc32`, `hmacSha256`, `jsonValid`,
|
|
7
|
+
`gzipCompress`, `validateEmail`, …) exposed through the native addon.
|
|
8
|
+
|
|
9
|
+
The golden rule: **a new op must be reachable from the public `rust.*` surface,
|
|
10
|
+
bound on BOTH transports (bun:ffi primary + napi fallback), pinned by a
|
|
11
|
+
bind-time self-test, and listed in the op-selection metadata** — or the parity
|
|
12
|
+
guards fail.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 0. The six wiring points at a glance
|
|
17
|
+
|
|
18
|
+
| # | File | What you add |
|
|
19
|
+
|---|------|--------------|
|
|
20
|
+
| 1 | `rust/<domain>/<module>.rs` (+ `mod.rs`) | The pure-Rust core (no napi types — unit-testable) |
|
|
21
|
+
| 2 | `rust/ffi/<domain>.rs` (+ `rust/ffi/util.rs`) | The `#[no_mangle] extern "C"` export (`panic_guard`, correct ABI) |
|
|
22
|
+
| 3 | `src/native/ffi.ts` + `src/native/ffi/types.ts` + `src/native/ffi/build/<domain>.ts` | The `bun:ffi` dlopen map entry + `BunFFI` method + per-domain self-test check |
|
|
23
|
+
| 4 | `src/rust-ffi/scalar/<area>.ts` (or `text`/`batch`/`packed`) | The public `rust.*` wrapper |
|
|
24
|
+
| 5 | `src/selection.json` + `src/shared/proven.ts` + `src/selection.ts` | The baked op-selection / delegation metadata |
|
|
25
|
+
| 6 | `test/unit/native/ffi.test.ts` + `test/unit/contract/wiring.test.ts` | The parity tests |
|
|
26
|
+
|
|
27
|
+
Follow the numbered sections below. When you're done, the parity gates
|
|
28
|
+
(§7) are your safety net.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. Add a new Rust module (the core)
|
|
33
|
+
|
|
34
|
+
Create the pure-Rust core in its domain folder, e.g. `rust/crypto/hmac_sha256.rs`.
|
|
35
|
+
Keep **napi types out of the core** — internal signatures use `&[u8]` /
|
|
36
|
+
`&str` / `Result<_, String>` so it stays unit-testable with plain `cargo test`.
|
|
37
|
+
|
|
38
|
+
```rust
|
|
39
|
+
/// Compute an FNV-1a 64-bit hash of `input`.
|
|
40
|
+
#[inline]
|
|
41
|
+
pub fn fnv1a64_bytes(input: &[u8]) -> u64 { /* ... */ }
|
|
42
|
+
|
|
43
|
+
#[cfg(test)]
|
|
44
|
+
mod tests {
|
|
45
|
+
use super::*;
|
|
46
|
+
#[test]
|
|
47
|
+
fn standard_vectors() { /* ... */ }
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- Register the module in `rust/lib.rs` (the module map comment) and in the
|
|
52
|
+
folder's `mod.rs` re-exports.
|
|
53
|
+
- Ship `#[cfg(test)] mod tests` in the SAME file (cross-module tests live in
|
|
54
|
+
`rust/panic_safety.rs` / `rust/proptest_suite.rs`; shared helpers in
|
|
55
|
+
`rust/test_support.rs`).
|
|
56
|
+
- If it needs a napi-visible class or batch, add a thin `api.rs` boundary in
|
|
57
|
+
the module folder (name it `api`, **not** `napi` — that collides with the
|
|
58
|
+
`napi` crate under `use super::*`).
|
|
59
|
+
|
|
60
|
+
## 2. Expose it over the C ABI (bun:ffi primary)
|
|
61
|
+
|
|
62
|
+
Add the `#[no_mangle] extern "C"` export in `rust/ffi/<domain>.rs`. Every
|
|
63
|
+
fallible / allocating export **must** route its core through `panic_guard`
|
|
64
|
+
(defined in `rust/ffi/util.rs`) so an uncaught panic can't unwind through
|
|
65
|
+
`extern "C"` and kill the process.
|
|
66
|
+
|
|
67
|
+
Rules (see `docs/FFI_BUN_GUIDE.md` for the full contract):
|
|
68
|
+
- Keep signatures **scalar + `(ptr,len)`** — no struct-by-value, no variadics.
|
|
69
|
+
- Null-check pointers and opaque handles (return `0` / `null` instead of
|
|
70
|
+
dereferencing).
|
|
71
|
+
- Byte-count returns MUST use `u64_fast` (`U64_FAST` in ffi.ts); opaque handles
|
|
72
|
+
pass as `usize`/number.
|
|
73
|
+
- String ARGS come in as `*const c_char` (bun:ffi `cstring`); use
|
|
74
|
+
`CStr::from_ptr(header).to_bytes()`.
|
|
75
|
+
- String RETURNS use `cstring_return` (per-thread `CSTR_BUF`, cloned at call
|
|
76
|
+
time — never return a pointer into JS-owned memory).
|
|
77
|
+
- **Variable-size outputs**: return the EXACT required byte count on a
|
|
78
|
+
too-small buffer (0 = real error) so JS allocates once and retries at most
|
|
79
|
+
once.
|
|
80
|
+
|
|
81
|
+
```rust
|
|
82
|
+
/// FNV-1a 64-bit hash → u64.
|
|
83
|
+
#[no_mangle]
|
|
84
|
+
pub unsafe extern "C" fn castrum_fnv1a64(data: *const u8, len: usize) -> u64 {
|
|
85
|
+
if data.is_null() { return 0; }
|
|
86
|
+
panic_guard(
|
|
87
|
+
|| crate::crypto::hashing::fnv1a64_bytes(std::slice::from_raw_parts(data, len)),
|
|
88
|
+
0,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- A `#[no_mangle]` symbol emits regardless of module path — no re-export needed
|
|
94
|
+
for the C ABI itself (re-exports are only needed for unit tests to reach the
|
|
95
|
+
item).
|
|
96
|
+
|
|
97
|
+
## 3. Bind it on the bun:ffi path (transport core + build folder)
|
|
98
|
+
|
|
99
|
+
`src/native/ffi.ts` owns the `dlopen` symbol map (`bind()`). Add your symbol
|
|
100
|
+
key (the EXACT `castrum_*` name) to that map with its ABI spec.
|
|
101
|
+
|
|
102
|
+
Then add the ergonomic method where it belongs in the decomposed bind surface:
|
|
103
|
+
- `src/native/ffi/build/codecs.ts` — hashing / codec / crypto / auth
|
|
104
|
+
- `src/native/ffi/build/compress.ts` — gzip/brotli + jsonPatch
|
|
105
|
+
- `src/native/ffi/build/parse.ts` — packed parsers + media-type/http-date/accept
|
|
106
|
+
- `src/native/ffi/build/instances.ts` — opaque-handle evals (schema/jwt/template/route/ingress)
|
|
107
|
+
|
|
108
|
+
Add the method signature to the `BunFFI` interface in
|
|
109
|
+
`src/native/ffi/types.ts` (use a `Raw2..Raw12` / `RawCStr` signature for the
|
|
110
|
+
raw symbol, or add a new one if the arity doesn't fit).
|
|
111
|
+
|
|
112
|
+
**Add the self-test next to the binding**: each `build/*.ts` file exports a
|
|
113
|
+
`selfTest<Domain>` function; add a known-good vector check for your method
|
|
114
|
+
there. `src/native/ffi/selftest.ts` is a thin aggregator that ANDs all four
|
|
115
|
+
domain self-tests — a new symbol is bound **and** self-tested in one file.
|
|
116
|
+
|
|
117
|
+
## 4. Expose it on the public `rust.*` surface
|
|
118
|
+
|
|
119
|
+
`src/rust-ffi/scalar/<area>.ts` (or `text.ts` / `batch/` / `packed.ts`) builds
|
|
120
|
+
the public wrapper. The wrapper normalizes the string-vs-bytes return
|
|
121
|
+
divergence (`toBytes`/`toText` — `src/shared/bytes.ts`), adds the pooled
|
|
122
|
+
`*Into` variants where hot, and (optionally) delegates to Bun built-ins when
|
|
123
|
+
`isBun()` via `src/runtime/builtins.ts` (`BUILTIN_OPS`).
|
|
124
|
+
|
|
125
|
+
Add a JSDoc block (`@param`/`@returns`/`@example`) — `bun run check:jsdoc`
|
|
126
|
+
enforces a 95% floor on exported symbols.
|
|
127
|
+
|
|
128
|
+
## 5. Op-selection metadata (baked)
|
|
129
|
+
|
|
130
|
+
- `src/selection.json` — the machine-readable selection row for the op
|
|
131
|
+
(native/js/bun winner). Generated by `scripts/select-native.ts --write`.
|
|
132
|
+
- **GOTCHA**: `select-native.ts`'s `OPS` array is the source of truth for what
|
|
133
|
+
stays in `selection.json`. ANY op with a baked native/js decision MUST be
|
|
134
|
+
listed there (or as a `skip+pinned` entry) or `--write` DELETES it from the
|
|
135
|
+
selection. If you add an op to `rust/selection.rs`, add it to `OPS` too.
|
|
136
|
+
- `src/shared/proven.ts` — `PROVEN_SELECTION` (PURE DATA) maps each op to its
|
|
137
|
+
proven winner (`native` / `js` / `bun`). Add/update the entry.
|
|
138
|
+
- `src/selection.ts` — `BUN_WINS` delegation registry (which ops delegate to
|
|
139
|
+
Bun built-ins under Bun). Keep `src/runtime/builtins.ts` `BUILTIN_OPS` in
|
|
140
|
+
sync.
|
|
141
|
+
|
|
142
|
+
## 6. Parity tests
|
|
143
|
+
|
|
144
|
+
- `test/unit/native/ffi.test.ts` — pin the ffi-vs-napi byte-parity for the new
|
|
145
|
+
op (the transport self-test only guards bind-time vectors; this pins real
|
|
146
|
+
parity against a live napi instance).
|
|
147
|
+
- `test/unit/contract/wiring.test.ts` — verifies each baked `PROVEN_SELECTION`
|
|
148
|
+
winner is actually wired (`opImpl(op)` agrees, `builtins.has(op)` matches,
|
|
149
|
+
native/js entries match the addon's embedded `selection.json`).
|
|
150
|
+
- `test/unit/native/ffi-symbol-parity.test.ts` — scans `rust/ffi/` for every
|
|
151
|
+
`castrum_*` symbol and asserts it's bound in the ffi.ts map AND exercised by
|
|
152
|
+
the self-test. New symbol not bound → this fails.
|
|
153
|
+
|
|
154
|
+
## 7. Run the gates
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
cargo test # Rust cores + C-ABI tests
|
|
158
|
+
bun test # TS suite (incl. the parity tests)
|
|
159
|
+
bunx tsc --noEmit && bunx tsc --noEmit -p tsconfig.test.json
|
|
160
|
+
bun run lint # Biome
|
|
161
|
+
bun run check:jsdoc # ≥95% JSDoc on exported symbols
|
|
162
|
+
bun run check:clean # module headers, runtime seam, purity, doc links
|
|
163
|
+
bun run check:version # package.json ↔ Cargo.toml ↔ CHANGELOG
|
|
164
|
+
bun run bench:http:smoke # if you touched the ingress path
|
|
165
|
+
bun run bench:startup # if you touched the transport/loader
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Checklist
|
|
171
|
+
|
|
172
|
+
- [ ] Pure-Rust core in the right domain folder with `#[cfg(test)] mod tests`
|
|
173
|
+
- [ ] `#[no_mangle] extern "C"` export with `panic_guard` + null checks +
|
|
174
|
+
correct `u64_fast`/cstring ABI
|
|
175
|
+
- [ ] Symbol in the `ffi.ts` dlopen map + `BunFFI` interface method
|
|
176
|
+
- [ ] Per-domain `selfTest*` vector in the same `ffi/build/*.ts` file
|
|
177
|
+
- [ ] Public `rust.*` wrapper with JSDoc (+ `*Into` pooled variant if hot)
|
|
178
|
+
- [ ] `selection.json` + `PROVEN_SELECTION` + `BUILTIN_OPS` (if delegating)
|
|
179
|
+
- [ ] `ffi.test.ts` + `wiring.test.ts` parity coverage
|
|
180
|
+
- [ ] All §7 gates green
|
package/docs/INGRESS-ROUTER.md
CHANGED
|
@@ -38,6 +38,7 @@ wire route factories over that route's compiled handler:
|
|
|
38
38
|
| `cookies` | GET read (cookies-style) handler |
|
|
39
39
|
| `delete` | DELETE read-style handler |
|
|
40
40
|
| `responder` | JS responder route (`methods`, `readBody`, `terminalStyle`) |
|
|
41
|
+
| `native` | LEAN native-stack responder route: the route-wire v3 per-route stack (`createNativeRoute`) runs ONLY the plan's stages (parseQuery/parseCookies/requireJsonBody/validateBody) in ONE native call — no CORS/rate-limit/security/IP/metadata envelope. The responder builds the 2xx from the decoded snapshot. Measured ~580ns cheaper per request than the full-pipeline responder on a parseQuery+parseCookies route, and **+34% RPS at the HTTP level** on the bench server's `/api/native` vs `/api/users` (server-bound config). |
|
|
41
42
|
| `raw` | Raw `Request → Response` handler served OUTSIDE the pipeline (health/metrics probes) |
|
|
42
43
|
| `maxBodyBytes` / `bodyTimeoutMs` | Per-route write/echo overrides |
|
|
43
44
|
|
|
@@ -76,6 +77,6 @@ const router = createIngressRouter({
|
|
|
76
77
|
const server = Bun.serve({ routes: router.routes }) // or router.fetch
|
|
77
78
|
```
|
|
78
79
|
|
|
79
|
-
The benchmark server `bench/servers/router-server.ts` uses this API (same route
|
|
80
|
+
The benchmark server `bench/http/servers/router-server.ts` uses this API (same route
|
|
80
81
|
surface as `ingress-server.ts` but per-route compiled). Cost breakdown:
|
|
81
|
-
`bun bench/router-cost.ts` (`bun run bench:router`).
|
|
82
|
+
`bun bench/cost/router-cost.ts` (`bun run bench:router`).
|