castrum 0.9.0 → 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.
Files changed (120) hide show
  1. package/README.md +153 -630
  2. package/castrum.linux-x64-gnu.node +0 -0
  3. package/dist/index.d.ts +1653 -1817
  4. package/dist/index.js +6172 -4294
  5. package/docs/API.md +337 -0
  6. package/docs/ARCHITECTURE.md +111 -17
  7. package/docs/BENCHMARKS.md +117 -49
  8. package/docs/CASE_STUDY.md +243 -0
  9. package/docs/ENVIRONMENT.md +24 -7
  10. package/docs/FFI_BUN_GUIDE.md +335 -0
  11. package/docs/GETTING_STARTED.md +18 -34
  12. package/docs/HOW_TO_ADD_AN_OP.md +180 -0
  13. package/docs/INGRESS-ROUTER.md +82 -0
  14. package/docs/INGRESS.md +11 -7
  15. package/docs/NATIVE-ROUTE.md +77 -0
  16. package/docs/REPO_MAP.md +103 -53
  17. package/docs/adr/0001-two-wire-formats.md +1 -1
  18. package/docs/adr/0003-bun-builtins-delegation.md +6 -5
  19. package/docs/ai/LOCAL_DEV.md +81 -0
  20. package/docs/ai/TREE.md +1070 -0
  21. package/docs/bun-builtins-decision-matrix.md +25 -18
  22. package/index.ts +9 -16
  23. package/package.json +41 -28
  24. package/src/ingress/body.ts +167 -27
  25. package/src/ingress/decode/result-base.ts +10 -2
  26. package/src/ingress/handlers.ts +116 -285
  27. package/src/ingress/headers/security.ts +41 -0
  28. package/src/ingress/health.ts +1 -0
  29. package/src/ingress/index.ts +45 -169
  30. package/src/ingress/native-route.ts +182 -0
  31. package/src/ingress/packing/gather-raw-headers.ts +110 -22
  32. package/src/ingress/packing/header-packing.ts +5 -5
  33. package/src/ingress/packing/input-packer.ts +17 -3
  34. package/src/ingress/packing/route-wire.ts +197 -0
  35. package/src/ingress/packing/select-headers.ts +20 -7
  36. package/src/ingress/path-matcher.ts +115 -0
  37. package/src/ingress/response/baked-response.ts +302 -0
  38. package/src/ingress/router.ts +277 -0
  39. package/src/ingress/routes/common.ts +50 -1
  40. package/src/ingress/routes/fallback.ts +2 -2
  41. package/src/ingress/routes/head.ts +3 -13
  42. package/src/ingress/routes/index.ts +5 -4
  43. package/src/ingress/routes/json-write.ts +26 -47
  44. package/src/ingress/routes/native.ts +130 -0
  45. package/src/ingress/routes/options.ts +2 -12
  46. package/src/ingress/routes/read.ts +3 -13
  47. package/src/ingress/routes/responder.ts +159 -0
  48. package/src/ingress/server-node.ts +7 -7
  49. package/src/ingress/server.ts +83 -112
  50. package/src/ingress/sync.ts +151 -0
  51. package/src/ingress/types.ts +44 -1
  52. package/src/integration/pipeline.ts +3 -0
  53. package/src/integration/streaming.ts +2 -3
  54. package/src/integration/websocket.ts +4 -3
  55. package/src/loader/batch.ts +4 -0
  56. package/src/loader/cost.ts +1 -0
  57. package/src/loader/index.ts +43 -228
  58. package/src/loader/state.ts +84 -0
  59. package/src/loader/types.ts +145 -0
  60. package/src/native/ffi/build/codecs.ts +736 -0
  61. package/src/native/ffi/build/compress.ts +248 -0
  62. package/src/native/ffi/build/instances.ts +598 -0
  63. package/src/native/ffi/build/parse.ts +539 -0
  64. package/src/native/ffi/build/util.ts +140 -0
  65. package/src/native/ffi/build.ts +94 -0
  66. package/src/native/ffi/constants.ts +58 -0
  67. package/src/native/ffi/selftest.ts +22 -0
  68. package/src/native/ffi/types.ts +499 -0
  69. package/src/native/ffi.ts +307 -1357
  70. package/src/native/loader.ts +52 -6
  71. package/src/native/types/instances.ts +279 -0
  72. package/src/native/types.ts +41 -238
  73. package/src/runtime/builtins.ts +131 -0
  74. package/src/runtime/codec.ts +147 -0
  75. package/src/runtime/detect.ts +36 -0
  76. package/src/runtime/index.ts +28 -0
  77. package/src/runtime/native.ts +35 -0
  78. package/src/runtime/server.ts +31 -0
  79. package/src/runtime/transport.ts +64 -0
  80. package/src/runtime/types.ts +173 -0
  81. package/src/runtime/uuid.ts +45 -0
  82. package/src/runtime/websocket.ts +30 -0
  83. package/src/rust-ffi/addon.ts +1 -1
  84. package/src/rust-ffi/batch/build.ts +155 -0
  85. package/src/rust-ffi/batch/index.ts +8 -0
  86. package/src/rust-ffi/batch/types.ts +92 -0
  87. package/src/rust-ffi/client.ts +5 -6
  88. package/src/rust-ffi/context.ts +39 -10
  89. package/src/rust-ffi/index.ts +28 -17
  90. package/src/rust-ffi/into.ts +21 -0
  91. package/src/rust-ffi/options.ts +1 -1
  92. package/src/rust-ffi/packed.ts +40 -2
  93. package/src/rust-ffi/proven.ts +16 -30
  94. package/src/rust-ffi/scalar/crypto.ts +188 -107
  95. package/src/rust-ffi/scalar/factories.ts +257 -27
  96. package/src/rust-ffi/scalar/hashing.ts +43 -41
  97. package/src/rust-ffi/scalar/http.ts +208 -77
  98. package/src/rust-ffi/scalar/index.ts +3 -3
  99. package/src/rust-ffi/scalar/interface.ts +88 -213
  100. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  101. package/src/rust-ffi/scalar/json.ts +28 -12
  102. package/src/rust-ffi/scalar/payload.ts +159 -42
  103. package/src/rust-ffi/text.ts +31 -5
  104. package/src/selection.ts +27 -31
  105. package/src/shared/adaptive.ts +1 -2
  106. package/src/shared/buffer-pool.ts +72 -17
  107. package/src/shared/bytes.ts +75 -6
  108. package/src/shared/codec.ts +78 -0
  109. package/src/shared/env.ts +4 -4
  110. package/src/shared/log.ts +1 -0
  111. package/src/shared/packed/index.ts +10 -0
  112. package/src/shared/packed/parsers.ts +31 -0
  113. package/src/shared/packed/schema.ts +23 -0
  114. package/src/shared/{packed.ts → packed/wire.ts} +31 -39
  115. package/src/shared/proven.ts +189 -496
  116. package/src/shared/runtime.ts +20 -25
  117. package/src/shared/uuid.ts +12 -28
  118. package/src/data/json-rows.ts +0 -33
  119. package/src/rust-ffi/batch.ts +0 -431
  120. package/src/shared/bench-classify.ts +0 -209
package/README.md CHANGED
@@ -1,501 +1,40 @@
1
1
  # castrum
2
2
 
3
- **Enterprise-Grade Bun + Rust FFI Runtime Benchmark Package**
3
+ **Rust-accelerated primitives and a production-grade HTTP pipeline for Bun — and Node.js.**
4
4
 
5
- A high-performance Bun backend framework with Rust-accelerated FFI functions, providing production-grade HTTP ingress pipeline, validation, hashing, JSON processing, and more — all with benchmark-grade performance.
5
+ castrum is a hybrid TypeScript + Rust package. A single Rust addon does the
6
+ performance-critical work — hashing, JSON validation, parsing, crypto,
7
+ compression, and a full HTTP request pipeline — while TypeScript gives you a
8
+ small, flat API. Bun talks to the addon through `bun:ffi` (a ~10–20 ns C-ABI
9
+ call); Node.js falls back to NAPI automatically.
6
10
 
7
- ---
11
+ ## Why castrum
8
12
 
9
- ## Architecture Overview
13
+ - **Native speed without the plumbing** — call `rust.crc32()`, `rust.jsonValid()`,
14
+ `rust.validateEmail()`, … directly from TypeScript. No WASM, no subprocess.
15
+ - **A real HTTP server in a few lines** — `createIngressServer` wires CORS, rate
16
+ limiting, body-size guards, JSON-schema validation, and security headers for you.
17
+ - **Zero-copy hot paths** — request data travels in packed binary buffers shared
18
+ with Rust, so there's no serialization and no per-request allocation.
19
+ - **Honest performance data** — the CPU benchmark (`bun run check`) races every
20
+ `rust.*` op against a pure-JS baseline and writes a machine-readable report, so
21
+ you always know what to reach for.
22
+ - **Bun first, Node ready** — the same API runs on Node ≥ 20.3 via a compiled ESM
23
+ entry.
10
24
 
11
- ```
12
- ┌─────────────────────────────────────────────────────────────────┐
13
- │ TypeScript (Bun) Layer │
14
- │ │
15
- │ index.ts (entry point) │
16
- │ ├── src/rust-ffi/ Raw Rust FFI bindings │
17
- │ ├── src/native/ bun:ffi (primary on Bun) + NAPI loader │
18
- │ ├── src/ingress/ High-performance HTTP ingress pipeline │
19
- │ ├── src/integration/ Framework-agnostic pipeline/WS/SSE │
20
- │ ├── src/loader/ Higher-order loader (HFC) │
21
- │ ├── src/baseline/ JS baseline (benchmark reference) │
22
- │ └── src/shared/ Shared constants and helpers │
23
- │ │
24
- ├──────── Native Bridge: bun:ffi (Bun, primary) + NAPI (fallback) ────────┤
25
- │ │
26
- │ Rust (cdylib) Layer │
27
- │ │
28
- │ rust/lib.rs │
29
- │ ├── util/ bytes, packed, batch, batch_core, threadpool, │
30
- │ │ validation │
31
- │ ├── http/ headers, method, http_parser, cookie/query │
32
- │ │ parsers, form, media_type, url_codec/join, │
33
- │ │ etag, accept, mime_lookup, multipart │
34
- │ ├── crypto/ hmac_sha256, cookie_sign, csrf, jwt, aead, │
35
- │ │ argon2, base64, hashing, random_token │
36
- │ ├── json/ json_ops, json_ser, json_patch_ops, │
37
- │ │ json_schema, fast_schema/ (zero-DOM) │
38
- │ ├── payload/ compress, sse, ws_frames, websocket, template │
39
- │ ├── ingress/ mod.rs (napi boundary), pipeline.rs (core), │
40
- │ │ options/time/packed, cors, proxy, ip_trust, │
41
- │ │ rate_limit, terminal, output, constants │
42
- │ └── ffi.rs extern "C" exports (castrum_*) for bun:ffi │
43
- └─────────────────────────────────────────────────────────────────┘
44
- ```
45
-
46
- ## Quick Start
47
-
48
- ```bash
49
- # Install dependencies
50
- bun install
51
-
52
- # Build Rust native addon (release)
53
- bun run build
54
-
55
- # Build the compiled ESM entry for Node.js consumers (dist/)
56
- bun run build:js
57
-
58
- # Run benchmarks
59
- bun bench.ts
60
-
61
- # Run tests
62
- bun test
63
-
64
- # Run the Node.js smoke suite (requires `bun run build:js` first)
65
- bun run test:node
66
- ```
67
-
68
- ## Node.js Support
69
-
70
- **Bun is the primary target** (raw TypeScript, zero startup cost). The package is
71
- *also* consumable from Node.js via a compiled ESM entry:
72
-
73
- - Bun resolves the `bun` exports condition → `index.ts` (native TS execution).
74
- - Node.js resolves the `node`/`default` condition → `dist/index.js` (compiled ESM,
75
- requires Node.js >= 20.3). The bundled `dist/index.d.ts` covers the full API.
76
- - **One cdylib, two transports.** The addon is built with napi-rs (a standard N-API
77
- binary) but ALSO exports `extern "C"` symbols (`rust/ffi.rs`) that Bun loads directly:
78
- - **Bun** — `bun:ffi` is the PRIMARY transport. `src/native/ffi.ts` `dlopen`s the same
79
- `.node` file and JIT-calls the C ABI exports (~10-20ns crossing), covering the whole
80
- stateless scalar API + the ingress per-request pipeline + the ingress layout
81
- constants. NAPI is the fallback (Node, `CASTRUM_FFI_MODE=napi`, or a failed bind-time
82
- self-test).
83
- - **Node.js** — the N-API addon (`src/native/loader.ts`) is the fallback transport.
84
- - `rust.transport()` returns `"ffi"` / `"napi"` (the resolved transport) and
85
- `rust.ffiActive()` reports whether the bun:ffi transport is live; `CASTRUM_FFI_MODE`
86
- overrides the selection (see docs/ENVIRONMENT.md).
87
-
88
- ```ts
89
- // Node.js (ESM)
90
- import { rust, createIngressHandler, readHandler, createIngressServerNode } from "castrum";
25
+ > **Want the full story?** See the [case study](./docs/CASE_STUDY.md) for how
26
+ > castrum was built and measured — and where native speed did (and didn't) pay off.
91
27
 
92
- rust.crc32(new Uint8Array([104, 105])); // native FFI works identically (napi fallback)
28
+ ## Quick start
93
29
 
94
- // Serve the SAME pre-baked ingress handlers over node:http
95
- const srv = createIngressServerNode({ port: 3000, routes: { "/health": { read: handler } } });
96
- await srv.ready; // resolves once the server is listening
97
- ```
98
-
99
- `createIngressServer` (Bun.serve) is Bun-only; Node users use `createIngressServerNode`
100
- with the same route handlers.
101
-
102
- ## Build
30
+ ### 1. Install
103
31
 
104
32
  ```bash
105
- # Production build (native addon)
106
- bun run build
107
-
108
- # Debug build
109
- bun run build:debug
110
-
111
- # Compiled JS entry for Node (bundle + bundled types -> dist/)
112
- bun run build:js
113
- ```
114
-
115
- ## Benchmarks
116
-
117
- ```bash
118
- # Full benchmark suite
119
- bun bench.ts
120
-
121
- # HTTP server benchmarks
122
- bun run bench:http
123
-
124
- # Compare with specific frameworks
125
- bun run bench:http:elysia # vs Elysia
126
- bun run bench:http:ingress # vs Ingress (native)
127
- bun run bench:http:bun-only # vs raw Bun
128
-
129
- # Load patterns
130
- bun run bench:http:smoke # smoke test (CI wire-format guard)
131
- bun run bench:http:stress # stress test
132
- bun run bench:http:heavy # heavy JSON
133
- bun run bench:http:crud # CRUD operations
134
- bun run bench:http:spike # spike test
135
- bun run bench:http:soak # soak (endurance) test
136
- bun run bench:http:storm # storm (burst) test
137
- bun run bench:http:boundary # boundary conditions
138
- bun run bench:http:all-heavy # heavy-JSON scenarios (substring filter "1": 01-smoke, 10-12, 13-20)
139
- ```
140
-
141
- ### Bun built-ins comparison ("don't reinvent the wheel")
142
-
143
- `bun run check` also races castrum ops against Bun's native built-ins
144
- (`Bun.hash`, `Bun.password`, `Bun.CryptoHasher`, `Bun.gzipSync`,
145
- `Bun.randomUUIDv7`) as a **diagnostic** set (`diag:` task names — never
146
- audited by `check:proven`). Findings on Bun 1.4 / release addon:
147
-
148
- - **Bun wins → `@flux/native` should prefer Bun**: non-crypto hashing
149
- (`Bun.hash.crc32` ~3x, `xxHash3` ~4x, wyhash ~1-2x), gzip
150
- (`Bun.gzipSync`/`gunzipSync` ~1.3-2x), `Bun.randomUUIDv7` (~2x), HMAC
151
- (`Bun.CryptoHasher` ~1.2x).
152
- - **Rust wins → keep**: argon2id password hashing/verify (~1.8-2x vs
153
- `Bun.password`), bcrypt verify (~1.5x), and everything with no sync Bun
154
- equivalent (brotli, PBKDF2, zero-DOM JSON/parsers).
155
-
156
- Full table + per-op decisions: [`docs/bun-builtins-decision-matrix.md`](docs/bun-builtins-decision-matrix.md).
157
-
158
- ### New primitives
159
-
160
- | `rust.*` | Description | Benchmark vs |
161
- |---|---|---|
162
- | `xxh3` | XXH3-64 (non-crypto hash) | `Bun.hash.xxHash3` (~4x Bun — prefer Bun under Bun) |
163
- | `passwordHashBcrypt` / `passwordVerifyBcrypt` | bcrypt `$2b$` PHC | `Bun.password` bcrypt (hash parity, verify rust ~1.5x) |
164
- | `pbkdf2Sha256` | PBKDF2-HMAC-SHA256 | `node:crypto.pbkdf2Sync` (parity; only sync option in Bun) |
165
- | `uuidv7()` (TS) | UUIDv7 — delegates to `Bun.randomUUIDv7`, `crypto.randomUUID` on Node | `Bun.randomUUIDv7` |
166
-
167
- ## API Reference
168
-
169
- ### Core Exports
170
-
171
- ```ts
172
- import { rust, proven } from "castrum";
173
-
174
- // `rust` — All Rust FFI native implementations (flat, complete API)
175
- // `proven` — The same full `rust.*` surface, with performance annotations
176
- ```
177
-
178
- ## Performance-annotated surface
179
-
180
- Not every `rust.*` function beats its JavaScript baseline — e.g. `rust.jsonParse`
181
- loses ~5x to Bun's `JSON.parse` (DOM + napi marshaling), and the native schema
182
- validator is slower than `ajv` for small documents. `proven` exposes the **full
183
- `rust.*` surface** (it is literally the same object as `rust`); performance is
184
- communicated by `@performance` / `@deprecated` JSDoc annotations rather than by
185
- filtering, so a function's classification is visible in your editor and in
186
- `PROVEN_SURFACE`:
187
-
188
- ```ts
189
- import { proven, PROVEN_SURFACE } from "castrum";
190
-
191
- proven.jsonValid(bytes); // ✓ proven — Rust wins vs the JS baseline
192
- proven.fnv1a64(bytes); // ✓ proven — Rust wins ~9-16x
193
- proven.jsonParse(bytes); // ✗ @deprecated — loses ~5x to JSON.parse
194
- ```
195
-
196
- - `proven` is the full `rust.*` client (`export const proven = rust`). Each
197
- exported function's JSDoc carries its measured performance vs the JS baseline
198
- (`@performance`) and is marked `@deprecated` when it loses — so the editor
199
- steers you to the JS/Bun baseline for those. The single source of truth for the
200
- classifications is `PROVEN_SURFACE` (`src/shared/proven.ts`). Statuses:
201
- `proven` / `parity` / `not-competitive` / `unmeasured`.
202
- - The full surface is unchanged — `rust.jsonParse`, `rust.createSchemaValidator`,
203
- etc. remain available for completeness; they just aren't advertised as
204
- performance wins.
205
- - Classifications are based on the **release build on the shipped baseline CPU**
206
- (published artifacts are baseline — not the local SIMD `build:perf`).
207
-
208
- Keep the registry honest with the audit:
209
-
210
- ```bash
211
- bun run check # writes bench/results/cpu/latest.json (release build)
212
- bun run check:proven # report-only: proves vs loses per function
213
- bun run check:proven:fail # CI gate: exits 1 if a "proven" function regresses
214
- ```
215
-
216
- The audit compares each registry entry against the latest benchmark report and
217
- fails (with `--fail`) when a `proven` function loses to its baseline by more
218
- than the tolerance — catching real performance regressions (and wrong
219
- classifications) before they ship.
220
-
221
- ### Native-vs-JS selection surface
222
-
223
- For framework consumers that want to bind each operation to a fixed
224
- implementation at load time (instead of calling `rust.*` directly), castrum
225
- exposes an auto-selected decision surface:
226
-
227
- ```ts
228
- import { opImpl, isNativeOp, opDecision } from "castrum";
229
-
230
- opImpl("gzipCompress"); // "native" | "js" | null
231
- isNativeOp("crc32"); // boolean
232
- opDecision("fnv1a64"); // { impl, note? } | null
233
- ```
234
-
235
- - The single source of truth is `rust/selection.rs`, which embeds the
236
- benchmark-generated `src/selection.json` (produced by
237
- `scripts/select-native.ts --write`, audited by `--check` in CI). Under Bun,
238
- ops where the Bun built-in beats the Rust addon (gzip, crc32, xxh3, HMAC,
239
- random tokens — see `docs/bun-builtins-decision-matrix.md`) are selected as
240
- `"js"` (a JS path that delegates to the Bun built-in); under Node the base
241
- benchmark decision stands.
242
- - Consumers read `opImpl(op)` **once at startup** and bind each op to a fixed
243
- implementation — they do not swap native↔js per call.
244
-
245
- ### Rust Utility API
246
-
247
- `rust` is a single flat object exposing **every** native Rust utility with
248
- optimized defaults baked in. Text-oriented operations also have ergonomic
249
- string variants under `rust.text`.
250
-
251
- | Namespace | Description |
252
- |-----------|-------------|
253
- | `rust.crc32(...)`, `rust.jsonValid(...)`, … | Scalar utilities (bytes in → typed out) |
254
- | `rust.text.mimeFromExtension(".js")`, … | String ergonomics (string in → string/bool out) |
255
- | `rust.batch.jsonValid(docs)`, … | High-throughput batch (arrays → unpacked results) |
256
- | `rust.packed.jsonValidBatchPacked(p)`, … | Raw packed low-level variants + metadata |
257
- | `rust.configure({ ... })` | Override defaults on the shared instance |
258
-
259
- #### Scalar utilities
260
-
261
- | Function | Returns |
262
- |----------|---------|
263
- | `crc32(input)` | `number` — CRC32 checksum |
264
- | `fnv1a64(input)` | `bigint` — FNV-1a 64-bit hash |
265
- | `hmacSha256(key, data)` | `Uint8Array` — HMAC-SHA256 signature |
266
- | `hmacSha256Verify(key, data, sig)` | `boolean` |
267
- | `jsonValid(input)` | `boolean` — JSON validity check |
268
- | `jsonParse(input)` | `unknown` — JSON → JS value (native sonic-rs DOM; throws on invalid) |
269
- | `jsonSumIds(input)` | `bigint` — sum of numeric `id` fields |
270
- | `jsonPatch(doc, patch)` | `Uint8Array` — RFC 6902 result |
271
- | `createSchemaValidator(schema)` | `SchemaValidator` — compile a JSON Schema (draft-07); `.validate(doc): boolean`, `.validateDetailed(doc): SchemaError[]`, `.validateFirstError(doc): SchemaError | null`, `.validateBatchPackedCount(packed): number`, … |
272
- | `mimeFromExtension(ext)` | `Uint8Array` — MIME type |
273
- | `randomToken(byteLen)` | `Uint8Array` — CSPRNG token |
274
- | `urlEncode(input)` / `urlDecode(input)` | `Uint8Array` — percent encode/decode |
275
- | `urlDecodeBytes(input)` | `Uint8Array` — strict %-decode (no UTF-8 check) |
276
- | `validateEmail/Uuid/Ipv4/Ipv6(input)` | `boolean` |
277
- | `wsAcceptKey(key)` | `Uint8Array` — WebSocket accept key |
278
-
279
- #### Framework actions (HTTP semantics + security)
280
-
281
- | Function | Returns |
282
- |----------|---------|
283
- | `formParsePacked(body)` | `Uint8Array` — packed x-www-form-urlencoded pairs |
284
- | `createFormParser(capacity?)` | `FormParser` — reusable-buffer form parser (`.parse`, `.parseInto`) |
285
- | `parseMediaType(header)` | `{ mediaType, charset, boundary, params }` — Content-Type parse |
286
- | `createMediaTypeParser()` | `MediaTypeParser` — `.parse`, `.matches(actual, expected)` (wildcards) |
287
- | `etag(data, weak?)` | `Uint8Array` — strong/weak ETag (crc32-based) |
288
- | `httpDate(secs?)` / `parseHttpDate(input)` | `Uint8Array` / `bigint | null` — IMF-fixdate format/parse |
289
- | `createConditionalRequest(etag, lastModifiedSecs?)` | `ConditionalRequest` — `.isNotModified(ifNoneMatch, ifModifiedSince)` → 304 |
290
- | `parseAcceptEncoding(header)` | `{ encoding, q, order }[]` |
291
- | `createAcceptNegotiator(supported[])` | `AcceptNegotiator` — `.negotiate(header)` → best encoding |
292
- | `base64Encode/Decode(input, urlSafe?, padding?)`, `base64UrlEncode/Decode(input)` | `Uint8Array` |
293
- | `createBase64Codec(urlSafe?, padding?)` | `Base64Codec` — `.encode`, `.decode` |
294
- | `hexEncode(input)` / `hexDecode(input)` | `Uint8Array` — lowercase hex |
295
- | `signCookie(value, secret)` / `verifyCookie(signed, secret)` | `Uint8Array` / `Uint8Array | null` — signed cookies |
296
- | `createCookieSigner(secret)` | `CookieSigner` — `.sign`, `.verify` (HMAC key compiled once) |
297
- | `csrfToken(secret)` / `csrfVerify(token, secret)` | `Uint8Array` / `boolean` |
298
- | `createCsrfProtector(secret)` | `CsrfProtector` — `.create`, `.verify` |
299
- | `urlResolve(base, reference)` | `Uint8Array` — RFC 3986 resolution |
300
- | `urlEncodeQuery(params)` | `Uint8Array` — percent-encoded query (sorted keys) |
301
- | `createUrlBuilder(base)` | `UrlBuilder` — `.resolve(reference)` (base parsed once) |
302
- | `createJwtSigner(secret, ttlSeconds?)` | `JwtSigner` — `.sign(claims, now)`, `.verify(token, now)` (HS256 key + ttl compiled once) |
303
- | `createAeadCipher(key, algorithm?)` | `AeadCipher` — `.encrypt(nonce, pt)`, `.decrypt(nonce, ct)` (key compiled once) |
304
- | `createArgon2Hasher(options?)` | `Argon2Hasher` — `.hash(password, salt)`, `.verify(password, phc)` (params compiled once) |
305
- | `createMediaTypeMatcher(expected)` | `MediaTypeMatcher` — `.matches(actual)` (expected precompiled once) |
306
-
307
- Low-level / packed variants: `rust.httpParseRequestPacked`, `rust.queryParsePacked`,
308
- `rust.cookieParsePacked`, `rust.formParsePacked`.
309
-
310
- #### String ergonomics (`rust.text`)
311
-
312
- ```ts
313
- rust.text.mimeFromExtension(".js"); // "text/javascript"
314
- rust.text.mimeFromExtension("html"); // "text/html"
315
- rust.text.urlEncode("a b"); // "a%20b"
316
- rust.text.urlDecode("a%20b"); // "a b"
317
- rust.text.wsAcceptKey(keyBase64); // base64 accept key
318
- rust.text.validateEmail("a@b.co"); // true
319
- ```
320
-
321
- #### Batch operations (`rust.batch`)
322
-
323
- All batch helpers accept `Uint8Array[]` and return unpacked, ready-to-use results:
324
-
325
- ```ts
326
- const docs = [encoder.encode('{"id":1}'), encoder.encode('{"id":2}')];
327
-
328
- rust.batch.jsonValid(docs); // Uint8Array bitset (1 per doc)
329
- rust.batch.crc32(docs); // Uint32Array
330
- rust.batch.jsonSumIds(docs); // BigInt64Array
331
- rust.batch.queryParse(docs); // Uint8Array[]
332
- rust.batch.formParse(docs); // Uint8Array[] (packed pairs per doc)
333
- rust.batch.signCookie(docs, secret); // Uint8Array[] (signed)
334
- rust.batch.verifyCookie(docs, secret); // Uint8Array bitset (valid?)
335
- rust.batch.csrfVerify(tokens, secret); // Uint8Array bitset (valid?)
336
- rust.batch.schemaValidate(validator, docs); // bitset
337
-
338
- // Hashing / url / mime / ws / base64url
339
- rust.batch.fnv1a64(docs); // BigUint64Array (unsigned hashes)
340
- rust.batch.etag(docs); // Uint8Array[]
341
- rust.batch.urlEncode(docs); // Uint8Array[]
342
- rust.batch.urlDecode(docs); // Uint8Array[]
343
- rust.batch.base64UrlEncode(docs); // Uint8Array[]
344
- rust.batch.wsAcceptKey(keys); // Uint8Array[]
345
- rust.batch.mimeFromExtension(exts); // Uint8Array[]
346
-
347
- // Backend-framework batches (zipped lists / shared args)
348
- rust.batch.passwordVerify(pwds, phcs); // Uint8Array bitset (zipped)
349
- rust.batch.urlResolve(bases, refs); // Uint8Array[] (zipped)
350
- rust.batch.jwtSign(claimDocs, secret); // Uint8Array[] (sign N JSON claim docs)
351
- rust.batch.jwtVerify(tokens, secret, now); // Uint8Array bitset
352
- rust.batch.sseEncode(items, "event"); // Uint8Array[]
353
- rust.batch.wsFrameEncode(items, 1, false, true); // Uint8Array[]
354
- rust.batch.wsFrameDecode(frames); // Uint8Array[] (payloads)
355
- rust.batch.multipartParse(bodies, boundary); // MultipartPart[][]
356
- ```
357
-
358
- Raw packed + metadata variants (advanced): `rust.packed.*` mirrors the native
359
- functions 1:1 (`jsonValidBatchPacked`, `jsonValidBatchCountPacked`,
360
- `queryParseBatchTotalLenPacked`, …).
361
-
362
- #### Higher-order data loader (`castrum.loader`)
363
-
364
- `loader` is a callable higher-order function over the whole batch surface. It
365
- pre-binds an op so repeated calls skip registry dispatch, and it routes small
366
- vs. large workloads automatically: a single item → one scalar native call, a
367
- bulk → ONE packed batch call, and `load()` coalesces N same-tick calls into one
368
- packed call (DataLoader-style) with a bounded LRU cache.
369
-
370
- ```ts
371
- import { loader } from "castrum";
372
-
373
- const isEmail = loader("validateEmail");
374
- isEmail(emailBytes); // scalar → boolean
375
- isEmail([a, b, c]); // bulk → Uint8Array bitset (one packed call)
376
- await isEmail.load(a); // coalesced + cached (auto batch/strategy)
377
-
378
- // Covered op families (44 ops):
379
- // hash crc32, fnv1a64
380
- // json jsonValid, jsonSumIds, jsonPatch*, schemaValidate
381
- // validate email, uuid, ipv4, ipv6
382
- // parse query, cookie, form, httpParseRequest, urlResolve*
383
- // encode hex, base64, base64Url, urlEncode, urlDecode, urlDecodeBytes, etag
384
- // compress gzip, brotli
385
- // crypto hmacSha256, hmacSha256Verify*, signCookie, verifyCookie, csrfVerify,
386
- // passwordHash, passwordVerify*, aeadEncrypt, aeadDecrypt, jwtSign, jwtVerify
387
- // web wsAcceptKey, wsFrameEncode, sseEncode, mimeFromExtension
388
- // (* = paired: per-item companion array, e.g. loader.run("jsonPatch", docs, patches))
389
-
390
- // Paired ops take a companion array:
391
- loader.run("jsonPatch", docs, patches); // bulk
392
- loader.run("jsonPatch", doc, patch); // single
393
-
394
- // verifyCookie / jwtVerify are boolean-validity ops (valid/invalid);
395
- // use rust.verifyCookie / rust.jwtVerify for the decoded value.
396
-
397
- // Bind a schema validator for repeated single/bulk validation + count:
398
- const schema = loader.schema(rust.createSchemaValidator(schemaBytes));
399
- schema(doc); // boolean
400
- schema(docs); // Uint8Array bitset
401
- schema.count(docs); // number of valid docs
402
-
403
- // Fine-grained control: createLoader({ adaptive, batchMin, maxCacheKeys, ... })
404
- ```
405
-
406
- #### Configuring defaults
407
-
408
- Defaults are selected automatically, and can be overridden:
409
-
410
- ```ts
411
- // Defaults:
412
- // rayonThreads = max(1, hardwareConcurrency - 1)
413
- // (env override: CASTRUM_RAYON_THREADS / RUST_RAYON_THREADS)
414
- // mimeCache = true
415
- // hmacCache = true
416
-
417
- // Override per-instance options on the shared instance:
418
- rust.configure({ mimeCache: false, hmacCache: false });
419
-
420
- // Or create an isolated instance with custom defaults:
421
- import { createRust } from "castrum";
422
- const myRust = createRust({ mimeCache: false });
423
- ```
424
-
425
- > **Thread pool note**: the rayon pool is process-wide and initialized **once**
426
- > (native `OnceLock`) — the first initialization wins. Because importing the
427
- > module initializes it with the default, set
428
- > `CASTRUM_RAYON_THREADS` / `RUST_RAYON_THREADS` (or call
429
- > `rust.configure({ rayonThreads })` / `createRust({ rayonThreads })` before any
430
- > other pool use) to tune it. `mimeCache` / `hmacCache` are per-instance and can
431
- > be toggled any time.
432
-
433
- ### Sub-modules
434
-
435
- | Import | Description |
436
- |--------|-------------|
437
- | `import { encoder, decoder } from "castrum"` | Text encode/decode utilities |
438
- | `import { packBatch, packPairs, unpackBitset, unpackByteResults, unpackI64ArrayAsBigInt, unpackU32Array } from "castrum"` | Batch/pair packing + unpacking utilities |
439
- | `import { readPairsPacked, pairsToObject, readHttpPacked } from "castrum"` | Decode packed pairs / parsed HTTP buffers |
440
- | `import { parseQueryString, parseCookieHeader, parseFormBody } from "castrum"` | High-level parsers (no hand-packing) |
441
- | `import { createIngress, createIngressSync, createIngressFast } from "castrum"` | HTTP ingress pipeline (low-level) |
442
- | `import { createIngressHandler, createIngressServer, readHandler, jsonWriteHandler, echoHandler, fallbackHandler } from "castrum"` | **Pre-baked** ingress handlers — route factories + Bun.serve builder |
443
-
444
- ```ts
445
- import { parseQueryString, parseCookieHeader } from "castrum";
446
-
447
- parseQueryString("a=1&b=2&tag=a&tag=b"); // { a: "1", b: "2", tag: ["a", "b"] }
448
- parseCookieHeader("session=abc; theme=dark"); // { session: "abc", theme: "dark" }
449
- ```
450
-
451
- > **Environment variables**: See [docs/ENVIRONMENT.md](./docs/ENVIRONMENT.md) for the
452
- > full reference of `INGRESS_*`, `CASTRUM_*` and `RUST_*` runtime variables.
453
-
454
- ### Ingress Pipeline
455
-
456
- The ingress system is a high-performance request processing pipeline that handles:
457
-
458
- - **Method routing** — GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS
459
- - **CORS evaluation** — Origin validation, preflight handling
460
- - **Rate limiting** — Token-bucket per IP with configurable window
461
- - **IP trust & proxy** — X-Forwarded-For, X-Real-IP resolution
462
- - **HTTPS detection** — X-Forwarded-Proto based
463
- - **Cookie parsing** — Structured JSON output
464
- - **Query parsing** — Key-value pair extraction
465
- - **JSON body validation** — Syntax + optional schema validation
466
- - **Body size guarding** — Configurable max body size
467
- - **Security headers** — HSTS, CSP, X-Frame-Options, etc.
468
-
469
- ```ts
470
- // Fast synchronous handler (zero allocations per call)
471
- import { createIngressFast } from "castrum";
472
-
473
- const handler = createIngressFast({
474
- cors: { allowOrigin: ["https://example.com"] },
475
- rateLimit: { limit: 100, windowMs: 60_000 },
476
- parseCookies: true,
477
- parseQuery: true,
478
- });
479
-
480
- // Inside a Bun server:
481
- Bun.serve({
482
- port: 3000,
483
- async fetch(req) {
484
- return handler.run(req, req.headers.get("x-forwarded-for"), null, "", (result) => {
485
- if (result.ok) {
486
- return new Response("OK", { status: 200 });
487
- }
488
- return new Response("Error", { status: result.status });
489
- });
490
- },
491
- });
33
+ bun install
34
+ bun run build # compile the Rust addon (release)
492
35
  ```
493
36
 
494
- #### Pre-baked handlers (recommended for servers)
495
-
496
- `src/ingress/handlers.ts` provides ready-to-use route handlers and a `Bun.serve`
497
- builder so **any system can consume the optimized ingress pipeline in a few
498
- lines** — no need to hand-build responses or manage header templates:
37
+ ### 2. Start a server
499
38
 
500
39
  ```ts
501
40
  import { createIngressHandler, createIngressServer } from "castrum";
@@ -507,189 +46,173 @@ const ingress = createIngressHandler({
507
46
  rateLimit: { limit: 1000, windowMs: 60_000 },
508
47
  });
509
48
 
510
- createIngressServer({
49
+ const server = createIngressServer({
511
50
  port: 3000,
512
51
  routes: {
513
- "/health": { read: ingress }, // GET + HEAD
514
- "/api/users": { read: ingress, write: ingress }, // + POST/PUT/PATCH
515
- "/api/echo": { echo: ingress }, // POST echo
52
+ "/health": { read: ingress }, // GET + HEAD
53
+ "/api/users": { read: ingress, write: ingress }, // + POST/PUT/PATCH (+OPTIONS)
54
+ "/api/echo": { echo: ingress }, // POST echo
516
55
  },
517
56
  });
518
- ```
519
57
 
520
- The route factories (`readHandler`, `headHandler`, `jsonWriteHandler`,
521
- `echoHandler`, `fallbackHandler`) are runtime-agnostic — use them with any
522
- fetch-style server. The benchmark server (`bench/servers/ingress-server.ts`)
523
- is built entirely from these and re-exports them. See
524
- [docs/INGRESS.md](./docs/INGRESS.md) for the full pre-baked API reference.
58
+ console.log("listening on", server.port);
59
+ ```
525
60
 
61
+ Run it with `bun`, then `curl http://localhost:3000/health`. A complete
62
+ walkthrough lives in [`examples/basic-server.ts`](examples/basic-server.ts) and
63
+ the tutorial in [`docs/GETTING_STARTED.md`](./docs/GETTING_STARTED.md).
526
64
 
527
- ---
65
+ ### 3. Call native primitives
528
66
 
529
- ## Project Structure
67
+ ```ts
68
+ import { rust, encoder } from "castrum";
530
69
 
531
- ```
532
- castrum/
533
- ├── index.ts # Entry point / public API
534
- ├── AGENTS.md # AI agent instructions (commands, constraints, gotchas)
535
- ├── bench.ts # Benchmark runner
536
- ├── build.rs # NAPI build script
537
- ├── Cargo.toml # Rust dependencies & build config
538
- ├── package.json # Node/Bun package config
539
- ├── tsconfig.json # TypeScript config
540
- │
541
- ├── rust/ # Rust source (one cdylib, domain folders)
542
- │ ├── lib.rs # Crate root: folder declarations + module map
543
- │ ├── util/ # Shared infra: bytes, packed, batch(+core), threadpool, validation
544
- │ ├── http/ # Wire formats: headers, method, http/query/cookie parsers,
545
- │ │ # form, media_type, url_codec/join, etag, accept, mime_lookup, multipart
546
- │ ├── crypto/ # Auth & hashing: hmac, cookie_sign, csrf, jwt, aead, argon2,
547
- │ │ # base64, hashing, random_token
548
- │ ├── json/ # JSON & schema: json_ops, json_ser, json_patch_ops, json_schema,
549
- │ │ # fast_schema/ (zero-DOM draft-07 engine: types/cursor/compile/
550
- │ │ # validate/errors — incl. pattern, enum/const, multipleOf,
551
- │ │ # combinators, $ref, uniqueItems, detailed errors)
552
- │ ├── payload/ # Output & streaming: compress, sse, ws_frames, websocket, template
553
- │ ├── ingress/ # The ingress pipeline: mod.rs (napi boundary), pipeline.rs (core),
554
- │ │ # tests.rs, options/time/packed, cors, proxy, ip_trust,
555
- │ │ # rate_limit, terminal, output, ingress_constants
556
- │ ├── test_support.rs # Shared test helpers
557
- │ └── unit_tests.rs # Cross-module test suite
558
- │
559
- ├── src/ # TypeScript source
560
- │ ├── ingress/ # Ingress pipeline (TS layer), decomposed by task
561
- │ │ ├── index.ts # Public API barrel + async factory
562
- │ │ ├── fast.ts # Thin: createIngressFast (packed input)
563
- │ │ ├── handlers.ts # Thin: createIngressHandler (JS-packs frame → handleRequestPacked, pre-baked)
564
- │ │ ├── server.ts # createIngressServer (Bun.serve builder)
565
- │ │ ├── server-node.ts # createIngressServerNode (node:http adapter)
566
- │ │ ├── constants.ts # Layout constants (from Rust)
567
- │ │ ├── shared.ts # JS constants shared by both paths
568
- │ │ ├── status.ts # Status normalization helpers
569
- │ │ ├── errors.ts # Fast-path error-code mapping
570
- │ │ ├── options.ts # IngressFast option types + validation
571
- │ │ ├── types.ts # Public API types
572
- │ │ ├── body.ts # Streaming request-body reader
573
- │ │ ├── context.ts # Result snapshot + synthetic contexts
574
- │ │ ├── packing/ # header-packing, input-packer, gather-raw-headers
575
- │ │ ├── headers/ # cors, hsts, fast-templates, baked-templates
576
- │ │ ├── decode/ # fast-result, baked-result
577
- │ │ ├── response/ # terminal, error-bodies
578
- │ │ └── routes/ # read/head/json-write/echo/delete/options/fallback factories
579
- │ ├── native/ # Native transport: bun:ffi (primary on Bun) + NAPI fallback
580
- │ │ ├── types.ts # NativeAddon interface + instance types
581
- │ │ ├── loader.ts # getAddon/lazyAddon path resolution
582
- │ │ ├── ffi.ts # bun:ffi C-ABI bindings (Bun-only; bind-time self-test)
583
- │ │ └── index.ts # Barrel
584
- │ ├── rust-ffi/ # Rust FFI bindings
585
- │ │ ├── options.ts # RustOptions + input-normalization helpers
586
- │ │ ├── addon.ts # shared lazy addon proxy
587
- │ │ ├── context.ts # per-instance state + namespace helpers
588
- │ │ ├── text.ts # string namespace
589
- │ │ ├── batch.ts # array-of-bytes namespace
590
- │ │ ├── packed.ts # raw packed-wire namespace
591
- │ │ ├── scalar/ # scalar/feature methods (interface + per-domain builders)
592
- │ │ ├── client.ts # createRust factory + default `rust`
593
- │ │ ├── proven.ts # `proven` client (full surface, annotated)
594
- │ │ └── index.ts # Barrel
595
- │ ├── baseline/ # JS baseline implementations
596
- │ │ ├── index.ts # Aggregator
597
- │ │ └── tasks/ # Individual baseline implementations
598
- │ ├── bench/ # Benchmark framework
599
- │ ├── data/ # Data serialization
600
- │ └── shared/ # Shared utilities
601
- │
602
- ├── bench/ # HTTP benchmark servers
603
- │ ├── servers/ # bun-server.ts, elysia-server.ts, ingress-server.ts (+ shared.ts)
604
- │ ├── load.ts # HTTP load generator & scenarios
605
- │ └── run-bench.ts # Benchmark runner
606
- │
607
- ├── test/ # Tests (see CONTRIBUTING.md → Testing Requirements)
608
- ├── docs/ # Documentation
609
- │ ├── REPO_MAP.md # ← START HERE: what is where & why
610
- │ ├── GETTING_STARTED.md # Intern-friendly tutorial
611
- │ ├── ARCHITECTURE.md # Deep-dive internals
612
- │ ├── INGRESS.md # Pre-baked ingress API
613
- │ ├── BENCHMARKS.md # Benchmark scenarios
614
- │ └── ENVIRONMENT.md # Env vars
615
- └── scripts/ # Build/utility scripts
70
+ rust.crc32(encoder.encode("hello")); // number
71
+ rust.jsonValid(encoder.encode('{"a":1}')); // boolean (zero-DOM)
72
+ rust.validateEmail(encoder.encode("a@b.co")); // boolean
73
+ rust.text.mimeFromExtension(".js"); // "text/javascript"
74
+ rust.batch.jsonValid([docA, docB]); // Uint8Array bitset (one packed call)
616
75
  ```
617
76
 
618
- ---
77
+ Everything is synchronous. For repeated calls, the higher-order
78
+ [`loader`](#api-at-a-glance) batches and caches for you automatically.
619
79
 
620
- ## Performance Characteristics
80
+ ## Node.js support
621
81
 
622
- All Rust FFI functions are designed for **zero-allocation** paths where possible:
82
+ Bun is the primary target — it resolves the `bun` export condition to raw
83
+ `index.ts` (zero startup cost). Node.js ≥ 20.3 resolves the `node`/`default`
84
+ condition to a compiled ESM bundle (`dist/`, built with `bun run build:js`). It's
85
+ the same cdylib either way: Bun uses `bun:ffi`, Node uses the NAPI fallback, so
86
+ `rust.*` calls behave identically on both runtimes.
623
87
 
624
- - **Ingress pipeline**: Single output buffer written by Rust, read by JS — minimal copies
625
- - **Validation functions**: Pure CPU-bound, 10-100x faster than JS equivalents
626
- - **Hashing**: SIMD-optimized crates under the hood
627
- - **Batch operations**: Packed binary format minimizes boundary crossings
628
- (the batch/packed namespaces use the NAPI fallback — no C-ABI batch symbols)
629
- - **JSON parsing**: Uses `sonic-rs` (Rust) — one of the fastest JSON parsers available
88
+ ```ts
89
+ // Node (ESM) — the same pre-baked handlers over node:http
90
+ import { createIngressServerNode } from "castrum";
91
+ const srv = createIngressServerNode({ port: 3000, routes: { "/health": { read: ingress } } });
92
+ await srv.ready; // resolves once listening
93
+ ```
630
94
 
631
- ---
95
+ The public `createIngressServer` is runtime-adaptive — it picks `Bun.serve` on
96
+ Bun and `node:http` on Node; `createIngressServerNode` stays available for pinned
97
+ Node users with the same route handlers. `rust.transport()` /
98
+ `rust.ffiActive()` report which transport is live, and `CASTRUM_FFI_MODE` forces
99
+ one (see [`docs/ENVIRONMENT.md`](./docs/ENVIRONMENT.md)).
632
100
 
633
- ## Testing
101
+ ## How it works
634
102
 
635
- ```bash
636
- # Run all tests
637
- bun test
103
+ TypeScript is the API; a Rust cdylib (`rust/`, one crate) is the engine. Bun
104
+ `dlopen`s the addon and JIT-calls its `extern "C"` exports — that's `bun:ffi`,
105
+ the primary transport (~10–20 ns per crossing). On Node, or if the bind-time
106
+ ffi self-test fails, the same addon loads through napi-rs (NAPI). Request data is
107
+ packed into shared, length-prefixed buffers so hot paths allocate nothing per
108
+ request. See [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) for the full deep
109
+ dive.
638
110
 
639
- # Run specific test file
640
- bun test test/unit/ingress/fast.test.ts
111
+ ## Benchmarks
641
112
 
642
- # Run tests with coverage
643
- bun test --coverage
644
- ```
113
+ Two suites ship with the repo:
645
114
 
646
- ---
115
+ - **CPU benchmark** — `bun run check` races every `rust.*` op against a pure-JS
116
+ baseline (and Bun's built-ins) and writes a machine-readable report to
117
+ `bench/results/cpu/`.
118
+ - **HTTP benchmark** — `bun run bench:http` compares raw `Bun.serve`, Elysia,
119
+ and the castrum ingress server across ~20 load scenarios (smoke, stress,
120
+ soak, spike, heavy JSON, slowloris, …).
647
121
 
648
- ## Publishing
122
+ ```bash
123
+ bun run check # CPU benchmark + correctness (writes latest.json)
124
+ bun run bench:http # HTTP benchmark, all servers
125
+ bun run bench:http:smoke # fast HTTP sanity — the CI wire-format guard
126
+ ```
127
+
128
+ For scenario details, how to read the reports, and the benchmark-specific env
129
+ controls, see [`docs/BENCHMARKS.md`](./docs/BENCHMARKS.md). The keep/delegate
130
+ decisions for ops where Bun's built-ins win (gzip, crc32, xxh3, `randomUUIDv7`,
131
+ …) live in [`docs/bun-builtins-decision-matrix.md`](./docs/bun-builtins-decision-matrix.md).
132
+
133
+ ## API at a glance
134
+
135
+ The complete, verified reference lives in [`docs/API.md`](./docs/API.md). This
136
+ is the map:
137
+
138
+ | Export | What it is |
139
+ |--------|-----------|
140
+ | `rust` | The flat native surface: hashing, JSON, validation, URL/cookie/query/form parsing, MIME, WebSocket/SSE, crypto (HMAC, JWT, AEAD, argon2, bcrypt, PBKDF2), signed cookies, CSRF, gzip/brotli, and more. |
141
+ | `rust.text.*` | String-in / string-out ergonomics over `rust.*`. |
142
+ | `rust.batch.*` | Array-of-bytes → native results (bitsets, typed arrays, byte arrays) in one packed call. |
143
+ | `rust.packed.*` | Raw packed-wire low-level variants + metadata. |
144
+ | `loader` / `createLoader` | Higher-order loader: pre-bound ops, automatic scalar-vs-bulk dispatch, DataLoader-style coalescing, LRU cache. |
145
+ | `opImpl` / `isNativeOp` / `opDecision` | Benchmark-driven native-vs-JS selection hints for framework consumers. |
146
+ | `createIngressHandler` + route factories | Pre-baked HTTP pipeline: CORS, rate limit, body guard, schema validation, security headers. |
147
+ | `createIngressServer` / `createIngressServerNode` | Server builders — the runtime adapter picks `Bun.serve` on Bun and `node:http` on Node. |
148
+ | `createIngress` / `createIngressFast` / `createIngressSync` | Lower-level ingress entry points. |
149
+ | `createPipeline` / `createWebSocketUpgrade` / `sseResponse` | Framework-agnostic integration helpers. |
150
+ | `createMetrics` / `createIngressMetrics` / `livenessHandler` / `readinessHandler` / trace helpers | Zero-dependency observability. |
151
+ | `uuidv7` / `AdaptiveEstimate` / packing & parsing utilities | Shared utilities (`encoder`, `decoder`, `packBatch`, `parseQueryString`, …). |
649
152
 
650
- The package ships **all platform native addons in a single tarball**, so a release
651
- needs every `napi.targets` platform artifact present (see `scripts/prepublish.mjs`).
153
+ ```ts
154
+ import { rust, loader } from "castrum";
652
155
 
653
- ### Automatic (recommended)
156
+ rust.jsonValid(bytes); // scalar boolean
157
+ const isEmail = loader("validateEmail");
158
+ isEmail([a, b, c]); // one packed batch call
159
+ ```
654
160
 
655
- CI builds every platform addon and publishes to npm when you push a `v*` tag.
656
- Bump `version` in `package.json` (and `Cargo.toml`), add a `CHANGELOG.md` entry,
657
- then:
161
+ > **The selection surface** — framework consumers that want to bind each op to
162
+ > a fixed implementation at load time use `opImpl(op)` / `isNativeOp(op)` /
163
+ > `opDecision(op)` (source of truth: `rust/selection.rs` + `src/selection.json`,
164
+ > audited by CI). They read it once at startup — they don't swap native↔js per
165
+ > call. See [`docs/API.md`](./docs/API.md) and
166
+ > [`docs/bun-builtins-decision-matrix.md`](./docs/bun-builtins-decision-matrix.md).
167
+
168
+ ## Testing
658
169
 
659
170
  ```bash
660
- git tag v<version> && git push origin v<version>
171
+ bun test # TypeScript unit tests (~760)
172
+ bun run test:rust # Rust unit tests (cargo test)
173
+ bun run typecheck # TypeScript typecheck
174
+ bun run test:node # Node.js integration tests (run `bun run build:js` first)
175
+ bun run bench:http:smoke # HTTP smoke — the CI wire-format guard
661
176
  ```
662
177
 
663
- > Run `bun run check:version` first — it verifies `package.json`, `Cargo.toml`,
664
- > and `CHANGELOG.md` agree. See [docs/REPO_MAP.md](./docs/REPO_MAP.md) §6.
178
+ For coverage floors, the typecheck/test configs, and the full test matrix, see
179
+ [`docs/REPO_MAP.md`](./docs/REPO_MAP.md).
665
180
 
666
- ### Manual
181
+ ## Publishing
667
182
 
668
- To build and publish a release by hand (no CI needed; npm logged in or `NPM_TOKEN`
669
- exported):
183
+ The package ships **all platform addons in a single tarball**. Push a `v*` tag
184
+ and CI builds + publishes every platform to npm; or use `bun run publish:manual`
185
+ for a quick single-platform release:
670
186
 
671
187
  ```bash
672
- bun run publish:manual # build + publish current version
673
- bun run publish:manual -- --increment minor # bump version + tag + push + publish
674
- bun run publish:manual:dry # print the plan, change nothing
188
+ bun run publish:manual -- --increment minor # bump + tag + build + publish
189
+ bun run publish:manual:dry # print the plan, change nothing
675
190
  ```
676
191
 
677
- `publish:manual` bumps the version (`--increment`), creates + pushes the `v<version>`
678
- git tag via `bun pm version` (keeping `Cargo.toml`, `Cargo.lock`, and `CHANGELOG.md`
679
- in sync), builds the host addon locally, and runs `npm publish --access public`.
192
+ Run `bun run check:version` before tagging (it verifies `package.json`,
193
+ `Cargo.toml`, and `CHANGELOG.md` agree). See [`docs/REPO_MAP.md`](./docs/REPO_MAP.md)
194
+ §6 for the full flow.
680
195
 
681
- > **Platform note:** because only the current platform can be built locally, the
682
- > manual publish ships a **single-platform tarball** (it sets
683
- > `CASTRUM_PUBLISH_ALLOW_PARTIAL=1` so `prepublishOnly` doesn't fail on the missing
684
- > platforms). For a full multi-platform tarball, push a `v*` tag and let the CI
685
- > workflow build + publish.
196
+ ## Documentation
686
197
 
687
- ---
198
+ | Doc | Read it for |
199
+ |-----|-------------|
200
+ | [`docs/GETTING_STARTED.md`](./docs/GETTING_STARTED.md) | First-time walkthrough: install → server → bench → test. |
201
+ | [`docs/API.md`](./docs/API.md) | The complete public API reference. |
202
+ | [`docs/INGRESS.md`](./docs/INGRESS.md) | The ingress pipeline: options, route factories, servers, framework integration. |
203
+ | [`docs/CASE_STUDY.md`](./docs/CASE_STUDY.md) | Data-driven case study: how castrum was built and measured. |
204
+ | [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) | Internals: modules, data flow, wire layout, memory, concurrency. |
205
+ | [`docs/BENCHMARKS.md`](./docs/BENCHMARKS.md) | Benchmark scenarios + how to interpret reports. |
206
+ | [`docs/ENVIRONMENT.md`](./docs/ENVIRONMENT.md) | Every `CASTRUM_*` / `INGRESS_*` env var. |
207
+ | [`docs/REPO_MAP.md`](./docs/REPO_MAP.md) | Where everything lives + how to build/test/publish. |
208
+ | [`docs/bun-builtins-decision-matrix.md`](./docs/bun-builtins-decision-matrix.md) | Bun built-in vs castrum keep/delegate decisions. |
209
+ | [`examples/`](examples/README.md) | Runnable examples. |
688
210
 
689
211
  ## License
690
212
 
691
- MIT
213
+ MIT — see [LICENSE](./LICENSE).
692
214
 
693
215
  ## Contributing
694
216
 
695
- Please see [CONTRIBUTING.md](./CONTRIBUTING.md).
217
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) and [`AGENTS.md`](./AGENTS.md) (AI-agent
218
+ guidance) for contribution and testing requirements.