castrum 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +153 -630
  2. package/castrum.linux-x64-gnu.node +0 -0
  3. package/dist/index.d.ts +1374 -1772
  4. package/dist/index.js +5570 -4169
  5. package/docs/API.md +337 -0
  6. package/docs/ARCHITECTURE.md +106 -14
  7. package/docs/BENCHMARKS.md +83 -47
  8. package/docs/CASE_STUDY.md +240 -0
  9. package/docs/ENVIRONMENT.md +8 -6
  10. package/docs/FFI_BUN_GUIDE.md +327 -0
  11. package/docs/GETTING_STARTED.md +15 -32
  12. package/docs/INGRESS-ROUTER.md +81 -0
  13. package/docs/INGRESS.md +5 -4
  14. package/docs/NATIVE-ROUTE.md +53 -0
  15. package/docs/REPO_MAP.md +46 -26
  16. package/docs/adr/0003-bun-builtins-delegation.md +6 -5
  17. package/docs/bun-builtins-decision-matrix.md +25 -18
  18. package/index.ts +9 -16
  19. package/package.json +36 -24
  20. package/src/ingress/body.ts +167 -27
  21. package/src/ingress/handlers.ts +91 -15
  22. package/src/ingress/health.ts +1 -0
  23. package/src/ingress/index.ts +39 -27
  24. package/src/ingress/packing/gather-raw-headers.ts +85 -21
  25. package/src/ingress/packing/header-packing.ts +5 -5
  26. package/src/ingress/packing/input-packer.ts +17 -3
  27. package/src/ingress/packing/select-headers.ts +12 -6
  28. package/src/ingress/router.ts +237 -0
  29. package/src/ingress/routes/common.ts +42 -1
  30. package/src/ingress/routes/fallback.ts +2 -2
  31. package/src/ingress/routes/head.ts +3 -13
  32. package/src/ingress/routes/index.ts +5 -4
  33. package/src/ingress/routes/json-write.ts +7 -31
  34. package/src/ingress/routes/options.ts +2 -12
  35. package/src/ingress/routes/read.ts +3 -13
  36. package/src/ingress/routes/responder.ts +159 -0
  37. package/src/ingress/server-node.ts +6 -6
  38. package/src/ingress/server.ts +45 -13
  39. package/src/ingress/types.ts +35 -1
  40. package/src/integration/pipeline.ts +3 -0
  41. package/src/integration/streaming.ts +2 -3
  42. package/src/integration/websocket.ts +4 -3
  43. package/src/loader/batch.ts +4 -0
  44. package/src/loader/cost.ts +1 -0
  45. package/src/loader/index.ts +43 -228
  46. package/src/loader/state.ts +84 -0
  47. package/src/loader/types.ts +145 -0
  48. package/src/native/ffi/constants.ts +58 -0
  49. package/src/native/ffi/selftest.ts +729 -0
  50. package/src/native/ffi/types.ts +474 -0
  51. package/src/native/ffi.ts +1063 -829
  52. package/src/native/loader.ts +52 -6
  53. package/src/native/types/instances.ts +274 -0
  54. package/src/native/types.ts +41 -238
  55. package/src/runtime/builtins.ts +131 -0
  56. package/src/runtime/codec.ts +147 -0
  57. package/src/runtime/detect.ts +36 -0
  58. package/src/runtime/index.ts +28 -0
  59. package/src/runtime/native.ts +35 -0
  60. package/src/runtime/server.ts +31 -0
  61. package/src/runtime/transport.ts +64 -0
  62. package/src/runtime/types.ts +173 -0
  63. package/src/runtime/uuid.ts +45 -0
  64. package/src/runtime/websocket.ts +30 -0
  65. package/src/rust-ffi/addon.ts +1 -1
  66. package/src/rust-ffi/batch/build.ts +155 -0
  67. package/src/rust-ffi/batch/index.ts +8 -0
  68. package/src/rust-ffi/batch/types.ts +92 -0
  69. package/src/rust-ffi/client.ts +5 -6
  70. package/src/rust-ffi/context.ts +39 -10
  71. package/src/rust-ffi/index.ts +28 -17
  72. package/src/rust-ffi/into.ts +21 -0
  73. package/src/rust-ffi/options.ts +1 -1
  74. package/src/rust-ffi/packed.ts +2 -2
  75. package/src/rust-ffi/proven.ts +16 -30
  76. package/src/rust-ffi/scalar/crypto.ts +188 -107
  77. package/src/rust-ffi/scalar/factories.ts +253 -27
  78. package/src/rust-ffi/scalar/hashing.ts +43 -41
  79. package/src/rust-ffi/scalar/http.ts +208 -77
  80. package/src/rust-ffi/scalar/index.ts +3 -3
  81. package/src/rust-ffi/scalar/interface.ts +88 -213
  82. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  83. package/src/rust-ffi/scalar/json.ts +28 -12
  84. package/src/rust-ffi/scalar/payload.ts +159 -42
  85. package/src/rust-ffi/text.ts +31 -5
  86. package/src/selection.ts +27 -31
  87. package/src/shared/adaptive.ts +1 -2
  88. package/src/shared/buffer-pool.ts +72 -17
  89. package/src/shared/bytes.ts +75 -6
  90. package/src/shared/codec.ts +78 -0
  91. package/src/shared/env.ts +4 -4
  92. package/src/shared/log.ts +1 -0
  93. package/src/shared/packed/index.ts +10 -0
  94. package/src/shared/packed/parsers.ts +31 -0
  95. package/src/shared/packed/schema.ts +23 -0
  96. package/src/shared/{packed.ts → packed/wire.ts} +31 -39
  97. package/src/shared/proven.ts +189 -496
  98. package/src/shared/runtime.ts +20 -25
  99. package/src/shared/uuid.ts +12 -28
  100. package/src/data/json-rows.ts +0 -33
  101. package/src/rust-ffi/batch.ts +0 -431
  102. package/src/shared/bench-classify.ts +0 -209
@@ -1,8 +1,7 @@
1
1
  // src/ingress/routes/options.ts — Pre-baked OPTIONS handler (CORS preflight).
2
2
 
3
3
  import type { OptimizedIngressHandler } from '../types'
4
- import { resolveIp, type BakedHandlerOptions } from './common'
5
- import { secondsFromMs } from '../shared'
4
+ import { buildSuccessInit, resolveIp, type BakedHandlerOptions } from './common'
6
5
 
7
6
  /**
8
7
  * Pre-baked OPTIONS handler: serves the native pipeline's CORS preflight
@@ -22,15 +21,6 @@ export function optionsHandler(
22
21
  const terminal = ingress.terminalResponse(req, result, ctx)
23
22
  if (terminal) return terminal
24
23
 
25
- return new Response(null, {
26
- status: 204,
27
- headers: ingress.responseHeaders(
28
- result.headerVariant,
29
- ctx.requestIdHeader,
30
- ctx.origin,
31
- result.rateRemaining,
32
- result.rateResetMs > 0 ? secondsFromMs(result.rateResetMs) : undefined,
33
- ),
34
- })
24
+ return new Response(null, buildSuccessInit(ingress, result, ctx, 204))
35
25
  })
36
26
  }
@@ -2,8 +2,7 @@
2
2
 
3
3
  import type { BakedContext, OptimizedIngressHandler } from '../types'
4
4
  import type { BakedIngressResult } from '../decode/baked-result'
5
- import { resolveIp, type BakedHandlerOptions } from './common'
6
- import { secondsFromMs } from '../shared'
5
+ import { buildSuccessInit, runBaked, type BakedHandlerOptions } from './common'
7
6
 
8
7
  /**
9
8
  * Pre-baked GET read handler: returns the ingress body JSON on success.
@@ -25,21 +24,12 @@ export function readHandler(
25
24
  return ingress.internalErrorResponse(ctx, result)
26
25
  }
27
26
 
28
- const init: ResponseInit = {
29
- status: 200,
30
- headers: ingress.responseHeaders(
31
- result.headerVariant,
32
- ctx.requestIdHeader,
33
- ctx.origin,
34
- result.rateRemaining,
35
- result.rateResetMs > 0 ? secondsFromMs(result.rateResetMs) : undefined,
36
- ),
37
- }
27
+ const init = buildSuccessInit(ingress, result, ctx)
38
28
 
39
29
  return copyBody
40
30
  ? new Response(result.bodyJson(true), init)
41
31
  : ingress.zeroCopyResponse(result, ctx, init)
42
32
  }
43
33
 
44
- return (req, srv) => ingress.run<Response>(req, resolveIp(req, srv, opts), null, respond)
34
+ return runBaked(ingress, opts, respond)
45
35
  }
@@ -0,0 +1,159 @@
1
+ // src/ingress/routes/responder.ts — JS responder bridge over the native
2
+ // pipeline ("native decides, JS formats the 2xx").
3
+ //
4
+ // The responder bridge is the castrum foundation for delegating a WHOLE
5
+ // request to the per-route native `IngressInner` while keeping the 2xx body in
6
+ // JS: the native pipeline runs parse/validate/CORS/rate-limit/limits and either
7
+ // produces a terminal (rejection) response — in the configured envelope style —
8
+ // or hands a decoded snapshot to a `NativeResponder` that builds the 2xx.
9
+ //
10
+ // The sync/async boundary is deliberate: `OptimizedIngressHandler.run()` is
11
+ // SYNC-only (`assertSyncCallback` rejects thenables), so the factory captures
12
+ // the decoded snapshot (requestId/query/cookies/body/verdicts) INSIDE the sync
13
+ // callback and calls the async `responder` OUTSIDE it, after the pooled output
14
+ // buffer has been released. `result.body` is the caller-provided request body
15
+ // (never the pooled output buffer), so the snapshot's `body` is safe to hold.
16
+
17
+ import { decoder } from '../../shared/bytes'
18
+ import { generateRequestId } from '../../shared/request-id'
19
+ import { readBodyWithLimit } from '../body'
20
+ import type { BakedIngressResult } from '../decode/baked-result'
21
+ import { errorCodeName, errorMessage } from '../errors'
22
+ import type { RouteHandler } from '../server'
23
+ import { DEFAULT_BODY_TIMEOUT_MS, DEFAULT_MAX_BODY_BYTES } from '../shared'
24
+ import { safeTerminalStatus } from '../status'
25
+ import type {
26
+ BakedContext,
27
+ NativeRequestContext,
28
+ NativeResponder,
29
+ OptimizedIngressHandler,
30
+ TerminalStyle,
31
+ } from '../types'
32
+ import { type BakedHandlerOptions, resolveIp } from './common'
33
+
34
+ /** Options for {@link nativeResponderRoute}. */
35
+ export interface ResponderRouteOptions extends BakedHandlerOptions {
36
+ /** Terminal envelope style. Default: `'castrum'` (the pre-baked wire). */
37
+ terminalStyle?: TerminalStyle
38
+ /**
39
+ * Read the request body and pass it to the native pipeline for validation
40
+ * (`requireJsonBody`/schema). Default `false` — the framework owns the body
41
+ * (the responder reads it via `ctx.body`); native only needs the bytes when
42
+ * a body stage is enabled in the route's `IngressHandlerOptions`.
43
+ */
44
+ readBody?: boolean
45
+ }
46
+
47
+ /** ignex framework security posture for terminal responses. */
48
+ const IGNGEX_SECURITY_HEADERS: [string, string][] = [
49
+ ['x-frame-options', 'DENY'],
50
+ ['x-content-type-options', 'nosniff'],
51
+ ['referrer-policy', 'no-referrer'],
52
+ ]
53
+
54
+ /** Build the ignex-framework terminal envelope (`{error,status,code}` + security headers). */
55
+ function buildIgngexTerminal(result: BakedIngressResult, _ctx: BakedContext): Response {
56
+ const preflightAllowed = result.isPreflight && result.corsAllowed
57
+ if (preflightAllowed) {
58
+ return new Response(null, { status: 204, headers: IGNGEX_SECURITY_HEADERS })
59
+ }
60
+ const status = safeTerminalStatus(result)
61
+ const body = JSON.stringify({
62
+ error: errorMessage(status, result.errorCode),
63
+ status,
64
+ code: errorCodeName(result.errorCode),
65
+ })
66
+ const headers = new Headers(IGNGEX_SECURITY_HEADERS)
67
+ headers.set('content-type', 'application/json; charset=utf-8')
68
+ return new Response(body, { status, headers })
69
+ }
70
+
71
+ /** Parse a `queryJson()`/`cookiesJson()` section into an object (empty on failure). */
72
+ function parseSection(json: string): Record<string, unknown> {
73
+ try {
74
+ const v: unknown = JSON.parse(json)
75
+ return v !== null && typeof v === 'object' && !Array.isArray(v)
76
+ ? (v as Record<string, unknown>)
77
+ : {}
78
+ } catch {
79
+ return {}
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Build a responder route: native pipeline decides + rejects (terminal
85
+ * response in the chosen style); on success the JS `responder` builds the 2xx.
86
+ * Returns an async `RouteHandler` (`(req, srv?, params?) => Promise<Response>`).
87
+ */
88
+ export function nativeResponderRoute(
89
+ ingress: OptimizedIngressHandler,
90
+ responder: NativeResponder,
91
+ opts: ResponderRouteOptions = {},
92
+ ): RouteHandler {
93
+ const terminalStyle: TerminalStyle = opts.terminalStyle ?? 'castrum'
94
+ const readBody = opts.readBody ?? false
95
+ const maxBodyBytes = opts.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
96
+ const bodyTimeoutMs = opts.bodyTimeoutMs ?? DEFAULT_BODY_TIMEOUT_MS
97
+
98
+ return async (req, srv, _params) => {
99
+ const ip = resolveIp(req, srv, opts)
100
+ // The request-id the framework surfaces (same source as the pre-baked path's
101
+ // `ctx.requestIdHeader`): the baked result does not carry it, so generate it
102
+ // here — the native pipeline uses the same rid for its internal frame.
103
+ const requestId = decoder.decode(generateRequestId())
104
+ // Read the body ONLY when native validation needs it (the framework owns
105
+ // the body otherwise). A `BODY_TOO_LARGE`/`REQUEST_TIMEOUT` throw becomes
106
+ // a terminal 413/408 from the native path.
107
+ let body: Uint8Array | null = null
108
+ if (readBody) {
109
+ try {
110
+ body = await readBodyWithLimit(req, maxBodyBytes, true, bodyTimeoutMs)
111
+ } catch (err) {
112
+ const code = (err as Error & { code?: string }).code
113
+ const status = code === 'REQUEST_TIMEOUT' ? 408 : code === 'BODY_TOO_LARGE' ? 413 : 400
114
+ const payload = JSON.stringify({ error: 'Bad Request', status, code: 'BAD_REQUEST' })
115
+ const headers = new Headers(IGNGEX_SECURITY_HEADERS)
116
+ headers.set('content-type', 'application/json; charset=utf-8')
117
+ return new Response(payload, { status, headers })
118
+ }
119
+ }
120
+
121
+ let terminal: Response | null = null
122
+ let snapshot: NativeRequestContext | null = null
123
+
124
+ // Sync callback (never returns a thenable). Captures the decoded snapshot
125
+ // or the terminal response, then the async responder runs outside `run()`.
126
+ ingress.run<null>(req, ip, body, (result, ctx) => {
127
+ if (result.terminal || !result.ok) {
128
+ terminal =
129
+ terminalStyle === 'ignex'
130
+ ? buildIgngexTerminal(result, ctx)
131
+ : ingress.terminalResponse(req, result, ctx)
132
+ return null
133
+ }
134
+ const query = parseSection(result.queryJson())
135
+ const cookies = parseSection(result.cookiesJson())
136
+ snapshot = {
137
+ requestId,
138
+ status: result.status,
139
+ rateLimited: result.rateLimited,
140
+ retryAfterMs: result.retryAfterMs,
141
+ query: query as Record<string, string | string[]>,
142
+ cookies: cookies as Record<string, string>,
143
+ body: body ?? new Uint8Array(0),
144
+ ip,
145
+ req,
146
+ }
147
+ return null
148
+ })
149
+
150
+ if (terminal !== null) {
151
+ return terminal
152
+ }
153
+ if (snapshot === null) {
154
+ // Native internal error (setInternalError) — the result was never OK.
155
+ return ingress.internalErrorResponse({ requestIdHeader: null, origin: null })
156
+ }
157
+ return responder(snapshot)
158
+ }
159
+ }
@@ -15,21 +15,20 @@
15
15
 
16
16
  import {
17
17
  createServer,
18
- type IncomingMessage,
19
18
  type Server as HttpServer,
19
+ type IncomingMessage,
20
20
  type ServerResponse,
21
21
  } from 'node:http'
22
- import { Readable, type Duplex } from 'node:stream'
23
-
22
+ import { type Duplex, Readable } from 'node:stream'
23
+ import { fallbackHandler } from './routes'
24
24
  import {
25
+ type BakedServer,
25
26
  buildPathMatcher,
26
27
  buildRouteHandlers,
27
- type BakedServer,
28
28
  type CreateIngressServerOptions,
29
- type ServerHandle,
30
29
  type RouteHandler,
30
+ type ServerHandle,
31
31
  } from './server'
32
- import { fallbackHandler } from './routes'
33
32
 
34
33
  export type { RouteHandler } from './server'
35
34
 
@@ -41,6 +40,7 @@ export type { RouteHandler } from './server'
41
40
  * body-read loop after headers arrive.
42
41
  */
43
42
  const DEFAULT_SOCKET_TIMEOUT_MS = 30_000
43
+
44
44
  import type { BakedHandlerOptions } from './routes/common'
45
45
 
46
46
  /** A Node.js ingress server: {@link BakedServer} plus an async ready signal. */
@@ -1,17 +1,18 @@
1
1
  // src/ingress/server.ts — Bun.serve builder over pre-baked route handlers.
2
2
 
3
+ import { isBun } from '../shared/runtime'
3
4
  import {
4
- readHandler,
5
- headHandler,
6
- jsonWriteHandler,
5
+ deleteHandler,
7
6
  echoHandler,
8
7
  fallbackHandler,
9
- deleteHandler,
8
+ headHandler,
9
+ jsonWriteHandler,
10
10
  optionsHandler,
11
+ readHandler,
11
12
  } from './routes'
12
13
  import type { BakedHandlerOptions } from './routes/common'
13
- import type { OptimizedIngressHandler } from './types'
14
- import { isBun } from '../shared/runtime'
14
+ import { nativeResponderRoute } from './routes/responder'
15
+ import type { NativeResponder, OptimizedIngressHandler, TerminalStyle } from './types'
15
16
 
16
17
  /** Server-level default for the socket request-body cap (16 MiB). */
17
18
  export const DEFAULT_MAX_REQUEST_BODY_SIZE = 16 * 1024 * 1024
@@ -45,6 +46,24 @@ export interface BakedRoute {
45
46
  cookies?: OptimizedIngressHandler
46
47
  /** Wires a DELETE read-style handler. */
47
48
  delete?: OptimizedIngressHandler
49
+ /**
50
+ * A JS responder route: the native pipeline owns parse/validate/CORS/
51
+ * rate-limit + terminal rejections; the responder builds the 2xx body
52
+ * (async OK). Wired for `methods` (default GET). Built by
53
+ * `nativeResponderRoute` (src/ingress/routes/responder.ts).
54
+ */
55
+ responder?: {
56
+ /** The route's compiled native `IngressInner`. */
57
+ ingress: OptimizedIngressHandler
58
+ /** The JS 2xx builder. */
59
+ handler: NativeResponder
60
+ /** HTTP methods to wire (default `['GET']`). */
61
+ methods?: ReadonlyArray<string>
62
+ /** Terminal envelope style (`'castrum'` default / `'ignex'`). */
63
+ terminalStyle?: TerminalStyle
64
+ /** Read the body for native validation (default false — framework owns it). */
65
+ readBody?: boolean
66
+ }
48
67
  /** Overrides `maxBodyBytes` for this route's write/echo handlers. */
49
68
  maxBodyBytes?: number
50
69
  /**
@@ -124,6 +143,7 @@ export interface BakedServer {
124
143
  port: number
125
144
  }
126
145
 
146
+ /** Options for `gracefulShutdown` (drain grace period + signals). */
127
147
  export interface GracefulShutdownOptions {
128
148
  /** Grace period (ms) to drain in-flight requests before force-closing. */
129
149
  timeoutMs?: number
@@ -182,13 +202,6 @@ export function gracefulShutdown(
182
202
  }
183
203
  }
184
204
 
185
- /**
186
- * Build the route → { method → handler } map from a server spec.
187
- *
188
- * Shared by `createIngressServer` (Bun.serve) and `createIngressServerNode`
189
- * (node:http) so both runtimes wire the exact same route handlers.
190
- */
191
-
192
205
  /** Options `buildRouteHandlers` actually consumes (a subset of the server
193
206
  * options — it does not need `port` or the Node-only server fields). */
194
207
  export type BuildRouteHandlersOptions = Pick<
@@ -196,6 +209,12 @@ export type BuildRouteHandlersOptions = Pick<
196
209
  'routes' | 'fallback' | 'getIp' | 'copyBody'
197
210
  >
198
211
 
212
+ /**
213
+ * Build the route → { method → handler } map from a server spec.
214
+ *
215
+ * Shared by `createIngressServer` (Bun.serve) and `createIngressServerNode`
216
+ * (node:http) so both runtimes wire the exact same route handlers.
217
+ */
199
218
  export function buildRouteHandlers(options: BuildRouteHandlersOptions): {
200
219
  routes: Record<string, Record<string, unknown>>
201
220
  baseOpts: BakedHandlerOptions
@@ -249,6 +268,19 @@ export function buildRouteHandlers(options: BuildRouteHandlersOptions): {
249
268
  methods.DELETE = deleteHandler(spec.delete, routeOpts)
250
269
  }
251
270
 
271
+ if (spec.responder) {
272
+ // JS responder route: native decides + rejects; JS builds the 2xx.
273
+ const responderRoute = nativeResponderRoute(spec.responder.ingress, spec.responder.handler, {
274
+ ...routeOpts,
275
+ terminalStyle: spec.responder.terminalStyle,
276
+ readBody: spec.responder.readBody,
277
+ })
278
+ const responderMethods = spec.responder.methods ?? ['GET']
279
+ for (const m of responderMethods) {
280
+ methods[m] = responderRoute
281
+ }
282
+ }
283
+
252
284
  // CORS preflight (OPTIONS) is served for EVERY route with a NATIVE handler
253
285
  // (not just write routes with a fallback), so read-only routes also answer
254
286
  // preflights with 204/403 from the native pipeline. Raw probe handlers
@@ -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 {
@@ -155,3 +155,37 @@ export interface OptimizedIngressHandler {
155
155
  */
156
156
  zeroCopyResponse(result: BakedIngressResult, ctx: BakedContext, init: ResponseInit): Response
157
157
  }
158
+
159
+ // ── Responder bridge (native decides, JS formats the 2xx) ─────────
160
+
161
+ /** Terminal (rejection) response envelope style for responder routes.
162
+ * `"castrum"` (default) emits the pre-baked `ok:false`/`ratelimit-*` wire;
163
+ * `"ignex"` emits the ignex framework envelope (`{error,status,code}` +
164
+ * `x-frame-options`/`nosniff`/`referrer-policy` security headers). Additive —
165
+ * castrum's own wire formats are untouched. */
166
+ export type TerminalStyle = 'castrum' | 'ignex'
167
+
168
+ /**
169
+ * Decoded request context handed to a {@link NativeResponder} after the native
170
+ * pipeline succeeded (non-terminal). The native pipeline owns parse/validate/
171
+ * CORS/rate-limit + terminal rejections; the responder builds the 2xx body
172
+ * (async OK). Fields are captured INSIDE the sync `run()` callback, so the
173
+ * pooled output buffer is never aliased after it is released.
174
+ */
175
+ export interface NativeRequestContext {
176
+ readonly requestId: string
177
+ readonly status: number
178
+ readonly rateLimited: boolean
179
+ readonly retryAfterMs: number
180
+ /** Parsed query object (last-wins per key) — `{}` when `parseQuery` is off. */
181
+ readonly query: Record<string, string | string[]>
182
+ /** Parsed cookie object (last-wins per key) — `{}` when `parseCookies` is off. */
183
+ readonly cookies: Record<string, string>
184
+ /** Request body bytes when read for native validation, else empty. */
185
+ readonly body: Uint8Array
186
+ readonly ip: string | undefined
187
+ readonly req: Request
188
+ }
189
+
190
+ /** A JS 2xx builder invoked after the native pipeline succeeds (async OK). */
191
+ 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