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,930 @@
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
+ import {
23
+ LOADER_OPS,
24
+ fnvBatchKeysInto,
25
+ type LoaderBulkArgs,
26
+ type LoaderOpArgs,
27
+ type LoaderOpName,
28
+ type LoaderOpSpec,
29
+ type LoaderScalar,
30
+ type LoaderBulk,
31
+ } from './ops'
32
+ import { createCostModel, nowNs, type LoaderCostModel } from './cost'
33
+ import {
34
+ createLruCache,
35
+ createTickCoalescer,
36
+ type LoaderCache,
37
+ type LoadRequest,
38
+ type TickCoalescer,
39
+ } from './batch'
40
+ import { rust } from '../rust-ffi'
41
+ import { viewForArrayBuffer } from '../shared/bytes'
42
+ import type { SchemaValidator } from '../shared/packed'
43
+
44
+ // Re-export the op registry + types for introspection and authoring.
45
+ export {
46
+ LOADER_OPS,
47
+ LOADER_OP_NAMES,
48
+ type LoaderOpName,
49
+ type LoaderOpArgs,
50
+ type LoaderBulkArgs,
51
+ type LoaderBulk,
52
+ type LoaderScalar,
53
+ type LoaderResultKind,
54
+ type ScalarResult,
55
+ type BulkResult,
56
+ } from './ops'
57
+
58
+ // ── Public types ────────────────────────────────────────────────────────────
59
+
60
+ /** Options for `createLoader()` / `loader.configure()`. */
61
+ export interface LoaderOptions {
62
+ /** Let each op adapt `batchMin` from measured dispatch cost. Default true. */
63
+ adaptive?: boolean
64
+ /** Initial batch threshold in [2, 8]. Default 2 (batch wins for n >= 2). */
65
+ batchMin?: number
66
+ /** Max cached keys (bounded LRU). 0 disables caching. Default 256. */
67
+ maxCacheKeys?: number
68
+ /** Default `cache` behavior for `load()`. Default true. */
69
+ caching?: boolean
70
+ /** Sample timing every N dispatches (hot-path cost control). Default 32. */
71
+ sampleEvery?: number
72
+ /**
73
+ * `load()` dispatch strategy. `"auto"` (default) switches each op between
74
+ * `"single"` (low load → direct scalar call, no coalescer) and
75
+ * `"coalesce"` (rising load → one packed batch call) from observed load.
76
+ */
77
+ loadStrategy?: LoadStrategy
78
+ }
79
+
80
+ /** Options for `load()` / `opFn.load()`. */
81
+ export interface LoadOptions {
82
+ /**
83
+ * Explicit cache key. When omitted (and caching is on) the default key is
84
+ * `fnv1a64(input)` — which costs one extra native call per load.
85
+ */
86
+ key?: string
87
+ /** Override the loader-wide caching toggle for this call. Default true. */
88
+ cache?: boolean
89
+ }
90
+
91
+ /** Per-op counters exposed on `opFn.stats`. */
92
+ export interface LoaderOpStats {
93
+ scalarCalls: number
94
+ batchCalls: number
95
+ itemsDispatched: number
96
+ cachedHits: number
97
+ /** Current learned batch threshold for this op. */
98
+ batchMin: number
99
+ /** Current `load()` strategy: "single" (low load) or "coalesce" (bulk). */
100
+ mode: 'single' | 'coalesce'
101
+ }
102
+
103
+ /** Aggregate loader statistics. */
104
+ export interface LoaderStats {
105
+ scalarCalls: number
106
+ batchCalls: number
107
+ itemsDispatched: number
108
+ flushes: number
109
+ cachedHits: number
110
+ cacheSize: number
111
+ cacheEvictions: number
112
+ }
113
+
114
+ /** A specialized, pre-bound op function returned by `loader(op)`. */
115
+ export interface LoaderOpFn<K extends LoaderOpName> {
116
+ /** Single item → scalar result. */
117
+ (input: Uint8Array, ...rest: LoaderOpArgs[K]): LoaderScalar<K>
118
+ /** Bulk items → ONE packed batch call; same shape as `rust.batch.<op>`. */
119
+ (inputs: Uint8Array[], ...rest: LoaderBulkArgs<K>): LoaderBulk<K>
120
+ readonly name: K
121
+ readonly stats: LoaderOpStats
122
+ clear(): void
123
+ /** Sync cache peek (default key `fnv1a64(input)` unless `key` given). */
124
+ cache(input: Uint8Array, key?: string): LoaderScalar<K> | undefined
125
+ /**
126
+ * Coalesced, cache-aware load. Unavailable (`undefined`) on ops that require
127
+ * extra arguments (e.g. `hmacSha256`, `signCookie`) — use `run` for those.
128
+ */
129
+ load: K extends LoadableName ? LoaderLoadFn<K> : undefined
130
+ }
131
+
132
+ type LoaderLoadFn<K extends LoaderOpName> = (
133
+ input: Uint8Array,
134
+ opts?: LoadOptions,
135
+ ) => Promise<LoaderScalar<K>>
136
+
137
+ /** Ops whose extra args are all optional/empty → they support `load()`. */
138
+ type LoadableName = {
139
+ [K in LoaderOpName]: LoaderOpArgs[K] extends
140
+ | []
141
+ | [unknown?]
142
+ | [unknown?, unknown?]
143
+ | [unknown?, unknown?, unknown?]
144
+ ? K
145
+ : never
146
+ }[LoaderOpName]
147
+
148
+ /** A validator-bound helper returned by `loader.schema(validator)`. */
149
+ export interface LoaderSchemaFn {
150
+ /** Single JSON document → valid? */
151
+ (input: Uint8Array): boolean
152
+ /** Bulk JSON documents → bitset (1 per doc). */
153
+ (inputs: Uint8Array[]): Uint8Array
154
+ /** Count how many bulk documents are valid. */
155
+ count(inputs: Uint8Array[]): number
156
+ }
157
+
158
+ /** The global callable loader. */
159
+ export interface Loader {
160
+ /** Higher-order function: `loader(op)` → specialized hot op function. */
161
+ <K extends LoaderOpName>(op: K): LoaderOpFn<K>
162
+ /** Single-item dispatch. */
163
+ run<K extends LoaderOpName>(op: K, input: Uint8Array, ...rest: LoaderOpArgs[K]): LoaderScalar<K>
164
+ /** Bulk dispatch (one packed batch call, or adaptive scalar loop). */
165
+ run<K extends LoaderOpName>(
166
+ op: K,
167
+ inputs: Uint8Array[],
168
+ ...rest: LoaderBulkArgs<K>
169
+ ): LoaderBulk<K>
170
+ /** Coalesced, cache-aware load (ops with required extra args throw). */
171
+ load<K extends LoadableName>(
172
+ op: K,
173
+ input: Uint8Array,
174
+ opts?: LoadOptions,
175
+ ): Promise<LoaderScalar<K>>
176
+ /** Sync cache peek. */
177
+ cache<K extends LoaderOpName>(op: K, input: Uint8Array, key?: string): LoaderScalar<K> | undefined
178
+ /**
179
+ * Bind a `SchemaValidator` for repeated single/bulk JSON-schema validation
180
+ * plus a whole-batch `count`. The returned callable uses the loader's normal
181
+ * dispatch machinery (cost model + counters).
182
+ */
183
+ schema(validator: SchemaValidator): LoaderSchemaFn
184
+ /** Clear the shared cache (pending coalesced loads still flush). */
185
+ clear(): void
186
+ /** Update options at runtime. */
187
+ configure(options: LoaderOptions): void
188
+ readonly stats: LoaderStats
189
+ readonly opNames: LoaderOpName[]
190
+ }
191
+
192
+ // ── Internal state ──────────────────────────────────────────────────────────
193
+
194
+ interface OpCounters {
195
+ scalarCalls: number
196
+ batchCalls: number
197
+ itemsDispatched: number
198
+ cachedHits: number
199
+ }
200
+
201
+ /** Load-dispatch strategy selector for `load()`. */
202
+ type LoadStrategy = 'auto' | 'single' | 'coalesce'
203
+
204
+ /**
205
+ * Pre-bound per-op dispatch context — avoids Map lookups on the hot path.
206
+ * Holds the load-aware `load()` strategy plus its cheap integer signals so
207
+ * the single-vs-coalesce decision costs no Map lookup or allocation.
208
+ */
209
+ interface OpDispatchCtx {
210
+ cost: LoaderCostModel
211
+ counters: OpCounters
212
+ sampleEvery: number
213
+ /** Per-op sample tick counter (advances on every dispatch). */
214
+ counter: number
215
+ /** Current `load()` strategy for this op. */
216
+ mode: 'single' | 'coalesce'
217
+ /** Consecutive single-load flushes observed (coalesce mode only). */
218
+ singleStreak: number
219
+ /** A single-mode load is waiting on its microtask to dispatch. */
220
+ pendingSingle: boolean
221
+ pendingInput: Uint8Array | null
222
+ pendingResolve: ((value: unknown) => void) | null
223
+ pendingReject: ((error: unknown) => void) | null
224
+ pendingKey: string | undefined
225
+ /** Default-key attempts (adaptive key computation). */
226
+ keyAttempts: number
227
+ /** Default-key cache hits observed. */
228
+ keyHits: number
229
+ /** Periodic re-probe counter for skipped default keys. */
230
+ keyProbeCounter: number
231
+ }
232
+
233
+ /** A memoized op fn plus a hook to rebind its dispatch context on configure. */
234
+ interface OpFnEntry {
235
+ op: LoaderOpName
236
+ fn: LoaderOpFn<LoaderOpName>
237
+ /** Rebind to a fresh context after `configure()` so dispatch + stats stay current. */
238
+ rebind(ctx: OpDispatchCtx): void
239
+ }
240
+
241
+ interface LoaderState {
242
+ options: Required<
243
+ Pick<
244
+ LoaderOptions,
245
+ 'adaptive' | 'batchMin' | 'maxCacheKeys' | 'caching' | 'sampleEvery' | 'loadStrategy'
246
+ >
247
+ >
248
+ ctxs: Map<LoaderOpName, OpDispatchCtx>
249
+ cache: LoaderCache
250
+ coalescer: TickCoalescer
251
+ opFns: Map<LoaderOpName, OpFnEntry>
252
+ flushes: number
253
+ /** Reusable packed-input scratch for the zero-alloc fast flush (grows). */
254
+ packScratch: Uint8Array
255
+ /** Reusable packed-output buffer for the zero-alloc fast flush (grows). */
256
+ outScratch: Uint8Array
257
+ /** Reusable packed-key output buffer for the batched default-key flush. */
258
+ keyScratch: Uint8Array
259
+ }
260
+
261
+ const KEY_SEP = '\u0000'
262
+
263
+ // ── Load-aware `load()` strategy + adaptive-key tuning ──
264
+ /** Consecutive single-load flushes before an op drops the coalescer. */
265
+ const SINGLE_AFTER = 4
266
+ /** Default-key attempts before unique inputs stop paying the fnv1a64 key. */
267
+ const KEY_WARMUP = 8
268
+ /** After warm-up with zero hits, re-probe the default key every N loads. */
269
+ const KEY_PROBE_EVERY = 64
270
+
271
+ /** Get (or lazily create) the dispatch context for an op. */
272
+ function ctxFor(state: LoaderState, op: LoaderOpName): OpDispatchCtx {
273
+ let ctx = state.ctxs.get(op)
274
+ if (!ctx) {
275
+ ctx = {
276
+ cost: createCostModel({
277
+ adaptive: state.options.adaptive,
278
+ batchMin: state.options.batchMin,
279
+ }),
280
+ counters: { scalarCalls: 0, batchCalls: 0, itemsDispatched: 0, cachedHits: 0 },
281
+ sampleEvery: state.options.sampleEvery,
282
+ counter: 0,
283
+ // Load-aware strategy: "auto" starts coalescing so bursts batch from
284
+ // the first tick; it drops to "single" after sustained single-load
285
+ // flushes and back to "coalesce" on a same-tick burst.
286
+ mode: state.options.loadStrategy === 'single' ? 'single' : 'coalesce',
287
+ singleStreak: 0,
288
+ pendingSingle: false,
289
+ pendingInput: null,
290
+ pendingResolve: null,
291
+ pendingReject: null,
292
+ pendingKey: undefined,
293
+ keyAttempts: 0,
294
+ keyHits: 0,
295
+ keyProbeCounter: 0,
296
+ }
297
+ state.ctxs.set(op, ctx)
298
+ }
299
+ return ctx
300
+ }
301
+
302
+ /** Build a bulk-shaped result by looping scalar calls (the adaptive fallback). */
303
+ function scalarLoopBulk(spec: LoaderOpSpec, items: Uint8Array[], rest: unknown[]): unknown {
304
+ const n = items.length
305
+ const paired = spec.pairedRest ?? []
306
+ // For paired ops the `rest` holds per-item companion ARRAYS; split them so
307
+ // each scalar call gets its own companion + the shared args. Non-paired ops
308
+ // take the `rest` array directly (zero allocation on the fallback path).
309
+ const itemRest = (i: number): unknown[] =>
310
+ paired.length === 0
311
+ ? rest
312
+ : rest.map((r, idx) => (paired.includes(idx) ? (r as unknown[])[i] : r))
313
+ switch (spec.kind) {
314
+ case 'boolean': {
315
+ const out = new Uint8Array(n)
316
+ let i = 0
317
+ // NOTE: index the output with `i` (not `i++` inline) so `itemRest(i)`
318
+ // sees the CURRENT index — `out[i++] = f(...itemRest(i))` would read the
319
+ // already-incremented index (undefined companions for paired ops).
320
+ for (const item of items) {
321
+ out[i] = spec.scalar(item, ...itemRest(i)) ? 1 : 0
322
+ i++
323
+ }
324
+ return out
325
+ }
326
+ case 'number': {
327
+ const out = new Uint32Array(n)
328
+ let i = 0
329
+ for (const item of items) {
330
+ out[i] = spec.scalar(item, ...itemRest(i)) as number
331
+ i++
332
+ }
333
+ return out
334
+ }
335
+ case 'bigint': {
336
+ const out = spec.unsigned ? new BigUint64Array(n) : new BigInt64Array(n)
337
+ let i = 0
338
+ for (const item of items) {
339
+ out[i] = spec.scalar(item, ...itemRest(i)) as bigint
340
+ i++
341
+ }
342
+ return out
343
+ }
344
+ case 'bytes': {
345
+ const out = new Array<Uint8Array>(n)
346
+ let i = 0
347
+ for (const item of items) {
348
+ out[i] = spec.scalar(item, ...itemRest(i)) as Uint8Array
349
+ i++
350
+ }
351
+ return out
352
+ }
353
+ }
354
+ }
355
+
356
+ /** Single-item dispatch — no Map lookups; timed only on sample ticks. */
357
+ function dispatchSingle(
358
+ ctx: OpDispatchCtx,
359
+ spec: LoaderOpSpec,
360
+ input: Uint8Array,
361
+ rest: unknown[],
362
+ ): unknown {
363
+ const c = ctx.counters
364
+ c.scalarCalls++
365
+ c.itemsDispatched++
366
+ const t = ++ctx.counter % ctx.sampleEvery === 0 ? nowNs() : 0
367
+ const out = rest.length === 0 ? spec.scalar(input) : spec.scalar(input, ...rest)
368
+ if (t !== 0) ctx.cost.recordScalar(1, nowNs() - t)
369
+ return out
370
+ }
371
+
372
+ /** Bulk dispatch — packed batch, or adaptive scalar loop for tiny batches. */
373
+ function dispatchBulk(
374
+ ctx: OpDispatchCtx,
375
+ spec: LoaderOpSpec,
376
+ items: Uint8Array[],
377
+ rest: unknown[],
378
+ ): unknown {
379
+ const n = items.length
380
+ const c = ctx.counters
381
+ c.itemsDispatched += n
382
+
383
+ if (ctx.cost.decide(n)) {
384
+ const t = ++ctx.counter % ctx.sampleEvery === 0 ? nowNs() : 0
385
+ const out = spec.batch(items, ...rest)
386
+ if (t !== 0) ctx.cost.recordBatch(n, nowNs() - t)
387
+ c.batchCalls++
388
+ return out
389
+ }
390
+
391
+ const t = ++ctx.counter % ctx.sampleEvery === 0 ? nowNs() : 0
392
+ const out = scalarLoopBulk(spec, items, rest)
393
+ if (t !== 0) ctx.cost.recordScalar(n, nowNs() - t)
394
+ c.scalarCalls++
395
+ return out
396
+ }
397
+
398
+ /** Write a computed value into the cache when the request opted in. */
399
+ function maybeCache(state: LoaderState, req: LoadRequest, value: unknown): void {
400
+ if (req.key !== undefined) state.cache.set(req.key, value)
401
+ }
402
+
403
+ /** Handle a coalesced flush group for one op. */
404
+ function handleFlush(state: LoaderState, op: LoaderOpName, requests: LoadRequest[]): void {
405
+ state.flushes++
406
+ const ctx = ctxFor(state, op)
407
+ const spec: LoaderOpSpec = LOADER_OPS[op]
408
+ const n = requests.length
409
+ if (n === 1) {
410
+ // Low load observed. After a sustained run of single flushes, drop the
411
+ // coalescer for this op and dispatch singles directly (mode: "single").
412
+ if (ctx.mode === 'coalesce' && state.options.loadStrategy === 'auto') {
413
+ ctx.singleStreak++
414
+ if (ctx.singleStreak >= SINGLE_AFTER) ctx.mode = 'single'
415
+ }
416
+ const req = requests[0]
417
+ if (req === undefined) return
418
+ try {
419
+ // A lone coalesce-mode load may carry a deferred default key: compute it
420
+ // here (scalar fnv — no packed overhead for a single item), and resolve
421
+ // straight from the cache when present so no native op compute happens.
422
+ if (req.needsDefaultKey) {
423
+ if (shouldComputeDefaultKey(ctx)) {
424
+ ctx.keyAttempts++
425
+ const k = op + KEY_SEP + String(rust.fnv1a64(req.input))
426
+ const hit = state.cache.get(k)
427
+ if (hit !== undefined) {
428
+ ctx.counters.cachedHits++
429
+ ctx.keyHits++
430
+ req.resolve(hit)
431
+ return
432
+ }
433
+ req.key = k
434
+ }
435
+ req.needsDefaultKey = false
436
+ }
437
+ const value = dispatchSingle(ctx, spec, req.input, [])
438
+ maybeCache(state, req, value)
439
+ req.resolve(value)
440
+ } catch (err) {
441
+ req.reject(err)
442
+ }
443
+ return
444
+ }
445
+
446
+ // A real burst (n >= 2) keeps coalescing; reset the single-run counter.
447
+ ctx.singleStreak = 0
448
+
449
+ // Zero-alloc fast flush (boolean/number/bigint ops): pack the inputs into a
450
+ // reusable scratch, write the packed native result straight into a reusable
451
+ // output buffer via the `_into` variants, and read each element out of it —
452
+ // no intermediate unpack array, no per-call native Vec or JS allocation.
453
+ if (spec.fast !== undefined) {
454
+ const c = ctx.counters
455
+ c.batchCalls++
456
+ c.itemsDispatched += n
457
+
458
+ // Pack the requests directly (no `items.map`) into the reusable scratch.
459
+ let total = 4
460
+ for (const req of requests) total += 4 + req.input.byteLength
461
+ let scratch = state.packScratch
462
+ if (scratch.byteLength < total) {
463
+ scratch = new Uint8Array(Math.max(256, total * 2))
464
+ state.packScratch = scratch
465
+ }
466
+ const packView = viewForArrayBuffer(scratch.buffer, scratch.byteOffset)
467
+ packView.setUint32(0, n, true)
468
+ let pos = 4
469
+ for (const req of requests) {
470
+ packView.setUint32(pos, req.input.byteLength, true)
471
+ pos += 4
472
+ scratch.set(req.input, pos)
473
+ pos += req.input.byteLength
474
+ }
475
+
476
+ // Hybrid coalesce: when any request deferred a default key, compute ALL
477
+ // keys in ONE packed fnv1a64 crossing, resolve cache hits with NO native
478
+ // op compute, and run the packed op batch only on the remaining misses
479
+ // (repacked). The adaptive key-skip decision is made once per flush.
480
+ const needKeys = requests.some((r) => r.needsDefaultKey)
481
+ const computeKeys = needKeys && shouldComputeDefaultKey(ctx)
482
+ let keyView: DataView | null = null
483
+ if (computeKeys) {
484
+ ctx.keyAttempts += n
485
+ const keyTotal = 4 + n * 8
486
+ let keyBuf = state.keyScratch
487
+ if (keyBuf.byteLength < keyTotal) {
488
+ keyBuf = new Uint8Array(keyTotal)
489
+ state.keyScratch = keyBuf
490
+ }
491
+ fnvBatchKeysInto(scratch.subarray(0, total), keyBuf)
492
+ keyView = viewForArrayBuffer(keyBuf.buffer, keyBuf.byteOffset)
493
+ }
494
+
495
+ const pending: number[] = []
496
+ for (let i = 0; i < n; i++) {
497
+ const req = requests[i]
498
+ if (req === undefined) continue
499
+ let key = req.key
500
+ if (key === undefined && req.needsDefaultKey && keyView !== null) {
501
+ key = op + KEY_SEP + String(keyView.getBigUint64(4 + i * 8, true))
502
+ req.key = key
503
+ }
504
+ if (key !== undefined) {
505
+ const hit = state.cache.get(key)
506
+ if (hit !== undefined) {
507
+ ctx.counters.cachedHits++
508
+ if (req.needsDefaultKey) ctx.keyHits++
509
+ req.resolve(hit)
510
+ continue
511
+ }
512
+ }
513
+ pending.push(i)
514
+ }
515
+ if (pending.length === 0) return
516
+
517
+ // Repack the pending (miss) requests into the same scratch and run the
518
+ // packed op batch. scratch is sized >= total >= the subset size, so it is
519
+ // reused without reallocating; the op only runs on the misses.
520
+ const m = pending.length
521
+ let total2 = 4
522
+ for (const i of pending) {
523
+ const req = requests[i]
524
+ if (req !== undefined) total2 += 4 + req.input.byteLength
525
+ }
526
+ packView.setUint32(0, m, true)
527
+ let pos2 = 4
528
+ for (let j = 0; j < m; j++) {
529
+ const pi = pending[j]
530
+ if (pi === undefined) continue
531
+ const req = requests[pi]
532
+ if (req === undefined) continue
533
+ packView.setUint32(pos2, req.input.byteLength, true)
534
+ pos2 += 4
535
+ scratch.set(req.input, pos2)
536
+ pos2 += req.input.byteLength
537
+ }
538
+
539
+ // Size the reusable output buffer to the exact packed result.
540
+ const outSize = spec.fast.outSize(m)
541
+ let out = state.outScratch
542
+ if (out.byteLength < outSize) {
543
+ out = new Uint8Array(outSize)
544
+ state.outScratch = out
545
+ }
546
+ try {
547
+ spec.fast.batch(scratch.subarray(0, total2), out)
548
+ const outView = viewForArrayBuffer(out.buffer, out.byteOffset)
549
+ for (let j = 0; j < m; j++) {
550
+ const pi = pending[j]
551
+ if (pi === undefined) continue
552
+ const req = requests[pi]
553
+ if (req === undefined) continue
554
+ const value = spec.fast.element(outView, out, j)
555
+ maybeCache(state, req, value)
556
+ req.resolve(value)
557
+ }
558
+ } catch (err) {
559
+ for (const j of pending) {
560
+ const req = requests[j]
561
+ if (req === undefined) continue
562
+ req.reject(err)
563
+ }
564
+ }
565
+ return
566
+ }
567
+
568
+ const items = requests.map((r) => r.input)
569
+ try {
570
+ const bulk = dispatchBulk(ctx, spec, items, [])
571
+ for (let i = 0; i < n; i++) {
572
+ const req = requests[i]
573
+ if (req === undefined) continue
574
+ const value = spec.element(bulk, i)
575
+ maybeCache(state, req, value)
576
+ req.resolve(value)
577
+ }
578
+ } catch (err) {
579
+ for (const req of requests) req.reject(err)
580
+ }
581
+ }
582
+
583
+ /**
584
+ * Decide whether to compute a default (`fnv1a64`) cache key for this call.
585
+ * Warm-up always computes (documented default). Once enough attempts show
586
+ * ZERO hits the inputs are unique — skip the key (one native crossing +
587
+ * string alloc) so `load()` stays cheap for unique workloads; a periodic
588
+ * probe re-enables it if inputs start repeating.
589
+ */
590
+ function shouldComputeDefaultKey(ctx: OpDispatchCtx): boolean {
591
+ if (ctx.keyAttempts < KEY_WARMUP) return true
592
+ if (ctx.keyHits > 0) return true
593
+ return ++ctx.keyProbeCounter % KEY_PROBE_EVERY === 0
594
+ }
595
+
596
+ /** Resolve a single-mode load: dispatch the scalar and (optionally) cache. */
597
+ function flushSingle(state: LoaderState, op: LoaderOpName, ctx: OpDispatchCtx): void {
598
+ const spec = LOADER_OPS[op]
599
+ const input = ctx.pendingInput
600
+ const resolve = ctx.pendingResolve
601
+ const reject = ctx.pendingReject
602
+ const key = ctx.pendingKey
603
+ ctx.pendingSingle = false
604
+ ctx.pendingInput = null
605
+ ctx.pendingResolve = null
606
+ ctx.pendingReject = null
607
+ ctx.pendingKey = undefined
608
+ // A same-tick burst moved this request to the coalescer — nothing to do.
609
+ if (input === null || resolve === null) return
610
+ try {
611
+ const value = dispatchSingle(ctx, spec, input, [])
612
+ if (key !== undefined) state.cache.set(key, value)
613
+ resolve(value)
614
+ } catch (err) {
615
+ if (reject !== null) reject(err)
616
+ }
617
+ }
618
+
619
+ /** Shared `load` implementation used by `opFn.load` and `loader.load`. */
620
+ function loadOne(
621
+ state: LoaderState,
622
+ op: LoaderOpName,
623
+ input: Uint8Array,
624
+ opts?: LoadOptions,
625
+ ): Promise<unknown> {
626
+ // Ops with required extra args cannot be coalesced (all group members must
627
+ // share the same args); surface a clear error instead of a broken batch.
628
+ if (hasRequiredRest(op)) {
629
+ return Promise.reject(
630
+ new TypeError(
631
+ `loader.load: op "${op}" requires extra arguments; use loader.run("${op}", …) instead`,
632
+ ),
633
+ )
634
+ }
635
+
636
+ const ctx = ctxFor(state, op)
637
+ const spec: LoaderOpSpec = LOADER_OPS[op]
638
+ const caching = state.options.caching && state.options.maxCacheKeys > 0 && (opts?.cache ?? true)
639
+
640
+ let key: string | undefined
641
+ let needsDefaultKey = false
642
+ if (caching) {
643
+ if (opts?.key !== undefined) {
644
+ // Explicit key: computable synchronously, so keep the eager cache check
645
+ // and let a hit resolve without a coalescer flush.
646
+ key = op + KEY_SEP + opts.key
647
+ const hit = state.cache.get(key)
648
+ if (hit !== undefined) {
649
+ ctx.counters.cachedHits++
650
+ return Promise.resolve(hit)
651
+ }
652
+ } else if (ctx.mode === 'coalesce' && spec.fast !== undefined && state.coalescer.pending > 0) {
653
+ // Mid-burst on a cheap (fast) op: defer the default key to the flush so
654
+ // all keys are computed in ONE packed native crossing instead of one
655
+ // scalar fnv1a64 crossing per load. A LONE load (the first in the tick,
656
+ // `pending === 0`) keeps the eager key so a cache hit still resolves
657
+ // synchronously with no coalescer flush. Single mode and non-fast ops
658
+ // also keep eager keys.
659
+ needsDefaultKey = true
660
+ } else if (shouldComputeDefaultKey(ctx)) {
661
+ ctx.keyAttempts++
662
+ key = op + KEY_SEP + String(rust.fnv1a64(input))
663
+ const hit = state.cache.get(key)
664
+ if (hit !== undefined) {
665
+ ctx.counters.cachedHits++
666
+ ctx.keyHits++
667
+ return Promise.resolve(hit)
668
+ }
669
+ }
670
+ }
671
+
672
+ return new Promise((resolve, reject) => {
673
+ // Load-time strategy: low load → single scalar call (no coalescer
674
+ // bookkeeping, no flush); a rising same-tick load → switch to bulk.
675
+ if (ctx.mode === 'single') {
676
+ if (!ctx.pendingSingle) {
677
+ ctx.pendingSingle = true
678
+ ctx.pendingInput = input
679
+ ctx.pendingResolve = resolve
680
+ ctx.pendingReject = reject
681
+ ctx.pendingKey = key
682
+ queueMicrotask(() => flushSingle(state, op, ctx))
683
+ } else {
684
+ // A second load in the same event-loop tick = rising load → coalesce.
685
+ ctx.mode = 'coalesce'
686
+ const first: LoadRequest = {
687
+ input: ctx.pendingInput ?? input,
688
+ resolve: ctx.pendingResolve ?? resolve,
689
+ reject: ctx.pendingReject ?? reject,
690
+ key: ctx.pendingKey,
691
+ }
692
+ ctx.pendingSingle = false
693
+ ctx.pendingInput = null
694
+ ctx.pendingResolve = null
695
+ ctx.pendingReject = null
696
+ ctx.pendingKey = undefined
697
+ state.coalescer.enqueue(op, first)
698
+ state.coalescer.enqueue(op, { input, resolve, reject, key })
699
+ }
700
+ return
701
+ }
702
+ state.coalescer.enqueue(op, { input, resolve, reject, key, needsDefaultKey })
703
+ })
704
+ }
705
+
706
+ function hasRequiredRest(op: LoaderOpName): boolean {
707
+ // Derived statically; a cheap runtime guard is unnecessary, so keep this
708
+ // allowlist aligned with `LoadableName` above. Ops with required extra args
709
+ // OR per-item companions cannot be coalesced (all group members must share
710
+ // the same args) — `load()` is unavailable for them.
711
+ switch (op) {
712
+ case 'signCookie':
713
+ case 'verifyCookie':
714
+ case 'csrfVerify':
715
+ case 'passwordHash':
716
+ case 'passwordVerify':
717
+ case 'hmacSha256':
718
+ case 'hmacSha256Verify':
719
+ case 'aeadEncrypt':
720
+ case 'aeadDecrypt':
721
+ case 'jwtSign':
722
+ case 'jwtVerify':
723
+ case 'jsonPatch':
724
+ case 'urlResolve':
725
+ case 'wsFrameEncode':
726
+ case 'schemaValidate':
727
+ return true
728
+ default:
729
+ return false
730
+ }
731
+ }
732
+
733
+ /** Build a specialized hot function for one op. */
734
+ function makeOpFn<K extends LoaderOpName>(op: K, state: LoaderState): OpFnEntry {
735
+ const spec = LOADER_OPS[op]
736
+ // `ctx` is rebound on `configure()` (see OpFnEntry.rebind) so the hot path
737
+ // stays a zero-lookup closure WITHOUT going stale after reconfiguration.
738
+ let ctx = ctxFor(state, op)
739
+
740
+ function opFn(input: Uint8Array | Uint8Array[], ...rest: unknown[]): unknown {
741
+ if (Array.isArray(input)) {
742
+ return dispatchBulk(ctx, spec, input as Uint8Array[], rest)
743
+ }
744
+ return dispatchSingle(ctx, spec, input as Uint8Array, rest)
745
+ }
746
+
747
+ const statsGetter = (): LoaderOpStats => {
748
+ const c = ctx.counters
749
+ return {
750
+ scalarCalls: c.scalarCalls,
751
+ batchCalls: c.batchCalls,
752
+ itemsDispatched: c.itemsDispatched,
753
+ cachedHits: c.cachedHits,
754
+ batchMin: ctx.cost.batchMin,
755
+ mode: ctx.mode,
756
+ }
757
+ }
758
+
759
+ const loadFn = hasRequiredRest(op)
760
+ ? undefined
761
+ : (input: Uint8Array, opts?: LoadOptions) =>
762
+ loadOne(state, op, input, opts) as Promise<LoaderScalar<K>>
763
+
764
+ Object.defineProperties(opFn, {
765
+ name: { value: op, enumerable: true },
766
+ stats: { get: statsGetter, enumerable: true },
767
+ clear: { value: () => state.cache.clear(), enumerable: true },
768
+ cache: {
769
+ value: (input: Uint8Array, key?: string) => {
770
+ const raw = key ?? String(rust.fnv1a64(input))
771
+ return state.cache.get(op + KEY_SEP + raw) as LoaderScalar<K> | undefined
772
+ },
773
+ enumerable: true,
774
+ },
775
+ load: { value: loadFn, enumerable: true },
776
+ })
777
+
778
+ return {
779
+ op,
780
+ fn: opFn as unknown as LoaderOpFn<LoaderOpName>,
781
+ rebind(next: OpDispatchCtx) {
782
+ ctx = next
783
+ },
784
+ }
785
+ }
786
+
787
+ // ── Factory + singleton ─────────────────────────────────────────────────────
788
+
789
+ /** Create an isolated loader with the given defaults. */
790
+ export function createLoader(options: LoaderOptions = {}): Loader {
791
+ const state: LoaderState = {
792
+ options: {
793
+ adaptive: options.adaptive !== false,
794
+ batchMin: Math.max(2, Math.min(8, options.batchMin ?? 2)),
795
+ maxCacheKeys: options.maxCacheKeys ?? 256,
796
+ caching: options.caching !== false,
797
+ sampleEvery: options.sampleEvery ?? 32,
798
+ loadStrategy: options.loadStrategy ?? 'auto',
799
+ },
800
+ ctxs: new Map(),
801
+ cache: createLruCache(options.maxCacheKeys ?? 256),
802
+ coalescer: createTickCoalescer((op, requests) =>
803
+ handleFlush(state, op as LoaderOpName, requests),
804
+ ),
805
+ opFns: new Map(),
806
+ flushes: 0,
807
+ packScratch: new Uint8Array(0),
808
+ outScratch: new Uint8Array(0),
809
+ keyScratch: new Uint8Array(0),
810
+ }
811
+
812
+ const loader = ((op: LoaderOpName): LoaderOpFn<LoaderOpName> => {
813
+ let entry = state.opFns.get(op)
814
+ if (!entry) {
815
+ entry = makeOpFn(op, state)
816
+ state.opFns.set(op, entry)
817
+ }
818
+ return entry.fn
819
+ }) as unknown as Loader
820
+
821
+ const runFn = (
822
+ op: LoaderOpName,
823
+ input: Uint8Array | Uint8Array[],
824
+ ...rest: unknown[]
825
+ ): unknown => {
826
+ const spec = LOADER_OPS[op]
827
+ const ctx = ctxFor(state, op)
828
+ if (Array.isArray(input)) {
829
+ return dispatchBulk(ctx, spec, input, rest)
830
+ }
831
+ return dispatchSingle(ctx, spec, input, rest)
832
+ }
833
+
834
+ const loadFn = <K extends LoadableName>(op: K, input: Uint8Array, opts?: LoadOptions) =>
835
+ loadOne(state, op, input, opts) as Promise<LoaderScalar<K>>
836
+
837
+ const cacheFn = <K extends LoaderOpName>(
838
+ op: K,
839
+ input: Uint8Array,
840
+ key?: string,
841
+ ): LoaderScalar<K> | undefined => {
842
+ const raw = key ?? String(rust.fnv1a64(input))
843
+ return state.cache.get(op + KEY_SEP + raw) as LoaderScalar<K> | undefined
844
+ }
845
+
846
+ const configureFn = (next: LoaderOptions) => {
847
+ const prev = state.options
848
+ state.options = {
849
+ adaptive: next.adaptive ?? prev.adaptive,
850
+ batchMin: Math.max(2, Math.min(8, next.batchMin ?? prev.batchMin)),
851
+ maxCacheKeys: next.maxCacheKeys ?? prev.maxCacheKeys,
852
+ caching: next.caching ?? prev.caching,
853
+ sampleEvery: next.sampleEvery ?? prev.sampleEvery,
854
+ loadStrategy: next.loadStrategy ?? prev.loadStrategy,
855
+ }
856
+ // A config change invalidates learned thresholds; start fresh AND rebind
857
+ // every memoized op fn so its dispatch/stats keep using the current ctx.
858
+ state.ctxs.clear()
859
+ for (const entry of state.opFns.values()) {
860
+ entry.rebind(ctxFor(state, entry.op))
861
+ }
862
+ // Rebuild the cache if its capacity changed.
863
+ if (next.maxCacheKeys !== undefined && next.maxCacheKeys !== prev.maxCacheKeys) {
864
+ state.cache = createLruCache(state.options.maxCacheKeys)
865
+ }
866
+ }
867
+
868
+ const statsGetter = (): LoaderStats => {
869
+ let scalarCalls = 0
870
+ let batchCalls = 0
871
+ let itemsDispatched = 0
872
+ let cachedHits = 0
873
+ for (const ctx of state.ctxs.values()) {
874
+ const c = ctx.counters
875
+ scalarCalls += c.scalarCalls
876
+ batchCalls += c.batchCalls
877
+ itemsDispatched += c.itemsDispatched
878
+ cachedHits += c.cachedHits
879
+ }
880
+ return {
881
+ scalarCalls,
882
+ batchCalls,
883
+ itemsDispatched,
884
+ flushes: state.flushes,
885
+ cachedHits,
886
+ cacheSize: state.cache.size,
887
+ cacheEvictions: state.cache.evictions,
888
+ }
889
+ }
890
+
891
+ const opNamesGetter = (): LoaderOpName[] => Object.keys(LOADER_OPS) as LoaderOpName[]
892
+
893
+ // Bind a SchemaValidator: single/bulk validate through the loader's normal
894
+ // dispatch machinery (shared cost model + counters), plus a whole-batch count.
895
+ const schemaFn = (validator: SchemaValidator): LoaderSchemaFn => {
896
+ const spec = LOADER_OPS.schemaValidate
897
+ const ctx = ctxFor(state, 'schemaValidate')
898
+ const fn = ((input: Uint8Array | Uint8Array[]) => {
899
+ if (Array.isArray(input)) {
900
+ return dispatchBulk(ctx, spec, input, [validator]) as Uint8Array
901
+ }
902
+ return dispatchSingle(ctx, spec, input, [validator]) as boolean
903
+ }) as LoaderSchemaFn
904
+ Object.defineProperty(fn, 'count', {
905
+ value: (inputs: Uint8Array[]) => rust.batch.schemaValidateCount(validator, inputs),
906
+ enumerable: true,
907
+ })
908
+ return fn
909
+ }
910
+
911
+ Object.defineProperties(loader, {
912
+ run: { value: runFn, enumerable: true },
913
+ load: { value: loadFn, enumerable: true },
914
+ cache: { value: cacheFn, enumerable: true },
915
+ schema: { value: schemaFn, enumerable: true },
916
+ clear: { value: () => state.cache.clear(), enumerable: true },
917
+ configure: { value: configureFn, enumerable: true },
918
+ stats: { get: statsGetter, enumerable: true },
919
+ opNames: { get: opNamesGetter, enumerable: true },
920
+ })
921
+
922
+ return loader
923
+ }
924
+
925
+ /**
926
+ * The global, ready-to-use loader. Most consumers just do
927
+ * `import { loader } from "castrum"` and either `loader("validateEmail")(…)`
928
+ * (higher-order function) or `loader.run("validateEmail", …)`.
929
+ */
930
+ export const loader = createLoader()