castrum 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +153 -630
  2. package/castrum.linux-x64-gnu.node +0 -0
  3. package/dist/index.d.ts +1653 -1817
  4. package/dist/index.js +6172 -4294
  5. package/docs/API.md +337 -0
  6. package/docs/ARCHITECTURE.md +111 -17
  7. package/docs/BENCHMARKS.md +117 -49
  8. package/docs/CASE_STUDY.md +243 -0
  9. package/docs/ENVIRONMENT.md +24 -7
  10. package/docs/FFI_BUN_GUIDE.md +335 -0
  11. package/docs/GETTING_STARTED.md +18 -34
  12. package/docs/HOW_TO_ADD_AN_OP.md +180 -0
  13. package/docs/INGRESS-ROUTER.md +82 -0
  14. package/docs/INGRESS.md +11 -7
  15. package/docs/NATIVE-ROUTE.md +77 -0
  16. package/docs/REPO_MAP.md +103 -53
  17. package/docs/adr/0001-two-wire-formats.md +1 -1
  18. package/docs/adr/0003-bun-builtins-delegation.md +6 -5
  19. package/docs/ai/LOCAL_DEV.md +81 -0
  20. package/docs/ai/TREE.md +1070 -0
  21. package/docs/bun-builtins-decision-matrix.md +25 -18
  22. package/index.ts +9 -16
  23. package/package.json +41 -28
  24. package/src/ingress/body.ts +167 -27
  25. package/src/ingress/decode/result-base.ts +10 -2
  26. package/src/ingress/handlers.ts +116 -285
  27. package/src/ingress/headers/security.ts +41 -0
  28. package/src/ingress/health.ts +1 -0
  29. package/src/ingress/index.ts +45 -169
  30. package/src/ingress/native-route.ts +182 -0
  31. package/src/ingress/packing/gather-raw-headers.ts +110 -22
  32. package/src/ingress/packing/header-packing.ts +5 -5
  33. package/src/ingress/packing/input-packer.ts +17 -3
  34. package/src/ingress/packing/route-wire.ts +197 -0
  35. package/src/ingress/packing/select-headers.ts +20 -7
  36. package/src/ingress/path-matcher.ts +115 -0
  37. package/src/ingress/response/baked-response.ts +302 -0
  38. package/src/ingress/router.ts +277 -0
  39. package/src/ingress/routes/common.ts +50 -1
  40. package/src/ingress/routes/fallback.ts +2 -2
  41. package/src/ingress/routes/head.ts +3 -13
  42. package/src/ingress/routes/index.ts +5 -4
  43. package/src/ingress/routes/json-write.ts +26 -47
  44. package/src/ingress/routes/native.ts +130 -0
  45. package/src/ingress/routes/options.ts +2 -12
  46. package/src/ingress/routes/read.ts +3 -13
  47. package/src/ingress/routes/responder.ts +159 -0
  48. package/src/ingress/server-node.ts +7 -7
  49. package/src/ingress/server.ts +83 -112
  50. package/src/ingress/sync.ts +151 -0
  51. package/src/ingress/types.ts +44 -1
  52. package/src/integration/pipeline.ts +3 -0
  53. package/src/integration/streaming.ts +2 -3
  54. package/src/integration/websocket.ts +4 -3
  55. package/src/loader/batch.ts +4 -0
  56. package/src/loader/cost.ts +1 -0
  57. package/src/loader/index.ts +43 -228
  58. package/src/loader/state.ts +84 -0
  59. package/src/loader/types.ts +145 -0
  60. package/src/native/ffi/build/codecs.ts +736 -0
  61. package/src/native/ffi/build/compress.ts +248 -0
  62. package/src/native/ffi/build/instances.ts +598 -0
  63. package/src/native/ffi/build/parse.ts +539 -0
  64. package/src/native/ffi/build/util.ts +140 -0
  65. package/src/native/ffi/build.ts +94 -0
  66. package/src/native/ffi/constants.ts +58 -0
  67. package/src/native/ffi/selftest.ts +22 -0
  68. package/src/native/ffi/types.ts +499 -0
  69. package/src/native/ffi.ts +307 -1357
  70. package/src/native/loader.ts +52 -6
  71. package/src/native/types/instances.ts +279 -0
  72. package/src/native/types.ts +41 -238
  73. package/src/runtime/builtins.ts +131 -0
  74. package/src/runtime/codec.ts +147 -0
  75. package/src/runtime/detect.ts +36 -0
  76. package/src/runtime/index.ts +28 -0
  77. package/src/runtime/native.ts +35 -0
  78. package/src/runtime/server.ts +31 -0
  79. package/src/runtime/transport.ts +64 -0
  80. package/src/runtime/types.ts +173 -0
  81. package/src/runtime/uuid.ts +45 -0
  82. package/src/runtime/websocket.ts +30 -0
  83. package/src/rust-ffi/addon.ts +1 -1
  84. package/src/rust-ffi/batch/build.ts +155 -0
  85. package/src/rust-ffi/batch/index.ts +8 -0
  86. package/src/rust-ffi/batch/types.ts +92 -0
  87. package/src/rust-ffi/client.ts +5 -6
  88. package/src/rust-ffi/context.ts +39 -10
  89. package/src/rust-ffi/index.ts +28 -17
  90. package/src/rust-ffi/into.ts +21 -0
  91. package/src/rust-ffi/options.ts +1 -1
  92. package/src/rust-ffi/packed.ts +40 -2
  93. package/src/rust-ffi/proven.ts +16 -30
  94. package/src/rust-ffi/scalar/crypto.ts +188 -107
  95. package/src/rust-ffi/scalar/factories.ts +257 -27
  96. package/src/rust-ffi/scalar/hashing.ts +43 -41
  97. package/src/rust-ffi/scalar/http.ts +208 -77
  98. package/src/rust-ffi/scalar/index.ts +3 -3
  99. package/src/rust-ffi/scalar/interface.ts +88 -213
  100. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  101. package/src/rust-ffi/scalar/json.ts +28 -12
  102. package/src/rust-ffi/scalar/payload.ts +159 -42
  103. package/src/rust-ffi/text.ts +31 -5
  104. package/src/selection.ts +27 -31
  105. package/src/shared/adaptive.ts +1 -2
  106. package/src/shared/buffer-pool.ts +72 -17
  107. package/src/shared/bytes.ts +75 -6
  108. package/src/shared/codec.ts +78 -0
  109. package/src/shared/env.ts +4 -4
  110. package/src/shared/log.ts +1 -0
  111. package/src/shared/packed/index.ts +10 -0
  112. package/src/shared/packed/parsers.ts +31 -0
  113. package/src/shared/packed/schema.ts +23 -0
  114. package/src/shared/{packed.ts → packed/wire.ts} +31 -39
  115. package/src/shared/proven.ts +189 -496
  116. package/src/shared/runtime.ts +20 -25
  117. package/src/shared/uuid.ts +12 -28
  118. package/src/data/json-rows.ts +0 -33
  119. package/src/rust-ffi/batch.ts +0 -431
  120. package/src/shared/bench-classify.ts +0 -209
@@ -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
+ }
@@ -4,7 +4,7 @@
4
4
  // module does NOT dlopen the addon — the first native call triggers loading
5
5
  // exactly once.
6
6
 
7
- import { lazyAddon, getAddon, type NativeAddon } from '../native'
7
+ import { getAddon, lazyAddon, type NativeAddon } from '../native'
8
8
 
9
9
  /** The shared lazy addon proxy (typed as the full native surface). */
10
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
+ }
@@ -3,13 +3,12 @@
3
3
  // Composes the per-namespace builders (scalar / text / batch / packed) over a
4
4
  // shared per-instance context (./context.ts) and exposes `configure`.
5
5
 
6
- import { createContext } from './context'
7
- import { buildText, type RustText } from './text'
8
6
  import { buildBatch, type RustBatch } from './batch'
7
+ import { createContext } from './context'
8
+ import type { RustOptions } from './options'
9
9
  import { buildPacked, type RustPacked } from './packed'
10
10
  import { buildScalar, type RustScalar } from './scalar'
11
- import { getBunFFI } from '../native/ffi'
12
- import type { RustOptions } from './options'
11
+ import { buildText, type RustText } from './text'
13
12
 
14
13
  /** The full Rust FFI client (scalar methods + namespaces + configuration). */
15
14
  export interface RustClient extends RustScalar {
@@ -56,10 +55,10 @@ export function createRust(options: RustOptions = {}): RustClient {
56
55
 
57
56
  // ── Transport introspection (FFI-primary on Bun; napi is the fallback) ──
58
57
  transport() {
59
- return getBunFFI() !== null ? 'ffi' : 'napi'
58
+ return ctx.runtime.transport.name
60
59
  },
61
60
  ffiActive() {
62
- return getBunFFI() !== null
61
+ return ctx.runtime.transport.ffiActive()
63
62
  },
64
63
 
65
64
  // ── Configuration ──
@@ -6,11 +6,13 @@
6
6
  // (initialized flag / pending thread count) stays encapsulated behind accessor
7
7
  // functions so it cannot be corrupted externally.
8
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'
9
13
  import { addon } from './addon'
10
- import { resolveRayonThreads } from './options'
11
- import { normalizeExt } from './options'
12
14
  import type { RustOptions } from './options'
13
- import type { HmacSignerInstance } from '../native'
15
+ import { normalizeExt, resolveRayonThreads } from './options'
14
16
 
15
17
  // Module-level cache of resolved native fns. The native addon is a process
16
18
  // singleton, so the resolved function is identical across all client contexts;
@@ -20,9 +22,11 @@ import type { HmacSignerInstance } from '../native'
20
22
  const nativeFnCache = new Map<string, unknown>()
21
23
 
22
24
  /**
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.
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.
26
30
  */
27
31
  export function resolveNative(
28
32
  ctx: RustClientContext,
@@ -30,7 +34,8 @@ export function resolveNative(
30
34
  ): (...args: unknown[]) => unknown {
31
35
  let f = nativeFnCache.get(name)
32
36
  if (f === undefined) {
33
- f = (ctx.addon as unknown as Record<string, unknown>)[name]
37
+ const fn = ctx.runtime.transport.resolve(name)
38
+ f = fn ?? (ctx.addon as unknown as Record<string, unknown>)[name]
34
39
  nativeFnCache.set(name, f)
35
40
  }
36
41
  return f as (...args: unknown[]) => unknown
@@ -42,7 +47,8 @@ export function resolveNative(
42
47
  * `resolveNative` (plain scalars) must NOT init the pool so
43
48
  * `rust.configure({ rayonThreads })` can still take effect at startup.
44
49
  * Shares the SAME `nativeFnCache` as `resolveNative` (the batch fn names are
45
- * disjoint from the scalar names, so the shared cache is safe).
50
+ * disjoint from the scalar names, so the shared cache is safe). FFI-first via
51
+ * the runtime adapter's transport, like `resolveNative`.
46
52
  */
47
53
  export function resolvePoolNative(
48
54
  ctx: RustClientContext,
@@ -51,16 +57,38 @@ export function resolvePoolNative(
51
57
  let f = nativeFnCache.get(name)
52
58
  if (f === undefined) {
53
59
  ctx.ensurePool()
54
- f = (ctx.addon as unknown as Record<string, unknown>)[name]
60
+ const fn = ctx.runtime.transport.resolve(name)
61
+ f = fn ?? (ctx.addon as unknown as Record<string, unknown>)[name]
55
62
  nativeFnCache.set(name, f)
56
63
  }
57
64
  return f as (...args: unknown[]) => unknown
58
65
  }
59
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
+
60
86
  /** Shared per-instance state passed to the namespace/scalar builders. */
61
87
  export interface RustClientContext {
62
- /** The shared lazy addon proxy. */
88
+ /** The shared lazy addon proxy (napi surface). */
63
89
  addon: typeof addon
90
+ /** The runtime adapter seam (Bun/Node — builtins + ffi/napi transport). */
91
+ runtime: NativeRuntimeAdapter
64
92
  /** Per-instance toggles (mime/hmac caching). */
65
93
  state: { mimeCache: boolean; hmacCache: boolean }
66
94
  mimeByText: Map<string, Uint8Array>
@@ -151,6 +179,7 @@ export function createContext(options: RustOptions): RustClientContext {
151
179
 
152
180
  return {
153
181
  addon,
182
+ runtime: runtimeNative,
154
183
  state,
155
184
  mimeByText,
156
185
  hmacSigners,
@@ -26,29 +26,40 @@
26
26
  // rust.packed.jsonValidBatchPacked(p); // raw packed in/out
27
27
  // rust.configure({ rayonThreads: 8 }); // override defaults
28
28
 
29
- export { createRust, rust } from './client'
30
- export type { RustClient } from './client'
31
- export type { RustOptions } from './options'
32
- export type { RustText } from './text'
33
- export type { RustBatch } from './batch'
34
- export type { RustPacked } from './packed'
35
- export type { SchemaValidator } from '../shared/packed'
36
29
  export type {
37
- HmacSignerInstance,
38
- SchemaValidatorInstance,
39
- SchemaError,
40
- TemplateRendererInstance,
41
- FormParserInstance,
42
- MediaTypeParserInstance,
43
- MediaTypeResult,
44
- ConditionalRequestInstance,
45
- EncodingPrefResult,
46
30
  AcceptNegotiatorInstance,
47
31
  Base64CodecInstance,
32
+ ConditionalRequestInstance,
48
33
  CookieSignerInstance,
49
34
  CsrfProtectorInstance,
50
- UrlBuilderInstance,
35
+ EncodingPrefResult,
36
+ FormParserInstance,
37
+ HmacSignerInstance,
38
+ MediaTypeParserInstance,
39
+ MediaTypeResult,
51
40
  MultipartPart,
52
41
  PasswordHashOptions,
42
+ SchemaError,
43
+ SchemaValidatorInstance,
44
+ TemplateRendererInstance,
45
+ UrlBuilderInstance,
53
46
  WsFrame,
54
47
  } from '../native'
48
+ export type { SchemaValidator } from '../shared/packed'
49
+ export type { RustBatch } from './batch'
50
+ export type { RustClient } from './client'
51
+ export { createRust, rust } from './client'
52
+ export { proven } from './proven'
53
+ export {
54
+ PROVEN_SELECTION,
55
+ isProven,
56
+ provenEntry,
57
+ provenImpl,
58
+ provenStatus,
59
+ provenSummary,
60
+ provenSurface,
61
+ } from '../shared/proven'
62
+ export type { ProvenEntry, ProvenImpl, ProvenStatus } from '../shared/proven'
63
+ export type { RustOptions } from './options'
64
+ export type { RustPacked } from './packed'
65
+ export type { RustText } from './text'
@@ -0,0 +1,21 @@
1
+ // src/rust-ffi/into.ts — pooled `*Into` output-write helper (napi fallback).
2
+
3
+ /**
4
+ * Write `bytes` into a caller-owned `output` buffer, or throw when it does not
5
+ * fit.
6
+ *
7
+ * Shared tail of the pooled (`*Into`) napi-fallback paths: every op copies the
8
+ * allocating result into the caller buffer with the same capacity check, error
9
+ * message, and byte-length return. The bun:ffi `_into` path never reaches this
10
+ * helper — the C ABI streams straight into `output`.
11
+ *
12
+ * @param op Label for the error message (e.g. `"gzip compress"`).
13
+ * @param output The caller-owned destination buffer.
14
+ * @param bytes The allocating op result to copy in.
15
+ * @returns The number of bytes written (`bytes.length`).
16
+ */
17
+ export function writeInto(op: string, output: Uint8Array, bytes: Uint8Array): number {
18
+ if (output.length < bytes.length) throw new Error(`${op}: output buffer too small`)
19
+ output.set(bytes)
20
+ return bytes.length
21
+ }
@@ -51,7 +51,7 @@ export function resolveRayonThreads(explicit?: number): number {
51
51
  }
52
52
 
53
53
  const envThreads = Number(
54
- resolveEnvVar('CASTRUM_RAYON_THREADS', ['RUST_BENCH_RAYON_THREADS', 'RUST_RAYON_THREADS']),
54
+ resolveEnvVar('CASTRUM_RAYON_THREADS'),
55
55
  )
56
56
 
57
57
  if (Number.isFinite(envThreads) && envThreads > 0) {