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.
Files changed (120) hide show
  1. package/README.md +153 -630
  2. package/castrum.linux-x64-gnu.node +0 -0
  3. package/dist/index.d.ts +1653 -1817
  4. package/dist/index.js +6172 -4294
  5. package/docs/API.md +337 -0
  6. package/docs/ARCHITECTURE.md +111 -17
  7. package/docs/BENCHMARKS.md +117 -49
  8. package/docs/CASE_STUDY.md +243 -0
  9. package/docs/ENVIRONMENT.md +24 -7
  10. package/docs/FFI_BUN_GUIDE.md +335 -0
  11. package/docs/GETTING_STARTED.md +18 -34
  12. package/docs/HOW_TO_ADD_AN_OP.md +180 -0
  13. package/docs/INGRESS-ROUTER.md +82 -0
  14. package/docs/INGRESS.md +11 -7
  15. package/docs/NATIVE-ROUTE.md +77 -0
  16. package/docs/REPO_MAP.md +103 -53
  17. package/docs/adr/0001-two-wire-formats.md +1 -1
  18. package/docs/adr/0003-bun-builtins-delegation.md +6 -5
  19. package/docs/ai/LOCAL_DEV.md +81 -0
  20. package/docs/ai/TREE.md +1070 -0
  21. package/docs/bun-builtins-decision-matrix.md +25 -18
  22. package/index.ts +9 -16
  23. package/package.json +41 -28
  24. package/src/ingress/body.ts +167 -27
  25. package/src/ingress/decode/result-base.ts +10 -2
  26. package/src/ingress/handlers.ts +116 -285
  27. package/src/ingress/headers/security.ts +41 -0
  28. package/src/ingress/health.ts +1 -0
  29. package/src/ingress/index.ts +45 -169
  30. package/src/ingress/native-route.ts +182 -0
  31. package/src/ingress/packing/gather-raw-headers.ts +110 -22
  32. package/src/ingress/packing/header-packing.ts +5 -5
  33. package/src/ingress/packing/input-packer.ts +17 -3
  34. package/src/ingress/packing/route-wire.ts +197 -0
  35. package/src/ingress/packing/select-headers.ts +20 -7
  36. package/src/ingress/path-matcher.ts +115 -0
  37. package/src/ingress/response/baked-response.ts +302 -0
  38. package/src/ingress/router.ts +277 -0
  39. package/src/ingress/routes/common.ts +50 -1
  40. package/src/ingress/routes/fallback.ts +2 -2
  41. package/src/ingress/routes/head.ts +3 -13
  42. package/src/ingress/routes/index.ts +5 -4
  43. package/src/ingress/routes/json-write.ts +26 -47
  44. package/src/ingress/routes/native.ts +130 -0
  45. package/src/ingress/routes/options.ts +2 -12
  46. package/src/ingress/routes/read.ts +3 -13
  47. package/src/ingress/routes/responder.ts +159 -0
  48. package/src/ingress/server-node.ts +7 -7
  49. package/src/ingress/server.ts +83 -112
  50. package/src/ingress/sync.ts +151 -0
  51. package/src/ingress/types.ts +44 -1
  52. package/src/integration/pipeline.ts +3 -0
  53. package/src/integration/streaming.ts +2 -3
  54. package/src/integration/websocket.ts +4 -3
  55. package/src/loader/batch.ts +4 -0
  56. package/src/loader/cost.ts +1 -0
  57. package/src/loader/index.ts +43 -228
  58. package/src/loader/state.ts +84 -0
  59. package/src/loader/types.ts +145 -0
  60. package/src/native/ffi/build/codecs.ts +736 -0
  61. package/src/native/ffi/build/compress.ts +248 -0
  62. package/src/native/ffi/build/instances.ts +598 -0
  63. package/src/native/ffi/build/parse.ts +539 -0
  64. package/src/native/ffi/build/util.ts +140 -0
  65. package/src/native/ffi/build.ts +94 -0
  66. package/src/native/ffi/constants.ts +58 -0
  67. package/src/native/ffi/selftest.ts +22 -0
  68. package/src/native/ffi/types.ts +499 -0
  69. package/src/native/ffi.ts +307 -1357
  70. package/src/native/loader.ts +52 -6
  71. package/src/native/types/instances.ts +279 -0
  72. package/src/native/types.ts +41 -238
  73. package/src/runtime/builtins.ts +131 -0
  74. package/src/runtime/codec.ts +147 -0
  75. package/src/runtime/detect.ts +36 -0
  76. package/src/runtime/index.ts +28 -0
  77. package/src/runtime/native.ts +35 -0
  78. package/src/runtime/server.ts +31 -0
  79. package/src/runtime/transport.ts +64 -0
  80. package/src/runtime/types.ts +173 -0
  81. package/src/runtime/uuid.ts +45 -0
  82. package/src/runtime/websocket.ts +30 -0
  83. package/src/rust-ffi/addon.ts +1 -1
  84. package/src/rust-ffi/batch/build.ts +155 -0
  85. package/src/rust-ffi/batch/index.ts +8 -0
  86. package/src/rust-ffi/batch/types.ts +92 -0
  87. package/src/rust-ffi/client.ts +5 -6
  88. package/src/rust-ffi/context.ts +39 -10
  89. package/src/rust-ffi/index.ts +28 -17
  90. package/src/rust-ffi/into.ts +21 -0
  91. package/src/rust-ffi/options.ts +1 -1
  92. package/src/rust-ffi/packed.ts +40 -2
  93. package/src/rust-ffi/proven.ts +16 -30
  94. package/src/rust-ffi/scalar/crypto.ts +188 -107
  95. package/src/rust-ffi/scalar/factories.ts +257 -27
  96. package/src/rust-ffi/scalar/hashing.ts +43 -41
  97. package/src/rust-ffi/scalar/http.ts +208 -77
  98. package/src/rust-ffi/scalar/index.ts +3 -3
  99. package/src/rust-ffi/scalar/interface.ts +88 -213
  100. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  101. package/src/rust-ffi/scalar/json.ts +28 -12
  102. package/src/rust-ffi/scalar/payload.ts +159 -42
  103. package/src/rust-ffi/text.ts +31 -5
  104. package/src/selection.ts +27 -31
  105. package/src/shared/adaptive.ts +1 -2
  106. package/src/shared/buffer-pool.ts +72 -17
  107. package/src/shared/bytes.ts +75 -6
  108. package/src/shared/codec.ts +78 -0
  109. package/src/shared/env.ts +4 -4
  110. package/src/shared/log.ts +1 -0
  111. package/src/shared/packed/index.ts +10 -0
  112. package/src/shared/packed/parsers.ts +31 -0
  113. package/src/shared/packed/schema.ts +23 -0
  114. package/src/shared/{packed.ts → packed/wire.ts} +31 -39
  115. package/src/shared/proven.ts +189 -496
  116. package/src/shared/runtime.ts +20 -25
  117. package/src/shared/uuid.ts +12 -28
  118. package/src/data/json-rows.ts +0 -33
  119. package/src/rust-ffi/batch.ts +0 -431
  120. package/src/shared/bench-classify.ts +0 -209
@@ -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 (typeof Bun === 'undefined') {
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 " +
@@ -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 {
@@ -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
@@ -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
- type LoaderBulkArgs,
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 { 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
+ 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
- LOADER_OPS,
58
+ type BulkResult,
47
59
  LOADER_OP_NAMES,
48
- type LoaderOpName,
49
- type LoaderOpArgs,
50
- type LoaderBulkArgs,
60
+ LOADER_OPS,
51
61
  type LoaderBulk,
52
- type LoaderScalar,
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
- // ── 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'
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)) as Uint8Array
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'