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/runtime/codec.ts
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The UTF-8 codec branches (formerly in `src/shared/codec.ts`) are centralized
|
|
4
4
|
// here and selected ONCE at module load:
|
|
5
|
-
// - encode:
|
|
5
|
+
// - encode: shared `TextEncoder` singleton (Bun's native implementation
|
|
6
|
+
// measures ~3x faster than the old `Bun.ArrayBufferSink` start/write/flush
|
|
7
|
+
// path on Bun 1.4 — 34-50ns vs 188-215ns for small strings) vs `TextEncoder`
|
|
8
|
+
// under Node.
|
|
6
9
|
// - encodeInto: zero-copy `Buffer` view `.write` vs `TextEncoder.encodeInto`.
|
|
7
10
|
// - decode: `new CString(ptr, 0, len)` from `bun:ffi` vs `TextDecoder`.
|
|
8
11
|
// - decodeFatal: replacement-mode (Bun) vs fatal `TextDecoder` (Node).
|
|
@@ -36,7 +39,6 @@ function getNodeFatalDecoder(): TextDecoder {
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
// ── Bun codec ─────────────────────────────────────────────────────
|
|
39
|
-
let bunSink: Bun.ArrayBufferSink | null = null
|
|
40
42
|
let bunFfiCodec: typeof import('bun:ffi') | null | undefined
|
|
41
43
|
|
|
42
44
|
function getBunFfiCodec(): typeof import('bun:ffi') | null {
|
|
@@ -60,15 +62,13 @@ function createBunCodec(): Utf8Codec {
|
|
|
60
62
|
}
|
|
61
63
|
return getNodeDecoder().decode(bytes)
|
|
62
64
|
}
|
|
65
|
+
// Shared encoder singleton — Bun's native TextEncoder implementation
|
|
66
|
+
// allocates the exact-size buffer in-engine; measured ~3x faster than the
|
|
67
|
+
// previous ArrayBufferSink start/write/flush path on Bun 1.4 (see header).
|
|
68
|
+
const bunEncoder = new TextEncoder()
|
|
63
69
|
return {
|
|
64
70
|
encodeUtf8(s: string): Uint8Array {
|
|
65
|
-
|
|
66
|
-
const sink = bunSink
|
|
67
|
-
// `stream` resets the buffer on flush (reusable singleton); `flush()`
|
|
68
|
-
// returns the written data as a `Uint8Array`.
|
|
69
|
-
sink.start({ asUint8Array: true, stream: true })
|
|
70
|
-
sink.write(s)
|
|
71
|
-
return sink.flush() as Uint8Array
|
|
71
|
+
return bunEncoder.encode(s)
|
|
72
72
|
},
|
|
73
73
|
encodeUtf8Into(s: string, dest: Uint8Array, offset = 0): number {
|
|
74
74
|
return bunEncodeUtf8Into(s, dest, offset)
|
|
@@ -78,6 +78,7 @@ function createBunCodec(): Utf8Codec {
|
|
|
78
78
|
// `castrum_utf8_valid` probe (callers check it first via the transport's
|
|
79
79
|
// ffi surface). This fallback matches the historical behavior.
|
|
80
80
|
decodeUtf8Fatal: decodeUtf8Bun,
|
|
81
|
+
decodeUtf8Range: decodeUtf8RangeBun,
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -120,6 +121,39 @@ function bunEncodeUtf8Into(s: string, dest: Uint8Array, offset = 0): number {
|
|
|
120
121
|
return view.write(s, start, 'utf8')
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
// Shared cached full-backing view for the ranged DECODE fast path (same
|
|
125
|
+
// WeakMap pattern as the encode views above — one Buffer per ArrayBuffer,
|
|
126
|
+
// never per call).
|
|
127
|
+
const bunDecodeViewCache = new WeakMap<ArrayBufferLike, Buffer>()
|
|
128
|
+
|
|
129
|
+
function bunDecodeView(bytes: Uint8Array): Buffer {
|
|
130
|
+
const backing = bytes.buffer
|
|
131
|
+
let view = bunDecodeViewCache.get(backing)
|
|
132
|
+
if (view === undefined || view.byteLength !== backing.byteLength) {
|
|
133
|
+
view = Buffer.from(backing)
|
|
134
|
+
bunDecodeViewCache.set(backing, view)
|
|
135
|
+
}
|
|
136
|
+
return view
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Ranged decode: ASCII-only ranges take the latin1 `toString` path (JSC
|
|
141
|
+
* builds the string without the UTF-8 decoder machinery — measured ~2x the
|
|
142
|
+
* CString path on Bun 1.4); any high byte falls back to a real UTF-8 decode
|
|
143
|
+
* of exactly that range. Replacement-mode parity with `decodeUtf8` holds for
|
|
144
|
+
* both branches (latin1 is byte-identical to UTF-8 when no byte exceeds 0x7F).
|
|
145
|
+
*/
|
|
146
|
+
function decodeUtf8RangeBun(bytes: Uint8Array, start: number, end: number): string {
|
|
147
|
+
const view = bunDecodeView(bytes)
|
|
148
|
+
const base = bytes.byteOffset
|
|
149
|
+
for (let i = start; i < end; i++) {
|
|
150
|
+
if ((bytes[i] ?? 0) >= 0x80) {
|
|
151
|
+
return view.toString('utf8', base + start, base + end)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return view.toString('latin1', base + start, base + end)
|
|
155
|
+
}
|
|
156
|
+
|
|
123
157
|
// ── Node codec ────────────────────────────────────────────────────
|
|
124
158
|
function createNodeCodec(): Utf8Codec {
|
|
125
159
|
return {
|
|
@@ -135,6 +169,16 @@ function createNodeCodec(): Utf8Codec {
|
|
|
135
169
|
decodeUtf8Fatal(bytes: Uint8Array): string {
|
|
136
170
|
return getNodeFatalDecoder().decode(bytes)
|
|
137
171
|
},
|
|
172
|
+
decodeUtf8Range(bytes: Uint8Array, start: number, end: number): string {
|
|
173
|
+
// Node: ranged Buffer toString with the same ASCII fast path.
|
|
174
|
+
const view = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
175
|
+
for (let i = start; i < end; i++) {
|
|
176
|
+
if ((bytes[i] ?? 0) >= 0x80) {
|
|
177
|
+
return view.toString('utf8', start, end)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return view.toString('latin1', start, end)
|
|
181
|
+
},
|
|
138
182
|
}
|
|
139
183
|
}
|
|
140
184
|
|
package/src/runtime/types.ts
CHANGED
|
@@ -31,6 +31,13 @@ export interface Utf8Codec {
|
|
|
31
31
|
decodeUtf8(bytes: Uint8Array): string
|
|
32
32
|
/** Decode UTF-8 bytes to a string, throwing on invalid UTF-8. */
|
|
33
33
|
decodeUtf8Fatal(bytes: Uint8Array): string
|
|
34
|
+
/**
|
|
35
|
+
* Decode the byte RANGE `[start, end)` of `bytes` to a string (replacement
|
|
36
|
+
* mode on invalid). Zero-copy ranged decode for packed-wire unpackers —
|
|
37
|
+
* avoids the per-field `subarray` view allocation. ASCII-only ranges take
|
|
38
|
+
* a latin1 fast path (~2x on Bun); multi-byte ranges fall back to UTF-8.
|
|
39
|
+
*/
|
|
40
|
+
decodeUtf8Range(bytes: Uint8Array, start: number, end: number): string
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
/** UUIDv7 generation (Bun built-in vs crypto.randomUUID on Node). */
|
|
@@ -82,8 +82,7 @@ export function buildBatch(ctx: RustClientContext): RustBatch {
|
|
|
82
82
|
via(ctx, 'signCookieBatchPacked', unpackByteResults, [secret])(items),
|
|
83
83
|
verifyCookie: (items, secret) =>
|
|
84
84
|
via(ctx, 'verifyCookieBatchPacked', unpackBitset, [secret])(items),
|
|
85
|
-
csrfVerify: (items, secret) =>
|
|
86
|
-
via(ctx, 'csrfVerifyBatchPacked', unpackBitset, [secret])(items),
|
|
85
|
+
csrfVerify: (items, secret) => via(ctx, 'csrfVerifyBatchPacked', unpackBitset, [secret])(items),
|
|
87
86
|
httpParseRequest: via(ctx, 'httpParseRequestBatchPacked', unpackByteResults),
|
|
88
87
|
fnv1a64: via(ctx, 'fnv1A64BatchPacked', unpackU64ArrayAsBigInt),
|
|
89
88
|
etag: (items, weak) => via(ctx, 'etagBatchPacked', unpackByteResults, [weak ?? null])(items),
|
|
@@ -96,13 +95,18 @@ export function buildBatch(ctx: RustClientContext): RustBatch {
|
|
|
96
95
|
via(ctx, 'base64EncodeBatchPacked', unpackByteResults, [true, false])(items),
|
|
97
96
|
base64UrlDecode: (items) =>
|
|
98
97
|
via(ctx, 'base64DecodeBatchPacked', unpackByteResults, [true, false])(items),
|
|
99
|
-
jsonPatch: (docs, patches) =>
|
|
98
|
+
jsonPatch: (docs, patches) =>
|
|
99
|
+
via2(ctx, 'jsonPatchBatchPacked', unpackByteResults)(docs, patches),
|
|
100
100
|
hexEncode: via(ctx, 'hexEncodeBatchPacked', unpackByteResults),
|
|
101
101
|
hexDecode: via(ctx, 'hexDecodeBatchPacked', unpackByteResults),
|
|
102
102
|
base64Encode: (items, urlSafe, padding) =>
|
|
103
|
-
via(ctx, 'base64EncodeBatchPacked', unpackByteResults, [urlSafe ?? null, padding ?? null])(
|
|
103
|
+
via(ctx, 'base64EncodeBatchPacked', unpackByteResults, [urlSafe ?? null, padding ?? null])(
|
|
104
|
+
items,
|
|
105
|
+
),
|
|
104
106
|
base64Decode: (items, urlSafe, padding) =>
|
|
105
|
-
via(ctx, 'base64DecodeBatchPacked', unpackByteResults, [urlSafe ?? null, padding ?? null])(
|
|
107
|
+
via(ctx, 'base64DecodeBatchPacked', unpackByteResults, [urlSafe ?? null, padding ?? null])(
|
|
108
|
+
items,
|
|
109
|
+
),
|
|
106
110
|
schemaValidate(validator, items) {
|
|
107
111
|
return schemaValidateBatch(validator, items)
|
|
108
112
|
},
|
|
@@ -131,7 +135,8 @@ export function buildBatch(ctx: RustClientContext): RustBatch {
|
|
|
131
135
|
hmacSha256Verify: (items, sigs, key) =>
|
|
132
136
|
via2(ctx, 'hmacSha256VerifyBatchPacked', unpackBitset, [key])(items, sigs),
|
|
133
137
|
// ── Two-list ──
|
|
134
|
-
passwordVerify: (items, phcs) =>
|
|
138
|
+
passwordVerify: (items, phcs) =>
|
|
139
|
+
via2(ctx, 'passwordVerifyBatchPacked', unpackBitset)(items, phcs),
|
|
135
140
|
urlResolve: (bases, references) =>
|
|
136
141
|
via2(ctx, 'urlResolveBatchPacked', unpackByteResults)(bases, references),
|
|
137
142
|
templateRender: (source, contexts) =>
|
|
@@ -143,9 +148,11 @@ export function buildBatch(ctx: RustClientContext): RustBatch {
|
|
|
143
148
|
nowSeconds ?? Math.floor(Date.now() / 1000),
|
|
144
149
|
])(items),
|
|
145
150
|
sseEncode: (items, event, id, retry) =>
|
|
146
|
-
via(ctx, 'sseEncodeBatchPacked', unpackByteResults, [
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
via(ctx, 'sseEncodeBatchPacked', unpackByteResults, [
|
|
152
|
+
event ?? null,
|
|
153
|
+
id ?? null,
|
|
154
|
+
retry ?? null,
|
|
155
|
+
])(items),
|
|
149
156
|
wsFrameEncode: (items, opcode, mask, fin) =>
|
|
150
157
|
via(ctx, 'wsFrameEncodeBatchPacked', unpackByteResults, [opcode, mask, fin])(items),
|
|
151
158
|
wsFrameDecode: via(ctx, 'wsFrameDecodeBatchPacked', unpackByteResults),
|
package/src/rust-ffi/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ export type {
|
|
|
37
37
|
HmacSignerInstance,
|
|
38
38
|
MediaTypeParserInstance,
|
|
39
39
|
MediaTypeResult,
|
|
40
|
+
MetricsRegistryInstance,
|
|
40
41
|
MultipartPart,
|
|
41
42
|
PasswordHashOptions,
|
|
42
43
|
SchemaError,
|
|
@@ -46,20 +47,20 @@ export type {
|
|
|
46
47
|
WsFrame,
|
|
47
48
|
} from '../native'
|
|
48
49
|
export type { SchemaValidator } from '../shared/packed'
|
|
49
|
-
export type {
|
|
50
|
-
export type { RustClient } from './client'
|
|
51
|
-
export { createRust, rust } from './client'
|
|
52
|
-
export { proven } from './proven'
|
|
50
|
+
export type { ProvenEntry, ProvenImpl, ProvenStatus } from '../shared/proven'
|
|
53
51
|
export {
|
|
54
|
-
PROVEN_SELECTION,
|
|
55
52
|
isProven,
|
|
53
|
+
PROVEN_SELECTION,
|
|
56
54
|
provenEntry,
|
|
57
55
|
provenImpl,
|
|
58
56
|
provenStatus,
|
|
59
57
|
provenSummary,
|
|
60
58
|
provenSurface,
|
|
61
59
|
} from '../shared/proven'
|
|
62
|
-
export type {
|
|
60
|
+
export type { RustBatch } from './batch'
|
|
61
|
+
export type { RustClient } from './client'
|
|
62
|
+
export { createRust, rust } from './client'
|
|
63
63
|
export type { RustOptions } from './options'
|
|
64
64
|
export type { RustPacked } from './packed'
|
|
65
|
+
export { proven } from './proven'
|
|
65
66
|
export type { RustText } from './text'
|
package/src/rust-ffi/options.ts
CHANGED
|
@@ -50,9 +50,7 @@ export function resolveRayonThreads(explicit?: number): number {
|
|
|
50
50
|
return Math.max(1, Math.min(cores, Math.floor(explicit)))
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
const envThreads = Number(
|
|
54
|
-
resolveEnvVar('CASTRUM_RAYON_THREADS'),
|
|
55
|
-
)
|
|
53
|
+
const envThreads = Number(resolveEnvVar('CASTRUM_RAYON_THREADS'))
|
|
56
54
|
|
|
57
55
|
if (Number.isFinite(envThreads) && envThreads > 0) {
|
|
58
56
|
return Math.max(1, Math.min(cores, Math.floor(envThreads)))
|
package/src/rust-ffi/proven.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// live benchmark audit and no `@deprecated` JSDoc machinery (the old
|
|
9
9
|
// `check:proven` / `check:annotate` friction, intentionally not re-added).
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { type RustClient, rust } from './client'
|
|
12
12
|
|
|
13
13
|
/** The full `rust.*` surface — identical to `rust`; the proven wiring is baked (see {@link ../shared/proven}). */
|
|
14
14
|
export type ProvenClient = RustClient
|
|
@@ -16,13 +16,13 @@ export type ProvenClient = RustClient
|
|
|
16
16
|
/** The full `rust.*` surface, exposed for audit. Same object as `rust`. */
|
|
17
17
|
export const proven: ProvenClient = rust
|
|
18
18
|
|
|
19
|
+
export type { ProvenEntry, ProvenImpl, ProvenStatus } from '../shared/proven'
|
|
19
20
|
export {
|
|
20
|
-
PROVEN_SELECTION,
|
|
21
21
|
isProven,
|
|
22
|
+
PROVEN_SELECTION,
|
|
22
23
|
provenEntry,
|
|
23
24
|
provenImpl,
|
|
24
25
|
provenStatus,
|
|
25
26
|
provenSummary,
|
|
26
27
|
provenSurface,
|
|
27
28
|
} from '../shared/proven'
|
|
28
|
-
export type { ProvenEntry, ProvenImpl, ProvenStatus } from '../shared/proven'
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import type { PasswordHashOptions } from '../../native'
|
|
12
12
|
import { encoder } from '../../shared/bytes'
|
|
13
|
-
import { writeInto } from '../into'
|
|
14
13
|
import { memoizeFfi, type RustClientContext, resolveNative } from '../context'
|
|
14
|
+
import { writeInto } from '../into'
|
|
15
15
|
|
|
16
16
|
/** Auth / crypto scalar methods (`Pick<RustScalar, ...>`). */
|
|
17
17
|
export function buildCrypto(ctx: RustClientContext) {
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
JwtSignerInstance,
|
|
17
17
|
MediaTypeMatcherInstance,
|
|
18
18
|
MediaTypeParserInstance,
|
|
19
|
+
MetricsRegistryInstance,
|
|
19
20
|
PasswordHashOptions,
|
|
20
21
|
RateLimiterInstance,
|
|
21
22
|
SchemaValidatorInstance,
|
|
@@ -329,6 +330,71 @@ function ffiFormParser(ffi: BunFFI): FormParserInstance {
|
|
|
329
330
|
}
|
|
330
331
|
}
|
|
331
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Caller-owned-handle `MetricsRegistry`: on Bun the whole surface runs through
|
|
335
|
+
* the C-ABI (`castrum_metrics_*`) with a native handle — no napi instance is
|
|
336
|
+
* involved at all (the route-stack ownership model). Label values cross as a
|
|
337
|
+
* JOINED `\u001f` string via the cstring-ARG `_str` symbols: the engine
|
|
338
|
+
* transcodes them in-engine, so the per-event JS cost is just
|
|
339
|
+
* `values.join('\u001f')` — ZERO TextEncoder work (Bun 1.4 zero-copy text).
|
|
340
|
+
*/
|
|
341
|
+
function ffiMetricsRegistry(ffi: BunFFI): MetricsRegistryInstance {
|
|
342
|
+
const handle = ffi.metricsCreate()
|
|
343
|
+
return {
|
|
344
|
+
counter(name, labelKeys) {
|
|
345
|
+
return ffi.metricsCounter(handle, name, (labelKeys ?? []).join('\u001f'))
|
|
346
|
+
},
|
|
347
|
+
gauge(name, labelKeys) {
|
|
348
|
+
return ffi.metricsGauge(handle, name, (labelKeys ?? []).join('\u001f'))
|
|
349
|
+
},
|
|
350
|
+
histogram(name, labelKeys, buckets) {
|
|
351
|
+
return ffi.metricsHistogram(
|
|
352
|
+
handle,
|
|
353
|
+
name,
|
|
354
|
+
(labelKeys ?? []).join('\u001f'),
|
|
355
|
+
(buckets ?? []).join(','),
|
|
356
|
+
)
|
|
357
|
+
},
|
|
358
|
+
record(series, values, amount) {
|
|
359
|
+
const joined = (values ?? []).join('\u001f')
|
|
360
|
+
if (!ffi.metricsRecordStr(handle, series, joined, amount ?? 1)) {
|
|
361
|
+
throw new Error('metrics record: unknown series / arity mismatch / invalid amount')
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
gaugeSet(series, values, value) {
|
|
365
|
+
const joined = (values ?? []).join('\u001f')
|
|
366
|
+
if (!ffi.metricsGaugeSetStr(handle, series, joined, value)) {
|
|
367
|
+
throw new Error('metrics gauge set: unknown series / arity mismatch')
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
render() {
|
|
371
|
+
// Probe large enough for typical registries (one native pass), then
|
|
372
|
+
// grow exactly once if ever exceeded (needed-size convention).
|
|
373
|
+
let out = new Uint8Array(8192)
|
|
374
|
+
let w = ffi.metricsRender(handle, out)
|
|
375
|
+
if (w > out.length) {
|
|
376
|
+
out = new Uint8Array(w)
|
|
377
|
+
w = ffi.metricsRender(handle, out)
|
|
378
|
+
}
|
|
379
|
+
return decodeUtf8(out.subarray(0, w))
|
|
380
|
+
},
|
|
381
|
+
destroy() {
|
|
382
|
+
ffi.metricsDestroy(handle)
|
|
383
|
+
},
|
|
384
|
+
snapshot() {
|
|
385
|
+
// Packed v1 dump — decoded by consumers (the @ignex/native metrics
|
|
386
|
+
// wrapper); probe large, grow exactly once.
|
|
387
|
+
let out = new Uint8Array(4096)
|
|
388
|
+
let w = ffi.metricsSnapshot(handle, out)
|
|
389
|
+
if (w > out.length) {
|
|
390
|
+
out = new Uint8Array(w)
|
|
391
|
+
w = ffi.metricsSnapshot(handle, out)
|
|
392
|
+
}
|
|
393
|
+
return out.subarray(0, w)
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
332
398
|
/** Compiled-once factory + runtime-control methods (`Pick<RustScalar, ...>`). */
|
|
333
399
|
export function buildFactories(ctx: RustClientContext) {
|
|
334
400
|
const { transport } = ctx.runtime
|
|
@@ -450,6 +516,13 @@ export function buildFactories(ctx: RustClientContext) {
|
|
|
450
516
|
}
|
|
451
517
|
return new addon.RateLimiter(limit, windowMs, maxEntries ?? undefined)
|
|
452
518
|
},
|
|
519
|
+
createMetricsRegistry(): MetricsRegistryInstance {
|
|
520
|
+
// Fully C-ABI-backed on Bun (native handle owned by the wrapper — no
|
|
521
|
+
// napi instance involved); napi class on Node / fallback.
|
|
522
|
+
const ffi = transport.ffi
|
|
523
|
+
if (ffi) return ffiMetricsRegistry(ffi)
|
|
524
|
+
return new addon.MetricsRegistry()
|
|
525
|
+
},
|
|
453
526
|
initThreadPool(threads?: number): void {
|
|
454
527
|
// Explicit user call also establishes the pool state locally.
|
|
455
528
|
ctx.markPoolInitialized()
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// (bun:ffi first, napi fallback) — no inline `isBun()` / `getBunFFI()`.
|
|
10
10
|
|
|
11
11
|
import { toBytes } from '../../shared/bytes'
|
|
12
|
-
import { writeInto } from '../into'
|
|
13
12
|
import { memoizeFfi, type RustClientContext, resolveNative } from '../context'
|
|
13
|
+
import { writeInto } from '../into'
|
|
14
14
|
import { asBigInt, asNumber } from '../options'
|
|
15
15
|
|
|
16
16
|
/** Hashing scalar methods (`Pick<RustScalar, ...>`). */
|
|
@@ -11,11 +11,18 @@
|
|
|
11
11
|
|
|
12
12
|
import type { EncodingPrefResult, MediaTypeResult } from '../../native'
|
|
13
13
|
import type { BunFFI } from '../../native/ffi'
|
|
14
|
-
import { decoder } from '../../shared/bytes'
|
|
15
|
-
import { decodeUtf8Fatal } from '../../shared/codec'
|
|
14
|
+
import { decoder, hasNul } from '../../shared/bytes'
|
|
15
|
+
import { decodeUtf8Fatal, decodeUtf8Range } from '../../shared/codec'
|
|
16
16
|
import { packPairs } from '../../shared/packed'
|
|
17
|
-
import { writeInto } from '../into'
|
|
18
17
|
import { memoizeFfi, type RustClientContext, resolveNative } from '../context'
|
|
18
|
+
import { writeInto } from '../into'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* RFC 6455 `Sec-WebSocket-Accept` is always 28 chars. Reused across calls
|
|
22
|
+
* because the value is decoded to a string synchronously before returning —
|
|
23
|
+
* the same discipline as the other pooled scratches in this layer.
|
|
24
|
+
*/
|
|
25
|
+
const WS_ACCEPT_SCRATCH = new Uint8Array(64)
|
|
19
26
|
|
|
20
27
|
// Mirror napi `url_decode`'s UTF-8 validation (rust/http/url_codec.rs): only
|
|
21
28
|
// validate when a high byte is present (the ASCII fast path skips it — same as
|
|
@@ -60,23 +67,25 @@ function u32LE(b: Uint8Array, off: number): number {
|
|
|
60
67
|
* napi `HashMap` (which keeps every param including charset/boundary).
|
|
61
68
|
*/
|
|
62
69
|
function unpackMediaType(packed: Uint8Array): MediaTypeResult {
|
|
70
|
+
// Ranged decode straight off the (pooled) packed buffer — no per-field
|
|
71
|
+
// subarray views. ASCII fields take the latin1 fast path.
|
|
63
72
|
let off = 0
|
|
64
73
|
const mtLen = u32LE(packed, off)
|
|
65
74
|
off += 4
|
|
66
|
-
const mediaType =
|
|
75
|
+
const mediaType = decodeUtf8Range(packed, off, off + mtLen)
|
|
67
76
|
off += mtLen
|
|
68
77
|
let charset: string | null = null
|
|
69
78
|
const csLen = u32LE(packed, off)
|
|
70
79
|
off += 4
|
|
71
80
|
if (csLen !== 0xffffffff) {
|
|
72
|
-
charset =
|
|
81
|
+
charset = decodeUtf8Range(packed, off, off + csLen)
|
|
73
82
|
off += csLen
|
|
74
83
|
}
|
|
75
84
|
let boundary: string | null = null
|
|
76
85
|
const bLen = u32LE(packed, off)
|
|
77
86
|
off += 4
|
|
78
87
|
if (bLen !== 0xffffffff) {
|
|
79
|
-
boundary =
|
|
88
|
+
boundary = decodeUtf8Range(packed, off, off + bLen)
|
|
80
89
|
off += bLen
|
|
81
90
|
}
|
|
82
91
|
const count = u32LE(packed, off)
|
|
@@ -85,11 +94,11 @@ function unpackMediaType(packed: Uint8Array): MediaTypeResult {
|
|
|
85
94
|
for (let i = 0; i < count; i++) {
|
|
86
95
|
const kLen = u32LE(packed, off)
|
|
87
96
|
off += 4
|
|
88
|
-
const key =
|
|
97
|
+
const key = decodeUtf8Range(packed, off, off + kLen)
|
|
89
98
|
off += kLen
|
|
90
99
|
const vLen = u32LE(packed, off)
|
|
91
100
|
off += 4
|
|
92
|
-
const val =
|
|
101
|
+
const val = decodeUtf8Range(packed, off, off + vLen)
|
|
93
102
|
off += vLen
|
|
94
103
|
params[key] = val
|
|
95
104
|
}
|
|
@@ -97,18 +106,22 @@ function unpackMediaType(packed: Uint8Array): MediaTypeResult {
|
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
/** Unpack the `castrum_parse_accept_encoding` verdict (f32 q-values) into the
|
|
100
|
-
* napi-shaped array. Layout: `[u32 count]{[u32 encLen][enc][f32 q][u32 order]}`.
|
|
109
|
+
* napi-shaped array. Layout: `[u32 count]{[u32 encLen][enc][f32 q][u32 order]}`.
|
|
110
|
+
* Ranged decode off the pooled buffer + shared f32 bit-reinterpret views —
|
|
111
|
+
* no per-field subarrays, no per-call DataView. */
|
|
112
|
+
const f32Bits = new Float32Array(1)
|
|
113
|
+
const f32BitsU32 = new Uint32Array(f32Bits.buffer)
|
|
101
114
|
function unpackAcceptEncoding(packed: Uint8Array): EncodingPrefResult[] {
|
|
102
115
|
const count = u32LE(packed, 0)
|
|
103
116
|
let off = 4
|
|
104
117
|
const out: EncodingPrefResult[] = []
|
|
105
|
-
const view = new DataView(packed.buffer, packed.byteOffset, packed.byteLength)
|
|
106
118
|
for (let i = 0; i < count; i++) {
|
|
107
119
|
const encLen = u32LE(packed, off)
|
|
108
120
|
off += 4
|
|
109
|
-
const encoding =
|
|
121
|
+
const encoding = decodeUtf8Range(packed, off, off + encLen)
|
|
110
122
|
off += encLen
|
|
111
|
-
|
|
123
|
+
f32BitsU32[0] = u32LE(packed, off)
|
|
124
|
+
const q: number = f32Bits[0] ?? 0
|
|
112
125
|
off += 4
|
|
113
126
|
const order = u32LE(packed, off)
|
|
114
127
|
off += 4
|
|
@@ -138,11 +151,22 @@ export function buildHttp(ctx: RustClientContext) {
|
|
|
138
151
|
|
|
139
152
|
return {
|
|
140
153
|
mimeFromExtension(ext: Uint8Array): Uint8Array | string {
|
|
141
|
-
// FFI-first
|
|
142
|
-
//
|
|
143
|
-
//
|
|
154
|
+
// FFI-first. This op has no `*_bytes` sibling, so the bytes must be decoded
|
|
155
|
+
// for the `cstring` ARG — the decode is the price of the cstring form, not
|
|
156
|
+
// a speed trick (route new byte-shaped ops to a byte sibling instead; see
|
|
157
|
+
// docs/FFI_BUN_GUIDE.md §6.1). napi keeps the memoized-bytes path
|
|
158
|
+
// (cachedMime).
|
|
159
|
+
//
|
|
160
|
+
// A NUL would truncate the DECODED string native-side, so `.js\0evil`
|
|
161
|
+
// would resolve as `.js` — a Content-Type from a prefix. An extension
|
|
162
|
+
// containing a NUL is not an extension, so it takes the unknown sentinel
|
|
163
|
+
// (the guard is `indexOf` on an already-decoded string: ~4 ns).
|
|
144
164
|
const f = ffi()
|
|
145
|
-
if (f)
|
|
165
|
+
if (f) {
|
|
166
|
+
const name = decoder.decode(ext)
|
|
167
|
+
if (hasNul(name)) return 'application/octet-stream'
|
|
168
|
+
return f.mimeFromExtension(name) ?? 'application/octet-stream'
|
|
169
|
+
}
|
|
146
170
|
return ctx.cachedMime(ext)
|
|
147
171
|
},
|
|
148
172
|
urlEncode(input: Uint8Array): Uint8Array | string {
|
|
@@ -182,34 +206,54 @@ export function buildHttp(ctx: RustClientContext) {
|
|
|
182
206
|
return n('urlDecodeInto')(input, output) as number
|
|
183
207
|
},
|
|
184
208
|
validateEmail(input: Uint8Array): boolean {
|
|
185
|
-
// FFI-first
|
|
186
|
-
//
|
|
187
|
-
// `
|
|
188
|
-
//
|
|
209
|
+
// FFI-first. The bytes cross as a `buffer`/`buffer_length` pair (an atomic
|
|
210
|
+
// ptr+len snapshot, zero transcode) rather than being decoded to a JS
|
|
211
|
+
// string for the `cstring` ARG. Measured on Bun 1.4.2 over 400k calls:
|
|
212
|
+
// 101 ns vs 234 ns (email), 65 vs 164 (uuid), 31 vs 138 (ipv4) — and the
|
|
213
|
+
// byte form cannot truncate at an embedded NUL the way a cstring ARG
|
|
214
|
+
// does (a NUL byte used to make "a@b.com\0<script>" validate as TRUE).
|
|
189
215
|
const f = ffi()
|
|
190
|
-
if (f) return f.
|
|
216
|
+
if (f) return f.validateEmailBytes(input)
|
|
191
217
|
return n('validateEmail')(input) as boolean
|
|
192
218
|
},
|
|
193
219
|
validateUuid(input: Uint8Array): boolean {
|
|
194
220
|
const f = ffi()
|
|
195
|
-
if (f) return f.
|
|
221
|
+
if (f) return f.validateUuidBytes(input)
|
|
196
222
|
return n('validateUuid')(input) as boolean
|
|
197
223
|
},
|
|
198
224
|
validateIpv4(input: Uint8Array): boolean {
|
|
199
225
|
const f = ffi()
|
|
200
|
-
if (f) return f.
|
|
226
|
+
if (f) return f.validateIpv4Bytes(input)
|
|
201
227
|
return n('validateIpv4')(input) as boolean
|
|
202
228
|
},
|
|
203
229
|
validateIpv6(input: Uint8Array): boolean {
|
|
204
230
|
const f = ffi()
|
|
205
|
-
if (f) return f.
|
|
231
|
+
if (f) return f.validateIpv6Bytes(input)
|
|
206
232
|
return n('validateIpv6')(input) as boolean
|
|
207
233
|
},
|
|
208
234
|
wsAcceptKey(key: Uint8Array): Uint8Array | string {
|
|
209
|
-
// FFI-first: the `cstring` ARG is
|
|
210
|
-
//
|
|
235
|
+
// FFI-first: the `cstring` ARG + cstring return is the fast path (no JS
|
|
236
|
+
// encode, engine-cloned result) — end-to-end it measures ~367 ns, while
|
|
237
|
+
// routing every call through the byte sibling costs ~381 ns (the extra
|
|
238
|
+
// `subarray` + decode outweighs the transcode saved; measured over 300k
|
|
239
|
+
// calls on Bun 1.4.2 through THIS function, not the raw binding).
|
|
240
|
+
//
|
|
241
|
+
// Correctness rider: a NUL would truncate the `cstring` ARG, so the accept
|
|
242
|
+
// value would be computed for a PREFIX of the key — verified:
|
|
243
|
+
// `'dGhlIHNhbXBsZSBub25jZQ==\0junk'` returned exactly the value for the
|
|
244
|
+
// key WITHOUT the suffix. That case takes the byte sibling (exact length).
|
|
211
245
|
const f = ffi()
|
|
212
|
-
if (f)
|
|
246
|
+
if (f) {
|
|
247
|
+
const text = decoder.decode(key)
|
|
248
|
+
if (!hasNul(text)) return f.wsAcceptKey(text)
|
|
249
|
+
const w = f.wsAcceptKeyInto(key, WS_ACCEPT_SCRATCH)
|
|
250
|
+
if (w > 0 && w <= WS_ACCEPT_SCRATCH.length) {
|
|
251
|
+
return decoder.decode(WS_ACCEPT_SCRATCH.subarray(0, w))
|
|
252
|
+
}
|
|
253
|
+
// Unreachable for a live view (the native side writes 28 for any key);
|
|
254
|
+
// the napi path is byte-exact, so it can never resurrect truncation.
|
|
255
|
+
return n('wsAcceptKey')(key) as Uint8Array | string
|
|
256
|
+
}
|
|
213
257
|
return n('wsAcceptKey')(key) as Uint8Array | string
|
|
214
258
|
},
|
|
215
259
|
wsAcceptKeyInto(key: Uint8Array, output: Uint8Array): number {
|
|
@@ -15,6 +15,7 @@ import { buildHashing } from './hashing'
|
|
|
15
15
|
import { buildHttp } from './http'
|
|
16
16
|
import { buildJson } from './json'
|
|
17
17
|
import { buildPayload } from './payload'
|
|
18
|
+
import { buildValidation } from './validation'
|
|
18
19
|
|
|
19
20
|
export type { RustScalar } from './interface'
|
|
20
21
|
|
|
@@ -26,6 +27,7 @@ export function buildScalar(ctx: RustClientContext): import('./interface').RustS
|
|
|
26
27
|
...buildHttp(ctx),
|
|
27
28
|
...buildCrypto(ctx),
|
|
28
29
|
...buildPayload(ctx),
|
|
30
|
+
...buildValidation(ctx),
|
|
29
31
|
...buildFactories(ctx),
|
|
30
32
|
}
|
|
31
33
|
}
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
MediaTypeMatcherInstance,
|
|
20
20
|
MediaTypeParserInstance,
|
|
21
21
|
MediaTypeResult,
|
|
22
|
+
MetricsRegistryInstance,
|
|
22
23
|
MultipartPart,
|
|
23
24
|
PasswordHashOptions,
|
|
24
25
|
RateLimiterInstance,
|
|
@@ -67,6 +68,18 @@ export interface RustScalar {
|
|
|
67
68
|
validateUuid(input: Uint8Array): boolean
|
|
68
69
|
validateIpv4(input: Uint8Array): boolean
|
|
69
70
|
validateIpv6(input: Uint8Array): boolean
|
|
71
|
+
/**
|
|
72
|
+
* Batch fixed-width hex validation: NEWLINE-separated lines in → one
|
|
73
|
+
* verdict byte (1/0) per line. One native call replaces per-item
|
|
74
|
+
* `/^[0-9a-f]{width}$/` regex loops (ObjectIds = width 24). Accepts bytes,
|
|
75
|
+
* a joined string, or an array of id strings (joined internally).
|
|
76
|
+
*/
|
|
77
|
+
hexValidateBatch(input: Uint8Array | readonly string[], width: number): Uint8Array
|
|
78
|
+
/**
|
|
79
|
+
* Escape JS-RegExp metacharacters so `new RegExp(escaped, flags)` matches
|
|
80
|
+
* the input literally — the safe literal-substring-search primitive.
|
|
81
|
+
*/
|
|
82
|
+
regexEscape(input: Uint8Array | string): string
|
|
70
83
|
wsAcceptKey(key: Uint8Array): Uint8Array | string
|
|
71
84
|
wsAcceptKeyInto(key: Uint8Array, output: Uint8Array): number
|
|
72
85
|
|
|
@@ -212,6 +225,13 @@ export interface RustScalar {
|
|
|
212
225
|
windowMs: number,
|
|
213
226
|
maxEntries?: number | null,
|
|
214
227
|
): RateLimiterInstance
|
|
228
|
+
/**
|
|
229
|
+
* Create a sharded metrics registry (counters / gauges / histograms +
|
|
230
|
+
* Prometheus render). Fully C-ABI-backed on Bun (caller-owned native
|
|
231
|
+
* handle); the napi class on Node. Call `destroy()` on ffi-backed
|
|
232
|
+
* registries at shutdown.
|
|
233
|
+
*/
|
|
234
|
+
createMetricsRegistry(): MetricsRegistryInstance
|
|
215
235
|
initThreadPool(rayonThreads?: number): void
|
|
216
236
|
rayonNumThreads(): number
|
|
217
237
|
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import type { MultipartPart, WsFrame } from '../../native'
|
|
12
12
|
import { decoder } from '../../shared/bytes'
|
|
13
|
-
import { writeInto } from '../into'
|
|
14
13
|
import { memoizeFfi, type RustClientContext, resolveNative } from '../context'
|
|
14
|
+
import { writeInto } from '../into'
|
|
15
15
|
|
|
16
16
|
/** Read an unsigned little-endian u32 at `off`. */
|
|
17
17
|
function u32LE(b: Uint8Array, off: number): number {
|
|
@@ -102,7 +102,11 @@ export function buildPayload(ctx: RustClientContext) {
|
|
|
102
102
|
// it via the `_into` core; keeps the 64 MiB decompression-bomb cap).
|
|
103
103
|
const f = ffi()
|
|
104
104
|
if (f) return f.gzipDecompressInto(data, output, maxDecompressed ?? undefined)
|
|
105
|
-
return writeInto(
|
|
105
|
+
return writeInto(
|
|
106
|
+
'gzip decompress',
|
|
107
|
+
output,
|
|
108
|
+
addon.gzipDecompress(data, maxDecompressed ?? null),
|
|
109
|
+
)
|
|
106
110
|
},
|
|
107
111
|
brotliCompress(data: Uint8Array, quality?: number | null): Uint8Array {
|
|
108
112
|
const f = ffi()
|
|
@@ -127,7 +131,11 @@ export function buildPayload(ctx: RustClientContext) {
|
|
|
127
131
|
// Pooled sibling — caller-owned output buffer (keeps the 64 MiB cap).
|
|
128
132
|
const f = ffi()
|
|
129
133
|
if (f) return f.brotliDecompressInto(data, output, maxDecompressed ?? undefined)
|
|
130
|
-
return writeInto(
|
|
134
|
+
return writeInto(
|
|
135
|
+
'brotli decompress',
|
|
136
|
+
output,
|
|
137
|
+
addon.brotliDecompress(data, maxDecompressed ?? null),
|
|
138
|
+
)
|
|
131
139
|
},
|
|
132
140
|
multipartParse(body: Uint8Array, boundary: Uint8Array): MultipartPart[] {
|
|
133
141
|
// FFI-first: packed parts (castrum_multipart_parse_packed) → unpack to
|