castrum 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -630
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1653 -1817
- package/dist/index.js +6172 -4294
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +111 -17
- package/docs/BENCHMARKS.md +117 -49
- package/docs/CASE_STUDY.md +243 -0
- package/docs/ENVIRONMENT.md +24 -7
- package/docs/FFI_BUN_GUIDE.md +335 -0
- package/docs/GETTING_STARTED.md +18 -34
- package/docs/HOW_TO_ADD_AN_OP.md +180 -0
- package/docs/INGRESS-ROUTER.md +82 -0
- package/docs/INGRESS.md +11 -7
- package/docs/NATIVE-ROUTE.md +77 -0
- package/docs/REPO_MAP.md +103 -53
- package/docs/adr/0001-two-wire-formats.md +1 -1
- package/docs/adr/0003-bun-builtins-delegation.md +6 -5
- package/docs/ai/LOCAL_DEV.md +81 -0
- package/docs/ai/TREE.md +1070 -0
- package/docs/bun-builtins-decision-matrix.md +25 -18
- package/index.ts +9 -16
- package/package.json +41 -28
- package/src/ingress/body.ts +167 -27
- package/src/ingress/decode/result-base.ts +10 -2
- package/src/ingress/handlers.ts +116 -285
- package/src/ingress/headers/security.ts +41 -0
- package/src/ingress/health.ts +1 -0
- package/src/ingress/index.ts +45 -169
- package/src/ingress/native-route.ts +182 -0
- package/src/ingress/packing/gather-raw-headers.ts +110 -22
- package/src/ingress/packing/header-packing.ts +5 -5
- package/src/ingress/packing/input-packer.ts +17 -3
- package/src/ingress/packing/route-wire.ts +197 -0
- package/src/ingress/packing/select-headers.ts +20 -7
- package/src/ingress/path-matcher.ts +115 -0
- package/src/ingress/response/baked-response.ts +302 -0
- package/src/ingress/router.ts +277 -0
- package/src/ingress/routes/common.ts +50 -1
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +3 -13
- package/src/ingress/routes/index.ts +5 -4
- package/src/ingress/routes/json-write.ts +26 -47
- package/src/ingress/routes/native.ts +130 -0
- package/src/ingress/routes/options.ts +2 -12
- package/src/ingress/routes/read.ts +3 -13
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +7 -7
- package/src/ingress/server.ts +83 -112
- package/src/ingress/sync.ts +151 -0
- package/src/ingress/types.ts +44 -1
- package/src/integration/pipeline.ts +3 -0
- package/src/integration/streaming.ts +2 -3
- package/src/integration/websocket.ts +4 -3
- package/src/loader/batch.ts +4 -0
- package/src/loader/cost.ts +1 -0
- package/src/loader/index.ts +43 -228
- package/src/loader/state.ts +84 -0
- package/src/loader/types.ts +145 -0
- package/src/native/ffi/build/codecs.ts +736 -0
- package/src/native/ffi/build/compress.ts +248 -0
- package/src/native/ffi/build/instances.ts +598 -0
- package/src/native/ffi/build/parse.ts +539 -0
- package/src/native/ffi/build/util.ts +140 -0
- package/src/native/ffi/build.ts +94 -0
- package/src/native/ffi/constants.ts +58 -0
- package/src/native/ffi/selftest.ts +22 -0
- package/src/native/ffi/types.ts +499 -0
- package/src/native/ffi.ts +307 -1357
- package/src/native/loader.ts +52 -6
- package/src/native/types/instances.ts +279 -0
- package/src/native/types.ts +41 -238
- package/src/runtime/builtins.ts +131 -0
- package/src/runtime/codec.ts +147 -0
- package/src/runtime/detect.ts +36 -0
- package/src/runtime/index.ts +28 -0
- package/src/runtime/native.ts +35 -0
- package/src/runtime/server.ts +31 -0
- package/src/runtime/transport.ts +64 -0
- package/src/runtime/types.ts +173 -0
- package/src/runtime/uuid.ts +45 -0
- package/src/runtime/websocket.ts +30 -0
- package/src/rust-ffi/addon.ts +1 -1
- package/src/rust-ffi/batch/build.ts +155 -0
- package/src/rust-ffi/batch/index.ts +8 -0
- package/src/rust-ffi/batch/types.ts +92 -0
- package/src/rust-ffi/client.ts +5 -6
- package/src/rust-ffi/context.ts +39 -10
- package/src/rust-ffi/index.ts +28 -17
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +1 -1
- package/src/rust-ffi/packed.ts +40 -2
- package/src/rust-ffi/proven.ts +16 -30
- package/src/rust-ffi/scalar/crypto.ts +188 -107
- package/src/rust-ffi/scalar/factories.ts +257 -27
- package/src/rust-ffi/scalar/hashing.ts +43 -41
- package/src/rust-ffi/scalar/http.ts +208 -77
- package/src/rust-ffi/scalar/index.ts +3 -3
- package/src/rust-ffi/scalar/interface.ts +88 -213
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +28 -12
- package/src/rust-ffi/scalar/payload.ts +159 -42
- package/src/rust-ffi/text.ts +31 -5
- package/src/selection.ts +27 -31
- package/src/shared/adaptive.ts +1 -2
- package/src/shared/buffer-pool.ts +72 -17
- package/src/shared/bytes.ts +75 -6
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +4 -4
- package/src/shared/log.ts +1 -0
- package/src/shared/packed/index.ts +10 -0
- package/src/shared/packed/parsers.ts +31 -0
- package/src/shared/packed/schema.ts +23 -0
- package/src/shared/{packed.ts → packed/wire.ts} +31 -39
- package/src/shared/proven.ts +189 -496
- package/src/shared/runtime.ts +20 -25
- package/src/shared/uuid.ts +12 -28
- package/src/data/json-rows.ts +0 -33
- package/src/rust-ffi/batch.ts +0 -431
- package/src/shared/bench-classify.ts +0 -209
|
@@ -3,161 +3,277 @@
|
|
|
3
3
|
// Mirrors rust/http/* (packed parsers, media type, etag, HTTP dates,
|
|
4
4
|
// Accept-Encoding, URL codec/join, MIME lookup) plus the `util/validation`
|
|
5
5
|
// validators and the WebSocket accept-key.
|
|
6
|
+
//
|
|
7
|
+
// Runtime dispatch is centralized in the adapter (`ctx.runtime`): Bun built-in
|
|
8
|
+
// delegations (urlEncode/urlDecode/httpDate) come from `builtins.has(op)` and
|
|
9
|
+
// the native call from `transport.resolve(op)` / `transport.ffi` (bun:ffi
|
|
10
|
+
// first, napi fallback) — no inline `isBun()` / `getBunFFI()`.
|
|
6
11
|
|
|
7
12
|
import type { EncodingPrefResult, MediaTypeResult } from '../../native'
|
|
8
|
-
import {
|
|
9
|
-
import { decoder
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
13
|
+
import type { BunFFI } from '../../native/ffi'
|
|
14
|
+
import { decoder } from '../../shared/bytes'
|
|
15
|
+
import { decodeUtf8Fatal } from '../../shared/codec'
|
|
16
|
+
import { packPairs } from '../../shared/packed'
|
|
17
|
+
import { writeInto } from '../into'
|
|
18
|
+
import { memoizeFfi, type RustClientContext, resolveNative } from '../context'
|
|
12
19
|
|
|
13
20
|
// Mirror napi `url_decode`'s UTF-8 validation (rust/http/url_codec.rs): only
|
|
14
21
|
// validate when a high byte is present (the ASCII fast path skips it — same as
|
|
15
|
-
// Rust's saw_high check).
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
function ensureValidUtf8(bytes: Uint8Array): void {
|
|
22
|
+
// Rust's saw_high check). On the Bun path the check is a native C-ABI probe
|
|
23
|
+
// (`castrum_utf8_valid`); under Node (no ffi) the codec's fatal TextDecoder is
|
|
24
|
+
// the fallback — no TextDecoder runs on the Bun path.
|
|
25
|
+
function ensureValidUtf8(bytes: Uint8Array, ffi: BunFFI | null): void {
|
|
19
26
|
for (let i = 0; i < bytes.length; i++) {
|
|
20
27
|
if ((bytes[i] ?? 0) >= 0x80) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
if (ffi) {
|
|
29
|
+
if (!ffi.utf8Valid(bytes)) {
|
|
30
|
+
throw new Error('url decode: invalid UTF-8 in decoded output')
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
try {
|
|
34
|
+
decodeUtf8Fatal(bytes)
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error('url decode: invalid UTF-8 in decoded output')
|
|
37
|
+
}
|
|
25
38
|
}
|
|
26
39
|
return
|
|
27
40
|
}
|
|
28
41
|
}
|
|
29
42
|
}
|
|
30
43
|
|
|
44
|
+
/** Read an unsigned little-endian u32 at `off` (0 past the end). */
|
|
45
|
+
function u32LE(b: Uint8Array, off: number): number {
|
|
46
|
+
return (
|
|
47
|
+
((b[off] ?? 0) |
|
|
48
|
+
((b[off + 1] ?? 0) << 8) |
|
|
49
|
+
((b[off + 2] ?? 0) << 16) |
|
|
50
|
+
((b[off + 3] ?? 0) << 24)) >>>
|
|
51
|
+
0
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Unpack the `castrum_parse_media_type` verdict into the napi-shaped result.
|
|
57
|
+
* Layout: `[u32 mediaTypeLen][mediaType][u32 charsetLen (0xFFFFFFFF = none)]
|
|
58
|
+
* [charset][u32 boundaryLen][boundary][u32 paramCount]{[u32 keyLen][key]
|
|
59
|
+
* [u32 valLen][val]}` — charset/boundary also appear in `params`, matching the
|
|
60
|
+
* napi `HashMap` (which keeps every param including charset/boundary).
|
|
61
|
+
*/
|
|
62
|
+
function unpackMediaType(packed: Uint8Array): MediaTypeResult {
|
|
63
|
+
let off = 0
|
|
64
|
+
const mtLen = u32LE(packed, off)
|
|
65
|
+
off += 4
|
|
66
|
+
const mediaType = decoder.decode(packed.subarray(off, off + mtLen))
|
|
67
|
+
off += mtLen
|
|
68
|
+
let charset: string | null = null
|
|
69
|
+
const csLen = u32LE(packed, off)
|
|
70
|
+
off += 4
|
|
71
|
+
if (csLen !== 0xffffffff) {
|
|
72
|
+
charset = decoder.decode(packed.subarray(off, off + csLen))
|
|
73
|
+
off += csLen
|
|
74
|
+
}
|
|
75
|
+
let boundary: string | null = null
|
|
76
|
+
const bLen = u32LE(packed, off)
|
|
77
|
+
off += 4
|
|
78
|
+
if (bLen !== 0xffffffff) {
|
|
79
|
+
boundary = decoder.decode(packed.subarray(off, off + bLen))
|
|
80
|
+
off += bLen
|
|
81
|
+
}
|
|
82
|
+
const count = u32LE(packed, off)
|
|
83
|
+
off += 4
|
|
84
|
+
const params: Record<string, string> = {}
|
|
85
|
+
for (let i = 0; i < count; i++) {
|
|
86
|
+
const kLen = u32LE(packed, off)
|
|
87
|
+
off += 4
|
|
88
|
+
const key = decoder.decode(packed.subarray(off, off + kLen))
|
|
89
|
+
off += kLen
|
|
90
|
+
const vLen = u32LE(packed, off)
|
|
91
|
+
off += 4
|
|
92
|
+
const val = decoder.decode(packed.subarray(off, off + vLen))
|
|
93
|
+
off += vLen
|
|
94
|
+
params[key] = val
|
|
95
|
+
}
|
|
96
|
+
return { mediaType, charset, boundary, params }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Unpack the `castrum_parse_accept_encoding` verdict (f32 q-values) into the
|
|
100
|
+
* napi-shaped array. Layout: `[u32 count]{[u32 encLen][enc][f32 q][u32 order]}`. */
|
|
101
|
+
function unpackAcceptEncoding(packed: Uint8Array): EncodingPrefResult[] {
|
|
102
|
+
const count = u32LE(packed, 0)
|
|
103
|
+
let off = 4
|
|
104
|
+
const out: EncodingPrefResult[] = []
|
|
105
|
+
const view = new DataView(packed.buffer, packed.byteOffset, packed.byteLength)
|
|
106
|
+
for (let i = 0; i < count; i++) {
|
|
107
|
+
const encLen = u32LE(packed, off)
|
|
108
|
+
off += 4
|
|
109
|
+
const encoding = decoder.decode(packed.subarray(off, off + encLen))
|
|
110
|
+
off += encLen
|
|
111
|
+
const q = view.getFloat32(off, true)
|
|
112
|
+
off += 4
|
|
113
|
+
const order = u32LE(packed, off)
|
|
114
|
+
off += 4
|
|
115
|
+
out.push({ encoding, q, order })
|
|
116
|
+
}
|
|
117
|
+
return out
|
|
118
|
+
}
|
|
119
|
+
|
|
31
120
|
/** HTTP / parsing scalar methods (`Pick<RustScalar, ...>`). */
|
|
32
121
|
export function buildHttp(ctx: RustClientContext) {
|
|
122
|
+
const { builtins, transport } = ctx.runtime
|
|
33
123
|
// Resolve the native fns through the shared first-use cache so the hot
|
|
34
|
-
// scalar path skips the lazy-addon Proxy get on every call.
|
|
124
|
+
// scalar path skips the lazy-addon Proxy get on every call. `resolveNative`
|
|
125
|
+
// is FFI-first (bun:ffi when live, napi fallback) via the adapter.
|
|
35
126
|
const n = (name: string) => resolveNative(ctx, name)
|
|
127
|
+
// Hoist the immutable per-runtime builtin-delegation decisions into a
|
|
128
|
+
// bind-time map (BUILTIN_OPS is a module constant, never mutated after
|
|
129
|
+
// load) so the hot path reads an object property instead of a `Set.has`.
|
|
130
|
+
const HAS = {
|
|
131
|
+
urlEncode: builtins.has('urlEncode'),
|
|
132
|
+
urlDecode: builtins.has('urlDecode'),
|
|
133
|
+
httpDate: builtins.has('httpDate'),
|
|
134
|
+
}
|
|
135
|
+
// Lazy-memoized ffi surface: binds on first call (no eager dlopen at
|
|
136
|
+
// import), then a single local read per call — no getter hop.
|
|
137
|
+
const ffi = memoizeFfi(transport)
|
|
36
138
|
|
|
37
139
|
return {
|
|
38
|
-
mimeFromExtension(ext: Uint8Array): Uint8Array {
|
|
140
|
+
mimeFromExtension(ext: Uint8Array): Uint8Array | string {
|
|
141
|
+
// FFI-first: the `cstring` ARG is string-only, so bytes are decoded once
|
|
142
|
+
// (cheap CString clone) before the call. napi keeps the memoized-bytes
|
|
143
|
+
// path (cachedMime).
|
|
144
|
+
const f = ffi()
|
|
145
|
+
if (f) return f.mimeFromExtension(decoder.decode(ext)) ?? 'application/octet-stream'
|
|
39
146
|
return ctx.cachedMime(ext)
|
|
40
147
|
},
|
|
41
|
-
urlEncode(input: Uint8Array): Uint8Array {
|
|
148
|
+
urlEncode(input: Uint8Array): Uint8Array | string {
|
|
42
149
|
// Under Bun, Bun's native `encodeURIComponent` beats the rust+FFI/NAPI
|
|
43
150
|
// crossing (~4x on the string path, ~1.3x on bytes) and skips the C-ABI
|
|
44
|
-
// call entirely — see docs/bun-builtins-decision-matrix.md.
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
if (isBun()) return encoder.encode(encodeURIComponent(decoder.decode(input)))
|
|
49
|
-
const ffi = getBunFFI()
|
|
50
|
-
if (ffi) return ffi.urlEncode(input)
|
|
151
|
+
// call entirely — see docs/bun-builtins-decision-matrix.md. Bun returns
|
|
152
|
+
// the percent-encoded STRING (native transfer). Parity is pinned by
|
|
153
|
+
// test/unit/features/url.test.ts.
|
|
154
|
+
if (HAS.urlEncode) return builtins.urlEncode(input)
|
|
51
155
|
return n('urlEncode')(input) as Uint8Array
|
|
52
156
|
},
|
|
53
157
|
urlDecode(input: Uint8Array): Uint8Array {
|
|
54
158
|
// Same rationale as `urlEncode`; the strict `urlDecode` already throws on
|
|
55
159
|
// invalid UTF-8 (ensureValidUtf8 below), matching decodeURIComponent's
|
|
56
160
|
// URIError (a subclass of Error), so the JS path is behavior-equivalent.
|
|
57
|
-
if (
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
const out =
|
|
161
|
+
if (HAS.urlDecode) return builtins.urlDecode(input)
|
|
162
|
+
const f = ffi()
|
|
163
|
+
if (f) {
|
|
164
|
+
const out = f.urlDecode(input)
|
|
61
165
|
// napi `url_decode` validates UTF-8 (only when a high byte is present);
|
|
62
166
|
// `url_decode_bytes`/`url_decode_into` do not. This is the strict one.
|
|
63
|
-
ensureValidUtf8(out)
|
|
167
|
+
ensureValidUtf8(out, transport.ffi)
|
|
64
168
|
return out
|
|
65
169
|
}
|
|
66
170
|
return n('urlDecode')(input) as Uint8Array
|
|
67
171
|
},
|
|
68
172
|
urlDecodeBytes(input: Uint8Array): Uint8Array {
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
173
|
+
const f = ffi()
|
|
174
|
+
if (f) return f.urlDecode(input) // byte semantics match url_decode_bytes
|
|
71
175
|
return n('urlDecodeBytes')(input) as Uint8Array
|
|
72
176
|
},
|
|
73
177
|
urlEncodeInto(input: Uint8Array, output: Uint8Array): number {
|
|
74
|
-
const ffi = getBunFFI()
|
|
75
|
-
if (ffi) return ffi.urlEncodeInto(input, output)
|
|
76
178
|
return n('urlEncodeInto')(input, output) as number
|
|
77
179
|
},
|
|
78
180
|
urlDecodeInto(input: Uint8Array, output: Uint8Array): number {
|
|
79
|
-
|
|
80
|
-
if (ffi) return ffi.urlDecodeInto(input, output) // no UTF-8 check, matches napi
|
|
181
|
+
// no UTF-8 check — matches napi url_decode_into on both transports
|
|
81
182
|
return n('urlDecodeInto')(input, output) as number
|
|
82
183
|
},
|
|
83
184
|
validateEmail(input: Uint8Array): boolean {
|
|
84
|
-
|
|
85
|
-
|
|
185
|
+
// FFI-first: the `cstring` ARG is string-only, so bytes are decoded once
|
|
186
|
+
// (cheap CString clone) before the call — still faster than the old
|
|
187
|
+
// `(ptr,len)` view path (see bench:margin cstring scenario). napi
|
|
188
|
+
// fallback keeps the byte path.
|
|
189
|
+
const f = ffi()
|
|
190
|
+
if (f) return f.validateEmail(decoder.decode(input))
|
|
86
191
|
return n('validateEmail')(input) as boolean
|
|
87
192
|
},
|
|
88
193
|
validateUuid(input: Uint8Array): boolean {
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
194
|
+
const f = ffi()
|
|
195
|
+
if (f) return f.validateUuid(decoder.decode(input))
|
|
91
196
|
return n('validateUuid')(input) as boolean
|
|
92
197
|
},
|
|
93
198
|
validateIpv4(input: Uint8Array): boolean {
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
199
|
+
const f = ffi()
|
|
200
|
+
if (f) return f.validateIpv4(decoder.decode(input))
|
|
96
201
|
return n('validateIpv4')(input) as boolean
|
|
97
202
|
},
|
|
98
203
|
validateIpv6(input: Uint8Array): boolean {
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
204
|
+
const f = ffi()
|
|
205
|
+
if (f) return f.validateIpv6(decoder.decode(input))
|
|
101
206
|
return n('validateIpv6')(input) as boolean
|
|
102
207
|
},
|
|
103
|
-
wsAcceptKey(key: Uint8Array): Uint8Array {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
208
|
+
wsAcceptKey(key: Uint8Array): Uint8Array | string {
|
|
209
|
+
// FFI-first: the `cstring` ARG is string-only, so bytes are decoded once
|
|
210
|
+
// (cheap CString clone) before the call. napi fallback keeps the bytes.
|
|
211
|
+
const f = ffi()
|
|
212
|
+
if (f) return f.wsAcceptKey(decoder.decode(key))
|
|
213
|
+
return n('wsAcceptKey')(key) as Uint8Array | string
|
|
214
|
+
},
|
|
215
|
+
wsAcceptKeyInto(key: Uint8Array, output: Uint8Array): number {
|
|
216
|
+
const f = ffi()
|
|
217
|
+
if (f) return f.wsAcceptKeyInto(key, output)
|
|
218
|
+
return writeInto('ws accept key', output, n('wsAcceptKey')(key) as Uint8Array)
|
|
107
219
|
},
|
|
108
220
|
httpParseRequestPacked(input: Uint8Array): Uint8Array {
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
221
|
+
const f = ffi()
|
|
222
|
+
if (f) {
|
|
111
223
|
const out = new Uint8Array(input.length * 9 + 16)
|
|
112
|
-
const w =
|
|
224
|
+
const w = f.httpParseRequestPackedInto(input, out)
|
|
113
225
|
return out.subarray(0, w)
|
|
114
226
|
}
|
|
115
227
|
return n('httpParseRequestPacked')(input) as Uint8Array
|
|
116
228
|
},
|
|
117
229
|
httpParseRequestPackedInto(input: Uint8Array, output: Uint8Array): number {
|
|
118
|
-
const ffi = getBunFFI()
|
|
119
|
-
if (ffi) return ffi.httpParseRequestPackedInto(input, output)
|
|
120
230
|
return n('httpParseRequestPackedInto')(input, output) as number
|
|
121
231
|
},
|
|
122
232
|
queryParsePacked(input: Uint8Array): Uint8Array {
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
233
|
+
const f = ffi()
|
|
234
|
+
if (f) {
|
|
125
235
|
const out = new Uint8Array(input.length * 9 + 16)
|
|
126
|
-
const w =
|
|
236
|
+
const w = f.queryParsePackedInto(input, out)
|
|
127
237
|
return out.subarray(0, w)
|
|
128
238
|
}
|
|
129
239
|
return n('queryParsePacked')(input) as Uint8Array
|
|
130
240
|
},
|
|
131
241
|
queryParsePackedInto(input: Uint8Array, output: Uint8Array): number {
|
|
132
|
-
const ffi = getBunFFI()
|
|
133
|
-
if (ffi) return ffi.queryParsePackedInto(input, output)
|
|
134
242
|
return n('queryParsePackedInto')(input, output) as number
|
|
135
243
|
},
|
|
136
244
|
cookieParsePacked(input: Uint8Array): Uint8Array {
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
245
|
+
const f = ffi()
|
|
246
|
+
if (f) {
|
|
139
247
|
const out = new Uint8Array(input.length * 9 + 16)
|
|
140
|
-
const w =
|
|
248
|
+
const w = f.cookieParsePackedInto(input, out)
|
|
141
249
|
return out.subarray(0, w)
|
|
142
250
|
}
|
|
143
251
|
return n('cookieParsePacked')(input) as Uint8Array
|
|
144
252
|
},
|
|
145
253
|
cookieParsePackedInto(input: Uint8Array, output: Uint8Array): number {
|
|
146
|
-
const ffi = getBunFFI()
|
|
147
|
-
if (ffi) return ffi.cookieParsePackedInto(input, output)
|
|
148
254
|
return n('cookieParsePackedInto')(input, output) as number
|
|
149
255
|
},
|
|
150
256
|
formParsePacked(input: Uint8Array): Uint8Array {
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
257
|
+
const f = ffi()
|
|
258
|
+
if (f) {
|
|
153
259
|
// x-www-form-urlencoded uses the SAME packed-pairs core as query parse.
|
|
154
260
|
const out = new Uint8Array(input.length * 9 + 16)
|
|
155
|
-
const w =
|
|
261
|
+
const w = f.formParsePackedInto(input, out)
|
|
156
262
|
return out.subarray(0, w)
|
|
157
263
|
}
|
|
158
264
|
return n('formParsePacked')(input) as Uint8Array
|
|
159
265
|
},
|
|
266
|
+
formParsePackedInto(input: Uint8Array, output: Uint8Array): number {
|
|
267
|
+
// Pooled sibling of formParsePacked — caller-owned output buffer (the
|
|
268
|
+
// napi addon has no packed `Into` for form, so copy via the allocating
|
|
269
|
+
// call on that path; same wire output, so the copy is exact).
|
|
270
|
+
const f = ffi()
|
|
271
|
+
if (f) return f.formParsePackedInto(input, output)
|
|
272
|
+
return writeInto('form parse', output, n('formParsePacked')(input) as Uint8Array)
|
|
273
|
+
},
|
|
160
274
|
parseMediaType(input: Uint8Array): MediaTypeResult {
|
|
275
|
+
const f = ffi()
|
|
276
|
+
if (f) return unpackMediaType(f.parseMediaType(input))
|
|
161
277
|
const r = n('parseMediaType')(input) as MediaTypeResult
|
|
162
278
|
// napi serializes Option fields as undefined; normalize to null to match
|
|
163
279
|
// the declared `string | null` public type.
|
|
@@ -168,38 +284,53 @@ export function buildHttp(ctx: RustClientContext) {
|
|
|
168
284
|
params: r.params,
|
|
169
285
|
}
|
|
170
286
|
},
|
|
171
|
-
etag(data: Uint8Array, weak?: boolean): Uint8Array {
|
|
172
|
-
|
|
173
|
-
if (ffi) return ffi.etag(data, weak)
|
|
174
|
-
return n('etag')(data, weak ?? undefined) as Uint8Array
|
|
287
|
+
etag(data: Uint8Array, weak?: boolean): Uint8Array | string {
|
|
288
|
+
return n('etag')(data, weak ?? undefined) as Uint8Array | string
|
|
175
289
|
},
|
|
176
290
|
etagInto(data: Uint8Array, output: Uint8Array, weak?: boolean): number {
|
|
177
|
-
const ffi = getBunFFI()
|
|
178
|
-
if (ffi) return ffi.etagInto(data, output, weak)
|
|
179
291
|
return n('etagInto')(data, output, weak ?? undefined) as number
|
|
180
292
|
},
|
|
181
|
-
httpDate(secs?: number): Uint8Array {
|
|
293
|
+
httpDate(secs?: number): Uint8Array | string {
|
|
182
294
|
// Bun's Date.toUTCString (RFC 1123 IMF-fixdate) beats the rust+FFI
|
|
183
|
-
// crossing (~3.7x, measured);
|
|
184
|
-
if (
|
|
185
|
-
const t = secs ?? Math.floor(Date.now() / 1000)
|
|
186
|
-
return encoder.encode(new Date(t * 1000).toUTCString())
|
|
187
|
-
}
|
|
295
|
+
// crossing (~3.7x, measured); Bun returns the STRING (native transfer).
|
|
296
|
+
if (HAS.httpDate) return builtins.httpDate(secs)
|
|
188
297
|
return n('httpDate')(secs ?? undefined) as Uint8Array
|
|
189
298
|
},
|
|
190
299
|
httpDateInto(secs: number | undefined, output: Uint8Array): number {
|
|
300
|
+
// FFI-first: `castrum_http_date_into` writes directly into the caller
|
|
301
|
+
// buffer (no napi crossing). `undefined` → 0 (matches the napi
|
|
302
|
+
// `Option<f64>` default). napi fallback keeps the old path.
|
|
303
|
+
const f = ffi()
|
|
304
|
+
if (f) return f.httpDateInto(secs ?? 0, output)
|
|
191
305
|
return n('httpDateInto')(secs ?? undefined, output) as number
|
|
192
306
|
},
|
|
193
307
|
parseHttpDate(input: Uint8Array): bigint | null {
|
|
194
308
|
return n('parseHttpDate')(input) as bigint | null
|
|
195
309
|
},
|
|
196
310
|
parseAcceptEncoding(input: Uint8Array): EncodingPrefResult[] {
|
|
311
|
+
const f = ffi()
|
|
312
|
+
if (f) return unpackAcceptEncoding(f.parseAcceptEncoding(input))
|
|
197
313
|
return n('parseAcceptEncoding')(input) as EncodingPrefResult[]
|
|
198
314
|
},
|
|
199
|
-
urlResolve(base: Uint8Array, reference: Uint8Array): Uint8Array {
|
|
315
|
+
urlResolve(base: Uint8Array, reference: Uint8Array): Uint8Array | string {
|
|
316
|
+
// FFI-first: cstring resolution (native transfer — zero encode).
|
|
317
|
+
const f = ffi()
|
|
318
|
+
if (f) {
|
|
319
|
+
const s = f.urlResolve(base, reference)
|
|
320
|
+
if (s === null) throw new Error('url resolve: invalid UTF-8 in input')
|
|
321
|
+
return s
|
|
322
|
+
}
|
|
200
323
|
return n('urlResolve')(base, reference) as Uint8Array
|
|
201
324
|
},
|
|
202
|
-
urlEncodeQuery(params: Record<string, string>): Uint8Array {
|
|
325
|
+
urlEncodeQuery(params: Record<string, string>): Uint8Array | string {
|
|
326
|
+
// FFI-first: pack the params into the shared pairs layout, then the C
|
|
327
|
+
// side sorts keys (BTreeMap) and percent-encodes → cstring.
|
|
328
|
+
const f = ffi()
|
|
329
|
+
if (f) {
|
|
330
|
+
const s = f.urlEncodeQuery(packPairs(Object.entries(params)))
|
|
331
|
+
if (s === null) throw new Error('url encode query: invalid input')
|
|
332
|
+
return s
|
|
333
|
+
}
|
|
203
334
|
return n('urlEncodeQuery')(params) as Uint8Array
|
|
204
335
|
},
|
|
205
336
|
}
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
// `rust/` (hashing, json, http, crypto, payload, factories).
|
|
10
10
|
|
|
11
11
|
import type { RustClientContext } from '../context'
|
|
12
|
+
import { buildCrypto } from './crypto'
|
|
13
|
+
import { buildFactories } from './factories'
|
|
12
14
|
import { buildHashing } from './hashing'
|
|
13
|
-
import { buildJson } from './json'
|
|
14
15
|
import { buildHttp } from './http'
|
|
15
|
-
import {
|
|
16
|
+
import { buildJson } from './json'
|
|
16
17
|
import { buildPayload } from './payload'
|
|
17
|
-
import { buildFactories } from './factories'
|
|
18
18
|
|
|
19
19
|
export type { RustScalar } from './interface'
|
|
20
20
|
|