castrum 0.7.0 → 0.9.0

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