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.
- package/LICENSE +21 -0
- package/README.md +322 -59
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +3161 -0
- package/dist/index.js +7306 -0
- package/docs/ARCHITECTURE.md +415 -0
- package/docs/BENCHMARKS.md +307 -0
- package/docs/ENVIRONMENT.md +79 -0
- package/docs/GETTING_STARTED.md +209 -0
- package/docs/INGRESS.md +745 -0
- package/docs/REPO_MAP.md +259 -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 +42 -0
- package/docs/bun-builtins-decision-matrix.md +108 -0
- package/index.ts +51 -8
- package/package.json +73 -20
- package/src/data/json-rows.ts +13 -13
- package/src/ingress/body.ts +149 -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 +563 -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 +67 -0
- package/src/ingress/index.ts +130 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +75 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +150 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +96 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/routes/common.ts +38 -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 +37 -0
- package/src/ingress/routes/index.ts +10 -0
- package/src/ingress/routes/json-write.ts +153 -0
- package/src/ingress/routes/options.ts +36 -0
- package/src/ingress/routes/read.ts +45 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +443 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +157 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +217 -0
- package/src/integration/streaming.ts +79 -0
- package/src/integration/websocket.ts +92 -0
- package/src/loader/batch.ts +133 -0
- package/src/loader/cost.ts +104 -0
- package/src/loader/index.ts +930 -0
- package/src/loader/ops.ts +657 -0
- package/src/native/ffi.ts +1541 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +194 -0
- package/src/native/types.ts +598 -0
- package/src/rust-ffi/addon.ts +10 -0
- package/src/rust-ffi/batch.ts +431 -0
- package/src/rust-ffi/client.ts +88 -0
- package/src/rust-ffi/context.ts +164 -0
- package/src/rust-ffi/index.ts +40 -541
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +42 -0
- package/src/rust-ffi/scalar/crypto.ts +265 -0
- package/src/rust-ffi/scalar/factories.ts +233 -0
- package/src/rust-ffi/scalar/hashing.ts +72 -0
- package/src/rust-ffi/scalar/http.ts +206 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +450 -0
- package/src/rust-ffi/scalar/json.ts +45 -0
- package/src/rust-ffi/scalar/payload.ts +124 -0
- package/src/rust-ffi/text.ts +95 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +77 -0
- package/src/shared/adaptive.ts +68 -0
- package/src/shared/bench-classify.ts +209 -0
- package/src/shared/buffer-pool.ts +209 -0
- package/src/shared/bytes.ts +35 -5
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +74 -0
- package/src/shared/metrics.ts +247 -0
- package/src/shared/packed.ts +350 -154
- package/src/shared/proven.ts +596 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +34 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +31 -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/ingress/packed-input.ts +0 -59
- package/src/shared/json.ts +0 -18
package/src/shared/packed.ts
CHANGED
|
@@ -1,218 +1,336 @@
|
|
|
1
|
-
import { decoder, encoder } from
|
|
2
|
-
import
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
121
|
+
throw new RangeError('packed buffer: invalid i64 count')
|
|
82
122
|
}
|
|
83
123
|
|
|
84
|
-
const out = new
|
|
124
|
+
const out = new BigUint64Array(count)
|
|
85
125
|
|
|
86
126
|
for (let i = 0; i < count; i++) {
|
|
87
|
-
out[i] = dv.
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
398
|
+
const packScratchFree: Uint8Array[] = []
|
|
237
399
|
|
|
238
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
410
|
+
function releasePackScratch(buf: Uint8Array): void {
|
|
411
|
+
packScratchFree.push(buf)
|
|
412
|
+
}
|
|
243
413
|
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
)
|
|
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
|
-
|
|
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
|
|
306
|
-
)
|
|
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
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
}
|