castrum 0.9.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.
Files changed (102) hide show
  1. package/README.md +153 -630
  2. package/castrum.linux-x64-gnu.node +0 -0
  3. package/dist/index.d.ts +1374 -1772
  4. package/dist/index.js +5570 -4169
  5. package/docs/API.md +337 -0
  6. package/docs/ARCHITECTURE.md +106 -14
  7. package/docs/BENCHMARKS.md +83 -47
  8. package/docs/CASE_STUDY.md +240 -0
  9. package/docs/ENVIRONMENT.md +8 -6
  10. package/docs/FFI_BUN_GUIDE.md +327 -0
  11. package/docs/GETTING_STARTED.md +15 -32
  12. package/docs/INGRESS-ROUTER.md +81 -0
  13. package/docs/INGRESS.md +5 -4
  14. package/docs/NATIVE-ROUTE.md +53 -0
  15. package/docs/REPO_MAP.md +46 -26
  16. package/docs/adr/0003-bun-builtins-delegation.md +6 -5
  17. package/docs/bun-builtins-decision-matrix.md +25 -18
  18. package/index.ts +9 -16
  19. package/package.json +36 -24
  20. package/src/ingress/body.ts +167 -27
  21. package/src/ingress/handlers.ts +91 -15
  22. package/src/ingress/health.ts +1 -0
  23. package/src/ingress/index.ts +39 -27
  24. package/src/ingress/packing/gather-raw-headers.ts +85 -21
  25. package/src/ingress/packing/header-packing.ts +5 -5
  26. package/src/ingress/packing/input-packer.ts +17 -3
  27. package/src/ingress/packing/select-headers.ts +12 -6
  28. package/src/ingress/router.ts +237 -0
  29. package/src/ingress/routes/common.ts +42 -1
  30. package/src/ingress/routes/fallback.ts +2 -2
  31. package/src/ingress/routes/head.ts +3 -13
  32. package/src/ingress/routes/index.ts +5 -4
  33. package/src/ingress/routes/json-write.ts +7 -31
  34. package/src/ingress/routes/options.ts +2 -12
  35. package/src/ingress/routes/read.ts +3 -13
  36. package/src/ingress/routes/responder.ts +159 -0
  37. package/src/ingress/server-node.ts +6 -6
  38. package/src/ingress/server.ts +45 -13
  39. package/src/ingress/types.ts +35 -1
  40. package/src/integration/pipeline.ts +3 -0
  41. package/src/integration/streaming.ts +2 -3
  42. package/src/integration/websocket.ts +4 -3
  43. package/src/loader/batch.ts +4 -0
  44. package/src/loader/cost.ts +1 -0
  45. package/src/loader/index.ts +43 -228
  46. package/src/loader/state.ts +84 -0
  47. package/src/loader/types.ts +145 -0
  48. package/src/native/ffi/constants.ts +58 -0
  49. package/src/native/ffi/selftest.ts +729 -0
  50. package/src/native/ffi/types.ts +474 -0
  51. package/src/native/ffi.ts +1063 -829
  52. package/src/native/loader.ts +52 -6
  53. package/src/native/types/instances.ts +274 -0
  54. package/src/native/types.ts +41 -238
  55. package/src/runtime/builtins.ts +131 -0
  56. package/src/runtime/codec.ts +147 -0
  57. package/src/runtime/detect.ts +36 -0
  58. package/src/runtime/index.ts +28 -0
  59. package/src/runtime/native.ts +35 -0
  60. package/src/runtime/server.ts +31 -0
  61. package/src/runtime/transport.ts +64 -0
  62. package/src/runtime/types.ts +173 -0
  63. package/src/runtime/uuid.ts +45 -0
  64. package/src/runtime/websocket.ts +30 -0
  65. package/src/rust-ffi/addon.ts +1 -1
  66. package/src/rust-ffi/batch/build.ts +155 -0
  67. package/src/rust-ffi/batch/index.ts +8 -0
  68. package/src/rust-ffi/batch/types.ts +92 -0
  69. package/src/rust-ffi/client.ts +5 -6
  70. package/src/rust-ffi/context.ts +39 -10
  71. package/src/rust-ffi/index.ts +28 -17
  72. package/src/rust-ffi/into.ts +21 -0
  73. package/src/rust-ffi/options.ts +1 -1
  74. package/src/rust-ffi/packed.ts +2 -2
  75. package/src/rust-ffi/proven.ts +16 -30
  76. package/src/rust-ffi/scalar/crypto.ts +188 -107
  77. package/src/rust-ffi/scalar/factories.ts +253 -27
  78. package/src/rust-ffi/scalar/hashing.ts +43 -41
  79. package/src/rust-ffi/scalar/http.ts +208 -77
  80. package/src/rust-ffi/scalar/index.ts +3 -3
  81. package/src/rust-ffi/scalar/interface.ts +88 -213
  82. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  83. package/src/rust-ffi/scalar/json.ts +28 -12
  84. package/src/rust-ffi/scalar/payload.ts +159 -42
  85. package/src/rust-ffi/text.ts +31 -5
  86. package/src/selection.ts +27 -31
  87. package/src/shared/adaptive.ts +1 -2
  88. package/src/shared/buffer-pool.ts +72 -17
  89. package/src/shared/bytes.ts +75 -6
  90. package/src/shared/codec.ts +78 -0
  91. package/src/shared/env.ts +4 -4
  92. package/src/shared/log.ts +1 -0
  93. package/src/shared/packed/index.ts +10 -0
  94. package/src/shared/packed/parsers.ts +31 -0
  95. package/src/shared/packed/schema.ts +23 -0
  96. package/src/shared/{packed.ts → packed/wire.ts} +31 -39
  97. package/src/shared/proven.ts +189 -496
  98. package/src/shared/runtime.ts +20 -25
  99. package/src/shared/uuid.ts +12 -28
  100. package/src/data/json-rows.ts +0 -33
  101. package/src/rust-ffi/batch.ts +0 -431
  102. 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) {
@@ -3,9 +3,9 @@
3
3
  // `rust.packed.*` is the zero-copy API: callers pass already-packed buffers and
4
4
  // receive the raw packed output, plus the batch metadata/count helpers.
5
5
 
6
- import { asNumber } from './options'
7
- import { resolvePoolNative, type RustClientContext } from './context'
8
6
  import type { PasswordHashOptions } from '../native'
7
+ import { type RustClientContext, resolvePoolNative } from './context'
8
+ import { asNumber } from './options'
9
9
 
10
10
  /** Raw packed-wire FFI namespace. */
11
11
  export interface RustPacked {
@@ -1,42 +1,28 @@
1
- // src/rust-ffi/proven.ts — The performance-annotated export surface.
1
+ // src/rust-ffi/proven.ts — the baked "proven selection" surface.
2
2
  //
3
- // `proven` exposes the FULL `rust.*` surface. Each exported function's JSDoc
4
- // carries its measured performance vs the native JS baseline, annotated from
5
- // the CPU benchmark report by `scripts/annotate-performance.ts` (run via
6
- // `bun run check:annotate`). Functions that are SLOWER than the JS baseline
7
- // (e.g. `jsonParse` vs Bun's JSON.parse, or schema validation vs ajv) are
8
- // marked `@deprecated`.
9
- //
10
- // The registry (`src/shared/proven.ts`) is the single source of truth for the
11
- // annotations; `bun run check:proven` audits it against the latest benchmark
12
- // report (with `--fail` to gate CI).
3
+ // `proven` re-exposes the FULL `rust.*` surface (identical object) alongside
4
+ // the pure-data registry (`src/shared/proven.ts`) that states which
5
+ // implementation is the benchmark-proven winner for every op: 'native',
6
+ // 'js', or 'bun' (Bun built-in delegation). The winners are BAKED (committed,
7
+ // benchmark-derived) and verified by `test/unit/shared/proven.test.ts` — no
8
+ // live benchmark audit and no `@deprecated` JSDoc machinery (the old
9
+ // `check:proven` / `check:annotate` friction, intentionally not re-added).
13
10
 
14
11
  import { rust, type RustClient } from './client'
15
12
 
16
- /**
17
- * The performance-annotated client: the full {@link RustClient} surface.
18
- * Each exported function's JSDoc notes its measured performance vs the JS
19
- * baseline; functions slower than the baseline carry `@deprecated`.
20
- */
13
+ /** The full `rust.*` surface — identical to `rust`; the proven wiring is baked (see {@link ../shared/proven}). */
21
14
  export type ProvenClient = RustClient
22
15
 
23
- /**
24
- * The full `rust.*` surface with benchmark-annotated JSDoc. Identical to
25
- * `rust` — the annotations and `@deprecated` markers are what communicate
26
- * performance now, not a curated subset.
27
- */
16
+ /** The full `rust.*` surface, exposed for audit. Same object as `rust`. */
28
17
  export const proven: ProvenClient = rust
29
18
 
30
- // Re-export the registry + helpers so consumers can audit the surface.
31
19
  export {
32
- PROVEN_SURFACE,
33
- provenStatus,
20
+ PROVEN_SELECTION,
34
21
  isProven,
35
- provenSurface,
22
+ provenEntry,
23
+ provenImpl,
24
+ provenStatus,
36
25
  provenSummary,
26
+ provenSurface,
37
27
  } from '../shared/proven'
38
- export type {
39
- PerformanceStatus,
40
- ProvenEntry,
41
- ProvenKey,
42
- } from '../shared/proven'
28
+ export type { ProvenEntry, ProvenImpl, ProvenStatus } from '../shared/proven'