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.
Files changed (120) hide show
  1. package/README.md +153 -630
  2. package/castrum.linux-x64-gnu.node +0 -0
  3. package/dist/index.d.ts +1653 -1817
  4. package/dist/index.js +6172 -4294
  5. package/docs/API.md +337 -0
  6. package/docs/ARCHITECTURE.md +111 -17
  7. package/docs/BENCHMARKS.md +117 -49
  8. package/docs/CASE_STUDY.md +243 -0
  9. package/docs/ENVIRONMENT.md +24 -7
  10. package/docs/FFI_BUN_GUIDE.md +335 -0
  11. package/docs/GETTING_STARTED.md +18 -34
  12. package/docs/HOW_TO_ADD_AN_OP.md +180 -0
  13. package/docs/INGRESS-ROUTER.md +82 -0
  14. package/docs/INGRESS.md +11 -7
  15. package/docs/NATIVE-ROUTE.md +77 -0
  16. package/docs/REPO_MAP.md +103 -53
  17. package/docs/adr/0001-two-wire-formats.md +1 -1
  18. package/docs/adr/0003-bun-builtins-delegation.md +6 -5
  19. package/docs/ai/LOCAL_DEV.md +81 -0
  20. package/docs/ai/TREE.md +1070 -0
  21. package/docs/bun-builtins-decision-matrix.md +25 -18
  22. package/index.ts +9 -16
  23. package/package.json +41 -28
  24. package/src/ingress/body.ts +167 -27
  25. package/src/ingress/decode/result-base.ts +10 -2
  26. package/src/ingress/handlers.ts +116 -285
  27. package/src/ingress/headers/security.ts +41 -0
  28. package/src/ingress/health.ts +1 -0
  29. package/src/ingress/index.ts +45 -169
  30. package/src/ingress/native-route.ts +182 -0
  31. package/src/ingress/packing/gather-raw-headers.ts +110 -22
  32. package/src/ingress/packing/header-packing.ts +5 -5
  33. package/src/ingress/packing/input-packer.ts +17 -3
  34. package/src/ingress/packing/route-wire.ts +197 -0
  35. package/src/ingress/packing/select-headers.ts +20 -7
  36. package/src/ingress/path-matcher.ts +115 -0
  37. package/src/ingress/response/baked-response.ts +302 -0
  38. package/src/ingress/router.ts +277 -0
  39. package/src/ingress/routes/common.ts +50 -1
  40. package/src/ingress/routes/fallback.ts +2 -2
  41. package/src/ingress/routes/head.ts +3 -13
  42. package/src/ingress/routes/index.ts +5 -4
  43. package/src/ingress/routes/json-write.ts +26 -47
  44. package/src/ingress/routes/native.ts +130 -0
  45. package/src/ingress/routes/options.ts +2 -12
  46. package/src/ingress/routes/read.ts +3 -13
  47. package/src/ingress/routes/responder.ts +159 -0
  48. package/src/ingress/server-node.ts +7 -7
  49. package/src/ingress/server.ts +83 -112
  50. package/src/ingress/sync.ts +151 -0
  51. package/src/ingress/types.ts +44 -1
  52. package/src/integration/pipeline.ts +3 -0
  53. package/src/integration/streaming.ts +2 -3
  54. package/src/integration/websocket.ts +4 -3
  55. package/src/loader/batch.ts +4 -0
  56. package/src/loader/cost.ts +1 -0
  57. package/src/loader/index.ts +43 -228
  58. package/src/loader/state.ts +84 -0
  59. package/src/loader/types.ts +145 -0
  60. package/src/native/ffi/build/codecs.ts +736 -0
  61. package/src/native/ffi/build/compress.ts +248 -0
  62. package/src/native/ffi/build/instances.ts +598 -0
  63. package/src/native/ffi/build/parse.ts +539 -0
  64. package/src/native/ffi/build/util.ts +140 -0
  65. package/src/native/ffi/build.ts +94 -0
  66. package/src/native/ffi/constants.ts +58 -0
  67. package/src/native/ffi/selftest.ts +22 -0
  68. package/src/native/ffi/types.ts +499 -0
  69. package/src/native/ffi.ts +307 -1357
  70. package/src/native/loader.ts +52 -6
  71. package/src/native/types/instances.ts +279 -0
  72. package/src/native/types.ts +41 -238
  73. package/src/runtime/builtins.ts +131 -0
  74. package/src/runtime/codec.ts +147 -0
  75. package/src/runtime/detect.ts +36 -0
  76. package/src/runtime/index.ts +28 -0
  77. package/src/runtime/native.ts +35 -0
  78. package/src/runtime/server.ts +31 -0
  79. package/src/runtime/transport.ts +64 -0
  80. package/src/runtime/types.ts +173 -0
  81. package/src/runtime/uuid.ts +45 -0
  82. package/src/runtime/websocket.ts +30 -0
  83. package/src/rust-ffi/addon.ts +1 -1
  84. package/src/rust-ffi/batch/build.ts +155 -0
  85. package/src/rust-ffi/batch/index.ts +8 -0
  86. package/src/rust-ffi/batch/types.ts +92 -0
  87. package/src/rust-ffi/client.ts +5 -6
  88. package/src/rust-ffi/context.ts +39 -10
  89. package/src/rust-ffi/index.ts +28 -17
  90. package/src/rust-ffi/into.ts +21 -0
  91. package/src/rust-ffi/options.ts +1 -1
  92. package/src/rust-ffi/packed.ts +40 -2
  93. package/src/rust-ffi/proven.ts +16 -30
  94. package/src/rust-ffi/scalar/crypto.ts +188 -107
  95. package/src/rust-ffi/scalar/factories.ts +257 -27
  96. package/src/rust-ffi/scalar/hashing.ts +43 -41
  97. package/src/rust-ffi/scalar/http.ts +208 -77
  98. package/src/rust-ffi/scalar/index.ts +3 -3
  99. package/src/rust-ffi/scalar/interface.ts +88 -213
  100. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  101. package/src/rust-ffi/scalar/json.ts +28 -12
  102. package/src/rust-ffi/scalar/payload.ts +159 -42
  103. package/src/rust-ffi/text.ts +31 -5
  104. package/src/selection.ts +27 -31
  105. package/src/shared/adaptive.ts +1 -2
  106. package/src/shared/buffer-pool.ts +72 -17
  107. package/src/shared/bytes.ts +75 -6
  108. package/src/shared/codec.ts +78 -0
  109. package/src/shared/env.ts +4 -4
  110. package/src/shared/log.ts +1 -0
  111. package/src/shared/packed/index.ts +10 -0
  112. package/src/shared/packed/parsers.ts +31 -0
  113. package/src/shared/packed/schema.ts +23 -0
  114. package/src/shared/{packed.ts → packed/wire.ts} +31 -39
  115. package/src/shared/proven.ts +189 -496
  116. package/src/shared/runtime.ts +20 -25
  117. package/src/shared/uuid.ts +12 -28
  118. package/src/data/json-rows.ts +0 -33
  119. package/src/rust-ffi/batch.ts +0 -431
  120. 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
+ }