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,745 @@
1
+ // src/loader/index.ts — Global higher-order-function data loader (HFC).
2
+ //
3
+ // `loader` is a CALLABLE object — a higher-order function over the curated op
4
+ // set. `loader("validateEmail")` returns a specialized, pre-bound hot function
5
+ // whose closure carries that op's spec, cost model and shared cache, so
6
+ // repeated calls do NO registry dispatch.
7
+ //
8
+ // Runtime self-optimization ("bulk or single"):
9
+ // - single item → scalar call (1 crossing, zero packing)
10
+ // - bulk items → ONE packed batch call by default; the adaptive cost model
11
+ // may route tiny batches to a scalar loop when measurement
12
+ // shows it is actually cheaper
13
+ // - `load()` → DataLoader-style: N calls in the same event-loop tick are
14
+ // coalesced into ONE packed native call (N crossings → 1);
15
+ // sustained LOW load drops the coalescer and dispatches
16
+ // singles directly, switching back to bulk on a same-tick
17
+ // burst (all automatic, per op)
18
+ // - caching → bounded LRU ("Hot Function Cache"), default key =
19
+ // fnv1a64(input) (skipped for sustained-unique workloads),
20
+ // opt-in per call via `{ key, cache }`
21
+ //
22
+ // Structure: this file is the DISPATCH CORE + factory. The pure pieces live
23
+ // beside it: `types.ts` (public type surface), `state.ts` (internal dispatch
24
+ // state), `ops.ts` (op registry), `cost.ts` (adaptive cost model), `batch.ts`
25
+ // (LRU cache + tick coalescer). Public types are re-exported below.
26
+
27
+ import { rust } from '../rust-ffi'
28
+ import { encoder, viewForArrayBuffer } from '../shared/bytes'
29
+ import type { SchemaValidator } from '../shared/packed'
30
+ import {
31
+ createLruCache,
32
+ createTickCoalescer,
33
+ type LoadRequest,
34
+ } from './batch'
35
+ import { createCostModel, nowNs } from './cost'
36
+ import {
37
+ fnvBatchKeysInto,
38
+ LOADER_OPS,
39
+ type LoaderOpName,
40
+ type LoaderOpSpec,
41
+ type LoaderScalar,
42
+ } from './ops'
43
+ import type { LoaderState, OpDispatchCtx, OpFnEntry } from './state'
44
+ import { KEY_SEP } from './state'
45
+ import type {
46
+ LoadableName,
47
+ Loader,
48
+ LoaderOpFn,
49
+ LoaderOpStats,
50
+ LoaderOptions,
51
+ LoaderSchemaFn,
52
+ LoaderStats,
53
+ LoadOptions,
54
+ } from './types'
55
+
56
+ // Re-export the op registry + types for introspection and authoring.
57
+ export {
58
+ type BulkResult,
59
+ LOADER_OP_NAMES,
60
+ LOADER_OPS,
61
+ type LoaderBulk,
62
+ type LoaderBulkArgs,
63
+ type LoaderOpArgs,
64
+ type LoaderOpName,
65
+ type LoaderResultKind,
66
+ type LoaderScalar,
67
+ type ScalarResult,
68
+ } from './ops'
69
+
70
+ // Re-export the public type surface so `import type { LoaderOptions } from
71
+ // '../loader'` keeps working (existing call sites).
72
+ export * from './types'
73
+
74
+ // ── Load-aware `load()` strategy + adaptive-key tuning ──
75
+ /** Consecutive single-load flushes before an op drops the coalescer. */
76
+ const SINGLE_AFTER = 4
77
+ /** Default-key attempts before unique inputs stop paying the fnv1a64 key. */
78
+ const KEY_WARMUP = 8
79
+ /** After warm-up with zero hits, re-probe the default key every N loads. */
80
+ const KEY_PROBE_EVERY = 64
81
+
82
+ function ctxFor(state: LoaderState, op: LoaderOpName): OpDispatchCtx {
83
+ let ctx = state.ctxs.get(op)
84
+ if (!ctx) {
85
+ ctx = {
86
+ cost: createCostModel({
87
+ adaptive: state.options.adaptive,
88
+ batchMin: state.options.batchMin,
89
+ }),
90
+ counters: { scalarCalls: 0, batchCalls: 0, itemsDispatched: 0, cachedHits: 0 },
91
+ sampleEvery: state.options.sampleEvery,
92
+ counter: 0,
93
+ // Load-aware strategy: "auto" starts coalescing so bursts batch from
94
+ // the first tick; it drops to "single" after sustained single-load
95
+ // flushes and back to "coalesce" on a same-tick burst.
96
+ mode: state.options.loadStrategy === 'single' ? 'single' : 'coalesce',
97
+ singleStreak: 0,
98
+ pendingSingle: false,
99
+ pendingInput: null,
100
+ pendingResolve: null,
101
+ pendingReject: null,
102
+ pendingKey: undefined,
103
+ keyAttempts: 0,
104
+ keyHits: 0,
105
+ keyProbeCounter: 0,
106
+ }
107
+ state.ctxs.set(op, ctx)
108
+ }
109
+ return ctx
110
+ }
111
+
112
+ /** Normalize a loader scalar result to bytes (the string-returning rust.* ops
113
+ * return strings on the Bun path — the loader's BYTES contract stays bytes). */
114
+ const asBytes = (v: unknown): Uint8Array =>
115
+ typeof v === 'string' ? encoder.encode(v) : (v as Uint8Array)
116
+
117
+ /** Build a bulk-shaped result by looping scalar calls (the adaptive fallback). */
118
+ function scalarLoopBulk(spec: LoaderOpSpec, items: Uint8Array[], rest: unknown[]): unknown {
119
+ const n = items.length
120
+ const paired = spec.pairedRest ?? []
121
+ // For paired ops the `rest` holds per-item companion ARRAYS; split them so
122
+ // each scalar call gets its own companion + the shared args. Non-paired ops
123
+ // take the `rest` array directly (zero allocation on the fallback path).
124
+ const itemRest = (i: number): unknown[] =>
125
+ paired.length === 0
126
+ ? rest
127
+ : rest.map((r, idx) => (paired.includes(idx) ? (r as unknown[])[i] : r))
128
+ switch (spec.kind) {
129
+ case 'boolean': {
130
+ const out = new Uint8Array(n)
131
+ let i = 0
132
+ // NOTE: index the output with `i` (not `i++` inline) so `itemRest(i)`
133
+ // sees the CURRENT index — `out[i++] = f(...itemRest(i))` would read the
134
+ // already-incremented index (undefined companions for paired ops).
135
+ for (const item of items) {
136
+ out[i] = spec.scalar(item, ...itemRest(i)) ? 1 : 0
137
+ i++
138
+ }
139
+ return out
140
+ }
141
+ case 'number': {
142
+ const out = new Uint32Array(n)
143
+ let i = 0
144
+ for (const item of items) {
145
+ out[i] = spec.scalar(item, ...itemRest(i)) as number
146
+ i++
147
+ }
148
+ return out
149
+ }
150
+ case 'bigint': {
151
+ const out = spec.unsigned ? new BigUint64Array(n) : new BigInt64Array(n)
152
+ let i = 0
153
+ for (const item of items) {
154
+ out[i] = spec.scalar(item, ...itemRest(i)) as bigint
155
+ i++
156
+ }
157
+ return out
158
+ }
159
+ case 'bytes': {
160
+ const out = new Array<Uint8Array>(n)
161
+ let i = 0
162
+ for (const item of items) {
163
+ out[i] = asBytes(spec.scalar(item, ...itemRest(i)))
164
+ i++
165
+ }
166
+ return out
167
+ }
168
+ }
169
+ }
170
+
171
+ /** Single-item dispatch — no Map lookups; timed only on sample ticks. */
172
+ function dispatchSingle(
173
+ ctx: OpDispatchCtx,
174
+ spec: LoaderOpSpec,
175
+ input: Uint8Array,
176
+ rest: unknown[],
177
+ ): unknown {
178
+ const c = ctx.counters
179
+ c.scalarCalls++
180
+ c.itemsDispatched++
181
+ const t = ++ctx.counter % ctx.sampleEvery === 0 ? nowNs() : 0
182
+ const out = asBytes(rest.length === 0 ? spec.scalar(input) : spec.scalar(input, ...rest))
183
+ if (t !== 0) ctx.cost.recordScalar(1, nowNs() - t)
184
+ return out
185
+ }
186
+
187
+ /** Bulk dispatch — packed batch, or adaptive scalar loop for tiny batches. */
188
+ function dispatchBulk(
189
+ ctx: OpDispatchCtx,
190
+ spec: LoaderOpSpec,
191
+ items: Uint8Array[],
192
+ rest: unknown[],
193
+ ): unknown {
194
+ const n = items.length
195
+ const c = ctx.counters
196
+ c.itemsDispatched += n
197
+
198
+ if (ctx.cost.decide(n)) {
199
+ const t = ++ctx.counter % ctx.sampleEvery === 0 ? nowNs() : 0
200
+ const out = spec.batch(items, ...rest)
201
+ if (t !== 0) ctx.cost.recordBatch(n, nowNs() - t)
202
+ c.batchCalls++
203
+ return out
204
+ }
205
+
206
+ const t = ++ctx.counter % ctx.sampleEvery === 0 ? nowNs() : 0
207
+ const out = scalarLoopBulk(spec, items, rest)
208
+ if (t !== 0) ctx.cost.recordScalar(n, nowNs() - t)
209
+ c.scalarCalls++
210
+ return out
211
+ }
212
+
213
+ /** Write a computed value into the cache when the request opted in. */
214
+ function maybeCache(state: LoaderState, req: LoadRequest, value: unknown): void {
215
+ if (req.key !== undefined) state.cache.set(req.key, value)
216
+ }
217
+
218
+ /** Handle a coalesced flush group for one op. */
219
+ function handleFlush(state: LoaderState, op: LoaderOpName, requests: LoadRequest[]): void {
220
+ state.flushes++
221
+ const ctx = ctxFor(state, op)
222
+ const spec: LoaderOpSpec = LOADER_OPS[op]
223
+ const n = requests.length
224
+ if (n === 1) {
225
+ // Low load observed. After a sustained run of single flushes, drop the
226
+ // coalescer for this op and dispatch singles directly (mode: "single").
227
+ if (ctx.mode === 'coalesce' && state.options.loadStrategy === 'auto') {
228
+ ctx.singleStreak++
229
+ if (ctx.singleStreak >= SINGLE_AFTER) ctx.mode = 'single'
230
+ }
231
+ const req = requests[0]
232
+ if (req === undefined) return
233
+ try {
234
+ // A lone coalesce-mode load may carry a deferred default key: compute it
235
+ // here (scalar fnv — no packed overhead for a single item), and resolve
236
+ // straight from the cache when present so no native op compute happens.
237
+ if (req.needsDefaultKey) {
238
+ if (shouldComputeDefaultKey(ctx)) {
239
+ ctx.keyAttempts++
240
+ const k = op + KEY_SEP + String(rust.fnv1a64(req.input))
241
+ const hit = state.cache.get(k)
242
+ if (hit !== undefined) {
243
+ ctx.counters.cachedHits++
244
+ ctx.keyHits++
245
+ req.resolve(hit)
246
+ return
247
+ }
248
+ req.key = k
249
+ }
250
+ req.needsDefaultKey = false
251
+ }
252
+ const value = dispatchSingle(ctx, spec, req.input, [])
253
+ maybeCache(state, req, value)
254
+ req.resolve(value)
255
+ } catch (err) {
256
+ req.reject(err)
257
+ }
258
+ return
259
+ }
260
+
261
+ // A real burst (n >= 2) keeps coalescing; reset the single-run counter.
262
+ ctx.singleStreak = 0
263
+
264
+ // Zero-alloc fast flush (boolean/number/bigint ops): pack the inputs into a
265
+ // reusable scratch, write the packed native result straight into a reusable
266
+ // output buffer via the `_into` variants, and read each element out of it —
267
+ // no intermediate unpack array, no per-call native Vec or JS allocation.
268
+ if (spec.fast !== undefined) {
269
+ const c = ctx.counters
270
+ c.batchCalls++
271
+ c.itemsDispatched += n
272
+
273
+ // Pack the requests directly (no `items.map`) into the reusable scratch.
274
+ let total = 4
275
+ for (const req of requests) total += 4 + req.input.byteLength
276
+ let scratch = state.packScratch
277
+ if (scratch.byteLength < total) {
278
+ scratch = new Uint8Array(Math.max(256, total * 2))
279
+ state.packScratch = scratch
280
+ }
281
+ const packView = viewForArrayBuffer(scratch.buffer, scratch.byteOffset)
282
+ packView.setUint32(0, n, true)
283
+ let pos = 4
284
+ for (const req of requests) {
285
+ packView.setUint32(pos, req.input.byteLength, true)
286
+ pos += 4
287
+ scratch.set(req.input, pos)
288
+ pos += req.input.byteLength
289
+ }
290
+
291
+ // Hybrid coalesce: when any request deferred a default key, compute ALL
292
+ // keys in ONE packed fnv1a64 crossing, resolve cache hits with NO native
293
+ // op compute, and run the packed op batch only on the remaining misses
294
+ // (repacked). The adaptive key-skip decision is made once per flush.
295
+ const needKeys = requests.some((r) => r.needsDefaultKey)
296
+ const computeKeys = needKeys && shouldComputeDefaultKey(ctx)
297
+ let keyView: DataView | null = null
298
+ if (computeKeys) {
299
+ ctx.keyAttempts += n
300
+ const keyTotal = 4 + n * 8
301
+ let keyBuf = state.keyScratch
302
+ if (keyBuf.byteLength < keyTotal) {
303
+ keyBuf = new Uint8Array(keyTotal)
304
+ state.keyScratch = keyBuf
305
+ }
306
+ fnvBatchKeysInto(scratch.subarray(0, total), keyBuf)
307
+ keyView = viewForArrayBuffer(keyBuf.buffer, keyBuf.byteOffset)
308
+ }
309
+
310
+ const pending: number[] = []
311
+ for (let i = 0; i < n; i++) {
312
+ const req = requests[i]
313
+ if (req === undefined) continue
314
+ let key = req.key
315
+ if (key === undefined && req.needsDefaultKey && keyView !== null) {
316
+ key = op + KEY_SEP + String(keyView.getBigUint64(4 + i * 8, true))
317
+ req.key = key
318
+ }
319
+ if (key !== undefined) {
320
+ const hit = state.cache.get(key)
321
+ if (hit !== undefined) {
322
+ ctx.counters.cachedHits++
323
+ if (req.needsDefaultKey) ctx.keyHits++
324
+ req.resolve(hit)
325
+ continue
326
+ }
327
+ }
328
+ pending.push(i)
329
+ }
330
+ if (pending.length === 0) return
331
+
332
+ // Repack the pending (miss) requests into the same scratch and run the
333
+ // packed op batch. scratch is sized >= total >= the subset size, so it is
334
+ // reused without reallocating; the op only runs on the misses.
335
+ const m = pending.length
336
+ let total2 = 4
337
+ for (const i of pending) {
338
+ const req = requests[i]
339
+ if (req !== undefined) total2 += 4 + req.input.byteLength
340
+ }
341
+ packView.setUint32(0, m, true)
342
+ let pos2 = 4
343
+ for (let j = 0; j < m; j++) {
344
+ const pi = pending[j]
345
+ if (pi === undefined) continue
346
+ const req = requests[pi]
347
+ if (req === undefined) continue
348
+ packView.setUint32(pos2, req.input.byteLength, true)
349
+ pos2 += 4
350
+ scratch.set(req.input, pos2)
351
+ pos2 += req.input.byteLength
352
+ }
353
+
354
+ // Size the reusable output buffer to the exact packed result.
355
+ const outSize = spec.fast.outSize(m)
356
+ let out = state.outScratch
357
+ if (out.byteLength < outSize) {
358
+ out = new Uint8Array(outSize)
359
+ state.outScratch = out
360
+ }
361
+ try {
362
+ spec.fast.batch(scratch.subarray(0, total2), out)
363
+ const outView = viewForArrayBuffer(out.buffer, out.byteOffset)
364
+ for (let j = 0; j < m; j++) {
365
+ const pi = pending[j]
366
+ if (pi === undefined) continue
367
+ const req = requests[pi]
368
+ if (req === undefined) continue
369
+ const value = spec.fast.element(outView, out, j)
370
+ maybeCache(state, req, value)
371
+ req.resolve(value)
372
+ }
373
+ } catch (err) {
374
+ for (const j of pending) {
375
+ const req = requests[j]
376
+ if (req === undefined) continue
377
+ req.reject(err)
378
+ }
379
+ }
380
+ return
381
+ }
382
+
383
+ const items = requests.map((r) => r.input)
384
+ try {
385
+ const bulk = dispatchBulk(ctx, spec, items, [])
386
+ for (let i = 0; i < n; i++) {
387
+ const req = requests[i]
388
+ if (req === undefined) continue
389
+ const value = spec.element(bulk, i)
390
+ maybeCache(state, req, value)
391
+ req.resolve(value)
392
+ }
393
+ } catch (err) {
394
+ for (const req of requests) req.reject(err)
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Decide whether to compute a default (`fnv1a64`) cache key for this call.
400
+ * Warm-up always computes (documented default). Once enough attempts show
401
+ * ZERO hits the inputs are unique — skip the key (one native crossing +
402
+ * string alloc) so `load()` stays cheap for unique workloads; a periodic
403
+ * probe re-enables it if inputs start repeating.
404
+ */
405
+ function shouldComputeDefaultKey(ctx: OpDispatchCtx): boolean {
406
+ if (ctx.keyAttempts < KEY_WARMUP) return true
407
+ if (ctx.keyHits > 0) return true
408
+ return ++ctx.keyProbeCounter % KEY_PROBE_EVERY === 0
409
+ }
410
+
411
+ /** Resolve a single-mode load: dispatch the scalar and (optionally) cache. */
412
+ function flushSingle(state: LoaderState, op: LoaderOpName, ctx: OpDispatchCtx): void {
413
+ const spec = LOADER_OPS[op]
414
+ const input = ctx.pendingInput
415
+ const resolve = ctx.pendingResolve
416
+ const reject = ctx.pendingReject
417
+ const key = ctx.pendingKey
418
+ ctx.pendingSingle = false
419
+ ctx.pendingInput = null
420
+ ctx.pendingResolve = null
421
+ ctx.pendingReject = null
422
+ ctx.pendingKey = undefined
423
+ // A same-tick burst moved this request to the coalescer — nothing to do.
424
+ if (input === null || resolve === null) return
425
+ try {
426
+ const value = dispatchSingle(ctx, spec, input, [])
427
+ if (key !== undefined) state.cache.set(key, value)
428
+ resolve(value)
429
+ } catch (err) {
430
+ if (reject !== null) reject(err)
431
+ }
432
+ }
433
+
434
+ /** Shared `load` implementation used by `opFn.load` and `loader.load`. */
435
+ function loadOne(
436
+ state: LoaderState,
437
+ op: LoaderOpName,
438
+ input: Uint8Array,
439
+ opts?: LoadOptions,
440
+ ): Promise<unknown> {
441
+ // Ops with required extra args cannot be coalesced (all group members must
442
+ // share the same args); surface a clear error instead of a broken batch.
443
+ if (hasRequiredRest(op)) {
444
+ return Promise.reject(
445
+ new TypeError(
446
+ `loader.load: op "${op}" requires extra arguments; use loader.run("${op}", …) instead`,
447
+ ),
448
+ )
449
+ }
450
+
451
+ const ctx = ctxFor(state, op)
452
+ const spec: LoaderOpSpec = LOADER_OPS[op]
453
+ const caching = state.options.caching && state.options.maxCacheKeys > 0 && (opts?.cache ?? true)
454
+
455
+ let key: string | undefined
456
+ let needsDefaultKey = false
457
+ if (caching) {
458
+ if (opts?.key !== undefined) {
459
+ // Explicit key: computable synchronously, so keep the eager cache check
460
+ // and let a hit resolve without a coalescer flush.
461
+ key = op + KEY_SEP + opts.key
462
+ const hit = state.cache.get(key)
463
+ if (hit !== undefined) {
464
+ ctx.counters.cachedHits++
465
+ return Promise.resolve(hit)
466
+ }
467
+ } else if (ctx.mode === 'coalesce' && spec.fast !== undefined && state.coalescer.pending > 0) {
468
+ // Mid-burst on a cheap (fast) op: defer the default key to the flush so
469
+ // all keys are computed in ONE packed native crossing instead of one
470
+ // scalar fnv1a64 crossing per load. A LONE load (the first in the tick,
471
+ // `pending === 0`) keeps the eager key so a cache hit still resolves
472
+ // synchronously with no coalescer flush. Single mode and non-fast ops
473
+ // also keep eager keys.
474
+ needsDefaultKey = true
475
+ } else if (shouldComputeDefaultKey(ctx)) {
476
+ ctx.keyAttempts++
477
+ key = op + KEY_SEP + String(rust.fnv1a64(input))
478
+ const hit = state.cache.get(key)
479
+ if (hit !== undefined) {
480
+ ctx.counters.cachedHits++
481
+ ctx.keyHits++
482
+ return Promise.resolve(hit)
483
+ }
484
+ }
485
+ }
486
+
487
+ return new Promise((resolve, reject) => {
488
+ // Load-time strategy: low load → single scalar call (no coalescer
489
+ // bookkeeping, no flush); a rising same-tick load → switch to bulk.
490
+ if (ctx.mode === 'single') {
491
+ if (!ctx.pendingSingle) {
492
+ ctx.pendingSingle = true
493
+ ctx.pendingInput = input
494
+ ctx.pendingResolve = resolve
495
+ ctx.pendingReject = reject
496
+ ctx.pendingKey = key
497
+ queueMicrotask(() => flushSingle(state, op, ctx))
498
+ } else {
499
+ // A second load in the same event-loop tick = rising load → coalesce.
500
+ ctx.mode = 'coalesce'
501
+ const first: LoadRequest = {
502
+ input: ctx.pendingInput ?? input,
503
+ resolve: ctx.pendingResolve ?? resolve,
504
+ reject: ctx.pendingReject ?? reject,
505
+ key: ctx.pendingKey,
506
+ }
507
+ ctx.pendingSingle = false
508
+ ctx.pendingInput = null
509
+ ctx.pendingResolve = null
510
+ ctx.pendingReject = null
511
+ ctx.pendingKey = undefined
512
+ state.coalescer.enqueue(op, first)
513
+ state.coalescer.enqueue(op, { input, resolve, reject, key })
514
+ }
515
+ return
516
+ }
517
+ state.coalescer.enqueue(op, { input, resolve, reject, key, needsDefaultKey })
518
+ })
519
+ }
520
+
521
+ function hasRequiredRest(op: LoaderOpName): boolean {
522
+ // Derived statically; a cheap runtime guard is unnecessary, so keep this
523
+ // allowlist aligned with `LoadableName` above. Ops with required extra args
524
+ // OR per-item companions cannot be coalesced (all group members must share
525
+ // the same args) — `load()` is unavailable for them.
526
+ switch (op) {
527
+ case 'signCookie':
528
+ case 'verifyCookie':
529
+ case 'csrfVerify':
530
+ case 'passwordHash':
531
+ case 'passwordVerify':
532
+ case 'hmacSha256':
533
+ case 'hmacSha256Verify':
534
+ case 'aeadEncrypt':
535
+ case 'aeadDecrypt':
536
+ case 'jwtSign':
537
+ case 'jwtVerify':
538
+ case 'jsonPatch':
539
+ case 'urlResolve':
540
+ case 'wsFrameEncode':
541
+ case 'schemaValidate':
542
+ return true
543
+ default:
544
+ return false
545
+ }
546
+ }
547
+
548
+ /** Build a specialized hot function for one op. */
549
+ function makeOpFn<K extends LoaderOpName>(op: K, state: LoaderState): OpFnEntry {
550
+ const spec = LOADER_OPS[op]
551
+ // `ctx` is rebound on `configure()` (see OpFnEntry.rebind) so the hot path
552
+ // stays a zero-lookup closure WITHOUT going stale after reconfiguration.
553
+ let ctx = ctxFor(state, op)
554
+
555
+ function opFn(input: Uint8Array | Uint8Array[], ...rest: unknown[]): unknown {
556
+ if (Array.isArray(input)) {
557
+ return dispatchBulk(ctx, spec, input as Uint8Array[], rest)
558
+ }
559
+ return dispatchSingle(ctx, spec, input as Uint8Array, rest)
560
+ }
561
+
562
+ const statsGetter = (): LoaderOpStats => {
563
+ const c = ctx.counters
564
+ return {
565
+ scalarCalls: c.scalarCalls,
566
+ batchCalls: c.batchCalls,
567
+ itemsDispatched: c.itemsDispatched,
568
+ cachedHits: c.cachedHits,
569
+ batchMin: ctx.cost.batchMin,
570
+ mode: ctx.mode,
571
+ }
572
+ }
573
+
574
+ const loadFn = hasRequiredRest(op)
575
+ ? undefined
576
+ : (input: Uint8Array, opts?: LoadOptions) =>
577
+ loadOne(state, op, input, opts) as Promise<LoaderScalar<K>>
578
+
579
+ Object.defineProperties(opFn, {
580
+ name: { value: op, enumerable: true },
581
+ stats: { get: statsGetter, enumerable: true },
582
+ clear: { value: () => state.cache.clear(), enumerable: true },
583
+ cache: {
584
+ value: (input: Uint8Array, key?: string) => {
585
+ const raw = key ?? String(rust.fnv1a64(input))
586
+ return state.cache.get(op + KEY_SEP + raw) as LoaderScalar<K> | undefined
587
+ },
588
+ enumerable: true,
589
+ },
590
+ load: { value: loadFn, enumerable: true },
591
+ })
592
+
593
+ return {
594
+ op,
595
+ fn: opFn as unknown as LoaderOpFn<LoaderOpName>,
596
+ rebind(next: OpDispatchCtx) {
597
+ ctx = next
598
+ },
599
+ }
600
+ }
601
+
602
+ // ── Factory + singleton ─────────────────────────────────────────────────────
603
+
604
+ /** Create an isolated loader with the given defaults. */
605
+ export function createLoader(options: LoaderOptions = {}): Loader {
606
+ const state: LoaderState = {
607
+ options: {
608
+ adaptive: options.adaptive !== false,
609
+ batchMin: Math.max(2, Math.min(8, options.batchMin ?? 2)),
610
+ maxCacheKeys: options.maxCacheKeys ?? 256,
611
+ caching: options.caching !== false,
612
+ sampleEvery: options.sampleEvery ?? 32,
613
+ loadStrategy: options.loadStrategy ?? 'auto',
614
+ },
615
+ ctxs: new Map(),
616
+ cache: createLruCache(options.maxCacheKeys ?? 256),
617
+ coalescer: createTickCoalescer((op, requests) =>
618
+ handleFlush(state, op as LoaderOpName, requests),
619
+ ),
620
+ opFns: new Map(),
621
+ flushes: 0,
622
+ packScratch: new Uint8Array(0),
623
+ outScratch: new Uint8Array(0),
624
+ keyScratch: new Uint8Array(0),
625
+ }
626
+
627
+ const loader = ((op: LoaderOpName): LoaderOpFn<LoaderOpName> => {
628
+ let entry = state.opFns.get(op)
629
+ if (!entry) {
630
+ entry = makeOpFn(op, state)
631
+ state.opFns.set(op, entry)
632
+ }
633
+ return entry.fn
634
+ }) as unknown as Loader
635
+
636
+ const runFn = (
637
+ op: LoaderOpName,
638
+ input: Uint8Array | Uint8Array[],
639
+ ...rest: unknown[]
640
+ ): unknown => {
641
+ const spec = LOADER_OPS[op]
642
+ const ctx = ctxFor(state, op)
643
+ if (Array.isArray(input)) {
644
+ return dispatchBulk(ctx, spec, input, rest)
645
+ }
646
+ return dispatchSingle(ctx, spec, input, rest)
647
+ }
648
+
649
+ const loadFn = <K extends LoadableName>(op: K, input: Uint8Array, opts?: LoadOptions) =>
650
+ loadOne(state, op, input, opts) as Promise<LoaderScalar<K>>
651
+
652
+ const cacheFn = <K extends LoaderOpName>(
653
+ op: K,
654
+ input: Uint8Array,
655
+ key?: string,
656
+ ): LoaderScalar<K> | undefined => {
657
+ const raw = key ?? String(rust.fnv1a64(input))
658
+ return state.cache.get(op + KEY_SEP + raw) as LoaderScalar<K> | undefined
659
+ }
660
+
661
+ const configureFn = (next: LoaderOptions) => {
662
+ const prev = state.options
663
+ state.options = {
664
+ adaptive: next.adaptive ?? prev.adaptive,
665
+ batchMin: Math.max(2, Math.min(8, next.batchMin ?? prev.batchMin)),
666
+ maxCacheKeys: next.maxCacheKeys ?? prev.maxCacheKeys,
667
+ caching: next.caching ?? prev.caching,
668
+ sampleEvery: next.sampleEvery ?? prev.sampleEvery,
669
+ loadStrategy: next.loadStrategy ?? prev.loadStrategy,
670
+ }
671
+ // A config change invalidates learned thresholds; start fresh AND rebind
672
+ // every memoized op fn so its dispatch/stats keep using the current ctx.
673
+ state.ctxs.clear()
674
+ for (const entry of state.opFns.values()) {
675
+ entry.rebind(ctxFor(state, entry.op))
676
+ }
677
+ // Rebuild the cache if its capacity changed.
678
+ if (next.maxCacheKeys !== undefined && next.maxCacheKeys !== prev.maxCacheKeys) {
679
+ state.cache = createLruCache(state.options.maxCacheKeys)
680
+ }
681
+ }
682
+
683
+ const statsGetter = (): LoaderStats => {
684
+ let scalarCalls = 0
685
+ let batchCalls = 0
686
+ let itemsDispatched = 0
687
+ let cachedHits = 0
688
+ for (const ctx of state.ctxs.values()) {
689
+ const c = ctx.counters
690
+ scalarCalls += c.scalarCalls
691
+ batchCalls += c.batchCalls
692
+ itemsDispatched += c.itemsDispatched
693
+ cachedHits += c.cachedHits
694
+ }
695
+ return {
696
+ scalarCalls,
697
+ batchCalls,
698
+ itemsDispatched,
699
+ flushes: state.flushes,
700
+ cachedHits,
701
+ cacheSize: state.cache.size,
702
+ cacheEvictions: state.cache.evictions,
703
+ }
704
+ }
705
+
706
+ const opNamesGetter = (): LoaderOpName[] => Object.keys(LOADER_OPS) as LoaderOpName[]
707
+
708
+ // Bind a SchemaValidator: single/bulk validate through the loader's normal
709
+ // dispatch machinery (shared cost model + counters), plus a whole-batch count.
710
+ const schemaFn = (validator: SchemaValidator): LoaderSchemaFn => {
711
+ const spec = LOADER_OPS.schemaValidate
712
+ const ctx = ctxFor(state, 'schemaValidate')
713
+ const fn = ((input: Uint8Array | Uint8Array[]) => {
714
+ if (Array.isArray(input)) {
715
+ return dispatchBulk(ctx, spec, input, [validator]) as Uint8Array
716
+ }
717
+ return dispatchSingle(ctx, spec, input, [validator]) as boolean
718
+ }) as LoaderSchemaFn
719
+ Object.defineProperty(fn, 'count', {
720
+ value: (inputs: Uint8Array[]) => rust.batch.schemaValidateCount(validator, inputs),
721
+ enumerable: true,
722
+ })
723
+ return fn
724
+ }
725
+
726
+ Object.defineProperties(loader, {
727
+ run: { value: runFn, enumerable: true },
728
+ load: { value: loadFn, enumerable: true },
729
+ cache: { value: cacheFn, enumerable: true },
730
+ schema: { value: schemaFn, enumerable: true },
731
+ clear: { value: () => state.cache.clear(), enumerable: true },
732
+ configure: { value: configureFn, enumerable: true },
733
+ stats: { get: statsGetter, enumerable: true },
734
+ opNames: { get: opNamesGetter, enumerable: true },
735
+ })
736
+
737
+ return loader
738
+ }
739
+
740
+ /**
741
+ * The global, ready-to-use loader. Most consumers just do
742
+ * `import { loader } from "castrum"` and either `loader("validateEmail")(…)`
743
+ * (higher-order function) or `loader.run("validateEmail", …)`.
744
+ */
745
+ export const loader = createLoader()