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,194 @@
1
+ {
2
+ "generatedAt": "2026-08-13T09:13:26.107Z",
3
+ "note": "Auto-selected by `bun scripts/select-native.ts --write` from measurements of the castrum addon vs representative pure-TS implementations. Do NOT hand-edit — regenerate with the benchmark script. The public API reads this via `opImpl(op)` and binds each op to a fixed impl (native or js) at load time.",
4
+ "ops": {
5
+ "fnv1a64": {
6
+ "impl": "native",
7
+ "nativeRatio": 12.603
8
+ },
9
+ "crc32": {
10
+ "impl": "js",
11
+ "nativeRatio": 0.659
12
+ },
13
+ "hmacSha256": {
14
+ "impl": "native",
15
+ "nativeRatio": 1.212
16
+ },
17
+ "hmacSha256Verify": {
18
+ "impl": "native",
19
+ "nativeRatio": 9.028
20
+ },
21
+ "signCookie": {
22
+ "impl": "native",
23
+ "nativeRatio": 3.64
24
+ },
25
+ "verifyCookie": {
26
+ "impl": "native",
27
+ "nativeRatio": 1.772
28
+ },
29
+ "csrfToken": {
30
+ "impl": "native",
31
+ "nativeRatio": 12.671
32
+ },
33
+ "csrfVerify": {
34
+ "impl": "native",
35
+ "nativeRatio": 12.458
36
+ },
37
+ "randomToken": {
38
+ "impl": "native",
39
+ "nativeRatio": 9.546
40
+ },
41
+ "passwordHash": {
42
+ "impl": "native",
43
+ "nativeRatio": 1.657
44
+ },
45
+ "aeadEncrypt": {
46
+ "impl": "native",
47
+ "nativeRatio": 1.288
48
+ },
49
+ "aeadDecrypt": {
50
+ "impl": "native",
51
+ "nativeRatio": 1.108
52
+ },
53
+ "queryPairs": {
54
+ "impl": "js",
55
+ "nativeRatio": 0.279
56
+ },
57
+ "cookiePairs": {
58
+ "impl": "js",
59
+ "nativeRatio": 0.105
60
+ },
61
+ "formPairs": {
62
+ "impl": "js",
63
+ "nativeRatio": 0.328
64
+ },
65
+ "etag": {
66
+ "impl": "js",
67
+ "nativeRatio": 0.187
68
+ },
69
+ "parseMediaType": {
70
+ "impl": "js",
71
+ "nativeRatio": 0.378
72
+ },
73
+ "parseAcceptEncoding": {
74
+ "impl": "js",
75
+ "nativeRatio": 0.367
76
+ },
77
+ "createConditionalRequest": {
78
+ "impl": "js",
79
+ "nativeRatio": 0.275
80
+ },
81
+ "createAcceptNegotiator": {
82
+ "impl": "native",
83
+ "nativeRatio": 2.01
84
+ },
85
+ "multipartParse": {
86
+ "impl": "js",
87
+ "nativeRatio": null
88
+ },
89
+ "jsonValid": {
90
+ "impl": "js",
91
+ "nativeRatio": 0.749
92
+ },
93
+ "jsonPatch": {
94
+ "impl": "js",
95
+ "nativeRatio": 0.014
96
+ },
97
+ "gzipCompress": {
98
+ "impl": "native",
99
+ "nativeRatio": 22.132
100
+ },
101
+ "gzipDecompress": {
102
+ "impl": "native",
103
+ "nativeRatio": 61.039
104
+ },
105
+ "brotliCompress": {
106
+ "impl": "native",
107
+ "nativeRatio": 9.983
108
+ },
109
+ "brotliDecompress": {
110
+ "impl": "native",
111
+ "nativeRatio": 33.327
112
+ },
113
+ "sseEncode": {
114
+ "impl": "js",
115
+ "nativeRatio": 0.586
116
+ },
117
+ "wsFrameEncode": {
118
+ "impl": "js",
119
+ "nativeRatio": 0.152
120
+ },
121
+ "wsFrameDecode": {
122
+ "impl": "js",
123
+ "nativeRatio": 0.988
124
+ },
125
+ "wsAcceptKey": {
126
+ "impl": "js",
127
+ "nativeRatio": 0.939
128
+ },
129
+ "validateEmail": {
130
+ "impl": "js",
131
+ "nativeRatio": 0.007
132
+ },
133
+ "validateUuid": {
134
+ "impl": "js",
135
+ "nativeRatio": 0.008
136
+ },
137
+ "validateIpv4": {
138
+ "impl": "js",
139
+ "nativeRatio": 0.012
140
+ },
141
+ "validateIpv6": {
142
+ "impl": "js",
143
+ "nativeRatio": 0.078
144
+ },
145
+ "createRateLimiter": {
146
+ "impl": "js",
147
+ "nativeRatio": 0.061
148
+ },
149
+ "renderTemplate": {
150
+ "impl": "js",
151
+ "nativeRatio": 0.141
152
+ },
153
+ "xxh3": {
154
+ "impl": "native",
155
+ "nativeRatio": null
156
+ },
157
+ "urlEncode": {
158
+ "impl": "js",
159
+ "nativeRatio": 0.214
160
+ },
161
+ "urlDecode": {
162
+ "impl": "js",
163
+ "nativeRatio": 0.092
164
+ },
165
+ "base64Encode": {
166
+ "impl": "js",
167
+ "nativeRatio": 0.376
168
+ },
169
+ "httpDate": {
170
+ "impl": "js",
171
+ "nativeRatio": 0.401
172
+ },
173
+ "jwtSign": {
174
+ "impl": "native",
175
+ "nativeRatio": null
176
+ },
177
+ "jwtVerify": {
178
+ "impl": "native",
179
+ "nativeRatio": null
180
+ },
181
+ "passwordVerify": {
182
+ "impl": "native",
183
+ "nativeRatio": null
184
+ },
185
+ "createTemplate": {
186
+ "impl": "native",
187
+ "nativeRatio": null
188
+ },
189
+ "createSchemaValidator": {
190
+ "impl": "native",
191
+ "nativeRatio": null
192
+ }
193
+ }
194
+ }
@@ -0,0 +1,73 @@
1
+ // src/selection.ts — native-vs-JS selection surface (OWNED BY CASTRUM).
2
+ //
3
+ // The decision of whether an operation should use the Rust addon ("native")
4
+ // or a pure-JS implementation ("js") is a property of this library, not of
5
+ // any consumer. The single source of truth is `rust/selection.rs`
6
+ // (benchmark-driven, audited by `scripts/select-native.ts --check`); this
7
+ // module is its TS projection and the API consumers call ONCE at load time.
8
+ //
9
+ // Consumers bind each operation to a fixed implementation at startup by
10
+ // reading `opImpl(op)` here — they do NOT swap native↔js per call.
11
+ //
12
+ // The baked default comes from the PROVEN SELECTION registry
13
+ // (`src/shared/proven.ts`, `PROVEN_SELECTION`): for each op the
14
+ // benchmark-proven winner is 'native' | 'js' | 'bun' (Bun built-in
15
+ // delegation). Bun built-in delegation is runtime-aware via the adapter's
16
+ // `builtins` registry (`runtimeNative.builtins.has(op)` — empty under Node).
17
+ // `test/unit/shared/proven.test.ts` verifies this baked registry matches the
18
+ // live wiring, so it can't drift.
19
+
20
+ import { getAddon, lazyAddon } from './native'
21
+ import { provenImpl } from './shared/proven'
22
+ import { runtimeNative } from './runtime/native'
23
+
24
+ /** Recommended implementation for an operation. */
25
+ export type OpImpl = 'native' | 'js'
26
+
27
+ /** A selection decision: recommended implementation + rationale note. */
28
+ export interface OpDecision {
29
+ /** The recommended implementation when the addon is available. */
30
+ readonly impl: OpImpl
31
+ /** Why this decision (measured ratio / rationale). */
32
+ readonly note?: string
33
+ }
34
+
35
+ // Lazy: importing this module does NOT dlopen the addon; `opImpl` reads the
36
+ // baked decision from Rust on first call.
37
+ const addon = lazyAddon(getAddon)
38
+
39
+ /**
40
+ * The benchmark-driven native-vs-JS decision for `op` — runtime-aware.
41
+ *
42
+ * Returns `"native"` when the Rust addon is the recommended implementation,
43
+ * `"js"` when a pure-JS (or Bun built-in) implementation wins, or `null` for
44
+ * unknown ops (or when the addon is unavailable — with no addon there is no
45
+ * "native" choice). Under Bun, ops in the adapter's builtins registry resolve
46
+ * to `"js"` so the consumer's JS path delegates to the faster Bun built-in.
47
+ *
48
+ * The default is the BAKED proven selection (`src/shared/proven.ts`):
49
+ * `native` / `js` directly, `bun` resolves to `"js"` via the builtins check
50
+ * above. Unknown ops (and `bun` entries on a non-Bun runtime, where no
51
+ * built-in exists) fall back to the addon's embedded decision
52
+ * (`rust/selection.rs` ← `src/selection.json`).
53
+ */
54
+ export const opImpl = (op: string): OpImpl | null => {
55
+ // Bun built-in delegation (runtime-aware — the registry is empty under Node).
56
+ if (runtimeNative.builtins.has(op)) return 'js'
57
+ // Baked proven winners are the default selection.
58
+ const proven = provenImpl(op)
59
+ if (proven === 'native') return 'native'
60
+ if (proven === 'js') return 'js'
61
+ // 'bun' on a non-Bun runtime (no built-in) or unknown ops → addon decision.
62
+ return addon.opImpl(op)
63
+ }
64
+
65
+ /** `true` when the Rust addon is the recommended impl for `op`. */
66
+ export const isNativeOp = (op: string): boolean => opImpl(op) === 'native'
67
+
68
+ /** The decision record for `op`, or `null` when unknown. */
69
+ export const opDecision = (op: string): OpDecision | null => {
70
+ const impl = opImpl(op)
71
+ if (impl === null) return null
72
+ return { impl }
73
+ }
@@ -0,0 +1,67 @@
1
+ // src/shared/adaptive.ts — Reusable EWMA adaptive-estimate utility.
2
+ //
3
+ // The loader's cost model (src/loader/cost.ts) learns per-op dispatch costs at
4
+ // runtime with an exponential moving average (EWMA). This is that same
5
+ // machinery, extracted as a tiny reusable class so OTHER subsystems can
6
+ // self-optimize without re-implementing the math — e.g. BufferPool learns the
7
+ // observed request size to pre-size future buffers (`adaptive` option), and
8
+ // consumers can drive their own runtime decisions (batch-vs-loop, delegate
9
+ // vs-native, cache-size) with a bounded, noise-smoothed estimate.
10
+ /** Options for an `AdaptiveEstimate`: EWMA smoothing, bounds, starting value. */ export interface AdaptiveEstimateOptions {
11
+ /**
12
+ * EWMA smoothing factor in (0, 1]. Higher reacts faster to new samples;
13
+ * lower smooths more. Default: 0.25.
14
+ */
15
+ alpha?: number
16
+ /** Starting estimate. Default 0 (the first sample wins). */
17
+ initial?: number
18
+ /** Clamp the estimate to at least this. Default 0. */
19
+ min?: number
20
+ /** Clamp the estimate to at most this (bounds memory / resources). Default Infinity. */
21
+ max?: number
22
+ }
23
+
24
+ /**
25
+ * A bounded exponential-moving-average estimate of an observed quantity.
26
+ *
27
+ * `sample(x)` folds a new observation into the running estimate; `estimate`
28
+ * reads the current value. The estimate is clamped to `[min, max]`, so it is
29
+ * safe to drive resource sizing (buffer pools, batch thresholds) with it —
30
+ * a single outlier cannot blow up the estimate, and it decays toward recent
31
+ * observations.
32
+ */
33
+ export class AdaptiveEstimate {
34
+ private readonly alpha: number
35
+ private readonly min: number
36
+ private readonly max: number
37
+ private readonly initial: number
38
+ private value: number
39
+
40
+ constructor(options: AdaptiveEstimateOptions = {}) {
41
+ const alpha = options.alpha ?? 0.25
42
+ if (!Number.isFinite(alpha) || alpha <= 0 || alpha > 1) {
43
+ throw new RangeError(`AdaptiveEstimate: alpha must be in (0,1], got ${alpha}`)
44
+ }
45
+ this.alpha = alpha
46
+ this.min = Math.max(0, options.min ?? 0)
47
+ this.max = options.max ?? Number.POSITIVE_INFINITY
48
+ this.initial = options.initial ?? 0
49
+ this.value = this.initial
50
+ }
51
+
52
+ /** Fold a new observation into the running estimate. */
53
+ sample(x: number): void {
54
+ const v = this.value === 0 ? x : this.value * (1 - this.alpha) + x * this.alpha
55
+ this.value = v < this.min ? this.min : v > this.max ? this.max : v
56
+ }
57
+
58
+ /** Current estimate (clamped to `[min, max]`). */
59
+ get estimate(): number {
60
+ return this.value
61
+ }
62
+
63
+ /** Reset the estimate (back to `initial`). */
64
+ reset(): void {
65
+ this.value = this.initial
66
+ }
67
+ }
@@ -0,0 +1,264 @@
1
+ // src/shared/buffer-pool.ts — generic reusable byte-buffer pool for hot-path FFI calls.
2
+ //
3
+ // Hot native-FFI paths (e.g. the ingress pipeline) write into large output
4
+ // buffers. Allocating a fresh buffer per call is a measurable cost at high
5
+ // request rates. This pool hands out buffers that are returned to the pool
6
+ // once the consumer is done with them, eliminating steady-state allocation.
7
+ //
8
+ // It is deliberately generic: it works with any `Uint8Array` consumer, is safe
9
+ // for a single-threaded event loop, and supports both eager release (callers
10
+ // that copy out immediately) and in-flight borrowing (zero-copy callers that
11
+ // hold the buffer past the call). Memory is bounded by `maxBuffers` retained
12
+ // buffers; under heavy concurrency, extra temporary buffers are allocated and
13
+ // discarded on release.
14
+ /** Options for configuring a reusable byte-buffer pool. */ export interface BufferPoolOptions {
15
+ /**
16
+ * Initial buffer size in bytes. Default: 131072.
17
+ */
18
+ initialSize?: number
19
+ /**
20
+ * Upper bound on buffers retained by the pool. When this is reached and no
21
+ * free buffer is large enough, the largest free buffer is grown in place
22
+ * (replaced) so memory stays bounded. If the pool is fully in-flight, a
23
+ * temporary buffer is allocated and discarded on release. Default: 16.
24
+ */
25
+ maxBuffers?: number
26
+ /**
27
+ * Hard cap on the number of buffers simultaneously in flight (acquired but
28
+ * not yet released). When exceeded, `acquire()` throws a `RangeError` — a
29
+ * backpressure signal for zero-copy responses consumed too slowly. 0
30
+ * (default) is unlimited: when the retained set is exhausted, temporary
31
+ * buffers are allocated and discarded on release (unbounded under a slow
32
+ * zero-copy consumer — set this to bound it).
33
+ */
34
+ maxInFlight?: number
35
+ /**
36
+ * When true, learn the observed request size (EWMA over `minSize` at each
37
+ * take) and pre-size new buffers to at least the learned estimate (bounded
38
+ * by `maxBuffers` retention). Helps workloads with varying request sizes
39
+ * converge on one buffer size instead of re-growing each time. Default false.
40
+ */
41
+ adaptive?: boolean
42
+ }
43
+
44
+ /** A borrowed buffer handle handed out by a pool; `release()` returns it. */
45
+ export interface PooledBuffer {
46
+ /**
47
+ * The underlying buffer. Its byteLength is guaranteed to be at least the
48
+ * `minSize` requested at acquire time.
49
+ */
50
+ readonly buffer: Uint8Array
51
+ /**
52
+ * Whether this handle has already been returned to the pool.
53
+ */
54
+ readonly released: boolean
55
+ /**
56
+ * Return the buffer to the pool for reuse. Safe to call more than once;
57
+ * subsequent calls are no-ops.
58
+ */
59
+ release(): void
60
+ }
61
+
62
+ /**
63
+ * Concrete `PooledBuffer` handle.
64
+ *
65
+ * Implemented as a class (not a per-acquire object literal with a getter +
66
+ * closure) so that `acquire()` allocates a single small object instead of an
67
+ * object + getter + closure per request. The hot ingress `run()` path
68
+ * acquires/releases once per request, so this removes one object allocation
69
+ * and one closure allocation from the per-request cost. `release()` lives on
70
+ * the prototype; `pool`/`buffer` are the only per-instance fields.
71
+ *
72
+ * Handles are RECYCLED by the pool (see `BufferPool.freeHandles`): a released
73
+ * handle object is re-pointed at a fresh buffer on the next `acquire()` so the
74
+ * steady state allocates zero handle objects. `buffer` and `released` are the
75
+ * only fields that change across a recycle. Recycling is safe because it only
76
+ * happens after `release()` and every internal + documented usage releases
77
+ * each checkout exactly once; the `released` guard still makes a double
78
+ * release within a single checkout a no-op.
79
+ */
80
+ class PooledBufferHandle implements PooledBuffer {
81
+ readonly pool: BufferPool
82
+ buffer: Uint8Array
83
+ released = false
84
+
85
+ constructor(pool: BufferPool, buffer: Uint8Array) {
86
+ this.pool = pool
87
+ this.buffer = buffer
88
+ }
89
+
90
+ release(): void {
91
+ if (this.released) {
92
+ return
93
+ }
94
+ this.released = true
95
+ this.pool.returnHandle(this)
96
+ }
97
+ }
98
+
99
+ import { AdaptiveEstimate } from './adaptive'
100
+
101
+ /**
102
+ * A pool of reusable `Uint8Array` buffers.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * const pool = new BufferPool({ initialSize: 131072 });
107
+ *
108
+ * const handle = pool.acquire(131072);
109
+ * try {
110
+ * // write into handle.buffer ...
111
+ * useBytes(handle.buffer.subarray(0, written));
112
+ * } finally {
113
+ * handle.release();
114
+ * }
115
+ * ```
116
+ */
117
+ export class BufferPool {
118
+ private readonly free: Uint8Array[] = []
119
+ /** Released handle objects available for recycling (bounded — see `maxHandles`). */
120
+ private readonly freeHandles: PooledBufferHandle[] = []
121
+ private readonly initialSize: number
122
+ private readonly maxBuffers: number
123
+ private readonly maxInFlight: number
124
+ /** Upper bound on recycled handle objects retained (peak concurrency). */
125
+ private readonly maxHandles: number
126
+ private readonly estimate: AdaptiveEstimate | null
127
+ private created = 0
128
+ private inFlight = 0
129
+
130
+ constructor(options: BufferPoolOptions = {}) {
131
+ this.initialSize = Math.max(1, Math.floor(options.initialSize ?? 131_072))
132
+ this.maxBuffers = Math.max(1, Math.floor(options.maxBuffers ?? 16))
133
+ this.maxInFlight = Math.max(0, Math.floor(options.maxInFlight ?? 0))
134
+ this.maxHandles = this.maxBuffers + Math.max(1, this.maxInFlight)
135
+ this.estimate =
136
+ options.adaptive === true
137
+ ? new AdaptiveEstimate({ alpha: 0.25, min: this.initialSize })
138
+ : null
139
+ this.free.push(new Uint8Array(this.initialSize))
140
+ this.created = 1
141
+ }
142
+
143
+ /** Number of buffers currently available for immediate reuse. */
144
+ get freeCount(): number {
145
+ return this.free.length
146
+ }
147
+
148
+ /** Total number of buffers this pool has allocated (monotonic). */
149
+ get createdCount(): number {
150
+ return this.created
151
+ }
152
+
153
+ /**
154
+ * Acquire a buffer of at least `minSize` bytes. The returned handle must be
155
+ * released (directly, or via a zero-copy Response) once the caller is done.
156
+ *
157
+ * @param minSize - Minimum buffer size in bytes. Default: 0 (any free buffer).
158
+ * @throws RangeError when `maxInFlight` is set and that many buffers are
159
+ * already borrowed (never released).
160
+ */
161
+ acquire(minSize = 0): PooledBuffer {
162
+ if (this.maxInFlight > 0 && this.inFlight >= this.maxInFlight) {
163
+ throw new RangeError(
164
+ `BufferPool: maxInFlight (${this.maxInFlight}) exceeded — too many ` +
165
+ 'buffers borrowed at once (unreleased zero-copy responses?). ' +
166
+ 'Raise maxInFlight or consume/release responses faster.',
167
+ )
168
+ }
169
+ const buffer = this.take(Math.max(0, Math.floor(minSize)))
170
+ this.inFlight++
171
+ // Recycle a released handle object in the steady state instead of
172
+ // allocating a fresh one per acquire (removes one per-request object from
173
+ // the hot ingress `run()` path). Safe: handles enter `freeHandles` only
174
+ // after `release()`, and each checkout is released exactly once by every
175
+ // internal + documented usage. `buffer`/`released` are the only fields
176
+ // that change across a recycle.
177
+ const handle = this.freeHandles.pop()
178
+ if (handle !== undefined) {
179
+ handle.buffer = buffer
180
+ handle.released = false
181
+ return handle
182
+ }
183
+ return new PooledBufferHandle(this, buffer)
184
+ }
185
+
186
+ /**
187
+ * Return a borrowed handle's buffer to the pool (called by
188
+ * `PooledBufferHandle.release`). Internal to the pool — do not call from
189
+ * outside.
190
+ */
191
+ returnHandle(handle: PooledBufferHandle): void {
192
+ this.inFlight--
193
+ this.releaseBuffer(handle.buffer)
194
+ // Recycle the handle object for a later acquire, bounding retention so
195
+ // memory stays proportional to concurrency (peak in-flight), not the
196
+ // cumulative number of acquisitions.
197
+ if (this.freeHandles.length < this.maxHandles) {
198
+ this.freeHandles.push(handle)
199
+ }
200
+ }
201
+
202
+ /** Take a buffer of at least `minSize` bytes out of circulation. */
203
+ private take(minSize: number): Uint8Array {
204
+ // Learn the observed demand so future allocations pre-size to it.
205
+ this.estimate?.sample(minSize)
206
+
207
+ // 1) Reuse a free buffer that is already large enough.
208
+ for (let i = 0; i < this.free.length; i++) {
209
+ const candidate = this.free[i] as Uint8Array
210
+ if (candidate.byteLength >= minSize) {
211
+ this.free.splice(i, 1)
212
+ return candidate
213
+ }
214
+ }
215
+
216
+ const target = this.nextSize(minSize)
217
+
218
+ // 2) No free buffer is large enough: grow the largest free buffer in place
219
+ // to keep the number of retained buffers bounded.
220
+ let largestBuf: Uint8Array | null = null
221
+ let largest = -1
222
+ for (let i = 0; i < this.free.length; i++) {
223
+ const candidate = this.free[i] as Uint8Array
224
+ if (largestBuf === null || candidate.byteLength > largestBuf.byteLength) {
225
+ largestBuf = candidate
226
+ largest = i
227
+ }
228
+ }
229
+ if (largestBuf !== null) {
230
+ this.free.splice(largest, 1)
231
+ this.created++
232
+ return new Uint8Array(target)
233
+ }
234
+
235
+ // 3) Pool exhausted (all buffers in flight): allocate a fresh buffer.
236
+ this.created++
237
+ return new Uint8Array(target)
238
+ }
239
+
240
+ /** Return a buffer to the free list, bounding total retained memory. */
241
+ private releaseBuffer(buffer: Uint8Array): void {
242
+ if (this.free.length >= this.maxBuffers) {
243
+ // Free list is full: drop the smallest retained buffer to stay bounded.
244
+ let smallestBuf: Uint8Array | null = null
245
+ let smallest = 0
246
+ for (let i = 0; i < this.free.length; i++) {
247
+ const candidate = this.free[i] as Uint8Array
248
+ if (smallestBuf === null || candidate.byteLength < smallestBuf.byteLength) {
249
+ smallestBuf = candidate
250
+ smallest = i
251
+ }
252
+ }
253
+ this.free.splice(smallest, 1)
254
+ }
255
+ this.free.push(buffer)
256
+ }
257
+
258
+ private nextSize(minSize: number): number {
259
+ const floor = this.estimate
260
+ ? Math.max(this.initialSize, Math.ceil(this.estimate.estimate))
261
+ : this.initialSize
262
+ return Math.max(floor, minSize)
263
+ }
264
+ }