castrum 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -59
  3. package/castrum.linux-x64-gnu.node +0 -0
  4. package/dist/index.d.ts +3161 -0
  5. package/dist/index.js +7306 -0
  6. package/docs/ARCHITECTURE.md +415 -0
  7. package/docs/BENCHMARKS.md +307 -0
  8. package/docs/ENVIRONMENT.md +79 -0
  9. package/docs/GETTING_STARTED.md +209 -0
  10. package/docs/INGRESS.md +745 -0
  11. package/docs/REPO_MAP.md +259 -0
  12. package/docs/adr/0001-two-wire-formats.md +39 -0
  13. package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
  14. package/docs/adr/0003-bun-builtins-delegation.md +42 -0
  15. package/docs/bun-builtins-decision-matrix.md +108 -0
  16. package/index.ts +51 -8
  17. package/package.json +73 -20
  18. package/src/data/json-rows.ts +13 -13
  19. package/src/ingress/body.ts +149 -0
  20. package/src/ingress/constants.ts +220 -48
  21. package/src/ingress/context.ts +136 -0
  22. package/src/ingress/decode/baked-result.ts +121 -0
  23. package/src/ingress/decode/fast-result.ts +160 -0
  24. package/src/ingress/decode/packed-sections.ts +56 -0
  25. package/src/ingress/decode/result-base.ts +172 -0
  26. package/src/ingress/errors.ts +72 -0
  27. package/src/ingress/fast.ts +152 -1021
  28. package/src/ingress/handlers.ts +563 -1001
  29. package/src/ingress/headers/baked-templates.ts +102 -0
  30. package/src/ingress/headers/cors.ts +44 -0
  31. package/src/ingress/headers/fast-templates.ts +238 -0
  32. package/src/ingress/headers/hsts.ts +83 -0
  33. package/src/ingress/health.ts +67 -0
  34. package/src/ingress/index.ts +130 -405
  35. package/src/ingress/metrics.ts +132 -0
  36. package/src/ingress/options.ts +197 -0
  37. package/src/ingress/packing/gather-raw-headers.ts +75 -0
  38. package/src/ingress/packing/header-packing.ts +75 -0
  39. package/src/ingress/packing/input-packer.ts +150 -0
  40. package/src/ingress/packing/scratch.ts +50 -0
  41. package/src/ingress/packing/select-headers.ts +96 -0
  42. package/src/ingress/response/error-bodies.ts +75 -0
  43. package/src/ingress/response/terminal.ts +59 -0
  44. package/src/ingress/routes/common.ts +38 -0
  45. package/src/ingress/routes/delete.ts +14 -0
  46. package/src/ingress/routes/echo.ts +107 -0
  47. package/src/ingress/routes/fallback.ts +23 -0
  48. package/src/ingress/routes/head.ts +37 -0
  49. package/src/ingress/routes/index.ts +10 -0
  50. package/src/ingress/routes/json-write.ts +153 -0
  51. package/src/ingress/routes/options.ts +36 -0
  52. package/src/ingress/routes/read.ts +45 -0
  53. package/src/ingress/server-node.ts +376 -0
  54. package/src/ingress/server.ts +443 -0
  55. package/src/ingress/shared.ts +132 -0
  56. package/src/ingress/status.ts +97 -0
  57. package/src/ingress/types.ts +157 -0
  58. package/src/integration/batch.ts +58 -0
  59. package/src/integration/index.ts +29 -0
  60. package/src/integration/pipeline.ts +217 -0
  61. package/src/integration/streaming.ts +79 -0
  62. package/src/integration/websocket.ts +92 -0
  63. package/src/loader/batch.ts +133 -0
  64. package/src/loader/cost.ts +104 -0
  65. package/src/loader/index.ts +930 -0
  66. package/src/loader/ops.ts +657 -0
  67. package/src/native/ffi.ts +1541 -0
  68. package/src/native/index.ts +8 -207
  69. package/src/native/loader.ts +194 -0
  70. package/src/native/types.ts +598 -0
  71. package/src/rust-ffi/addon.ts +10 -0
  72. package/src/rust-ffi/batch.ts +431 -0
  73. package/src/rust-ffi/client.ts +88 -0
  74. package/src/rust-ffi/context.ts +164 -0
  75. package/src/rust-ffi/index.ts +40 -541
  76. package/src/rust-ffi/options.ts +113 -0
  77. package/src/rust-ffi/packed.ts +253 -0
  78. package/src/rust-ffi/proven.ts +42 -0
  79. package/src/rust-ffi/scalar/crypto.ts +265 -0
  80. package/src/rust-ffi/scalar/factories.ts +233 -0
  81. package/src/rust-ffi/scalar/hashing.ts +72 -0
  82. package/src/rust-ffi/scalar/http.ts +206 -0
  83. package/src/rust-ffi/scalar/index.ts +31 -0
  84. package/src/rust-ffi/scalar/interface.ts +450 -0
  85. package/src/rust-ffi/scalar/json.ts +45 -0
  86. package/src/rust-ffi/scalar/payload.ts +124 -0
  87. package/src/rust-ffi/text.ts +95 -0
  88. package/src/selection.json +194 -0
  89. package/src/selection.ts +77 -0
  90. package/src/shared/adaptive.ts +68 -0
  91. package/src/shared/bench-classify.ts +209 -0
  92. package/src/shared/buffer-pool.ts +209 -0
  93. package/src/shared/bytes.ts +35 -5
  94. package/src/shared/env.ts +20 -0
  95. package/src/shared/log.ts +74 -0
  96. package/src/shared/metrics.ts +247 -0
  97. package/src/shared/packed.ts +350 -154
  98. package/src/shared/proven.ts +596 -0
  99. package/src/shared/request-id.ts +67 -0
  100. package/src/shared/response.ts +104 -0
  101. package/src/shared/runtime.ts +34 -0
  102. package/src/shared/trace.ts +81 -0
  103. package/src/shared/uuid.ts +31 -0
  104. package/Cargo.toml +0 -73
  105. package/bench.ts +0 -3
  106. package/rust/batch.rs +0 -195
  107. package/rust/cookie_parser.rs +0 -70
  108. package/rust/cors.rs +0 -263
  109. package/rust/hashing.rs +0 -164
  110. package/rust/headers.rs +0 -234
  111. package/rust/hmac_sha256.rs +0 -113
  112. package/rust/http_parser.rs +0 -132
  113. package/rust/ingress.rs +0 -616
  114. package/rust/ingress_constants.rs +0 -89
  115. package/rust/ip_trust.rs +0 -185
  116. package/rust/json_ops.rs +0 -66
  117. package/rust/json_patch_ops.rs +0 -25
  118. package/rust/json_schema.rs +0 -151
  119. package/rust/json_ser.rs +0 -429
  120. package/rust/lib.rs +0 -48
  121. package/rust/method.rs +0 -141
  122. package/rust/mime_lookup.rs +0 -76
  123. package/rust/output.rs +0 -238
  124. package/rust/proxy.rs +0 -90
  125. package/rust/query_parser.rs +0 -141
  126. package/rust/random_token.rs +0 -52
  127. package/rust/rate_limit.rs +0 -203
  128. package/rust/terminal.rs +0 -95
  129. package/rust/unit_tests.rs +0 -843
  130. package/rust/url_codec.rs +0 -133
  131. package/rust/util.rs +0 -537
  132. package/rust/validation.rs +0 -107
  133. package/rust/websocket.rs +0 -23
  134. package/src/baseline/index.ts +0 -12
  135. package/src/baseline/tasks/cookie.ts +0 -22
  136. package/src/baseline/tasks/hashing.ts +0 -18
  137. package/src/baseline/tasks/hmac.ts +0 -38
  138. package/src/baseline/tasks/http.ts +0 -62
  139. package/src/baseline/tasks/json-patch.ts +0 -12
  140. package/src/baseline/tasks/json.ts +0 -50
  141. package/src/baseline/tasks/mime.ts +0 -5
  142. package/src/baseline/tasks/query.ts +0 -14
  143. package/src/baseline/tasks/token.ts +0 -15
  144. package/src/baseline/tasks/url.ts +0 -11
  145. package/src/baseline/tasks/validation.ts +0 -19
  146. package/src/baseline/tasks/websocket.ts +0 -14
  147. package/src/bench/assert.ts +0 -70
  148. package/src/bench/checks.ts +0 -227
  149. package/src/bench/checksum.ts +0 -27
  150. package/src/bench/comparisons.ts +0 -70
  151. package/src/bench/concurrent-worker.ts +0 -135
  152. package/src/bench/fixtures.ts +0 -233
  153. package/src/bench/index.ts +0 -2
  154. package/src/bench/measure.ts +0 -177
  155. package/src/bench/now.ts +0 -3
  156. package/src/bench/report.ts +0 -79
  157. package/src/bench/run.ts +0 -57
  158. package/src/bench/tasks/complex.ts +0 -232
  159. package/src/bench/tasks/concurrent.ts +0 -213
  160. package/src/bench/tasks/cookie.ts +0 -44
  161. package/src/bench/tasks/hashing.ts +0 -33
  162. package/src/bench/tasks/hmac.ts +0 -36
  163. package/src/bench/tasks/http.ts +0 -50
  164. package/src/bench/tasks/index.ts +0 -52
  165. package/src/bench/tasks/json-patch.ts +0 -21
  166. package/src/bench/tasks/json.ts +0 -33
  167. package/src/bench/tasks/mime.ts +0 -21
  168. package/src/bench/tasks/query.ts +0 -44
  169. package/src/bench/tasks/stress.ts +0 -142
  170. package/src/bench/tasks/token.ts +0 -20
  171. package/src/bench/tasks/url.ts +0 -34
  172. package/src/bench/tasks/validation.ts +0 -57
  173. package/src/bench/tasks/websocket.ts +0 -21
  174. package/src/bench/types.ts +0 -60
  175. package/src/ingress/packed-input.ts +0 -59
  176. package/src/shared/json.ts +0 -18
@@ -1,10 +1,19 @@
1
1
  // src/rust-ffi/index.ts — Flat, complete public API for the Rust utilities.
2
2
  //
3
- // This is the SINGLE implementation module for all Rust FFI functions. It wraps
4
- // the raw NAPI addon directly (no intermediate wrapper layer), exposes every
5
- // native function with normalized, easy-to-consume types, and selects optimized
6
- // defaults that can be overridden per-instance via createRust() or on the
7
- // shared `rust` instance via rust.configure().
3
+ // Barrel module. The implementation is decomposed into task-focused modules:
4
+ // - options.ts RustOptions + input-normalization helpers
5
+ // - addon.ts shared lazy addon proxy
6
+ // - context.ts per-instance state + shared namespace helpers
7
+ // - scalar/ scalar/feature methods (hashing, json, http, crypto, payload, factories)
8
+ // - text.ts string namespace
9
+ // - batch.ts array-of-bytes namespace
10
+ // - packed.ts raw packed-wire namespace
11
+ // - client.ts createRust() factory + default `rust` instance
12
+ //
13
+ // This barrel re-exports the exact public surface of the original single-file
14
+ // implementation so existing imports (`rust`, `createRust`, all types) keep
15
+ // working unchanged. The deprecated `rustBatch` alias was removed — use
16
+ // `rust.batch` instead.
8
17
  //
9
18
  // import { rust } from "castrum";
10
19
  //
@@ -16,540 +25,30 @@
16
25
  // rust.batch.jsonValid(docs); // Uint8Array bitset
17
26
  // rust.packed.jsonValidBatchPacked(p); // raw packed in/out
18
27
  // rust.configure({ rayonThreads: 8 }); // override defaults
19
- //
20
- // Back-compat alias (non-breaking):
21
- // rustBatch === rust.batch
22
-
23
- import addon, {
24
- type HmacSignerInstance,
25
- type SchemaValidatorInstance,
26
- } from "../native";
27
- import { decoder, encoder } from "../shared/bytes";
28
- import {
29
- packBatch,
30
- schemaValidateBatch,
31
- schemaValidateBatchCount,
32
- unpackBitset,
33
- unpackByteResults,
34
- unpackI64ArrayAsBigInt,
35
- unpackU32Array,
36
- type SchemaValidator,
37
- } from "../shared/packed";
38
-
39
- export type { HmacSignerInstance, SchemaValidatorInstance };
40
- export type { SchemaValidator };
41
-
42
- // ── Options & defaults ─────────────────────────────────────────
43
-
44
- export interface RustOptions {
45
- /**
46
- * Rayon thread pool size.
47
- *
48
- * The rayon pool is process-wide and initialized **once** (native
49
- * `OnceLock`) — the first initialization wins. Importing this module
50
- * initializes the pool with defaults, so this option only takes effect when
51
- * the pool has not yet been created. For reliable control set
52
- * `CASTRUM_RAYON_THREADS` / `RUST_RAYON_THREADS` before import.
53
- * Default: max(1, hardwareConcurrency - 1).
54
- */
55
- rayonThreads?: number;
56
- /** Cache MIME lookups keyed by normalized extension. Default: true. */
57
- mimeCache?: boolean;
58
- /** Reuse HMAC signers keyed by key-buffer identity. Default: true. */
59
- hmacCache?: boolean;
60
- }
61
-
62
- const FALLBACK_CORES = 4;
63
-
64
- function resolveRayonThreads(explicit?: number): number {
65
- const cores = navigator.hardwareConcurrency || FALLBACK_CORES;
66
-
67
- if (explicit !== undefined && Number.isFinite(explicit) && explicit > 0) {
68
- return Math.max(1, Math.min(cores, Math.floor(explicit)));
69
- }
70
-
71
- const envThreads = Number(
72
- process.env.CASTRUM_RAYON_THREADS ??
73
- process.env.RUST_BENCH_RAYON_THREADS ??
74
- process.env.RUST_RAYON_THREADS,
75
- );
76
-
77
- if (Number.isFinite(envThreads) && envThreads > 0) {
78
- return Math.max(1, Math.min(cores, Math.floor(envThreads)));
79
- }
80
-
81
- return Math.max(1, cores - 1);
82
- }
83
-
84
- function asBigInt(value: unknown): bigint {
85
- if (typeof value === "bigint") {
86
- return value;
87
- }
88
-
89
- if (typeof value === "number" && Number.isFinite(value)) {
90
- return BigInt(Math.trunc(value));
91
- }
92
-
93
- if (typeof value === "string" && value.trim().length > 0) {
94
- return BigInt(value);
95
- }
96
-
97
- throw new TypeError(
98
- `Expected bigint-compatible value, got ${typeof value}: ${String(value)}`,
99
- );
100
- }
101
-
102
- function asNumber(value: unknown): number {
103
- if (typeof value === "number") {
104
- return value;
105
- }
106
-
107
- if (typeof value === "bigint") {
108
- return Number(value);
109
- }
110
-
111
- if (typeof value === "string" && value.trim().length > 0) {
112
- return Number(value);
113
- }
114
-
115
- return 0;
116
- }
117
-
118
- function normalizeExt(ext: Uint8Array): string {
119
- let s: string;
120
-
121
- try {
122
- s = decoder.decode(ext);
123
- } catch {
124
- return "";
125
- }
126
-
127
- if (s.charCodeAt(0) === 46) {
128
- s = s.slice(1);
129
- }
130
-
131
- return s.toLowerCase();
132
- }
133
-
134
- // ── Namespace types ────────────────────────────────────────────
135
-
136
- export interface RustText {
137
- mimeFromExtension(ext: string): string;
138
- urlEncode(input: string): string;
139
- urlDecode(input: string): string;
140
- wsAcceptKey(key: string): string;
141
- validateEmail(input: string): boolean;
142
- validateUuid(input: string): boolean;
143
- validateIpv4(input: string): boolean;
144
- validateIpv6(input: string): boolean;
145
- }
146
-
147
- export interface RustBatch {
148
- jsonValid(items: Uint8Array[]): Uint8Array;
149
- crc32(items: Uint8Array[]): Uint32Array;
150
- validateEmail(items: Uint8Array[]): Uint8Array;
151
- validateUuid(items: Uint8Array[]): Uint8Array;
152
- validateIpv4(items: Uint8Array[]): Uint8Array;
153
- validateIpv6(items: Uint8Array[]): Uint8Array;
154
- jsonSumIds(items: Uint8Array[]): BigInt64Array;
155
- queryParse(items: Uint8Array[]): Uint8Array[];
156
- cookieParse(items: Uint8Array[]): Uint8Array[];
157
- httpParseRequest(items: Uint8Array[]): Uint8Array[];
158
- schemaValidate(validator: SchemaValidator, items: Uint8Array[]): Uint8Array;
159
- schemaValidateCount(validator: SchemaValidator, items: Uint8Array[]): number;
160
- }
161
-
162
- export interface RustPacked {
163
- // ── Sync batch (packed in → packed out) ──
164
- crc32BatchPacked(input: Uint8Array): Uint8Array;
165
- jsonValidBatchPacked(input: Uint8Array): Uint8Array;
166
- validateEmailBatchPacked(input: Uint8Array): Uint8Array;
167
- validateUuidBatchPacked(input: Uint8Array): Uint8Array;
168
- validateIpv4BatchPacked(input: Uint8Array): Uint8Array;
169
- validateIpv6BatchPacked(input: Uint8Array): Uint8Array;
170
- jsonSumBatchPacked(input: Uint8Array): Uint8Array;
171
- queryParseBatchPacked(input: Uint8Array): Uint8Array;
172
- cookieParseBatchPacked(input: Uint8Array): Uint8Array;
173
- httpParseRequestBatchPacked(input: Uint8Array): Uint8Array;
174
-
175
- // ── Batch metadata / counts ──
176
- jsonValidBatchCountPacked(input: Uint8Array): number;
177
- validateEmailBatchCountPacked(input: Uint8Array): number;
178
- validateUuidBatchCountPacked(input: Uint8Array): number;
179
- validateIpv4BatchCountPacked(input: Uint8Array): number;
180
- validateIpv6BatchCountPacked(input: Uint8Array): number;
181
- jsonSumBatchTotalPacked(input: Uint8Array): number;
182
- queryParseBatchTotalLenPacked(input: Uint8Array): number;
183
- cookieParseBatchTotalLenPacked(input: Uint8Array): number;
184
- httpParseRequestBatchTotalLenPacked(input: Uint8Array): number;
185
- }
186
-
187
- export interface RustClient {
188
- // ── Scalar utilities (bytes in → normalized out) ──
189
- crc32(input: Uint8Array): number;
190
- fnv1a64(input: Uint8Array): bigint;
191
- hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array;
192
- hmacSha256Verify(
193
- key: Uint8Array,
194
- data: Uint8Array,
195
- sig: Uint8Array,
196
- ): boolean;
197
- jsonValid(input: Uint8Array): boolean;
198
- jsonSumIds(input: Uint8Array): bigint;
199
- jsonPatch(doc: Uint8Array, patch: Uint8Array): Uint8Array;
200
- mimeFromExtension(ext: Uint8Array): Uint8Array;
201
- randomToken(byteLen: number): Uint8Array;
202
- urlEncode(input: Uint8Array): Uint8Array;
203
- urlDecode(input: Uint8Array): Uint8Array;
204
- /** Strict percent-decode without UTF-8 validation. */
205
- urlDecodeBytes(input: Uint8Array): Uint8Array;
206
- validateEmail(input: Uint8Array): boolean;
207
- validateUuid(input: Uint8Array): boolean;
208
- validateIpv4(input: Uint8Array): boolean;
209
- validateIpv6(input: Uint8Array): boolean;
210
- wsAcceptKey(key: Uint8Array): Uint8Array;
211
-
212
- // ── Low-level / packed scalar ──
213
- httpParseRequestPacked(input: Uint8Array): Uint8Array;
214
- queryParsePacked(input: Uint8Array): Uint8Array;
215
- cookieParsePacked(input: Uint8Array): Uint8Array;
216
-
217
- // ── Factories / runtime ──
218
- createSchemaValidator(schema: Uint8Array): SchemaValidatorInstance;
219
- createHmacSigner(key: Uint8Array): HmacSignerInstance;
220
- initThreadPool(rayonThreads?: number): void;
221
- rayonNumThreads(): number;
222
-
223
- // ── Namespaces ──
224
- text: RustText;
225
- batch: RustBatch;
226
- packed: RustPacked;
227
-
228
- // ── Configuration ──
229
- /**
230
- * Update defaults on this instance. `mimeCache` / `hmacCache` apply
231
- * immediately (per-instance). `rayonThreads` is applied only if the
232
- * process-wide pool is not yet initialized (see {@link RustOptions}).
233
- */
234
- configure(options: RustOptions): void;
235
- }
236
-
237
- // ── Implementation ─────────────────────────────────────────────
238
-
239
- /**
240
- * Create an isolated Rust client with the given defaults.
241
- *
242
- * `mimeCache` / `hmacCache` are per-instance. `rayonThreads` only applies
243
- * before the process-wide rayon pool is initialized (once per process — see
244
- * {@link RustOptions.rayonThreads}); the pool defaults to the env var or
245
- * `max(1, hardwareConcurrency - 1)` otherwise.
246
- */
247
- export function createRust(options: RustOptions = {}): RustClient {
248
- const mimeByText = new Map<string, Uint8Array>();
249
- const hmacSigners = new WeakMap<Uint8Array, HmacSignerInstance>();
250
- const state = {
251
- mimeCache: options.mimeCache !== false,
252
- hmacCache: options.hmacCache !== false,
253
- };
254
-
255
- // ── Lazy rayon pool initialization ─────────────────────────────
256
- // The native rayon pool is a process-wide OnceLock: the FIRST call wins.
257
- // We therefore defer init until the first batch/packed operation so that
258
- // rust.configure({ rayonThreads }) called at startup actually takes effect
259
- // (previously the pool was initialized eagerly at module load, silently
260
- // ignoring later rayonThreads tuning).
261
- let poolInitialized = false;
262
- let pendingThreads = options.rayonThreads;
263
-
264
- function ensurePool(): void {
265
- if (poolInitialized) return;
266
- poolInitialized = true;
267
- addon.initThreadPool(resolveRayonThreads(pendingThreads));
268
- }
269
-
270
- // Any access to a rayon-backed namespace triggers pool init exactly once.
271
- function withPoolInit<T extends object>(target: T): T {
272
- return new Proxy(target, {
273
- get(obj, prop, receiver) {
274
- ensurePool();
275
- return Reflect.get(obj, prop, receiver);
276
- },
277
- });
278
- }
279
-
280
- function cachedMime(ext: Uint8Array): Uint8Array {
281
- if (!state.mimeCache) {
282
- return addon.mimeFromExtension(ext);
283
- }
284
-
285
- const key = normalizeExt(ext);
286
-
287
- let val = mimeByText.get(key);
288
- if (!val) {
289
- val = addon.mimeFromExtension(ext);
290
- mimeByText.set(key, val);
291
- }
292
-
293
- // Return a copy so callers cannot mutate cached bytes.
294
- return val.slice();
295
- }
296
-
297
- function hmacSigner(key: Uint8Array): HmacSignerInstance {
298
- if (!state.hmacCache) {
299
- return new addon.HmacSigner(key);
300
- }
301
-
302
- let signer = hmacSigners.get(key);
303
-
304
- if (!signer) {
305
- signer = new addon.HmacSigner(key);
306
- hmacSigners.set(key, signer);
307
- }
308
-
309
- return signer;
310
- }
311
-
312
- const text: RustText = {
313
- mimeFromExtension(ext) {
314
- return decoder.decode(cachedMime(encoder.encode(ext)));
315
- },
316
- urlEncode(input) {
317
- return decoder.decode(addon.urlEncode(encoder.encode(input)));
318
- },
319
- urlDecode(input) {
320
- return decoder.decode(addon.urlDecode(encoder.encode(input)));
321
- },
322
- wsAcceptKey(key) {
323
- return decoder.decode(addon.wsAcceptKey(encoder.encode(key)));
324
- },
325
- validateEmail(input) {
326
- return addon.validateEmail(encoder.encode(input));
327
- },
328
- validateUuid(input) {
329
- return addon.validateUuid(encoder.encode(input));
330
- },
331
- validateIpv4(input) {
332
- return addon.validateIpv4(encoder.encode(input));
333
- },
334
- validateIpv6(input) {
335
- return addon.validateIpv6(encoder.encode(input));
336
- },
337
- };
338
-
339
- const batch: RustBatch = withPoolInit<RustBatch>({
340
- jsonValid(items) {
341
- return unpackBitset(addon.jsonValidBatchPacked(packBatch(items)));
342
- },
343
- crc32(items) {
344
- return unpackU32Array(addon.crc32BatchPacked(packBatch(items)));
345
- },
346
- validateEmail(items) {
347
- return unpackBitset(addon.validateEmailBatchPacked(packBatch(items)));
348
- },
349
- validateUuid(items) {
350
- return unpackBitset(addon.validateUuidBatchPacked(packBatch(items)));
351
- },
352
- validateIpv4(items) {
353
- return unpackBitset(addon.validateIpv4BatchPacked(packBatch(items)));
354
- },
355
- validateIpv6(items) {
356
- return unpackBitset(addon.validateIpv6BatchPacked(packBatch(items)));
357
- },
358
- jsonSumIds(items) {
359
- return unpackI64ArrayAsBigInt(addon.jsonSumBatchPacked(packBatch(items)));
360
- },
361
- queryParse(items) {
362
- return unpackByteResults(addon.queryParseBatchPacked(packBatch(items)));
363
- },
364
- cookieParse(items) {
365
- return unpackByteResults(addon.cookieParseBatchPacked(packBatch(items)));
366
- },
367
- httpParseRequest(items) {
368
- return unpackByteResults(
369
- addon.httpParseRequestBatchPacked(packBatch(items)),
370
- );
371
- },
372
- schemaValidate(validator, items) {
373
- return schemaValidateBatch(validator, items);
374
- },
375
- schemaValidateCount(validator, items) {
376
- return schemaValidateBatchCount(validator, items);
377
- },
378
- });
379
-
380
- const packed: RustPacked = withPoolInit<RustPacked>({
381
- crc32BatchPacked(input) {
382
- return addon.crc32BatchPacked(input);
383
- },
384
- jsonValidBatchPacked(input) {
385
- return addon.jsonValidBatchPacked(input);
386
- },
387
- validateEmailBatchPacked(input) {
388
- return addon.validateEmailBatchPacked(input);
389
- },
390
- validateUuidBatchPacked(input) {
391
- return addon.validateUuidBatchPacked(input);
392
- },
393
- validateIpv4BatchPacked(input) {
394
- return addon.validateIpv4BatchPacked(input);
395
- },
396
- validateIpv6BatchPacked(input) {
397
- return addon.validateIpv6BatchPacked(input);
398
- },
399
- jsonSumBatchPacked(input) {
400
- return addon.jsonSumBatchPacked(input);
401
- },
402
- queryParseBatchPacked(input) {
403
- return addon.queryParseBatchPacked(input);
404
- },
405
- cookieParseBatchPacked(input) {
406
- return addon.cookieParseBatchPacked(input);
407
- },
408
- httpParseRequestBatchPacked(input) {
409
- return addon.httpParseRequestBatchPacked(input);
410
- },
411
-
412
- jsonValidBatchCountPacked(input) {
413
- return asNumber(addon.jsonValidBatchCountPacked(input));
414
- },
415
- validateEmailBatchCountPacked(input) {
416
- return asNumber(addon.validateEmailBatchCountPacked(input));
417
- },
418
- validateUuidBatchCountPacked(input) {
419
- return asNumber(addon.validateUuidBatchCountPacked(input));
420
- },
421
- validateIpv4BatchCountPacked(input) {
422
- return asNumber(addon.validateIpv4BatchCountPacked(input));
423
- },
424
- validateIpv6BatchCountPacked(input) {
425
- return asNumber(addon.validateIpv6BatchCountPacked(input));
426
- },
427
- jsonSumBatchTotalPacked(input) {
428
- return asNumber(addon.jsonSumBatchTotalPacked(input));
429
- },
430
- queryParseBatchTotalLenPacked(input) {
431
- return asNumber(addon.queryParseBatchTotalLenPacked(input));
432
- },
433
- cookieParseBatchTotalLenPacked(input) {
434
- return asNumber(addon.cookieParseBatchTotalLenPacked(input));
435
- },
436
- httpParseRequestBatchTotalLenPacked(input) {
437
- return asNumber(addon.httpParseRequestBatchTotalLenPacked(input));
438
- },
439
- });
440
-
441
- const client: RustClient = {
442
- // ── Scalar ──
443
- crc32(bytes) {
444
- return asNumber(addon.crc32(bytes) as unknown) >>> 0;
445
- },
446
- fnv1a64(bytes) {
447
- return asBigInt(addon.fnv1a64(bytes) as unknown);
448
- },
449
- hmacSha256(key, data) {
450
- return hmacSigner(key).sign(data);
451
- },
452
- hmacSha256Verify(key, data, sig) {
453
- return hmacSigner(key).verify(data, sig);
454
- },
455
- jsonValid(bytes) {
456
- return addon.jsonValid(bytes);
457
- },
458
- jsonSumIds(bytes) {
459
- return asBigInt(addon.jsonSumIds(bytes) as unknown);
460
- },
461
- jsonPatch(doc, patch) {
462
- return addon.jsonPatch(doc, patch);
463
- },
464
- mimeFromExtension(ext) {
465
- return cachedMime(ext);
466
- },
467
- randomToken(byteLen) {
468
- return addon.randomToken(byteLen);
469
- },
470
- urlEncode(bytes) {
471
- return addon.urlEncode(bytes);
472
- },
473
- urlDecode(bytes) {
474
- return addon.urlDecode(bytes);
475
- },
476
- urlDecodeBytes(bytes) {
477
- return addon.urlDecodeBytes(bytes);
478
- },
479
- validateEmail(bytes) {
480
- return addon.validateEmail(bytes);
481
- },
482
- validateUuid(bytes) {
483
- return addon.validateUuid(bytes);
484
- },
485
- validateIpv4(bytes) {
486
- return addon.validateIpv4(bytes);
487
- },
488
- validateIpv6(bytes) {
489
- return addon.validateIpv6(bytes);
490
- },
491
- wsAcceptKey(key) {
492
- return addon.wsAcceptKey(key);
493
- },
494
-
495
- // ── Low-level / packed scalar ──
496
- httpParseRequestPacked(bytes) {
497
- return addon.httpParseRequestPacked(bytes);
498
- },
499
- queryParsePacked(bytes) {
500
- return addon.queryParsePacked(bytes);
501
- },
502
- cookieParsePacked(bytes) {
503
- return addon.cookieParsePacked(bytes);
504
- },
505
-
506
- // ── Factories / runtime ──
507
- createSchemaValidator(schema) {
508
- return new addon.SchemaValidator(schema);
509
- },
510
- createHmacSigner(key) {
511
- return new addon.HmacSigner(key);
512
- },
513
- initThreadPool(threads) {
514
- // Explicit user call also establishes the pool state locally.
515
- poolInitialized = true;
516
- if (threads !== undefined) pendingThreads = threads;
517
- return addon.initThreadPool(threads);
518
- },
519
- rayonNumThreads() {
520
- return asNumber(addon.rayonNumThreads() as unknown);
521
- },
522
-
523
- // ── Namespaces ──
524
- text,
525
- batch,
526
- packed,
527
-
528
- // ── Configuration ──
529
- configure(next) {
530
- // rayonThreads only takes effect BEFORE the pool is first used.
531
- if (next.rayonThreads !== undefined && !poolInitialized) {
532
- pendingThreads = next.rayonThreads;
533
- }
534
-
535
- if (next.mimeCache !== undefined) {
536
- state.mimeCache = next.mimeCache;
537
- }
538
-
539
- if (next.hmacCache !== undefined) {
540
- state.hmacCache = next.hmacCache;
541
- }
542
- },
543
- };
544
-
545
- return client;
546
- }
547
-
548
- // ── Default instance (optimized defaults) ─────────────────────
549
-
550
- export const rust = createRust();
551
-
552
- // ── Back-compat alias (non-breaking) ───────────────────────────
553
28
 
554
- /** @deprecated Use `rust.batch` (same object). */
555
- export const rustBatch = rust.batch;
29
+ export { createRust, rust } from './client'
30
+ export type { RustClient } from './client'
31
+ export type { RustOptions } from './options'
32
+ export type { RustText } from './text'
33
+ export type { RustBatch } from './batch'
34
+ export type { RustPacked } from './packed'
35
+ export type { SchemaValidator } from '../shared/packed'
36
+ export type {
37
+ HmacSignerInstance,
38
+ SchemaValidatorInstance,
39
+ SchemaError,
40
+ TemplateRendererInstance,
41
+ FormParserInstance,
42
+ MediaTypeParserInstance,
43
+ MediaTypeResult,
44
+ ConditionalRequestInstance,
45
+ EncodingPrefResult,
46
+ AcceptNegotiatorInstance,
47
+ Base64CodecInstance,
48
+ CookieSignerInstance,
49
+ CsrfProtectorInstance,
50
+ UrlBuilderInstance,
51
+ MultipartPart,
52
+ PasswordHashOptions,
53
+ WsFrame,
54
+ } from '../native'
@@ -0,0 +1,113 @@
1
+ // src/rust-ffi/options.ts — Client options + input-normalization helpers.
2
+ //
3
+ // Holds the `RustOptions` contract, the rayon thread-count resolution, and the
4
+ // small coercion helpers (bigint/number/ext normalization) used across the FFI
5
+ // namespaces.
6
+
7
+ import { availableParallelism, cpus } from 'node:os'
8
+ import { decoder } from '../shared/bytes'
9
+ import { resolveEnvVar } from '../shared/env'
10
+
11
+ /** Options accepted by `createRust` / `rust.configure`. */
12
+ export interface RustOptions {
13
+ /**
14
+ * Rayon thread pool size.
15
+ *
16
+ * The rayon pool is process-wide and initialized **once** (native
17
+ * `OnceLock`) — the first initialization wins. Importing this module
18
+ * initializes the pool with defaults, so this option only takes effect when
19
+ * the pool has not yet been created. For reliable control set
20
+ * `CASTRUM_RAYON_THREADS` / `RUST_RAYON_THREADS` before import.
21
+ * Default: max(1, hardwareConcurrency - 1).
22
+ */
23
+ rayonThreads?: number
24
+ /** Cache MIME lookups keyed by normalized extension. Default: true. */
25
+ mimeCache?: boolean
26
+ /** Reuse HMAC signers keyed by key-buffer identity. Default: true. */
27
+ hmacCache?: boolean
28
+ }
29
+
30
+ const FALLBACK_CORES = 4
31
+
32
+ /**
33
+ * Resolve the CPU count without relying on `navigator.hardwareConcurrency`,
34
+ * which throws a ReferenceError under Node.js < 21. `os.availableParallelism`
35
+ * exists in Bun and Node >= 18.14; older Node falls back to `os.cpus().length`.
36
+ */
37
+ function hardwareConcurrency(): number {
38
+ if (typeof availableParallelism === 'function') {
39
+ return availableParallelism()
40
+ }
41
+ const count = cpus()?.length
42
+ return count && count > 0 ? count : FALLBACK_CORES
43
+ }
44
+
45
+ /** Resolve the effective rayon thread count from explicit/env/default. */
46
+ export function resolveRayonThreads(explicit?: number): number {
47
+ const cores = hardwareConcurrency()
48
+
49
+ if (explicit !== undefined && Number.isFinite(explicit) && explicit > 0) {
50
+ return Math.max(1, Math.min(cores, Math.floor(explicit)))
51
+ }
52
+
53
+ const envThreads = Number(
54
+ resolveEnvVar('CASTRUM_RAYON_THREADS', ['RUST_BENCH_RAYON_THREADS', 'RUST_RAYON_THREADS']),
55
+ )
56
+
57
+ if (Number.isFinite(envThreads) && envThreads > 0) {
58
+ return Math.max(1, Math.min(cores, Math.floor(envThreads)))
59
+ }
60
+
61
+ return Math.max(1, cores - 1)
62
+ }
63
+
64
+ /** Coerce a value to bigint (accepts bigint/number/string). */
65
+ export function asBigInt(value: unknown): bigint {
66
+ if (typeof value === 'bigint') {
67
+ return value
68
+ }
69
+
70
+ if (typeof value === 'number' && Number.isFinite(value)) {
71
+ return BigInt(Math.trunc(value))
72
+ }
73
+
74
+ if (typeof value === 'string' && value.trim().length > 0) {
75
+ return BigInt(value)
76
+ }
77
+
78
+ throw new TypeError(`Expected bigint-compatible value, got ${typeof value}: ${String(value)}`)
79
+ }
80
+
81
+ /** Coerce a value to number (accepts number/bigint/string). */
82
+ export function asNumber(value: unknown): number {
83
+ if (typeof value === 'number') {
84
+ return value
85
+ }
86
+
87
+ if (typeof value === 'bigint') {
88
+ return Number(value)
89
+ }
90
+
91
+ if (typeof value === 'string' && value.trim().length > 0) {
92
+ return Number(value)
93
+ }
94
+
95
+ return 0
96
+ }
97
+
98
+ /** Normalize a file-extension byte slice to a lowercase string w/o a dot. */
99
+ export function normalizeExt(ext: Uint8Array): string {
100
+ let s: string
101
+
102
+ try {
103
+ s = decoder.decode(ext)
104
+ } catch {
105
+ return ''
106
+ }
107
+
108
+ if (s.charCodeAt(0) === 46) {
109
+ s = s.slice(1)
110
+ }
111
+
112
+ return s.toLowerCase()
113
+ }