castrum 0.9.3 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +2242 -0
- package/Cargo.toml +155 -0
- package/README.md +14 -6
- package/build.rs +5 -0
- package/castrum.darwin-arm64.node +0 -0
- package/castrum.darwin-x64.node +0 -0
- package/castrum.linux-arm64-gnu.node +0 -0
- package/castrum.linux-arm64-musl.node +0 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/castrum.linux-x64-musl.node +0 -0
- package/castrum.linux-x64-v3-gnu.node +0 -0
- package/castrum.win32-x64-msvc.node +0 -0
- package/dist/index.d.ts +1508 -1291
- package/dist/index.js +3391 -2069
- package/docs/API.md +2 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +5 -3
- package/docs/ENVIRONMENT.md +25 -2
- package/docs/FFI_BUN_GUIDE.md +242 -14
- package/docs/HOW_TO_ADD_AN_OP.md +10 -0
- package/docs/REPO_MAP.md +17 -6
- package/docs/RND-CONCURRENCY.md +772 -0
- package/docs/bun-builtins-decision-matrix.md +4 -1
- package/index.ts +24 -25
- package/package.json +173 -156
- package/rust/crypto/aead.rs +378 -0
- package/rust/crypto/argon2.rs +265 -0
- package/rust/crypto/base64.rs +420 -0
- package/rust/crypto/bcrypt.rs +89 -0
- package/rust/crypto/cookie_sign.rs +169 -0
- package/rust/crypto/csrf.rs +106 -0
- package/rust/crypto/ed25519.rs +215 -0
- package/rust/crypto/hashing.rs +140 -0
- package/rust/crypto/hmac_sha256.rs +88 -0
- package/rust/crypto/jwt/api.rs +286 -0
- package/rust/crypto/jwt/mod.rs +22 -0
- package/rust/crypto/jwt/tests.rs +312 -0
- package/rust/crypto/jwt/token.rs +360 -0
- package/rust/crypto/mod.rs +27 -0
- package/rust/crypto/pbkdf2.rs +105 -0
- package/rust/crypto/random_token.rs +85 -0
- package/rust/crypto/session.rs +222 -0
- package/rust/ffi/crypto.rs +708 -0
- package/rust/ffi/hashing.rs +285 -0
- package/rust/ffi/http.rs +752 -0
- package/rust/ffi/ingress.rs +303 -0
- package/rust/ffi/json.rs +517 -0
- package/rust/ffi/jwt.rs +419 -0
- package/rust/ffi/metrics.rs +373 -0
- package/rust/ffi/mod.rs +69 -0
- package/rust/ffi/payload.rs +275 -0
- package/rust/ffi/probe.rs +60 -0
- package/rust/ffi/rate_limit.rs +77 -0
- package/rust/ffi/route.rs +85 -0
- package/rust/ffi/task.rs +172 -0
- package/rust/ffi/tests.rs +2333 -0
- package/rust/ffi/util.rs +152 -0
- package/rust/ffi/validators.rs +123 -0
- package/rust/http/accept.rs +606 -0
- package/rust/http/cookie_parser.rs +140 -0
- package/rust/http/etag.rs +289 -0
- package/rust/http/form.rs +136 -0
- package/rust/http/headers.rs +267 -0
- package/rust/http/http_date.rs +286 -0
- package/rust/http/http_parser.rs +252 -0
- package/rust/http/media_type.rs +355 -0
- package/rust/http/method.rs +200 -0
- package/rust/http/mime_lookup.rs +130 -0
- package/rust/http/mod.rs +32 -0
- package/rust/http/multipart.rs +685 -0
- package/rust/http/query_parser.rs +211 -0
- package/rust/http/url_codec.rs +296 -0
- package/rust/http/url_join.rs +324 -0
- package/rust/ingress/api.rs +355 -0
- package/rust/ingress/cors.rs +403 -0
- package/rust/ingress/ingress_constants.rs +209 -0
- package/rust/ingress/ip_trust.rs +312 -0
- package/rust/ingress/mod.rs +48 -0
- package/rust/ingress/native_route.rs +770 -0
- package/rust/ingress/options.rs +176 -0
- package/rust/ingress/output.rs +406 -0
- package/rust/ingress/packed.rs +191 -0
- package/rust/ingress/pipeline.rs +534 -0
- package/rust/ingress/proxy.rs +138 -0
- package/rust/ingress/rate_limit.rs +538 -0
- package/rust/ingress/terminal.rs +114 -0
- package/rust/ingress/tests.rs +567 -0
- package/rust/ingress/time.rs +70 -0
- package/rust/json/fast_schema/capture.rs +336 -0
- package/rust/json/fast_schema/compile.rs +556 -0
- package/rust/json/fast_schema/cursor.rs +368 -0
- package/rust/json/fast_schema/email.rs +282 -0
- package/rust/json/fast_schema/errors.rs +247 -0
- package/rust/json/fast_schema/mod.rs +57 -0
- package/rust/json/fast_schema/tests.rs +1143 -0
- package/rust/json/fast_schema/types.rs +140 -0
- package/rust/json/fast_schema/validate.rs +931 -0
- package/rust/json/json_ops.rs +121 -0
- package/rust/json/json_schema.rs +837 -0
- package/rust/json/json_ser.rs +883 -0
- package/rust/json/mod.rs +19 -0
- package/rust/json/napi_marshal.rs +318 -0
- package/rust/json/patch/api.rs +60 -0
- package/rust/json/patch/engine.rs +153 -0
- package/rust/json/patch/mod.rs +47 -0
- package/rust/json/patch/ops.rs +270 -0
- package/rust/json/patch/pointer.rs +49 -0
- package/rust/json/patch/tests.rs +499 -0
- package/rust/lib.rs +73 -0
- package/rust/metrics/api.rs +148 -0
- package/rust/metrics/mod.rs +21 -0
- package/rust/metrics/registry.rs +961 -0
- package/rust/panic_safety.rs +40 -0
- package/rust/payload/compress.rs +512 -0
- package/rust/payload/mod.rs +13 -0
- package/rust/payload/sse.rs +214 -0
- package/rust/payload/template.rs +253 -0
- package/rust/payload/websocket.rs +69 -0
- package/rust/payload/ws_frames.rs +354 -0
- package/rust/proptest_suite.rs +139 -0
- package/rust/selection.rs +95 -0
- package/rust/src/bin/native-bench.rs +495 -0
- package/rust/task/completion.rs +119 -0
- package/rust/task/mod.rs +65 -0
- package/rust/task/ops.rs +503 -0
- package/rust/task/runtime.rs +220 -0
- package/rust/task/tests.rs +231 -0
- package/rust/test_support.rs +71 -0
- package/rust/util/batch/api.rs +266 -0
- package/rust/util/batch/core.rs +473 -0
- package/rust/util/batch/mod.rs +23 -0
- package/rust/util/batch/tests.rs +445 -0
- package/rust/util/batch_core.rs +184 -0
- package/rust/util/bytes.rs +594 -0
- package/rust/util/mod.rs +34 -0
- package/rust/util/packed.rs +473 -0
- package/rust/util/text.rs +130 -0
- package/rust/util/threadpool.rs +128 -0
- package/rust/util/validation.rs +419 -0
- package/rust-toolchain.toml +17 -0
- package/scripts/postinstall.mjs +199 -0
- package/src/ingress/constants.ts +1 -1
- package/src/ingress/context.ts +2 -2
- package/src/ingress/decode/baked-result.ts +13 -13
- package/src/ingress/decode/fast-result.ts +17 -17
- package/src/ingress/errors.ts +7 -7
- package/src/ingress/headers/baked-templates.ts +1 -1
- package/src/ingress/headers/fast-templates.ts +3 -3
- package/src/ingress/health.ts +1 -2
- package/src/ingress/index.ts +1 -1
- package/src/ingress/native-route.ts +9 -5
- package/src/ingress/options.ts +1 -1
- package/src/ingress/packing/gather-raw-headers.ts +1 -1
- package/src/ingress/packing/route-wire.ts +1 -5
- package/src/ingress/response/error-bodies.ts +5 -5
- package/src/ingress/response/terminal.ts +1 -1
- package/src/ingress/routes/echo.ts +4 -4
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +2 -2
- package/src/ingress/routes/native.ts +2 -1
- package/src/ingress/routes/options.ts +1 -1
- package/src/ingress/routes/read.ts +2 -2
- package/src/ingress/server.ts +10 -6
- package/src/ingress/status.ts +7 -7
- package/src/integration/batch.ts +1 -1
- package/src/integration/index.ts +9 -9
- package/src/integration/pipeline.ts +6 -6
- package/src/integration/websocket.ts +2 -2
- package/src/loader/index.ts +1 -5
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs.ts +130 -12
- package/src/native/ffi/build/compress.ts +22 -10
- package/src/native/ffi/build/instances.ts +46 -4
- package/src/native/ffi/build/metrics.ts +247 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/task.ts +79 -0
- package/src/native/ffi/build/util.ts +32 -1
- package/src/native/ffi/build.ts +4 -0
- package/src/native/ffi/selftest.ts +12 -3
- package/src/native/ffi/types.ts +151 -9
- package/src/native/ffi.ts +160 -15
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +55 -5
- package/src/native/types/instances.ts +34 -0
- package/src/native/types.ts +11 -0
- package/src/runtime/codec.ts +53 -9
- package/src/runtime/types.ts +7 -0
- package/src/rust-ffi/batch/build.ts +16 -9
- package/src/rust-ffi/index.ts +7 -6
- package/src/rust-ffi/options.ts +1 -3
- package/src/rust-ffi/proven.ts +3 -3
- package/src/rust-ffi/scalar/crypto.ts +1 -1
- package/src/rust-ffi/scalar/factories.ts +73 -0
- package/src/rust-ffi/scalar/hashing.ts +1 -1
- package/src/rust-ffi/scalar/http.ts +71 -27
- package/src/rust-ffi/scalar/index.ts +2 -0
- package/src/rust-ffi/scalar/interface.ts +20 -0
- package/src/rust-ffi/scalar/payload.ts +11 -3
- package/src/rust-ffi/scalar/validation.ts +121 -0
- package/src/rust-ffi/text.ts +25 -3
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +24 -4
- package/src/shared/codec.ts +18 -3
- package/src/task/index.ts +24 -0
- package/src/task/op.ts +171 -0
- package/src/task/runtime.ts +568 -0
package/src/native/ffi/types.ts
CHANGED
|
@@ -29,14 +29,23 @@ export interface BunFFI {
|
|
|
29
29
|
/** RFC 3986 percent-encode into `output`; returns bytes written. */
|
|
30
30
|
urlEncodeInto(input: Uint8Array, output: Uint8Array): number
|
|
31
31
|
|
|
32
|
-
// ── Validators (u8 → boolean)
|
|
33
|
-
//
|
|
32
|
+
// ── Validators (u8 → boolean) ─────────────────────────────────────
|
|
33
|
+
// String forms cross as a `cstring` ARG: the engine transcodes the JS string
|
|
34
34
|
// to a call-scoped NUL-terminated UTF-8 buffer in-engine, so the JS side does
|
|
35
|
-
// ZERO
|
|
35
|
+
// ZERO encode work and Rust borrows via `CStr::from_ptr`. Byte forms are the
|
|
36
|
+
// `(ptr,len)` siblings — zero transcode when the caller already holds bytes.
|
|
36
37
|
validateEmail(input: string): boolean
|
|
37
38
|
validateUuid(input: string): boolean
|
|
38
39
|
validateIpv4(input: string): boolean
|
|
39
40
|
validateIpv6(input: string): boolean
|
|
41
|
+
/** Byte-input sibling of {@link validateEmail} (`(ptr,len)` — no decode). */
|
|
42
|
+
validateEmailBytes(input: Uint8Array): boolean
|
|
43
|
+
/** Byte-input sibling of {@link validateUuid} (`(ptr,len)` — no decode). */
|
|
44
|
+
validateUuidBytes(input: Uint8Array): boolean
|
|
45
|
+
/** Byte-input sibling of {@link validateIpv4} (`(ptr,len)` — no decode). */
|
|
46
|
+
validateIpv4Bytes(input: Uint8Array): boolean
|
|
47
|
+
/** Byte-input sibling of {@link validateIpv6} (`(ptr,len)` — no decode). */
|
|
48
|
+
validateIpv6Bytes(input: Uint8Array): boolean
|
|
40
49
|
/** Sum of `id` fields across a JSON array → bigint (throws on non-array input). */
|
|
41
50
|
jsonSumIds(input: Uint8Array): bigint
|
|
42
51
|
|
|
@@ -114,11 +123,7 @@ export interface BunFFI {
|
|
|
114
123
|
nowSeconds: number,
|
|
115
124
|
): string | null
|
|
116
125
|
/** EdDSA (Ed25519) JWT verify → claims JSON string; `null` = invalid. */
|
|
117
|
-
jwtEdDSAVerify(
|
|
118
|
-
token: Uint8Array,
|
|
119
|
-
publicKey: Uint8Array,
|
|
120
|
-
nowSeconds: number,
|
|
121
|
-
): string | null
|
|
126
|
+
jwtEdDSAVerify(token: Uint8Array, publicKey: Uint8Array, nowSeconds: number): string | null
|
|
122
127
|
/** Compiled `TemplateRenderer` → UTF-8 bytes from a pre-serialized JSON
|
|
123
128
|
* context (opaque `inner` handle); throws on invalid context / render error. */
|
|
124
129
|
templateRender(inner: number, contextJson: Uint8Array): Uint8Array
|
|
@@ -213,6 +218,12 @@ export interface BunFFI {
|
|
|
213
218
|
gzipDecompress(data: Uint8Array, maxDecompressed?: number): Uint8Array
|
|
214
219
|
/** gzip-decompress into `output`; returns bytes written (throws if too small). */
|
|
215
220
|
gzipDecompressInto(data: Uint8Array, output: Uint8Array, maxDecompressed?: number): number
|
|
221
|
+
/**
|
|
222
|
+
* gzip ISIZE trailer probe → the uncompressed size for a single-member gzip
|
|
223
|
+
* stream, or `0` when unknown (invalid stream / multi-member). Used to
|
|
224
|
+
* PRE-SIZE a destination buffer for the zero-copy task path.
|
|
225
|
+
*/
|
|
226
|
+
gzipIsize(data: Uint8Array): number
|
|
216
227
|
/** brotli-compress into a fresh buffer (quality clamped 0..=11, default 5). */
|
|
217
228
|
brotliCompress(data: Uint8Array, quality?: number): Uint8Array
|
|
218
229
|
/** brotli-compress into `output`; returns bytes written (throws if too small). */
|
|
@@ -308,7 +319,10 @@ export interface BunFFI {
|
|
|
308
319
|
/**
|
|
309
320
|
* Encode one SSE event → bytes (fresh buffer). event/id null = line omitted
|
|
310
321
|
* (a present-but-empty string emits the line, matching napi `Option`).
|
|
311
|
-
*
|
|
322
|
+
* NOTE: event/id stay `(ptr,len)` byte args — a cstring-ARG conversion was
|
|
323
|
+
* attempted and reverted (2026-08-23); see docs/FFI_BUN_GUIDE.md §14 before
|
|
324
|
+
* touching this signature.
|
|
325
|
+
* FFI sibling of the napi `sse_encode_event`.
|
|
312
326
|
*/
|
|
313
327
|
sseEncodeEvent(
|
|
314
328
|
event: string | null,
|
|
@@ -388,6 +402,132 @@ export interface BunFFI {
|
|
|
388
402
|
routeRun(handle: number, frame: Uint8Array, output: Uint8Array): number
|
|
389
403
|
/** Destroy a compiled route handle (frees the native `Box<NativeRoute>`). */
|
|
390
404
|
routeDestroy(handle: number): void
|
|
405
|
+
|
|
406
|
+
// ── Metrics registry (`castrum_metrics_*`) ─────────────────────────
|
|
407
|
+
/** Create an empty metrics registry → a caller-owned opaque handle. */
|
|
408
|
+
metricsCreate(): number
|
|
409
|
+
/**
|
|
410
|
+
* Declare a counter family → its `u32` series id (`0xFFFFFFFF` = error).
|
|
411
|
+
* `name` and `labelKeys` are `cstring` ARGs; label keys are
|
|
412
|
+
* `\u001f`-separated (empty string = no labels).
|
|
413
|
+
*/
|
|
414
|
+
metricsCounter(handle: number, name: string, labelKeys: string): number
|
|
415
|
+
/** Declare a gauge family → its series id. Same ABI as {@link metricsCounter}. */
|
|
416
|
+
metricsGauge(handle: number, name: string, labelKeys: string): number
|
|
417
|
+
/**
|
|
418
|
+
* Declare a histogram family → its series id. `bucketsCsv` is a
|
|
419
|
+
* comma-separated finite positive list (empty string = default buckets).
|
|
420
|
+
*/
|
|
421
|
+
metricsHistogram(handle: number, name: string, labelKeys: string, bucketsCsv: string): number
|
|
422
|
+
/**
|
|
423
|
+
* Counter/gauge `+= amount`, or histogram observe `amount`. `values` is the
|
|
424
|
+
* packed `\u001f`-separated label values. Returns true on success.
|
|
425
|
+
*/
|
|
426
|
+
metricsRecord(handle: number, series: number, values: Uint8Array, amount: number): boolean
|
|
427
|
+
/** Gauge assignment (`= value`). Returns true on success. */
|
|
428
|
+
metricsGaugeSet(handle: number, series: number, values: Uint8Array, value: number): boolean
|
|
429
|
+
/**
|
|
430
|
+
* Zero-encode sibling of {@link metricsRecord}: `values` is the JOINED
|
|
431
|
+
* `\u001f`-separated JS string — the engine transcodes it in-engine.
|
|
432
|
+
*/
|
|
433
|
+
metricsRecordStr(handle: number, series: number, values: string, amount: number): boolean
|
|
434
|
+
/** Zero-encode sibling of {@link metricsGaugeSet} (joined string values). */
|
|
435
|
+
metricsGaugeSetStr(handle: number, series: number, values: string, value: number): boolean
|
|
436
|
+
/**
|
|
437
|
+
* Render the Prometheus text format into `output`; returns bytes written
|
|
438
|
+
* (`0` = null handle / error; `> output.length` = exact required size).
|
|
439
|
+
*/
|
|
440
|
+
metricsRender(handle: number, output: Uint8Array): number
|
|
441
|
+
/**
|
|
442
|
+
* Dump the packed v1 series snapshot into `output`; returns bytes written
|
|
443
|
+
* (`0` = null handle / error; `> output.length` = exact required size).
|
|
444
|
+
*/
|
|
445
|
+
metricsSnapshot(handle: number, output: Uint8Array): number
|
|
446
|
+
/** Destroy a registry handle (frees the native `Box<MetricsRegistry>`). */
|
|
447
|
+
metricsDestroy(handle: number): void
|
|
448
|
+
/**
|
|
449
|
+
* Record a BATCH of events in one crossing. Packed layout:
|
|
450
|
+
* `[u32 n]{[u32 series][u32 valsLen][vals][f64 amount]}`; returns true when
|
|
451
|
+
* all entries applied.
|
|
452
|
+
*/
|
|
453
|
+
metricsRecordBatch(handle: number, packed: Uint8Array): boolean
|
|
454
|
+
/**
|
|
455
|
+
* Fused wire-level validation: RAW query string → JSON → draft-07 validate
|
|
456
|
+
* against the compiled schema (`inner` from `SchemaValidator.innerPtr()`).
|
|
457
|
+
*/
|
|
458
|
+
queryValidate(inner: number, qs: string): boolean
|
|
459
|
+
/** Fused cookie-header variant of {@link queryValidate}. */
|
|
460
|
+
cookieValidate(inner: number, header: string): boolean
|
|
461
|
+
/**
|
|
462
|
+
* Seal a session envelope natively: builds
|
|
463
|
+
* `{"id":"…","data":<dataJson>,"exp":exp}` and HMAC-signs it →
|
|
464
|
+
* `payload.<hex>` token string (`null` = failure).
|
|
465
|
+
*/
|
|
466
|
+
sessionSeal(id: string, dataJson: string, expSecs: number, secret: string): string | null
|
|
467
|
+
/**
|
|
468
|
+
* Open a sealed session token: verify + extract in one crossing into
|
|
469
|
+
* `output` as `[u8 ok][i64 exp][u32 idLen][id][u32 dataLen][dataJson]`.
|
|
470
|
+
* Returns bytes written (`0` = bad signature / error; `> output.length` =
|
|
471
|
+
* exact required size).
|
|
472
|
+
*/
|
|
473
|
+
sessionOpen(token: string, secret: string, output: Uint8Array): number
|
|
474
|
+
|
|
475
|
+
// ── Batch fixed-width hex validation / RegExp escaping ─────────────
|
|
476
|
+
/**
|
|
477
|
+
* Validate NEWLINE-separated lines as fixed-width hex into `output`
|
|
478
|
+
* (one verdict byte per line); returns bytes written (`0` = bad width /
|
|
479
|
+
* error; `> output.length` = exact required size).
|
|
480
|
+
*/
|
|
481
|
+
hexValidateBatchInto(input: Uint8Array, width: number, output: Uint8Array): number
|
|
482
|
+
/**
|
|
483
|
+
* String-input sibling of {@link hexValidateBatchInto}: the NEWLINE-
|
|
484
|
+
* separated ids cross as a `cstring` ARG (engine-transcoded — no encode).
|
|
485
|
+
*/
|
|
486
|
+
hexValidateBatchStr(ids: string, width: number, output: Uint8Array): number
|
|
487
|
+
/**
|
|
488
|
+
* Escape JS-RegExp metacharacters into `output`; returns bytes written
|
|
489
|
+
* (`0` = error; `> output.length` = exact required size).
|
|
490
|
+
*/
|
|
491
|
+
regexEscapeInto(input: Uint8Array, output: Uint8Array): number
|
|
492
|
+
/**
|
|
493
|
+
* Zero-copy text sibling of {@link regexEscapeInto}: cstring ARG in,
|
|
494
|
+
* cstring return out (engine-cloned — zero JS decode). `null` = failure.
|
|
495
|
+
*/
|
|
496
|
+
regexEscapeStr(input: string): string | null
|
|
497
|
+
|
|
498
|
+
// ── Off-thread task runtime (castrum_task_*) ──────────────────────
|
|
499
|
+
/** Start the shared native task pool; 0 = started, 1 = already running. */
|
|
500
|
+
taskInit(threads: number): number
|
|
501
|
+
/** Submit a task op with packed `args` under `taskId`; 1 = accepted. */
|
|
502
|
+
taskSubmit(op: number, args: Uint8Array, taskId: number): number
|
|
503
|
+
/**
|
|
504
|
+
* Zero-copy submit: the op writes its result DIRECTLY into `output` on the
|
|
505
|
+
* pool thread. `output` must be a stable, caller-owned buffer that stays
|
|
506
|
+
* alive (and untouched) until the completion is drained — the runtime holds
|
|
507
|
+
* the reference. The completion body is the 8-byte LE written length.
|
|
508
|
+
*/
|
|
509
|
+
taskSubmitOut(op: number, args: Uint8Array, taskId: number, output: Uint8Array): number
|
|
510
|
+
/**
|
|
511
|
+
* Zero-copy INPUT: only `hdr` is copied; `data` is read in place by the pool
|
|
512
|
+
* thread, so the caller must keep it alive (and untouched) until completion.
|
|
513
|
+
*/
|
|
514
|
+
taskSubmitSlice(op: number, hdr: Uint8Array, data: Uint8Array, taskId: number): number
|
|
515
|
+
/**
|
|
516
|
+
* Drain finished tasks into `output` (packed `[u32 count][entry…]`).
|
|
517
|
+
* Returns bytes written, `0` when nothing is pending, or the EXACT required
|
|
518
|
+
* size when `output` is too small (nothing is consumed in that case).
|
|
519
|
+
*/
|
|
520
|
+
taskDrain(output: Uint8Array): number
|
|
521
|
+
/** Finished-but-undrained completion count. */
|
|
522
|
+
taskPending(): number
|
|
523
|
+
/** Mark a task cancelled; 1 = newly marked, 0 = unknown/already finished. */
|
|
524
|
+
taskCancel(taskId: number): number
|
|
525
|
+
/** Register the thread-safe doorbell trampoline address (`0` disables it). */
|
|
526
|
+
taskSetDoorbell(ptr: number): number
|
|
527
|
+
/** Stop the task pool and drop pending completions. */
|
|
528
|
+
taskShutdown(): number
|
|
529
|
+
/** Configured task-pool worker count (0 before the pool starts). */
|
|
530
|
+
taskThreads(): number
|
|
391
531
|
}
|
|
392
532
|
|
|
393
533
|
/**
|
|
@@ -411,6 +551,8 @@ export interface RateLimiterVerdict {
|
|
|
411
551
|
resetMs: number
|
|
412
552
|
}
|
|
413
553
|
|
|
554
|
+
/** Raw 0-arg C-ABI symbol signature (`castrum_metrics_create`). */
|
|
555
|
+
export type Raw0 = () => number | bigint
|
|
414
556
|
/** Raw 2-arg C-ABI symbol signature (`(ptr,len)` pairs → scalar result). */
|
|
415
557
|
export type Raw2 = (a: unknown, b: unknown) => number | bigint
|
|
416
558
|
/** Raw 3-arg C-ABI symbol signature. */
|
package/src/native/ffi.ts
CHANGED
|
@@ -34,7 +34,7 @@ import type { BunFFI, FfiMode } from './ffi/types'
|
|
|
34
34
|
// Static import: loader.ts has no side effects (path resolution only — no
|
|
35
35
|
// dlopen), so this adds zero import-time cost while letting `bind()` reuse the
|
|
36
36
|
// exact same resolved `.node` path the napi fallback uses (the shared seam).
|
|
37
|
-
import { getAddonPath } from './loader'
|
|
37
|
+
import { getAddonPath, getAddonPathCandidates } from './loader'
|
|
38
38
|
|
|
39
39
|
// Re-export the type surface so `import type { BunFFI } from '../native/ffi'`
|
|
40
40
|
// keeps working (existing call sites).
|
|
@@ -49,6 +49,12 @@ let cached: BunFFI | null | undefined
|
|
|
49
49
|
*/
|
|
50
50
|
let bufferAbiMode: 'buffer-pair' | 'ptr-len' | null = null
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The addon file the live `bun:ffi` binding was opened from, or `null` while
|
|
54
|
+
* unbound. Diagnostics only — a v3→baseline fallback is visible here.
|
|
55
|
+
*/
|
|
56
|
+
let boundAddonPath: string | null = null
|
|
57
|
+
|
|
52
58
|
// ── Transport selection (CASTRUM_FFI_MODE) ───────────────────────
|
|
53
59
|
|
|
54
60
|
function resolveFfiMode(): FfiMode {
|
|
@@ -96,6 +102,20 @@ export function ffiBufferMode(): 'buffer-pair' | 'ptr-len' | null {
|
|
|
96
102
|
return bufferAbiMode
|
|
97
103
|
}
|
|
98
104
|
|
|
105
|
+
/**
|
|
106
|
+
* The addon file path the live `bun:ffi` binding was opened from, or `null`
|
|
107
|
+
* when the transport is unavailable (Node, `CASTRUM_FFI_MODE=napi`, or every
|
|
108
|
+
* candidate failed). Lazy — triggers the same one-time bind as
|
|
109
|
+
* {@link getBunFFI}. Use it to confirm WHICH artifact bound, e.g. when a stale
|
|
110
|
+
* v3 SIMD binary forced the baseline fallback.
|
|
111
|
+
*/
|
|
112
|
+
export function ffiAddonPath(): string | null {
|
|
113
|
+
if (cached === undefined) {
|
|
114
|
+
getBunFFI()
|
|
115
|
+
}
|
|
116
|
+
return boundAddonPath
|
|
117
|
+
}
|
|
118
|
+
|
|
99
119
|
/**
|
|
100
120
|
* Probe whether this Bun build accepts the `buffer`/`buffer_length` ABI pair in
|
|
101
121
|
* `dlopen`. Bun's docs list `buffer_length` as engine-native (dlopen-supported),
|
|
@@ -127,7 +147,7 @@ function probeBufferLength(dlopen: typeof import('bun:ffi')['dlopen'], path: str
|
|
|
127
147
|
}
|
|
128
148
|
}
|
|
129
149
|
|
|
130
|
-
function bind(): BunFFI | null {
|
|
150
|
+
function bind(forcedPath?: string, tried: string[] = []): BunFFI | null {
|
|
131
151
|
bufferAbiMode = null // reset — the mode below is only valid for a live bind
|
|
132
152
|
const mode = resolveFfiMode()
|
|
133
153
|
if (!isBun()) {
|
|
@@ -143,12 +163,13 @@ function bind(): BunFFI | null {
|
|
|
143
163
|
if (mode === 'napi') {
|
|
144
164
|
return null
|
|
145
165
|
}
|
|
166
|
+
// Resolve the SAME addon file napi uses (./loader.ts — statically imported
|
|
167
|
+
// above; it only resolves the path, it never dlopens the addon). On a retry,
|
|
168
|
+
// `forcedPath` is the next candidate from `getAddonPathCandidates()`.
|
|
169
|
+
const path = forcedPath ?? getAddonPath()
|
|
146
170
|
try {
|
|
147
171
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
148
172
|
const { dlopen } = require('bun:ffi') as typeof import('bun:ffi')
|
|
149
|
-
// Resolve the SAME addon file napi uses (./loader.ts — statically imported
|
|
150
|
-
// above; it only resolves the path, it never dlopens the addon).
|
|
151
|
-
const path = getAddonPath()
|
|
152
173
|
|
|
153
174
|
// Probe `buffer`/`buffer_length` (the engine reads ptr + len off the SAME
|
|
154
175
|
// TypedArray at call time — an atomic snapshot) once; when supported we use
|
|
@@ -206,10 +227,25 @@ function bind(): BunFFI | null {
|
|
|
206
227
|
castrum_url_decode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
207
228
|
// String-input validators: `cstring` ARG (the engine transcodes the JS
|
|
208
229
|
// string in-engine — no JS-side encode; Rust borrows via CStr::from_ptr).
|
|
230
|
+
// The `_bytes` siblings take a `(ptr,len)` pair — zero transcode when
|
|
231
|
+
// the caller already holds bytes (a cstring arg would force a decode +
|
|
232
|
+
// engine re-encode; docs/FFI_BUN_GUIDE.md §3 shape 4).
|
|
233
|
+
//
|
|
234
|
+
// SAFETY RULE for every `cstring` ARG added here: the transcode is
|
|
235
|
+
// NUL-TERMINATED, so an embedded U+0000 truncates the value in-engine. A
|
|
236
|
+
// verdict op bound this way can be fooled (`validateEmail` on
|
|
237
|
+
// 'a@b.com\0<script>' returned TRUE before the byte re-route). Either
|
|
238
|
+
// give the op a `_bytes` sibling and route byte callers to it, or guard
|
|
239
|
+
// the string form with `hasNul` — see docs/FFI_BUN_GUIDE.md §6.1,
|
|
240
|
+
// pinned by test/unit/native/cstring-nul.test.ts.
|
|
209
241
|
castrum_validate_email: { args: ['cstring'], returns: 'u8' },
|
|
210
242
|
castrum_validate_uuid: { args: ['cstring'], returns: 'u8' },
|
|
211
243
|
castrum_validate_ipv4: { args: ['cstring'], returns: 'u8' },
|
|
212
244
|
castrum_validate_ipv6: { args: ['cstring'], returns: 'u8' },
|
|
245
|
+
castrum_validate_email_bytes: { args: inputAbi, returns: 'u8' },
|
|
246
|
+
castrum_validate_uuid_bytes: { args: inputAbi, returns: 'u8' },
|
|
247
|
+
castrum_validate_ipv4_bytes: { args: inputAbi, returns: 'u8' },
|
|
248
|
+
castrum_validate_ipv6_bytes: { args: inputAbi, returns: 'u8' },
|
|
213
249
|
castrum_json_sum_ids: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
214
250
|
castrum_hmac_sha256_verify: {
|
|
215
251
|
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
|
|
@@ -357,6 +393,11 @@ function bind(): BunFFI | null {
|
|
|
357
393
|
},
|
|
358
394
|
castrum_http_date_into: { args: ['f64', ...abi(['ptr', 'usize'])], returns: U64_FAST },
|
|
359
395
|
castrum_sse_encode_into: {
|
|
396
|
+
// event/id stay `(ptr,len)` byte args. A cstring-ARG conversion was
|
|
397
|
+
// tried (2026-08-23) and reverted: the win was unproven once the
|
|
398
|
+
// codec encode path got faster, and ABI-desync segfaults during the
|
|
399
|
+
// experiment were misread as an engine bug — see docs/FFI_BUN_GUIDE.md
|
|
400
|
+
// §14 before touching this signature.
|
|
360
401
|
args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'u32', 'ptr', 'usize']),
|
|
361
402
|
returns: U64_FAST,
|
|
362
403
|
},
|
|
@@ -467,24 +508,128 @@ function bind(): BunFFI | null {
|
|
|
467
508
|
returns: U64_FAST,
|
|
468
509
|
},
|
|
469
510
|
castrum_route_destroy: { args: ['usize'], returns: 'void' },
|
|
511
|
+
// ── Batch fixed-width hex validation (ObjectId-shaped ids, etc.) ──
|
|
512
|
+
// NEWLINE-separated lines in; one verdict byte (1/0) per line out.
|
|
513
|
+
castrum_hex_validate_batch: {
|
|
514
|
+
args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
|
|
515
|
+
returns: U64_FAST,
|
|
516
|
+
},
|
|
517
|
+
// ── JS-RegExp metacharacter escaping (needed-size convention) ──
|
|
518
|
+
castrum_regex_escape: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
519
|
+
// ── Metrics registry (`castrum_metrics_*`) ──────────────────────
|
|
520
|
+
// Caller-owned handle (route-stack ownership model): create returns a
|
|
521
|
+
// Box<Registry> as usize; declare fns take name/label-keys as cstring
|
|
522
|
+
// ARGS (label keys `\x1f`-separated) and return the u32 series id
|
|
523
|
+
// (0xFFFFFFFF = error); record/set cross the packed label VALUES as a
|
|
524
|
+
// `(ptr,len)` pair + the amount as f64.
|
|
525
|
+
castrum_metrics_create: { args: [], returns: U64_FAST },
|
|
526
|
+
castrum_metrics_counter: { args: ['usize', 'cstring', 'cstring'], returns: 'u32' },
|
|
527
|
+
castrum_metrics_gauge: { args: ['usize', 'cstring', 'cstring'], returns: 'u32' },
|
|
528
|
+
castrum_metrics_histogram: {
|
|
529
|
+
args: ['usize', 'cstring', 'cstring', 'cstring'],
|
|
530
|
+
returns: 'u32',
|
|
531
|
+
},
|
|
532
|
+
castrum_metrics_record: {
|
|
533
|
+
args: abi(['usize', 'u32', 'ptr', 'usize', 'f64']),
|
|
534
|
+
returns: 'u8',
|
|
535
|
+
},
|
|
536
|
+
castrum_metrics_gauge_set: {
|
|
537
|
+
args: abi(['usize', 'u32', 'ptr', 'usize', 'f64']),
|
|
538
|
+
returns: 'u8',
|
|
539
|
+
},
|
|
540
|
+
// Zero-encode siblings: the joined label values cross as ONE `cstring`
|
|
541
|
+
// ARG — the engine transcodes the JS string in-engine (no TextEncoder).
|
|
542
|
+
castrum_metrics_record_str: {
|
|
543
|
+
args: ['usize', 'u32', 'cstring', 'f64'],
|
|
544
|
+
returns: 'u8',
|
|
545
|
+
},
|
|
546
|
+
castrum_metrics_gauge_set_str: {
|
|
547
|
+
args: ['usize', 'u32', 'cstring', 'f64'],
|
|
548
|
+
returns: 'u8',
|
|
549
|
+
},
|
|
550
|
+
// Zero-copy text escape: cstring ARG in, cstring return out — the JS
|
|
551
|
+
// side does zero encode AND zero decode (engine clones at return).
|
|
552
|
+
castrum_regex_escape_str: { args: ['cstring'], returns: 'cstring' },
|
|
553
|
+
// String-input batch hex validation: `ids.join('\n')` crosses as a
|
|
554
|
+
// cstring ARG.
|
|
555
|
+
castrum_hex_validate_batch_str: {
|
|
556
|
+
args: abi(['cstring', 'u32', 'ptr', 'usize']),
|
|
557
|
+
returns: U64_FAST,
|
|
558
|
+
},
|
|
559
|
+
castrum_metrics_render: { args: abi(['usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
560
|
+
castrum_metrics_snapshot: { args: abi(['usize', 'ptr', 'usize']), returns: U64_FAST },
|
|
561
|
+
castrum_metrics_record_batch: {
|
|
562
|
+
args: abi(['usize', 'ptr', 'usize']),
|
|
563
|
+
returns: 'u8',
|
|
564
|
+
},
|
|
565
|
+
// Fused wire-level validation: RAW query/cookie → JSON → draft-07 gate.
|
|
566
|
+
// `inner` is the SchemaValidator inner handle; strings cross as cstring
|
|
567
|
+
// ARGs (engine-transcoded).
|
|
568
|
+
castrum_query_validate: { args: ['usize', 'cstring'], returns: 'u8' },
|
|
569
|
+
castrum_cookie_validate: { args: ['usize', 'cstring'], returns: 'u8' },
|
|
570
|
+
// Fused session envelope: seal builds+signs; open verifies + extracts.
|
|
571
|
+
castrum_session_seal: {
|
|
572
|
+
args: ['cstring', 'cstring', 'i64', 'cstring'],
|
|
573
|
+
returns: 'cstring',
|
|
574
|
+
},
|
|
575
|
+
castrum_session_open: {
|
|
576
|
+
args: abi(['cstring', 'cstring', 'ptr', 'usize']),
|
|
577
|
+
returns: U64_FAST,
|
|
578
|
+
},
|
|
579
|
+
// Off-thread task runtime: `submit` takes a packed `(ptr,len)` arg blob
|
|
580
|
+
// plus a JS-assigned id; `drain` writes the packed completion batch into
|
|
581
|
+
// a caller buffer (needed-size convention). `set_doorbell` takes the raw
|
|
582
|
+
// `JSCallback({threadsafe:true}).ptr` as a bare pointer (`abi` leaves a
|
|
583
|
+
// lone `ptr` untouched), so the pool can wake JS once per batch.
|
|
584
|
+
castrum_task_init: { args: ['u32'], returns: 'u32' },
|
|
585
|
+
castrum_task_submit: { args: abi(['u32', 'ptr', 'usize', 'usize']), returns: 'u32' },
|
|
586
|
+
castrum_task_submit_slice: {
|
|
587
|
+
args: abi(['u32', 'ptr', 'usize', 'ptr', 'usize', 'usize']),
|
|
588
|
+
returns: 'u32',
|
|
589
|
+
},
|
|
590
|
+
castrum_task_submit_out: {
|
|
591
|
+
args: abi(['u32', 'ptr', 'usize', 'usize', 'ptr', 'usize']),
|
|
592
|
+
returns: 'u32',
|
|
593
|
+
},
|
|
594
|
+
castrum_task_drain: { args: abi(['ptr', 'usize']), returns: U64_FAST },
|
|
595
|
+
castrum_task_pending: { args: [], returns: 'u32' },
|
|
596
|
+
castrum_task_cancel: { args: ['usize'], returns: 'u32' },
|
|
597
|
+
castrum_task_set_doorbell: { args: ['ptr'], returns: 'u32' },
|
|
598
|
+
castrum_task_shutdown: { args: [], returns: 'u32' },
|
|
599
|
+
castrum_task_threads: { args: [], returns: 'u32' },
|
|
600
|
+
castrum_metrics_destroy: { args: ['usize'], returns: 'void' },
|
|
470
601
|
})
|
|
471
602
|
|
|
472
|
-
|
|
603
|
+
// (`as unknown as`: the zero-arg `castrum_metrics_create` infers
|
|
604
|
+
// `(...args: never[])`, which can't overlap `(...a: unknown[])` directly.)
|
|
605
|
+
const bindings = build(
|
|
606
|
+
symbols as unknown as Record<string, (...a: unknown[]) => unknown>,
|
|
607
|
+
useBufferLength,
|
|
608
|
+
)
|
|
473
609
|
if (!selfTest(bindings)) {
|
|
474
|
-
|
|
475
|
-
throw new Error(
|
|
476
|
-
'CASTRUM_FFI_MODE=ffi: the bun:ffi bind-time self-test failed, so the ' +
|
|
477
|
-
'primary transport cannot be trusted on this Bun/addon combination. ' +
|
|
478
|
-
'Unset CASTRUM_FFI_MODE (or use CASTRUM_FFI_MODE=auto) to fall back to napi.',
|
|
479
|
-
)
|
|
480
|
-
}
|
|
481
|
-
return null
|
|
610
|
+
throw new Error(`bun:ffi bind-time self-test failed on ${path}`)
|
|
482
611
|
}
|
|
612
|
+
boundAddonPath = path
|
|
483
613
|
return bindings
|
|
484
614
|
} catch (err) {
|
|
615
|
+
bufferAbiMode = null
|
|
616
|
+
// The preferred artifact failed to dlopen/bind/self-test. The common cause
|
|
617
|
+
// is a STALE v3 SIMD binary (`castrum.linux-x64-v3-gnu.node`) that predates
|
|
618
|
+
// a newly added `castrum_*` symbol: dlopen fails, and without this walk the
|
|
619
|
+
// whole FFI transport would be disabled (every call silently downgraded to
|
|
620
|
+
// napi, and the task runtime silently made synchronous). Try the remaining
|
|
621
|
+
// existing candidates — the baseline binary — before giving up.
|
|
622
|
+
for (const next of getAddonPathCandidates()) {
|
|
623
|
+
if (next === path || tried.includes(next)) continue
|
|
624
|
+
const bound = bind(next, [...tried, path])
|
|
625
|
+
if (bound) return bound
|
|
626
|
+
}
|
|
485
627
|
if (mode === 'ffi') {
|
|
486
628
|
const cause = err instanceof Error ? `: ${err.message}` : ''
|
|
487
|
-
throw new Error(
|
|
629
|
+
throw new Error(
|
|
630
|
+
`CASTRUM_FFI_MODE=ffi: failed to bind bun:ffi${cause} ` +
|
|
631
|
+
`(tried: ${[...tried, path].join(', ')})`,
|
|
632
|
+
)
|
|
488
633
|
}
|
|
489
634
|
return null
|
|
490
635
|
}
|
package/src/native/index.ts
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
// (./loader.ts). Importing this barrel does NOT dlopen the addon — use
|
|
5
5
|
// `getAddon()` / `lazyAddon` at call time.
|
|
6
6
|
|
|
7
|
+
export { getAddon, getAddonPath, lazyAddon } from './loader'
|
|
7
8
|
export * from './types'
|
|
8
|
-
export { getAddonPath, getAddon, lazyAddon } from './loader'
|
package/src/native/loader.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// `import castrum` as cheap as possible on Bun.
|
|
6
6
|
|
|
7
7
|
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
8
|
-
import { fileURLToPath } from 'node:url'
|
|
9
|
-
import { join, dirname } from 'node:path'
|
|
10
8
|
import { createRequire } from 'node:module'
|
|
9
|
+
import { dirname, join } from 'node:path'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
11
|
import { resolveEnvVar } from '../shared/env'
|
|
12
12
|
import type { NativeAddon } from './types'
|
|
13
13
|
|
|
@@ -67,15 +67,16 @@ export function supportsX8664V3(cpuinfo?: string): boolean {
|
|
|
67
67
|
* @param arch `process.arch`
|
|
68
68
|
* @param cpuinfo optional `/proc/cpuinfo` text (tests inject it; the real call
|
|
69
69
|
* omits it and reads the file lazily on linux/x64 only)
|
|
70
|
-
* @returns
|
|
70
|
+
* @returns every candidate path, in preference order (v3 SIMD first on a
|
|
71
|
+
* capable linux/x64 host, then the baseline names)
|
|
71
72
|
*/
|
|
72
|
-
export function
|
|
73
|
+
export function addonCandidatesFrom(
|
|
73
74
|
envOverride: string | undefined,
|
|
74
75
|
baseDir: string,
|
|
75
76
|
platform: string,
|
|
76
77
|
arch: string,
|
|
77
78
|
cpuinfo?: string,
|
|
78
|
-
): string {
|
|
79
|
+
): string[] {
|
|
79
80
|
// napi-rs artifact naming: castrum.<platform>-<arch>[-<libc>].node
|
|
80
81
|
// e.g. linux-x64-gnu, linux-x64-musl, win32-x64-msvc, darwin-arm64.
|
|
81
82
|
const libcVariants =
|
|
@@ -156,6 +157,22 @@ export function resolveAddonPathFrom(
|
|
|
156
157
|
candidates.push(join(baseDir, '..', '..', 'target', 'release', name))
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
return candidates
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve the addon path: the FIRST existing candidate from
|
|
165
|
+
* {@link addonCandidatesFrom} (v3 SIMD first on a capable linux/x64 host).
|
|
166
|
+
* Throws when nothing exists.
|
|
167
|
+
*/
|
|
168
|
+
export function resolveAddonPathFrom(
|
|
169
|
+
envOverride: string | undefined,
|
|
170
|
+
baseDir: string,
|
|
171
|
+
platform: string,
|
|
172
|
+
arch: string,
|
|
173
|
+
cpuinfo?: string,
|
|
174
|
+
): string {
|
|
175
|
+
const candidates = addonCandidatesFrom(envOverride, baseDir, platform, arch, cpuinfo)
|
|
159
176
|
for (const candidate of candidates) {
|
|
160
177
|
if (existsSync(candidate)) {
|
|
161
178
|
return candidate
|
|
@@ -169,6 +186,39 @@ export function resolveAddonPathFrom(
|
|
|
169
186
|
)
|
|
170
187
|
}
|
|
171
188
|
|
|
189
|
+
/** Cached result of {@link getAddonPathCandidates} (existing files only). */
|
|
190
|
+
let cachedCandidates: string[] | undefined
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Every EXISTING addon candidate, in preference order (v3 SIMD first on a
|
|
194
|
+
* capable linux/x64 host, then the baseline names).
|
|
195
|
+
*
|
|
196
|
+
* The bun:ffi binder walks this list so a stale or partial *preferred* artifact
|
|
197
|
+
* — e.g. a `castrum.linux-x64-v3-gnu.node` built before a newly added
|
|
198
|
+
* `castrum_*` symbol — falls back to the baseline binary instead of silently
|
|
199
|
+
* disabling the whole FFI transport (which would downgrade every native call to
|
|
200
|
+
* napi and make the off-thread task runtime run synchronously).
|
|
201
|
+
*/
|
|
202
|
+
export function getAddonPathCandidates(): string[] {
|
|
203
|
+
if (cachedCandidates) return cachedCandidates
|
|
204
|
+
const all = addonCandidatesFrom(
|
|
205
|
+
resolveEnvVar('CASTRUM_NATIVE_LIBRARY_PATH', ['NAPI_RS_NATIVE_LIBRARY_PATH']),
|
|
206
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
207
|
+
process.platform,
|
|
208
|
+
process.arch,
|
|
209
|
+
)
|
|
210
|
+
const seen = new Set<string>()
|
|
211
|
+
const existing: string[] = []
|
|
212
|
+
for (const candidate of all) {
|
|
213
|
+
if (!seen.has(candidate) && existsSync(candidate)) {
|
|
214
|
+
seen.add(candidate)
|
|
215
|
+
existing.push(candidate)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
cachedCandidates = existing
|
|
219
|
+
return existing
|
|
220
|
+
}
|
|
221
|
+
|
|
172
222
|
function resolveAddonPath(): string {
|
|
173
223
|
return resolveAddonPathFrom(
|
|
174
224
|
resolveEnvVar('CASTRUM_NATIVE_LIBRARY_PATH', ['NAPI_RS_NATIVE_LIBRARY_PATH']),
|
|
@@ -57,6 +57,40 @@ export interface RouteInstance {
|
|
|
57
57
|
destroy?(): void
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The native `MetricsRegistry` class instance — sharded counters / gauges /
|
|
62
|
+
* histograms with a Prometheus text-format render. Series are declared once
|
|
63
|
+
* (`counter`/`gauge`/`histogram` return a reusable id); per-event updates
|
|
64
|
+
* take the id + the label values for that event.
|
|
65
|
+
*/
|
|
66
|
+
export interface MetricsRegistryInstance {
|
|
67
|
+
/** Declare a counter family → its series id (idempotent per shape). */
|
|
68
|
+
counter(name: string, labelKeys?: string[]): number
|
|
69
|
+
/** Declare a gauge family → its series id (idempotent per shape). */
|
|
70
|
+
gauge(name: string, labelKeys?: string[]): number
|
|
71
|
+
/**
|
|
72
|
+
* Declare a histogram family → its series id. Omitted/empty `buckets`
|
|
73
|
+
* selects the default latency buckets.
|
|
74
|
+
*/
|
|
75
|
+
histogram(name: string, labelKeys?: string[], buckets?: number[]): number
|
|
76
|
+
/** Counter/gauge `+= amount`, or histogram observe `amount`. */
|
|
77
|
+
record(series: number, values?: string[], amount?: number): void
|
|
78
|
+
/** Gauge assignment (`= value`). */
|
|
79
|
+
gaugeSet(series: number, values: string[] | undefined, value: number): void
|
|
80
|
+
/** Render the Prometheus text exposition format (deterministic order). */
|
|
81
|
+
render(): string
|
|
82
|
+
/**
|
|
83
|
+
* Packed v1 series snapshot bytes (families then series — see the Rust
|
|
84
|
+
* registry docs). bun:ffi-backed wrappers only; napi exposes
|
|
85
|
+
* `MetricsRegistry.snapshot()` directly.
|
|
86
|
+
*/
|
|
87
|
+
snapshot?(): Uint8Array
|
|
88
|
+
/** Number of live series (tests / introspection; napi transport only). */
|
|
89
|
+
seriesCount?(): number
|
|
90
|
+
/** Free the native handle (bun:ffi-backed wrappers only). */
|
|
91
|
+
destroy?(): void
|
|
92
|
+
}
|
|
93
|
+
|
|
60
94
|
/** A single JSON Schema validation error (fast path + DOM fallback). */
|
|
61
95
|
export interface SchemaError {
|
|
62
96
|
/** RFC 6901 JSON pointer to the failing instance value ("" = root). */
|
package/src/native/types.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
MediaTypeMatcherInstance,
|
|
26
26
|
MediaTypeParserInstance,
|
|
27
27
|
MediaTypeResult,
|
|
28
|
+
MetricsRegistryInstance,
|
|
28
29
|
MultipartPart,
|
|
29
30
|
PasswordHashOptions,
|
|
30
31
|
RateLimiterInstance,
|
|
@@ -145,6 +146,10 @@ export interface NativeAddon {
|
|
|
145
146
|
validateUuid(input: Uint8Array): boolean
|
|
146
147
|
validateIpv4(input: Uint8Array): boolean
|
|
147
148
|
validateIpv6(input: Uint8Array): boolean
|
|
149
|
+
/** Batch fixed-width hex validation → one verdict byte per newline-separated line. */
|
|
150
|
+
hexValidateBatch(input: Uint8Array, width: number): Uint8Array
|
|
151
|
+
/** Escape JS-RegExp metacharacters so a pattern matches the input literally. */
|
|
152
|
+
regexEscape(input: string): string
|
|
148
153
|
|
|
149
154
|
wsAcceptKey(key: Uint8Array): Uint8Array
|
|
150
155
|
wsAcceptKeyBatchPacked(input: Uint8Array): Uint8Array
|
|
@@ -244,6 +249,12 @@ export interface NativeAddon {
|
|
|
244
249
|
windowMs: number,
|
|
245
250
|
maxEntries?: number | null,
|
|
246
251
|
) => RateLimiterInstance
|
|
252
|
+
/**
|
|
253
|
+
* Sharded counters / gauges / histograms registry with a Prometheus
|
|
254
|
+
* text-format render (the napi fallback transport for the `castrum_metrics_*`
|
|
255
|
+
* C-ABI surface).
|
|
256
|
+
*/
|
|
257
|
+
MetricsRegistry: new () => MetricsRegistryInstance
|
|
247
258
|
|
|
248
259
|
wsFrameEncode(opcode: number, payload: Uint8Array, mask: boolean, fin: boolean): Uint8Array
|
|
249
260
|
wsFrameDecode(data: Uint8Array): WsFrame | null
|