castrum 0.9.3 → 0.9.6
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/Cargo.lock +2242 -0
- package/Cargo.toml +155 -0
- package/README.md +14 -6
- package/build.rs +5 -0
- package/castrum.darwin-arm64.node +0 -0
- package/castrum.darwin-x64.node +0 -0
- package/castrum.linux-arm64-gnu.node +0 -0
- package/castrum.linux-arm64-musl.node +0 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/castrum.linux-x64-musl.node +0 -0
- package/castrum.linux-x64-v3-gnu.node +0 -0
- package/castrum.win32-x64-msvc.node +0 -0
- package/dist/index.d.ts +1508 -1291
- package/dist/index.js +3391 -2069
- package/docs/API.md +2 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +5 -3
- package/docs/ENVIRONMENT.md +25 -2
- package/docs/FFI_BUN_GUIDE.md +242 -14
- package/docs/HOW_TO_ADD_AN_OP.md +10 -0
- package/docs/REPO_MAP.md +17 -6
- package/docs/RND-CONCURRENCY.md +772 -0
- package/docs/bun-builtins-decision-matrix.md +4 -1
- package/index.ts +24 -25
- package/package.json +173 -156
- package/rust/crypto/aead.rs +378 -0
- package/rust/crypto/argon2.rs +265 -0
- package/rust/crypto/base64.rs +420 -0
- package/rust/crypto/bcrypt.rs +89 -0
- package/rust/crypto/cookie_sign.rs +169 -0
- package/rust/crypto/csrf.rs +106 -0
- package/rust/crypto/ed25519.rs +215 -0
- package/rust/crypto/hashing.rs +140 -0
- package/rust/crypto/hmac_sha256.rs +88 -0
- package/rust/crypto/jwt/api.rs +286 -0
- package/rust/crypto/jwt/mod.rs +22 -0
- package/rust/crypto/jwt/tests.rs +312 -0
- package/rust/crypto/jwt/token.rs +360 -0
- package/rust/crypto/mod.rs +27 -0
- package/rust/crypto/pbkdf2.rs +105 -0
- package/rust/crypto/random_token.rs +85 -0
- package/rust/crypto/session.rs +222 -0
- package/rust/ffi/crypto.rs +708 -0
- package/rust/ffi/hashing.rs +285 -0
- package/rust/ffi/http.rs +752 -0
- package/rust/ffi/ingress.rs +303 -0
- package/rust/ffi/json.rs +517 -0
- package/rust/ffi/jwt.rs +419 -0
- package/rust/ffi/metrics.rs +373 -0
- package/rust/ffi/mod.rs +69 -0
- package/rust/ffi/payload.rs +275 -0
- package/rust/ffi/probe.rs +60 -0
- package/rust/ffi/rate_limit.rs +77 -0
- package/rust/ffi/route.rs +85 -0
- package/rust/ffi/task.rs +172 -0
- package/rust/ffi/tests.rs +2333 -0
- package/rust/ffi/util.rs +152 -0
- package/rust/ffi/validators.rs +123 -0
- package/rust/http/accept.rs +606 -0
- package/rust/http/cookie_parser.rs +140 -0
- package/rust/http/etag.rs +289 -0
- package/rust/http/form.rs +136 -0
- package/rust/http/headers.rs +267 -0
- package/rust/http/http_date.rs +286 -0
- package/rust/http/http_parser.rs +252 -0
- package/rust/http/media_type.rs +355 -0
- package/rust/http/method.rs +200 -0
- package/rust/http/mime_lookup.rs +130 -0
- package/rust/http/mod.rs +32 -0
- package/rust/http/multipart.rs +685 -0
- package/rust/http/query_parser.rs +211 -0
- package/rust/http/url_codec.rs +296 -0
- package/rust/http/url_join.rs +324 -0
- package/rust/ingress/api.rs +355 -0
- package/rust/ingress/cors.rs +403 -0
- package/rust/ingress/ingress_constants.rs +209 -0
- package/rust/ingress/ip_trust.rs +312 -0
- package/rust/ingress/mod.rs +48 -0
- package/rust/ingress/native_route.rs +770 -0
- package/rust/ingress/options.rs +176 -0
- package/rust/ingress/output.rs +406 -0
- package/rust/ingress/packed.rs +191 -0
- package/rust/ingress/pipeline.rs +534 -0
- package/rust/ingress/proxy.rs +138 -0
- package/rust/ingress/rate_limit.rs +538 -0
- package/rust/ingress/terminal.rs +114 -0
- package/rust/ingress/tests.rs +567 -0
- package/rust/ingress/time.rs +70 -0
- package/rust/json/fast_schema/capture.rs +336 -0
- package/rust/json/fast_schema/compile.rs +556 -0
- package/rust/json/fast_schema/cursor.rs +368 -0
- package/rust/json/fast_schema/email.rs +282 -0
- package/rust/json/fast_schema/errors.rs +247 -0
- package/rust/json/fast_schema/mod.rs +57 -0
- package/rust/json/fast_schema/tests.rs +1143 -0
- package/rust/json/fast_schema/types.rs +140 -0
- package/rust/json/fast_schema/validate.rs +931 -0
- package/rust/json/json_ops.rs +121 -0
- package/rust/json/json_schema.rs +837 -0
- package/rust/json/json_ser.rs +883 -0
- package/rust/json/mod.rs +19 -0
- package/rust/json/napi_marshal.rs +318 -0
- package/rust/json/patch/api.rs +60 -0
- package/rust/json/patch/engine.rs +153 -0
- package/rust/json/patch/mod.rs +47 -0
- package/rust/json/patch/ops.rs +270 -0
- package/rust/json/patch/pointer.rs +49 -0
- package/rust/json/patch/tests.rs +499 -0
- package/rust/lib.rs +73 -0
- package/rust/metrics/api.rs +148 -0
- package/rust/metrics/mod.rs +21 -0
- package/rust/metrics/registry.rs +961 -0
- package/rust/panic_safety.rs +40 -0
- package/rust/payload/compress.rs +512 -0
- package/rust/payload/mod.rs +13 -0
- package/rust/payload/sse.rs +214 -0
- package/rust/payload/template.rs +253 -0
- package/rust/payload/websocket.rs +69 -0
- package/rust/payload/ws_frames.rs +354 -0
- package/rust/proptest_suite.rs +139 -0
- package/rust/selection.rs +95 -0
- package/rust/src/bin/native-bench.rs +495 -0
- package/rust/task/completion.rs +119 -0
- package/rust/task/mod.rs +65 -0
- package/rust/task/ops.rs +503 -0
- package/rust/task/runtime.rs +220 -0
- package/rust/task/tests.rs +231 -0
- package/rust/test_support.rs +71 -0
- package/rust/util/batch/api.rs +266 -0
- package/rust/util/batch/core.rs +473 -0
- package/rust/util/batch/mod.rs +23 -0
- package/rust/util/batch/tests.rs +445 -0
- package/rust/util/batch_core.rs +184 -0
- package/rust/util/bytes.rs +594 -0
- package/rust/util/mod.rs +34 -0
- package/rust/util/packed.rs +473 -0
- package/rust/util/text.rs +130 -0
- package/rust/util/threadpool.rs +128 -0
- package/rust/util/validation.rs +419 -0
- package/rust-toolchain.toml +17 -0
- package/scripts/postinstall.mjs +199 -0
- package/src/ingress/constants.ts +1 -1
- package/src/ingress/context.ts +2 -2
- package/src/ingress/decode/baked-result.ts +13 -13
- package/src/ingress/decode/fast-result.ts +17 -17
- package/src/ingress/errors.ts +7 -7
- package/src/ingress/headers/baked-templates.ts +1 -1
- package/src/ingress/headers/fast-templates.ts +3 -3
- package/src/ingress/health.ts +1 -2
- package/src/ingress/index.ts +1 -1
- package/src/ingress/native-route.ts +9 -5
- package/src/ingress/options.ts +1 -1
- package/src/ingress/packing/gather-raw-headers.ts +1 -1
- package/src/ingress/packing/route-wire.ts +1 -5
- package/src/ingress/response/error-bodies.ts +5 -5
- package/src/ingress/response/terminal.ts +1 -1
- package/src/ingress/routes/echo.ts +4 -4
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +2 -2
- package/src/ingress/routes/native.ts +2 -1
- package/src/ingress/routes/options.ts +1 -1
- package/src/ingress/routes/read.ts +2 -2
- package/src/ingress/server.ts +10 -6
- package/src/ingress/status.ts +7 -7
- package/src/integration/batch.ts +1 -1
- package/src/integration/index.ts +9 -9
- package/src/integration/pipeline.ts +6 -6
- package/src/integration/websocket.ts +2 -2
- package/src/loader/index.ts +1 -5
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs.ts +130 -12
- package/src/native/ffi/build/compress.ts +22 -10
- package/src/native/ffi/build/instances.ts +46 -4
- package/src/native/ffi/build/metrics.ts +247 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/task.ts +79 -0
- package/src/native/ffi/build/util.ts +32 -1
- package/src/native/ffi/build.ts +4 -0
- package/src/native/ffi/selftest.ts +12 -3
- package/src/native/ffi/types.ts +151 -9
- package/src/native/ffi.ts +160 -15
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +55 -5
- package/src/native/types/instances.ts +34 -0
- package/src/native/types.ts +11 -0
- package/src/runtime/codec.ts +53 -9
- package/src/runtime/types.ts +7 -0
- package/src/rust-ffi/batch/build.ts +16 -9
- package/src/rust-ffi/index.ts +7 -6
- package/src/rust-ffi/options.ts +1 -3
- package/src/rust-ffi/proven.ts +3 -3
- package/src/rust-ffi/scalar/crypto.ts +1 -1
- package/src/rust-ffi/scalar/factories.ts +73 -0
- package/src/rust-ffi/scalar/hashing.ts +1 -1
- package/src/rust-ffi/scalar/http.ts +71 -27
- package/src/rust-ffi/scalar/index.ts +2 -0
- package/src/rust-ffi/scalar/interface.ts +20 -0
- package/src/rust-ffi/scalar/payload.ts +11 -3
- package/src/rust-ffi/scalar/validation.ts +121 -0
- package/src/rust-ffi/text.ts +25 -3
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +24 -4
- package/src/shared/codec.ts +18 -3
- package/src/task/index.ts +24 -0
- package/src/task/op.ts +171 -0
- package/src/task/runtime.ts +568 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// src/native/ffi/build/metrics.ts — metrics-registry BunFFI methods.
|
|
2
|
+
//
|
|
3
|
+
// Wraps the `castrum_metrics_*` caller-owned-handle surface (route-stack
|
|
4
|
+
// ownership model): create returns a native `Box<MetricsRegistry>` handle,
|
|
5
|
+
// declare fns cross name/label-keys as cstring ARGS and return the u32 series
|
|
6
|
+
// id, and record/set/render take packed byte slices. Receives the raw dlopen'd
|
|
7
|
+
// symbols from `build()`.
|
|
8
|
+
|
|
9
|
+
import { decodeUtf8, encodeUtf8 } from '../../../shared/codec'
|
|
10
|
+
import type { BunFFI, Raw0, Raw3, Raw4, Raw5 } from '../types'
|
|
11
|
+
import type { BuildCtx } from './util'
|
|
12
|
+
import { hasNul } from './util'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Declare-time NUL guard: a metric NAME or label-key set containing `U+0000`
|
|
16
|
+
* would be truncated by the `cstring` arg, silently colliding two series. This
|
|
17
|
+
* runs once at declaration, so failing fast costs nothing on the hot path.
|
|
18
|
+
*/
|
|
19
|
+
function throwIfNul(value: string, what: string): void {
|
|
20
|
+
if (hasNul(value)) {
|
|
21
|
+
throw new Error(`metrics: ${what} must not contain a NUL character (U+0000)`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sentinel returned by the declare fns (`u32::MAX`) on invalid input — fits
|
|
27
|
+
* f64 exactly and can never collide with a real dense family id.
|
|
28
|
+
*/
|
|
29
|
+
export const METRICS_DECLARE_ERR = 0xffffffff
|
|
30
|
+
|
|
31
|
+
/** Empty label-values view (zero-label families). */
|
|
32
|
+
const EMPTY = new Uint8Array(0)
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build the metrics-registry methods of the BunFFI surface.
|
|
36
|
+
*/
|
|
37
|
+
export function buildMetrics(
|
|
38
|
+
sym: Record<string, (...a: unknown[]) => unknown>,
|
|
39
|
+
ctx: BuildCtx,
|
|
40
|
+
): Partial<BunFFI> {
|
|
41
|
+
const { lenOrView } = ctx
|
|
42
|
+
const metricsCreateRaw = sym.castrum_metrics_create as Raw0
|
|
43
|
+
const metricsCounterRaw = sym.castrum_metrics_counter as Raw3
|
|
44
|
+
const metricsGaugeRaw = sym.castrum_metrics_gauge as Raw3
|
|
45
|
+
const metricsHistogramRaw = sym.castrum_metrics_histogram as Raw4
|
|
46
|
+
const metricsRecordRaw = sym.castrum_metrics_record as Raw5
|
|
47
|
+
const metricsGaugeSetRaw = sym.castrum_metrics_gauge_set as Raw5
|
|
48
|
+
// Zero-encode siblings: joined values cross as a cstring ARG.
|
|
49
|
+
const metricsRecordStrRaw = sym.castrum_metrics_record_str as (...a: unknown[]) => number | bigint
|
|
50
|
+
const metricsGaugeSetStrRaw = sym.castrum_metrics_gauge_set_str as (
|
|
51
|
+
...a: unknown[]
|
|
52
|
+
) => number | bigint
|
|
53
|
+
const metricsRenderRaw = sym.castrum_metrics_render as Raw3
|
|
54
|
+
const metricsSnapshotRaw = sym.castrum_metrics_snapshot as Raw3
|
|
55
|
+
const metricsRecordBatchRaw = sym.castrum_metrics_record_batch as Raw3
|
|
56
|
+
const metricsDestroyRaw = sym.castrum_metrics_destroy as (h: unknown) => void
|
|
57
|
+
|
|
58
|
+
/** Declare helper: maps the C sentinel to a throw (fail fast at declare). */
|
|
59
|
+
const declareOrThrow = (id: number | bigint, what: string): number => {
|
|
60
|
+
const n = Number(id)
|
|
61
|
+
if (n === METRICS_DECLARE_ERR) {
|
|
62
|
+
throw new Error(`metrics: failed to declare ${what} (invalid name/labels/buckets)`)
|
|
63
|
+
}
|
|
64
|
+
return n
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
metricsCreate() {
|
|
69
|
+
const h = Number(metricsCreateRaw())
|
|
70
|
+
if (h === 0) throw new Error('metrics: registry allocation failed')
|
|
71
|
+
return h
|
|
72
|
+
},
|
|
73
|
+
metricsCounter(handle, name, labelKeys) {
|
|
74
|
+
throwIfNul(name, 'counter name')
|
|
75
|
+
throwIfNul(labelKeys, 'counter label keys')
|
|
76
|
+
return declareOrThrow(metricsCounterRaw(handle, name, labelKeys), `counter "${name}"`)
|
|
77
|
+
},
|
|
78
|
+
metricsGauge(handle, name, labelKeys) {
|
|
79
|
+
throwIfNul(name, 'gauge name')
|
|
80
|
+
throwIfNul(labelKeys, 'gauge label keys')
|
|
81
|
+
return declareOrThrow(metricsGaugeRaw(handle, name, labelKeys), `gauge "${name}"`)
|
|
82
|
+
},
|
|
83
|
+
metricsHistogram(handle, name, labelKeys, bucketsCsv) {
|
|
84
|
+
throwIfNul(name, 'histogram name')
|
|
85
|
+
throwIfNul(labelKeys, 'histogram label keys')
|
|
86
|
+
return declareOrThrow(
|
|
87
|
+
metricsHistogramRaw(handle, name, labelKeys, bucketsCsv),
|
|
88
|
+
`histogram "${name}"`,
|
|
89
|
+
)
|
|
90
|
+
},
|
|
91
|
+
metricsRecord(handle, series, values, amount) {
|
|
92
|
+
// Packed `\x1f`-separated values cross as `(ptr,len)`; u8 verdict.
|
|
93
|
+
return Number(metricsRecordRaw(handle, series, values, lenOrView(values), amount)) === 1
|
|
94
|
+
},
|
|
95
|
+
metricsGaugeSet(handle, series, values, value) {
|
|
96
|
+
return Number(metricsGaugeSetRaw(handle, series, values, lenOrView(values), value)) === 1
|
|
97
|
+
},
|
|
98
|
+
metricsRecordStr(handle, series, values, amount) {
|
|
99
|
+
// `values` is the JOINED `\x1f` string — engine-transcoded (zero encode).
|
|
100
|
+
// Label values come from user data, so a NUL would truncate the join and
|
|
101
|
+
// silently record the WRONG series (the native side rejects a NUL value,
|
|
102
|
+
// but only if it ever sees it). Fail loudly and say why.
|
|
103
|
+
throwIfNul(values, 'label values')
|
|
104
|
+
return Number(metricsRecordStrRaw(handle, series, values, amount)) === 1
|
|
105
|
+
},
|
|
106
|
+
metricsGaugeSetStr(handle, series, values, value) {
|
|
107
|
+
throwIfNul(values, 'label values')
|
|
108
|
+
return Number(metricsGaugeSetStrRaw(handle, series, values, value)) === 1
|
|
109
|
+
},
|
|
110
|
+
metricsRender(handle, output) {
|
|
111
|
+
// Needed-size convention (routeRun parity): `0` = real error → throw;
|
|
112
|
+
// `> output.length` = the EXACT required size (caller grows once and
|
|
113
|
+
// retries — never a doubling re-run loop).
|
|
114
|
+
const w = Number(metricsRenderRaw(handle, output, lenOrView(output)))
|
|
115
|
+
if (w === 0) throw new Error('metrics render: registry handle invalid or render failed')
|
|
116
|
+
return w
|
|
117
|
+
},
|
|
118
|
+
metricsSnapshot(handle, output) {
|
|
119
|
+
// Needed-size convention (routeRun parity): 0 throws; > length = exact.
|
|
120
|
+
const w = Number(metricsSnapshotRaw(handle, output, lenOrView(output)))
|
|
121
|
+
if (w === 0) throw new Error('metrics snapshot: registry handle invalid')
|
|
122
|
+
return w
|
|
123
|
+
},
|
|
124
|
+
metricsRecordBatch(handle, packed) {
|
|
125
|
+
return Number(metricsRecordBatchRaw(handle, packed, lenOrView(packed))) === 1
|
|
126
|
+
},
|
|
127
|
+
metricsDestroy(handle) {
|
|
128
|
+
metricsDestroyRaw(handle)
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Bind-time self-test for the metrics surface (unique `ct.selftest.*` names).
|
|
135
|
+
* Exercises every symbol's ABI at bind time; `false` disables the ffi layer
|
|
136
|
+
* and forces the napi fallback.
|
|
137
|
+
*/
|
|
138
|
+
export function selfTestMetrics(b: BunFFI): boolean {
|
|
139
|
+
let handle = 0
|
|
140
|
+
try {
|
|
141
|
+
handle = b.metricsCreate()
|
|
142
|
+
if (handle === 0) return false
|
|
143
|
+
|
|
144
|
+
const counter = b.metricsCounter(handle, 'ct_selftest_hits', 'route\u001fstatus')
|
|
145
|
+
if (!Number.isInteger(counter) || counter < 0 || counter === METRICS_DECLARE_ERR) {
|
|
146
|
+
console.error('FAIL: declare')
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
if (!b.metricsRecord(handle, counter, encodeUtf8('/a\u001f200'), 2)) {
|
|
150
|
+
console.error('FAIL: record1')
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
if (!b.metricsRecord(handle, counter, encodeUtf8('/a\u001f200'), 1)) {
|
|
154
|
+
console.error('FAIL: record2')
|
|
155
|
+
return false
|
|
156
|
+
}
|
|
157
|
+
if (!b.metricsRecordStr(handle, counter, '/b\u001f200', 2)) {
|
|
158
|
+
console.error('FAIL: recordStr')
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.error('CK: counter')
|
|
163
|
+
const gauge = b.metricsGauge(handle, 'ct_selftest_depth', 'q')
|
|
164
|
+
if (gauge === METRICS_DECLARE_ERR) return false
|
|
165
|
+
if (!b.metricsGaugeSet(handle, gauge, encodeUtf8('jobs'), 4)) return false
|
|
166
|
+
if (!b.metricsGaugeSetStr(handle, gauge, 'io', 6)) return false
|
|
167
|
+
|
|
168
|
+
console.error('CK: gauge')
|
|
169
|
+
const hist = b.metricsHistogram(handle, 'ct_selftest_latency', '', '0.1,0.5')
|
|
170
|
+
if (hist === METRICS_DECLARE_ERR) return false
|
|
171
|
+
if (!b.metricsRecord(handle, hist, EMPTY, 0.25)) return false
|
|
172
|
+
if (b.metricsRecord(handle, hist, EMPTY, -1)) return false // negatives rejected
|
|
173
|
+
|
|
174
|
+
console.error('CK: hist')
|
|
175
|
+
// Arity mismatch must fail safely.
|
|
176
|
+
if (b.metricsRecord(handle, counter, encodeUtf8('/a'), 1)) return false
|
|
177
|
+
|
|
178
|
+
console.error('CK: arity')
|
|
179
|
+
// Render: probe with a small buffer → exact required size; then render.
|
|
180
|
+
const probe = new Uint8Array(16)
|
|
181
|
+
const needed = b.metricsRender(handle, probe)
|
|
182
|
+
if (needed <= probe.length) return false
|
|
183
|
+
const out = new Uint8Array(needed)
|
|
184
|
+
const written = b.metricsRender(handle, out)
|
|
185
|
+
if (written !== needed) return false
|
|
186
|
+
const text = decodeUtf8(out.subarray(0, written))
|
|
187
|
+
if (
|
|
188
|
+
!text.includes('ct_selftest_hits{route="/a",status="200"} 3\n') ||
|
|
189
|
+
!text.includes('ct_selftest_hits{route="/b",status="200"} 2\n') ||
|
|
190
|
+
!text.includes('ct_selftest_depth{q="jobs"} 4\n') ||
|
|
191
|
+
!text.includes('ct_selftest_depth{q="io"} 6\n') ||
|
|
192
|
+
!text.includes('ct_selftest_latency_bucket{le="0.5"} 1\n') ||
|
|
193
|
+
!text.includes('ct_selftest_latency_count 1\n')
|
|
194
|
+
) {
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.error('CK: render')
|
|
199
|
+
// Snapshot: packed dump starts with version=1 + familyCount>=3.
|
|
200
|
+
{
|
|
201
|
+
const probe2 = new Uint8Array(16)
|
|
202
|
+
const need = b.metricsSnapshot(handle, probe2)
|
|
203
|
+
if (need <= probe2.length) return false
|
|
204
|
+
const snap = new Uint8Array(need)
|
|
205
|
+
if (b.metricsSnapshot(handle, snap) !== need) return false
|
|
206
|
+
const sv = new DataView(snap.buffer)
|
|
207
|
+
if (sv.getUint32(0, true) !== 1) return false
|
|
208
|
+
if (sv.getUint32(4, true) < 3) return false
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.error('CK: snapshot')
|
|
212
|
+
// Batch: two entries in ONE crossing.
|
|
213
|
+
{
|
|
214
|
+
const c2 = b.metricsCounter(handle, 'ct_selftest_batch', 'k')
|
|
215
|
+
const buf = new Uint8Array(64)
|
|
216
|
+
const dv = new DataView(buf.buffer)
|
|
217
|
+
dv.setUint32(0, 1, true)
|
|
218
|
+
dv.setUint32(4, c2, true)
|
|
219
|
+
dv.setUint32(8, 1, true)
|
|
220
|
+
buf[12] = 'v'.charCodeAt(0)
|
|
221
|
+
dv.setFloat64(13, 4, true)
|
|
222
|
+
if (!b.metricsRecordBatch(handle, buf.subarray(0, 21))) return false
|
|
223
|
+
const probe3 = new Uint8Array(16)
|
|
224
|
+
const need3 = b.metricsRender(handle, probe3)
|
|
225
|
+
const out3 = new Uint8Array(need3)
|
|
226
|
+
b.metricsRender(handle, out3)
|
|
227
|
+
if (!decodeUtf8(out3.subarray(0, need3)).includes('ct_selftest_batch{k="v"} 4')) {
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.error('CK: batch')
|
|
233
|
+
// A null handle must throw on declare (never dereference freed state).
|
|
234
|
+
try {
|
|
235
|
+
b.metricsCounter(0, 'ct_selftest_x', '')
|
|
236
|
+
return false
|
|
237
|
+
} catch {
|
|
238
|
+
// expected
|
|
239
|
+
}
|
|
240
|
+
return true
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.error('METRICS SELFTEST THROW:', err)
|
|
243
|
+
return false
|
|
244
|
+
} finally {
|
|
245
|
+
b.metricsDestroy(handle)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -11,12 +11,29 @@ import { decodeUtf8, encodeUtf8 } from '../../../shared/codec'
|
|
|
11
11
|
import { EMPTY_VIEW, SELFTEST_HEX } from '../constants'
|
|
12
12
|
import type { BunFFI, Raw3, Raw4, Raw6, Raw7, Raw10 } from '../types'
|
|
13
13
|
import type { BuildCtx } from './util'
|
|
14
|
-
import { flag, growExact } from './util'
|
|
14
|
+
import { allocOut, flag, growExact } from './util'
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Build the parser/wire-format methods of the BunFFI surface. `ctx` is
|
|
18
18
|
* destructured so the method bodies read exactly as the original `build()`.
|
|
19
19
|
*/
|
|
20
|
+
|
|
21
|
+
// ── SSE event/id encode memo ────────────────────────────────────────────────
|
|
22
|
+
// SSE streams repeat the same event NAME constantly ('message'/'update') and
|
|
23
|
+
// often repeat ids; re-encoding them per event is pure waste (~60ns each).
|
|
24
|
+
// One memo slot each: a `===` hit returns the cached bytes, a miss re-encodes.
|
|
25
|
+
// Per-Worker module state (single-threaded event loop), same safety model as
|
|
26
|
+
// the pooled scratches in BuildCtx.
|
|
27
|
+
const sseMemoEvent = { str: null as string | null, bytes: EMPTY_VIEW }
|
|
28
|
+
const sseMemoId = { str: null as string | null, bytes: EMPTY_VIEW }
|
|
29
|
+
function sseMemoEncode(slot: { str: string | null; bytes: Uint8Array }, s: string): Uint8Array {
|
|
30
|
+
if (s === slot.str) return slot.bytes
|
|
31
|
+
const bytes = encodeUtf8(s)
|
|
32
|
+
slot.str = s
|
|
33
|
+
slot.bytes = bytes
|
|
34
|
+
return bytes
|
|
35
|
+
}
|
|
36
|
+
|
|
20
37
|
export function buildParse(
|
|
21
38
|
sym: Record<string, (...a: unknown[]) => unknown>,
|
|
22
39
|
ctx: BuildCtx,
|
|
@@ -67,8 +84,9 @@ export function buildParse(
|
|
|
67
84
|
return w
|
|
68
85
|
},
|
|
69
86
|
wsFrameEncode(opcode, payload, mask, fin) {
|
|
70
|
-
// Header (max 10) + payload + mask key (4).
|
|
71
|
-
|
|
87
|
+
// Header (max 10) + payload + mask key (4). allocOut skips the zeroing
|
|
88
|
+
// (the native write fills [0,w) and only that slice escapes).
|
|
89
|
+
const out = allocOut(payload.length + 14)
|
|
72
90
|
const w = Number(
|
|
73
91
|
wsFrameEncode(
|
|
74
92
|
opcode,
|
|
@@ -182,15 +200,24 @@ export function buildParse(
|
|
|
182
200
|
parseMediaType(input) {
|
|
183
201
|
// Packed verdict: [u32 mediaTypeLen][mediaType][u32 charsetLen
|
|
184
202
|
// (0xFFFFFFFF = none)][charset][u32 boundaryLen][boundary]
|
|
185
|
-
// [u32 paramCount]{[u32 keyLen][key][u32 valLen][val]}.
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
)
|
|
203
|
+
// [u32 paramCount]{[u32 keyLen][key][u32 valLen][val]}.
|
|
204
|
+
// POOLED SCRATCH: the only consumer (the scalar `unpackMediaType`)
|
|
205
|
+
// decodes the verdict synchronously into a JS object before the next
|
|
206
|
+
// call, so the packed bytes never need to outlive this call — no
|
|
207
|
+
// growExact fresh-buffer per request. 0 = invalid media type (throw).
|
|
208
|
+
// 2x + 128 covers the worst-case packed expansion (every byte becomes a
|
|
209
|
+
// param key/value length prefix); a still-too-small write falls back to
|
|
210
|
+
// ONE exact-size buffer (needed-size convention).
|
|
211
|
+
let out = scratchFor(input.length * 2 + 128)
|
|
212
|
+
let w = Number(parseMediaTypeSym(input, lenOrView(input), out, lenOrView(out)))
|
|
213
|
+
if (w > out.length) {
|
|
214
|
+
out = new Uint8Array(w)
|
|
215
|
+
w = Number(parseMediaTypeSym(input, lenOrView(input), out, lenOrView(out)))
|
|
216
|
+
}
|
|
217
|
+
if (w === 0 || w > out.length) {
|
|
218
|
+
throw new Error('media type parse: invalid media type or output buffer too small')
|
|
219
|
+
}
|
|
220
|
+
return out.subarray(0, w)
|
|
194
221
|
},
|
|
195
222
|
parseHttpDate(input) {
|
|
196
223
|
// Packed [u8 ok][i64 secs LE] (9 B) / 1 B (ok=0). A too-small buffer is
|
|
@@ -207,13 +234,18 @@ export function buildParse(
|
|
|
207
234
|
},
|
|
208
235
|
parseAcceptEncoding(input) {
|
|
209
236
|
// Packed: [u32 count]{[u32 encLen][enc][f32 q][u32 order]} (empty header →
|
|
210
|
-
// count 0, 4 bytes).
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
237
|
+
// count 0, 4 bytes). POOLED SCRATCH: consumed synchronously by the scalar
|
|
238
|
+
// `unpackAcceptEncoding` — never escapes as raw bytes. 0 = too small.
|
|
239
|
+
let out = scratchFor(input.length * 2 + 128)
|
|
240
|
+
let w = Number(parseAcceptEncodingSym(input, lenOrView(input), out, lenOrView(out)))
|
|
241
|
+
if (w > out.length) {
|
|
242
|
+
out = new Uint8Array(w)
|
|
243
|
+
w = Number(parseAcceptEncodingSym(input, lenOrView(input), out, lenOrView(out)))
|
|
244
|
+
}
|
|
245
|
+
if (w === 0 || w > out.length) {
|
|
246
|
+
throw new Error('accept encoding parse: output buffer too small')
|
|
247
|
+
}
|
|
248
|
+
return out.subarray(0, w)
|
|
217
249
|
},
|
|
218
250
|
urlEncodeQuery(input) {
|
|
219
251
|
// Packed pairs `[u32 count]{[u32 keyLen][key][u32 valLen][val]}` (the JS
|
|
@@ -255,8 +287,12 @@ export function buildParse(
|
|
|
255
287
|
// non-null (flag bits 1/2/4 = present) so a present-but-empty string is
|
|
256
288
|
// distinct from absent (napi Option parity). growExact with the needed-size
|
|
257
289
|
// convention; `data.length + 64` covers the common single-line case.
|
|
258
|
-
|
|
259
|
-
|
|
290
|
+
// NOTE (2026-08-24): cstring-ARG variants were once blamed for JIT
|
|
291
|
+
// crashes here — docs/FFI_BUN_GUIDE.md §14 traced every report to
|
|
292
|
+
// dev-loop ABI desync, not bun:ffi. The memo captures the win without
|
|
293
|
+
// an ABI change anyway.
|
|
294
|
+
const ev = event === null ? EMPTY_VIEW : sseMemoEncode(sseMemoEvent, event)
|
|
295
|
+
const idv = id === null ? EMPTY_VIEW : sseMemoEncode(sseMemoId, id)
|
|
260
296
|
const flags = (event === null ? 0 : 1) | (id === null ? 0 : 2) | (retry === null ? 0 : 4)
|
|
261
297
|
return growExact(
|
|
262
298
|
(out) =>
|
|
@@ -283,8 +319,8 @@ export function buildParse(
|
|
|
283
319
|
// Pooled sibling — caller-owned output buffer (sized ≥ `data.length + 64`
|
|
284
320
|
// for the common single-line case). Needed-size convention: w > output.length
|
|
285
321
|
// = exact required size → throw; w === 0 = real error (invalid UTF-8).
|
|
286
|
-
const ev = event === null ? EMPTY_VIEW :
|
|
287
|
-
const idv = id === null ? EMPTY_VIEW :
|
|
322
|
+
const ev = event === null ? EMPTY_VIEW : sseMemoEncode(sseMemoEvent, event)
|
|
323
|
+
const idv = id === null ? EMPTY_VIEW : sseMemoEncode(sseMemoId, id)
|
|
288
324
|
const flags = (event === null ? 0 : 1) | (id === null ? 0 : 2) | (retry === null ? 0 : 4)
|
|
289
325
|
const w = Number(
|
|
290
326
|
sseEncodeIntoRaw(
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/native/ffi/build/task.ts — off-thread task runtime BunFFI methods.
|
|
2
|
+
//
|
|
3
|
+
// The thinnest possible binding: `rust/task/` owns the pool, the result ring,
|
|
4
|
+
// and the batched doorbell. This file only adapts the `(ptr,len)` ABI pair
|
|
5
|
+
// (probe-gated `buffer`/`buffer_length` vs explicit lengths) and converts the
|
|
6
|
+
// `u64_fast` byte counts to numbers.
|
|
7
|
+
//
|
|
8
|
+
// See docs/RND-CONCURRENCY.md for the design and the measured costs.
|
|
9
|
+
|
|
10
|
+
import type { BunFFI, Raw0, Raw2, Raw4, Raw6 } from '../types'
|
|
11
|
+
import type { BuildCtx } from './util'
|
|
12
|
+
|
|
13
|
+
/** Raw 1-arg C-ABI symbol signature (a lone scalar arg or a bare pointer). */
|
|
14
|
+
type Raw1 = (a: unknown) => number | bigint
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build the off-thread task-runtime methods of the BunFFI surface. `ctx`
|
|
18
|
+
* supplies the probe-gated length adapter (`lenOrView`).
|
|
19
|
+
*/
|
|
20
|
+
export function buildTask(
|
|
21
|
+
sym: Record<string, (...a: unknown[]) => unknown>,
|
|
22
|
+
ctx: BuildCtx,
|
|
23
|
+
): Partial<BunFFI> {
|
|
24
|
+
const { lenOrView } = ctx
|
|
25
|
+
|
|
26
|
+
const init = sym.castrum_task_init as Raw1
|
|
27
|
+
const submit = sym.castrum_task_submit as Raw4
|
|
28
|
+
const submitOut = sym.castrum_task_submit_out as Raw6
|
|
29
|
+
const submitSlice = sym.castrum_task_submit_slice as Raw6
|
|
30
|
+
const drain = sym.castrum_task_drain as Raw2
|
|
31
|
+
const pending = sym.castrum_task_pending as Raw0
|
|
32
|
+
const cancel = sym.castrum_task_cancel as Raw1
|
|
33
|
+
const setDoorbell = sym.castrum_task_set_doorbell as Raw1
|
|
34
|
+
const shutdown = sym.castrum_task_shutdown as Raw0
|
|
35
|
+
const threads = sym.castrum_task_threads as Raw0
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
taskInit: (n) => Number(init(n)),
|
|
39
|
+
// `args` is copied onto the worker by Rust, so the caller's pooled buffer
|
|
40
|
+
// may be released as soon as this returns.
|
|
41
|
+
taskSubmit: (op, args, taskId) => Number(submit(op, args, lenOrView(args), taskId)),
|
|
42
|
+
taskSubmitOut: (op, args, taskId, output) =>
|
|
43
|
+
Number(submitOut(op, args, lenOrView(args), taskId, output, lenOrView(output))),
|
|
44
|
+
// Zero-copy INPUT: the header is copied by Rust, the payload is read in
|
|
45
|
+
// place — which is why the runtime must hold a reference to `data` for the
|
|
46
|
+
// whole task (see `Deferred.keep` in src/task/runtime.ts).
|
|
47
|
+
taskSubmitSlice: (op, hdr, data, taskId) =>
|
|
48
|
+
Number(submitSlice(op, hdr, lenOrView(hdr), data, lenOrView(data), taskId)),
|
|
49
|
+
taskDrain: (output) => Number(drain(output, lenOrView(output))),
|
|
50
|
+
taskPending: () => Number(pending()),
|
|
51
|
+
taskCancel: (taskId) => Number(cancel(taskId)),
|
|
52
|
+
taskSetDoorbell: (ptr) => Number(setDoorbell(ptr)),
|
|
53
|
+
taskShutdown: () => Number(shutdown()),
|
|
54
|
+
taskThreads: () => Number(threads()),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Bind-time self-test for the task symbols. The pool is intentionally NOT
|
|
60
|
+
* started here (no threads at import time) and no completion is produced;
|
|
61
|
+
* the full submit→drain→doorbell path is exercised by `test/unit/task/`.
|
|
62
|
+
*/
|
|
63
|
+
export function selfTestTask(b: BunFFI): boolean {
|
|
64
|
+
try {
|
|
65
|
+
if (typeof b.taskPending() !== 'number') return false
|
|
66
|
+
if (typeof b.taskThreads() !== 'number' || b.taskThreads() < 0) return false
|
|
67
|
+
// `0` disables the doorbell — safe at bind time (nothing is in flight).
|
|
68
|
+
if (b.taskSetDoorbell(0) !== 1) return false
|
|
69
|
+
// An empty ring → 0; exercises the drain symbol without allocating anything.
|
|
70
|
+
if (typeof b.taskDrain(new Uint8Array(4)) !== 'number') return false
|
|
71
|
+
// NOT called: submitting here would start the pool threads at bind time.
|
|
72
|
+
// The full submit/submit_out path is exercised by test/unit/task/.
|
|
73
|
+
if (typeof b.taskSubmitOut !== 'function') return false
|
|
74
|
+
if (typeof b.taskSubmitSlice !== 'function') return false
|
|
75
|
+
return true
|
|
76
|
+
} catch {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -42,6 +42,37 @@ export interface BuildCtx {
|
|
|
42
42
|
/** Coerce an optional boolean to the C `u8` flag (1/0). */
|
|
43
43
|
export const flag = (v?: boolean): number => (v ? 1 : 0)
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Does this string contain a NUL (`U+0000`)?
|
|
47
|
+
*
|
|
48
|
+
* `bun:ffi` transcodes a JS string passed to a `cstring` ARG into a
|
|
49
|
+
* NUL-TERMINATED UTF-8 buffer, so an embedded NUL SILENTLY TRUNCATES the value
|
|
50
|
+
* on the native side (Bun's docs describe the transcode; the truncation is the
|
|
51
|
+
* C convention it inherits). That is fine when the string is developer-supplied
|
|
52
|
+
* config or NUL-free by construction — and it is a real bug when the callee
|
|
53
|
+
* answers with a VERDICT (validators, query/cookie gates, batch validators: a
|
|
54
|
+
* truncated prefix passes where the full input fails) or when the exact bytes
|
|
55
|
+
* are the result (`regexEscape`, metric label values).
|
|
56
|
+
*
|
|
57
|
+
* Guard contract for every `cstring` ARG reachable from user input: either
|
|
58
|
+
* reject/short-circuit on NUL, or route to the `(ptr, usize)` byte form which
|
|
59
|
+
* carries the exact length. `test/unit/native/cstring-nul.test.ts` pins this.
|
|
60
|
+
*/
|
|
61
|
+
export { hasNul } from '../../../shared/bytes'
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Output-buffer allocation for the FFI wrappers. The native write fills
|
|
65
|
+
* `[0, w)` and only `subarray(0, w)` escapes, so zero-initialization is pure
|
|
66
|
+
* waste: `Buffer.allocUnsafe` skips it (measured ~2x at 4 KB, ~3.4x at 16 KB
|
|
67
|
+
* on Bun 1.4; small sizes stay on the plain constructor where it is faster).
|
|
68
|
+
*/
|
|
69
|
+
export function allocOut(n: number): Uint8Array {
|
|
70
|
+
if (n >= 512 && typeof Buffer !== 'undefined' && typeof Buffer.allocUnsafe === 'function') {
|
|
71
|
+
return Buffer.allocUnsafe(n)
|
|
72
|
+
}
|
|
73
|
+
return new Uint8Array(n)
|
|
74
|
+
}
|
|
75
|
+
|
|
45
76
|
/**
|
|
46
77
|
* Write with the C ABI's "needed" convention. The C side returns the EXACT
|
|
47
78
|
* total bytes the result requires: `0` = real error (throw immediately — no
|
|
@@ -59,7 +90,7 @@ export function growExact(
|
|
|
59
90
|
): Uint8Array {
|
|
60
91
|
let cap = Math.min(Math.max(initial, 16), max)
|
|
61
92
|
for (;;) {
|
|
62
|
-
const out =
|
|
93
|
+
const out = allocOut(cap)
|
|
63
94
|
const w = Number(write(out))
|
|
64
95
|
if (w === 0) throw new Error(error)
|
|
65
96
|
if (w <= out.length) return out.subarray(0, w)
|
package/src/native/ffi/build.ts
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
import { buildCodecs } from './build/codecs'
|
|
15
15
|
import { buildCompress } from './build/compress'
|
|
16
16
|
import { buildInstances } from './build/instances'
|
|
17
|
+
import { buildMetrics } from './build/metrics'
|
|
17
18
|
import { buildParse } from './build/parse'
|
|
19
|
+
import { buildTask } from './build/task'
|
|
18
20
|
import type { BuildCtx } from './build/util'
|
|
19
21
|
import type { BunFFI } from './types'
|
|
20
22
|
|
|
@@ -90,5 +92,7 @@ export function build(
|
|
|
90
92
|
...buildCompress(sym, ctx),
|
|
91
93
|
...buildParse(sym, ctx),
|
|
92
94
|
...buildInstances(sym, ctx),
|
|
95
|
+
...buildMetrics(sym, ctx),
|
|
96
|
+
...buildTask(sym, ctx),
|
|
93
97
|
} as BunFFI
|
|
94
98
|
}
|
|
@@ -7,16 +7,25 @@
|
|
|
7
7
|
// the safety net. New C-ABI symbols MUST be added to the self-test for their
|
|
8
8
|
// domain: each check lives in the same `ffi/build/` file where the method is
|
|
9
9
|
// bound (`selfTestCodecs` / `selfTestCompress` / `selfTestParse` /
|
|
10
|
-
// `selfTestInstances`), and this entry point ANDs them
|
|
11
|
-
// failure disables the whole layer.
|
|
10
|
+
// `selfTestInstances` / `selfTestMetrics`), and this entry point ANDs them
|
|
11
|
+
// together so any single failure disables the whole layer.
|
|
12
12
|
|
|
13
13
|
import { selfTestCodecs } from './build/codecs'
|
|
14
14
|
import { selfTestCompress } from './build/compress'
|
|
15
15
|
import { selfTestInstances } from './build/instances'
|
|
16
|
+
import { selfTestMetrics } from './build/metrics'
|
|
16
17
|
import { selfTestParse } from './build/parse'
|
|
18
|
+
import { selfTestTask } from './build/task'
|
|
17
19
|
import type { BunFFI } from './types'
|
|
18
20
|
|
|
19
21
|
/** Verify every bound function against known-good results; false disables ffi. */
|
|
20
22
|
export function selfTest(b: BunFFI): boolean {
|
|
21
|
-
return
|
|
23
|
+
return (
|
|
24
|
+
selfTestCodecs(b) &&
|
|
25
|
+
selfTestCompress(b) &&
|
|
26
|
+
selfTestParse(b) &&
|
|
27
|
+
selfTestInstances(b) &&
|
|
28
|
+
selfTestMetrics(b) &&
|
|
29
|
+
selfTestTask(b)
|
|
30
|
+
)
|
|
22
31
|
}
|