castrum 0.9.8 → 0.9.10
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/Cargo.lock +11 -1
- package/Cargo.toml +17 -3
- package/castrum.darwin-arm64.node +0 -0
- package/castrum.darwin-x64.node +0 -0
- package/castrum.linux-arm64-gnu.node +0 -0
- package/castrum.linux-arm64-musl.node +0 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/castrum.linux-x64-musl.node +0 -0
- package/castrum.linux-x64-v3-gnu.node +0 -0
- package/castrum.win32-x64-msvc.node +0 -0
- package/dist/index.d.ts +164 -4
- package/dist/index.js +434 -70
- package/docs/ENVIRONMENT.md +7 -0
- package/docs/RND-CONCURRENCY.md +20 -4
- package/docs/bun-builtins-decision-matrix.md +17 -7
- package/docs/superpowers/plans/2026-09-18-elysia-learnings-adoption.md +634 -0
- package/index.ts +5 -0
- package/package.json +2 -1
- package/rust/crypto/base64.rs +472 -57
- package/rust/crypto/hashing.rs +68 -0
- package/rust/ffi/task.rs +19 -18
- package/rust/ingress/api.rs +21 -5
- package/rust/ingress/mod.rs +7 -0
- package/rust/ingress/native_route.rs +4 -2
- package/rust/ingress/schema_cache.rs +266 -0
- package/rust/payload/sse.rs +78 -5
- package/rust/task/mod.rs +1 -0
- package/rust/task/runtime.rs +55 -7
- package/rust/task/tests.rs +98 -1
- package/rust/util/text.rs +112 -10
- package/rust/util/validation.rs +34 -0
- package/src/ingress/abort.ts +48 -0
- package/src/ingress/body.ts +104 -8
- package/src/ingress/handlers.ts +1 -0
- package/src/ingress/health.ts +22 -0
- package/src/ingress/index.ts +3 -1
- package/src/ingress/router.ts +36 -1
- package/src/ingress/routes/common.ts +9 -1
- package/src/ingress/routes/echo.ts +10 -0
- package/src/ingress/routes/json-write.ts +11 -0
- package/src/ingress/routes/native.ts +24 -5
- package/src/ingress/routes/options.ts +6 -2
- package/src/ingress/routes/responder.ts +25 -5
- package/src/ingress/routes/safe-record.ts +57 -0
- package/src/ingress/server-error.ts +111 -0
- package/src/ingress/server-node.ts +102 -23
- package/src/ingress/server.ts +140 -32
- package/src/integration/pipeline.ts +6 -0
- package/src/native/ffi/build/task.ts +2 -1
- package/src/native/ffi/types.ts +6 -1
- package/src/native/types.ts +6 -0
- package/src/runtime/detect.ts +14 -0
- package/src/rust-ffi/context.ts +46 -3
- package/src/rust-ffi/scalar/factories.ts +6 -0
- package/src/rust-ffi/scalar/interface.ts +14 -0
- package/src/rust-ffi/text.ts +5 -4
- package/src/shared/buffer-pool.ts +10 -0
- package/src/shared/memory.ts +68 -0
- package/src/shared/metrics.ts +86 -3
- package/src/shared/runtime.ts +7 -0
- package/src/task/runtime.ts +52 -9
package/docs/ENVIRONMENT.md
CHANGED
|
@@ -38,8 +38,15 @@ for false; anything else is warned about and falls back to the default.
|
|
|
38
38
|
| Variable | Type | Default | Description |
|
|
39
39
|
|----------|------|---------|-------------|
|
|
40
40
|
| `CASTRUM_TASK_THREADS` | int | `max(1, cores-1)` | Worker threads for the off-thread task runtime (castrum Tasks). Read when the pool first starts — the pool is process-wide and cannot be resized afterwards (`tasks.stats().threads` reports the live count). |
|
|
41
|
+
| `CASTRUM_TASK_QUEUE_MAX` | int | `4096` | Admission bound on the off-thread task work queue. Read when the pool first starts (minimum 1). A submit that would exceed it is rejected rather than growing memory: the native submit returns `2` and the task promise rejects with a typed `OVERLOADED` error. |
|
|
41
42
|
| `CASTRUM_TASK_PIN_CORES` | (presence) | — | When set (Linux), pin each task worker to its own core (core 0 is skipped so the JS event loop keeps it). `CASTRUM_PIN_CORES` enables it too. Off by default: pinning competes with the JS/rayon threads for the same box. |
|
|
42
43
|
|
|
44
|
+
## Compiled-schema cache (`rust/ingress/schema_cache.rs`)
|
|
45
|
+
|
|
46
|
+
| Variable | Type | Default | Description |
|
|
47
|
+
|----------|------|---------|-------------|
|
|
48
|
+
| `CASTRUM_SCHEMA_CACHE_MAX` | int | `128` | Max distinct request-body schemas retained process-wide. `Ingress`/`NativeRoute` share one compiled `IngressSchema` per distinct schema byte string; this bounds that LRU. Read at first schema compile (the cache is process-wide and cannot be resized afterwards). Values below 1 (unset, unparseable, `0`) fall back to the default; values are clamped to `4096`. Call `flushMemory()` to drop the cache explicitly. |
|
|
49
|
+
|
|
43
50
|
## Native transport selection (`src/native/ffi.ts`)
|
|
44
51
|
|
|
45
52
|
`bun:ffi` is the PRIMARY transport under Bun; NAPI is the fallback (Node,
|
package/docs/RND-CONCURRENCY.md
CHANGED
|
@@ -69,7 +69,8 @@ documented `JSTypedArrayBytesDeallocator` hook crashes the process (fault at
|
|
|
69
69
|
|
|
70
70
|
```
|
|
71
71
|
rust/task/runtime.rs condvar pool, N = cores−1 (CASTRUM_TASK_THREADS), auto-start on submit,
|
|
72
|
-
batch dequeue + bounded spin-then-park, optional core pinning
|
|
72
|
+
batch dequeue + bounded spin-then-park, optional core pinning,
|
|
73
|
+
HARD admission bound on the queue (CASTRUM_TASK_QUEUE_MAX)
|
|
73
74
|
rust/task/completion.rs result ring + ARMED coalescing doorbell + needed-size drain layout
|
|
74
75
|
rust/task/ops.rs op dispatch (header + payload split), cancellation set, catch_unwind
|
|
75
76
|
containment, `_into` (zero-copy output) and slice (zero-copy input) forms
|
|
@@ -121,6 +122,17 @@ right one is picked per op:
|
|
|
121
122
|
copied and the payload is read in place, so a large argument never crosses the
|
|
122
123
|
boundary. This one was forced by a measurement, not symmetry (§7b).
|
|
123
124
|
|
|
125
|
+
**Bounded admission (backpressure).** The work queue is HARD-bounded by
|
|
126
|
+
`CASTRUM_TASK_QUEUE_MAX` (default 4096, minimum 1; read once when the pool is
|
|
127
|
+
created). A submit that would exceed the bound is rejected instead of growing
|
|
128
|
+
memory: `castrum_task_submit{,_out,_slice}` return `2 = overloaded`, and the JS
|
|
129
|
+
runtime rejects the task promise with a typed `code: 'OVERLOADED'` error. The
|
|
130
|
+
existing return meanings are unchanged — `1` accepted, `0` invalid args / panic.
|
|
131
|
+
A rejected zero-copy submit drops its `output`/`keep` reference exactly like any
|
|
132
|
+
other rejection, so a full queue cannot leak buffers. (The richer M2 design — a
|
|
133
|
+
`WouldBlock` + re-queue-behind-drain policy and priority levels — remains a
|
|
134
|
+
later milestone; v1 is a hard cap with a distinct overload outcome.)
|
|
135
|
+
|
|
124
136
|
**Measured comparison** (`bun run bench:task`, Bun 1.4.2, 12 cores):
|
|
125
137
|
|
|
126
138
|
| variant | time | loop stall | note |
|
|
@@ -466,9 +478,12 @@ rust/task/
|
|
|
466
478
|
as in rayon/Go. Deliberately **separate from the rayon global pool**: rayon
|
|
467
479
|
stays the batch executor for `par_iter`; the task runtime owns offloaded ops.
|
|
468
480
|
`CASTRUM_TASK_THREADS` sizes it (default `cores − 1`).
|
|
469
|
-
- **Admission/backpressure
|
|
470
|
-
|
|
471
|
-
|
|
481
|
+
- **Admission/backpressure** (v1 shipped): the queue is hard-bounded by
|
|
482
|
+
`CASTRUM_TASK_QUEUE_MAX` (default 4096, min 1, read once at pool creation).
|
|
483
|
+
`submit` returns `false` when full; the C ABI reports `2 = overloaded` and JS
|
|
484
|
+
rejects that promise with a typed `OVERLOADED` error — no unbounded growth
|
|
485
|
+
(fixes F6). M2 replaces this hard cap with `WouldBlock` + requeue-behind-drain
|
|
486
|
+
and priority levels; v1 has neither.
|
|
472
487
|
- **Cancellation**: each task carries an `Arc<CancelToken>` (atomic + reason).
|
|
473
488
|
Ops check it at safepoints (between chunks: gzip blocks, schema subtrees,
|
|
474
489
|
Argon2 is atomic and reports "not cancellable"). Scope cancellation cascades
|
|
@@ -618,6 +633,7 @@ Each of these removes syscalls, lock traffic, or reallocations from the hot path
|
|
|
618
633
|
| **Zero-copy INPUT** (`castrum_task_submit_slice` + the header/payload split in `ops.rs`) | Forced by a measurement: packing the payload into the args blob copied the whole input on the JS thread, making offloaded `gzipCompress` of 24 MiB **18.6 ms / 11.7 ms stall against 5.6 ms synchronous** — a net loss on both axes. Reading the payload in place: **7.3 ms / 2.1 ms**. The split also simplified every op arm (no more `args[4..]` offset arithmetic). |
|
|
619
634
|
| **No BigInt, no per-completion DataView** | The drain loop reads ids and lengths as two `u32` reads at an offset — `getBigUint64` boxes a BigInt and `readU64` allocated a DataView *per completion*. Exact for any value under 2^53 (ids are a JS counter, sizes are byte counts). A zero-copy completion's body is never materialized at all. |
|
|
620
635
|
| **Reusable args scratch** (copy path only) | Rust copies args on submit, so one growable buffer serves every synchronous submission. Deliberately NOT used for secret-bearing ops (pbkdf2/argon2) — a shared buffer would keep plaintext passwords alive far longer than a per-call allocation — nor for zero-copy retries, which re-submit after an `await` and must own their memory. |
|
|
636
|
+
| **Bounded admission** (`CASTRUM_TASK_QUEUE_MAX`, default 4096) | A hard cap turns sustained overload into an explicit rejection (`2 = overloaded` → typed `OVERLOADED`) instead of unbounded queue/`inflight` growth. The bound is a length check inside the same lock that pushes, so it is one comparison on a path that already locks. |
|
|
621
637
|
|
|
622
638
|
---
|
|
623
639
|
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|---|---|---|---|---|
|
|
32
32
|
| `rust.fnv1a64` | `Bun.hash` (wyhash) | 42 B | Bun (parity) | 1.06× |
|
|
33
33
|
| `rust.crc32` | `Bun.hash.crc32` | 42 B | **Bun** | 2.8–8.4× |
|
|
34
|
-
| `rust.xxh3` (new) | `Bun.hash.xxHash3` | 42 B | **Bun** |
|
|
34
|
+
| `rust.xxh3` (new) | `Bun.hash.xxHash3` | 42 B–1 MB | **Bun** (≥1 KB) | 0.97× (≤64 B); ~2.0× (≥1 KB) |
|
|
35
35
|
| `rust.hmacSha256` | `Bun.CryptoHasher("sha256", key)` | 32 B | Bun (mild) | 1.1–1.4× |
|
|
36
36
|
| `rust.passwordHash` (argon2id) | `Bun.password.hashSync` (argon2id) | 12 B | **rust** | 0.55× (rust 1.83×) |
|
|
37
37
|
| `rust.passwordVerify` | `Bun.password.verifySync` | 12 B | **rust** | 0.53× (rust 1.88×) |
|
|
@@ -62,6 +62,14 @@
|
|
|
62
62
|
These rust ops lose to Bun's native implementation on the SAME workload — the
|
|
63
63
|
FFI crossing cannot beat Bun's in-process C++:
|
|
64
64
|
|
|
65
|
+
> Re-swept 2026-09-19 (64 B → 1 MB, interleaved medians + per-call input
|
|
66
|
+
> rotation): the hex and url rows in this table no longer lose — rust hex
|
|
67
|
+
> encode/decode is now 4.7–18.9× / 1.4–6.7× over Buffer, the url codec
|
|
68
|
+
> ~2–3× over `encodeURIComponent`, and base64 decode wins 1.4–6.0×. Only the
|
|
69
|
+
> rows still marked "Prefer Bun" below remain genuinely faster in Bun. See
|
|
70
|
+
> the rows themselves and `src/runtime/builtins.ts` for what the adapter
|
|
71
|
+
> currently delegates.
|
|
72
|
+
|
|
65
73
|
This table mirrors `BUILTIN_OPS` in `src/runtime/builtins.ts` (the 11 ops that
|
|
66
74
|
delegate to Bun built-ins under Bun; `src/selection.ts` derives from it). `fnv1a64` is parity/either but is NOT delegated
|
|
67
75
|
(`Bun.hash` is wyhash — a different algorithm — and rust `fnv1a64` stays the
|
|
@@ -70,13 +78,13 @@ benchmarked impl).
|
|
|
70
78
|
| Op | Recommendation |
|
|
71
79
|
|---|---|
|
|
72
80
|
| `crc32` | Prefer `Bun.hash.crc32`; keep rust as fallback (pure-TS path already exists). |
|
|
73
|
-
| `xxh3` | Prefer `Bun.hash.xxHash3` (~
|
|
81
|
+
| `xxh3` | Prefer `Bun.hash.xxHash3` (~2× at ≥1 KB via AVX2; parity ≤64 B). The rust core is FLAT ~30 GB/s at every size (re-swept 2026-09-19 — the gap is Bun's AVX2 width, not a size pathology); keep it as the Node/non-Bun fast path. The adapter delegates the public `rust.xxh3` to Bun under Bun. |
|
|
74
82
|
| `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. |
|
|
75
83
|
| `randomToken` | Prefer `Bun.randomUUIDv7()` (or `crypto.getRandomValues`) for token-sized output; keep rust for byte-precise control. |
|
|
76
84
|
| `hmacSha256` | Prefer `Bun.CryptoHasher("sha256", key)` (1.17× — mild; keep rust batch path, which wins on larger inputs). |
|
|
77
|
-
| `urlEncode` / `urlDecode` |
|
|
78
|
-
| `base64Encode` / `base64UrlEncode` |
|
|
79
|
-
| `hexEncode` | Prefer
|
|
85
|
+
| `urlEncode` / `urlDecode` | Fan-out. Re-swept 2026-09-19 on escape-heavy input the rust LUT+run-copy codec is ~2.8–3.2× (encode) / ~2× (decode) FASTER than `encodeURIComponent` / `decodeURIComponent` — the earlier ~11.5× reading was input/profile-dependent. The current adapter still delegates via `BUILTIN_OPS`. |
|
|
86
|
+
| `base64Encode` / `base64UrlEncode` | rust wins ≤1 KB (encode 1.8–2.3×); parity ≥16 KiB (0.88–1.07× — both AVX2 and memory-bound). Base64 **decode** is the strong rust play (1.4–6.0×). Fine to prefer rust for both; the adapter currently delegates encode to Buffer per `BUILTIN_OPS`. |
|
|
87
|
+
| `hexEncode` / `hexDecode` | Prefer rust: encode 4.7–18.9×, decode 1.4–6.7× over Buffer (faster-hex SIMD + single-pass AVX2 decode kernel; re-swept 2026-09-19 — previously Buffer won). The adapter still delegates to Buffer per `BUILTIN_OPS`. |
|
|
80
88
|
| `httpDate` | Prefer `Date.toUTCString()`. |
|
|
81
89
|
|
|
82
90
|
**Delegation rule of thumb**: rust stays the default only where it beats its
|
|
@@ -96,7 +104,7 @@ genuinely wins or Bun has no equivalent.
|
|
|
96
104
|
### → Implemented primitives (gaps closed)
|
|
97
105
|
| Primitive | Outcome |
|
|
98
106
|
|---|---|
|
|
99
|
-
| `rust.xxh3` (XXH3-64) | Exposed;
|
|
107
|
+
| `rust.xxh3` (XXH3-64) | Exposed; re-swept 2026-09-19: parity ≤64 B, Bun ~2× at ≥1 KB (its AVX2 width; rust is flat 30 GB/s) → the adapter still delegates under Bun (Node keeps the addon path). |
|
|
100
108
|
| `rust.passwordHashBcrypt` / `rust.passwordVerifyBcrypt` | bcrypt `$2b$` PHC; hash parity, verify rust 1.49×. |
|
|
101
109
|
| `rust.pbkdf2Sha256` | PBKDF2-HMAC-SHA256; parity with `node:crypto`, the only sync option in Bun. |
|
|
102
110
|
| UUIDv7 (`uuidv7()`) | `Bun.randomUUIDv7` already wins — delegated to Bun (`crypto.randomUUID` on Node), not built in Rust. |
|
|
@@ -108,7 +116,9 @@ random tokens, and gzip, Bun 1.4's built-ins are already faster than any
|
|
|
108
116
|
FFI-crossing Rust op — the selection layer should prefer them. Rust retains a clear,
|
|
109
117
|
defensible moat exactly where it matters: **argon2id password hashing
|
|
110
118
|
(≈1.9× vs Bun)**, the **zero-DOM / zero-copy parsers and JSON paths** (no Bun
|
|
111
|
-
sync equivalent),
|
|
119
|
+
sync equivalent), **brotli** (no Bun sync API), and — since the 2026-09-19
|
|
120
|
+
re-sweep — the **hex and URL byte codecs and base64 decode** (1.4–18.9× over
|
|
121
|
+
the corresponding Buffer / URI built-ins at every size).
|
|
112
122
|
|
|
113
123
|
## Re-run
|
|
114
124
|
|