castrum 0.7.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +154 -368
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +2763 -0
- package/dist/index.js +8707 -0
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +507 -0
- package/docs/BENCHMARKS.md +343 -0
- package/docs/CASE_STUDY.md +240 -0
- package/docs/ENVIRONMENT.md +81 -0
- package/docs/FFI_BUN_GUIDE.md +327 -0
- package/docs/GETTING_STARTED.md +192 -0
- package/docs/INGRESS-ROUTER.md +81 -0
- package/docs/INGRESS.md +746 -0
- package/docs/NATIVE-ROUTE.md +53 -0
- package/docs/REPO_MAP.md +279 -0
- package/docs/adr/0001-two-wire-formats.md +39 -0
- package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
- package/docs/adr/0003-bun-builtins-delegation.md +43 -0
- package/docs/bun-builtins-decision-matrix.md +115 -0
- package/index.ts +44 -8
- package/package.json +98 -33
- package/src/ingress/body.ts +289 -0
- package/src/ingress/constants.ts +220 -48
- package/src/ingress/context.ts +136 -0
- package/src/ingress/decode/baked-result.ts +121 -0
- package/src/ingress/decode/fast-result.ts +160 -0
- package/src/ingress/decode/packed-sections.ts +56 -0
- package/src/ingress/decode/result-base.ts +172 -0
- package/src/ingress/errors.ts +72 -0
- package/src/ingress/fast.ts +152 -1021
- package/src/ingress/handlers.ts +639 -1001
- package/src/ingress/headers/baked-templates.ts +102 -0
- package/src/ingress/headers/cors.ts +44 -0
- package/src/ingress/headers/fast-templates.ts +238 -0
- package/src/ingress/headers/hsts.ts +83 -0
- package/src/ingress/health.ts +68 -0
- package/src/ingress/index.ts +142 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +139 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +164 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +102 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/router.ts +237 -0
- package/src/ingress/routes/common.ts +79 -0
- package/src/ingress/routes/delete.ts +14 -0
- package/src/ingress/routes/echo.ts +107 -0
- package/src/ingress/routes/fallback.ts +23 -0
- package/src/ingress/routes/head.ts +27 -0
- package/src/ingress/routes/index.ts +11 -0
- package/src/ingress/routes/json-write.ts +129 -0
- package/src/ingress/routes/options.ts +26 -0
- package/src/ingress/routes/read.ts +35 -0
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +475 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +191 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +220 -0
- package/src/integration/streaming.ts +78 -0
- package/src/integration/websocket.ts +93 -0
- package/src/loader/batch.ts +137 -0
- package/src/loader/cost.ts +105 -0
- package/src/loader/index.ts +745 -0
- package/src/loader/ops.ts +657 -0
- 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 +1775 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +240 -0
- package/src/native/types/instances.ts +274 -0
- package/src/native/types.ts +401 -0
- 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 +10 -0
- 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 +87 -0
- package/src/rust-ffi/context.ts +193 -0
- package/src/rust-ffi/index.ts +51 -541
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +28 -0
- package/src/rust-ffi/scalar/crypto.ts +346 -0
- package/src/rust-ffi/scalar/factories.ts +459 -0
- package/src/rust-ffi/scalar/hashing.ts +74 -0
- package/src/rust-ffi/scalar/http.ts +337 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +325 -0
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +61 -0
- package/src/rust-ffi/scalar/payload.ts +241 -0
- package/src/rust-ffi/text.ts +121 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +73 -0
- package/src/shared/adaptive.ts +67 -0
- package/src/shared/buffer-pool.ts +264 -0
- package/src/shared/bytes.ts +108 -9
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +75 -0
- package/src/shared/metrics.ts +247 -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/wire.ts +504 -0
- package/src/shared/proven.ts +289 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +29 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +15 -0
- package/Cargo.toml +0 -73
- package/bench.ts +0 -3
- package/rust/batch.rs +0 -195
- package/rust/cookie_parser.rs +0 -70
- package/rust/cors.rs +0 -263
- package/rust/hashing.rs +0 -164
- package/rust/headers.rs +0 -234
- package/rust/hmac_sha256.rs +0 -113
- package/rust/http_parser.rs +0 -132
- package/rust/ingress.rs +0 -616
- package/rust/ingress_constants.rs +0 -89
- package/rust/ip_trust.rs +0 -185
- package/rust/json_ops.rs +0 -66
- package/rust/json_patch_ops.rs +0 -25
- package/rust/json_schema.rs +0 -151
- package/rust/json_ser.rs +0 -429
- package/rust/lib.rs +0 -48
- package/rust/method.rs +0 -141
- package/rust/mime_lookup.rs +0 -76
- package/rust/output.rs +0 -238
- package/rust/proxy.rs +0 -90
- package/rust/query_parser.rs +0 -141
- package/rust/random_token.rs +0 -52
- package/rust/rate_limit.rs +0 -203
- package/rust/terminal.rs +0 -95
- package/rust/unit_tests.rs +0 -843
- package/rust/url_codec.rs +0 -133
- package/rust/util.rs +0 -537
- package/rust/validation.rs +0 -107
- package/rust/websocket.rs +0 -23
- package/src/baseline/index.ts +0 -12
- package/src/baseline/tasks/cookie.ts +0 -22
- package/src/baseline/tasks/hashing.ts +0 -18
- package/src/baseline/tasks/hmac.ts +0 -38
- package/src/baseline/tasks/http.ts +0 -62
- package/src/baseline/tasks/json-patch.ts +0 -12
- package/src/baseline/tasks/json.ts +0 -50
- package/src/baseline/tasks/mime.ts +0 -5
- package/src/baseline/tasks/query.ts +0 -14
- package/src/baseline/tasks/token.ts +0 -15
- package/src/baseline/tasks/url.ts +0 -11
- package/src/baseline/tasks/validation.ts +0 -19
- package/src/baseline/tasks/websocket.ts +0 -14
- package/src/bench/assert.ts +0 -70
- package/src/bench/checks.ts +0 -227
- package/src/bench/checksum.ts +0 -27
- package/src/bench/comparisons.ts +0 -70
- package/src/bench/concurrent-worker.ts +0 -135
- package/src/bench/fixtures.ts +0 -233
- package/src/bench/index.ts +0 -2
- package/src/bench/measure.ts +0 -177
- package/src/bench/now.ts +0 -3
- package/src/bench/report.ts +0 -79
- package/src/bench/run.ts +0 -57
- package/src/bench/tasks/complex.ts +0 -232
- package/src/bench/tasks/concurrent.ts +0 -213
- package/src/bench/tasks/cookie.ts +0 -44
- package/src/bench/tasks/hashing.ts +0 -33
- package/src/bench/tasks/hmac.ts +0 -36
- package/src/bench/tasks/http.ts +0 -50
- package/src/bench/tasks/index.ts +0 -52
- package/src/bench/tasks/json-patch.ts +0 -21
- package/src/bench/tasks/json.ts +0 -33
- package/src/bench/tasks/mime.ts +0 -21
- package/src/bench/tasks/query.ts +0 -44
- package/src/bench/tasks/stress.ts +0 -142
- package/src/bench/tasks/token.ts +0 -20
- package/src/bench/tasks/url.ts +0 -34
- package/src/bench/tasks/validation.ts +0 -57
- package/src/bench/tasks/websocket.ts +0 -21
- package/src/bench/types.ts +0 -60
- package/src/data/json-rows.ts +0 -33
- package/src/ingress/packed-input.ts +0 -59
- package/src/shared/json.ts +0 -18
- package/src/shared/packed.ts +0 -316
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Native route stack — the `@ignex/native` wire (route-wire v3)
|
|
2
|
+
|
|
3
|
+
`rust/ingress/native_route.rs` exposes the **per-route native stack**: a route
|
|
4
|
+
descriptor compiles once into a pre-baked `NativeRoute`, then each request runs
|
|
5
|
+
ONE native call. It is the LIVE external wire consumed by `@ignex/native`'s
|
|
6
|
+
`createNativeRoute` — a `castrum` consumer that compiles routes natively.
|
|
7
|
+
|
|
8
|
+
> **History**: the old `rust/route.rs` was dead external-project wire (a THIRD
|
|
9
|
+
> wire format, never bound in `src/native/ffi.ts`) and was removed in v0.9.0.
|
|
10
|
+
> `native_route.rs` implements the now-live ignex contract and supersedes it.
|
|
11
|
+
|
|
12
|
+
## Route-wire v3
|
|
13
|
+
|
|
14
|
+
- Magic `ROUT` (0x524f5554), version 3.
|
|
15
|
+
- Stage tags: `parseQuery = 0 … requireJsonBody = 5`.
|
|
16
|
+
- Result layout: `[flags u32][errorCode u32]` + optional query/cookie pair
|
|
17
|
+
sections.
|
|
18
|
+
|
|
19
|
+
Descriptor/stage/part tags + result layout must match `route-wire.ts` EXACTLY —
|
|
20
|
+
`ROUTE_DESC_VERSION` bumps on any wire change (a mismatched compiler/addon must
|
|
21
|
+
be a hard reject, never a silent misparse).
|
|
22
|
+
|
|
23
|
+
## Parse semantics (LENIENT)
|
|
24
|
+
|
|
25
|
+
Byte-parity with ignex's JS `queryPairs` / `cookiePairs`: malformed `%ZZ` /
|
|
26
|
+
invalid-UTF-8 `%FF` pass through raw, `+` → space, `%2B` → `+`, cookies trim +
|
|
27
|
+
DQUOTE-unwrap the VALUE but not the name, no cookie URL-decoding. Do NOT reuse
|
|
28
|
+
the strict scalar `query_parser` for this wire.
|
|
29
|
+
|
|
30
|
+
## Validation
|
|
31
|
+
|
|
32
|
+
The stack validates the BODY only (via `IngressSchema`); a non-body schema in
|
|
33
|
+
the descriptor is an unsupported feature → fail compile so the caller falls
|
|
34
|
+
back to JS. `requireJsonBody` → 400; `validateBody` schema fail → 422;
|
|
35
|
+
first-failure-wins in stage order.
|
|
36
|
+
|
|
37
|
+
## Surfaces
|
|
38
|
+
|
|
39
|
+
- `castrum_route_compile` / `castrum_route_run` / `castrum_route_destroy`
|
|
40
|
+
(C-ABI, needed-size convention, panic-guarded, immutable `&self` run).
|
|
41
|
+
- napi `Route` class (Node / fallback path).
|
|
42
|
+
|
|
43
|
+
## Tests / parity
|
|
44
|
+
|
|
45
|
+
- `rust/ingress/native_route.rs` unit tests + `rust/ffi.rs` C-ABI tests.
|
|
46
|
+
- `test/unit/ingress/native-route.test.ts`.
|
|
47
|
+
- `scripts/verify-native-route.ts` + ignex's `route-wire.test.ts` /
|
|
48
|
+
`packages/native/test/route.test.ts`.
|
|
49
|
+
|
|
50
|
+
> **Naming note (flux → ignex)**: the external consumer was historically called
|
|
51
|
+
> `@flux/native` (castrum v0.8.0, guarded by `test/compat/flux-contract.test.ts`).
|
|
52
|
+
> The current live wire is `@ignex/native`. Both names appear in changelogs;
|
|
53
|
+
> `@ignex/native` is the active contract.
|
package/docs/REPO_MAP.md
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# castrum — Repo Map (what is where, and why it works this way)
|
|
2
|
+
|
|
3
|
+
This is the **navigation doc**. It tells you where everything lives, how the
|
|
4
|
+
pieces connect, and how to run/build/test/bench/publish. If you are brand new,
|
|
5
|
+
read [`GETTING_STARTED.md`](./GETTING_STARTED.md) first; if you want deep-dive
|
|
6
|
+
architecture, read [`ARCHITECTURE.md`](./ARCHITECTURE.md). If you are an AI
|
|
7
|
+
agent editing this repo, [`AGENTS.md`](../AGENTS.md) is the agent-facing map.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. What this project is
|
|
12
|
+
|
|
13
|
+
`castrum` is a hybrid **Bun (TypeScript) + Rust** package:
|
|
14
|
+
|
|
15
|
+
- A **Rust cdylib** (`castrum.<platform>-<arch>.node`, built with napi-rs,
|
|
16
|
+
ALSO exporting `extern "C"` symbols for Bun's `bun:ffi` — see
|
|
17
|
+
`rust/ffi.rs`) provides the performance-critical primitives (parsers,
|
|
18
|
+
crypto, hashing, JSON, compression, and the HTTP **ingress pipeline**).
|
|
19
|
+
Under Bun, `bun:ffi` is the PRIMARY transport; NAPI is the fallback
|
|
20
|
+
(Node, `CASTRUM_FFI_MODE=napi`, or a failed ffi self-test).
|
|
21
|
+
- **TypeScript** (`src/`) provides the ergonomic public API: the flat `rust.*`
|
|
22
|
+
FFI client and the ingress layer.
|
|
23
|
+
- It ships benchmarks: a **CPU benchmark** (`bench.ts` → `src/bench`) and an
|
|
24
|
+
**HTTP benchmark** (`bench/run-bench.ts` + `bench/servers/*`).
|
|
25
|
+
|
|
26
|
+
Bun is the primary runtime; Node.js ≥20.3 is supported via a compiled ESM entry
|
|
27
|
+
(`dist/`).
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 2. Command cheat-sheet
|
|
32
|
+
|
|
33
|
+
| Task | Command | Notes |
|
|
34
|
+
|------|---------|-------|
|
|
35
|
+
| Install deps | `bun install` | |
|
|
36
|
+
| Build Rust addon (release) | `bun run build` | `napi build --release --platform` |
|
|
37
|
+
| Build Rust addon (LOCAL max perf) | `bun run build:perf` | x86-64-v3 + AVX2 — **never for publish** |
|
|
38
|
+
| Build Rust addon (debug) | `bun run build:debug` | |
|
|
39
|
+
| Build compiled JS entry (Node) | `bun run build:js` | bundle + types → `dist/` (gitignored) |
|
|
40
|
+
| TS unit tests | `bun test` | ~540 tests, `test/unit/**` |
|
|
41
|
+
| Rust unit tests | `bun run test:rust` | `cargo test`, ~440 tests |
|
|
42
|
+
| Node smoke tests | `bun run test:node` | explicit `.mjs` paths (Node 24 rejects dir arg) |
|
|
43
|
+
| Installed-tarball e2e | `bun run verify:install` | pack → install → import from `node_modules` |
|
|
44
|
+
| Typecheck | `bun run typecheck` | `bunx tsc --noEmit` (only `index.ts`, `bench.ts`, `src/`) |
|
|
45
|
+
| CPU benchmark + correctness | `bun run check` | == `bun bench.ts`; writes `bench/results/cpu/` |
|
|
46
|
+
| HTTP bench (all servers) | `bun run bench:http` | |
|
|
47
|
+
| HTTP smoke (fast sanity) | `bun run bench:http:smoke` | **the wire-format guard** (CI-gated) |
|
|
48
|
+
| Startup bench | `bun run bench:startup` | import + first-call timing |
|
|
49
|
+
| JS lint (Biome) | `bun run lint` / `lint:fix` | `bunx biome check [--write] .` |
|
|
50
|
+
| JS format (Biome) | `bun run format` | `bunx biome format --write .` |
|
|
51
|
+
| Version consistency | `bun run check:version` | package.json ↔ Cargo.toml ↔ CHANGELOG |
|
|
52
|
+
| JS dependency audit | `bun run audit` | `bun audit` (reports advisories) |
|
|
53
|
+
| Cargo deny audit | `bun run deny` | `cargo deny check` |
|
|
54
|
+
| Publish (manual, single-platform) | `bun run publish:manual[:dry]` | see §6 |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 3. Directory map
|
|
59
|
+
|
|
60
|
+
### Source (what you edit)
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
index.ts Package entry (Bun). Re-exports src/rust-ffi, shared helpers, src/ingress.
|
|
64
|
+
bench.ts CPU benchmark entry point → src/bench/run.ts.
|
|
65
|
+
|
|
66
|
+
examples/ Runnable sample app (basic-server.ts + README) — see also README Quick Start.
|
|
67
|
+
basic-server.ts Minimal pre-baked ingress server + rust.* primitives.
|
|
68
|
+
|
|
69
|
+
src/
|
|
70
|
+
native/ The native transport layer: bun:ffi (primary on Bun) + NAPI fallback.
|
|
71
|
+
types.ts NativeAddon module-surface interface (mirrors index.d.ts).
|
|
72
|
+
types/instances.ts Per-class INSTANCE types (Ingress, SchemaValidator, JwtSigner, ...) + result types.
|
|
73
|
+
loader.ts Addon path resolution (multi-root) + getAddon()/lazyAddon().
|
|
74
|
+
ffi.ts bun:ffi C-ABI bindings (Bun-only): lazy bind + bind-time self-test,
|
|
75
|
+
CASTRUM_FFI_MODE gating, castrum_ingress_layout blob for constants.ts.
|
|
76
|
+
ffi/ ffi/types.ts (BunFFI interface) + constants.ts + selftest.ts (bind-time self-test).
|
|
77
|
+
index.ts Barrel.
|
|
78
|
+
runtime/ THE RUNTIME ADAPTER seam (Bun/Node) — selected once at load.
|
|
79
|
+
detect.ts Cached runtime detection — the ONLY place `typeof Bun` is checked.
|
|
80
|
+
types.ts RuntimeAdapter contract (codec/uuid/env/builtins/transport/server/websocket).
|
|
81
|
+
codec.ts Runtime-native UTF-8 codec (Bun transfer vs TextEncoder/Decoder).
|
|
82
|
+
uuid.ts uuidv7 (Bun.randomUUIDv7 vs crypto.randomUUID).
|
|
83
|
+
builtins.ts BUN_WINS delegation registry (single source; `has(op)`).
|
|
84
|
+
transport.ts bun:ffi-first `resolve(op)` + ffi/napi surfaces (lazy bind).
|
|
85
|
+
native.ts FFI-facing seam (codec/uuid/env/builtins/transport) — imported by rust-ffi.
|
|
86
|
+
server.ts createIngressServer: Bun.serve on Bun, node:http on Node.
|
|
87
|
+
websocket.ts 101 upgrade on Bun; clear Bun-only error on Node.
|
|
88
|
+
index.ts FULL adapter `runtime` (native + server + websocket) — for the public API.
|
|
89
|
+
rust-ffi/ The flat `rust.*` FFI client (TS wrappers over the addon).
|
|
90
|
+
client.ts createRust() factory + the default `rust` instance + configure().
|
|
91
|
+
scalar/ / text.ts / packed.ts namespaces (buildScalar, buildText, ...).
|
|
92
|
+
batch/ Array-of-bytes batch namespace: types.ts (RustBatch interface) + build.ts (impl) + index.ts.
|
|
93
|
+
context.ts Per-instance state (caches, rayon-pool bookkeeping, runtime seam).
|
|
94
|
+
options.ts RustOptions + rayon-thread resolution + coercion helpers.
|
|
95
|
+
addon.ts The single shared lazy addon proxy.
|
|
96
|
+
index.ts Barrel (exports rust, createRust, types).
|
|
97
|
+
shared/ Cross-cutting helpers.
|
|
98
|
+
runtime.ts Facade over src/runtime/detect.ts (isBun/isNode — cached).
|
|
99
|
+
codec.ts Facade over src/runtime/codec.ts.
|
|
100
|
+
uuid.ts Facade over src/runtime/uuid.ts.
|
|
101
|
+
bytes.ts encoder/decoder (Uint8Array|string union normalization) + toPlainBuffer.
|
|
102
|
+
packed/ Packed wire-format helpers (split from the old packed.ts monolith).
|
|
103
|
+
wire.ts PURE byte encode/decode (u32/bitset/i64/byte-results/multipart, pairs, pack-scratch) — no addon.
|
|
104
|
+
schema.ts SchemaValidator alias + schemaValidateBatch/Count.
|
|
105
|
+
parsers.ts lazy-addon string parsers (parseQueryString/parseCookieHeader/parseFormBody).
|
|
106
|
+
index.ts Barrel (existing `../shared/packed` imports unchanged).
|
|
107
|
+
request-id.ts Zero-alloc request-id generator (both ingress paths).
|
|
108
|
+
buffer-pool.ts Generic reusable byte-buffer pool (pooled ingress output).
|
|
109
|
+
response.ts pooledBodyResponse (releases the pool on body consume).
|
|
110
|
+
log.ts createStructuredLogger (CASTRUM_LOG_LEVEL-gated JSON lines).
|
|
111
|
+
ingress/ The HTTP ingress pipeline (two paths — see §4.2).
|
|
112
|
+
fast.ts PATH 1: createIngressFast (packed-input via handleRequestPacked).
|
|
113
|
+
handlers.ts PATH 2: createIngressHandler (JS-packs the frame via IngressInputPacker
|
|
114
|
+
+ gatherRawHeadersPacked → handleRequestPacked).
|
|
115
|
+
index.ts Barrel + createIngress / createIngressSync convenience factories.
|
|
116
|
+
shared.ts JS constants + buildHeaderPlan + assertSyncCallback (shared by both paths).
|
|
117
|
+
constants.ts Binary-layout constants read from Rust at runtime (SINGLE SOURCE).
|
|
118
|
+
status.ts / errors.ts / options.ts / types.ts / body.ts / context.ts
|
|
119
|
+
packing/ header-packing.ts, input-packer.ts, gather-raw-headers.ts
|
|
120
|
+
headers/ cors.ts, hsts.ts, fast-templates.ts, baked-templates.ts
|
|
121
|
+
decode/ fast-result.ts, baked-result.ts (TWO decoders — see §4.2)
|
|
122
|
+
response/ terminal.ts, error-bodies.ts
|
|
123
|
+
routes/ read/head/json-write/echo/fallback factories + common.ts
|
|
124
|
+
server.ts createIngressServer (Bun.serve) + buildRouteHandlers + gracefulShutdown.
|
|
125
|
+
server-node.ts createIngressServerNode (node:http adapter, same route handlers).
|
|
126
|
+
integration/ Framework integration (wraps path 2 — createIngressHandler).
|
|
127
|
+
pipeline.ts createPipeline (framework-agnostic request stage for Hono/Elysia/Bun.serve).
|
|
128
|
+
websocket.ts createWebSocketUpgrade (RFC 6455 101 handshake + subprotocol).
|
|
129
|
+
streaming.ts sseResponse (SSE frames via rust.sseEncodeEvent).
|
|
130
|
+
baseline/ Pure-JS baseline implementations (benchmark reference). Internal.
|
|
131
|
+
bench/ CPU benchmark framework (tasks, measure, report, checks, comparisons).
|
|
132
|
+
data/ json-rows.ts fixture generator (benchmark-only).
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
rust/ ONE cdylib crate (Cargo [lib] → lib.rs).
|
|
137
|
+
lib.rs Declaration hub + module map + crate docs.
|
|
138
|
+
util/ Shared infrastructure: bytes, packed, batch (+batch_core), threadpool, validation.
|
|
139
|
+
http/ HTTP wire formats & parsing: headers, method, http/cookie/query parsers,
|
|
140
|
+
form, media_type, url_codec, url_join, etag, accept, mime_lookup, multipart.
|
|
141
|
+
crypto/ Auth & hashing: hmac_sha256, cookie_sign, csrf, jwt, aead, argon2,
|
|
142
|
+
base64, hashing (fnv/crc32/xxh3), random_token.
|
|
143
|
+
json/ JSON & schema: json_ops, json_ser, json_patch_ops, json_schema (napi),
|
|
144
|
+
fast_schema/ (zero-DOM engine — pure, no napi).
|
|
145
|
+
payload/ Output & streaming: compress, sse, ws_frames, websocket, template.
|
|
146
|
+
ingress/ THE ingress pipeline: mod.rs (napi boundary), pipeline.rs (core 8-stage),
|
|
147
|
+
options/time/packed, cors, proxy, ip_trust, rate_limit, terminal,
|
|
148
|
+
output.rs (single numeric layout source), ingress_constants.rs (napi projection).
|
|
149
|
+
ffi.rs #[no_mangle] extern "C" exports (79 castrum_* symbols — 71 direct + 4
|
|
150
|
+
validator_c_abi! + 4 compress_to_out!; parity guarded by
|
|
151
|
+
test/unit/features/ffi-symbol-parity.test.ts) for Bun's bun:ffi
|
|
152
|
+
primary transport, incl. castrum_ingress_layout (the layout blob).
|
|
153
|
+
unit_tests.rs / test_support.rs Cross-module Rust test suites.
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Generated / ignored (do NOT edit)
|
|
157
|
+
|
|
158
|
+
| Path | What it is | Why ignored |
|
|
159
|
+
|------|-----------|-------------|
|
|
160
|
+
| `dist/` | Compiled ESM entry + types for Node | Built by `build:js`; gitignored |
|
|
161
|
+
| `index.js` / `index.d.ts` | NAPI-RS generated loader/declarations | Regenerated by `napi build`; gitignored |
|
|
162
|
+
| `castrum.<platform>-<arch>.node` | The native addon binary | Built locally; gitignored |
|
|
163
|
+
| `bench/results/` | Bench reports (cpu/, http/) | Generated; gitignored |
|
|
164
|
+
| `target/` | Cargo build output | gitignored |
|
|
165
|
+
| `node_modules/` | Deps | gitignored |
|
|
166
|
+
| `artifacts/` | CI publish staging dir | gitignored |
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 4. How the pieces connect (the 5 cross-cutting flows)
|
|
171
|
+
|
|
172
|
+
### 4.1 Constants never drift (single numeric source)
|
|
173
|
+
`rust/ingress/output.rs` defines every ingress layout constant once → projected
|
|
174
|
+
to JS two ways: `rust/ingress/ingress_constants.rs` via `#[napi] const` (NAPI)
|
|
175
|
+
AND `rust/ffi.rs` `IngressLayout` via `castrum_ingress_layout` (C ABI, primary
|
|
176
|
+
on Bun) → `src/ingress/constants.ts` reads them at runtime (ffi blob on Bun,
|
|
177
|
+
napi on Node). Drift is pinned by the Rust unit test
|
|
178
|
+
`ingress_layout_c_abi_matches_output_source` and the bun:ffi bind-time
|
|
179
|
+
self-test. **Never hardcode a layout value** — if you change the layout, change
|
|
180
|
+
`output.rs` and nothing else.
|
|
181
|
+
|
|
182
|
+
### 4.2 The TWO ingress paths (do not conflate)
|
|
183
|
+
1. **`src/ingress/fast.ts`** — `createIngressFast(options)`. JS packs request
|
|
184
|
+
headers into a binary buffer (`IngressInputPacker`), Rust parses via
|
|
185
|
+
`handleRequestPacked`. Responses: `{"error":{code,status,message,requestId}}`
|
|
186
|
+
with `x-ratelimit-*` headers.
|
|
187
|
+
2. **`src/ingress/handlers.ts`** — `createIngressHandler(options, runtime)`.
|
|
188
|
+
The request frame is packed in JS (`IngressInputPacker` +
|
|
189
|
+
`gatherRawHeadersPacked`) and driven through the SAME native core as path 1
|
|
190
|
+
(`handleRequestPacked`), writing into a **pooled** output buffer. Responses:
|
|
191
|
+
`{"ok":true,...,"requestId":...}` (success) / `{"ok":false,"error":{code,
|
|
192
|
+
message}}` (errors) with `ratelimit-*` headers.
|
|
193
|
+
|
|
194
|
+
These wire formats are a **benchmark contract** (`bench/load.ts` validates
|
|
195
|
+
them) — they are intentionally NOT unified. The two decoders
|
|
196
|
+
(`decode/fast-result.ts` vs `decode/baked-result.ts`) and the two header-template
|
|
197
|
+
builders (`headers/fast-templates.ts` vs `headers/baked-templates.ts`) exist for
|
|
198
|
+
this reason. **Shared between both paths**: `generateRequestId`
|
|
199
|
+
(`src/shared/request-id.ts`) and `buildHeaderPlan` / `assertSyncCallback`
|
|
200
|
+
(`src/ingress/shared.ts`).
|
|
201
|
+
|
|
202
|
+
### 4.3 Addon loading + runtime seam
|
|
203
|
+
- `src/native/loader.ts` resolves the `.node` from multiple roots so it works
|
|
204
|
+
from both the source layout (`src/…`) and the bundled layout (`dist/…`).
|
|
205
|
+
Honors `CASTRUM_NATIVE_LIBRARY_PATH` / `NAPI_RS_NATIVE_LIBRARY_PATH`.
|
|
206
|
+
- Runtime detection lives **only** in `src/runtime/detect.ts` (resolved once at
|
|
207
|
+
load; `src/shared/runtime.ts` is a thin facade). The public
|
|
208
|
+
`createIngressServer` (via the adapter's `server`) picks `Bun.serve` on Bun
|
|
209
|
+
and the `node:http` backend on Node; `createIngressServerNode` stays for
|
|
210
|
+
pinned Node users (same route handlers via `buildRouteHandlers`).
|
|
211
|
+
- ⚠️ `src/ingress/constants.ts` is the **one module that dlopens the addon at
|
|
212
|
+
import time** (its constants are needed as soon as ingress is imported).
|
|
213
|
+
Everything else is lazy.
|
|
214
|
+
|
|
215
|
+
### 4.4 The runtime adapter
|
|
216
|
+
`src/runtime/` is the single seam for every Bun-vs-Node and transport decision
|
|
217
|
+
(detection, codec, uuid, env, the BUN_WINS built-in delegation, the bun:ffi-vs-
|
|
218
|
+
napi transport, the ingress server backend, websocket) — selected ONCE at
|
|
219
|
+
module load. The FFI layer imports the seam (`src/runtime/native.ts`); the
|
|
220
|
+
public API imports the full `runtime` (`src/runtime/index.ts`). No
|
|
221
|
+
`isBun()` / `getBunFFI()` branches in builders.
|
|
222
|
+
|
|
223
|
+
### 4.5 Pooled output buffers (path 2)
|
|
224
|
+
`createIngressHandler` owns a `BufferPool` (`src/shared/buffer-pool.ts`) sized
|
|
225
|
+
by `runtime.outputBufferSize`. `run()` packs the frame in JS (`IngressInputPacker`
|
|
226
|
+
+ `gatherRawHeadersPacked`) and writes via `handleRequestPacked`
|
|
227
|
+
into the pooled buffer, passing the exact written subarray to
|
|
228
|
+
`BakedIngressResult.refresh`. In copy
|
|
229
|
+
mode the buffer is released at the end of `run()`; in zero-copy mode
|
|
230
|
+
`ingress.zeroCopyResponse()` serves a `pooledBodyResponse`
|
|
231
|
+
(`src/shared/response.ts`) that keeps the buffer in flight until the body is
|
|
232
|
+
consumed.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 5. Testing & benchmarks
|
|
237
|
+
|
|
238
|
+
- **TS**: `bun test` → `test/unit/**` (`ingress/`, `shared/`, `features/`).
|
|
239
|
+
- **Rust**: `cargo test` → per-module `#[cfg(test)] mod tests` + cross-module
|
|
240
|
+
`rust/unit_tests.rs`.
|
|
241
|
+
- **Node**: `test/integration/node-smoke.test.mjs` + `node-enterprise.test.mjs`
|
|
242
|
+
via `node --test` (run with `bun run test:node`).
|
|
243
|
+
- **CPU bench**: `bun run check` (correctness checks + comparisons). Sub-µs ops
|
|
244
|
+
are batched (`CASTRUM_BENCH_BATCH_SIZE`). Report → `bench/results/cpu/`.
|
|
245
|
+
- **HTTP bench**: `bench/run-bench.ts` + `bench/load.ts` (load generator that
|
|
246
|
+
validates response SHAPE) + `bench/servers/{bun,elysia,ingress}-server.ts`.
|
|
247
|
+
`bench:http:smoke` is the fast CI wire-format gate.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## 6. Publishing (npm)
|
|
252
|
+
|
|
253
|
+
- Package name `castrum` (unscoped, public). Native binary name `castrum`
|
|
254
|
+
(declared in package.json `napi.binaryName` + Cargo.toml — keep in sync).
|
|
255
|
+
- **Multi-platform (recommended)**: push a `v*` tag → `.github/workflows/ci.yml`
|
|
256
|
+
builds each platform addon, the `publish` job downloads them into
|
|
257
|
+
`./artifacts`, stages them, and runs `npm publish` (needs `NPM_TOKEN`).
|
|
258
|
+
- **Manual single-platform**: `bun run publish:manual --increment minor`
|
|
259
|
+
(syncs package.json ↔ Cargo.toml ↔ CHANGELOG, tags, builds, publishes with
|
|
260
|
+
`CASTRUM_PUBLISH_ALLOW_PARTIAL=1`). `--dry-run` plans only.
|
|
261
|
+
- `prepublishOnly` runs `build:js` + `scripts/prepublish.mjs` (stages
|
|
262
|
+
`artifacts/*.node` and verifies every `napi.targets` maps to a `.node`).
|
|
263
|
+
- Run `bun run check:version` before tagging.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 7. Cross-links
|
|
268
|
+
|
|
269
|
+
| Doc | Purpose |
|
|
270
|
+
|-----|---------|
|
|
271
|
+
| [`GETTING_STARTED.md`](./GETTING_STARTED.md) | Beginner tutorial (intern-friendly) |
|
|
272
|
+
| [`API.md`](./API.md) | The complete public API reference |
|
|
273
|
+
| [`CASE_STUDY.md`](./CASE_STUDY.md) | Data-driven case study: how castrum was built and measured |
|
|
274
|
+
| [`ARCHITECTURE.md`](./ARCHITECTURE.md) | Deep-dive: modules, data flow, output layout, memory, concurrency |
|
|
275
|
+
| [`INGRESS.md`](./INGRESS.md) | The pre-baked ingress API + route factories + servers |
|
|
276
|
+
| [`BENCHMARKS.md`](./BENCHMARKS.md) | Benchmark scenarios, report format, how to run |
|
|
277
|
+
| [`ENVIRONMENT.md`](./ENVIRONMENT.md) | All `CASTRUM_*` env vars (+ legacy aliases) |
|
|
278
|
+
| [`AGENTS.md`](../AGENTS.md) | AI-agent editing guidance (commands, constraints, gotchas) |
|
|
279
|
+
| `README.md` | Public-facing overview + quick start |
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# ADR-0001: The two ingress wire formats are a contract, not a bug
|
|
2
|
+
|
|
3
|
+
- **Status**: Accepted (2026-08-06)
|
|
4
|
+
- **Deciders**: maintainers (via benchmark + hardening passes)
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
The ingress layer ships two paths that consume the SAME native core
|
|
9
|
+
(`handleRequestPacked`):
|
|
10
|
+
|
|
11
|
+
1. `src/ingress/fast.ts` — `createIngressFast`; JS packs headers via
|
|
12
|
+
`IngressInputPacker`; errors are `{"error":{code,status,message,requestId}}`
|
|
13
|
+
with `x-ratelimit-*` headers.
|
|
14
|
+
2. `src/ingress/handlers.ts` — `createIngressHandler`; JS packs the full frame;
|
|
15
|
+
success is Rust-generated `{"ok":true,...,"requestId":...}`, errors are
|
|
16
|
+
`{"ok":false,"error":{"code","message"}}`, with `ratelimit-*` headers.
|
|
17
|
+
|
|
18
|
+
They also use separate result decoders (`FastIngressResult` vs
|
|
19
|
+
`BakedIngressResult`) and separate header-template builders.
|
|
20
|
+
|
|
21
|
+
## Decision
|
|
22
|
+
|
|
23
|
+
**Do not unify them.** The HTTP benchmark load generator (`bench/load.ts`)
|
|
24
|
+
requires `ok === true` + `requestId: string` on success and `error.code` /
|
|
25
|
+
`error.message` on errors — i.e. path 2's format. The two formats are a
|
|
26
|
+
**benchmark contract**: the benchmark server uses path 2, and changing its
|
|
27
|
+
wire shape invalidates baselines.
|
|
28
|
+
|
|
29
|
+
## Consequences
|
|
30
|
+
|
|
31
|
+
- Both paths stay hot and zero-alloc.
|
|
32
|
+
- Duplicated decoders/templates are intentional; sharing them is
|
|
33
|
+
**forbidden** by AGENTS.md (it would couple two differently-shaped outputs).
|
|
34
|
+
- New contributors must not "helpfully" unify the formats.
|
|
35
|
+
|
|
36
|
+
## See also
|
|
37
|
+
|
|
38
|
+
- AGENTS.md §"Ingress: the two paths (do NOT conflate)"
|
|
39
|
+
- docs/INGRESS.md §"Wire format"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# ADR-0002: Zero-DOM fast path for JSON Schema validation
|
|
2
|
+
|
|
3
|
+
- **Status**: Accepted (2026-08-07), extended 2026-08-11
|
|
4
|
+
- **Deciders**: maintainers (measured via `bun run check`)
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
`jsonschema::Validator::is_valid` requires a `serde_json::Value` — a full DOM
|
|
9
|
+
per document, measured at ~95% of validation cost. That made the Rust schema
|
|
10
|
+
path parity with `ajv` at best, so castrum couldn't match its own zero-DOM
|
|
11
|
+
wins (json_valid / json_sum / packed parsers).
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
Implement a **zero-DOM draft-07 fast path** (`rust/json/fast_schema/`) that
|
|
16
|
+
compiles the common keyword subset into a `FastNode` AST and validates raw
|
|
17
|
+
bytes with a zero-alloc cursor. Unsupported keywords → `compile()` returns
|
|
18
|
+
`Err` → the caller falls back to the authoritative `jsonschema` crate (DOM
|
|
19
|
+
path). Parity is **byte-for-byte gated** against the crate
|
|
20
|
+
(`rust/json/fast_schema/tests.rs`).
|
|
21
|
+
|
|
22
|
+
Draft-07 is pinned because `jsonschema` 0.48's default draft is 2020-12 whose
|
|
23
|
+
`items` / `$ref`-sibling / boolean-exclusive semantics differ; both
|
|
24
|
+
`SchemaValidator` and `IngressSchema` build the reference with
|
|
25
|
+
`.with_draft(Draft7)` + `should_validate_formats(true)`.
|
|
26
|
+
|
|
27
|
+
## Consequences
|
|
28
|
+
|
|
29
|
+
- Fast path: scalar ~1.2–2.4x, batch ~1.0–1.56x vs `ajv` (release build).
|
|
30
|
+
- A `$schema` declaring non-draft-07 (or unsupported keywords) still falls
|
|
31
|
+
back — versatility preserved.
|
|
32
|
+
- The fast path MUST stay byte-parity with the crate for the supported
|
|
33
|
+
keyword surface; any divergence is a bug.
|
|
34
|
+
|
|
35
|
+
## See also
|
|
36
|
+
|
|
37
|
+
- rust/json/fast_schema/tests.rs (parity corpus)
|
|
38
|
+
- docs/ARCHITECTURE.md §JSON & schema
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# ADR-0003: Don't reinvent the wheel — benchmark, then delegate to Bun built-ins
|
|
2
|
+
|
|
3
|
+
- **Status**: Accepted (2026-08-11)
|
|
4
|
+
- **Deciders**: maintainers (measured on Bun 1.4.0 / release addon)
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
castrum ships Rust ops that overlap with Bun's native built-ins
|
|
9
|
+
(`Bun.hash`, `Bun.password`, `Bun.CryptoHasher`, `Bun.gzipSync`,
|
|
10
|
+
`Bun.randomUUIDv7`, `crypto.subtle`, `JSON.parse`, `URLSearchParams`). For the
|
|
11
|
+
same workload, the in-process Bun implementation can beat an FFI-crossing Rust
|
|
12
|
+
op — spending effort building or keeping such ops is reinventing the wheel.
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
Every overlapping op is **benchmarked against its Bun built-in** before it is
|
|
17
|
+
built, kept, or delegated. Results live in
|
|
18
|
+
`docs/bun-builtins-decision-matrix.md`. Rules:
|
|
19
|
+
|
|
20
|
+
1. **Delegate to Bun** at the selection/delegation layer where Bun wins
|
|
21
|
+
clearly: non-crypto hashing (`Bun.hash.crc32` ~3x, `xxHash3` ~4x),
|
|
22
|
+
gzip (`Bun.gzipSync`/`gunzipSync`), `Bun.randomUUIDv7` (~2x), HMAC
|
|
23
|
+
(`Bun.CryptoHasher` ~1.2x).
|
|
24
|
+
2. **Keep rust** where it wins or Bun has no synchronous equivalent:
|
|
25
|
+
argon2id password hashing (~1.8–2x vs `Bun.password`), bcrypt verify
|
|
26
|
+
(~1.5x), brotli, PBKDF2, zero-DOM JSON/parsers.
|
|
27
|
+
3. **Don't build** what Bun already wins at (UUIDv7 → `uuidv7()` delegates to
|
|
28
|
+
`Bun.randomUUIDv7`, falls back to `crypto.randomUUID`).
|
|
29
|
+
|
|
30
|
+
## Consequences
|
|
31
|
+
|
|
32
|
+
- New `rust.*` primitives ship with a Bun-built-in baseline + decision-matrix
|
|
33
|
+
entry (`xxh3`, `passwordHashBcrypt`/`verifyBcrypt`, `pbkdf2Sha256`).
|
|
34
|
+
- The Bun/JS baselines are recorded honestly in the decision matrix
|
|
35
|
+
(e.g. `xxh3` is measured `Bun.hash.xxHash3` ~4x faster — the public
|
|
36
|
+
`rust.xxh3` delegates to it under Bun via the runtime adapter's `builtins`).
|
|
37
|
+
- Diagnostic benchmark tasks use `diag:` names so they are never mistaken for
|
|
38
|
+
shipped-op `native:`/`rust:` comparisons.
|
|
39
|
+
|
|
40
|
+
## See also
|
|
41
|
+
|
|
42
|
+
- docs/bun-builtins-decision-matrix.md
|
|
43
|
+
- src/bench/tasks/bun-builtins.ts
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Bun Built-ins vs castrum — Decision Matrix
|
|
2
|
+
|
|
3
|
+
> **Question this answers**: for the same workload, does a Bun native built-in
|
|
4
|
+
> beat the castrum Rust op? Where it does, castrum should stop reinventing the
|
|
5
|
+
> wheel and delegate to Bun; where castrum wins (or Bun
|
|
6
|
+
> has no sync equivalent), the Rust op stands.
|
|
7
|
+
|
|
8
|
+
## Methodology
|
|
9
|
+
|
|
10
|
+
- **Runtime**: Bun **1.4.0** (the primary target).
|
|
11
|
+
- **Addon**: castrum **release** build (`bun run build`, baseline CPU — the
|
|
12
|
+
shipped profile).
|
|
13
|
+
- **Driver**: `bun run check` (CPU benchmark → `bench/results/cpu/latest.json`).
|
|
14
|
+
Each pair runs on the SAME input (fixture payloads, see table).
|
|
15
|
+
- **Naming**: diagnostic comparisons use `diag:` task names so they are kept out
|
|
16
|
+
of the shipped-op `native:`/`rust:` comparisons (see `src/bench/comparisons.ts`).
|
|
17
|
+
- **Honesty guard**: every Bun API is verified to actually execute real work at
|
|
18
|
+
runtime before a result is trusted. This is important — the first run
|
|
19
|
+
produced false "wins" (brotli 156×, validators 3–5×) because this Bun
|
|
20
|
+
version has **no** `Bun.brotliCompressSync` and **no** `Bun.validators`, and
|
|
21
|
+
the feature-detect fell through to an empty return. Those pairs were removed,
|
|
22
|
+
not reported.
|
|
23
|
+
- Ratio is **nativeAvgMs / rustAvgMs** (>1 = Bun faster; <1 = rust faster).
|
|
24
|
+
|
|
25
|
+
## Results (Bun 1.4.0, release addon, 2026-08-11)
|
|
26
|
+
|
|
27
|
+
| castrum op | Bun built-in | Input | Winner | Ratio |
|
|
28
|
+
|---|---|---|---|---|
|
|
29
|
+
| `rust.fnv1a64` | `Bun.hash` (wyhash) | 42 B | Bun (parity) | 1.06× |
|
|
30
|
+
| `rust.crc32` | `Bun.hash.crc32` | 42 B | **Bun** | 2.8–8.4× |
|
|
31
|
+
| `rust.xxh3` (new) | `Bun.hash.xxHash3` | 42 B | **Bun** | 4.15× |
|
|
32
|
+
| `rust.hmacSha256` | `Bun.CryptoHasher("sha256", key)` | 32 B | Bun (mild) | 1.1–1.4× |
|
|
33
|
+
| `rust.passwordHash` (argon2id) | `Bun.password.hashSync` (argon2id) | 12 B | **rust** | 0.55× (rust 1.83×) |
|
|
34
|
+
| `rust.passwordVerify` | `Bun.password.verifySync` | 12 B | **rust** | 0.53× (rust 1.88×) |
|
|
35
|
+
| `rust.passwordHashBcrypt` (new) | `Bun.password.hashSync` (bcrypt) | 12 B | Bun (mild) | 1.24× |
|
|
36
|
+
| `rust.passwordVerifyBcrypt` (new) | `Bun.password.verifySync` (bcrypt) | 12 B | **rust** | 0.67× (rust 1.49×) |
|
|
37
|
+
| `rust.pbkdf2Sha256` (new) | `node:crypto.pbkdf2Sync` | 12 B | rust (parity) | 0.93× (rust 1.08×) |
|
|
38
|
+
| `rust.randomToken` (16 B) | `Bun.randomUUIDv7` | — | **Bun** | 1.62× |
|
|
39
|
+
| `rust.gzipCompress` | `Bun.gzipSync` | 11 179 B | **Bun** | 2.02× |
|
|
40
|
+
| `rust.gzipDecompress` | `Bun.gunzipSync` | 1 045 B | **Bun** | 1.38× |
|
|
41
|
+
| `rust.brotliCompress` | *no sync Bun API* | — | N/A | — |
|
|
42
|
+
| `rust.brotliDecompress` | *no sync Bun API* | — | N/A | — |
|
|
43
|
+
| `rust.validateEmail/Uuid/Ipv4/Ipv6` | `Bun.validators` (*absent in 1.4*) | — | N/A | — |
|
|
44
|
+
|
|
45
|
+
> Same-cost note: `Bun.password.hashSync` ran at `memoryCost 4096 / timeCost 1`
|
|
46
|
+
> (4 MiB, 1 iter) matching the rust bench cost (`mCost 4096 / tCost 1`) — the
|
|
47
|
+
> rust argon2id win is cost-for-cost, not a cost cheat.
|
|
48
|
+
|
|
49
|
+
> Transport note: "FFI-crossing Rust op" below means any Rust call over the
|
|
50
|
+
> native bridge. Under Bun that bridge is now `bun:ffi` (the PRIMARY transport);
|
|
51
|
+
> under Node it is the napi fallback. This Bun-builtin-vs-Rust axis is
|
|
52
|
+
> independent of the ffi-vs-napi transport axis — these decisions recommend
|
|
53
|
+
> WHICH implementation to call (`rust.*` vs a Bun built-in), not which transport
|
|
54
|
+
> carries the call.
|
|
55
|
+
|
|
56
|
+
## Decisions
|
|
57
|
+
|
|
58
|
+
### → Delegate to Bun at the selection layer (Bun wins)
|
|
59
|
+
These rust ops lose to Bun's native implementation on the SAME workload — the
|
|
60
|
+
FFI crossing cannot beat Bun's in-process C++:
|
|
61
|
+
|
|
62
|
+
This table mirrors `BUILTIN_OPS` in `src/runtime/builtins.ts` (the 11 ops that
|
|
63
|
+
delegate to Bun built-ins under Bun; `src/selection.ts` derives from it). `fnv1a64` is parity/either but is NOT delegated
|
|
64
|
+
(`Bun.hash` is wyhash — a different algorithm — and rust `fnv1a64` stays the
|
|
65
|
+
benchmarked impl).
|
|
66
|
+
|
|
67
|
+
| Op | Recommendation |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `crc32` | Prefer `Bun.hash.crc32`; keep rust as fallback (pure-TS path already exists). |
|
|
70
|
+
| `xxh3` | Prefer `Bun.hash.xxHash3` (~4×); keep the rust export as the Node/non-Bun fast path. The runtime adapter delegates the public `rust.xxh3` to Bun under Bun. |
|
|
71
|
+
| `gzipCompress` | Prefer `Bun.gzipSync` (~2×). **`gzipDecompress` is deliberately NOT delegated** — `Bun.gunzipSync` has no decompression-bomb cap; the rust surface keeps its native 64 MiB-capped path under Bun. |
|
|
72
|
+
| `randomToken` | Prefer `Bun.randomUUIDv7()` (or `crypto.getRandomValues`) for token-sized output; keep rust for byte-precise control. |
|
|
73
|
+
| `hmacSha256` | Prefer `Bun.CryptoHasher("sha256", key)` (1.17× — mild; keep rust batch path, which wins on larger inputs). |
|
|
74
|
+
| `urlEncode` / `urlDecode` | Prefer `encodeURIComponent` / `decodeURIComponent` (JSC string builtins — ~11.5×, zero alloc). |
|
|
75
|
+
| `base64Encode` / `base64UrlEncode` | Prefer `Buffer.toString('base64'|'base64url')`. |
|
|
76
|
+
| `hexEncode` | Prefer `Buffer.toString('hex')`. |
|
|
77
|
+
| `httpDate` | Prefer `Date.toUTCString()`. |
|
|
78
|
+
|
|
79
|
+
**Delegation rule of thumb**: rust stays the default only where it beats its
|
|
80
|
+
**Bun** baseline (not just its JS baseline). These are the ops where rust
|
|
81
|
+
genuinely wins or Bun has no equivalent.
|
|
82
|
+
|
|
83
|
+
### → Keep rust (rust wins or Bun has no equivalent)
|
|
84
|
+
| Op | Rationale |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `passwordHash` / `passwordVerify` (argon2id) | rust 1.83–1.88× faster than `Bun.password` at equal cost. **Do not delegate.** |
|
|
87
|
+
| `passwordHashBcrypt` / `passwordVerifyBcrypt` (new) | Hash ≈ parity (Bun 1.24×), verify rust 1.49×. Keep rust — self-contained and works under Node. |
|
|
88
|
+
| `pbkdf2Sha256` (new) | Bun has no sync PBKDF2; rust ≈ parity with node:crypto (1.08×). Keep rust. |
|
|
89
|
+
| `brotliCompress` / `brotliDecompress` | Bun 1.4 has no synchronous brotli; keep rust (async `CompressionStream` is not drop-in). |
|
|
90
|
+
| `validateEmail` / `validateUuid` / `validateIpv4` / `validateIpv6` | `Bun.validators` absent in 1.4; keep rust. Re-check when Bun adds it. |
|
|
91
|
+
| `jsonValid` / `jsonSumIds` / packed parsers / ingress | No Bun sync equivalent for the zero-DOM / zero-copy semantics; keep rust. |
|
|
92
|
+
|
|
93
|
+
### → Implemented primitives (gaps closed)
|
|
94
|
+
| Primitive | Outcome |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `rust.xxh3` (XXH3-64) | Exposed; measured `Bun.hash.xxHash3` 4.15× faster → the runtime adapter delegates the public `rust.xxh3` to Bun under Bun (Node keeps the addon). |
|
|
97
|
+
| `rust.passwordHashBcrypt` / `rust.passwordVerifyBcrypt` | bcrypt `$2b$` PHC; hash parity, verify rust 1.49×. |
|
|
98
|
+
| `rust.pbkdf2Sha256` | PBKDF2-HMAC-SHA256; parity with `node:crypto`, the only sync option in Bun. |
|
|
99
|
+
| UUIDv7 (`uuidv7()`) | `Bun.randomUUIDv7` already wins — delegated to Bun (`crypto.randomUUID` on Node), not built in Rust. |
|
|
100
|
+
|
|
101
|
+
## Summary
|
|
102
|
+
|
|
103
|
+
**The "don't reinvent the wheel" takeaway:** for non-cryptographic hashing,
|
|
104
|
+
random tokens, and gzip, Bun 1.4's built-ins are already faster than any
|
|
105
|
+
FFI-crossing Rust op — the selection layer should prefer them. Rust retains a clear,
|
|
106
|
+
defensible moat exactly where it matters: **argon2id password hashing
|
|
107
|
+
(≈1.9× vs Bun)**, the **zero-DOM / zero-copy parsers and JSON paths** (no Bun
|
|
108
|
+
sync equivalent), and **brotli** (no Bun sync API).
|
|
109
|
+
|
|
110
|
+
## Re-run
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
bun run build # release addon
|
|
114
|
+
bun run check # writes latest.json; grep the "vs Bun" lines
|
|
115
|
+
```
|
package/index.ts
CHANGED
|
@@ -1,8 +1,36 @@
|
|
|
1
|
-
|
|
1
|
+
// index.ts — castrum package entry (barrel).
|
|
2
|
+
//
|
|
3
|
+
// Re-exports the full public API: the flat `rust.*` FFI surface
|
|
4
|
+
// (src/rust-ffi), the runtime selection hints (src/selection), and the
|
|
5
|
+
// higher-order loader (src/loader). See AGENTS.md for the layout map.
|
|
6
|
+
//
|
|
7
|
+
// EAGER-DLOPEN CONTRACT: this barrel unconditionally re-exports `src/ingress`
|
|
8
|
+
// → `src/ingress/constants.ts`, which is the ONE module that touches native at
|
|
9
|
+
// import time (it reads the Rust binary layout and runs the bind-time
|
|
10
|
+
// self-test). This is intentional — even `import { rust }` pays the eager
|
|
11
|
+
// load. Do NOT add more eager-native modules; keep everything else lazy (see
|
|
12
|
+
// test/unit/features/import-contract.test.ts).
|
|
2
13
|
|
|
3
|
-
export *
|
|
14
|
+
export * from './src/rust-ffi'
|
|
4
15
|
|
|
5
|
-
|
|
16
|
+
// Native-vs-JS selection (OWNED HERE, not by consumers): `opImpl(op)` is the
|
|
17
|
+
// benchmark-driven recommendation for which operations should use the Rust
|
|
18
|
+
// addon vs a pure-JS implementation. Consumers read it once at load time and
|
|
19
|
+
// bind each op to a fixed impl. Source: rust/selection.rs (audited by
|
|
20
|
+
// `bun run check:selection`). See src/selection.ts.
|
|
21
|
+
export { opImpl, isNativeOp, opDecision } from './src/selection'
|
|
22
|
+
export type { OpImpl, OpDecision } from './src/selection'
|
|
23
|
+
|
|
24
|
+
export { encoder, decoder } from './src/shared/bytes'
|
|
25
|
+
// Bounded EWMA adaptive-estimate utility (drives BufferPool adaptive sizing
|
|
26
|
+
// and reusable runtime self-optimization decisions).
|
|
27
|
+
export { AdaptiveEstimate } from './src/shared/adaptive'
|
|
28
|
+
export type { AdaptiveEstimateOptions } from './src/shared/adaptive'
|
|
29
|
+
// UUIDv7 generation — delegates to Bun.randomUUIDv7, crypto.randomUUID on Node.
|
|
30
|
+
export { uuidv7 } from './src/shared/uuid'
|
|
31
|
+
// Zero-dep metrics registry (counters/gauges/histograms + Prometheus render).
|
|
32
|
+
export { createMetrics, DEFAULT_BUCKETS } from './src/shared/metrics'
|
|
33
|
+
export type { MetricsRegistry, Counter, Gauge, Histogram } from './src/shared/metrics'
|
|
6
34
|
|
|
7
35
|
export {
|
|
8
36
|
packBatch,
|
|
@@ -12,13 +40,21 @@ export {
|
|
|
12
40
|
pairsToObject,
|
|
13
41
|
parseQueryString,
|
|
14
42
|
parseCookieHeader,
|
|
43
|
+
parseFormBody,
|
|
15
44
|
unpackBitset,
|
|
16
45
|
unpackByteResults,
|
|
17
46
|
unpackI64ArrayAsBigInt,
|
|
18
47
|
unpackU32Array,
|
|
19
|
-
} from
|
|
20
|
-
export type { Pair, ParsedHttpRequestPacked } from
|
|
48
|
+
} from './src/shared/packed'
|
|
49
|
+
export type { Pair, ParsedHttpRequestPacked } from './src/shared/packed'
|
|
50
|
+
|
|
51
|
+
export * from './src/ingress'
|
|
52
|
+
|
|
53
|
+
// Framework-agnostic integration helpers: run the ingress pipeline as a
|
|
54
|
+
// request stage (createPipeline), RFC 6455 upgrade (createWebSocketUpgrade),
|
|
55
|
+
// and SSE framing (sseResponse). See docs/INGRESS.md.
|
|
56
|
+
export * from './src/integration'
|
|
21
57
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export * from
|
|
58
|
+
// Higher-order loader (HFC) over the curated op set: createLoader / loader
|
|
59
|
+
// dispatch single items, packed batches, and load() coalescing. See README.
|
|
60
|
+
export * from './src/loader'
|