castrum 0.7.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/LICENSE +21 -0
- package/README.md +154 -368
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +2763 -0
- package/dist/index.js +8707 -0
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +507 -0
- package/docs/BENCHMARKS.md +343 -0
- package/docs/CASE_STUDY.md +240 -0
- package/docs/ENVIRONMENT.md +81 -0
- package/docs/FFI_BUN_GUIDE.md +327 -0
- package/docs/GETTING_STARTED.md +192 -0
- package/docs/INGRESS-ROUTER.md +81 -0
- package/docs/INGRESS.md +746 -0
- package/docs/NATIVE-ROUTE.md +53 -0
- package/docs/REPO_MAP.md +279 -0
- package/docs/adr/0001-two-wire-formats.md +39 -0
- package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
- package/docs/adr/0003-bun-builtins-delegation.md +43 -0
- package/docs/bun-builtins-decision-matrix.md +115 -0
- package/index.ts +44 -8
- package/package.json +98 -33
- package/src/ingress/body.ts +289 -0
- package/src/ingress/constants.ts +220 -48
- package/src/ingress/context.ts +136 -0
- package/src/ingress/decode/baked-result.ts +121 -0
- package/src/ingress/decode/fast-result.ts +160 -0
- package/src/ingress/decode/packed-sections.ts +56 -0
- package/src/ingress/decode/result-base.ts +172 -0
- package/src/ingress/errors.ts +72 -0
- package/src/ingress/fast.ts +152 -1021
- package/src/ingress/handlers.ts +639 -1001
- package/src/ingress/headers/baked-templates.ts +102 -0
- package/src/ingress/headers/cors.ts +44 -0
- package/src/ingress/headers/fast-templates.ts +238 -0
- package/src/ingress/headers/hsts.ts +83 -0
- package/src/ingress/health.ts +68 -0
- package/src/ingress/index.ts +142 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +139 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +164 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +102 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/router.ts +237 -0
- package/src/ingress/routes/common.ts +79 -0
- package/src/ingress/routes/delete.ts +14 -0
- package/src/ingress/routes/echo.ts +107 -0
- package/src/ingress/routes/fallback.ts +23 -0
- package/src/ingress/routes/head.ts +27 -0
- package/src/ingress/routes/index.ts +11 -0
- package/src/ingress/routes/json-write.ts +129 -0
- package/src/ingress/routes/options.ts +26 -0
- package/src/ingress/routes/read.ts +35 -0
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +475 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +191 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +220 -0
- package/src/integration/streaming.ts +78 -0
- package/src/integration/websocket.ts +93 -0
- package/src/loader/batch.ts +137 -0
- package/src/loader/cost.ts +105 -0
- package/src/loader/index.ts +745 -0
- package/src/loader/ops.ts +657 -0
- 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 +1775 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +240 -0
- package/src/native/types/instances.ts +274 -0
- package/src/native/types.ts +401 -0
- 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 +10 -0
- 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 +87 -0
- package/src/rust-ffi/context.ts +193 -0
- package/src/rust-ffi/index.ts +51 -541
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +28 -0
- package/src/rust-ffi/scalar/crypto.ts +346 -0
- package/src/rust-ffi/scalar/factories.ts +459 -0
- package/src/rust-ffi/scalar/hashing.ts +74 -0
- package/src/rust-ffi/scalar/http.ts +337 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +325 -0
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +61 -0
- package/src/rust-ffi/scalar/payload.ts +241 -0
- package/src/rust-ffi/text.ts +121 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +73 -0
- package/src/shared/adaptive.ts +67 -0
- package/src/shared/buffer-pool.ts +264 -0
- package/src/shared/bytes.ts +108 -9
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +75 -0
- package/src/shared/metrics.ts +247 -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/wire.ts +504 -0
- package/src/shared/proven.ts +289 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +29 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +15 -0
- package/Cargo.toml +0 -73
- package/bench.ts +0 -3
- package/rust/batch.rs +0 -195
- package/rust/cookie_parser.rs +0 -70
- package/rust/cors.rs +0 -263
- package/rust/hashing.rs +0 -164
- package/rust/headers.rs +0 -234
- package/rust/hmac_sha256.rs +0 -113
- package/rust/http_parser.rs +0 -132
- package/rust/ingress.rs +0 -616
- package/rust/ingress_constants.rs +0 -89
- package/rust/ip_trust.rs +0 -185
- package/rust/json_ops.rs +0 -66
- package/rust/json_patch_ops.rs +0 -25
- package/rust/json_schema.rs +0 -151
- package/rust/json_ser.rs +0 -429
- package/rust/lib.rs +0 -48
- package/rust/method.rs +0 -141
- package/rust/mime_lookup.rs +0 -76
- package/rust/output.rs +0 -238
- package/rust/proxy.rs +0 -90
- package/rust/query_parser.rs +0 -141
- package/rust/random_token.rs +0 -52
- package/rust/rate_limit.rs +0 -203
- package/rust/terminal.rs +0 -95
- package/rust/unit_tests.rs +0 -843
- package/rust/url_codec.rs +0 -133
- package/rust/util.rs +0 -537
- package/rust/validation.rs +0 -107
- package/rust/websocket.rs +0 -23
- package/src/baseline/index.ts +0 -12
- package/src/baseline/tasks/cookie.ts +0 -22
- package/src/baseline/tasks/hashing.ts +0 -18
- package/src/baseline/tasks/hmac.ts +0 -38
- package/src/baseline/tasks/http.ts +0 -62
- package/src/baseline/tasks/json-patch.ts +0 -12
- package/src/baseline/tasks/json.ts +0 -50
- package/src/baseline/tasks/mime.ts +0 -5
- package/src/baseline/tasks/query.ts +0 -14
- package/src/baseline/tasks/token.ts +0 -15
- package/src/baseline/tasks/url.ts +0 -11
- package/src/baseline/tasks/validation.ts +0 -19
- package/src/baseline/tasks/websocket.ts +0 -14
- package/src/bench/assert.ts +0 -70
- package/src/bench/checks.ts +0 -227
- package/src/bench/checksum.ts +0 -27
- package/src/bench/comparisons.ts +0 -70
- package/src/bench/concurrent-worker.ts +0 -135
- package/src/bench/fixtures.ts +0 -233
- package/src/bench/index.ts +0 -2
- package/src/bench/measure.ts +0 -177
- package/src/bench/now.ts +0 -3
- package/src/bench/report.ts +0 -79
- package/src/bench/run.ts +0 -57
- package/src/bench/tasks/complex.ts +0 -232
- package/src/bench/tasks/concurrent.ts +0 -213
- package/src/bench/tasks/cookie.ts +0 -44
- package/src/bench/tasks/hashing.ts +0 -33
- package/src/bench/tasks/hmac.ts +0 -36
- package/src/bench/tasks/http.ts +0 -50
- package/src/bench/tasks/index.ts +0 -52
- package/src/bench/tasks/json-patch.ts +0 -21
- package/src/bench/tasks/json.ts +0 -33
- package/src/bench/tasks/mime.ts +0 -21
- package/src/bench/tasks/query.ts +0 -44
- package/src/bench/tasks/stress.ts +0 -142
- package/src/bench/tasks/token.ts +0 -20
- package/src/bench/tasks/url.ts +0 -34
- package/src/bench/tasks/validation.ts +0 -57
- package/src/bench/tasks/websocket.ts +0 -21
- package/src/bench/types.ts +0 -60
- package/src/data/json-rows.ts +0 -33
- package/src/ingress/packed-input.ts +0 -59
- package/src/shared/json.ts +0 -18
- package/src/shared/packed.ts +0 -316
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// src/runtime/builtins.ts — Bun built-in delegation registry (the BUN_WINS set).
|
|
2
|
+
//
|
|
3
|
+
// Single source of truth for "under Bun this op is faster as a built-in than
|
|
4
|
+
// via the rust addon" (measured in docs/bun-builtins-decision-matrix.md). The
|
|
5
|
+
// scalar/text builders prefer `builtins.has(op)` over calling the native
|
|
6
|
+
// transport; `src/selection.ts` derives its `opImpl` decision from the same
|
|
7
|
+
// registry, so the hardcoded `BUN_WINS` set + `isBun()` branch disappear.
|
|
8
|
+
//
|
|
9
|
+
// NOTE: `gzipDecompress` is deliberately NOT delegated — the rust surface
|
|
10
|
+
// keeps the native path (with its 64 MiB decompression-bomb cap) even under
|
|
11
|
+
// Bun, because `Bun.gunzipSync` has no output-size bound.
|
|
12
|
+
//
|
|
13
|
+
// On Node the registry is EMPTY (`has()` returns false; methods throw if
|
|
14
|
+
// called) so every op falls through to the native transport.
|
|
15
|
+
|
|
16
|
+
import { runtimeCodec } from './codec'
|
|
17
|
+
import { detectedRuntime } from './detect'
|
|
18
|
+
import type { BuiltinsAdapter, RuntimeName } from './types'
|
|
19
|
+
|
|
20
|
+
/** The delegated op names (matches the historical `BUN_WINS` set). */
|
|
21
|
+
export const BUILTIN_OPS = [
|
|
22
|
+
'gzipCompress',
|
|
23
|
+
'crc32',
|
|
24
|
+
'randomToken',
|
|
25
|
+
'hmacSha256',
|
|
26
|
+
'xxh3',
|
|
27
|
+
'urlEncode',
|
|
28
|
+
'urlDecode',
|
|
29
|
+
'base64Encode',
|
|
30
|
+
'base64UrlEncode',
|
|
31
|
+
'hexEncode',
|
|
32
|
+
'httpDate',
|
|
33
|
+
] as const
|
|
34
|
+
|
|
35
|
+
const MAX_TOKEN_BYTES = 16 * 1024 * 1024 // 16 MiB guard (matches the native path)
|
|
36
|
+
|
|
37
|
+
function createBunBuiltins(): BuiltinsAdapter {
|
|
38
|
+
const ops = new Set<string>(BUILTIN_OPS)
|
|
39
|
+
return {
|
|
40
|
+
ops: BUILTIN_OPS,
|
|
41
|
+
has(op: string): boolean {
|
|
42
|
+
return ops.has(op)
|
|
43
|
+
},
|
|
44
|
+
crc32(input: Uint8Array): number {
|
|
45
|
+
return Bun.hash.crc32(input) >>> 0 // preserves the unsigned-32 contract
|
|
46
|
+
},
|
|
47
|
+
xxh3(input: Uint8Array): bigint {
|
|
48
|
+
return Bun.hash.xxHash3(input)
|
|
49
|
+
},
|
|
50
|
+
hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array | string {
|
|
51
|
+
const hasher = new Bun.CryptoHasher('sha256', key)
|
|
52
|
+
hasher.update(data)
|
|
53
|
+
return Buffer.from(hasher.digest()).toString('hex')
|
|
54
|
+
},
|
|
55
|
+
gzipCompress(data: Uint8Array, level?: number | null): Uint8Array {
|
|
56
|
+
const opts = level != null ? ({ level } as Parameters<typeof Bun.gzipSync>[1]) : undefined
|
|
57
|
+
return Bun.gzipSync(data as unknown as Uint8Array<ArrayBuffer>, opts)
|
|
58
|
+
},
|
|
59
|
+
randomToken(byteLen: number): Uint8Array | string {
|
|
60
|
+
if (byteLen > MAX_TOKEN_BYTES) {
|
|
61
|
+
throw new RangeError('randomToken: byteLen exceeds 16 MiB limit')
|
|
62
|
+
}
|
|
63
|
+
return Buffer.from(crypto.getRandomValues(new Uint8Array(byteLen))).toString('hex')
|
|
64
|
+
},
|
|
65
|
+
base64Encode(
|
|
66
|
+
input: Uint8Array,
|
|
67
|
+
urlSafe?: boolean,
|
|
68
|
+
padding?: boolean,
|
|
69
|
+
): Uint8Array | string | undefined {
|
|
70
|
+
// Only the standard padded, non-url-safe case delegates (SIMD Buffer);
|
|
71
|
+
// url-safe/unpadded falls through to the native transport.
|
|
72
|
+
if (urlSafe || padding === false) return undefined
|
|
73
|
+
return Buffer.from(input).toString('base64')
|
|
74
|
+
},
|
|
75
|
+
base64UrlEncode(input: Uint8Array): Uint8Array | string {
|
|
76
|
+
return Buffer.from(input).toString('base64url')
|
|
77
|
+
},
|
|
78
|
+
hexEncode(input: Uint8Array): Uint8Array | string {
|
|
79
|
+
return Buffer.from(input).toString('hex')
|
|
80
|
+
},
|
|
81
|
+
urlEncode(input: Uint8Array): string {
|
|
82
|
+
return encodeURIComponent(runtimeCodec.decodeUtf8(input))
|
|
83
|
+
},
|
|
84
|
+
urlDecode(input: Uint8Array): Uint8Array {
|
|
85
|
+
return runtimeCodec.encodeUtf8(decodeURIComponent(runtimeCodec.decodeUtf8(input)))
|
|
86
|
+
},
|
|
87
|
+
urlEncodeStr(input: string): string {
|
|
88
|
+
return encodeURIComponent(input)
|
|
89
|
+
},
|
|
90
|
+
urlDecodeStr(input: string): string {
|
|
91
|
+
return decodeURIComponent(input)
|
|
92
|
+
},
|
|
93
|
+
httpDate(secs?: number): string {
|
|
94
|
+
const t = secs ?? Math.floor(Date.now() / 1000)
|
|
95
|
+
return new Date(t * 1000).toUTCString()
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createNodeBuiltins(): BuiltinsAdapter {
|
|
101
|
+
const unavailable = (op: string): never => {
|
|
102
|
+
throw new Error(`builtin "${op}" is a Bun-only delegation — not available on Node`)
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
ops: [],
|
|
106
|
+
has(): boolean {
|
|
107
|
+
return false
|
|
108
|
+
},
|
|
109
|
+
crc32: () => unavailable('crc32'),
|
|
110
|
+
xxh3: () => unavailable('xxh3'),
|
|
111
|
+
hmacSha256: () => unavailable('hmacSha256'),
|
|
112
|
+
gzipCompress: () => unavailable('gzipCompress'),
|
|
113
|
+
randomToken: () => unavailable('randomToken'),
|
|
114
|
+
base64Encode: () => unavailable('base64Encode'),
|
|
115
|
+
base64UrlEncode: () => unavailable('base64UrlEncode'),
|
|
116
|
+
hexEncode: () => unavailable('hexEncode'),
|
|
117
|
+
urlEncode: () => unavailable('urlEncode'),
|
|
118
|
+
urlDecode: () => unavailable('urlDecode'),
|
|
119
|
+
urlEncodeStr: () => unavailable('urlEncodeStr'),
|
|
120
|
+
urlDecodeStr: () => unavailable('urlDecodeStr'),
|
|
121
|
+
httpDate: () => unavailable('httpDate'),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Build the builtins registry for a runtime. */
|
|
126
|
+
export function createBuiltins(name: RuntimeName): BuiltinsAdapter {
|
|
127
|
+
return name === 'bun' ? createBunBuiltins() : createNodeBuiltins()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The shared builtins registry for the detected runtime (selected once at load). */
|
|
131
|
+
export const runtimeBuiltins: BuiltinsAdapter = createBuiltins(detectedRuntime)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// src/runtime/codec.ts — runtime-native UTF-8 codec (Bun transfer, Node fallback).
|
|
2
|
+
//
|
|
3
|
+
// The UTF-8 codec branches (formerly in `src/shared/codec.ts`) are centralized
|
|
4
|
+
// here and selected ONCE at module load:
|
|
5
|
+
// - encode: Bun `Bun.ArrayBufferSink` vs `TextEncoder`.
|
|
6
|
+
// - encodeInto: zero-copy `Buffer` view `.write` vs `TextEncoder.encodeInto`.
|
|
7
|
+
// - decode: `new CString(ptr, 0, len)` from `bun:ffi` vs `TextDecoder`.
|
|
8
|
+
// - decodeFatal: replacement-mode (Bun) vs fatal `TextDecoder` (Node).
|
|
9
|
+
//
|
|
10
|
+
// `src/shared/codec.ts` re-exports these so existing callers are unchanged.
|
|
11
|
+
// This module must NOT import `src/native/ffi.ts` (it would create a cycle
|
|
12
|
+
// through the bind-time self-test) — the Bun decode lazily `require`s
|
|
13
|
+
// `bun:ffi` itself, and depends only on the detection leaf.
|
|
14
|
+
|
|
15
|
+
import { detectedRuntime } from './detect'
|
|
16
|
+
import type { RuntimeName, Utf8Codec } from './types'
|
|
17
|
+
|
|
18
|
+
// ── Lazy Node singletons ──────────────────────────────────────────
|
|
19
|
+
let nodeEncoder: TextEncoder | null = null
|
|
20
|
+
let nodeDecoder: TextDecoder | null = null
|
|
21
|
+
let nodeFatalDecoder: TextDecoder | null = null
|
|
22
|
+
|
|
23
|
+
function getNodeEncoder(): TextEncoder {
|
|
24
|
+
if (nodeEncoder === null) nodeEncoder = new TextEncoder()
|
|
25
|
+
return nodeEncoder
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getNodeDecoder(): TextDecoder {
|
|
29
|
+
if (nodeDecoder === null) nodeDecoder = new TextDecoder()
|
|
30
|
+
return nodeDecoder
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getNodeFatalDecoder(): TextDecoder {
|
|
34
|
+
if (nodeFatalDecoder === null) nodeFatalDecoder = new TextDecoder('utf-8', { fatal: true })
|
|
35
|
+
return nodeFatalDecoder
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Bun codec ─────────────────────────────────────────────────────
|
|
39
|
+
let bunSink: Bun.ArrayBufferSink | null = null
|
|
40
|
+
let bunFfiCodec: typeof import('bun:ffi') | null | undefined
|
|
41
|
+
|
|
42
|
+
function getBunFfiCodec(): typeof import('bun:ffi') | null {
|
|
43
|
+
if (bunFfiCodec === undefined) {
|
|
44
|
+
try {
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
46
|
+
bunFfiCodec = require('bun:ffi') as typeof import('bun:ffi')
|
|
47
|
+
} catch {
|
|
48
|
+
bunFfiCodec = null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return bunFfiCodec
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createBunCodec(): Utf8Codec {
|
|
55
|
+
const decodeUtf8Bun = (bytes: Uint8Array): string => {
|
|
56
|
+
if (bytes.byteLength === 0) return ''
|
|
57
|
+
const ffi = getBunFfiCodec()
|
|
58
|
+
if (ffi) {
|
|
59
|
+
return new ffi.CString(ffi.ptr(bytes), 0, bytes.byteLength).toString()
|
|
60
|
+
}
|
|
61
|
+
return getNodeDecoder().decode(bytes)
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
encodeUtf8(s: string): Uint8Array {
|
|
65
|
+
if (bunSink === null) bunSink = new Bun.ArrayBufferSink()
|
|
66
|
+
const sink = bunSink
|
|
67
|
+
// `stream` resets the buffer on flush (reusable singleton); `flush()`
|
|
68
|
+
// returns the written data as a `Uint8Array`.
|
|
69
|
+
sink.start({ asUint8Array: true, stream: true })
|
|
70
|
+
sink.write(s)
|
|
71
|
+
return sink.flush() as Uint8Array
|
|
72
|
+
},
|
|
73
|
+
encodeUtf8Into(s: string, dest: Uint8Array, offset = 0): number {
|
|
74
|
+
return bunEncodeUtf8Into(s, dest, offset)
|
|
75
|
+
},
|
|
76
|
+
decodeUtf8: decodeUtf8Bun,
|
|
77
|
+
// Bun's CString is replacement-mode; strictness is enforced by the native
|
|
78
|
+
// `castrum_utf8_valid` probe (callers check it first via the transport's
|
|
79
|
+
// ffi surface). This fallback matches the historical behavior.
|
|
80
|
+
decodeUtf8Fatal: decodeUtf8Bun,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Cached zero-copy `Buffer` views keyed by the backing `ArrayBuffer`.
|
|
85
|
+
//
|
|
86
|
+
// `Buffer.write` requires a `Buffer` view; the historical implementation
|
|
87
|
+
// created one with `Buffer.from(backing, start, len)` on EVERY call — an
|
|
88
|
+
// object allocation on the hot path (the ingress packers encode the URL/IP
|
|
89
|
+
// twice per request through this function). Caching the full-backing view per
|
|
90
|
+
// `ArrayBuffer` (they are immutable-size in this codebase — the packers
|
|
91
|
+
// replace the whole buffer on growth, giving a fresh `ArrayBuffer` key) turns
|
|
92
|
+
// the per-call `Buffer.from` into a WeakMap hit.
|
|
93
|
+
const bunEncodeViewCache = new WeakMap<ArrayBufferLike, Buffer>()
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Write `s` (UTF-8) into `dest` starting at `offset` and return bytes written.
|
|
97
|
+
*
|
|
98
|
+
* Fast path (the hot callers — the ingress input/header packers — pass `dest`
|
|
99
|
+
* as a subarray that extends to the END of its backing buffer): reuse the
|
|
100
|
+
* cached full-backing `Buffer` view and write at the absolute position — the
|
|
101
|
+
* write bound is then identical to the historical `Buffer.from(backing, start,
|
|
102
|
+
* len)` behaviour, so no bytes outside `dest` can be touched.
|
|
103
|
+
*
|
|
104
|
+
* When `dest` does NOT extend to the end of its backing (a mid-buffer slice),
|
|
105
|
+
* fall back to a bounded `subarray` so the truncation semantics match the
|
|
106
|
+
* original `Buffer.from(backing, start, len)` exactly.
|
|
107
|
+
*/
|
|
108
|
+
function bunEncodeUtf8Into(s: string, dest: Uint8Array, offset = 0): number {
|
|
109
|
+
const backing = dest.buffer
|
|
110
|
+
let view = bunEncodeViewCache.get(backing)
|
|
111
|
+
if (view === undefined || view.byteLength !== backing.byteLength) {
|
|
112
|
+
view = Buffer.from(backing)
|
|
113
|
+
bunEncodeViewCache.set(backing, view)
|
|
114
|
+
}
|
|
115
|
+
const start = dest.byteOffset + offset
|
|
116
|
+
if (dest.byteOffset + dest.byteLength !== backing.byteLength) {
|
|
117
|
+
// Non-suffix dest: bound the write to `dest` exactly (historical semantics).
|
|
118
|
+
return view.subarray(start, start + dest.byteLength - offset).write(s, 0, 'utf8')
|
|
119
|
+
}
|
|
120
|
+
return view.write(s, start, 'utf8')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Node codec ────────────────────────────────────────────────────
|
|
124
|
+
function createNodeCodec(): Utf8Codec {
|
|
125
|
+
return {
|
|
126
|
+
encodeUtf8(s: string): Uint8Array {
|
|
127
|
+
return getNodeEncoder().encode(s)
|
|
128
|
+
},
|
|
129
|
+
encodeUtf8Into(s: string, dest: Uint8Array, offset = 0): number {
|
|
130
|
+
return getNodeEncoder().encodeInto(s, dest.subarray(offset)).written
|
|
131
|
+
},
|
|
132
|
+
decodeUtf8(bytes: Uint8Array): string {
|
|
133
|
+
return getNodeDecoder().decode(bytes)
|
|
134
|
+
},
|
|
135
|
+
decodeUtf8Fatal(bytes: Uint8Array): string {
|
|
136
|
+
return getNodeFatalDecoder().decode(bytes)
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Build the codec for a runtime (used by `createRuntimeAdapter` / tests). */
|
|
142
|
+
export function createCodec(name: RuntimeName): Utf8Codec {
|
|
143
|
+
return name === 'bun' ? createBunCodec() : createNodeCodec()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The shared codec for the detected runtime (selected once at load). */
|
|
147
|
+
export const runtimeCodec: Utf8Codec = createCodec(detectedRuntime)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/runtime/detect.ts — one-time runtime detection (cached).
|
|
2
|
+
//
|
|
3
|
+
// The ONLY place `typeof Bun` is checked. The result is resolved ONCE at
|
|
4
|
+
// module load into a constant — hot paths never re-evaluate the detection,
|
|
5
|
+
// and `src/shared/runtime.ts` is a thin facade over this module so existing
|
|
6
|
+
// `isBun()` / `isNode()` call sites keep working unchanged.
|
|
7
|
+
|
|
8
|
+
import type { RuntimeName } from './types'
|
|
9
|
+
|
|
10
|
+
function detectRuntimeName(): RuntimeName {
|
|
11
|
+
// Bun defines the global `Bun` object; Node does not.
|
|
12
|
+
if (typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined') return 'bun'
|
|
13
|
+
if (typeof process !== 'undefined' && process.versions?.node != null) return 'node'
|
|
14
|
+
return 'unknown'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The host runtime, resolved once at module load. */
|
|
18
|
+
export const detectedRuntime: RuntimeName = detectRuntimeName()
|
|
19
|
+
|
|
20
|
+
/** Whether the host runtime is Bun (equivalent to `detectedRuntime === "bun"`). */
|
|
21
|
+
export function isBunRuntime(): boolean {
|
|
22
|
+
return detectedRuntime === 'bun'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Whether the host runtime is Node.js. */
|
|
26
|
+
export function isNodeRuntime(): boolean {
|
|
27
|
+
return detectedRuntime === 'node'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The Node.js major version when running under Node, otherwise `null`. */
|
|
31
|
+
export function nodeMajorVersion(): number | null {
|
|
32
|
+
if (detectedRuntime !== 'node') return null
|
|
33
|
+
const v = process.versions.node
|
|
34
|
+
const major = Number(v?.split('.')[0])
|
|
35
|
+
return Number.isFinite(major) ? major : null
|
|
36
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/runtime/index.ts — runtime adapter barrel (FULL adapter).
|
|
2
|
+
//
|
|
3
|
+
// Composes the FFI-facing runtime seam (`src/runtime/native.ts`) with the
|
|
4
|
+
// platform server / websocket adapters (`src/runtime/server.ts` +
|
|
5
|
+
// `websocket.ts`). The FFI layer imports the seam from `./native` (it never
|
|
6
|
+
// pulls the ingress graph); the public API imports the full `runtime` from
|
|
7
|
+
// here.
|
|
8
|
+
|
|
9
|
+
import { runtimeNative } from './native'
|
|
10
|
+
import { runtimeServer } from './server'
|
|
11
|
+
import type { RuntimeAdapter } from './types'
|
|
12
|
+
import { runtimeWebsocket } from './websocket'
|
|
13
|
+
|
|
14
|
+
/** The full runtime adapter for the detected host runtime (native + server + websocket). */
|
|
15
|
+
export const runtime: RuntimeAdapter = {
|
|
16
|
+
...runtimeNative,
|
|
17
|
+
server: runtimeServer,
|
|
18
|
+
websocket: runtimeWebsocket,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Runtime detection (cached) — the single place `typeof Bun` is checked.
|
|
22
|
+
export { detectedRuntime, isBunRuntime, isNodeRuntime, nodeMajorVersion } from './detect'
|
|
23
|
+
export { runtimeNative } from './native'
|
|
24
|
+
// Runtime-driven server factory (Bun.serve on Bun, node:http on Node) — the
|
|
25
|
+
// public `createIngressServer` (src/ingress/index.ts) re-exports this.
|
|
26
|
+
export { createIngressServer } from './server'
|
|
27
|
+
|
|
28
|
+
export type { RuntimeName } from './types'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/runtime/native.ts — the FFI-facing runtime seam.
|
|
2
|
+
//
|
|
3
|
+
// What the `rust-ffi` layer / selection / shared helpers consume: runtime
|
|
4
|
+
// detection, UTF-8 codec, uuid, env resolution, the Bun built-in delegation
|
|
5
|
+
// registry, and the native transport (bun:ffi vs napi). Importing this module
|
|
6
|
+
// does NOT eagerly bind the ffi transport (lazy) and never pulls the ingress
|
|
7
|
+
// graph, so `rust-ffi/*` can import it without a cycle.
|
|
8
|
+
//
|
|
9
|
+
// The FULL adapter (adding `server` + `websocket`) is composed in
|
|
10
|
+
// `src/runtime/index.ts` / `src/runtime/server.ts` for the public API.
|
|
11
|
+
|
|
12
|
+
import { resolveEnvVar } from '../shared/env'
|
|
13
|
+
import { runtimeBuiltins } from './builtins'
|
|
14
|
+
import { runtimeCodec } from './codec'
|
|
15
|
+
import { detectedRuntime } from './detect'
|
|
16
|
+
import { createTransport } from './transport'
|
|
17
|
+
import type { NativeRuntimeAdapter } from './types'
|
|
18
|
+
import { runtimeUuid } from './uuid'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The shared runtime adapter (native seam) for the detected host runtime —
|
|
22
|
+
* codec / uuid / env / builtins / transport. The FULL adapter (adding
|
|
23
|
+
* `server` + `websocket`) is composed in `src/runtime/index.ts` for the
|
|
24
|
+
* public API; the FFI layer imports THIS seam so it never pulls the ingress
|
|
25
|
+
* graph.
|
|
26
|
+
*/
|
|
27
|
+
export const runtimeNative: NativeRuntimeAdapter = {
|
|
28
|
+
name: detectedRuntime,
|
|
29
|
+
isWebStandard: detectedRuntime === 'bun',
|
|
30
|
+
codec: runtimeCodec,
|
|
31
|
+
uuid: runtimeUuid,
|
|
32
|
+
env: { resolveVar: resolveEnvVar },
|
|
33
|
+
builtins: runtimeBuiltins,
|
|
34
|
+
transport: createTransport(),
|
|
35
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/runtime/server.ts — platform HTTP server adapter (Bun.serve / node:http).
|
|
2
|
+
//
|
|
3
|
+
// The SAME pre-baked ingress route handlers (`buildRouteHandlers`) are served
|
|
4
|
+
// by `Bun.serve` on Bun (src/ingress/server.ts) and the `node:http` adapter on
|
|
5
|
+
// Node (src/ingress/server-node.ts). Historically consumers had to pick
|
|
6
|
+
// `createIngressServer` (Bun-only) vs `createIngressServerNode` explicitly;
|
|
7
|
+
// this adapter lets the public `createIngressServer` pick the backend once,
|
|
8
|
+
// at module load. Selected ONCE — the branch never runs in the hot path.
|
|
9
|
+
|
|
10
|
+
import type { BakedServer, CreateIngressServerOptions } from '../ingress/server'
|
|
11
|
+
import { createIngressServer as createIngressServerBun } from '../ingress/server'
|
|
12
|
+
import { createIngressServerNode } from '../ingress/server-node'
|
|
13
|
+
import { detectedRuntime } from './detect'
|
|
14
|
+
import type { ServerAdapter } from './types'
|
|
15
|
+
|
|
16
|
+
/** The shared server adapter for the detected host runtime. */
|
|
17
|
+
export const runtimeServer: ServerAdapter = {
|
|
18
|
+
runtime: detectedRuntime === 'bun' ? 'bun' : 'node',
|
|
19
|
+
createIngressServer,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create the runtime-appropriate ingress server: `Bun.serve` on Bun, the
|
|
24
|
+
* `node:http` adapter on Node. Selected ONCE at module load — the branch
|
|
25
|
+
* never runs in the hot path.
|
|
26
|
+
*/
|
|
27
|
+
export function createIngressServer(options: CreateIngressServerOptions): BakedServer {
|
|
28
|
+
return detectedRuntime === 'bun'
|
|
29
|
+
? createIngressServerBun(options)
|
|
30
|
+
: createIngressServerNode(options)
|
|
31
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/runtime/transport.ts — native transport adapter (bun:ffi primary, napi fallback).
|
|
2
|
+
//
|
|
3
|
+
// The SAME cdylib is reached two ways: the bun:ffi C-ABI transport
|
|
4
|
+
// (`getBunFFI()`, PRIMARY under Bun — ~10-20ns crossing) and the napi addon
|
|
5
|
+
// (`addon`, Node / forced `CASTRUM_FFI_MODE=napi` / failed ffi self-test).
|
|
6
|
+
// Historically every scalar builder repeated `const ffi = getBunFFI();
|
|
7
|
+
// if (ffi) return ffi.X(...); return addon.X(...)`. This adapter centralizes
|
|
8
|
+
// that selection: `resolve(op)` returns the bound op (ffi-first, cached,
|
|
9
|
+
// napi fallback), and the raw `ffi` / `napi` surfaces are exposed for the few
|
|
10
|
+
// ops with transport-specific JS handling (jsonParse packed path, urlDecode
|
|
11
|
+
// UTF-8 probe, packed unboxers).
|
|
12
|
+
//
|
|
13
|
+
// LAZY: the one-time ffi bind is deferred until the first `resolve` /
|
|
14
|
+
// introspection, so importing the adapter does not eagerly dlopen.
|
|
15
|
+
|
|
16
|
+
import type { BunFFI } from '../native/ffi'
|
|
17
|
+
import { getBunFFI } from '../native/ffi'
|
|
18
|
+
import { addon } from '../rust-ffi/addon'
|
|
19
|
+
import type { NativeCallable, TransportAdapter } from './types'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build a native transport adapter: resolves native ops ffi-first (bun:ffi,
|
|
23
|
+
* cached) with the napi addon as the fallback. The one-time ffi bind is
|
|
24
|
+
* deferred until the first resolve / introspection — importing the adapter
|
|
25
|
+
* does not eagerly dlopen.
|
|
26
|
+
*/
|
|
27
|
+
export function createTransport(): TransportAdapter {
|
|
28
|
+
let cachedFfi: BunFFI | null | undefined // undefined = not yet attempted
|
|
29
|
+
const getFfi = (): BunFFI | null => {
|
|
30
|
+
if (cachedFfi === undefined) cachedFfi = getBunFFI()
|
|
31
|
+
return cachedFfi
|
|
32
|
+
}
|
|
33
|
+
const cache = new Map<string, NativeCallable>()
|
|
34
|
+
|
|
35
|
+
const resolve = (op: string): NativeCallable | undefined => {
|
|
36
|
+
let f = cache.get(op)
|
|
37
|
+
if (f === undefined) {
|
|
38
|
+
const ffiSurface = getFfi() as unknown as Record<string, unknown> | null
|
|
39
|
+
const candidate =
|
|
40
|
+
ffiSurface && typeof ffiSurface[op] === 'function'
|
|
41
|
+
? (ffiSurface[op] as NativeCallable)
|
|
42
|
+
: (addon as unknown as Record<string, unknown>)[op]
|
|
43
|
+
if (typeof candidate !== 'function') return undefined
|
|
44
|
+
f = candidate as NativeCallable
|
|
45
|
+
cache.set(op, f)
|
|
46
|
+
}
|
|
47
|
+
return f
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
get name(): 'ffi' | 'napi' {
|
|
52
|
+
return getFfi() !== null ? 'ffi' : 'napi'
|
|
53
|
+
},
|
|
54
|
+
get returnType(): 'string' | 'bytes' {
|
|
55
|
+
return getFfi() !== null ? 'string' : 'bytes'
|
|
56
|
+
},
|
|
57
|
+
get ffi(): BunFFI | null {
|
|
58
|
+
return getFfi()
|
|
59
|
+
},
|
|
60
|
+
napi: addon,
|
|
61
|
+
ffiActive: () => getFfi() !== null,
|
|
62
|
+
resolve,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// src/runtime/types.ts — Runtime adapter contracts (Bun / Node).
|
|
2
|
+
//
|
|
3
|
+
// The single seam that centralizes every Bun-vs-Node and native-transport
|
|
4
|
+
// decision: detection (detect.ts), UTF-8 codec, uuid, env, Bun built-in
|
|
5
|
+
// delegation (builtins.ts), and the bun:ffi-vs-napi transport (transport.ts).
|
|
6
|
+
// Modeled on Elysia's adapter (`ElysiaAdapterOptions` / `createAdapter`) but
|
|
7
|
+
// internal-only, typed with concrete sub-objects (no string-union escape
|
|
8
|
+
// hatch), and selected ONCE at module load — hot paths never re-check
|
|
9
|
+
// `typeof Bun`.
|
|
10
|
+
//
|
|
11
|
+
// `src/runtime/native.ts` exposes the FFI-facing seam ({ name, codec, uuid,
|
|
12
|
+
// env, builtins, transport }); `src/runtime/index.ts` composes the FULL
|
|
13
|
+
// adapter (adding server + websocket in src/runtime/server.ts). Pure modules
|
|
14
|
+
// must not import this boundary — take the functions they need via DI.
|
|
15
|
+
|
|
16
|
+
import type { BakedServer, CreateIngressServerOptions } from '../ingress/server'
|
|
17
|
+
import type { WebSocketUpgradeOptions, WebSocketUpgradeResult } from '../integration/websocket'
|
|
18
|
+
import type { NativeAddon } from '../native'
|
|
19
|
+
import type { BunFFI } from '../native/ffi'
|
|
20
|
+
|
|
21
|
+
/** Host runtime identifier. */
|
|
22
|
+
export type RuntimeName = 'bun' | 'node' | 'unknown'
|
|
23
|
+
|
|
24
|
+
/** Runtime-native UTF-8 codec (Bun transfer machinery vs TextEncoder/TextDecoder). */
|
|
25
|
+
export interface Utf8Codec {
|
|
26
|
+
/** Encode a string to UTF-8 bytes (fresh buffer). */
|
|
27
|
+
encodeUtf8(s: string): Uint8Array
|
|
28
|
+
/** Encode into `dest` starting at `offset`; returns bytes written (no split chars). */
|
|
29
|
+
encodeUtf8Into(s: string, dest: Uint8Array, offset?: number): number
|
|
30
|
+
/** Decode UTF-8 bytes to a string (replacement mode on invalid). */
|
|
31
|
+
decodeUtf8(bytes: Uint8Array): string
|
|
32
|
+
/** Decode UTF-8 bytes to a string, throwing on invalid UTF-8. */
|
|
33
|
+
decodeUtf8Fatal(bytes: Uint8Array): string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** UUIDv7 generation (Bun built-in vs crypto.randomUUID on Node). */
|
|
37
|
+
export interface UuidAdapter {
|
|
38
|
+
uuidv7(): string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Env-var resolution (CASTRUM_* preferred, napi-rs `NAPI_RS_*` aliases). */
|
|
42
|
+
export interface EnvAdapter {
|
|
43
|
+
resolveVar(preferred: string, legacy: readonly string[]): string | undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A bound native callable (bun:ffi C-ABI or napi addon). */
|
|
47
|
+
export type NativeCallable = (...args: unknown[]) => unknown
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Native transport: the SAME cdylib accessed via bun:ffi (C-ABI, PRIMARY on
|
|
51
|
+
* Bun) or napi (Node / fallback). `resolve(op)` returns the bound op
|
|
52
|
+
* (ffi-first, cached, napi fallback) so hot call sites have no per-call
|
|
53
|
+
* `getBunFFI()` branch.
|
|
54
|
+
*/
|
|
55
|
+
export interface TransportAdapter {
|
|
56
|
+
/** Active transport: `"ffi"` (bun:ffi live) or `"napi"`. */
|
|
57
|
+
readonly name: 'ffi' | 'napi'
|
|
58
|
+
/** Native text-return contract: `"string"` on the ffi path, `"bytes"` on napi. */
|
|
59
|
+
readonly returnType: 'string' | 'bytes'
|
|
60
|
+
/** The raw bun:ffi surface when live, else `null` (for ops with ffi-specific JS handling). */
|
|
61
|
+
readonly ffi: BunFFI | null
|
|
62
|
+
/** The napi addon proxy (lazy — first native call triggers the dlopen). */
|
|
63
|
+
readonly napi: NativeAddon
|
|
64
|
+
/** Whether the bun:ffi transport is live (equivalent to `name === "ffi"`). */
|
|
65
|
+
ffiActive(): boolean
|
|
66
|
+
/** Bound op callable (ffi-first, napi fallback), or `undefined` for unknown ops. */
|
|
67
|
+
resolve(op: string): NativeCallable | undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Bun built-in delegation registry (the `BUN_WINS` set). Single source of
|
|
72
|
+
* truth for "under Bun this op is faster as a built-in than via the addon".
|
|
73
|
+
* On Node the registry is empty (`has()` is false) so builders fall through
|
|
74
|
+
* to the native transport.
|
|
75
|
+
*
|
|
76
|
+
* `ops` is the exported op-name list; `selection.ts` derives its BUN_WINS
|
|
77
|
+
* decision from `has(op)` so the hardcoded set + `isBun()` branch disappear.
|
|
78
|
+
*/
|
|
79
|
+
export interface BuiltinsAdapter {
|
|
80
|
+
/** Op names this runtime delegates to built-ins (empty on Node). */
|
|
81
|
+
readonly ops: readonly string[]
|
|
82
|
+
/** Whether `op` has a built-in delegation on this runtime. */
|
|
83
|
+
has(op: string): boolean
|
|
84
|
+
|
|
85
|
+
// ── The delegated ops (bytes-in scalar surface, Bun contracts) ──
|
|
86
|
+
crc32(input: Uint8Array): number
|
|
87
|
+
xxh3(input: Uint8Array): bigint
|
|
88
|
+
/** HMAC-SHA256 → lowercase-hex STRING on Bun. */
|
|
89
|
+
hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array | string
|
|
90
|
+
gzipCompress(data: Uint8Array, level?: number | null): Uint8Array
|
|
91
|
+
/** Random hex token (2n chars) with the 16 MiB guard. */
|
|
92
|
+
randomToken(byteLen: number): Uint8Array | string
|
|
93
|
+
/** Standard padded base64 only; `undefined` when url-safe/unpadded (falls to native). */
|
|
94
|
+
base64Encode(
|
|
95
|
+
input: Uint8Array,
|
|
96
|
+
urlSafe?: boolean,
|
|
97
|
+
padding?: boolean,
|
|
98
|
+
): Uint8Array | string | undefined
|
|
99
|
+
base64UrlEncode(input: Uint8Array): Uint8Array | string
|
|
100
|
+
hexEncode(input: Uint8Array): Uint8Array | string
|
|
101
|
+
/** Percent-encode bytes → percent-encoded STRING. */
|
|
102
|
+
urlEncode(input: Uint8Array): string
|
|
103
|
+
/** Percent-decode bytes → bytes. */
|
|
104
|
+
urlDecode(input: Uint8Array): Uint8Array
|
|
105
|
+
/** String-in percent-encode (`encodeURIComponent`). */
|
|
106
|
+
urlEncodeStr(input: string): string
|
|
107
|
+
/** String-in percent-decode (`decodeURIComponent`). */
|
|
108
|
+
urlDecodeStr(input: string): string
|
|
109
|
+
/** RFC 1123 IMF-fixdate STRING for `secs` (default now). */
|
|
110
|
+
httpDate(secs?: number): string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Platform HTTP server adapter. `createIngressServer` builds a `Bun.serve`
|
|
115
|
+
* server on Bun and the `node:http` adapter on Node — the SAME pre-baked
|
|
116
|
+
* route handlers (shared `buildRouteHandlers`) either way. The explicit
|
|
117
|
+
* `createIngressServerNode` stays exported for consumers who want the Node
|
|
118
|
+
* backend pinned.
|
|
119
|
+
*/
|
|
120
|
+
export interface ServerAdapter {
|
|
121
|
+
/** The backend this adapter targets. */
|
|
122
|
+
readonly runtime: 'bun' | 'node'
|
|
123
|
+
createIngressServer(options: CreateIngressServerOptions): BakedServer
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Platform WebSocket adapter. `createWebSocketUpgrade` returns the RFC 6455
|
|
128
|
+
* 101 handshake on Bun (whose `Response` can carry status 101) and throws a
|
|
129
|
+
* clear Bun-only error on Node (which must use the server-node `upgrade`
|
|
130
|
+
* option instead).
|
|
131
|
+
*/
|
|
132
|
+
export interface WebsocketAdapter {
|
|
133
|
+
createWebSocketUpgrade(
|
|
134
|
+
req: Request,
|
|
135
|
+
opts?: WebSocketUpgradeOptions,
|
|
136
|
+
): WebSocketUpgradeResult | null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The runtime adapter — the single seam centralizing every Bun-vs-Node and
|
|
141
|
+
* native-transport decision. Selected ONCE at module load (`src/runtime`).
|
|
142
|
+
* `src/runtime/native.ts` composes the FFI-facing subset
|
|
143
|
+
* ({@link NativeRuntimeAdapter}); the FULL adapter additionally carries
|
|
144
|
+
* `server` + `websocket` (composed in `src/runtime/index.ts` for the public
|
|
145
|
+
* API).
|
|
146
|
+
*/
|
|
147
|
+
export interface RuntimeAdapter {
|
|
148
|
+
/** Host runtime (cached at load — never re-checked in hot paths). */
|
|
149
|
+
readonly name: RuntimeName
|
|
150
|
+
/** Whether the runtime speaks the web-standard Request/Response surface. */
|
|
151
|
+
readonly isWebStandard: boolean
|
|
152
|
+
/** Runtime-native UTF-8 codec. */
|
|
153
|
+
readonly codec: Utf8Codec
|
|
154
|
+
/** UUIDv7 generation. */
|
|
155
|
+
readonly uuid: UuidAdapter
|
|
156
|
+
/** Env-var alias resolution. */
|
|
157
|
+
readonly env: EnvAdapter
|
|
158
|
+
/** Bun built-in delegation registry (empty on Node). */
|
|
159
|
+
readonly builtins: BuiltinsAdapter
|
|
160
|
+
/** Native transport (bun:ffi primary, napi fallback). */
|
|
161
|
+
readonly transport: TransportAdapter
|
|
162
|
+
/** Platform HTTP server (Bun.serve on Bun, node:http on Node). */
|
|
163
|
+
readonly server: ServerAdapter
|
|
164
|
+
/** Platform WebSocket 101 handshake (Bun-only). */
|
|
165
|
+
readonly websocket: WebsocketAdapter
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The FFI-facing runtime subset (no server/websocket) — what the `rust-ffi`
|
|
170
|
+
* layer / selection / shared helpers import, so the native seam never pulls
|
|
171
|
+
* the ingress graph.
|
|
172
|
+
*/
|
|
173
|
+
export type NativeRuntimeAdapter = Omit<RuntimeAdapter, 'server' | 'websocket'>
|