castrum 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -630
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1374 -1772
- package/dist/index.js +5570 -4169
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +106 -14
- package/docs/BENCHMARKS.md +83 -47
- package/docs/CASE_STUDY.md +240 -0
- package/docs/ENVIRONMENT.md +8 -6
- package/docs/FFI_BUN_GUIDE.md +327 -0
- package/docs/GETTING_STARTED.md +15 -32
- package/docs/INGRESS-ROUTER.md +81 -0
- package/docs/INGRESS.md +5 -4
- package/docs/NATIVE-ROUTE.md +53 -0
- package/docs/REPO_MAP.md +46 -26
- package/docs/adr/0003-bun-builtins-delegation.md +6 -5
- package/docs/bun-builtins-decision-matrix.md +25 -18
- package/index.ts +9 -16
- package/package.json +36 -24
- package/src/ingress/body.ts +167 -27
- package/src/ingress/handlers.ts +91 -15
- package/src/ingress/health.ts +1 -0
- package/src/ingress/index.ts +39 -27
- package/src/ingress/packing/gather-raw-headers.ts +85 -21
- package/src/ingress/packing/header-packing.ts +5 -5
- package/src/ingress/packing/input-packer.ts +17 -3
- package/src/ingress/packing/select-headers.ts +12 -6
- package/src/ingress/router.ts +237 -0
- package/src/ingress/routes/common.ts +42 -1
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +3 -13
- package/src/ingress/routes/index.ts +5 -4
- package/src/ingress/routes/json-write.ts +7 -31
- package/src/ingress/routes/options.ts +2 -12
- package/src/ingress/routes/read.ts +3 -13
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +6 -6
- package/src/ingress/server.ts +45 -13
- package/src/ingress/types.ts +35 -1
- package/src/integration/pipeline.ts +3 -0
- package/src/integration/streaming.ts +2 -3
- package/src/integration/websocket.ts +4 -3
- package/src/loader/batch.ts +4 -0
- package/src/loader/cost.ts +1 -0
- package/src/loader/index.ts +43 -228
- package/src/loader/state.ts +84 -0
- package/src/loader/types.ts +145 -0
- package/src/native/ffi/constants.ts +58 -0
- package/src/native/ffi/selftest.ts +729 -0
- package/src/native/ffi/types.ts +474 -0
- package/src/native/ffi.ts +1063 -829
- package/src/native/loader.ts +52 -6
- package/src/native/types/instances.ts +274 -0
- package/src/native/types.ts +41 -238
- package/src/runtime/builtins.ts +131 -0
- package/src/runtime/codec.ts +147 -0
- package/src/runtime/detect.ts +36 -0
- package/src/runtime/index.ts +28 -0
- package/src/runtime/native.ts +35 -0
- package/src/runtime/server.ts +31 -0
- package/src/runtime/transport.ts +64 -0
- package/src/runtime/types.ts +173 -0
- package/src/runtime/uuid.ts +45 -0
- package/src/runtime/websocket.ts +30 -0
- package/src/rust-ffi/addon.ts +1 -1
- package/src/rust-ffi/batch/build.ts +155 -0
- package/src/rust-ffi/batch/index.ts +8 -0
- package/src/rust-ffi/batch/types.ts +92 -0
- package/src/rust-ffi/client.ts +5 -6
- package/src/rust-ffi/context.ts +39 -10
- package/src/rust-ffi/index.ts +28 -17
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +1 -1
- package/src/rust-ffi/packed.ts +2 -2
- package/src/rust-ffi/proven.ts +16 -30
- package/src/rust-ffi/scalar/crypto.ts +188 -107
- package/src/rust-ffi/scalar/factories.ts +253 -27
- package/src/rust-ffi/scalar/hashing.ts +43 -41
- package/src/rust-ffi/scalar/http.ts +208 -77
- package/src/rust-ffi/scalar/index.ts +3 -3
- package/src/rust-ffi/scalar/interface.ts +88 -213
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +28 -12
- package/src/rust-ffi/scalar/payload.ts +159 -42
- package/src/rust-ffi/text.ts +31 -5
- package/src/selection.ts +27 -31
- package/src/shared/adaptive.ts +1 -2
- package/src/shared/buffer-pool.ts +72 -17
- package/src/shared/bytes.ts +75 -6
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +4 -4
- package/src/shared/log.ts +1 -0
- package/src/shared/packed/index.ts +10 -0
- package/src/shared/packed/parsers.ts +31 -0
- package/src/shared/packed/schema.ts +23 -0
- package/src/shared/{packed.ts → packed/wire.ts} +31 -39
- package/src/shared/proven.ts +189 -496
- package/src/shared/runtime.ts +20 -25
- package/src/shared/uuid.ts +12 -28
- package/src/data/json-rows.ts +0 -33
- package/src/rust-ffi/batch.ts +0 -431
- package/src/shared/bench-classify.ts +0 -209
package/README.md
CHANGED
|
@@ -1,501 +1,40 @@
|
|
|
1
1
|
# castrum
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Rust-accelerated primitives and a production-grade HTTP pipeline for Bun — and Node.js.**
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
## Quick start
|
|
93
29
|
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 },
|
|
514
|
-
"/api/users": { read: ingress, write: ingress },
|
|
515
|
-
"/api/echo": { echo: ingress },
|
|
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
|
-
|
|
521
|
-
|
|
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
|
-
|
|
67
|
+
```ts
|
|
68
|
+
import { rust, encoder } from "castrum";
|
|
530
69
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
##
|
|
80
|
+
## Node.js support
|
|
621
81
|
|
|
622
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
##
|
|
101
|
+
## How it works
|
|
634
102
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
640
|
-
bun test test/unit/ingress/fast.test.ts
|
|
111
|
+
## Benchmarks
|
|
641
112
|
|
|
642
|
-
|
|
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
|
-
|
|
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
|
-
|
|
651
|
-
|
|
153
|
+
```ts
|
|
154
|
+
import { rust, loader } from "castrum";
|
|
652
155
|
|
|
653
|
-
|
|
156
|
+
rust.jsonValid(bytes); // scalar boolean
|
|
157
|
+
const isEmail = loader("validateEmail");
|
|
158
|
+
isEmail([a, b, c]); // one packed batch call
|
|
159
|
+
```
|
|
654
160
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
171
|
+
bun test # TypeScript unit tests (~540)
|
|
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
|
-
|
|
664
|
-
|
|
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
|
-
|
|
181
|
+
## Publishing
|
|
667
182
|
|
|
668
|
-
|
|
669
|
-
|
|
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
|
|
673
|
-
bun run publish:manual
|
|
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
|
-
`
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) and [`AGENTS.md`](./AGENTS.md) (AI-agent
|
|
218
|
+
guidance) for contribution and testing requirements.
|