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
@@ -1,20 +0,0 @@
1
- import * as native from "../../baseline";
2
- import { rust } from "../../rust-ffi";
3
- import type { BenchTask } from "../types";
4
-
5
- export function tokenTasks(): BenchTask[] {
6
- return [
7
- {
8
- name: "native:random_token",
9
- run: () => native.nativeRandomToken(32).byteLength,
10
- iterations: 1000,
11
- warmup: 100,
12
- },
13
- {
14
- name: "rust:random_token",
15
- run: () => rust.randomToken(32).byteLength,
16
- iterations: 1000,
17
- warmup: 100,
18
- },
19
- ];
20
- }
@@ -1,34 +0,0 @@
1
- import * as native from "../../baseline";
2
- import { rust } from "../../rust-ffi";
3
- import type { BenchFixtures } from "../fixtures";
4
- import type { BenchTask } from "../types";
5
-
6
- export function urlTasks(f: BenchFixtures): BenchTask[] {
7
- return [
8
- {
9
- name: "native:url_encode",
10
- run: () => native.nativeUrlEncode("hello world & foo=bar").length,
11
- iterations: 1000,
12
- warmup: 100,
13
- },
14
- {
15
- name: "rust:url_encode",
16
- run: () => rust.urlEncode(f.urlEncodeInput).byteLength,
17
- iterations: 1000,
18
- warmup: 100,
19
- },
20
- {
21
- name: "native:url_decode",
22
- run: () =>
23
- native.nativeUrlDecode("hello%20world%20%26%20foo%3Dbar").length,
24
- iterations: 1000,
25
- warmup: 100,
26
- },
27
- {
28
- name: "rust:url_decode",
29
- run: () => rust.urlDecode(f.urlDecodeInput).byteLength,
30
- iterations: 1000,
31
- warmup: 100,
32
- },
33
- ];
34
- }
@@ -1,57 +0,0 @@
1
- import * as native from "../../baseline";
2
- import { rust } from "../../rust-ffi";
3
- import type { BenchFixtures } from "../fixtures";
4
- import type { BenchTask } from "../types";
5
-
6
- export function validationTasks(f: BenchFixtures): BenchTask[] {
7
- return [
8
- {
9
- name: "native:validate_email",
10
- run: () => (native.nativeValidateEmail(f.emailOk) ? 1 : 0),
11
- iterations: 1000,
12
- warmup: 100,
13
- },
14
- {
15
- name: "rust:validate_email",
16
- run: () => rust.validateEmail(f.emailOk),
17
- iterations: 1000,
18
- warmup: 100,
19
- },
20
- {
21
- name: "native:validate_uuid",
22
- run: () => (native.nativeValidateUuid(f.uuidOk) ? 1 : 0),
23
- iterations: 1000,
24
- warmup: 100,
25
- },
26
- {
27
- name: "rust:validate_uuid",
28
- run: () => rust.validateUuid(f.uuidOk),
29
- iterations: 1000,
30
- warmup: 100,
31
- },
32
- {
33
- name: "native:validate_ipv4",
34
- run: () => (native.nativeValidateIpv4(f.ipv4Ok) ? 1 : 0),
35
- iterations: 1000,
36
- warmup: 100,
37
- },
38
- {
39
- name: "rust:validate_ipv4",
40
- run: () => rust.validateIpv4(f.ipv4Ok),
41
- iterations: 1000,
42
- warmup: 100,
43
- },
44
- {
45
- name: "native:validate_ipv6",
46
- run: () => (native.nativeValidateIpv6(f.ipv6Ok) ? 1 : 0),
47
- iterations: 1000,
48
- warmup: 100,
49
- },
50
- {
51
- name: "rust:validate_ipv6",
52
- run: () => rust.validateIpv6(f.ipv6Ok),
53
- iterations: 1000,
54
- warmup: 100,
55
- },
56
- ];
57
- }
@@ -1,21 +0,0 @@
1
- import * as native from "../../baseline";
2
- import { rust } from "../../rust-ffi";
3
- import type { BenchFixtures } from "../fixtures";
4
- import type { BenchTask } from "../types";
5
-
6
- export function websocketTasks(f: BenchFixtures): BenchTask[] {
7
- return [
8
- {
9
- name: "native:ws_accept_key",
10
- run: () => native.nativeWsAcceptKey(f.wsKey).byteLength,
11
- iterations: 1000,
12
- warmup: 100,
13
- },
14
- {
15
- name: "rust:ws_accept_key",
16
- run: () => rust.wsAcceptKey(f.wsKeyBytes).byteLength,
17
- iterations: 1000,
18
- warmup: 100,
19
- },
20
- ];
21
- }
@@ -1,60 +0,0 @@
1
- export interface BenchResult {
2
- name: string;
3
- iterations: number;
4
- avgMs: number;
5
- p50Ms: number;
6
- p95Ms: number;
7
- opsPerSec: number;
8
- checksum: string;
9
- }
10
-
11
- export interface ConcurrentBenchResult {
12
- name: string;
13
- iterations: number;
14
- concurrency: number;
15
- totalOps: number;
16
- totalMs: number;
17
- avgMs: number;
18
- p50Ms: number;
19
- p95Ms: number;
20
- opsPerSec: number;
21
- checksum: string;
22
- }
23
-
24
- export interface StressBenchResult {
25
- name: string;
26
- durationMs: number;
27
- totalOps: number;
28
- opsPerSec: number;
29
- avgMs: number;
30
- checksum: string;
31
- }
32
-
33
- export interface BenchTask {
34
- name: string;
35
- run: () => unknown;
36
- iterations: number;
37
- warmup: number;
38
- }
39
-
40
- export interface ConcurrentBenchTask {
41
- name: string;
42
- op: string;
43
- payload: unknown;
44
- concurrency: number;
45
- iterationsPerSlot: number;
46
- warmupPerSlot: number;
47
- }
48
-
49
- export interface StressBenchTask {
50
- name: string;
51
- run: () => unknown;
52
- durationMs: number;
53
- warmupMs: number;
54
- }
55
-
56
- export interface ComparisonReport {
57
- label: string;
58
- nativeName: string;
59
- rustName: string;
60
- }
@@ -1,33 +0,0 @@
1
- import { encoder } from "../shared/bytes";
2
-
3
- export interface JsonRowNested {
4
- version: number;
5
- createdAt: string;
6
- }
7
-
8
- export interface JsonRow {
9
- id: number;
10
- name: string;
11
- active: boolean;
12
- score: number;
13
- tags: string[];
14
- nested: JsonRowNested;
15
- }
16
-
17
- export function createJsonRows(rows: number): JsonRow[] {
18
- return Array.from({ length: rows }, (_, i) => ({
19
- id: i,
20
- name: `user_${i}`,
21
- active: i % 2 === 0,
22
- score: i * 1.25,
23
- tags: ["alpha", "beta", "gamma"],
24
- nested: {
25
- version: i % 10,
26
- createdAt: "2026-01-01T00:00:00Z",
27
- },
28
- }));
29
- }
30
-
31
- export function jsonRowsBytes(rows: number): Uint8Array {
32
- return encoder.encode(JSON.stringify(createJsonRows(rows)));
33
- }
@@ -1,59 +0,0 @@
1
- export class IngressInputPacker {
2
- private buf: Uint8Array;
3
- private view: DataView;
4
- private pos = 0;
5
-
6
- constructor(initialSize = 65536) {
7
- this.buf = new Uint8Array(Math.max(1024, initialSize));
8
- this.view = new DataView(this.buf.buffer);
9
- }
10
-
11
- private ensure(additional: number): void {
12
- const needed = this.pos + additional;
13
- if (needed <= this.buf.byteLength) return;
14
-
15
- let nextSize = this.buf.byteLength * 2;
16
- while (nextSize < needed) {
17
- nextSize *= 2;
18
- }
19
-
20
- const next = new Uint8Array(nextSize);
21
- next.set(this.buf.subarray(0, this.pos));
22
-
23
- this.buf = next;
24
- this.view = new DataView(next.buffer);
25
- }
26
-
27
- private writeU8(value: number): void {
28
- this.ensure(1);
29
- this.buf[this.pos++] = value & 0xff;
30
- }
31
-
32
- private writeLenPrefixed(bytes: Uint8Array): void {
33
- this.ensure(4 + bytes.byteLength);
34
-
35
- this.view.setUint32(this.pos, bytes.byteLength, true);
36
- this.pos += 4;
37
-
38
- this.buf.set(bytes, this.pos);
39
- this.pos += bytes.byteLength;
40
- }
41
-
42
- pack(
43
- methodKind: number,
44
- urlBytes: Uint8Array,
45
- ipBytes: Uint8Array,
46
- requestIdBytes: Uint8Array,
47
- headers: Uint8Array,
48
- ): Uint8Array {
49
- this.pos = 0;
50
-
51
- this.writeU8(methodKind);
52
- this.writeLenPrefixed(urlBytes);
53
- this.writeLenPrefixed(ipBytes);
54
- this.writeLenPrefixed(requestIdBytes);
55
- this.writeLenPrefixed(headers);
56
-
57
- return this.buf.subarray(0, this.pos);
58
- }
59
- }
@@ -1,18 +0,0 @@
1
- export function sortKeys(value: unknown): unknown {
2
- if (value === null || typeof value !== "object") {
3
- return value;
4
- }
5
-
6
- if (Array.isArray(value)) {
7
- return value.map(sortKeys);
8
- }
9
-
10
- const record = value as Record<string, unknown>;
11
- const sorted: Record<string, unknown> = {};
12
-
13
- for (const key of Object.keys(record).sort()) {
14
- sorted[key] = sortKeys(record[key]);
15
- }
16
-
17
- return sorted;
18
- }
@@ -1,316 +0,0 @@
1
- import { decoder, encoder } from "./bytes";
2
- import addon from "../native";
3
- import type { SchemaValidatorInstance } from "../native";
4
-
5
- export type SchemaValidator = SchemaValidatorInstance;
6
-
7
- function dataView(bytes: Uint8Array): DataView {
8
- return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
9
- }
10
-
11
- function readU32(dv: DataView, offset: number): number {
12
- if (offset + 4 > dv.byteLength) {
13
- throw new RangeError("packed buffer: truncated u32");
14
- }
15
-
16
- return dv.getUint32(offset, true);
17
- }
18
-
19
- function readSlice(bytes: Uint8Array, offset: number, len: number): Uint8Array {
20
- if (len < 0 || offset + len > bytes.byteLength) {
21
- throw new RangeError("packed buffer: truncated bytes");
22
- }
23
-
24
- return bytes.subarray(offset, offset + len);
25
- }
26
-
27
- export function unpackU32Array(bytes: Uint8Array): Uint32Array {
28
- if (bytes.byteLength < 4) {
29
- return new Uint32Array(0);
30
- }
31
-
32
- const dv = dataView(bytes);
33
- const count = readU32(dv, 0);
34
-
35
- if (count > Math.floor((bytes.byteLength - 4) / 4)) {
36
- throw new RangeError("packed buffer: invalid u32 count");
37
- }
38
-
39
- const out = new Uint32Array(count);
40
-
41
- for (let i = 0; i < count; i++) {
42
- out[i] = readU32(dv, 4 + i * 4);
43
- }
44
-
45
- return out;
46
- }
47
-
48
- export function unpackBitset(bytes: Uint8Array): Uint8Array {
49
- if (bytes.byteLength < 4) {
50
- return new Uint8Array(0);
51
- }
52
-
53
- const dv = dataView(bytes);
54
- const count = readU32(dv, 0);
55
- const expectedBytes = Math.ceil(count / 8);
56
-
57
- if (bytes.byteLength < 4 + expectedBytes) {
58
- throw new RangeError("packed buffer: truncated bitset");
59
- }
60
-
61
- const bits = bytes.subarray(4);
62
- const out = new Uint8Array(count);
63
-
64
- for (let i = 0; i < count; i++) {
65
- const byte = bits[i >> 3] ?? 0;
66
- out[i] = (byte >> (i & 7)) & 1;
67
- }
68
-
69
- return out;
70
- }
71
-
72
- export function unpackI64ArrayAsBigInt(bytes: Uint8Array): BigInt64Array {
73
- if (bytes.byteLength < 4) {
74
- return new BigInt64Array(0);
75
- }
76
-
77
- const dv = dataView(bytes);
78
- const count = readU32(dv, 0);
79
-
80
- if (count > Math.floor((bytes.byteLength - 4) / 8)) {
81
- throw new RangeError("packed buffer: invalid i64 count");
82
- }
83
-
84
- const out = new BigInt64Array(count);
85
-
86
- for (let i = 0; i < count; i++) {
87
- out[i] = dv.getBigInt64(4 + i * 8, true);
88
- }
89
-
90
- return out;
91
- }
92
-
93
- export function unpackByteResults(bytes: Uint8Array): Uint8Array[] {
94
- if (bytes.byteLength < 4) {
95
- return [];
96
- }
97
-
98
- const dv = dataView(bytes);
99
- const count = readU32(dv, 0);
100
-
101
- const out: Uint8Array[] = [];
102
- let offset = 4;
103
-
104
- for (let i = 0; i < count; i++) {
105
- const len = readU32(dv, offset);
106
- offset += 4;
107
-
108
- const slice = readSlice(bytes, offset, len);
109
- offset += len;
110
-
111
- out.push(slice);
112
- }
113
-
114
- return out;
115
- }
116
-
117
- export type Pair = [string, string];
118
-
119
- export function readPairsPacked(bytes: Uint8Array): Pair[] {
120
- if (bytes.byteLength < 4) {
121
- return [];
122
- }
123
-
124
- const dv = dataView(bytes);
125
- const count = readU32(dv, 0);
126
-
127
- if (count === 0) {
128
- return [];
129
- }
130
-
131
- const out: Pair[] = [];
132
- let offset = 4;
133
-
134
- for (let i = 0; i < count; i++) {
135
- const keyLen = readU32(dv, offset);
136
- offset += 4;
137
-
138
- const keyBytes = readSlice(bytes, offset, keyLen);
139
- offset += keyLen;
140
-
141
- const valueLen = readU32(dv, offset);
142
- offset += 4;
143
-
144
- const valueBytes = readSlice(bytes, offset, valueLen);
145
- offset += valueLen;
146
-
147
- out.push([decoder.decode(keyBytes), decoder.decode(valueBytes)]);
148
- }
149
-
150
- return out;
151
- }
152
-
153
- export function pairsToObject(
154
- pairs: Pair[],
155
- ): Record<string, string | string[]> {
156
- const out: Record<string, string | string[]> = {};
157
-
158
- for (const [key, value] of pairs) {
159
- const existing = out[key];
160
-
161
- if (existing === undefined) {
162
- out[key] = value;
163
- } else if (Array.isArray(existing)) {
164
- existing.push(value);
165
- } else {
166
- out[key] = [existing, value];
167
- }
168
- }
169
-
170
- return out;
171
- }
172
-
173
- export interface ParsedHttpRequestPacked {
174
- method: string;
175
- path: string;
176
- version: string;
177
- headers: Record<string, string>;
178
- }
179
-
180
- export function readHttpPacked(bytes: Uint8Array): ParsedHttpRequestPacked {
181
- const dv = dataView(bytes);
182
- let offset = 0;
183
-
184
- const methodLen = readU32(dv, offset);
185
- offset += 4;
186
- const method = decoder.decode(readSlice(bytes, offset, methodLen));
187
- offset += methodLen;
188
-
189
- const pathLen = readU32(dv, offset);
190
- offset += 4;
191
- const path = decoder.decode(readSlice(bytes, offset, pathLen));
192
- offset += pathLen;
193
-
194
- const versionLen = readU32(dv, offset);
195
- offset += 4;
196
- const version = decoder.decode(readSlice(bytes, offset, versionLen));
197
- offset += versionLen;
198
-
199
- const headerCount = readU32(dv, offset);
200
- offset += 4;
201
-
202
- const headers: Record<string, string> = {};
203
-
204
- 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;
209
-
210
- const valueLen = readU32(dv, offset);
211
- offset += 4;
212
- const value = decoder.decode(readSlice(bytes, offset, valueLen));
213
- offset += valueLen;
214
-
215
- headers[name] = value;
216
- }
217
-
218
- return {
219
- method,
220
- path,
221
- version,
222
- headers,
223
- };
224
- }
225
-
226
- export function packBatch(items: Uint8Array[]): Uint8Array {
227
- let total = 4;
228
-
229
- for (const item of items) {
230
- total += 4 + item.byteLength;
231
- }
232
-
233
- const out = new Uint8Array(total);
234
- const dv = new DataView(out.buffer);
235
-
236
- dv.setUint32(0, items.length, true);
237
-
238
- let offset = 4;
239
-
240
- for (const item of items) {
241
- dv.setUint32(offset, item.byteLength, true);
242
- offset += 4;
243
-
244
- out.set(item, offset);
245
- offset += item.byteLength;
246
- }
247
-
248
- return out;
249
- }
250
-
251
- 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
- );
255
-
256
- let total = 4;
257
-
258
- for (const [key, value] of encoded) {
259
- total += 8 + key.byteLength + value.byteLength;
260
- }
261
-
262
- const out = new Uint8Array(total);
263
- const dv = new DataView(out.buffer);
264
-
265
- dv.setUint32(0, encoded.length, true);
266
-
267
- let offset = 4;
268
-
269
- 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;
279
- }
280
-
281
- return out;
282
- }
283
-
284
- export function schemaValidateBatch(
285
- validator: SchemaValidator,
286
- items: Uint8Array[],
287
- ): Uint8Array {
288
- return unpackBitset(validator.validateBatchPackedBitset(packBatch(items)));
289
- }
290
-
291
- export function schemaValidateBatchCount(
292
- validator: SchemaValidator,
293
- items: Uint8Array[],
294
- ): number {
295
- return validator.validateBatchPackedCount(packBatch(items));
296
- }
297
-
298
- // ── High-level string parsers (convenience) ──────────────────────
299
- // Wrap the native packed parsers + decoders for ergonomic use, so consumers
300
- // don't have to hand-pack buffers or decode packed pairs:
301
- // parseQueryString("a=1&b=2") // { a: "1", b: "2" }
302
- // parseCookieHeader("a=1; b=2") // { a: "1", b: "2" }
303
-
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));
309
- }
310
-
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
- }