castrum 0.7.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -368
  3. package/castrum.linux-x64-gnu.node +0 -0
  4. package/dist/index.d.ts +2763 -0
  5. package/dist/index.js +8707 -0
  6. package/docs/API.md +337 -0
  7. package/docs/ARCHITECTURE.md +507 -0
  8. package/docs/BENCHMARKS.md +343 -0
  9. package/docs/CASE_STUDY.md +240 -0
  10. package/docs/ENVIRONMENT.md +81 -0
  11. package/docs/FFI_BUN_GUIDE.md +327 -0
  12. package/docs/GETTING_STARTED.md +192 -0
  13. package/docs/INGRESS-ROUTER.md +81 -0
  14. package/docs/INGRESS.md +746 -0
  15. package/docs/NATIVE-ROUTE.md +53 -0
  16. package/docs/REPO_MAP.md +279 -0
  17. package/docs/adr/0001-two-wire-formats.md +39 -0
  18. package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
  19. package/docs/adr/0003-bun-builtins-delegation.md +43 -0
  20. package/docs/bun-builtins-decision-matrix.md +115 -0
  21. package/index.ts +44 -8
  22. package/package.json +98 -33
  23. package/src/ingress/body.ts +289 -0
  24. package/src/ingress/constants.ts +220 -48
  25. package/src/ingress/context.ts +136 -0
  26. package/src/ingress/decode/baked-result.ts +121 -0
  27. package/src/ingress/decode/fast-result.ts +160 -0
  28. package/src/ingress/decode/packed-sections.ts +56 -0
  29. package/src/ingress/decode/result-base.ts +172 -0
  30. package/src/ingress/errors.ts +72 -0
  31. package/src/ingress/fast.ts +152 -1021
  32. package/src/ingress/handlers.ts +639 -1001
  33. package/src/ingress/headers/baked-templates.ts +102 -0
  34. package/src/ingress/headers/cors.ts +44 -0
  35. package/src/ingress/headers/fast-templates.ts +238 -0
  36. package/src/ingress/headers/hsts.ts +83 -0
  37. package/src/ingress/health.ts +68 -0
  38. package/src/ingress/index.ts +142 -405
  39. package/src/ingress/metrics.ts +132 -0
  40. package/src/ingress/options.ts +197 -0
  41. package/src/ingress/packing/gather-raw-headers.ts +139 -0
  42. package/src/ingress/packing/header-packing.ts +75 -0
  43. package/src/ingress/packing/input-packer.ts +164 -0
  44. package/src/ingress/packing/scratch.ts +50 -0
  45. package/src/ingress/packing/select-headers.ts +102 -0
  46. package/src/ingress/response/error-bodies.ts +75 -0
  47. package/src/ingress/response/terminal.ts +59 -0
  48. package/src/ingress/router.ts +237 -0
  49. package/src/ingress/routes/common.ts +79 -0
  50. package/src/ingress/routes/delete.ts +14 -0
  51. package/src/ingress/routes/echo.ts +107 -0
  52. package/src/ingress/routes/fallback.ts +23 -0
  53. package/src/ingress/routes/head.ts +27 -0
  54. package/src/ingress/routes/index.ts +11 -0
  55. package/src/ingress/routes/json-write.ts +129 -0
  56. package/src/ingress/routes/options.ts +26 -0
  57. package/src/ingress/routes/read.ts +35 -0
  58. package/src/ingress/routes/responder.ts +159 -0
  59. package/src/ingress/server-node.ts +376 -0
  60. package/src/ingress/server.ts +475 -0
  61. package/src/ingress/shared.ts +132 -0
  62. package/src/ingress/status.ts +97 -0
  63. package/src/ingress/types.ts +191 -0
  64. package/src/integration/batch.ts +58 -0
  65. package/src/integration/index.ts +29 -0
  66. package/src/integration/pipeline.ts +220 -0
  67. package/src/integration/streaming.ts +78 -0
  68. package/src/integration/websocket.ts +93 -0
  69. package/src/loader/batch.ts +137 -0
  70. package/src/loader/cost.ts +105 -0
  71. package/src/loader/index.ts +745 -0
  72. package/src/loader/ops.ts +657 -0
  73. package/src/loader/state.ts +84 -0
  74. package/src/loader/types.ts +145 -0
  75. package/src/native/ffi/constants.ts +58 -0
  76. package/src/native/ffi/selftest.ts +729 -0
  77. package/src/native/ffi/types.ts +474 -0
  78. package/src/native/ffi.ts +1775 -0
  79. package/src/native/index.ts +8 -207
  80. package/src/native/loader.ts +240 -0
  81. package/src/native/types/instances.ts +274 -0
  82. package/src/native/types.ts +401 -0
  83. package/src/runtime/builtins.ts +131 -0
  84. package/src/runtime/codec.ts +147 -0
  85. package/src/runtime/detect.ts +36 -0
  86. package/src/runtime/index.ts +28 -0
  87. package/src/runtime/native.ts +35 -0
  88. package/src/runtime/server.ts +31 -0
  89. package/src/runtime/transport.ts +64 -0
  90. package/src/runtime/types.ts +173 -0
  91. package/src/runtime/uuid.ts +45 -0
  92. package/src/runtime/websocket.ts +30 -0
  93. package/src/rust-ffi/addon.ts +10 -0
  94. package/src/rust-ffi/batch/build.ts +155 -0
  95. package/src/rust-ffi/batch/index.ts +8 -0
  96. package/src/rust-ffi/batch/types.ts +92 -0
  97. package/src/rust-ffi/client.ts +87 -0
  98. package/src/rust-ffi/context.ts +193 -0
  99. package/src/rust-ffi/index.ts +51 -541
  100. package/src/rust-ffi/into.ts +21 -0
  101. package/src/rust-ffi/options.ts +113 -0
  102. package/src/rust-ffi/packed.ts +253 -0
  103. package/src/rust-ffi/proven.ts +28 -0
  104. package/src/rust-ffi/scalar/crypto.ts +346 -0
  105. package/src/rust-ffi/scalar/factories.ts +459 -0
  106. package/src/rust-ffi/scalar/hashing.ts +74 -0
  107. package/src/rust-ffi/scalar/http.ts +337 -0
  108. package/src/rust-ffi/scalar/index.ts +31 -0
  109. package/src/rust-ffi/scalar/interface.ts +325 -0
  110. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  111. package/src/rust-ffi/scalar/json.ts +61 -0
  112. package/src/rust-ffi/scalar/payload.ts +241 -0
  113. package/src/rust-ffi/text.ts +121 -0
  114. package/src/selection.json +194 -0
  115. package/src/selection.ts +73 -0
  116. package/src/shared/adaptive.ts +67 -0
  117. package/src/shared/buffer-pool.ts +264 -0
  118. package/src/shared/bytes.ts +108 -9
  119. package/src/shared/codec.ts +78 -0
  120. package/src/shared/env.ts +20 -0
  121. package/src/shared/log.ts +75 -0
  122. package/src/shared/metrics.ts +247 -0
  123. package/src/shared/packed/index.ts +10 -0
  124. package/src/shared/packed/parsers.ts +31 -0
  125. package/src/shared/packed/schema.ts +23 -0
  126. package/src/shared/packed/wire.ts +504 -0
  127. package/src/shared/proven.ts +289 -0
  128. package/src/shared/request-id.ts +67 -0
  129. package/src/shared/response.ts +104 -0
  130. package/src/shared/runtime.ts +29 -0
  131. package/src/shared/trace.ts +81 -0
  132. package/src/shared/uuid.ts +15 -0
  133. package/Cargo.toml +0 -73
  134. package/bench.ts +0 -3
  135. package/rust/batch.rs +0 -195
  136. package/rust/cookie_parser.rs +0 -70
  137. package/rust/cors.rs +0 -263
  138. package/rust/hashing.rs +0 -164
  139. package/rust/headers.rs +0 -234
  140. package/rust/hmac_sha256.rs +0 -113
  141. package/rust/http_parser.rs +0 -132
  142. package/rust/ingress.rs +0 -616
  143. package/rust/ingress_constants.rs +0 -89
  144. package/rust/ip_trust.rs +0 -185
  145. package/rust/json_ops.rs +0 -66
  146. package/rust/json_patch_ops.rs +0 -25
  147. package/rust/json_schema.rs +0 -151
  148. package/rust/json_ser.rs +0 -429
  149. package/rust/lib.rs +0 -48
  150. package/rust/method.rs +0 -141
  151. package/rust/mime_lookup.rs +0 -76
  152. package/rust/output.rs +0 -238
  153. package/rust/proxy.rs +0 -90
  154. package/rust/query_parser.rs +0 -141
  155. package/rust/random_token.rs +0 -52
  156. package/rust/rate_limit.rs +0 -203
  157. package/rust/terminal.rs +0 -95
  158. package/rust/unit_tests.rs +0 -843
  159. package/rust/url_codec.rs +0 -133
  160. package/rust/util.rs +0 -537
  161. package/rust/validation.rs +0 -107
  162. package/rust/websocket.rs +0 -23
  163. package/src/baseline/index.ts +0 -12
  164. package/src/baseline/tasks/cookie.ts +0 -22
  165. package/src/baseline/tasks/hashing.ts +0 -18
  166. package/src/baseline/tasks/hmac.ts +0 -38
  167. package/src/baseline/tasks/http.ts +0 -62
  168. package/src/baseline/tasks/json-patch.ts +0 -12
  169. package/src/baseline/tasks/json.ts +0 -50
  170. package/src/baseline/tasks/mime.ts +0 -5
  171. package/src/baseline/tasks/query.ts +0 -14
  172. package/src/baseline/tasks/token.ts +0 -15
  173. package/src/baseline/tasks/url.ts +0 -11
  174. package/src/baseline/tasks/validation.ts +0 -19
  175. package/src/baseline/tasks/websocket.ts +0 -14
  176. package/src/bench/assert.ts +0 -70
  177. package/src/bench/checks.ts +0 -227
  178. package/src/bench/checksum.ts +0 -27
  179. package/src/bench/comparisons.ts +0 -70
  180. package/src/bench/concurrent-worker.ts +0 -135
  181. package/src/bench/fixtures.ts +0 -233
  182. package/src/bench/index.ts +0 -2
  183. package/src/bench/measure.ts +0 -177
  184. package/src/bench/now.ts +0 -3
  185. package/src/bench/report.ts +0 -79
  186. package/src/bench/run.ts +0 -57
  187. package/src/bench/tasks/complex.ts +0 -232
  188. package/src/bench/tasks/concurrent.ts +0 -213
  189. package/src/bench/tasks/cookie.ts +0 -44
  190. package/src/bench/tasks/hashing.ts +0 -33
  191. package/src/bench/tasks/hmac.ts +0 -36
  192. package/src/bench/tasks/http.ts +0 -50
  193. package/src/bench/tasks/index.ts +0 -52
  194. package/src/bench/tasks/json-patch.ts +0 -21
  195. package/src/bench/tasks/json.ts +0 -33
  196. package/src/bench/tasks/mime.ts +0 -21
  197. package/src/bench/tasks/query.ts +0 -44
  198. package/src/bench/tasks/stress.ts +0 -142
  199. package/src/bench/tasks/token.ts +0 -20
  200. package/src/bench/tasks/url.ts +0 -34
  201. package/src/bench/tasks/validation.ts +0 -57
  202. package/src/bench/tasks/websocket.ts +0 -21
  203. package/src/bench/types.ts +0 -60
  204. package/src/data/json-rows.ts +0 -33
  205. package/src/ingress/packed-input.ts +0 -59
  206. package/src/shared/json.ts +0 -18
  207. package/src/shared/packed.ts +0 -316
@@ -0,0 +1,504 @@
1
+ // src/shared/packed/wire.ts — Pure packed wire-format encode/decode helpers.
2
+ //
3
+ // The zero-allocation byte layouts shared by the batch/loader/rust-ffi layers:
4
+ // - unpackers: u32 arrays, bitsets, i64/u64 arrays, byte-results, multipart
5
+ // - packers: `[u32 count]{[u32 len][bytes]}` batches + string pairs
6
+ // - decoders: pairs, HTTP request, and the reusable pack-scratch pool
7
+ //
8
+ // This module is PURE (no addon dlopen): consumers that only need these
9
+ // helpers never touch the native layer. The native-backed ergonomic parsers
10
+ // live in ./parsers.ts.
11
+
12
+ import type { MultipartPart } from '../../native'
13
+ import { decoder, encoder, viewForArrayBuffer } from '../bytes'
14
+
15
+ function dataView(bytes: Uint8Array): DataView {
16
+ return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
17
+ }
18
+
19
+ function readU32(dv: DataView, offset: number): number {
20
+ if (offset + 4 > dv.byteLength) {
21
+ throw new RangeError('packed buffer: truncated u32')
22
+ }
23
+
24
+ return dv.getUint32(offset, true)
25
+ }
26
+
27
+ function readSlice(bytes: Uint8Array, offset: number, len: number): Uint8Array {
28
+ if (len < 0 || offset + len > bytes.byteLength) {
29
+ throw new RangeError('packed buffer: truncated bytes')
30
+ }
31
+
32
+ return bytes.subarray(offset, offset + len)
33
+ }
34
+
35
+ /**
36
+ * Unpack a packed u32 array: `[u32 count]{[u32 value]}` → `Uint32Array`.
37
+ * @throws {RangeError} when the buffer is shorter than `count` × 4 bytes.
38
+ */
39
+ export function unpackU32Array(bytes: Uint8Array): Uint32Array {
40
+ if (bytes.byteLength < 4) {
41
+ return new Uint32Array(0)
42
+ }
43
+
44
+ const dv = dataView(bytes)
45
+ const count = readU32(dv, 0)
46
+
47
+ if (count > Math.floor((bytes.byteLength - 4) / 4)) {
48
+ throw new RangeError('packed buffer: invalid u32 count')
49
+ }
50
+
51
+ const out = new Uint32Array(count)
52
+
53
+ for (let i = 0; i < count; i++) {
54
+ out[i] = readU32(dv, 4 + i * 4)
55
+ }
56
+
57
+ return out
58
+ }
59
+
60
+ /**
61
+ * Unpack a packed bitset: `[u32 count]{bit per item}` → one 0/1 byte per item.
62
+ * @throws {RangeError} when the buffer is shorter than `count` bits.
63
+ */
64
+ export function unpackBitset(bytes: Uint8Array): Uint8Array {
65
+ if (bytes.byteLength < 4) {
66
+ return new Uint8Array(0)
67
+ }
68
+
69
+ const dv = dataView(bytes)
70
+ const count = readU32(dv, 0)
71
+ const expectedBytes = Math.ceil(count / 8)
72
+
73
+ if (bytes.byteLength < 4 + expectedBytes) {
74
+ throw new RangeError('packed buffer: truncated bitset')
75
+ }
76
+
77
+ const bits = bytes.subarray(4)
78
+ const out = new Uint8Array(count)
79
+
80
+ for (let i = 0; i < count; i++) {
81
+ const byte = bits[i >> 3] ?? 0
82
+ out[i] = (byte >> (i & 7)) & 1
83
+ }
84
+
85
+ return out
86
+ }
87
+
88
+ /**
89
+ * Unpack a packed i64 array as SIGNED `BigInt64Array`.
90
+ * @throws {RangeError} when the buffer is shorter than `count` × 8 bytes.
91
+ */
92
+ export function unpackI64ArrayAsBigInt(bytes: Uint8Array): BigInt64Array {
93
+ if (bytes.byteLength < 4) {
94
+ return new BigInt64Array(0)
95
+ }
96
+
97
+ const dv = dataView(bytes)
98
+ const count = readU32(dv, 0)
99
+
100
+ if (count > Math.floor((bytes.byteLength - 4) / 8)) {
101
+ throw new RangeError('packed buffer: invalid i64 count')
102
+ }
103
+
104
+ const out = new BigInt64Array(count)
105
+
106
+ for (let i = 0; i < count; i++) {
107
+ out[i] = dv.getBigInt64(4 + i * 8, true)
108
+ }
109
+
110
+ return out
111
+ }
112
+
113
+ /**
114
+ * Unpack a packed i64 array as UNSIGNED `BigUint64Array`. Used by hash
115
+ * batches (e.g. `fnv1a64`) so the bulk result matches the unsigned `bigint`
116
+ * the scalar FFI returns.
117
+ */
118
+ export function unpackU64ArrayAsBigInt(bytes: Uint8Array): BigUint64Array {
119
+ if (bytes.byteLength < 4) {
120
+ return new BigUint64Array(0)
121
+ }
122
+
123
+ const dv = dataView(bytes)
124
+ const count = readU32(dv, 0)
125
+
126
+ if (count > Math.floor((bytes.byteLength - 4) / 8)) {
127
+ throw new RangeError('packed buffer: invalid i64 count')
128
+ }
129
+
130
+ const out = new BigUint64Array(count)
131
+
132
+ for (let i = 0; i < count; i++) {
133
+ out[i] = dv.getBigUint64(4 + i * 8, true)
134
+ }
135
+
136
+ return out
137
+ }
138
+
139
+ /**
140
+ * Unpack a packed byte-results buffer: `[u32 count]{[u32 len][bytes]}` → one
141
+ * `Uint8Array` slice per item. The slices reference `bytes`'s backing store
142
+ * (zero-copy) — copy them if held beyond the buffer's lifetime.
143
+ */
144
+ export function unpackByteResults(bytes: Uint8Array): Uint8Array[] {
145
+ if (bytes.byteLength < 4) {
146
+ return []
147
+ }
148
+
149
+ const dv = dataView(bytes)
150
+ const count = readU32(dv, 0)
151
+
152
+ const out: Uint8Array[] = []
153
+ let offset = 4
154
+
155
+ for (let i = 0; i < count; i++) {
156
+ const len = readU32(dv, offset)
157
+ offset += 4
158
+
159
+ const slice = readSlice(bytes, offset, len)
160
+ offset += len
161
+
162
+ out.push(slice)
163
+ }
164
+
165
+ return out
166
+ }
167
+
168
+ /**
169
+ * Decode a packed multipart batch: `[u32 item_count]{[u32 parts_len]
170
+ * [parts_packed]}` where each `parts_packed` is
171
+ * `[u32 part_count]{[u32 name_len][name][u8 has_filename][u32 filename_len]
172
+ * [filename][u8 has_ct][u32 ct_len][ct][u32 data_len][data]}`. Returns one
173
+ * array of parts per input item.
174
+ */
175
+ export function unpackMultipartParts(bytes: Uint8Array): MultipartPart[][] {
176
+ if (bytes.byteLength < 4) {
177
+ return []
178
+ }
179
+
180
+ const dv = dataView(bytes)
181
+ const itemCount = readU32(dv, 0)
182
+ const out: MultipartPart[][] = []
183
+ let offset = 4
184
+
185
+ for (let i = 0; i < itemCount; i++) {
186
+ const partsLen = readU32(dv, offset)
187
+ offset += 4
188
+ const partsBytes = readSlice(bytes, offset, partsLen)
189
+ offset += partsLen
190
+ out.push(readMultipartPartsPacked(partsBytes))
191
+ }
192
+
193
+ return out
194
+ }
195
+
196
+ /** Decode a single `[u32 part_count]{…parts…}` packed buffer into objects. */
197
+ function readMultipartPartsPacked(bytes: Uint8Array): MultipartPart[] {
198
+ if (bytes.byteLength < 4) {
199
+ return []
200
+ }
201
+
202
+ const dv = dataView(bytes)
203
+ const count = readU32(dv, 0)
204
+ const out: MultipartPart[] = []
205
+ let offset = 4
206
+
207
+ for (let i = 0; i < count; i++) {
208
+ const nameLen = readU32(dv, offset)
209
+ offset += 4
210
+ const name = decoder.decode(readSlice(bytes, offset, nameLen))
211
+ offset += nameLen
212
+
213
+ const hasFilename = bytes[offset] ?? 0
214
+ offset += 1
215
+ const filenameLen = readU32(dv, offset)
216
+ offset += 4
217
+ const filenameBytes = readSlice(bytes, offset, filenameLen)
218
+ offset += filenameLen
219
+
220
+ const hasCt = bytes[offset] ?? 0
221
+ offset += 1
222
+ const ctLen = readU32(dv, offset)
223
+ offset += 4
224
+ const ctBytes = readSlice(bytes, offset, ctLen)
225
+ offset += ctLen
226
+
227
+ const dataLen = readU32(dv, offset)
228
+ offset += 4
229
+ const data = readSlice(bytes, offset, dataLen)
230
+ offset += dataLen
231
+
232
+ out.push({
233
+ name,
234
+ filename: hasFilename ? decoder.decode(filenameBytes) : null,
235
+ contentType: hasCt ? decoder.decode(ctBytes) : null,
236
+ data,
237
+ })
238
+ }
239
+
240
+ return out
241
+ }
242
+
243
+ /** A decoded `[name, value]` pair from a packed buffer. */
244
+ export type Pair = [string, string]
245
+
246
+ /**
247
+ * Decode a packed `[u32 count][u32 keyLen][key][u32 valLen][val]...` buffer
248
+ * into `[name, value]` string pairs (see `packPairs` for the layout).
249
+ */
250
+ export function readPairsPacked(bytes: Uint8Array): Pair[] {
251
+ if (bytes.byteLength < 4) {
252
+ return []
253
+ }
254
+
255
+ const dv = dataView(bytes)
256
+ const count = readU32(dv, 0)
257
+
258
+ if (count === 0) {
259
+ return []
260
+ }
261
+
262
+ const out: Pair[] = []
263
+ let offset = 4
264
+
265
+ for (let i = 0; i < count; i++) {
266
+ const keyLen = readU32(dv, offset)
267
+ offset += 4
268
+
269
+ const keyBytes = readSlice(bytes, offset, keyLen)
270
+ offset += keyLen
271
+
272
+ const valueLen = readU32(dv, offset)
273
+ offset += 4
274
+
275
+ const valueBytes = readSlice(bytes, offset, valueLen)
276
+ offset += valueLen
277
+
278
+ out.push([decoder.decode(keyBytes), decoder.decode(valueBytes)])
279
+ }
280
+
281
+ return out
282
+ }
283
+
284
+ /**
285
+ * Fold `[name, value]` pairs into an object; repeated names collect into a
286
+ * string array (query-string/cookie semantics).
287
+ */
288
+ export function pairsToObject(pairs: Pair[]): Record<string, string | string[]> {
289
+ const out: Record<string, string | string[]> = {}
290
+
291
+ for (const [key, value] of pairs) {
292
+ const existing = out[key]
293
+
294
+ if (existing === undefined) {
295
+ out[key] = value
296
+ } else if (Array.isArray(existing)) {
297
+ existing.push(value)
298
+ } else {
299
+ out[key] = [existing, value]
300
+ }
301
+ }
302
+
303
+ return out
304
+ }
305
+
306
+ /** A decoded HTTP request from the native packed parser. */
307
+ export interface ParsedHttpRequestPacked {
308
+ method: string
309
+ path: string
310
+ version: string
311
+ headers: Record<string, string>
312
+ }
313
+
314
+ /**
315
+ * Decode a native `httpParseRequestPacked` buffer into a plain request object
316
+ * (zero-alloc on the Rust side; this builds the JS strings once).
317
+ */
318
+ export function readHttpPacked(bytes: Uint8Array): ParsedHttpRequestPacked {
319
+ const dv = dataView(bytes)
320
+ let offset = 0
321
+
322
+ const methodLen = readU32(dv, offset)
323
+ offset += 4
324
+ const method = decoder.decode(readSlice(bytes, offset, methodLen))
325
+ offset += methodLen
326
+
327
+ const pathLen = readU32(dv, offset)
328
+ offset += 4
329
+ const path = decoder.decode(readSlice(bytes, offset, pathLen))
330
+ offset += pathLen
331
+
332
+ const versionLen = readU32(dv, offset)
333
+ offset += 4
334
+ const version = decoder.decode(readSlice(bytes, offset, versionLen))
335
+ offset += versionLen
336
+
337
+ const headerCount = readU32(dv, offset)
338
+ offset += 4
339
+
340
+ const headers: Record<string, string> = {}
341
+
342
+ for (let i = 0; i < headerCount; i++) {
343
+ const nameLen = readU32(dv, offset)
344
+ offset += 4
345
+ const name = decoder.decode(readSlice(bytes, offset, nameLen))
346
+ offset += nameLen
347
+
348
+ const valueLen = readU32(dv, offset)
349
+ offset += 4
350
+ const value = decoder.decode(readSlice(bytes, offset, valueLen))
351
+ offset += valueLen
352
+
353
+ headers[name] = value
354
+ }
355
+
356
+ return {
357
+ method,
358
+ path,
359
+ version,
360
+ headers,
361
+ }
362
+ }
363
+
364
+ /** Total packed bytes for an item list: `[u32 count] { [u32 len][bytes] }`. */
365
+ export function packedTotal(items: readonly Uint8Array[]): number {
366
+ let total = 4
367
+ for (const item of items) {
368
+ total += 4 + item.byteLength
369
+ }
370
+ return total
371
+ }
372
+
373
+ /**
374
+ * Write `[u32 count] { [u32 len][bytes] }` for `items` into `target` starting
375
+ * at `offset`, returning the offset just past the written bytes. Throws
376
+ * `RangeError` when `target` is too small. Pass a reusable `DataView` (see
377
+ * `viewForArrayBuffer`) to avoid re-creating one per hot call.
378
+ */
379
+ export function writePack(
380
+ items: readonly Uint8Array[],
381
+ target: Uint8Array,
382
+ offset = 0,
383
+ dv?: DataView,
384
+ ): number {
385
+ const end = offset + packedTotal(items)
386
+ if (end > target.byteLength) {
387
+ throw new RangeError(`packed buffer: target too small (${target.byteLength} < ${end})`)
388
+ }
389
+ const view = dv ?? viewForArrayBuffer(target.buffer, target.byteOffset)
390
+ view.setUint32(offset, items.length, true)
391
+ let pos = offset + 4
392
+ for (const item of items) {
393
+ view.setUint32(pos, item.byteLength, true)
394
+ pos += 4
395
+ target.set(item, pos)
396
+ pos += item.byteLength
397
+ }
398
+ return end
399
+ }
400
+
401
+ /**
402
+ * Build a packed batch buffer. Returns an OWNED buffer (safe to hold across
403
+ * later calls) — this is the public entry point. Hot internal paths that can
404
+ * guarantee synchronous consumption should use `withPackScratch` instead.
405
+ */
406
+ export function packBatch(items: Uint8Array[]): Uint8Array {
407
+ const out = new Uint8Array(packedTotal(items))
408
+ writePack(items, out)
409
+ return out
410
+ }
411
+
412
+ // ── Reusable packed-input scratch (hot batch path) ──────────────────
413
+ // Native batch calls read the packed buffer synchronously and return an
414
+ // independent result, so the input buffer may be reused across calls. A small
415
+ // free-list keeps the steady-state hot path allocation-free. The caller MUST
416
+ // NOT retain the subarray handed to `fn` past that call.
417
+
418
+ const packScratchFree: Uint8Array[] = []
419
+
420
+ function acquirePackScratch(min: number): Uint8Array {
421
+ const buf = packScratchFree.pop() ?? new Uint8Array(0)
422
+ if (buf.byteLength < min) {
423
+ let size = Math.max(256, buf.byteLength * 2)
424
+ while (size < min) size *= 2
425
+ return new Uint8Array(size)
426
+ }
427
+ return buf
428
+ }
429
+
430
+ function releasePackScratch(buf: Uint8Array): void {
431
+ packScratchFree.push(buf)
432
+ }
433
+
434
+ /**
435
+ * Pack `items` into a reusable scratch and call `fn(packed)`, returning its
436
+ * result. The scratch is returned to the pool when `fn` returns (or throws).
437
+ */
438
+ export function withPackScratch<T>(items: readonly Uint8Array[], fn: (packed: Uint8Array) => T): T {
439
+ const buf = acquirePackScratch(packedTotal(items))
440
+ const end = writePack(items, buf)
441
+ try {
442
+ return fn(buf.subarray(0, end))
443
+ } finally {
444
+ releasePackScratch(buf)
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Pack TWO item lists into two reusable scratches and call `fn(a, b)` — for
450
+ * paired ops (jsonPatch / hmacSha256Verify / passwordVerify / urlResolve) that
451
+ * feed two packed lists to one native call. Safe because the native call
452
+ * consumes both inputs synchronously.
453
+ */
454
+ export function withPackScratch2<T>(
455
+ a: readonly Uint8Array[],
456
+ b: readonly Uint8Array[],
457
+ fn: (packedA: Uint8Array, packedB: Uint8Array) => T,
458
+ ): T {
459
+ const bufA = acquirePackScratch(packedTotal(a))
460
+ const bufB = acquirePackScratch(packedTotal(b))
461
+ const endA = writePack(a, bufA)
462
+ const endB = writePack(b, bufB)
463
+ try {
464
+ return fn(bufA.subarray(0, endA), bufB.subarray(0, endB))
465
+ } finally {
466
+ releasePackScratch(bufA)
467
+ releasePackScratch(bufB)
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Pack `[name, value]` string pairs into the native `[u32 count]...` layout
473
+ * that `readPairsPacked` / the batch validators consume.
474
+ */
475
+ export function packPairs(pairs: Array<[string, string]>): Uint8Array {
476
+ const encoded = pairs.map(([key, value]) => [encoder.encode(key), encoder.encode(value)] as const)
477
+
478
+ let total = 4
479
+
480
+ for (const [key, value] of encoded) {
481
+ total += 8 + key.byteLength + value.byteLength
482
+ }
483
+
484
+ const out = new Uint8Array(total)
485
+ const dv = new DataView(out.buffer)
486
+
487
+ dv.setUint32(0, encoded.length, true)
488
+
489
+ let offset = 4
490
+
491
+ for (const [key, value] of encoded) {
492
+ dv.setUint32(offset, key.byteLength, true)
493
+ offset += 4
494
+ out.set(key, offset)
495
+ offset += key.byteLength
496
+
497
+ dv.setUint32(offset, value.byteLength, true)
498
+ offset += 4
499
+ out.set(value, offset)
500
+ offset += value.byteLength
501
+ }
502
+
503
+ return out
504
+ }