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.
- package/LICENSE +21 -0
- package/README.md +322 -59
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +3161 -0
- package/dist/index.js +7306 -0
- package/docs/ARCHITECTURE.md +415 -0
- package/docs/BENCHMARKS.md +307 -0
- package/docs/ENVIRONMENT.md +79 -0
- package/docs/GETTING_STARTED.md +209 -0
- package/docs/INGRESS.md +745 -0
- package/docs/REPO_MAP.md +259 -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 +42 -0
- package/docs/bun-builtins-decision-matrix.md +108 -0
- package/index.ts +51 -8
- package/package.json +73 -20
- package/src/data/json-rows.ts +13 -13
- package/src/ingress/body.ts +149 -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 +563 -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 +67 -0
- package/src/ingress/index.ts +130 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +75 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +150 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +96 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/routes/common.ts +38 -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 +37 -0
- package/src/ingress/routes/index.ts +10 -0
- package/src/ingress/routes/json-write.ts +153 -0
- package/src/ingress/routes/options.ts +36 -0
- package/src/ingress/routes/read.ts +45 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +443 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +157 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +217 -0
- package/src/integration/streaming.ts +79 -0
- package/src/integration/websocket.ts +92 -0
- package/src/loader/batch.ts +133 -0
- package/src/loader/cost.ts +104 -0
- package/src/loader/index.ts +930 -0
- package/src/loader/ops.ts +657 -0
- package/src/native/ffi.ts +1541 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +194 -0
- package/src/native/types.ts +598 -0
- package/src/rust-ffi/addon.ts +10 -0
- package/src/rust-ffi/batch.ts +431 -0
- package/src/rust-ffi/client.ts +88 -0
- package/src/rust-ffi/context.ts +164 -0
- package/src/rust-ffi/index.ts +40 -541
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +42 -0
- package/src/rust-ffi/scalar/crypto.ts +265 -0
- package/src/rust-ffi/scalar/factories.ts +233 -0
- package/src/rust-ffi/scalar/hashing.ts +72 -0
- package/src/rust-ffi/scalar/http.ts +206 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +450 -0
- package/src/rust-ffi/scalar/json.ts +45 -0
- package/src/rust-ffi/scalar/payload.ts +124 -0
- package/src/rust-ffi/text.ts +95 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +77 -0
- package/src/shared/adaptive.ts +68 -0
- package/src/shared/bench-classify.ts +209 -0
- package/src/shared/buffer-pool.ts +209 -0
- package/src/shared/bytes.ts +35 -5
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +74 -0
- package/src/shared/metrics.ts +247 -0
- package/src/shared/packed.ts +350 -154
- package/src/shared/proven.ts +596 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +34 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +31 -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/ingress/packed-input.ts +0 -59
- package/src/shared/json.ts +0 -18
package/src/rust-ffi/index.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
// src/rust-ffi/index.ts — Flat, complete public API for the Rust utilities.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
-
|
|
555
|
-
export
|
|
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
|
+
}
|