castrum 0.7.0 → 0.9.1
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 +154 -368
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +2763 -0
- package/dist/index.js +8707 -0
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +507 -0
- package/docs/BENCHMARKS.md +343 -0
- package/docs/CASE_STUDY.md +240 -0
- package/docs/ENVIRONMENT.md +81 -0
- package/docs/FFI_BUN_GUIDE.md +327 -0
- package/docs/GETTING_STARTED.md +192 -0
- package/docs/INGRESS-ROUTER.md +81 -0
- package/docs/INGRESS.md +746 -0
- package/docs/NATIVE-ROUTE.md +53 -0
- package/docs/REPO_MAP.md +279 -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 +43 -0
- package/docs/bun-builtins-decision-matrix.md +115 -0
- package/index.ts +44 -8
- package/package.json +98 -33
- package/src/ingress/body.ts +289 -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 +639 -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 +68 -0
- package/src/ingress/index.ts +142 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +139 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +164 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +102 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/router.ts +237 -0
- package/src/ingress/routes/common.ts +79 -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 +27 -0
- package/src/ingress/routes/index.ts +11 -0
- package/src/ingress/routes/json-write.ts +129 -0
- package/src/ingress/routes/options.ts +26 -0
- package/src/ingress/routes/read.ts +35 -0
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +475 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +191 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +220 -0
- package/src/integration/streaming.ts +78 -0
- package/src/integration/websocket.ts +93 -0
- package/src/loader/batch.ts +137 -0
- package/src/loader/cost.ts +105 -0
- package/src/loader/index.ts +745 -0
- package/src/loader/ops.ts +657 -0
- package/src/loader/state.ts +84 -0
- package/src/loader/types.ts +145 -0
- package/src/native/ffi/constants.ts +58 -0
- package/src/native/ffi/selftest.ts +729 -0
- package/src/native/ffi/types.ts +474 -0
- package/src/native/ffi.ts +1775 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +240 -0
- package/src/native/types/instances.ts +274 -0
- package/src/native/types.ts +401 -0
- package/src/runtime/builtins.ts +131 -0
- package/src/runtime/codec.ts +147 -0
- package/src/runtime/detect.ts +36 -0
- package/src/runtime/index.ts +28 -0
- package/src/runtime/native.ts +35 -0
- package/src/runtime/server.ts +31 -0
- package/src/runtime/transport.ts +64 -0
- package/src/runtime/types.ts +173 -0
- package/src/runtime/uuid.ts +45 -0
- package/src/runtime/websocket.ts +30 -0
- package/src/rust-ffi/addon.ts +10 -0
- package/src/rust-ffi/batch/build.ts +155 -0
- package/src/rust-ffi/batch/index.ts +8 -0
- package/src/rust-ffi/batch/types.ts +92 -0
- package/src/rust-ffi/client.ts +87 -0
- package/src/rust-ffi/context.ts +193 -0
- package/src/rust-ffi/index.ts +51 -541
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +28 -0
- package/src/rust-ffi/scalar/crypto.ts +346 -0
- package/src/rust-ffi/scalar/factories.ts +459 -0
- package/src/rust-ffi/scalar/hashing.ts +74 -0
- package/src/rust-ffi/scalar/http.ts +337 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +325 -0
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +61 -0
- package/src/rust-ffi/scalar/payload.ts +241 -0
- package/src/rust-ffi/text.ts +121 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +73 -0
- package/src/shared/adaptive.ts +67 -0
- package/src/shared/buffer-pool.ts +264 -0
- package/src/shared/bytes.ts +108 -9
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +75 -0
- package/src/shared/metrics.ts +247 -0
- package/src/shared/packed/index.ts +10 -0
- package/src/shared/packed/parsers.ts +31 -0
- package/src/shared/packed/schema.ts +23 -0
- package/src/shared/packed/wire.ts +504 -0
- package/src/shared/proven.ts +289 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +29 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +15 -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/data/json-rows.ts +0 -33
- package/src/ingress/packed-input.ts +0 -59
- package/src/shared/json.ts +0 -18
- package/src/shared/packed.ts +0 -316
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/runtime/uuid.ts — UUIDv7 generation (Bun built-in, Node fallback).
|
|
2
|
+
//
|
|
3
|
+
// Deliberately does NOT implement UUIDv7 in Rust: the decision matrix measured
|
|
4
|
+
// `Bun.randomUUIDv7` ~2x faster than the FFI-crossing rust random-token path,
|
|
5
|
+
// so this delegates to the Bun built-in and falls back to
|
|
6
|
+
// `crypto.randomUUID` (RFC 4122 v4) under Node. Formerly in
|
|
7
|
+
// `src/shared/uuid.ts`; the branches now live here, selected once at load.
|
|
8
|
+
|
|
9
|
+
import { detectedRuntime } from './detect'
|
|
10
|
+
import type { UuidAdapter } from './types'
|
|
11
|
+
|
|
12
|
+
/** Last-resort synchronous UUIDv4 (only if `crypto.randomUUID` is unavailable). */
|
|
13
|
+
function fallbackRandomUuid(): string {
|
|
14
|
+
const bytes = new Uint8Array(16)
|
|
15
|
+
const c = globalThis.crypto
|
|
16
|
+
if (c?.getRandomValues) {
|
|
17
|
+
c.getRandomValues(bytes)
|
|
18
|
+
} else {
|
|
19
|
+
for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256)
|
|
20
|
+
}
|
|
21
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40 // version 4
|
|
22
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80 // variant 10xx
|
|
23
|
+
const hex = [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
24
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function bunUuidv7(): string {
|
|
28
|
+
return (
|
|
29
|
+
(globalThis as { Bun?: { randomUUIDv7?: () => string } }).Bun?.randomUUIDv7?.() ??
|
|
30
|
+
globalThis.crypto?.randomUUID?.() ??
|
|
31
|
+
fallbackRandomUuid()
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function nodeUuidv7(): string {
|
|
36
|
+
return globalThis.crypto?.randomUUID?.() ?? fallbackRandomUuid()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Build the uuid adapter for a runtime. */
|
|
40
|
+
export function createUuidAdapter(name: 'bun' | 'node' | 'unknown'): UuidAdapter {
|
|
41
|
+
return name === 'bun' ? { uuidv7: bunUuidv7 } : { uuidv7: nodeUuidv7 }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The shared uuid adapter for the detected runtime (selected once at load). */
|
|
45
|
+
export const runtimeUuid: UuidAdapter = createUuidAdapter(detectedRuntime)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/runtime/websocket.ts — platform WebSocket upgrade adapter.
|
|
2
|
+
//
|
|
3
|
+
// `createWebSocketUpgrade` returns the RFC 6455 101 handshake on Bun (whose
|
|
4
|
+
// `Response` can carry status 101) and throws a clear Bun-only error on Node
|
|
5
|
+
// (whose fetch `Response` rejects 101 — Node users must use the server-node
|
|
6
|
+
// `upgrade` option instead). The handshake computation lives in
|
|
7
|
+
// `src/integration/websocket.ts`; this adapter owns the runtime branch.
|
|
8
|
+
|
|
9
|
+
import type { WebSocketUpgradeOptions, WebSocketUpgradeResult } from '../integration/websocket'
|
|
10
|
+
import { createWebSocketUpgrade as buildBunUpgrade } from '../integration/websocket'
|
|
11
|
+
import { detectedRuntime } from './detect'
|
|
12
|
+
import type { WebsocketAdapter } from './types'
|
|
13
|
+
|
|
14
|
+
const BUN_ONLY_ERROR =
|
|
15
|
+
"createWebSocketUpgrade is Bun-only: Node's Response cannot carry status 101. " +
|
|
16
|
+
"On Node, use createIngressServerNode's `upgrade` option and compute the " +
|
|
17
|
+
'accept key with rust.text.wsAcceptKey().'
|
|
18
|
+
|
|
19
|
+
/** The shared websocket adapter for the detected host runtime. */
|
|
20
|
+
export const runtimeWebsocket: WebsocketAdapter = {
|
|
21
|
+
createWebSocketUpgrade(
|
|
22
|
+
req: Request,
|
|
23
|
+
opts?: WebSocketUpgradeOptions,
|
|
24
|
+
): WebSocketUpgradeResult | null {
|
|
25
|
+
if (detectedRuntime !== 'bun') {
|
|
26
|
+
throw new Error(BUN_ONLY_ERROR)
|
|
27
|
+
}
|
|
28
|
+
return buildBunUpgrade(req, opts)
|
|
29
|
+
},
|
|
30
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// src/rust-ffi/addon.ts — Shared lazy addon accessor for the FFI layer.
|
|
2
|
+
//
|
|
3
|
+
// All rust-ffi namespaces share a single lazily-loading proxy: importing this
|
|
4
|
+
// module does NOT dlopen the addon — the first native call triggers loading
|
|
5
|
+
// exactly once.
|
|
6
|
+
|
|
7
|
+
import { getAddon, lazyAddon, type NativeAddon } from '../native'
|
|
8
|
+
|
|
9
|
+
/** The shared lazy addon proxy (typed as the full native surface). */
|
|
10
|
+
export const addon: NativeAddon = lazyAddon(getAddon)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// src/rust-ffi/batch/build.ts — `buildBatch` implementation.
|
|
2
|
+
//
|
|
3
|
+
// Packs an array of byte slices into the packed wire format, calls the native
|
|
4
|
+
// batch function, and unpacks the result into the friendly JS shape. Each
|
|
5
|
+
// method wires one `RustBatch` (./types.ts) member to its `*BatchPacked` napi
|
|
6
|
+
// fn via the shared pool-native resolver (context.ts).
|
|
7
|
+
//
|
|
8
|
+
// The `via` / `via2` builders below collapse the pack → native → unpack
|
|
9
|
+
// plumbing every bulk op shares; each public method keeps its exact signature,
|
|
10
|
+
// so the table stays type-safe against `RustBatch`.
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
schemaValidateBatch,
|
|
14
|
+
schemaValidateBatchCount,
|
|
15
|
+
unpackBitset,
|
|
16
|
+
unpackByteResults,
|
|
17
|
+
unpackI64ArrayAsBigInt,
|
|
18
|
+
unpackMultipartParts,
|
|
19
|
+
unpackU32Array,
|
|
20
|
+
unpackU64ArrayAsBigInt,
|
|
21
|
+
withPackScratch,
|
|
22
|
+
withPackScratch2,
|
|
23
|
+
} from '../../shared/packed'
|
|
24
|
+
import { type RustClientContext, resolvePoolNative } from '../context'
|
|
25
|
+
import type { RustBatch } from './types'
|
|
26
|
+
|
|
27
|
+
// Native batch fns are process-wide singletons (one lazy addon). `resolvePoolNative`
|
|
28
|
+
// resolves each once (triggering pool init + addon load on first use) and caches
|
|
29
|
+
// it, so the hot path skips the lazy-addon Proxy get on every call — the same
|
|
30
|
+
// cache `resolveNative`/packed use (context.ts), with pool init for the
|
|
31
|
+
// rayon-backed batch surface.
|
|
32
|
+
function nativeFn(ctx: RustClientContext, name: string): (...args: unknown[]) => unknown {
|
|
33
|
+
return resolvePoolNative(ctx, name)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build a single-list bulk op: `withPackScratch(items)` →
|
|
38
|
+
* `unpack(native(packed, ...extras))`. `R` is inferred from `unpack`, so the
|
|
39
|
+
* result stays assignable to the matching `RustBatch` method.
|
|
40
|
+
*/
|
|
41
|
+
function via<R>(
|
|
42
|
+
ctx: RustClientContext,
|
|
43
|
+
name: string,
|
|
44
|
+
unpack: (bytes: Uint8Array) => R,
|
|
45
|
+
extras: unknown[] = [],
|
|
46
|
+
): (items: Uint8Array[]) => R {
|
|
47
|
+
return (items) =>
|
|
48
|
+
withPackScratch(items, (packed) => unpack(nativeFn(ctx, name)(packed, ...extras) as Buffer))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build a two-list bulk op (docs+patches, items+sigs, ...): `withPackScratch2(a, b)`
|
|
53
|
+
* → `unpack(native(packedA, packedB, ...extras))`.
|
|
54
|
+
*/
|
|
55
|
+
function via2<R>(
|
|
56
|
+
ctx: RustClientContext,
|
|
57
|
+
name: string,
|
|
58
|
+
unpack: (bytes: Uint8Array) => R,
|
|
59
|
+
extras: unknown[] = [],
|
|
60
|
+
): (a: Uint8Array[], b: Uint8Array[]) => R {
|
|
61
|
+
return (a, b) =>
|
|
62
|
+
withPackScratch2(a, b, (packedA, packedB) =>
|
|
63
|
+
unpack(nativeFn(ctx, name)(packedA, packedB, ...extras) as Buffer),
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Build the `batch` namespace for a client context. */
|
|
68
|
+
export function buildBatch(ctx: RustClientContext): RustBatch {
|
|
69
|
+
return {
|
|
70
|
+
// ── Single-list, no extras ──
|
|
71
|
+
jsonValid: via(ctx, 'jsonValidBatchPacked', unpackBitset),
|
|
72
|
+
crc32: via(ctx, 'crc32BatchPacked', unpackU32Array),
|
|
73
|
+
validateEmail: via(ctx, 'validateEmailBatchPacked', unpackBitset),
|
|
74
|
+
validateUuid: via(ctx, 'validateUuidBatchPacked', unpackBitset),
|
|
75
|
+
validateIpv4: via(ctx, 'validateIpv4BatchPacked', unpackBitset),
|
|
76
|
+
validateIpv6: via(ctx, 'validateIpv6BatchPacked', unpackBitset),
|
|
77
|
+
jsonSumIds: via(ctx, 'jsonSumBatchPacked', unpackI64ArrayAsBigInt),
|
|
78
|
+
queryParse: via(ctx, 'queryParseBatchPacked', unpackByteResults),
|
|
79
|
+
cookieParse: via(ctx, 'cookieParseBatchPacked', unpackByteResults),
|
|
80
|
+
formParse: via(ctx, 'formParseBatchPacked', unpackByteResults),
|
|
81
|
+
signCookie: (items, secret) =>
|
|
82
|
+
via(ctx, 'signCookieBatchPacked', unpackByteResults, [secret])(items),
|
|
83
|
+
verifyCookie: (items, secret) =>
|
|
84
|
+
via(ctx, 'verifyCookieBatchPacked', unpackBitset, [secret])(items),
|
|
85
|
+
csrfVerify: (items, secret) =>
|
|
86
|
+
via(ctx, 'csrfVerifyBatchPacked', unpackBitset, [secret])(items),
|
|
87
|
+
httpParseRequest: via(ctx, 'httpParseRequestBatchPacked', unpackByteResults),
|
|
88
|
+
fnv1a64: via(ctx, 'fnv1A64BatchPacked', unpackU64ArrayAsBigInt),
|
|
89
|
+
etag: (items, weak) => via(ctx, 'etagBatchPacked', unpackByteResults, [weak ?? null])(items),
|
|
90
|
+
urlEncode: via(ctx, 'urlEncodeBatchPacked', unpackByteResults),
|
|
91
|
+
urlDecode: via(ctx, 'urlDecodeBatchPacked', unpackByteResults),
|
|
92
|
+
urlDecodeBytes: via(ctx, 'urlDecodeBytesBatchPacked', unpackByteResults),
|
|
93
|
+
mimeFromExtension: via(ctx, 'mimeFromExtensionBatchPacked', unpackByteResults),
|
|
94
|
+
wsAcceptKey: via(ctx, 'wsAcceptKeyBatchPacked', unpackByteResults),
|
|
95
|
+
base64UrlEncode: (items) =>
|
|
96
|
+
via(ctx, 'base64EncodeBatchPacked', unpackByteResults, [true, false])(items),
|
|
97
|
+
base64UrlDecode: (items) =>
|
|
98
|
+
via(ctx, 'base64DecodeBatchPacked', unpackByteResults, [true, false])(items),
|
|
99
|
+
jsonPatch: (docs, patches) => via2(ctx, 'jsonPatchBatchPacked', unpackByteResults)(docs, patches),
|
|
100
|
+
hexEncode: via(ctx, 'hexEncodeBatchPacked', unpackByteResults),
|
|
101
|
+
hexDecode: via(ctx, 'hexDecodeBatchPacked', unpackByteResults),
|
|
102
|
+
base64Encode: (items, urlSafe, padding) =>
|
|
103
|
+
via(ctx, 'base64EncodeBatchPacked', unpackByteResults, [urlSafe ?? null, padding ?? null])(items),
|
|
104
|
+
base64Decode: (items, urlSafe, padding) =>
|
|
105
|
+
via(ctx, 'base64DecodeBatchPacked', unpackByteResults, [urlSafe ?? null, padding ?? null])(items),
|
|
106
|
+
schemaValidate(validator, items) {
|
|
107
|
+
return schemaValidateBatch(validator, items)
|
|
108
|
+
},
|
|
109
|
+
schemaValidateCount(validator, items) {
|
|
110
|
+
return schemaValidateBatchCount(validator, items)
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// ── Backend-framework features ──
|
|
114
|
+
passwordHash: (passwords, salt, options) =>
|
|
115
|
+
via(ctx, 'passwordHashBatchPacked', unpackByteResults, [salt, options ?? null])(passwords),
|
|
116
|
+
aeadEncrypt: (key, nonce, items, algorithm) =>
|
|
117
|
+
via(ctx, 'aeadEncryptBatchPacked', unpackByteResults, [key, nonce, algorithm ?? null])(items),
|
|
118
|
+
aeadDecrypt: (key, nonce, items, algorithm) =>
|
|
119
|
+
via(ctx, 'aeadDecryptBatchPacked', unpackByteResults, [key, nonce, algorithm ?? null])(items),
|
|
120
|
+
gzipCompress: (items, level) =>
|
|
121
|
+
via(ctx, 'gzipCompressBatchPacked', unpackByteResults, [level ?? null])(items),
|
|
122
|
+
gzipDecompress: (items, maxDecompressed) =>
|
|
123
|
+
via(ctx, 'gzipDecompressBatchPacked', unpackByteResults, [maxDecompressed ?? null])(items),
|
|
124
|
+
brotliCompress: (items, quality) =>
|
|
125
|
+
via(ctx, 'brotliCompressBatchPacked', unpackByteResults, [quality ?? null])(items),
|
|
126
|
+
brotliDecompress: (items, maxDecompressed) =>
|
|
127
|
+
via(ctx, 'brotliDecompressBatchPacked', unpackByteResults, [maxDecompressed ?? null])(items),
|
|
128
|
+
jwtVerify: (tokens, secret, nowSeconds) =>
|
|
129
|
+
via(ctx, 'jwtVerifyBatchPacked', unpackBitset, [secret, nowSeconds])(tokens),
|
|
130
|
+
hmacSha256: (items, key) => via(ctx, 'hmacSha256BatchPacked', unpackByteResults, [key])(items),
|
|
131
|
+
hmacSha256Verify: (items, sigs, key) =>
|
|
132
|
+
via2(ctx, 'hmacSha256VerifyBatchPacked', unpackBitset, [key])(items, sigs),
|
|
133
|
+
// ── Two-list ──
|
|
134
|
+
passwordVerify: (items, phcs) => via2(ctx, 'passwordVerifyBatchPacked', unpackBitset)(items, phcs),
|
|
135
|
+
urlResolve: (bases, references) =>
|
|
136
|
+
via2(ctx, 'urlResolveBatchPacked', unpackByteResults)(bases, references),
|
|
137
|
+
templateRender: (source, contexts) =>
|
|
138
|
+
via(ctx, 'templateRenderBatchPacked', unpackByteResults, [source])(contexts),
|
|
139
|
+
jwtSign: (items, secret, ttlSeconds, nowSeconds) =>
|
|
140
|
+
via(ctx, 'jwtSignBatchPacked', unpackByteResults, [
|
|
141
|
+
secret,
|
|
142
|
+
ttlSeconds ?? null,
|
|
143
|
+
nowSeconds ?? Math.floor(Date.now() / 1000),
|
|
144
|
+
])(items),
|
|
145
|
+
sseEncode: (items, event, id, retry) =>
|
|
146
|
+
via(ctx, 'sseEncodeBatchPacked', unpackByteResults, [event ?? null, id ?? null, retry ?? null])(
|
|
147
|
+
items,
|
|
148
|
+
),
|
|
149
|
+
wsFrameEncode: (items, opcode, mask, fin) =>
|
|
150
|
+
via(ctx, 'wsFrameEncodeBatchPacked', unpackByteResults, [opcode, mask, fin])(items),
|
|
151
|
+
wsFrameDecode: via(ctx, 'wsFrameDecodeBatchPacked', unpackByteResults),
|
|
152
|
+
multipartParse: (items, boundary) =>
|
|
153
|
+
via(ctx, 'multipartParseBatchPacked', unpackMultipartParts, [boundary])(items),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// src/rust-ffi/batch/index.ts — Array-of-bytes batch namespace (barrel).
|
|
2
|
+
//
|
|
3
|
+
// Re-exports the `RustBatch` interface (./types.ts) and its builder
|
|
4
|
+
// (./build.ts) so existing `import { buildBatch, type RustBatch } from
|
|
5
|
+
// '../rust-ffi/batch'` call sites keep working.
|
|
6
|
+
|
|
7
|
+
export { buildBatch } from './build'
|
|
8
|
+
export type { RustBatch } from './types'
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/rust-ffi/batch/types.ts — Array-of-bytes batch namespace type.
|
|
2
|
+
//
|
|
3
|
+
// The `RustBatch` interface — the friendly JS shape of `rust.batch.*`
|
|
4
|
+
// (bitsets, u32 arrays, i64 arrays, byte results) — separated from the
|
|
5
|
+
// implementation (./build.ts) so type-only consumers don't pull the native
|
|
6
|
+
// resolver.
|
|
7
|
+
|
|
8
|
+
import type { MultipartPart, PasswordHashOptions } from '../../native'
|
|
9
|
+
import type { SchemaValidator } from '../../shared/packed'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The `rust.batch.*` array-of-bytes batch surface: N items in → one packed
|
|
13
|
+
* native call → bitsets / typed arrays / byte arrays out. Separated from the
|
|
14
|
+
* implementation (`./build.ts`) so type-only consumers don't pull the native
|
|
15
|
+
* resolver.
|
|
16
|
+
*/
|
|
17
|
+
export interface RustBatch {
|
|
18
|
+
jsonValid(items: Uint8Array[]): Uint8Array
|
|
19
|
+
crc32(items: Uint8Array[]): Uint32Array
|
|
20
|
+
validateEmail(items: Uint8Array[]): Uint8Array
|
|
21
|
+
validateUuid(items: Uint8Array[]): Uint8Array
|
|
22
|
+
validateIpv4(items: Uint8Array[]): Uint8Array
|
|
23
|
+
validateIpv6(items: Uint8Array[]): Uint8Array
|
|
24
|
+
jsonSumIds(items: Uint8Array[]): BigInt64Array
|
|
25
|
+
queryParse(items: Uint8Array[]): Uint8Array[]
|
|
26
|
+
cookieParse(items: Uint8Array[]): Uint8Array[]
|
|
27
|
+
httpParseRequest(items: Uint8Array[]): Uint8Array[]
|
|
28
|
+
jsonPatch(docs: Uint8Array[], patches: Uint8Array[]): Uint8Array[]
|
|
29
|
+
hexEncode(items: Uint8Array[]): Uint8Array[]
|
|
30
|
+
hexDecode(items: Uint8Array[]): Uint8Array[]
|
|
31
|
+
base64Encode(items: Uint8Array[], urlSafe?: boolean, padding?: boolean): Uint8Array[]
|
|
32
|
+
base64Decode(items: Uint8Array[], urlSafe?: boolean, padding?: boolean): Uint8Array[]
|
|
33
|
+
formParse(items: Uint8Array[]): Uint8Array[]
|
|
34
|
+
signCookie(items: Uint8Array[], secret: Uint8Array): Uint8Array[]
|
|
35
|
+
verifyCookie(items: Uint8Array[], secret: Uint8Array): Uint8Array
|
|
36
|
+
csrfVerify(items: Uint8Array[], secret: Uint8Array): Uint8Array
|
|
37
|
+
schemaValidate(validator: SchemaValidator, items: Uint8Array[]): Uint8Array
|
|
38
|
+
schemaValidateCount(validator: SchemaValidator, items: Uint8Array[]): number
|
|
39
|
+
fnv1a64(items: Uint8Array[]): BigUint64Array
|
|
40
|
+
etag(items: Uint8Array[], weak?: boolean | null): Uint8Array[]
|
|
41
|
+
urlEncode(items: Uint8Array[]): Uint8Array[]
|
|
42
|
+
urlDecode(items: Uint8Array[]): Uint8Array[]
|
|
43
|
+
urlDecodeBytes(items: Uint8Array[]): Uint8Array[]
|
|
44
|
+
mimeFromExtension(items: Uint8Array[]): Uint8Array[]
|
|
45
|
+
wsAcceptKey(items: Uint8Array[]): Uint8Array[]
|
|
46
|
+
base64UrlEncode(items: Uint8Array[]): Uint8Array[]
|
|
47
|
+
base64UrlDecode(items: Uint8Array[]): Uint8Array[]
|
|
48
|
+
|
|
49
|
+
// ── Backend-framework features ──
|
|
50
|
+
passwordHash(
|
|
51
|
+
passwords: Uint8Array[],
|
|
52
|
+
salt: Uint8Array,
|
|
53
|
+
options?: PasswordHashOptions | null,
|
|
54
|
+
): Uint8Array[]
|
|
55
|
+
aeadEncrypt(
|
|
56
|
+
key: Uint8Array,
|
|
57
|
+
nonce: Uint8Array,
|
|
58
|
+
items: Uint8Array[],
|
|
59
|
+
algorithm?: string | null,
|
|
60
|
+
): Uint8Array[]
|
|
61
|
+
aeadDecrypt(
|
|
62
|
+
key: Uint8Array,
|
|
63
|
+
nonce: Uint8Array,
|
|
64
|
+
items: Uint8Array[],
|
|
65
|
+
algorithm?: string | null,
|
|
66
|
+
): Uint8Array[]
|
|
67
|
+
gzipCompress(items: Uint8Array[], level?: number | null): Uint8Array[]
|
|
68
|
+
gzipDecompress(items: Uint8Array[], maxDecompressed?: number | null): Uint8Array[]
|
|
69
|
+
brotliCompress(items: Uint8Array[], quality?: number | null): Uint8Array[]
|
|
70
|
+
brotliDecompress(items: Uint8Array[], maxDecompressed?: number | null): Uint8Array[]
|
|
71
|
+
jwtVerify(tokens: Uint8Array[], secret: Uint8Array, nowSeconds: number): Uint8Array
|
|
72
|
+
hmacSha256(items: Uint8Array[], key: Uint8Array): Uint8Array[]
|
|
73
|
+
hmacSha256Verify(items: Uint8Array[], sigs: Uint8Array[], key: Uint8Array): Uint8Array
|
|
74
|
+
passwordVerify(items: Uint8Array[], phcs: Uint8Array[]): Uint8Array
|
|
75
|
+
urlResolve(bases: Uint8Array[], references: Uint8Array[]): Uint8Array[]
|
|
76
|
+
jwtSign(
|
|
77
|
+
items: Uint8Array[],
|
|
78
|
+
secret: Uint8Array,
|
|
79
|
+
ttlSeconds?: number | null,
|
|
80
|
+
nowSeconds?: number,
|
|
81
|
+
): Uint8Array[]
|
|
82
|
+
sseEncode(
|
|
83
|
+
items: Uint8Array[],
|
|
84
|
+
event?: string | null,
|
|
85
|
+
id?: string | null,
|
|
86
|
+
retry?: number | null,
|
|
87
|
+
): Uint8Array[]
|
|
88
|
+
wsFrameEncode(items: Uint8Array[], opcode: number, mask: boolean, fin: boolean): Uint8Array[]
|
|
89
|
+
wsFrameDecode(items: Uint8Array[]): Uint8Array[]
|
|
90
|
+
multipartParse(items: Uint8Array[], boundary: Uint8Array): MultipartPart[][]
|
|
91
|
+
templateRender(source: string, contexts: Uint8Array[]): Uint8Array[]
|
|
92
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
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 { buildBatch, type RustBatch } from './batch'
|
|
7
|
+
import { createContext } from './context'
|
|
8
|
+
import type { RustOptions } from './options'
|
|
9
|
+
import { buildPacked, type RustPacked } from './packed'
|
|
10
|
+
import { buildScalar, type RustScalar } from './scalar'
|
|
11
|
+
import { buildText, type RustText } from './text'
|
|
12
|
+
|
|
13
|
+
/** The full Rust FFI client (scalar methods + namespaces + configuration). */
|
|
14
|
+
export interface RustClient extends RustScalar {
|
|
15
|
+
// ── Namespaces ──
|
|
16
|
+
text: RustText
|
|
17
|
+
batch: RustBatch
|
|
18
|
+
packed: RustPacked
|
|
19
|
+
|
|
20
|
+
// ── Transport introspection ──
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the native transport in use on this runtime: `"ffi"` (bun:ffi —
|
|
23
|
+
* the PRIMARY transport under Bun) or `"napi"` (Node, forced
|
|
24
|
+
* `CASTRUM_FFI_MODE=napi`, or a failed ffi bind-time self-test).
|
|
25
|
+
*/
|
|
26
|
+
transport(): 'ffi' | 'napi'
|
|
27
|
+
/** Whether the bun:ffi transport is live (equivalent to `transport() === "ffi"`). */
|
|
28
|
+
ffiActive(): boolean
|
|
29
|
+
|
|
30
|
+
// ── Configuration ──
|
|
31
|
+
/**
|
|
32
|
+
* Update defaults on this instance. `mimeCache` / `hmacCache` apply
|
|
33
|
+
* immediately (per-instance). `rayonThreads` is applied only if the
|
|
34
|
+
* process-wide pool is not yet initialized (see {@link RustOptions}).
|
|
35
|
+
*/
|
|
36
|
+
configure(options: RustOptions): void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create an isolated Rust client with the given defaults.
|
|
41
|
+
*
|
|
42
|
+
* `mimeCache` / `hmacCache` are per-instance. `rayonThreads` only applies
|
|
43
|
+
* before the process-wide rayon pool is initialized (once per process — see
|
|
44
|
+
* {@link RustOptions.rayonThreads}); the pool defaults to the env var or
|
|
45
|
+
* `max(1, hardwareConcurrency - 1)` otherwise.
|
|
46
|
+
*/
|
|
47
|
+
export function createRust(options: RustOptions = {}): RustClient {
|
|
48
|
+
const ctx = createContext(options)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...buildScalar(ctx),
|
|
52
|
+
text: buildText(ctx),
|
|
53
|
+
batch: buildBatch(ctx),
|
|
54
|
+
packed: buildPacked(ctx),
|
|
55
|
+
|
|
56
|
+
// ── Transport introspection (FFI-primary on Bun; napi is the fallback) ──
|
|
57
|
+
transport() {
|
|
58
|
+
return ctx.runtime.transport.name
|
|
59
|
+
},
|
|
60
|
+
ffiActive() {
|
|
61
|
+
return ctx.runtime.transport.ffiActive()
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// ── Configuration ──
|
|
65
|
+
configure(next) {
|
|
66
|
+
// rayonThreads only takes effect BEFORE the pool is first used.
|
|
67
|
+
if (next.rayonThreads !== undefined && !ctx.isPoolInitialized()) {
|
|
68
|
+
ctx.setPendingThreads(next.rayonThreads)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (next.mimeCache !== undefined) {
|
|
72
|
+
ctx.state.mimeCache = next.mimeCache
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (next.hmacCache !== undefined) {
|
|
76
|
+
ctx.state.hmacCache = next.hmacCache
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The default, ready-to-use `RustClient` created with no options (resolves
|
|
84
|
+
* rayon threads from the host automatically). Most consumers just do
|
|
85
|
+
* `import { rust } from "castrum"` and call `rust.<fn>(...)` directly.
|
|
86
|
+
*/
|
|
87
|
+
export const rust = createRust()
|
|
@@ -0,0 +1,193 @@
|
|
|
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 type { HmacSignerInstance } from '../native'
|
|
10
|
+
import type { BunFFI } from '../native/ffi'
|
|
11
|
+
import { runtimeNative } from '../runtime/native'
|
|
12
|
+
import type { NativeRuntimeAdapter, TransportAdapter } from '../runtime/types'
|
|
13
|
+
import { addon } from './addon'
|
|
14
|
+
import type { RustOptions } from './options'
|
|
15
|
+
import { normalizeExt, resolveRayonThreads } from './options'
|
|
16
|
+
|
|
17
|
+
// Module-level cache of resolved native fns. The native addon is a process
|
|
18
|
+
// singleton, so the resolved function is identical across all client contexts;
|
|
19
|
+
// caching it here lets scalar/packed/text wrappers skip the lazy-addon Proxy
|
|
20
|
+
// `get` trap (a branch + `Reflect.get`) on every call — the same pattern
|
|
21
|
+
// batch.ts uses for its packed entries.
|
|
22
|
+
const nativeFnCache = new Map<string, unknown>()
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a native op on first use and cache it for all subsequent calls —
|
|
26
|
+
* FFI-FIRST via the runtime adapter's transport (`bun:ffi` on Bun, napi
|
|
27
|
+
* fallback otherwise), so builders no longer repeat `getBunFFI()` inline.
|
|
28
|
+
* Preserves the lazy-load contract (the addon is only loaded on the first
|
|
29
|
+
* real native call) while removing per-call Proxy overhead.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveNative(
|
|
32
|
+
ctx: RustClientContext,
|
|
33
|
+
name: string,
|
|
34
|
+
): (...args: unknown[]) => unknown {
|
|
35
|
+
let f = nativeFnCache.get(name)
|
|
36
|
+
if (f === undefined) {
|
|
37
|
+
const fn = ctx.runtime.transport.resolve(name)
|
|
38
|
+
f = fn ?? (ctx.addon as unknown as Record<string, unknown>)[name]
|
|
39
|
+
nativeFnCache.set(name, f)
|
|
40
|
+
}
|
|
41
|
+
return f as (...args: unknown[]) => unknown
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a native fn and ensure the process-wide rayon pool is initialized
|
|
46
|
+
* on first use — the rayon-backed `batch`/`packed` surfaces need this, while
|
|
47
|
+
* `resolveNative` (plain scalars) must NOT init the pool so
|
|
48
|
+
* `rust.configure({ rayonThreads })` can still take effect at startup.
|
|
49
|
+
* Shares the SAME `nativeFnCache` as `resolveNative` (the batch fn names are
|
|
50
|
+
* disjoint from the scalar names, so the shared cache is safe). FFI-first via
|
|
51
|
+
* the runtime adapter's transport, like `resolveNative`.
|
|
52
|
+
*/
|
|
53
|
+
export function resolvePoolNative(
|
|
54
|
+
ctx: RustClientContext,
|
|
55
|
+
name: string,
|
|
56
|
+
): (...args: unknown[]) => unknown {
|
|
57
|
+
let f = nativeFnCache.get(name)
|
|
58
|
+
if (f === undefined) {
|
|
59
|
+
ctx.ensurePool()
|
|
60
|
+
const fn = ctx.runtime.transport.resolve(name)
|
|
61
|
+
f = fn ?? (ctx.addon as unknown as Record<string, unknown>)[name]
|
|
62
|
+
nativeFnCache.set(name, f)
|
|
63
|
+
}
|
|
64
|
+
return f as (...args: unknown[]) => unknown
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Lazy memoized `bun:ffi` resolver for the scalar builders.
|
|
69
|
+
*
|
|
70
|
+
* Returns a closure that resolves `transport.ffi` ONCE — on the first call —
|
|
71
|
+
* and reads the local from then on. This keeps the FFI bind DEFERRED
|
|
72
|
+
* (importing `rust` does not eagerly dlopen; the builder factories run at
|
|
73
|
+
* module load) while removing the per-call `() => transport.ffi` getter hop
|
|
74
|
+
* from the hot path. The transport's own cache already makes the steady-state
|
|
75
|
+
* resolve a single `undefined`-check; this collapses the getter + inner
|
|
76
|
+
* closure into one local read.
|
|
77
|
+
*/
|
|
78
|
+
export function memoizeFfi(transport: TransportAdapter): () => BunFFI | null {
|
|
79
|
+
let resolved: BunFFI | null | undefined
|
|
80
|
+
return () => {
|
|
81
|
+
if (resolved === undefined) resolved = transport.ffi
|
|
82
|
+
return resolved
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Shared per-instance state passed to the namespace/scalar builders. */
|
|
87
|
+
export interface RustClientContext {
|
|
88
|
+
/** The shared lazy addon proxy (napi surface). */
|
|
89
|
+
addon: typeof addon
|
|
90
|
+
/** The runtime adapter seam (Bun/Node — builtins + ffi/napi transport). */
|
|
91
|
+
runtime: NativeRuntimeAdapter
|
|
92
|
+
/** Per-instance toggles (mime/hmac caching). */
|
|
93
|
+
state: { mimeCache: boolean; hmacCache: boolean }
|
|
94
|
+
mimeByText: Map<string, Uint8Array>
|
|
95
|
+
hmacSigners: WeakMap<Uint8Array, HmacSignerInstance>
|
|
96
|
+
|
|
97
|
+
/** Initialize the process-wide rayon pool exactly once (no-op after). */
|
|
98
|
+
ensurePool(): void
|
|
99
|
+
/** Whether the rayon pool state has been established on this instance. */
|
|
100
|
+
isPoolInitialized(): boolean
|
|
101
|
+
/** Record the caller's intent for the process-wide pool before it inits. */
|
|
102
|
+
setPendingThreads(threads: number | undefined): void
|
|
103
|
+
/** Mark the pool state as established (explicit initThreadPool calls). */
|
|
104
|
+
markPoolInitialized(): void
|
|
105
|
+
/** Cached MIME lookup (returns the cached bytes by reference — do not mutate). */
|
|
106
|
+
cachedMime(ext: Uint8Array): Uint8Array
|
|
107
|
+
/** Cached HMAC signer lookup. */
|
|
108
|
+
hmacSigner(key: Uint8Array): HmacSignerInstance
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Build a per-instance context from client options. */
|
|
112
|
+
export function createContext(options: RustOptions): RustClientContext {
|
|
113
|
+
const mimeByText = new Map<string, Uint8Array>()
|
|
114
|
+
const hmacSigners = new WeakMap<Uint8Array, HmacSignerInstance>()
|
|
115
|
+
const state = {
|
|
116
|
+
mimeCache: options.mimeCache !== false,
|
|
117
|
+
hmacCache: options.hmacCache !== false,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The native rayon pool is a process-wide OnceLock: the FIRST call wins.
|
|
121
|
+
// We therefore defer init until the first batch/packed operation so that
|
|
122
|
+
// rust.configure({ rayonThreads }) called at startup actually takes effect
|
|
123
|
+
// (previously the pool was initialized eagerly at module load, silently
|
|
124
|
+
// ignoring later rayonThreads tuning).
|
|
125
|
+
let poolInitialized = false
|
|
126
|
+
let pendingThreads = options.rayonThreads
|
|
127
|
+
|
|
128
|
+
function ensurePool(): void {
|
|
129
|
+
if (poolInitialized) return
|
|
130
|
+
poolInitialized = true
|
|
131
|
+
addon.initThreadPool(resolveRayonThreads(pendingThreads))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isPoolInitialized(): boolean {
|
|
135
|
+
return poolInitialized
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function setPendingThreads(threads: number | undefined): void {
|
|
139
|
+
pendingThreads = threads
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function markPoolInitialized(): void {
|
|
143
|
+
poolInitialized = true
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function cachedMime(ext: Uint8Array): Uint8Array {
|
|
147
|
+
if (!state.mimeCache) {
|
|
148
|
+
return addon.mimeFromExtension(ext)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const key = normalizeExt(ext)
|
|
152
|
+
|
|
153
|
+
let val = mimeByText.get(key)
|
|
154
|
+
if (!val) {
|
|
155
|
+
val = addon.mimeFromExtension(ext)
|
|
156
|
+
mimeByText.set(key, val)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Return the cached bytes BY REFERENCE (no per-call defensive copy — a
|
|
160
|
+
// measurable win on the MIME parity op). The returned slice aliases the
|
|
161
|
+
// cache: callers must NOT mutate it (same contract as `generateRequestId`).
|
|
162
|
+
return val
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function hmacSigner(key: Uint8Array): HmacSignerInstance {
|
|
166
|
+
if (!state.hmacCache) {
|
|
167
|
+
return new addon.HmacSigner(key)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let signer = hmacSigners.get(key)
|
|
171
|
+
|
|
172
|
+
if (!signer) {
|
|
173
|
+
signer = new addon.HmacSigner(key)
|
|
174
|
+
hmacSigners.set(key, signer)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return signer
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
addon,
|
|
182
|
+
runtime: runtimeNative,
|
|
183
|
+
state,
|
|
184
|
+
mimeByText,
|
|
185
|
+
hmacSigners,
|
|
186
|
+
ensurePool,
|
|
187
|
+
isPoolInitialized,
|
|
188
|
+
setPendingThreads,
|
|
189
|
+
markPoolInitialized,
|
|
190
|
+
cachedMime,
|
|
191
|
+
hmacSigner,
|
|
192
|
+
}
|
|
193
|
+
}
|