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.
- package/LICENSE +21 -0
- package/README.md +154 -368
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +2763 -0
- package/dist/index.js +8707 -0
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +507 -0
- package/docs/BENCHMARKS.md +343 -0
- package/docs/CASE_STUDY.md +240 -0
- package/docs/ENVIRONMENT.md +81 -0
- package/docs/FFI_BUN_GUIDE.md +327 -0
- package/docs/GETTING_STARTED.md +192 -0
- package/docs/INGRESS-ROUTER.md +81 -0
- package/docs/INGRESS.md +746 -0
- package/docs/NATIVE-ROUTE.md +53 -0
- package/docs/REPO_MAP.md +279 -0
- package/docs/adr/0001-two-wire-formats.md +39 -0
- package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
- package/docs/adr/0003-bun-builtins-delegation.md +43 -0
- package/docs/bun-builtins-decision-matrix.md +115 -0
- package/index.ts +44 -8
- package/package.json +98 -33
- package/src/ingress/body.ts +289 -0
- package/src/ingress/constants.ts +220 -48
- package/src/ingress/context.ts +136 -0
- package/src/ingress/decode/baked-result.ts +121 -0
- package/src/ingress/decode/fast-result.ts +160 -0
- package/src/ingress/decode/packed-sections.ts +56 -0
- package/src/ingress/decode/result-base.ts +172 -0
- package/src/ingress/errors.ts +72 -0
- package/src/ingress/fast.ts +152 -1021
- package/src/ingress/handlers.ts +639 -1001
- package/src/ingress/headers/baked-templates.ts +102 -0
- package/src/ingress/headers/cors.ts +44 -0
- package/src/ingress/headers/fast-templates.ts +238 -0
- package/src/ingress/headers/hsts.ts +83 -0
- package/src/ingress/health.ts +68 -0
- package/src/ingress/index.ts +142 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +139 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +164 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +102 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/router.ts +237 -0
- package/src/ingress/routes/common.ts +79 -0
- package/src/ingress/routes/delete.ts +14 -0
- package/src/ingress/routes/echo.ts +107 -0
- package/src/ingress/routes/fallback.ts +23 -0
- package/src/ingress/routes/head.ts +27 -0
- package/src/ingress/routes/index.ts +11 -0
- package/src/ingress/routes/json-write.ts +129 -0
- package/src/ingress/routes/options.ts +26 -0
- package/src/ingress/routes/read.ts +35 -0
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +475 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +191 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +220 -0
- package/src/integration/streaming.ts +78 -0
- package/src/integration/websocket.ts +93 -0
- package/src/loader/batch.ts +137 -0
- package/src/loader/cost.ts +105 -0
- package/src/loader/index.ts +745 -0
- package/src/loader/ops.ts +657 -0
- package/src/loader/state.ts +84 -0
- package/src/loader/types.ts +145 -0
- package/src/native/ffi/constants.ts +58 -0
- package/src/native/ffi/selftest.ts +729 -0
- package/src/native/ffi/types.ts +474 -0
- package/src/native/ffi.ts +1775 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +240 -0
- package/src/native/types/instances.ts +274 -0
- package/src/native/types.ts +401 -0
- package/src/runtime/builtins.ts +131 -0
- package/src/runtime/codec.ts +147 -0
- package/src/runtime/detect.ts +36 -0
- package/src/runtime/index.ts +28 -0
- package/src/runtime/native.ts +35 -0
- package/src/runtime/server.ts +31 -0
- package/src/runtime/transport.ts +64 -0
- package/src/runtime/types.ts +173 -0
- package/src/runtime/uuid.ts +45 -0
- package/src/runtime/websocket.ts +30 -0
- package/src/rust-ffi/addon.ts +10 -0
- package/src/rust-ffi/batch/build.ts +155 -0
- package/src/rust-ffi/batch/index.ts +8 -0
- package/src/rust-ffi/batch/types.ts +92 -0
- package/src/rust-ffi/client.ts +87 -0
- package/src/rust-ffi/context.ts +193 -0
- package/src/rust-ffi/index.ts +51 -541
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +28 -0
- package/src/rust-ffi/scalar/crypto.ts +346 -0
- package/src/rust-ffi/scalar/factories.ts +459 -0
- package/src/rust-ffi/scalar/hashing.ts +74 -0
- package/src/rust-ffi/scalar/http.ts +337 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +325 -0
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +61 -0
- package/src/rust-ffi/scalar/payload.ts +241 -0
- package/src/rust-ffi/text.ts +121 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +73 -0
- package/src/shared/adaptive.ts +67 -0
- package/src/shared/buffer-pool.ts +264 -0
- package/src/shared/bytes.ts +108 -9
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +75 -0
- package/src/shared/metrics.ts +247 -0
- package/src/shared/packed/index.ts +10 -0
- package/src/shared/packed/parsers.ts +31 -0
- package/src/shared/packed/schema.ts +23 -0
- package/src/shared/packed/wire.ts +504 -0
- package/src/shared/proven.ts +289 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +29 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +15 -0
- package/Cargo.toml +0 -73
- package/bench.ts +0 -3
- package/rust/batch.rs +0 -195
- package/rust/cookie_parser.rs +0 -70
- package/rust/cors.rs +0 -263
- package/rust/hashing.rs +0 -164
- package/rust/headers.rs +0 -234
- package/rust/hmac_sha256.rs +0 -113
- package/rust/http_parser.rs +0 -132
- package/rust/ingress.rs +0 -616
- package/rust/ingress_constants.rs +0 -89
- package/rust/ip_trust.rs +0 -185
- package/rust/json_ops.rs +0 -66
- package/rust/json_patch_ops.rs +0 -25
- package/rust/json_schema.rs +0 -151
- package/rust/json_ser.rs +0 -429
- package/rust/lib.rs +0 -48
- package/rust/method.rs +0 -141
- package/rust/mime_lookup.rs +0 -76
- package/rust/output.rs +0 -238
- package/rust/proxy.rs +0 -90
- package/rust/query_parser.rs +0 -141
- package/rust/random_token.rs +0 -52
- package/rust/rate_limit.rs +0 -203
- package/rust/terminal.rs +0 -95
- package/rust/unit_tests.rs +0 -843
- package/rust/url_codec.rs +0 -133
- package/rust/util.rs +0 -537
- package/rust/validation.rs +0 -107
- package/rust/websocket.rs +0 -23
- package/src/baseline/index.ts +0 -12
- package/src/baseline/tasks/cookie.ts +0 -22
- package/src/baseline/tasks/hashing.ts +0 -18
- package/src/baseline/tasks/hmac.ts +0 -38
- package/src/baseline/tasks/http.ts +0 -62
- package/src/baseline/tasks/json-patch.ts +0 -12
- package/src/baseline/tasks/json.ts +0 -50
- package/src/baseline/tasks/mime.ts +0 -5
- package/src/baseline/tasks/query.ts +0 -14
- package/src/baseline/tasks/token.ts +0 -15
- package/src/baseline/tasks/url.ts +0 -11
- package/src/baseline/tasks/validation.ts +0 -19
- package/src/baseline/tasks/websocket.ts +0 -14
- package/src/bench/assert.ts +0 -70
- package/src/bench/checks.ts +0 -227
- package/src/bench/checksum.ts +0 -27
- package/src/bench/comparisons.ts +0 -70
- package/src/bench/concurrent-worker.ts +0 -135
- package/src/bench/fixtures.ts +0 -233
- package/src/bench/index.ts +0 -2
- package/src/bench/measure.ts +0 -177
- package/src/bench/now.ts +0 -3
- package/src/bench/report.ts +0 -79
- package/src/bench/run.ts +0 -57
- package/src/bench/tasks/complex.ts +0 -232
- package/src/bench/tasks/concurrent.ts +0 -213
- package/src/bench/tasks/cookie.ts +0 -44
- package/src/bench/tasks/hashing.ts +0 -33
- package/src/bench/tasks/hmac.ts +0 -36
- package/src/bench/tasks/http.ts +0 -50
- package/src/bench/tasks/index.ts +0 -52
- package/src/bench/tasks/json-patch.ts +0 -21
- package/src/bench/tasks/json.ts +0 -33
- package/src/bench/tasks/mime.ts +0 -21
- package/src/bench/tasks/query.ts +0 -44
- package/src/bench/tasks/stress.ts +0 -142
- package/src/bench/tasks/token.ts +0 -20
- package/src/bench/tasks/url.ts +0 -34
- package/src/bench/tasks/validation.ts +0 -57
- package/src/bench/tasks/websocket.ts +0 -21
- package/src/bench/types.ts +0 -60
- package/src/data/json-rows.ts +0 -33
- package/src/ingress/packed-input.ts +0 -59
- package/src/shared/json.ts +0 -18
- 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()
|