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.
- package/README.md +153 -630
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1374 -1772
- package/dist/index.js +5570 -4169
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +106 -14
- package/docs/BENCHMARKS.md +83 -47
- package/docs/CASE_STUDY.md +240 -0
- package/docs/ENVIRONMENT.md +8 -6
- package/docs/FFI_BUN_GUIDE.md +327 -0
- package/docs/GETTING_STARTED.md +15 -32
- package/docs/INGRESS-ROUTER.md +81 -0
- package/docs/INGRESS.md +5 -4
- package/docs/NATIVE-ROUTE.md +53 -0
- package/docs/REPO_MAP.md +46 -26
- package/docs/adr/0003-bun-builtins-delegation.md +6 -5
- package/docs/bun-builtins-decision-matrix.md +25 -18
- package/index.ts +9 -16
- package/package.json +36 -24
- package/src/ingress/body.ts +167 -27
- package/src/ingress/handlers.ts +91 -15
- package/src/ingress/health.ts +1 -0
- package/src/ingress/index.ts +39 -27
- package/src/ingress/packing/gather-raw-headers.ts +85 -21
- package/src/ingress/packing/header-packing.ts +5 -5
- package/src/ingress/packing/input-packer.ts +17 -3
- package/src/ingress/packing/select-headers.ts +12 -6
- package/src/ingress/router.ts +237 -0
- package/src/ingress/routes/common.ts +42 -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 +7 -31
- 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 +6 -6
- package/src/ingress/server.ts +45 -13
- package/src/ingress/types.ts +35 -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/constants.ts +58 -0
- package/src/native/ffi/selftest.ts +729 -0
- package/src/native/ffi/types.ts +474 -0
- package/src/native/ffi.ts +1063 -829
- package/src/native/loader.ts +52 -6
- package/src/native/types/instances.ts +274 -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 +2 -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 +253 -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
|
@@ -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 {
|
|
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
|
|
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 (
|
|
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 {
|
|
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. */
|
package/src/ingress/server.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
headHandler,
|
|
6
|
-
jsonWriteHandler,
|
|
5
|
+
deleteHandler,
|
|
7
6
|
echoHandler,
|
|
8
7
|
fallbackHandler,
|
|
9
|
-
|
|
8
|
+
headHandler,
|
|
9
|
+
jsonWriteHandler,
|
|
10
10
|
optionsHandler,
|
|
11
|
+
readHandler,
|
|
11
12
|
} from './routes'
|
|
12
13
|
import type { BakedHandlerOptions } from './routes/common'
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
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
|
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 {
|
|
@@ -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 (
|
|
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
|