castrum 0.7.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/Cargo.toml +73 -0
- package/README.md +432 -0
- package/bench.ts +3 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/index.ts +24 -0
- package/package.json +94 -0
- package/rust/batch.rs +195 -0
- package/rust/cookie_parser.rs +70 -0
- package/rust/cors.rs +263 -0
- package/rust/hashing.rs +164 -0
- package/rust/headers.rs +234 -0
- package/rust/hmac_sha256.rs +113 -0
- package/rust/http_parser.rs +132 -0
- package/rust/ingress.rs +616 -0
- package/rust/ingress_constants.rs +89 -0
- package/rust/ip_trust.rs +185 -0
- package/rust/json_ops.rs +66 -0
- package/rust/json_patch_ops.rs +25 -0
- package/rust/json_schema.rs +151 -0
- package/rust/json_ser.rs +429 -0
- package/rust/lib.rs +48 -0
- package/rust/method.rs +141 -0
- package/rust/mime_lookup.rs +76 -0
- package/rust/output.rs +238 -0
- package/rust/proxy.rs +90 -0
- package/rust/query_parser.rs +141 -0
- package/rust/random_token.rs +52 -0
- package/rust/rate_limit.rs +203 -0
- package/rust/terminal.rs +95 -0
- package/rust/unit_tests.rs +843 -0
- package/rust/url_codec.rs +133 -0
- package/rust/util.rs +537 -0
- package/rust/validation.rs +107 -0
- package/rust/websocket.rs +23 -0
- package/src/baseline/index.ts +12 -0
- package/src/baseline/tasks/cookie.ts +22 -0
- package/src/baseline/tasks/hashing.ts +18 -0
- package/src/baseline/tasks/hmac.ts +38 -0
- package/src/baseline/tasks/http.ts +62 -0
- package/src/baseline/tasks/json-patch.ts +12 -0
- package/src/baseline/tasks/json.ts +50 -0
- package/src/baseline/tasks/mime.ts +5 -0
- package/src/baseline/tasks/query.ts +14 -0
- package/src/baseline/tasks/token.ts +15 -0
- package/src/baseline/tasks/url.ts +11 -0
- package/src/baseline/tasks/validation.ts +19 -0
- package/src/baseline/tasks/websocket.ts +14 -0
- package/src/bench/assert.ts +70 -0
- package/src/bench/checks.ts +227 -0
- package/src/bench/checksum.ts +27 -0
- package/src/bench/comparisons.ts +70 -0
- package/src/bench/concurrent-worker.ts +135 -0
- package/src/bench/fixtures.ts +233 -0
- package/src/bench/index.ts +2 -0
- package/src/bench/measure.ts +177 -0
- package/src/bench/now.ts +3 -0
- package/src/bench/report.ts +79 -0
- package/src/bench/run.ts +57 -0
- package/src/bench/tasks/complex.ts +232 -0
- package/src/bench/tasks/concurrent.ts +213 -0
- package/src/bench/tasks/cookie.ts +44 -0
- package/src/bench/tasks/hashing.ts +33 -0
- package/src/bench/tasks/hmac.ts +36 -0
- package/src/bench/tasks/http.ts +50 -0
- package/src/bench/tasks/index.ts +52 -0
- package/src/bench/tasks/json-patch.ts +21 -0
- package/src/bench/tasks/json.ts +33 -0
- package/src/bench/tasks/mime.ts +21 -0
- package/src/bench/tasks/query.ts +44 -0
- package/src/bench/tasks/stress.ts +142 -0
- package/src/bench/tasks/token.ts +20 -0
- package/src/bench/tasks/url.ts +34 -0
- package/src/bench/tasks/validation.ts +57 -0
- package/src/bench/tasks/websocket.ts +21 -0
- package/src/bench/types.ts +60 -0
- package/src/data/json-rows.ts +33 -0
- package/src/ingress/constants.ts +56 -0
- package/src/ingress/fast.ts +1082 -0
- package/src/ingress/handlers.ts +1202 -0
- package/src/ingress/index.ts +457 -0
- package/src/ingress/packed-input.ts +59 -0
- package/src/native/index.ts +207 -0
- package/src/rust-ffi/index.ts +555 -0
- package/src/shared/bytes.ts +36 -0
- package/src/shared/json.ts +18 -0
- package/src/shared/packed.ts +316 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
// src/rust-ffi/index.ts — Flat, complete public API for the Rust utilities.
|
|
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().
|
|
8
|
+
//
|
|
9
|
+
// import { rust } from "castrum";
|
|
10
|
+
//
|
|
11
|
+
// rust.crc32(bytes); // number
|
|
12
|
+
// rust.fnv1a64(bytes); // bigint
|
|
13
|
+
// rust.jsonValid(bytes); // boolean
|
|
14
|
+
// rust.text.mimeFromExtension(".js"); // "text/javascript"
|
|
15
|
+
// rust.text.urlEncode("a b"); // "a%20b"
|
|
16
|
+
// rust.batch.jsonValid(docs); // Uint8Array bitset
|
|
17
|
+
// rust.packed.jsonValidBatchPacked(p); // raw packed in/out
|
|
18
|
+
// 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
|
+
|
|
554
|
+
/** @deprecated Use `rust.batch` (same object). */
|
|
555
|
+
export const rustBatch = rust.batch;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared TextEncoder instance for encoding strings to UTF-8 bytes.
|
|
3
|
+
*
|
|
4
|
+
* Reusing a single encoder avoids allocation overhead in hot paths.
|
|
5
|
+
*/
|
|
6
|
+
export const encoder = new TextEncoder();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shared TextDecoder instance for decoding UTF-8 bytes to strings.
|
|
10
|
+
*
|
|
11
|
+
* Reusing a single decoder avoids allocation overhead in hot paths.
|
|
12
|
+
*/
|
|
13
|
+
export const decoder = new TextDecoder();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates an independent copy of a Uint8Array.
|
|
17
|
+
*
|
|
18
|
+
* This ensures the returned buffer has its own backing store, decoupled
|
|
19
|
+
* from the original. Useful when the original buffer is a view (subarray)
|
|
20
|
+
* that may be reused or mutated.
|
|
21
|
+
*
|
|
22
|
+
* @param bytes - The source buffer to copy
|
|
23
|
+
* @returns A new Uint8Array with an independent backing store
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const original = new Uint8Array([1, 2, 3]);
|
|
28
|
+
* const copy = toPlainBuffer(original);
|
|
29
|
+
* copy[0] = 99; // does not affect original
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function toPlainBuffer(bytes: Uint8Array): Uint8Array {
|
|
33
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
34
|
+
copy.set(bytes);
|
|
35
|
+
return copy;
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function sortKeys(value: unknown): unknown {
|
|
2
|
+
if (value === null || typeof value !== "object") {
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value.map(sortKeys);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const record = value as Record<string, unknown>;
|
|
11
|
+
const sorted: Record<string, unknown> = {};
|
|
12
|
+
|
|
13
|
+
for (const key of Object.keys(record).sort()) {
|
|
14
|
+
sorted[key] = sortKeys(record[key]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return sorted;
|
|
18
|
+
}
|