castrum 0.9.0 → 0.9.2
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/README.md +153 -630
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1653 -1817
- package/dist/index.js +6172 -4294
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +111 -17
- package/docs/BENCHMARKS.md +117 -49
- package/docs/CASE_STUDY.md +243 -0
- package/docs/ENVIRONMENT.md +24 -7
- package/docs/FFI_BUN_GUIDE.md +335 -0
- package/docs/GETTING_STARTED.md +18 -34
- package/docs/HOW_TO_ADD_AN_OP.md +180 -0
- package/docs/INGRESS-ROUTER.md +82 -0
- package/docs/INGRESS.md +11 -7
- package/docs/NATIVE-ROUTE.md +77 -0
- package/docs/REPO_MAP.md +103 -53
- package/docs/adr/0001-two-wire-formats.md +1 -1
- package/docs/adr/0003-bun-builtins-delegation.md +6 -5
- package/docs/ai/LOCAL_DEV.md +81 -0
- package/docs/ai/TREE.md +1070 -0
- package/docs/bun-builtins-decision-matrix.md +25 -18
- package/index.ts +9 -16
- package/package.json +41 -28
- package/src/ingress/body.ts +167 -27
- package/src/ingress/decode/result-base.ts +10 -2
- package/src/ingress/handlers.ts +116 -285
- package/src/ingress/headers/security.ts +41 -0
- package/src/ingress/health.ts +1 -0
- package/src/ingress/index.ts +45 -169
- package/src/ingress/native-route.ts +182 -0
- package/src/ingress/packing/gather-raw-headers.ts +110 -22
- package/src/ingress/packing/header-packing.ts +5 -5
- package/src/ingress/packing/input-packer.ts +17 -3
- package/src/ingress/packing/route-wire.ts +197 -0
- package/src/ingress/packing/select-headers.ts +20 -7
- package/src/ingress/path-matcher.ts +115 -0
- package/src/ingress/response/baked-response.ts +302 -0
- package/src/ingress/router.ts +277 -0
- package/src/ingress/routes/common.ts +50 -1
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +3 -13
- package/src/ingress/routes/index.ts +5 -4
- package/src/ingress/routes/json-write.ts +26 -47
- package/src/ingress/routes/native.ts +130 -0
- package/src/ingress/routes/options.ts +2 -12
- package/src/ingress/routes/read.ts +3 -13
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +7 -7
- package/src/ingress/server.ts +83 -112
- package/src/ingress/sync.ts +151 -0
- package/src/ingress/types.ts +44 -1
- package/src/integration/pipeline.ts +3 -0
- package/src/integration/streaming.ts +2 -3
- package/src/integration/websocket.ts +4 -3
- package/src/loader/batch.ts +4 -0
- package/src/loader/cost.ts +1 -0
- package/src/loader/index.ts +43 -228
- package/src/loader/state.ts +84 -0
- package/src/loader/types.ts +145 -0
- package/src/native/ffi/build/codecs.ts +736 -0
- package/src/native/ffi/build/compress.ts +248 -0
- package/src/native/ffi/build/instances.ts +598 -0
- package/src/native/ffi/build/parse.ts +539 -0
- package/src/native/ffi/build/util.ts +140 -0
- package/src/native/ffi/build.ts +94 -0
- package/src/native/ffi/constants.ts +58 -0
- package/src/native/ffi/selftest.ts +22 -0
- package/src/native/ffi/types.ts +499 -0
- package/src/native/ffi.ts +307 -1357
- package/src/native/loader.ts +52 -6
- package/src/native/types/instances.ts +279 -0
- package/src/native/types.ts +41 -238
- 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 +1 -1
- 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 +5 -6
- package/src/rust-ffi/context.ts +39 -10
- package/src/rust-ffi/index.ts +28 -17
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +1 -1
- package/src/rust-ffi/packed.ts +40 -2
- package/src/rust-ffi/proven.ts +16 -30
- package/src/rust-ffi/scalar/crypto.ts +188 -107
- package/src/rust-ffi/scalar/factories.ts +257 -27
- package/src/rust-ffi/scalar/hashing.ts +43 -41
- package/src/rust-ffi/scalar/http.ts +208 -77
- package/src/rust-ffi/scalar/index.ts +3 -3
- package/src/rust-ffi/scalar/interface.ts +88 -213
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +28 -12
- package/src/rust-ffi/scalar/payload.ts +159 -42
- package/src/rust-ffi/text.ts +31 -5
- package/src/selection.ts +27 -31
- package/src/shared/adaptive.ts +1 -2
- package/src/shared/buffer-pool.ts +72 -17
- package/src/shared/bytes.ts +75 -6
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +4 -4
- package/src/shared/log.ts +1 -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.ts → packed/wire.ts} +31 -39
- package/src/shared/proven.ts +189 -496
- package/src/shared/runtime.ts +20 -25
- package/src/shared/uuid.ts +12 -28
- package/src/data/json-rows.ts +0 -33
- package/src/rust-ffi/batch.ts +0 -431
- package/src/shared/bench-classify.ts +0 -209
package/src/ingress/types.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// helpers in ./context.ts, so the implementation modules never need to import
|
|
5
5
|
// the public barrel.
|
|
6
6
|
|
|
7
|
-
import type { IngressFastOptions } from './options'
|
|
8
7
|
import type { BakedIngressResult } from './decode/baked-result'
|
|
8
|
+
import type { IngressFastOptions } from './options'
|
|
9
9
|
|
|
10
10
|
/** Public ingress options (extends the fast-path options). */
|
|
11
11
|
export interface IngressOptions extends IngressFastOptions {
|
|
@@ -147,6 +147,15 @@ export interface OptimizedIngressHandler {
|
|
|
147
147
|
|
|
148
148
|
withContentType(headers: ReadonlyArray<[string, string]>, contentType: string): [string, string][]
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Zero-DOM JSON-validity check bound to this handler's native transport
|
|
152
|
+
* (bun:ffi primary, napi fallback). Provided so the pure route factories
|
|
153
|
+
* (`routes/*`) can validate request bodies via dependency injection without
|
|
154
|
+
* importing the native layer. Always present on handlers from
|
|
155
|
+
* `createIngressHandler`; optional so hand-rolled mocks need not provide it.
|
|
156
|
+
*/
|
|
157
|
+
jsonValid?: (bytes: Uint8Array) => boolean
|
|
158
|
+
|
|
150
159
|
/**
|
|
151
160
|
* Build a zero-copy `Response` whose body is the request's pooled output
|
|
152
161
|
* slice. The pooled buffer is returned to its pool once the body has been
|
|
@@ -155,3 +164,37 @@ export interface OptimizedIngressHandler {
|
|
|
155
164
|
*/
|
|
156
165
|
zeroCopyResponse(result: BakedIngressResult, ctx: BakedContext, init: ResponseInit): Response
|
|
157
166
|
}
|
|
167
|
+
|
|
168
|
+
// ── Responder bridge (native decides, JS formats the 2xx) ─────────
|
|
169
|
+
|
|
170
|
+
/** Terminal (rejection) response envelope style for responder routes.
|
|
171
|
+
* `"castrum"` (default) emits the pre-baked `ok:false`/`ratelimit-*` wire;
|
|
172
|
+
* `"ignex"` emits the ignex framework envelope (`{error,status,code}` +
|
|
173
|
+
* `x-frame-options`/`nosniff`/`referrer-policy` security headers). Additive —
|
|
174
|
+
* castrum's own wire formats are untouched. */
|
|
175
|
+
export type TerminalStyle = 'castrum' | 'ignex'
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Decoded request context handed to a {@link NativeResponder} after the native
|
|
179
|
+
* pipeline succeeded (non-terminal). The native pipeline owns parse/validate/
|
|
180
|
+
* CORS/rate-limit + terminal rejections; the responder builds the 2xx body
|
|
181
|
+
* (async OK). Fields are captured INSIDE the sync `run()` callback, so the
|
|
182
|
+
* pooled output buffer is never aliased after it is released.
|
|
183
|
+
*/
|
|
184
|
+
export interface NativeRequestContext {
|
|
185
|
+
readonly requestId: string
|
|
186
|
+
readonly status: number
|
|
187
|
+
readonly rateLimited: boolean
|
|
188
|
+
readonly retryAfterMs: number
|
|
189
|
+
/** Parsed query object (last-wins per key) — `{}` when `parseQuery` is off. */
|
|
190
|
+
readonly query: Record<string, string | string[]>
|
|
191
|
+
/** Parsed cookie object (last-wins per key) — `{}` when `parseCookies` is off. */
|
|
192
|
+
readonly cookies: Record<string, string>
|
|
193
|
+
/** Request body bytes when read for native validation, else empty. */
|
|
194
|
+
readonly body: Uint8Array
|
|
195
|
+
readonly ip: string | undefined
|
|
196
|
+
readonly req: Request
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** A JS 2xx builder invoked after the native pipeline succeeds (async OK). */
|
|
200
|
+
export type NativeResponder = (snap: NativeRequestContext) => Response | Promise<Response>
|
|
@@ -45,6 +45,7 @@ export interface PipelineContext {
|
|
|
45
45
|
readonly locals: Map<string, unknown>
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** Result of running `preprocess`: terminal short-circuit or pass-through. */
|
|
48
49
|
export interface PreprocessOutcome {
|
|
49
50
|
/** True when the pipeline short-circuited (error/denied) — serve `response`. */
|
|
50
51
|
readonly terminal: boolean
|
|
@@ -53,6 +54,7 @@ export interface PreprocessOutcome {
|
|
|
53
54
|
readonly ctx: PipelineContext
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/** Options for `createPipeline`. */
|
|
56
58
|
export interface CreatePipelineOptions {
|
|
57
59
|
/** Ingress options (same surface as `createIngressHandler`). */
|
|
58
60
|
options?: IngressHandlerOptions
|
|
@@ -74,6 +76,7 @@ export interface CreatePipelineOptions {
|
|
|
74
76
|
render?: (result: PipelineResult, ctx: PipelineContext) => Response | Promise<Response>
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
/** The composed ingress pipeline: preprocess / handleRequest / readBody. */
|
|
77
80
|
export interface IngressPipeline {
|
|
78
81
|
/** The underlying pre-baked handler (custom route factories / responses). */
|
|
79
82
|
readonly ingress: OptimizedIngressHandler
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/integration/streaming.ts — SSE response helper.
|
|
2
2
|
|
|
3
3
|
import { rust } from '../rust-ffi'
|
|
4
|
-
|
|
5
|
-
export interface SseEvent {
|
|
4
|
+
import { encoder } from '../shared/bytes'
|
|
5
|
+
/** One SSE event to frame via `sseResponse` (event/id/retry/data). */ export interface SseEvent {
|
|
6
6
|
/** Optional `event:` field. */
|
|
7
7
|
event?: string | null
|
|
8
8
|
/** Optional `id:` field. */
|
|
@@ -34,7 +34,6 @@ export function sseResponse(
|
|
|
34
34
|
events: AsyncIterable<SseEvent> | Iterable<SseEvent>,
|
|
35
35
|
init: ResponseInit = {},
|
|
36
36
|
): Response {
|
|
37
|
-
const encoder = new TextEncoder()
|
|
38
37
|
const stream = new ReadableStream<Uint8Array>({
|
|
39
38
|
async start(controller) {
|
|
40
39
|
try {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// src/integration/websocket.ts — WebSocket upgrade handshake helper.
|
|
2
2
|
|
|
3
|
+
import { isBun } from '../shared/runtime'
|
|
3
4
|
import { rust } from '../rust-ffi'
|
|
4
|
-
|
|
5
|
-
export interface WebSocketUpgradeOptions {
|
|
5
|
+
/** Options for `createWebSocketUpgrade` (supported subprotocols). */export interface WebSocketUpgradeOptions {
|
|
6
6
|
/** Subprotocols this server supports; the first client-requested match wins. */
|
|
7
7
|
protocols?: ReadonlyArray<string>
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/** The WebSocket upgrade handshake result (101 response + negotiation info). */
|
|
10
11
|
export interface WebSocketUpgradeResult {
|
|
11
12
|
/** The 101 Switching Protocols response — return it from your fetch handler. */
|
|
12
13
|
readonly response: Response
|
|
@@ -69,7 +70,7 @@ export function createWebSocketUpgrade(
|
|
|
69
70
|
// Bun permits it. This helper produces a Bun.serve-compatible 101 Response,
|
|
70
71
|
// so on Node throw a clear error pointing at the Node adapter's `upgrade`
|
|
71
72
|
// option (which writes the RFC 6455 handshake head directly).
|
|
72
|
-
if (
|
|
73
|
+
if (!isBun()) {
|
|
73
74
|
throw new Error(
|
|
74
75
|
"createWebSocketUpgrade is Bun-only: Node's Response cannot carry status 101. " +
|
|
75
76
|
"On Node, use createIngressServerNode's `upgrade` option and compute the " +
|
package/src/loader/batch.ts
CHANGED
|
@@ -94,6 +94,10 @@ export interface TickCoalescer {
|
|
|
94
94
|
readonly pending: number
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Create a microtask-based coalescer: `enqueue` schedules one flush that
|
|
99
|
+
* drains ALL pending ops; each op group is delivered to `flush` once.
|
|
100
|
+
*/
|
|
97
101
|
export function createTickCoalescer(
|
|
98
102
|
flush: (op: string, requests: LoadRequest[]) => void,
|
|
99
103
|
): TickCoalescer {
|
package/src/loader/cost.ts
CHANGED
|
@@ -50,6 +50,7 @@ export const nowNs: () => number =
|
|
|
50
50
|
? () => Number(process.hrtime.bigint())
|
|
51
51
|
: () => performance.now() * 1e6
|
|
52
52
|
|
|
53
|
+
/** Options for `createCostModel` (adaptive batch-threshold learning). */
|
|
53
54
|
export interface CostModelOptions {
|
|
54
55
|
/** Allow the model to adjust `batchMin` from measurement. Default true. */
|
|
55
56
|
adaptive?: boolean
|
package/src/loader/index.ts
CHANGED
|
@@ -18,247 +18,58 @@
|
|
|
18
18
|
// - caching → bounded LRU ("Hot Function Cache"), default key =
|
|
19
19
|
// fnv1a64(input) (skipped for sustained-unique workloads),
|
|
20
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.
|
|
21
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'
|
|
22
36
|
import {
|
|
23
|
-
LOADER_OPS,
|
|
24
37
|
fnvBatchKeysInto,
|
|
25
|
-
|
|
26
|
-
type LoaderOpArgs,
|
|
38
|
+
LOADER_OPS,
|
|
27
39
|
type LoaderOpName,
|
|
28
40
|
type LoaderOpSpec,
|
|
29
41
|
type LoaderScalar,
|
|
30
|
-
type LoaderBulk,
|
|
31
42
|
} from './ops'
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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'
|
|
43
55
|
|
|
44
56
|
// Re-export the op registry + types for introspection and authoring.
|
|
45
57
|
export {
|
|
46
|
-
|
|
58
|
+
type BulkResult,
|
|
47
59
|
LOADER_OP_NAMES,
|
|
48
|
-
|
|
49
|
-
type LoaderOpArgs,
|
|
50
|
-
type LoaderBulkArgs,
|
|
60
|
+
LOADER_OPS,
|
|
51
61
|
type LoaderBulk,
|
|
52
|
-
type
|
|
62
|
+
type LoaderBulkArgs,
|
|
63
|
+
type LoaderOpArgs,
|
|
64
|
+
type LoaderOpName,
|
|
53
65
|
type LoaderResultKind,
|
|
66
|
+
type LoaderScalar,
|
|
54
67
|
type ScalarResult,
|
|
55
|
-
type BulkResult,
|
|
56
68
|
} from './ops'
|
|
57
69
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
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'
|
|
70
|
+
// Re-export the public type surface so `import type { LoaderOptions } from
|
|
71
|
+
// '../loader'` keeps working (existing call sites).
|
|
72
|
+
export * from './types'
|
|
262
73
|
|
|
263
74
|
// ── Load-aware `load()` strategy + adaptive-key tuning ──
|
|
264
75
|
/** Consecutive single-load flushes before an op drops the coalescer. */
|
|
@@ -268,7 +79,6 @@ const KEY_WARMUP = 8
|
|
|
268
79
|
/** After warm-up with zero hits, re-probe the default key every N loads. */
|
|
269
80
|
const KEY_PROBE_EVERY = 64
|
|
270
81
|
|
|
271
|
-
/** Get (or lazily create) the dispatch context for an op. */
|
|
272
82
|
function ctxFor(state: LoaderState, op: LoaderOpName): OpDispatchCtx {
|
|
273
83
|
let ctx = state.ctxs.get(op)
|
|
274
84
|
if (!ctx) {
|
|
@@ -299,6 +109,11 @@ function ctxFor(state: LoaderState, op: LoaderOpName): OpDispatchCtx {
|
|
|
299
109
|
return ctx
|
|
300
110
|
}
|
|
301
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
|
+
|
|
302
117
|
/** Build a bulk-shaped result by looping scalar calls (the adaptive fallback). */
|
|
303
118
|
function scalarLoopBulk(spec: LoaderOpSpec, items: Uint8Array[], rest: unknown[]): unknown {
|
|
304
119
|
const n = items.length
|
|
@@ -345,7 +160,7 @@ function scalarLoopBulk(spec: LoaderOpSpec, items: Uint8Array[], rest: unknown[]
|
|
|
345
160
|
const out = new Array<Uint8Array>(n)
|
|
346
161
|
let i = 0
|
|
347
162
|
for (const item of items) {
|
|
348
|
-
out[i] = spec.scalar(item, ...itemRest(i))
|
|
163
|
+
out[i] = asBytes(spec.scalar(item, ...itemRest(i)))
|
|
349
164
|
i++
|
|
350
165
|
}
|
|
351
166
|
return out
|
|
@@ -364,7 +179,7 @@ function dispatchSingle(
|
|
|
364
179
|
c.scalarCalls++
|
|
365
180
|
c.itemsDispatched++
|
|
366
181
|
const t = ++ctx.counter % ctx.sampleEvery === 0 ? nowNs() : 0
|
|
367
|
-
const out = rest.length === 0 ? spec.scalar(input) : spec.scalar(input, ...rest)
|
|
182
|
+
const out = asBytes(rest.length === 0 ? spec.scalar(input) : spec.scalar(input, ...rest))
|
|
368
183
|
if (t !== 0) ctx.cost.recordScalar(1, nowNs() - t)
|
|
369
184
|
return out
|
|
370
185
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/loader/state.ts — internal loader dispatch state (pure types).
|
|
2
|
+
//
|
|
3
|
+
// The per-op dispatch context, memoized op-fn entry, and the aggregate
|
|
4
|
+
// `LoaderState` shared by the dispatch core (index.ts). `KEY_SEP` is the
|
|
5
|
+
// cache-key separator for `op + '\0' + key`.
|
|
6
|
+
|
|
7
|
+
import type { LoaderCache, TickCoalescer } from './batch'
|
|
8
|
+
import type { LoaderCostModel } from './cost'
|
|
9
|
+
import type { LoaderOpName } from './ops'
|
|
10
|
+
import type { LoaderOpFn, LoaderOptions } from './types'
|
|
11
|
+
|
|
12
|
+
// ── Internal state ──────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/** Per-op dispatch counters: scalar calls, batch calls, items dispatched, and
|
|
15
|
+
* cache hits. Advanced on every dispatch and exposed via `loader.stats()`. */
|
|
16
|
+
export interface OpCounters {
|
|
17
|
+
scalarCalls: number
|
|
18
|
+
batchCalls: number
|
|
19
|
+
itemsDispatched: number
|
|
20
|
+
cachedHits: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pre-bound per-op dispatch context — avoids Map lookups on the hot path.
|
|
25
|
+
* Holds the load-aware `load()` strategy plus its cheap integer signals so
|
|
26
|
+
* the single-vs-coalesce decision costs no Map lookup or allocation.
|
|
27
|
+
*/
|
|
28
|
+
export interface OpDispatchCtx {
|
|
29
|
+
cost: LoaderCostModel
|
|
30
|
+
counters: OpCounters
|
|
31
|
+
sampleEvery: number
|
|
32
|
+
/** Per-op sample tick counter (advances on every dispatch). */
|
|
33
|
+
counter: number
|
|
34
|
+
/** Current `load()` strategy for this op. */
|
|
35
|
+
mode: 'single' | 'coalesce'
|
|
36
|
+
/** Consecutive single-load flushes observed (coalesce mode only). */
|
|
37
|
+
singleStreak: number
|
|
38
|
+
/** A single-mode load is waiting on its microtask to dispatch. */
|
|
39
|
+
pendingSingle: boolean
|
|
40
|
+
pendingInput: Uint8Array | null
|
|
41
|
+
pendingResolve: ((value: unknown) => void) | null
|
|
42
|
+
pendingReject: ((error: unknown) => void) | null
|
|
43
|
+
pendingKey: string | undefined
|
|
44
|
+
/** Default-key attempts (adaptive key computation). */
|
|
45
|
+
keyAttempts: number
|
|
46
|
+
/** Default-key cache hits observed. */
|
|
47
|
+
keyHits: number
|
|
48
|
+
/** Periodic re-probe counter for skipped default keys. */
|
|
49
|
+
keyProbeCounter: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A memoized op fn plus a hook to rebind its dispatch context on configure. */
|
|
53
|
+
export interface OpFnEntry {
|
|
54
|
+
op: LoaderOpName
|
|
55
|
+
fn: LoaderOpFn<LoaderOpName>
|
|
56
|
+
/** Rebind to a fresh context after `configure()` so dispatch + stats stay current. */
|
|
57
|
+
rebind(ctx: OpDispatchCtx): void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Aggregate loader state shared by the dispatch core (index.ts): the resolved
|
|
61
|
+
* options, per-op dispatch contexts, the LRU cache + tick coalescer, memoized
|
|
62
|
+
* op fns, and the reusable zero-alloc pack/out/key scratch buffers. */
|
|
63
|
+
export interface LoaderState {
|
|
64
|
+
options: Required<
|
|
65
|
+
Pick<
|
|
66
|
+
LoaderOptions,
|
|
67
|
+
'adaptive' | 'batchMin' | 'maxCacheKeys' | 'caching' | 'sampleEvery' | 'loadStrategy'
|
|
68
|
+
>
|
|
69
|
+
>
|
|
70
|
+
ctxs: Map<LoaderOpName, OpDispatchCtx>
|
|
71
|
+
cache: LoaderCache
|
|
72
|
+
coalescer: TickCoalescer
|
|
73
|
+
opFns: Map<LoaderOpName, OpFnEntry>
|
|
74
|
+
flushes: number
|
|
75
|
+
/** Reusable packed-input scratch for the zero-alloc fast flush (grows). */
|
|
76
|
+
packScratch: Uint8Array
|
|
77
|
+
/** Reusable packed-output buffer for the zero-alloc fast flush (grows). */
|
|
78
|
+
outScratch: Uint8Array
|
|
79
|
+
/** Reusable packed-key output buffer for the batched default-key flush. */
|
|
80
|
+
keyScratch: Uint8Array
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** NUL separator used to join composite cache keys (never appears in keys). */
|
|
84
|
+
export const KEY_SEP = '\u0000'
|