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.
- package/LICENSE +21 -0
- package/README.md +322 -59
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +3161 -0
- package/dist/index.js +7306 -0
- package/docs/ARCHITECTURE.md +415 -0
- package/docs/BENCHMARKS.md +307 -0
- package/docs/ENVIRONMENT.md +79 -0
- package/docs/GETTING_STARTED.md +209 -0
- package/docs/INGRESS.md +745 -0
- package/docs/REPO_MAP.md +259 -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 +42 -0
- package/docs/bun-builtins-decision-matrix.md +108 -0
- package/index.ts +51 -8
- package/package.json +73 -20
- package/src/data/json-rows.ts +13 -13
- package/src/ingress/body.ts +149 -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 +563 -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 +67 -0
- package/src/ingress/index.ts +130 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +75 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +150 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +96 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/routes/common.ts +38 -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 +37 -0
- package/src/ingress/routes/index.ts +10 -0
- package/src/ingress/routes/json-write.ts +153 -0
- package/src/ingress/routes/options.ts +36 -0
- package/src/ingress/routes/read.ts +45 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +443 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +157 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +217 -0
- package/src/integration/streaming.ts +79 -0
- package/src/integration/websocket.ts +92 -0
- package/src/loader/batch.ts +133 -0
- package/src/loader/cost.ts +104 -0
- package/src/loader/index.ts +930 -0
- package/src/loader/ops.ts +657 -0
- package/src/native/ffi.ts +1541 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +194 -0
- package/src/native/types.ts +598 -0
- package/src/rust-ffi/addon.ts +10 -0
- package/src/rust-ffi/batch.ts +431 -0
- package/src/rust-ffi/client.ts +88 -0
- package/src/rust-ffi/context.ts +164 -0
- package/src/rust-ffi/index.ts +40 -541
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +42 -0
- package/src/rust-ffi/scalar/crypto.ts +265 -0
- package/src/rust-ffi/scalar/factories.ts +233 -0
- package/src/rust-ffi/scalar/hashing.ts +72 -0
- package/src/rust-ffi/scalar/http.ts +206 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +450 -0
- package/src/rust-ffi/scalar/json.ts +45 -0
- package/src/rust-ffi/scalar/payload.ts +124 -0
- package/src/rust-ffi/text.ts +95 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +77 -0
- package/src/shared/adaptive.ts +68 -0
- package/src/shared/bench-classify.ts +209 -0
- package/src/shared/buffer-pool.ts +209 -0
- package/src/shared/bytes.ts +35 -5
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +74 -0
- package/src/shared/metrics.ts +247 -0
- package/src/shared/packed.ts +350 -154
- package/src/shared/proven.ts +596 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +34 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +31 -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/ingress/packed-input.ts +0 -59
- 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()
|