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.
- package/LICENSE +21 -0
- package/README.md +154 -368
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +2763 -0
- package/dist/index.js +8707 -0
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +507 -0
- package/docs/BENCHMARKS.md +343 -0
- package/docs/CASE_STUDY.md +240 -0
- package/docs/ENVIRONMENT.md +81 -0
- package/docs/FFI_BUN_GUIDE.md +327 -0
- package/docs/GETTING_STARTED.md +192 -0
- package/docs/INGRESS-ROUTER.md +81 -0
- package/docs/INGRESS.md +746 -0
- package/docs/NATIVE-ROUTE.md +53 -0
- package/docs/REPO_MAP.md +279 -0
- package/docs/adr/0001-two-wire-formats.md +39 -0
- package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
- package/docs/adr/0003-bun-builtins-delegation.md +43 -0
- package/docs/bun-builtins-decision-matrix.md +115 -0
- package/index.ts +44 -8
- package/package.json +98 -33
- package/src/ingress/body.ts +289 -0
- package/src/ingress/constants.ts +220 -48
- package/src/ingress/context.ts +136 -0
- package/src/ingress/decode/baked-result.ts +121 -0
- package/src/ingress/decode/fast-result.ts +160 -0
- package/src/ingress/decode/packed-sections.ts +56 -0
- package/src/ingress/decode/result-base.ts +172 -0
- package/src/ingress/errors.ts +72 -0
- package/src/ingress/fast.ts +152 -1021
- package/src/ingress/handlers.ts +639 -1001
- package/src/ingress/headers/baked-templates.ts +102 -0
- package/src/ingress/headers/cors.ts +44 -0
- package/src/ingress/headers/fast-templates.ts +238 -0
- package/src/ingress/headers/hsts.ts +83 -0
- package/src/ingress/health.ts +68 -0
- package/src/ingress/index.ts +142 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +139 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +164 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +102 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/router.ts +237 -0
- package/src/ingress/routes/common.ts +79 -0
- package/src/ingress/routes/delete.ts +14 -0
- package/src/ingress/routes/echo.ts +107 -0
- package/src/ingress/routes/fallback.ts +23 -0
- package/src/ingress/routes/head.ts +27 -0
- package/src/ingress/routes/index.ts +11 -0
- package/src/ingress/routes/json-write.ts +129 -0
- package/src/ingress/routes/options.ts +26 -0
- package/src/ingress/routes/read.ts +35 -0
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +475 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +191 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +220 -0
- package/src/integration/streaming.ts +78 -0
- package/src/integration/websocket.ts +93 -0
- package/src/loader/batch.ts +137 -0
- package/src/loader/cost.ts +105 -0
- package/src/loader/index.ts +745 -0
- package/src/loader/ops.ts +657 -0
- package/src/loader/state.ts +84 -0
- package/src/loader/types.ts +145 -0
- package/src/native/ffi/constants.ts +58 -0
- package/src/native/ffi/selftest.ts +729 -0
- package/src/native/ffi/types.ts +474 -0
- package/src/native/ffi.ts +1775 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +240 -0
- package/src/native/types/instances.ts +274 -0
- package/src/native/types.ts +401 -0
- 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 +10 -0
- 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 +87 -0
- package/src/rust-ffi/context.ts +193 -0
- package/src/rust-ffi/index.ts +51 -541
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +28 -0
- package/src/rust-ffi/scalar/crypto.ts +346 -0
- package/src/rust-ffi/scalar/factories.ts +459 -0
- package/src/rust-ffi/scalar/hashing.ts +74 -0
- package/src/rust-ffi/scalar/http.ts +337 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +325 -0
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +61 -0
- package/src/rust-ffi/scalar/payload.ts +241 -0
- package/src/rust-ffi/text.ts +121 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +73 -0
- package/src/shared/adaptive.ts +67 -0
- package/src/shared/buffer-pool.ts +264 -0
- package/src/shared/bytes.ts +108 -9
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +75 -0
- package/src/shared/metrics.ts +247 -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/wire.ts +504 -0
- package/src/shared/proven.ts +289 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +29 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +15 -0
- package/Cargo.toml +0 -73
- package/bench.ts +0 -3
- package/rust/batch.rs +0 -195
- package/rust/cookie_parser.rs +0 -70
- package/rust/cors.rs +0 -263
- package/rust/hashing.rs +0 -164
- package/rust/headers.rs +0 -234
- package/rust/hmac_sha256.rs +0 -113
- package/rust/http_parser.rs +0 -132
- package/rust/ingress.rs +0 -616
- package/rust/ingress_constants.rs +0 -89
- package/rust/ip_trust.rs +0 -185
- package/rust/json_ops.rs +0 -66
- package/rust/json_patch_ops.rs +0 -25
- package/rust/json_schema.rs +0 -151
- package/rust/json_ser.rs +0 -429
- package/rust/lib.rs +0 -48
- package/rust/method.rs +0 -141
- package/rust/mime_lookup.rs +0 -76
- package/rust/output.rs +0 -238
- package/rust/proxy.rs +0 -90
- package/rust/query_parser.rs +0 -141
- package/rust/random_token.rs +0 -52
- package/rust/rate_limit.rs +0 -203
- package/rust/terminal.rs +0 -95
- package/rust/unit_tests.rs +0 -843
- package/rust/url_codec.rs +0 -133
- package/rust/util.rs +0 -537
- package/rust/validation.rs +0 -107
- package/rust/websocket.rs +0 -23
- package/src/baseline/index.ts +0 -12
- package/src/baseline/tasks/cookie.ts +0 -22
- package/src/baseline/tasks/hashing.ts +0 -18
- package/src/baseline/tasks/hmac.ts +0 -38
- package/src/baseline/tasks/http.ts +0 -62
- package/src/baseline/tasks/json-patch.ts +0 -12
- package/src/baseline/tasks/json.ts +0 -50
- package/src/baseline/tasks/mime.ts +0 -5
- package/src/baseline/tasks/query.ts +0 -14
- package/src/baseline/tasks/token.ts +0 -15
- package/src/baseline/tasks/url.ts +0 -11
- package/src/baseline/tasks/validation.ts +0 -19
- package/src/baseline/tasks/websocket.ts +0 -14
- package/src/bench/assert.ts +0 -70
- package/src/bench/checks.ts +0 -227
- package/src/bench/checksum.ts +0 -27
- package/src/bench/comparisons.ts +0 -70
- package/src/bench/concurrent-worker.ts +0 -135
- package/src/bench/fixtures.ts +0 -233
- package/src/bench/index.ts +0 -2
- package/src/bench/measure.ts +0 -177
- package/src/bench/now.ts +0 -3
- package/src/bench/report.ts +0 -79
- package/src/bench/run.ts +0 -57
- package/src/bench/tasks/complex.ts +0 -232
- package/src/bench/tasks/concurrent.ts +0 -213
- package/src/bench/tasks/cookie.ts +0 -44
- package/src/bench/tasks/hashing.ts +0 -33
- package/src/bench/tasks/hmac.ts +0 -36
- package/src/bench/tasks/http.ts +0 -50
- package/src/bench/tasks/index.ts +0 -52
- package/src/bench/tasks/json-patch.ts +0 -21
- package/src/bench/tasks/json.ts +0 -33
- package/src/bench/tasks/mime.ts +0 -21
- package/src/bench/tasks/query.ts +0 -44
- package/src/bench/tasks/stress.ts +0 -142
- package/src/bench/tasks/token.ts +0 -20
- package/src/bench/tasks/url.ts +0 -34
- package/src/bench/tasks/validation.ts +0 -57
- package/src/bench/tasks/websocket.ts +0 -21
- package/src/bench/types.ts +0 -60
- package/src/data/json-rows.ts +0 -33
- package/src/ingress/packed-input.ts +0 -59
- package/src/shared/json.ts +0 -18
- 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
|
+
}
|