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
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// src/native/ffi/build/compress.ts — compression + JSON patch BunFFI methods.
|
|
2
|
+
//
|
|
3
|
+
// gzip / brotli compress-decompress (streaming C cores + the needed-size
|
|
4
|
+
// convention) and RFC 6902 JSON patch. Receives the raw dlopen'd symbols and
|
|
5
|
+
// the per-bind context from `build()`.
|
|
6
|
+
|
|
7
|
+
import { decodeUtf8, encodeUtf8 } from '../../../shared/codec'
|
|
8
|
+
import {
|
|
9
|
+
COMPRESS_HEADROOM,
|
|
10
|
+
COMPRESS_INITIAL_CAP,
|
|
11
|
+
COMPRESS_MAX_CAP,
|
|
12
|
+
DECOMPRESS_FALLBACK_CAP,
|
|
13
|
+
DECOMPRESS_GUESS_MULTIPLIER_BROTLI,
|
|
14
|
+
DECOMPRESS_GUESS_MULTIPLIER_GZIP,
|
|
15
|
+
DECOMPRESS_MIN_INITIAL,
|
|
16
|
+
MAX_DECOMPRESSED,
|
|
17
|
+
MAX_JSON_PATCH_OUTPUT,
|
|
18
|
+
SELFTEST_HEX,
|
|
19
|
+
} from '../constants'
|
|
20
|
+
import type { BunFFI, Raw2, Raw5, Raw6 } from '../types'
|
|
21
|
+
import type { BuildCtx } from './util'
|
|
22
|
+
import { growExact } from './util'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the compression/JSON-patch methods of the BunFFI surface. `ctx` is
|
|
26
|
+
* destructured so the method bodies read exactly as the original `build()`.
|
|
27
|
+
*/
|
|
28
|
+
export function buildCompress(
|
|
29
|
+
sym: Record<string, (...a: unknown[]) => unknown>,
|
|
30
|
+
ctx: BuildCtx,
|
|
31
|
+
): Partial<BunFFI> {
|
|
32
|
+
const { lenOrView } = ctx
|
|
33
|
+
|
|
34
|
+
const gzipCompress = sym.castrum_gzip_compress as Raw5
|
|
35
|
+
const gzipDecompress = sym.castrum_gzip_decompress as Raw5
|
|
36
|
+
const brotliCompress = sym.castrum_brotli_compress as Raw5
|
|
37
|
+
const brotliDecompress = sym.castrum_brotli_decompress as Raw5
|
|
38
|
+
const gzipIsize = sym.castrum_gzip_isize as Raw2
|
|
39
|
+
const jsonPatch = sym.castrum_json_patch as Raw6
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
jsonPatch(doc, patch) {
|
|
43
|
+
return decodeUtf8(
|
|
44
|
+
growExact(
|
|
45
|
+
(out) =>
|
|
46
|
+
Number(jsonPatch(doc, lenOrView(doc), patch, lenOrView(patch), out, lenOrView(out))),
|
|
47
|
+
Math.min(Math.max(doc.length, patch.length) + 16, 64 * 1024),
|
|
48
|
+
MAX_JSON_PATCH_OUTPUT,
|
|
49
|
+
'json patch: output buffer too small or patch inapplicable',
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
},
|
|
53
|
+
gzipCompress(data, level = 6) {
|
|
54
|
+
// The C ABI streams the compressed output directly into `out` (no internal
|
|
55
|
+
// Vec — see rust/payload/compress.rs gzip_compress_into). Cap the initial
|
|
56
|
+
// so large compressible inputs don't pay a full-size allocation (measured:
|
|
57
|
+
// a 75 KiB input compressed to <1 KiB — COMPRESS_INITIAL_CAP is plenty and
|
|
58
|
+
// single-pass). growExact handles incompressible data with at most one
|
|
59
|
+
// exact retry (no re-run loop).
|
|
60
|
+
return growExact(
|
|
61
|
+
(out) =>
|
|
62
|
+
Number(gzipCompress(data, lenOrView(data), Math.min(level, 9), out, lenOrView(out))),
|
|
63
|
+
Math.min(data.length + COMPRESS_HEADROOM, COMPRESS_INITIAL_CAP),
|
|
64
|
+
Math.max(data.length * 2 + COMPRESS_HEADROOM, COMPRESS_MAX_CAP),
|
|
65
|
+
'gzip compress: output buffer too small',
|
|
66
|
+
)
|
|
67
|
+
},
|
|
68
|
+
gzipCompressInto(data, output, level = 6) {
|
|
69
|
+
const w = Number(
|
|
70
|
+
gzipCompress(data, lenOrView(data), Math.min(level, 9), output, lenOrView(output)),
|
|
71
|
+
)
|
|
72
|
+
// Needed-size convention: w > output.length = too small; w === 0 = real error.
|
|
73
|
+
if (w === 0) {
|
|
74
|
+
throw new Error('gzip compress: invalid input')
|
|
75
|
+
}
|
|
76
|
+
if (w > output.length) {
|
|
77
|
+
throw new Error('gzip compress: output buffer too small')
|
|
78
|
+
}
|
|
79
|
+
return w
|
|
80
|
+
},
|
|
81
|
+
gzipDecompress(data, maxDecompressed) {
|
|
82
|
+
const max = maxDecompressed ?? MAX_DECOMPRESSED
|
|
83
|
+
// Pre-size from the gzip ISIZE trailer via the C probe (exact for
|
|
84
|
+
// single-member streams) so the happy path is a single native pass; fall
|
|
85
|
+
// back to a multiplier guess. growExact handles any residual miss with
|
|
86
|
+
// one exact retry, and invalid input throws immediately (no 64 MiB
|
|
87
|
+
// grow-to-max allocation storm).
|
|
88
|
+
const isize = Number(gzipIsize(data, lenOrView(data)))
|
|
89
|
+
const initial =
|
|
90
|
+
isize !== 0
|
|
91
|
+
? Math.min(isize, max)
|
|
92
|
+
: Math.min(data.length * DECOMPRESS_GUESS_MULTIPLIER_GZIP, DECOMPRESS_FALLBACK_CAP)
|
|
93
|
+
return growExact(
|
|
94
|
+
(out) => Number(gzipDecompress(data, lenOrView(data), max, out, lenOrView(out))),
|
|
95
|
+
initial,
|
|
96
|
+
max,
|
|
97
|
+
'gzip decompress: invalid stream or exceeded max decompressed size',
|
|
98
|
+
)
|
|
99
|
+
},
|
|
100
|
+
gzipDecompressInto(data, output, maxDecompressed) {
|
|
101
|
+
// Pooled sibling — the C ABI streams the decompressed output directly
|
|
102
|
+
// into the caller buffer via the `_into` core (no internal Vec), keeping
|
|
103
|
+
// the 64 MiB decompression-bomb cap. Needed-size convention: w === 0 =
|
|
104
|
+
// real error (invalid stream / cap exceeded); w > output.length = exact
|
|
105
|
+
// required size → throw (nothing to grow — the caller owns the buffer).
|
|
106
|
+
const max = maxDecompressed ?? MAX_DECOMPRESSED
|
|
107
|
+
const w = Number(gzipDecompress(data, lenOrView(data), max, output, lenOrView(output)))
|
|
108
|
+
if (w === 0) {
|
|
109
|
+
throw new Error('gzip decompress: invalid stream or exceeded max decompressed size')
|
|
110
|
+
}
|
|
111
|
+
if (w > output.length) {
|
|
112
|
+
throw new Error('gzip decompress: output buffer too small')
|
|
113
|
+
}
|
|
114
|
+
return w
|
|
115
|
+
},
|
|
116
|
+
brotliCompress(data, quality = 5) {
|
|
117
|
+
// Same cap rationale as gzipCompress (streaming core); growExact for
|
|
118
|
+
// incompressible data.
|
|
119
|
+
return growExact(
|
|
120
|
+
(out) =>
|
|
121
|
+
Number(brotliCompress(data, lenOrView(data), Math.min(quality, 11), out, lenOrView(out))),
|
|
122
|
+
Math.min(data.length + COMPRESS_HEADROOM, COMPRESS_INITIAL_CAP),
|
|
123
|
+
Math.max(data.length * 2 + COMPRESS_HEADROOM, COMPRESS_MAX_CAP),
|
|
124
|
+
'brotli compress: output buffer too small',
|
|
125
|
+
)
|
|
126
|
+
},
|
|
127
|
+
brotliCompressInto(data, output, quality = 5) {
|
|
128
|
+
const w = Number(
|
|
129
|
+
brotliCompress(data, lenOrView(data), Math.min(quality, 11), output, lenOrView(output)),
|
|
130
|
+
)
|
|
131
|
+
// Needed-size convention: w > output.length = too small; w === 0 = real error.
|
|
132
|
+
if (w === 0) {
|
|
133
|
+
throw new Error('brotli compress: invalid input')
|
|
134
|
+
}
|
|
135
|
+
if (w > output.length) {
|
|
136
|
+
throw new Error('brotli compress: output buffer too small')
|
|
137
|
+
}
|
|
138
|
+
return w
|
|
139
|
+
},
|
|
140
|
+
brotliDecompress(data, maxDecompressed) {
|
|
141
|
+
const max = maxDecompressed ?? MAX_DECOMPRESSED
|
|
142
|
+
// Brotli has no cheap trailer size: DECOMPRESS_GUESS_MULTIPLIER_BROTLI×
|
|
143
|
+
// initial covers typical JSON/text ratios (~10-30x) while the fallback
|
|
144
|
+
// cap bounds over-allocation on large streams; growExact handles
|
|
145
|
+
// higher-ratio streams with one exact retry.
|
|
146
|
+
return growExact(
|
|
147
|
+
(out) => Number(brotliDecompress(data, lenOrView(data), max, out, lenOrView(out))),
|
|
148
|
+
Math.min(
|
|
149
|
+
Math.max(data.length * DECOMPRESS_GUESS_MULTIPLIER_BROTLI, DECOMPRESS_MIN_INITIAL),
|
|
150
|
+
DECOMPRESS_FALLBACK_CAP,
|
|
151
|
+
),
|
|
152
|
+
max,
|
|
153
|
+
'brotli decompress: invalid stream or exceeded max decompressed size',
|
|
154
|
+
)
|
|
155
|
+
},
|
|
156
|
+
brotliDecompressInto(data, output, maxDecompressed) {
|
|
157
|
+
// Pooled sibling — streams into the caller buffer, keeps the 64 MiB cap
|
|
158
|
+
// (same convention as gzipDecompressInto above).
|
|
159
|
+
const max = maxDecompressed ?? MAX_DECOMPRESSED
|
|
160
|
+
const w = Number(brotliDecompress(data, lenOrView(data), max, output, lenOrView(output)))
|
|
161
|
+
if (w === 0) {
|
|
162
|
+
throw new Error('brotli decompress: invalid stream or exceeded max decompressed size')
|
|
163
|
+
}
|
|
164
|
+
if (w > output.length) {
|
|
165
|
+
throw new Error('brotli decompress: output buffer too small')
|
|
166
|
+
}
|
|
167
|
+
return w
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Bind-time self-test for the compression/JSON-patch surface (the methods
|
|
174
|
+
* built in `buildCompress`). `false` disables the ffi layer and forces the
|
|
175
|
+
* napi fallback.
|
|
176
|
+
*/
|
|
177
|
+
export function selfTestCompress(b: BunFFI): boolean {
|
|
178
|
+
const enc = { encode: encodeUtf8 }
|
|
179
|
+
const dec = { decode: decodeUtf8 }
|
|
180
|
+
|
|
181
|
+
// JSON patch: add a key.
|
|
182
|
+
const patched = b.jsonPatch(
|
|
183
|
+
enc.encode(`{"a":"b"}`),
|
|
184
|
+
enc.encode(`[{"op":"add","path":"/c","value":"d"}]`),
|
|
185
|
+
)
|
|
186
|
+
if (!patched.includes(`"c":"d"`)) {
|
|
187
|
+
return false
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// gzip / brotli round-trips.
|
|
191
|
+
const gz = b.gzipCompress(SELFTEST_HEX)
|
|
192
|
+
if (dec.decode(b.gzipDecompress(gz)) !== 'hello') {
|
|
193
|
+
return false
|
|
194
|
+
}
|
|
195
|
+
const br = b.brotliCompress(SELFTEST_HEX)
|
|
196
|
+
if (dec.decode(b.brotliDecompress(br)) !== 'hello') {
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Needed-size convention: invalid compressed input throws IMMEDIATELY (the C
|
|
201
|
+
// ABI returns 0 = real error, so the JS wrapper does NOT grow-retry re-runs
|
|
202
|
+
// or allocate up to the 64 MiB decompression cap per bad input).
|
|
203
|
+
let decompressThrew = false
|
|
204
|
+
try {
|
|
205
|
+
b.gzipDecompress(enc.encode('not-a-gzip-stream'))
|
|
206
|
+
} catch {
|
|
207
|
+
decompressThrew = true
|
|
208
|
+
}
|
|
209
|
+
if (!decompressThrew) {
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
decompressThrew = false
|
|
213
|
+
try {
|
|
214
|
+
b.brotliDecompress(enc.encode('not-brotli-stream'))
|
|
215
|
+
} catch {
|
|
216
|
+
decompressThrew = true
|
|
217
|
+
}
|
|
218
|
+
if (!decompressThrew) {
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// gzipCompressInto → decompress round-trip.
|
|
223
|
+
const gzInto = new Uint8Array(64)
|
|
224
|
+
const gzW = b.gzipCompressInto(SELFTEST_HEX, gzInto)
|
|
225
|
+
if (gzW === 0 || dec.decode(b.gzipDecompress(gzInto.subarray(0, gzW))) !== 'hello') {
|
|
226
|
+
return false
|
|
227
|
+
}
|
|
228
|
+
// brotliCompressInto → decompress round-trip.
|
|
229
|
+
const brInto = new Uint8Array(64)
|
|
230
|
+
const brW = b.brotliCompressInto(SELFTEST_HEX, brInto)
|
|
231
|
+
if (brW === 0 || dec.decode(b.brotliDecompress(brInto.subarray(0, brW))) !== 'hello') {
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
// gzipDecompressInto → decompress into a CALLER buffer round-trip.
|
|
235
|
+
const gzDst = new Uint8Array(16)
|
|
236
|
+
const gzDW = b.gzipDecompressInto(gzInto.subarray(0, gzW), gzDst)
|
|
237
|
+
if (gzDW === 0 || dec.decode(gzDst.subarray(0, gzDW)) !== 'hello') {
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
// brotliDecompressInto → decompress into a CALLER buffer round-trip.
|
|
241
|
+
const brDst = new Uint8Array(16)
|
|
242
|
+
const brDW = b.brotliDecompressInto(brInto.subarray(0, brW), brDst)
|
|
243
|
+
if (brDW === 0 || dec.decode(brDst.subarray(0, brDW)) !== 'hello') {
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return true
|
|
248
|
+
}
|