castrum 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +322 -59
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +3161 -0
- package/dist/index.js +7306 -0
- package/docs/ARCHITECTURE.md +415 -0
- package/docs/BENCHMARKS.md +307 -0
- package/docs/ENVIRONMENT.md +79 -0
- package/docs/GETTING_STARTED.md +209 -0
- package/docs/INGRESS.md +745 -0
- package/docs/REPO_MAP.md +259 -0
- package/docs/adr/0001-two-wire-formats.md +39 -0
- package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
- package/docs/adr/0003-bun-builtins-delegation.md +42 -0
- package/docs/bun-builtins-decision-matrix.md +108 -0
- package/index.ts +51 -8
- package/package.json +73 -20
- package/src/data/json-rows.ts +13 -13
- package/src/ingress/body.ts +149 -0
- package/src/ingress/constants.ts +220 -48
- package/src/ingress/context.ts +136 -0
- package/src/ingress/decode/baked-result.ts +121 -0
- package/src/ingress/decode/fast-result.ts +160 -0
- package/src/ingress/decode/packed-sections.ts +56 -0
- package/src/ingress/decode/result-base.ts +172 -0
- package/src/ingress/errors.ts +72 -0
- package/src/ingress/fast.ts +152 -1021
- package/src/ingress/handlers.ts +563 -1001
- package/src/ingress/headers/baked-templates.ts +102 -0
- package/src/ingress/headers/cors.ts +44 -0
- package/src/ingress/headers/fast-templates.ts +238 -0
- package/src/ingress/headers/hsts.ts +83 -0
- package/src/ingress/health.ts +67 -0
- package/src/ingress/index.ts +130 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +75 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +150 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +96 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/routes/common.ts +38 -0
- package/src/ingress/routes/delete.ts +14 -0
- package/src/ingress/routes/echo.ts +107 -0
- package/src/ingress/routes/fallback.ts +23 -0
- package/src/ingress/routes/head.ts +37 -0
- package/src/ingress/routes/index.ts +10 -0
- package/src/ingress/routes/json-write.ts +153 -0
- package/src/ingress/routes/options.ts +36 -0
- package/src/ingress/routes/read.ts +45 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +443 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +157 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +217 -0
- package/src/integration/streaming.ts +79 -0
- package/src/integration/websocket.ts +92 -0
- package/src/loader/batch.ts +133 -0
- package/src/loader/cost.ts +104 -0
- package/src/loader/index.ts +930 -0
- package/src/loader/ops.ts +657 -0
- package/src/native/ffi.ts +1541 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +194 -0
- package/src/native/types.ts +598 -0
- package/src/rust-ffi/addon.ts +10 -0
- package/src/rust-ffi/batch.ts +431 -0
- package/src/rust-ffi/client.ts +88 -0
- package/src/rust-ffi/context.ts +164 -0
- package/src/rust-ffi/index.ts +40 -541
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +42 -0
- package/src/rust-ffi/scalar/crypto.ts +265 -0
- package/src/rust-ffi/scalar/factories.ts +233 -0
- package/src/rust-ffi/scalar/hashing.ts +72 -0
- package/src/rust-ffi/scalar/http.ts +206 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +450 -0
- package/src/rust-ffi/scalar/json.ts +45 -0
- package/src/rust-ffi/scalar/payload.ts +124 -0
- package/src/rust-ffi/text.ts +95 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +77 -0
- package/src/shared/adaptive.ts +68 -0
- package/src/shared/bench-classify.ts +209 -0
- package/src/shared/buffer-pool.ts +209 -0
- package/src/shared/bytes.ts +35 -5
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +74 -0
- package/src/shared/metrics.ts +247 -0
- package/src/shared/packed.ts +350 -154
- package/src/shared/proven.ts +596 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +34 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +31 -0
- package/Cargo.toml +0 -73
- package/bench.ts +0 -3
- package/rust/batch.rs +0 -195
- package/rust/cookie_parser.rs +0 -70
- package/rust/cors.rs +0 -263
- package/rust/hashing.rs +0 -164
- package/rust/headers.rs +0 -234
- package/rust/hmac_sha256.rs +0 -113
- package/rust/http_parser.rs +0 -132
- package/rust/ingress.rs +0 -616
- package/rust/ingress_constants.rs +0 -89
- package/rust/ip_trust.rs +0 -185
- package/rust/json_ops.rs +0 -66
- package/rust/json_patch_ops.rs +0 -25
- package/rust/json_schema.rs +0 -151
- package/rust/json_ser.rs +0 -429
- package/rust/lib.rs +0 -48
- package/rust/method.rs +0 -141
- package/rust/mime_lookup.rs +0 -76
- package/rust/output.rs +0 -238
- package/rust/proxy.rs +0 -90
- package/rust/query_parser.rs +0 -141
- package/rust/random_token.rs +0 -52
- package/rust/rate_limit.rs +0 -203
- package/rust/terminal.rs +0 -95
- package/rust/unit_tests.rs +0 -843
- package/rust/url_codec.rs +0 -133
- package/rust/util.rs +0 -537
- package/rust/validation.rs +0 -107
- package/rust/websocket.rs +0 -23
- package/src/baseline/index.ts +0 -12
- package/src/baseline/tasks/cookie.ts +0 -22
- package/src/baseline/tasks/hashing.ts +0 -18
- package/src/baseline/tasks/hmac.ts +0 -38
- package/src/baseline/tasks/http.ts +0 -62
- package/src/baseline/tasks/json-patch.ts +0 -12
- package/src/baseline/tasks/json.ts +0 -50
- package/src/baseline/tasks/mime.ts +0 -5
- package/src/baseline/tasks/query.ts +0 -14
- package/src/baseline/tasks/token.ts +0 -15
- package/src/baseline/tasks/url.ts +0 -11
- package/src/baseline/tasks/validation.ts +0 -19
- package/src/baseline/tasks/websocket.ts +0 -14
- package/src/bench/assert.ts +0 -70
- package/src/bench/checks.ts +0 -227
- package/src/bench/checksum.ts +0 -27
- package/src/bench/comparisons.ts +0 -70
- package/src/bench/concurrent-worker.ts +0 -135
- package/src/bench/fixtures.ts +0 -233
- package/src/bench/index.ts +0 -2
- package/src/bench/measure.ts +0 -177
- package/src/bench/now.ts +0 -3
- package/src/bench/report.ts +0 -79
- package/src/bench/run.ts +0 -57
- package/src/bench/tasks/complex.ts +0 -232
- package/src/bench/tasks/concurrent.ts +0 -213
- package/src/bench/tasks/cookie.ts +0 -44
- package/src/bench/tasks/hashing.ts +0 -33
- package/src/bench/tasks/hmac.ts +0 -36
- package/src/bench/tasks/http.ts +0 -50
- package/src/bench/tasks/index.ts +0 -52
- package/src/bench/tasks/json-patch.ts +0 -21
- package/src/bench/tasks/json.ts +0 -33
- package/src/bench/tasks/mime.ts +0 -21
- package/src/bench/tasks/query.ts +0 -44
- package/src/bench/tasks/stress.ts +0 -142
- package/src/bench/tasks/token.ts +0 -20
- package/src/bench/tasks/url.ts +0 -34
- package/src/bench/tasks/validation.ts +0 -57
- package/src/bench/tasks/websocket.ts +0 -21
- package/src/bench/types.ts +0 -60
- package/src/ingress/packed-input.ts +0 -59
- package/src/shared/json.ts +0 -18
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// src/rust-ffi/batch.ts — Array-of-bytes FFI namespace.
|
|
2
|
+
//
|
|
3
|
+
// `rust.batch.*` packs an array of byte slices into the packed wire format,
|
|
4
|
+
// calls the native batch function, and unpacks the result into the friendly
|
|
5
|
+
// JS shape (bitsets, u32 arrays, i64 arrays, byte results).
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
schemaValidateBatch,
|
|
9
|
+
schemaValidateBatchCount,
|
|
10
|
+
unpackBitset,
|
|
11
|
+
unpackByteResults,
|
|
12
|
+
unpackI64ArrayAsBigInt,
|
|
13
|
+
unpackMultipartParts,
|
|
14
|
+
unpackU32Array,
|
|
15
|
+
unpackU64ArrayAsBigInt,
|
|
16
|
+
withPackScratch,
|
|
17
|
+
withPackScratch2,
|
|
18
|
+
type SchemaValidator,
|
|
19
|
+
} from '../shared/packed'
|
|
20
|
+
import { resolvePoolNative, type RustClientContext } from './context'
|
|
21
|
+
import type { MultipartPart, PasswordHashOptions } from '../native'
|
|
22
|
+
|
|
23
|
+
/** Array-of-bytes FFI namespace. */
|
|
24
|
+
export interface RustBatch {
|
|
25
|
+
jsonValid(items: Uint8Array[]): Uint8Array
|
|
26
|
+
crc32(items: Uint8Array[]): Uint32Array
|
|
27
|
+
validateEmail(items: Uint8Array[]): Uint8Array
|
|
28
|
+
validateUuid(items: Uint8Array[]): Uint8Array
|
|
29
|
+
validateIpv4(items: Uint8Array[]): Uint8Array
|
|
30
|
+
validateIpv6(items: Uint8Array[]): Uint8Array
|
|
31
|
+
jsonSumIds(items: Uint8Array[]): BigInt64Array
|
|
32
|
+
queryParse(items: Uint8Array[]): Uint8Array[]
|
|
33
|
+
cookieParse(items: Uint8Array[]): Uint8Array[]
|
|
34
|
+
httpParseRequest(items: Uint8Array[]): Uint8Array[]
|
|
35
|
+
/** Apply N RFC 6902 patches (docs and patches zipped). Fail-fast on error. */
|
|
36
|
+
jsonPatch(docs: Uint8Array[], patches: Uint8Array[]): Uint8Array[]
|
|
37
|
+
/** Encode N items to lowercase hex (packed in → byte results). */
|
|
38
|
+
hexEncode(items: Uint8Array[]): Uint8Array[]
|
|
39
|
+
/** Decode N lowercase/uppercase hex strings to bytes. */
|
|
40
|
+
hexDecode(items: Uint8Array[]): Uint8Array[]
|
|
41
|
+
/** Base64-encode N items (standard by default; url-safe/padding configurable). */
|
|
42
|
+
base64Encode(items: Uint8Array[], urlSafe?: boolean, padding?: boolean): Uint8Array[]
|
|
43
|
+
/** Base64-decode N items (standard by default; url-safe/padding configurable). */
|
|
44
|
+
base64Decode(items: Uint8Array[], urlSafe?: boolean, padding?: boolean): Uint8Array[]
|
|
45
|
+
formParse(items: Uint8Array[]): Uint8Array[]
|
|
46
|
+
/** Sign N cookie values (packed → byte results). */
|
|
47
|
+
signCookie(items: Uint8Array[], secret: Uint8Array): Uint8Array[]
|
|
48
|
+
/** Verify N signed cookies (bitset of valid). */
|
|
49
|
+
verifyCookie(items: Uint8Array[], secret: Uint8Array): Uint8Array
|
|
50
|
+
/** Verify N CSRF tokens (bitset of valid). */
|
|
51
|
+
csrfVerify(items: Uint8Array[], secret: Uint8Array): Uint8Array
|
|
52
|
+
schemaValidate(validator: SchemaValidator, items: Uint8Array[]): Uint8Array
|
|
53
|
+
schemaValidateCount(validator: SchemaValidator, items: Uint8Array[]): number
|
|
54
|
+
/** Hash N items with FNV-1a 64 (packed → unsigned u64 array). */
|
|
55
|
+
fnv1a64(items: Uint8Array[]): BigUint64Array
|
|
56
|
+
/** Generate N ETags (strong by default; weak flag applies to all items). */
|
|
57
|
+
etag(items: Uint8Array[], weak?: boolean | null): Uint8Array[]
|
|
58
|
+
urlEncode(items: Uint8Array[]): Uint8Array[]
|
|
59
|
+
/** Percent-decode N strings (UTF-8 validated; malformed items → empty). */
|
|
60
|
+
urlDecode(items: Uint8Array[]): Uint8Array[]
|
|
61
|
+
/** Strict percent-decode N strings (no UTF-8 validation). */
|
|
62
|
+
urlDecodeBytes(items: Uint8Array[]): Uint8Array[]
|
|
63
|
+
mimeFromExtension(items: Uint8Array[]): Uint8Array[]
|
|
64
|
+
wsAcceptKey(items: Uint8Array[]): Uint8Array[]
|
|
65
|
+
/** Base64url-encode N items (URL-safe, no padding). */
|
|
66
|
+
base64UrlEncode(items: Uint8Array[]): Uint8Array[]
|
|
67
|
+
/** Base64url-decode N items (URL-safe, no padding). */
|
|
68
|
+
base64UrlDecode(items: Uint8Array[]): Uint8Array[]
|
|
69
|
+
|
|
70
|
+
// ── Backend-framework features ──
|
|
71
|
+
passwordHash(
|
|
72
|
+
passwords: Uint8Array[],
|
|
73
|
+
salt: Uint8Array,
|
|
74
|
+
options?: PasswordHashOptions | null,
|
|
75
|
+
): Uint8Array[]
|
|
76
|
+
aeadEncrypt(
|
|
77
|
+
key: Uint8Array,
|
|
78
|
+
nonce: Uint8Array,
|
|
79
|
+
items: Uint8Array[],
|
|
80
|
+
algorithm?: string | null,
|
|
81
|
+
): Uint8Array[]
|
|
82
|
+
aeadDecrypt(
|
|
83
|
+
key: Uint8Array,
|
|
84
|
+
nonce: Uint8Array,
|
|
85
|
+
items: Uint8Array[],
|
|
86
|
+
algorithm?: string | null,
|
|
87
|
+
): Uint8Array[]
|
|
88
|
+
gzipCompress(items: Uint8Array[], level?: number | null): Uint8Array[]
|
|
89
|
+
gzipDecompress(items: Uint8Array[], maxDecompressed?: number | null): Uint8Array[]
|
|
90
|
+
brotliCompress(items: Uint8Array[], quality?: number | null): Uint8Array[]
|
|
91
|
+
brotliDecompress(items: Uint8Array[], maxDecompressed?: number | null): Uint8Array[]
|
|
92
|
+
jwtVerify(tokens: Uint8Array[], secret: Uint8Array, nowSeconds: number): Uint8Array
|
|
93
|
+
/** Sign N messages to lowercase-hex HMAC-SHA256 signatures (packed → byte results). */
|
|
94
|
+
hmacSha256(items: Uint8Array[], key: Uint8Array): Uint8Array[]
|
|
95
|
+
/** Verify N message + hex-signature pairs (two packed lists, zipped) → bitset of valid. */
|
|
96
|
+
hmacSha256Verify(items: Uint8Array[], sigs: Uint8Array[], key: Uint8Array): Uint8Array
|
|
97
|
+
/** Verify N password+PHC pairs (two packed lists, zipped) → bitset of valid. */
|
|
98
|
+
passwordVerify(items: Uint8Array[], phcs: Uint8Array[]): Uint8Array
|
|
99
|
+
/** Resolve N references against N bases (two packed lists, zipped). */
|
|
100
|
+
urlResolve(bases: Uint8Array[], references: Uint8Array[]): Uint8Array[]
|
|
101
|
+
/** Sign N JSON claim documents (packed → signed tokens). */
|
|
102
|
+
jwtSign(
|
|
103
|
+
items: Uint8Array[],
|
|
104
|
+
secret: Uint8Array,
|
|
105
|
+
ttlSeconds?: number | null,
|
|
106
|
+
nowSeconds?: number,
|
|
107
|
+
): Uint8Array[]
|
|
108
|
+
/** Encode N SSE events with shared event/id/retry. */
|
|
109
|
+
sseEncode(
|
|
110
|
+
items: Uint8Array[],
|
|
111
|
+
event?: string | null,
|
|
112
|
+
id?: string | null,
|
|
113
|
+
retry?: number | null,
|
|
114
|
+
): Uint8Array[]
|
|
115
|
+
/** Encode N frames (same opcode/mask/fin) from payloads. */
|
|
116
|
+
wsFrameEncode(items: Uint8Array[], opcode: number, mask: boolean, fin: boolean): Uint8Array[]
|
|
117
|
+
/** Decode N frames → payloads (opcodes/flags dropped; failed items → empty). */
|
|
118
|
+
wsFrameDecode(items: Uint8Array[]): Uint8Array[]
|
|
119
|
+
/** Parse N multipart bodies → array of parts per item (same boundary). */
|
|
120
|
+
multipartParse(items: Uint8Array[], boundary: Uint8Array): MultipartPart[][]
|
|
121
|
+
/**
|
|
122
|
+
* @performance Template render: ~1.7x slower than the JS baseline (1/3 tasks failed) [check:annotate]
|
|
123
|
+
* @deprecated Slower than the native JS baseline (~1.7x) — prefer the JS/Bun baseline. [check:annotate]
|
|
124
|
+
* @remarks Promotion candidate: 2/3 benchmarks won in the latest run (static: not-competitive). [check:annotate]
|
|
125
|
+
* @remarks The allocating render marshals the context via napi DOM (~1.5x loss). Use TemplateRenderer.renderBytes (JSON-bytes context) which avoids it and wins ~1.37x. [check:annotate]
|
|
126
|
+
*/
|
|
127
|
+
templateRender(source: string, contexts: Uint8Array[]): Uint8Array[]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Native batch fns are process-wide singletons (one lazy addon). `resolvePoolNative`
|
|
131
|
+
// resolves each once (triggering pool init + addon load on first use) and caches
|
|
132
|
+
// it, so the hot path skips the lazy-addon Proxy get on every call — the same
|
|
133
|
+
// cache `resolveNative`/packed use (context.ts), with pool init for the
|
|
134
|
+
// rayon-backed batch surface.
|
|
135
|
+
function nativeFn(ctx: RustClientContext, name: string): (...args: unknown[]) => unknown {
|
|
136
|
+
return resolvePoolNative(ctx, name)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Build the `batch` namespace for a client context. */
|
|
140
|
+
export function buildBatch(ctx: RustClientContext): RustBatch {
|
|
141
|
+
return {
|
|
142
|
+
jsonValid(items) {
|
|
143
|
+
return withPackScratch(items, (packed) =>
|
|
144
|
+
unpackBitset(nativeFn(ctx, 'jsonValidBatchPacked')(packed) as Buffer),
|
|
145
|
+
)
|
|
146
|
+
},
|
|
147
|
+
crc32(items) {
|
|
148
|
+
return withPackScratch(items, (packed) =>
|
|
149
|
+
unpackU32Array(nativeFn(ctx, 'crc32BatchPacked')(packed) as Buffer),
|
|
150
|
+
)
|
|
151
|
+
},
|
|
152
|
+
validateEmail(items) {
|
|
153
|
+
return withPackScratch(items, (packed) =>
|
|
154
|
+
unpackBitset(nativeFn(ctx, 'validateEmailBatchPacked')(packed) as Buffer),
|
|
155
|
+
)
|
|
156
|
+
},
|
|
157
|
+
validateUuid(items) {
|
|
158
|
+
return withPackScratch(items, (packed) =>
|
|
159
|
+
unpackBitset(nativeFn(ctx, 'validateUuidBatchPacked')(packed) as Buffer),
|
|
160
|
+
)
|
|
161
|
+
},
|
|
162
|
+
validateIpv4(items) {
|
|
163
|
+
return withPackScratch(items, (packed) =>
|
|
164
|
+
unpackBitset(nativeFn(ctx, 'validateIpv4BatchPacked')(packed) as Buffer),
|
|
165
|
+
)
|
|
166
|
+
},
|
|
167
|
+
validateIpv6(items) {
|
|
168
|
+
return withPackScratch(items, (packed) =>
|
|
169
|
+
unpackBitset(nativeFn(ctx, 'validateIpv6BatchPacked')(packed) as Buffer),
|
|
170
|
+
)
|
|
171
|
+
},
|
|
172
|
+
jsonSumIds(items) {
|
|
173
|
+
return withPackScratch(items, (packed) =>
|
|
174
|
+
unpackI64ArrayAsBigInt(nativeFn(ctx, 'jsonSumBatchPacked')(packed) as Buffer),
|
|
175
|
+
)
|
|
176
|
+
},
|
|
177
|
+
queryParse(items) {
|
|
178
|
+
return withPackScratch(items, (packed) =>
|
|
179
|
+
unpackByteResults(nativeFn(ctx, 'queryParseBatchPacked')(packed) as Buffer),
|
|
180
|
+
)
|
|
181
|
+
},
|
|
182
|
+
cookieParse(items) {
|
|
183
|
+
return withPackScratch(items, (packed) =>
|
|
184
|
+
unpackByteResults(nativeFn(ctx, 'cookieParseBatchPacked')(packed) as Buffer),
|
|
185
|
+
)
|
|
186
|
+
},
|
|
187
|
+
formParse(items) {
|
|
188
|
+
return withPackScratch(items, (packed) =>
|
|
189
|
+
unpackByteResults(nativeFn(ctx, 'formParseBatchPacked')(packed) as Buffer),
|
|
190
|
+
)
|
|
191
|
+
},
|
|
192
|
+
signCookie(items, secret) {
|
|
193
|
+
return withPackScratch(items, (packed) =>
|
|
194
|
+
unpackByteResults(nativeFn(ctx, 'signCookieBatchPacked')(packed, secret) as Buffer),
|
|
195
|
+
)
|
|
196
|
+
},
|
|
197
|
+
verifyCookie(items, secret) {
|
|
198
|
+
return withPackScratch(items, (packed) =>
|
|
199
|
+
unpackBitset(nativeFn(ctx, 'verifyCookieBatchPacked')(packed, secret) as Buffer),
|
|
200
|
+
)
|
|
201
|
+
},
|
|
202
|
+
csrfVerify(items, secret) {
|
|
203
|
+
return withPackScratch(items, (packed) =>
|
|
204
|
+
unpackBitset(nativeFn(ctx, 'csrfVerifyBatchPacked')(packed, secret) as Buffer),
|
|
205
|
+
)
|
|
206
|
+
},
|
|
207
|
+
httpParseRequest(items) {
|
|
208
|
+
return withPackScratch(items, (packed) =>
|
|
209
|
+
unpackByteResults(nativeFn(ctx, 'httpParseRequestBatchPacked')(packed) as Buffer),
|
|
210
|
+
)
|
|
211
|
+
},
|
|
212
|
+
fnv1a64(items) {
|
|
213
|
+
return withPackScratch(items, (packed) =>
|
|
214
|
+
unpackU64ArrayAsBigInt(nativeFn(ctx, 'fnv1A64BatchPacked')(packed) as Buffer),
|
|
215
|
+
)
|
|
216
|
+
},
|
|
217
|
+
etag(items, weak) {
|
|
218
|
+
return withPackScratch(items, (packed) =>
|
|
219
|
+
unpackByteResults(nativeFn(ctx, 'etagBatchPacked')(packed, weak ?? null) as Buffer),
|
|
220
|
+
)
|
|
221
|
+
},
|
|
222
|
+
urlEncode(items) {
|
|
223
|
+
return withPackScratch(items, (packed) =>
|
|
224
|
+
unpackByteResults(nativeFn(ctx, 'urlEncodeBatchPacked')(packed) as Buffer),
|
|
225
|
+
)
|
|
226
|
+
},
|
|
227
|
+
urlDecode(items) {
|
|
228
|
+
return withPackScratch(items, (packed) =>
|
|
229
|
+
unpackByteResults(nativeFn(ctx, 'urlDecodeBatchPacked')(packed) as Buffer),
|
|
230
|
+
)
|
|
231
|
+
},
|
|
232
|
+
urlDecodeBytes(items) {
|
|
233
|
+
return withPackScratch(items, (packed) =>
|
|
234
|
+
unpackByteResults(nativeFn(ctx, 'urlDecodeBytesBatchPacked')(packed) as Buffer),
|
|
235
|
+
)
|
|
236
|
+
},
|
|
237
|
+
mimeFromExtension(items) {
|
|
238
|
+
return withPackScratch(items, (packed) =>
|
|
239
|
+
unpackByteResults(nativeFn(ctx, 'mimeFromExtensionBatchPacked')(packed) as Buffer),
|
|
240
|
+
)
|
|
241
|
+
},
|
|
242
|
+
wsAcceptKey(items) {
|
|
243
|
+
return withPackScratch(items, (packed) =>
|
|
244
|
+
unpackByteResults(nativeFn(ctx, 'wsAcceptKeyBatchPacked')(packed) as Buffer),
|
|
245
|
+
)
|
|
246
|
+
},
|
|
247
|
+
base64UrlEncode(items) {
|
|
248
|
+
return withPackScratch(items, (packed) =>
|
|
249
|
+
unpackByteResults(nativeFn(ctx, 'base64EncodeBatchPacked')(packed, true, false) as Buffer),
|
|
250
|
+
)
|
|
251
|
+
},
|
|
252
|
+
base64UrlDecode(items) {
|
|
253
|
+
return withPackScratch(items, (packed) =>
|
|
254
|
+
unpackByteResults(nativeFn(ctx, 'base64DecodeBatchPacked')(packed, true, false) as Buffer),
|
|
255
|
+
)
|
|
256
|
+
},
|
|
257
|
+
jsonPatch(docs, patches) {
|
|
258
|
+
return withPackScratch2(docs, patches, (packedDocs, packedPatches) =>
|
|
259
|
+
unpackByteResults(
|
|
260
|
+
nativeFn(ctx, 'jsonPatchBatchPacked')(packedDocs, packedPatches) as Buffer,
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
},
|
|
264
|
+
hexEncode(items) {
|
|
265
|
+
return withPackScratch(items, (packed) =>
|
|
266
|
+
unpackByteResults(nativeFn(ctx, 'hexEncodeBatchPacked')(packed) as Buffer),
|
|
267
|
+
)
|
|
268
|
+
},
|
|
269
|
+
hexDecode(items) {
|
|
270
|
+
return withPackScratch(items, (packed) =>
|
|
271
|
+
unpackByteResults(nativeFn(ctx, 'hexDecodeBatchPacked')(packed) as Buffer),
|
|
272
|
+
)
|
|
273
|
+
},
|
|
274
|
+
base64Encode(items, urlSafe, padding) {
|
|
275
|
+
return withPackScratch(items, (packed) =>
|
|
276
|
+
unpackByteResults(
|
|
277
|
+
nativeFn(ctx, 'base64EncodeBatchPacked')(
|
|
278
|
+
packed,
|
|
279
|
+
urlSafe ?? null,
|
|
280
|
+
padding ?? null,
|
|
281
|
+
) as Buffer,
|
|
282
|
+
),
|
|
283
|
+
)
|
|
284
|
+
},
|
|
285
|
+
base64Decode(items, urlSafe, padding) {
|
|
286
|
+
return withPackScratch(items, (packed) =>
|
|
287
|
+
unpackByteResults(
|
|
288
|
+
nativeFn(ctx, 'base64DecodeBatchPacked')(
|
|
289
|
+
packed,
|
|
290
|
+
urlSafe ?? null,
|
|
291
|
+
padding ?? null,
|
|
292
|
+
) as Buffer,
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
},
|
|
296
|
+
schemaValidate(validator, items) {
|
|
297
|
+
return schemaValidateBatch(validator, items)
|
|
298
|
+
},
|
|
299
|
+
schemaValidateCount(validator, items) {
|
|
300
|
+
return schemaValidateBatchCount(validator, items)
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
// ── Backend-framework features ──
|
|
304
|
+
passwordHash(password, salt, options) {
|
|
305
|
+
return withPackScratch(password, (packed) =>
|
|
306
|
+
unpackByteResults(
|
|
307
|
+
nativeFn(ctx, 'passwordHashBatchPacked')(packed, salt, options ?? null) as Buffer,
|
|
308
|
+
),
|
|
309
|
+
)
|
|
310
|
+
},
|
|
311
|
+
aeadEncrypt(key, nonce, items, algorithm) {
|
|
312
|
+
return withPackScratch(items, (packed) =>
|
|
313
|
+
unpackByteResults(
|
|
314
|
+
nativeFn(ctx, 'aeadEncryptBatchPacked')(packed, key, nonce, algorithm ?? null) as Buffer,
|
|
315
|
+
),
|
|
316
|
+
)
|
|
317
|
+
},
|
|
318
|
+
aeadDecrypt(key, nonce, items, algorithm) {
|
|
319
|
+
return withPackScratch(items, (packed) =>
|
|
320
|
+
unpackByteResults(
|
|
321
|
+
nativeFn(ctx, 'aeadDecryptBatchPacked')(packed, key, nonce, algorithm ?? null) as Buffer,
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
},
|
|
325
|
+
gzipCompress(items, level) {
|
|
326
|
+
return withPackScratch(items, (packed) =>
|
|
327
|
+
unpackByteResults(
|
|
328
|
+
nativeFn(ctx, 'gzipCompressBatchPacked')(packed, level ?? null) as Buffer,
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
},
|
|
332
|
+
gzipDecompress(items, maxDecompressed) {
|
|
333
|
+
return withPackScratch(items, (packed) =>
|
|
334
|
+
unpackByteResults(
|
|
335
|
+
nativeFn(ctx, 'gzipDecompressBatchPacked')(packed, maxDecompressed ?? null) as Buffer,
|
|
336
|
+
),
|
|
337
|
+
)
|
|
338
|
+
},
|
|
339
|
+
brotliCompress(items, quality) {
|
|
340
|
+
return withPackScratch(items, (packed) =>
|
|
341
|
+
unpackByteResults(
|
|
342
|
+
nativeFn(ctx, 'brotliCompressBatchPacked')(packed, quality ?? null) as Buffer,
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
},
|
|
346
|
+
brotliDecompress(items, maxDecompressed) {
|
|
347
|
+
return withPackScratch(items, (packed) =>
|
|
348
|
+
unpackByteResults(
|
|
349
|
+
nativeFn(ctx, 'brotliDecompressBatchPacked')(packed, maxDecompressed ?? null) as Buffer,
|
|
350
|
+
),
|
|
351
|
+
)
|
|
352
|
+
},
|
|
353
|
+
jwtVerify(tokens, secret, nowSeconds) {
|
|
354
|
+
return withPackScratch(tokens, (packed) =>
|
|
355
|
+
unpackBitset(nativeFn(ctx, 'jwtVerifyBatchPacked')(packed, secret, nowSeconds) as Buffer),
|
|
356
|
+
)
|
|
357
|
+
},
|
|
358
|
+
hmacSha256(items, key) {
|
|
359
|
+
return withPackScratch(items, (packed) =>
|
|
360
|
+
unpackByteResults(nativeFn(ctx, 'hmacSha256BatchPacked')(packed, key) as Buffer),
|
|
361
|
+
)
|
|
362
|
+
},
|
|
363
|
+
hmacSha256Verify(items, sigs, key) {
|
|
364
|
+
return withPackScratch2(items, sigs, (packedItems, packedSigs) =>
|
|
365
|
+
unpackBitset(
|
|
366
|
+
nativeFn(ctx, 'hmacSha256VerifyBatchPacked')(packedItems, packedSigs, key) as Buffer,
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
},
|
|
370
|
+
templateRender(source, contexts) {
|
|
371
|
+
return withPackScratch(contexts, (packed) =>
|
|
372
|
+
unpackByteResults(nativeFn(ctx, 'templateRenderBatchPacked')(packed, source) as Buffer),
|
|
373
|
+
)
|
|
374
|
+
},
|
|
375
|
+
passwordVerify(items, phcs) {
|
|
376
|
+
return withPackScratch2(items, phcs, (packedItems, packedPhcs) =>
|
|
377
|
+
unpackBitset(nativeFn(ctx, 'passwordVerifyBatchPacked')(packedItems, packedPhcs) as Buffer),
|
|
378
|
+
)
|
|
379
|
+
},
|
|
380
|
+
urlResolve(bases, references) {
|
|
381
|
+
return withPackScratch2(bases, references, (packedBases, packedRefs) =>
|
|
382
|
+
unpackByteResults(
|
|
383
|
+
nativeFn(ctx, 'urlResolveBatchPacked')(packedBases, packedRefs) as Buffer,
|
|
384
|
+
),
|
|
385
|
+
)
|
|
386
|
+
},
|
|
387
|
+
jwtSign(items, secret, ttlSeconds, nowSeconds) {
|
|
388
|
+
return withPackScratch(items, (packed) =>
|
|
389
|
+
unpackByteResults(
|
|
390
|
+
nativeFn(ctx, 'jwtSignBatchPacked')(
|
|
391
|
+
packed,
|
|
392
|
+
secret,
|
|
393
|
+
ttlSeconds ?? null,
|
|
394
|
+
nowSeconds ?? Math.floor(Date.now() / 1000),
|
|
395
|
+
) as Buffer,
|
|
396
|
+
),
|
|
397
|
+
)
|
|
398
|
+
},
|
|
399
|
+
sseEncode(items, event, id, retry) {
|
|
400
|
+
return withPackScratch(items, (packed) =>
|
|
401
|
+
unpackByteResults(
|
|
402
|
+
nativeFn(ctx, 'sseEncodeBatchPacked')(
|
|
403
|
+
packed,
|
|
404
|
+
event ?? null,
|
|
405
|
+
id ?? null,
|
|
406
|
+
retry ?? null,
|
|
407
|
+
) as Buffer,
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
},
|
|
411
|
+
wsFrameEncode(items, opcode, mask, fin) {
|
|
412
|
+
return withPackScratch(items, (packed) =>
|
|
413
|
+
unpackByteResults(
|
|
414
|
+
nativeFn(ctx, 'wsFrameEncodeBatchPacked')(packed, opcode, mask, fin) as Buffer,
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
},
|
|
418
|
+
wsFrameDecode(items) {
|
|
419
|
+
return withPackScratch(items, (packed) =>
|
|
420
|
+
unpackByteResults(nativeFn(ctx, 'wsFrameDecodeBatchPacked')(packed) as Buffer),
|
|
421
|
+
)
|
|
422
|
+
},
|
|
423
|
+
multipartParse(items, boundary) {
|
|
424
|
+
return withPackScratch(items, (packed) =>
|
|
425
|
+
unpackMultipartParts(
|
|
426
|
+
nativeFn(ctx, 'multipartParseBatchPacked')(packed, boundary) as Buffer,
|
|
427
|
+
),
|
|
428
|
+
)
|
|
429
|
+
},
|
|
430
|
+
}
|
|
431
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/rust-ffi/client.ts — Rust client factory + default instance.
|
|
2
|
+
//
|
|
3
|
+
// Composes the per-namespace builders (scalar / text / batch / packed) over a
|
|
4
|
+
// shared per-instance context (./context.ts) and exposes `configure`.
|
|
5
|
+
|
|
6
|
+
import { createContext } from './context'
|
|
7
|
+
import { buildText, type RustText } from './text'
|
|
8
|
+
import { buildBatch, type RustBatch } from './batch'
|
|
9
|
+
import { buildPacked, type RustPacked } from './packed'
|
|
10
|
+
import { buildScalar, type RustScalar } from './scalar'
|
|
11
|
+
import { getBunFFI } from '../native/ffi'
|
|
12
|
+
import type { RustOptions } from './options'
|
|
13
|
+
|
|
14
|
+
/** The full Rust FFI client (scalar methods + namespaces + configuration). */
|
|
15
|
+
export interface RustClient extends RustScalar {
|
|
16
|
+
// ── Namespaces ──
|
|
17
|
+
text: RustText
|
|
18
|
+
batch: RustBatch
|
|
19
|
+
packed: RustPacked
|
|
20
|
+
|
|
21
|
+
// ── Transport introspection ──
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the native transport in use on this runtime: `"ffi"` (bun:ffi —
|
|
24
|
+
* the PRIMARY transport under Bun) or `"napi"` (Node, forced
|
|
25
|
+
* `CASTRUM_FFI_MODE=napi`, or a failed ffi bind-time self-test).
|
|
26
|
+
*/
|
|
27
|
+
transport(): 'ffi' | 'napi'
|
|
28
|
+
/** Whether the bun:ffi transport is live (equivalent to `transport() === "ffi"`). */
|
|
29
|
+
ffiActive(): boolean
|
|
30
|
+
|
|
31
|
+
// ── Configuration ──
|
|
32
|
+
/**
|
|
33
|
+
* Update defaults on this instance. `mimeCache` / `hmacCache` apply
|
|
34
|
+
* immediately (per-instance). `rayonThreads` is applied only if the
|
|
35
|
+
* process-wide pool is not yet initialized (see {@link RustOptions}).
|
|
36
|
+
*/
|
|
37
|
+
configure(options: RustOptions): void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create an isolated Rust client with the given defaults.
|
|
42
|
+
*
|
|
43
|
+
* `mimeCache` / `hmacCache` are per-instance. `rayonThreads` only applies
|
|
44
|
+
* before the process-wide rayon pool is initialized (once per process — see
|
|
45
|
+
* {@link RustOptions.rayonThreads}); the pool defaults to the env var or
|
|
46
|
+
* `max(1, hardwareConcurrency - 1)` otherwise.
|
|
47
|
+
*/
|
|
48
|
+
export function createRust(options: RustOptions = {}): RustClient {
|
|
49
|
+
const ctx = createContext(options)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...buildScalar(ctx),
|
|
53
|
+
text: buildText(ctx),
|
|
54
|
+
batch: buildBatch(ctx),
|
|
55
|
+
packed: buildPacked(ctx),
|
|
56
|
+
|
|
57
|
+
// ── Transport introspection (FFI-primary on Bun; napi is the fallback) ──
|
|
58
|
+
transport() {
|
|
59
|
+
return getBunFFI() !== null ? 'ffi' : 'napi'
|
|
60
|
+
},
|
|
61
|
+
ffiActive() {
|
|
62
|
+
return getBunFFI() !== null
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// ── Configuration ──
|
|
66
|
+
configure(next) {
|
|
67
|
+
// rayonThreads only takes effect BEFORE the pool is first used.
|
|
68
|
+
if (next.rayonThreads !== undefined && !ctx.isPoolInitialized()) {
|
|
69
|
+
ctx.setPendingThreads(next.rayonThreads)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (next.mimeCache !== undefined) {
|
|
73
|
+
ctx.state.mimeCache = next.mimeCache
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (next.hmacCache !== undefined) {
|
|
77
|
+
ctx.state.hmacCache = next.hmacCache
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The default, ready-to-use `RustClient` created with no options (resolves
|
|
85
|
+
* rayon threads from the host automatically). Most consumers just do
|
|
86
|
+
* `import { rust } from "castrum"` and call `rust.<fn>(...)` directly.
|
|
87
|
+
*/
|
|
88
|
+
export const rust = createRust()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// src/rust-ffi/context.ts — Per-instance state + shared helpers for the Rust
|
|
2
|
+
// client namespaces.
|
|
3
|
+
//
|
|
4
|
+
// Each `createRust()` call builds one context holding its own caches, toggles
|
|
5
|
+
// and the lazily-initialized rayon-pool bookkeeping. The mutable pool state
|
|
6
|
+
// (initialized flag / pending thread count) stays encapsulated behind accessor
|
|
7
|
+
// functions so it cannot be corrupted externally.
|
|
8
|
+
|
|
9
|
+
import { addon } from './addon'
|
|
10
|
+
import { resolveRayonThreads } from './options'
|
|
11
|
+
import { normalizeExt } from './options'
|
|
12
|
+
import type { RustOptions } from './options'
|
|
13
|
+
import type { HmacSignerInstance } from '../native'
|
|
14
|
+
|
|
15
|
+
// Module-level cache of resolved native fns. The native addon is a process
|
|
16
|
+
// singleton, so the resolved function is identical across all client contexts;
|
|
17
|
+
// caching it here lets scalar/packed/text wrappers skip the lazy-addon Proxy
|
|
18
|
+
// `get` trap (a branch + `Reflect.get`) on every call — the same pattern
|
|
19
|
+
// batch.ts uses for its packed entries.
|
|
20
|
+
const nativeFnCache = new Map<string, unknown>()
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a native addon function on first use and cache it for all subsequent
|
|
24
|
+
* calls. Preserves the lazy-load contract (the addon is only loaded on the
|
|
25
|
+
* first real native call) while removing per-call Proxy overhead.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveNative(
|
|
28
|
+
ctx: RustClientContext,
|
|
29
|
+
name: string,
|
|
30
|
+
): (...args: unknown[]) => unknown {
|
|
31
|
+
let f = nativeFnCache.get(name)
|
|
32
|
+
if (f === undefined) {
|
|
33
|
+
f = (ctx.addon as unknown as Record<string, unknown>)[name]
|
|
34
|
+
nativeFnCache.set(name, f)
|
|
35
|
+
}
|
|
36
|
+
return f as (...args: unknown[]) => unknown
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a native fn and ensure the process-wide rayon pool is initialized
|
|
41
|
+
* on first use — the rayon-backed `batch`/`packed` surfaces need this, while
|
|
42
|
+
* `resolveNative` (plain scalars) must NOT init the pool so
|
|
43
|
+
* `rust.configure({ rayonThreads })` can still take effect at startup.
|
|
44
|
+
* Shares the SAME `nativeFnCache` as `resolveNative` (the batch fn names are
|
|
45
|
+
* disjoint from the scalar names, so the shared cache is safe).
|
|
46
|
+
*/
|
|
47
|
+
export function resolvePoolNative(
|
|
48
|
+
ctx: RustClientContext,
|
|
49
|
+
name: string,
|
|
50
|
+
): (...args: unknown[]) => unknown {
|
|
51
|
+
let f = nativeFnCache.get(name)
|
|
52
|
+
if (f === undefined) {
|
|
53
|
+
ctx.ensurePool()
|
|
54
|
+
f = (ctx.addon as unknown as Record<string, unknown>)[name]
|
|
55
|
+
nativeFnCache.set(name, f)
|
|
56
|
+
}
|
|
57
|
+
return f as (...args: unknown[]) => unknown
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Shared per-instance state passed to the namespace/scalar builders. */
|
|
61
|
+
export interface RustClientContext {
|
|
62
|
+
/** The shared lazy addon proxy. */
|
|
63
|
+
addon: typeof addon
|
|
64
|
+
/** Per-instance toggles (mime/hmac caching). */
|
|
65
|
+
state: { mimeCache: boolean; hmacCache: boolean }
|
|
66
|
+
mimeByText: Map<string, Uint8Array>
|
|
67
|
+
hmacSigners: WeakMap<Uint8Array, HmacSignerInstance>
|
|
68
|
+
|
|
69
|
+
/** Initialize the process-wide rayon pool exactly once (no-op after). */
|
|
70
|
+
ensurePool(): void
|
|
71
|
+
/** Whether the rayon pool state has been established on this instance. */
|
|
72
|
+
isPoolInitialized(): boolean
|
|
73
|
+
/** Record the caller's intent for the process-wide pool before it inits. */
|
|
74
|
+
setPendingThreads(threads: number | undefined): void
|
|
75
|
+
/** Mark the pool state as established (explicit initThreadPool calls). */
|
|
76
|
+
markPoolInitialized(): void
|
|
77
|
+
/** Cached MIME lookup (returns the cached bytes by reference — do not mutate). */
|
|
78
|
+
cachedMime(ext: Uint8Array): Uint8Array
|
|
79
|
+
/** Cached HMAC signer lookup. */
|
|
80
|
+
hmacSigner(key: Uint8Array): HmacSignerInstance
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Build a per-instance context from client options. */
|
|
84
|
+
export function createContext(options: RustOptions): RustClientContext {
|
|
85
|
+
const mimeByText = new Map<string, Uint8Array>()
|
|
86
|
+
const hmacSigners = new WeakMap<Uint8Array, HmacSignerInstance>()
|
|
87
|
+
const state = {
|
|
88
|
+
mimeCache: options.mimeCache !== false,
|
|
89
|
+
hmacCache: options.hmacCache !== false,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The native rayon pool is a process-wide OnceLock: the FIRST call wins.
|
|
93
|
+
// We therefore defer init until the first batch/packed operation so that
|
|
94
|
+
// rust.configure({ rayonThreads }) called at startup actually takes effect
|
|
95
|
+
// (previously the pool was initialized eagerly at module load, silently
|
|
96
|
+
// ignoring later rayonThreads tuning).
|
|
97
|
+
let poolInitialized = false
|
|
98
|
+
let pendingThreads = options.rayonThreads
|
|
99
|
+
|
|
100
|
+
function ensurePool(): void {
|
|
101
|
+
if (poolInitialized) return
|
|
102
|
+
poolInitialized = true
|
|
103
|
+
addon.initThreadPool(resolveRayonThreads(pendingThreads))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isPoolInitialized(): boolean {
|
|
107
|
+
return poolInitialized
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function setPendingThreads(threads: number | undefined): void {
|
|
111
|
+
pendingThreads = threads
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function markPoolInitialized(): void {
|
|
115
|
+
poolInitialized = true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function cachedMime(ext: Uint8Array): Uint8Array {
|
|
119
|
+
if (!state.mimeCache) {
|
|
120
|
+
return addon.mimeFromExtension(ext)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const key = normalizeExt(ext)
|
|
124
|
+
|
|
125
|
+
let val = mimeByText.get(key)
|
|
126
|
+
if (!val) {
|
|
127
|
+
val = addon.mimeFromExtension(ext)
|
|
128
|
+
mimeByText.set(key, val)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Return the cached bytes BY REFERENCE (no per-call defensive copy — a
|
|
132
|
+
// measurable win on the MIME parity op). The returned slice aliases the
|
|
133
|
+
// cache: callers must NOT mutate it (same contract as `generateRequestId`).
|
|
134
|
+
return val
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function hmacSigner(key: Uint8Array): HmacSignerInstance {
|
|
138
|
+
if (!state.hmacCache) {
|
|
139
|
+
return new addon.HmacSigner(key)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let signer = hmacSigners.get(key)
|
|
143
|
+
|
|
144
|
+
if (!signer) {
|
|
145
|
+
signer = new addon.HmacSigner(key)
|
|
146
|
+
hmacSigners.set(key, signer)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return signer
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
addon,
|
|
154
|
+
state,
|
|
155
|
+
mimeByText,
|
|
156
|
+
hmacSigners,
|
|
157
|
+
ensurePool,
|
|
158
|
+
isPoolInitialized,
|
|
159
|
+
setPendingThreads,
|
|
160
|
+
markPoolInitialized,
|
|
161
|
+
cachedMime,
|
|
162
|
+
hmacSigner,
|
|
163
|
+
}
|
|
164
|
+
}
|