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.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -368
  3. package/castrum.linux-x64-gnu.node +0 -0
  4. package/dist/index.d.ts +2763 -0
  5. package/dist/index.js +8707 -0
  6. package/docs/API.md +337 -0
  7. package/docs/ARCHITECTURE.md +507 -0
  8. package/docs/BENCHMARKS.md +343 -0
  9. package/docs/CASE_STUDY.md +240 -0
  10. package/docs/ENVIRONMENT.md +81 -0
  11. package/docs/FFI_BUN_GUIDE.md +327 -0
  12. package/docs/GETTING_STARTED.md +192 -0
  13. package/docs/INGRESS-ROUTER.md +81 -0
  14. package/docs/INGRESS.md +746 -0
  15. package/docs/NATIVE-ROUTE.md +53 -0
  16. package/docs/REPO_MAP.md +279 -0
  17. package/docs/adr/0001-two-wire-formats.md +39 -0
  18. package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
  19. package/docs/adr/0003-bun-builtins-delegation.md +43 -0
  20. package/docs/bun-builtins-decision-matrix.md +115 -0
  21. package/index.ts +44 -8
  22. package/package.json +98 -33
  23. package/src/ingress/body.ts +289 -0
  24. package/src/ingress/constants.ts +220 -48
  25. package/src/ingress/context.ts +136 -0
  26. package/src/ingress/decode/baked-result.ts +121 -0
  27. package/src/ingress/decode/fast-result.ts +160 -0
  28. package/src/ingress/decode/packed-sections.ts +56 -0
  29. package/src/ingress/decode/result-base.ts +172 -0
  30. package/src/ingress/errors.ts +72 -0
  31. package/src/ingress/fast.ts +152 -1021
  32. package/src/ingress/handlers.ts +639 -1001
  33. package/src/ingress/headers/baked-templates.ts +102 -0
  34. package/src/ingress/headers/cors.ts +44 -0
  35. package/src/ingress/headers/fast-templates.ts +238 -0
  36. package/src/ingress/headers/hsts.ts +83 -0
  37. package/src/ingress/health.ts +68 -0
  38. package/src/ingress/index.ts +142 -405
  39. package/src/ingress/metrics.ts +132 -0
  40. package/src/ingress/options.ts +197 -0
  41. package/src/ingress/packing/gather-raw-headers.ts +139 -0
  42. package/src/ingress/packing/header-packing.ts +75 -0
  43. package/src/ingress/packing/input-packer.ts +164 -0
  44. package/src/ingress/packing/scratch.ts +50 -0
  45. package/src/ingress/packing/select-headers.ts +102 -0
  46. package/src/ingress/response/error-bodies.ts +75 -0
  47. package/src/ingress/response/terminal.ts +59 -0
  48. package/src/ingress/router.ts +237 -0
  49. package/src/ingress/routes/common.ts +79 -0
  50. package/src/ingress/routes/delete.ts +14 -0
  51. package/src/ingress/routes/echo.ts +107 -0
  52. package/src/ingress/routes/fallback.ts +23 -0
  53. package/src/ingress/routes/head.ts +27 -0
  54. package/src/ingress/routes/index.ts +11 -0
  55. package/src/ingress/routes/json-write.ts +129 -0
  56. package/src/ingress/routes/options.ts +26 -0
  57. package/src/ingress/routes/read.ts +35 -0
  58. package/src/ingress/routes/responder.ts +159 -0
  59. package/src/ingress/server-node.ts +376 -0
  60. package/src/ingress/server.ts +475 -0
  61. package/src/ingress/shared.ts +132 -0
  62. package/src/ingress/status.ts +97 -0
  63. package/src/ingress/types.ts +191 -0
  64. package/src/integration/batch.ts +58 -0
  65. package/src/integration/index.ts +29 -0
  66. package/src/integration/pipeline.ts +220 -0
  67. package/src/integration/streaming.ts +78 -0
  68. package/src/integration/websocket.ts +93 -0
  69. package/src/loader/batch.ts +137 -0
  70. package/src/loader/cost.ts +105 -0
  71. package/src/loader/index.ts +745 -0
  72. package/src/loader/ops.ts +657 -0
  73. package/src/loader/state.ts +84 -0
  74. package/src/loader/types.ts +145 -0
  75. package/src/native/ffi/constants.ts +58 -0
  76. package/src/native/ffi/selftest.ts +729 -0
  77. package/src/native/ffi/types.ts +474 -0
  78. package/src/native/ffi.ts +1775 -0
  79. package/src/native/index.ts +8 -207
  80. package/src/native/loader.ts +240 -0
  81. package/src/native/types/instances.ts +274 -0
  82. package/src/native/types.ts +401 -0
  83. package/src/runtime/builtins.ts +131 -0
  84. package/src/runtime/codec.ts +147 -0
  85. package/src/runtime/detect.ts +36 -0
  86. package/src/runtime/index.ts +28 -0
  87. package/src/runtime/native.ts +35 -0
  88. package/src/runtime/server.ts +31 -0
  89. package/src/runtime/transport.ts +64 -0
  90. package/src/runtime/types.ts +173 -0
  91. package/src/runtime/uuid.ts +45 -0
  92. package/src/runtime/websocket.ts +30 -0
  93. package/src/rust-ffi/addon.ts +10 -0
  94. package/src/rust-ffi/batch/build.ts +155 -0
  95. package/src/rust-ffi/batch/index.ts +8 -0
  96. package/src/rust-ffi/batch/types.ts +92 -0
  97. package/src/rust-ffi/client.ts +87 -0
  98. package/src/rust-ffi/context.ts +193 -0
  99. package/src/rust-ffi/index.ts +51 -541
  100. package/src/rust-ffi/into.ts +21 -0
  101. package/src/rust-ffi/options.ts +113 -0
  102. package/src/rust-ffi/packed.ts +253 -0
  103. package/src/rust-ffi/proven.ts +28 -0
  104. package/src/rust-ffi/scalar/crypto.ts +346 -0
  105. package/src/rust-ffi/scalar/factories.ts +459 -0
  106. package/src/rust-ffi/scalar/hashing.ts +74 -0
  107. package/src/rust-ffi/scalar/http.ts +337 -0
  108. package/src/rust-ffi/scalar/index.ts +31 -0
  109. package/src/rust-ffi/scalar/interface.ts +325 -0
  110. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  111. package/src/rust-ffi/scalar/json.ts +61 -0
  112. package/src/rust-ffi/scalar/payload.ts +241 -0
  113. package/src/rust-ffi/text.ts +121 -0
  114. package/src/selection.json +194 -0
  115. package/src/selection.ts +73 -0
  116. package/src/shared/adaptive.ts +67 -0
  117. package/src/shared/buffer-pool.ts +264 -0
  118. package/src/shared/bytes.ts +108 -9
  119. package/src/shared/codec.ts +78 -0
  120. package/src/shared/env.ts +20 -0
  121. package/src/shared/log.ts +75 -0
  122. package/src/shared/metrics.ts +247 -0
  123. package/src/shared/packed/index.ts +10 -0
  124. package/src/shared/packed/parsers.ts +31 -0
  125. package/src/shared/packed/schema.ts +23 -0
  126. package/src/shared/packed/wire.ts +504 -0
  127. package/src/shared/proven.ts +289 -0
  128. package/src/shared/request-id.ts +67 -0
  129. package/src/shared/response.ts +104 -0
  130. package/src/shared/runtime.ts +29 -0
  131. package/src/shared/trace.ts +81 -0
  132. package/src/shared/uuid.ts +15 -0
  133. package/Cargo.toml +0 -73
  134. package/bench.ts +0 -3
  135. package/rust/batch.rs +0 -195
  136. package/rust/cookie_parser.rs +0 -70
  137. package/rust/cors.rs +0 -263
  138. package/rust/hashing.rs +0 -164
  139. package/rust/headers.rs +0 -234
  140. package/rust/hmac_sha256.rs +0 -113
  141. package/rust/http_parser.rs +0 -132
  142. package/rust/ingress.rs +0 -616
  143. package/rust/ingress_constants.rs +0 -89
  144. package/rust/ip_trust.rs +0 -185
  145. package/rust/json_ops.rs +0 -66
  146. package/rust/json_patch_ops.rs +0 -25
  147. package/rust/json_schema.rs +0 -151
  148. package/rust/json_ser.rs +0 -429
  149. package/rust/lib.rs +0 -48
  150. package/rust/method.rs +0 -141
  151. package/rust/mime_lookup.rs +0 -76
  152. package/rust/output.rs +0 -238
  153. package/rust/proxy.rs +0 -90
  154. package/rust/query_parser.rs +0 -141
  155. package/rust/random_token.rs +0 -52
  156. package/rust/rate_limit.rs +0 -203
  157. package/rust/terminal.rs +0 -95
  158. package/rust/unit_tests.rs +0 -843
  159. package/rust/url_codec.rs +0 -133
  160. package/rust/util.rs +0 -537
  161. package/rust/validation.rs +0 -107
  162. package/rust/websocket.rs +0 -23
  163. package/src/baseline/index.ts +0 -12
  164. package/src/baseline/tasks/cookie.ts +0 -22
  165. package/src/baseline/tasks/hashing.ts +0 -18
  166. package/src/baseline/tasks/hmac.ts +0 -38
  167. package/src/baseline/tasks/http.ts +0 -62
  168. package/src/baseline/tasks/json-patch.ts +0 -12
  169. package/src/baseline/tasks/json.ts +0 -50
  170. package/src/baseline/tasks/mime.ts +0 -5
  171. package/src/baseline/tasks/query.ts +0 -14
  172. package/src/baseline/tasks/token.ts +0 -15
  173. package/src/baseline/tasks/url.ts +0 -11
  174. package/src/baseline/tasks/validation.ts +0 -19
  175. package/src/baseline/tasks/websocket.ts +0 -14
  176. package/src/bench/assert.ts +0 -70
  177. package/src/bench/checks.ts +0 -227
  178. package/src/bench/checksum.ts +0 -27
  179. package/src/bench/comparisons.ts +0 -70
  180. package/src/bench/concurrent-worker.ts +0 -135
  181. package/src/bench/fixtures.ts +0 -233
  182. package/src/bench/index.ts +0 -2
  183. package/src/bench/measure.ts +0 -177
  184. package/src/bench/now.ts +0 -3
  185. package/src/bench/report.ts +0 -79
  186. package/src/bench/run.ts +0 -57
  187. package/src/bench/tasks/complex.ts +0 -232
  188. package/src/bench/tasks/concurrent.ts +0 -213
  189. package/src/bench/tasks/cookie.ts +0 -44
  190. package/src/bench/tasks/hashing.ts +0 -33
  191. package/src/bench/tasks/hmac.ts +0 -36
  192. package/src/bench/tasks/http.ts +0 -50
  193. package/src/bench/tasks/index.ts +0 -52
  194. package/src/bench/tasks/json-patch.ts +0 -21
  195. package/src/bench/tasks/json.ts +0 -33
  196. package/src/bench/tasks/mime.ts +0 -21
  197. package/src/bench/tasks/query.ts +0 -44
  198. package/src/bench/tasks/stress.ts +0 -142
  199. package/src/bench/tasks/token.ts +0 -20
  200. package/src/bench/tasks/url.ts +0 -34
  201. package/src/bench/tasks/validation.ts +0 -57
  202. package/src/bench/tasks/websocket.ts +0 -21
  203. package/src/bench/types.ts +0 -60
  204. package/src/data/json-rows.ts +0 -33
  205. package/src/ingress/packed-input.ts +0 -59
  206. package/src/shared/json.ts +0 -18
  207. 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
+ }