castrum 0.7.0 → 0.9.0

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