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.
Files changed (86) hide show
  1. package/Cargo.toml +73 -0
  2. package/README.md +432 -0
  3. package/bench.ts +3 -0
  4. package/castrum.linux-x64-gnu.node +0 -0
  5. package/index.ts +24 -0
  6. package/package.json +94 -0
  7. package/rust/batch.rs +195 -0
  8. package/rust/cookie_parser.rs +70 -0
  9. package/rust/cors.rs +263 -0
  10. package/rust/hashing.rs +164 -0
  11. package/rust/headers.rs +234 -0
  12. package/rust/hmac_sha256.rs +113 -0
  13. package/rust/http_parser.rs +132 -0
  14. package/rust/ingress.rs +616 -0
  15. package/rust/ingress_constants.rs +89 -0
  16. package/rust/ip_trust.rs +185 -0
  17. package/rust/json_ops.rs +66 -0
  18. package/rust/json_patch_ops.rs +25 -0
  19. package/rust/json_schema.rs +151 -0
  20. package/rust/json_ser.rs +429 -0
  21. package/rust/lib.rs +48 -0
  22. package/rust/method.rs +141 -0
  23. package/rust/mime_lookup.rs +76 -0
  24. package/rust/output.rs +238 -0
  25. package/rust/proxy.rs +90 -0
  26. package/rust/query_parser.rs +141 -0
  27. package/rust/random_token.rs +52 -0
  28. package/rust/rate_limit.rs +203 -0
  29. package/rust/terminal.rs +95 -0
  30. package/rust/unit_tests.rs +843 -0
  31. package/rust/url_codec.rs +133 -0
  32. package/rust/util.rs +537 -0
  33. package/rust/validation.rs +107 -0
  34. package/rust/websocket.rs +23 -0
  35. package/src/baseline/index.ts +12 -0
  36. package/src/baseline/tasks/cookie.ts +22 -0
  37. package/src/baseline/tasks/hashing.ts +18 -0
  38. package/src/baseline/tasks/hmac.ts +38 -0
  39. package/src/baseline/tasks/http.ts +62 -0
  40. package/src/baseline/tasks/json-patch.ts +12 -0
  41. package/src/baseline/tasks/json.ts +50 -0
  42. package/src/baseline/tasks/mime.ts +5 -0
  43. package/src/baseline/tasks/query.ts +14 -0
  44. package/src/baseline/tasks/token.ts +15 -0
  45. package/src/baseline/tasks/url.ts +11 -0
  46. package/src/baseline/tasks/validation.ts +19 -0
  47. package/src/baseline/tasks/websocket.ts +14 -0
  48. package/src/bench/assert.ts +70 -0
  49. package/src/bench/checks.ts +227 -0
  50. package/src/bench/checksum.ts +27 -0
  51. package/src/bench/comparisons.ts +70 -0
  52. package/src/bench/concurrent-worker.ts +135 -0
  53. package/src/bench/fixtures.ts +233 -0
  54. package/src/bench/index.ts +2 -0
  55. package/src/bench/measure.ts +177 -0
  56. package/src/bench/now.ts +3 -0
  57. package/src/bench/report.ts +79 -0
  58. package/src/bench/run.ts +57 -0
  59. package/src/bench/tasks/complex.ts +232 -0
  60. package/src/bench/tasks/concurrent.ts +213 -0
  61. package/src/bench/tasks/cookie.ts +44 -0
  62. package/src/bench/tasks/hashing.ts +33 -0
  63. package/src/bench/tasks/hmac.ts +36 -0
  64. package/src/bench/tasks/http.ts +50 -0
  65. package/src/bench/tasks/index.ts +52 -0
  66. package/src/bench/tasks/json-patch.ts +21 -0
  67. package/src/bench/tasks/json.ts +33 -0
  68. package/src/bench/tasks/mime.ts +21 -0
  69. package/src/bench/tasks/query.ts +44 -0
  70. package/src/bench/tasks/stress.ts +142 -0
  71. package/src/bench/tasks/token.ts +20 -0
  72. package/src/bench/tasks/url.ts +34 -0
  73. package/src/bench/tasks/validation.ts +57 -0
  74. package/src/bench/tasks/websocket.ts +21 -0
  75. package/src/bench/types.ts +60 -0
  76. package/src/data/json-rows.ts +33 -0
  77. package/src/ingress/constants.ts +56 -0
  78. package/src/ingress/fast.ts +1082 -0
  79. package/src/ingress/handlers.ts +1202 -0
  80. package/src/ingress/index.ts +457 -0
  81. package/src/ingress/packed-input.ts +59 -0
  82. package/src/native/index.ts +207 -0
  83. package/src/rust-ffi/index.ts +555 -0
  84. package/src/shared/bytes.ts +36 -0
  85. package/src/shared/json.ts +18 -0
  86. 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
+ }