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
@@ -0,0 +1,104 @@
1
+ // src/shared/response.ts — Response helpers for pooled (reusable) output buffers.
2
+ //
3
+ // When a response body is backed by a pooled buffer, the pool handle must not
4
+ // be released until the body has actually been consumed, otherwise a later
5
+ // request could overwrite bytes the in-flight response still references.
6
+ // `pooledBodyResponse` wraps the slice in a streaming body that returns the
7
+ // handle to its pool once the body is read (or the request is aborted).
8
+
9
+ import type { PooledBuffer } from './buffer-pool'
10
+
11
+ /**
12
+ * Build a `Response` whose body is backed by a pooled byte slice, returning the
13
+ * pool handle once the body has been fully consumed (stream closed) or the
14
+ * request has been cancelled.
15
+ *
16
+ * The pooled buffer stays **in flight** (not reusable) until the response
17
+ * stream is closed/cancelled — this is what makes zero-copy pooling safe: a
18
+ * buffer is never handed to a second request while a previous response may
19
+ * still alias it.
20
+ *
21
+ * @param handle - The pooled-buffer handle to release on consumption.
22
+ * @param bytes - The (possibly shared) byte slice to serve as the body.
23
+ * @param init - Standard `ResponseInit` (status, headers, ...).
24
+ * @param timeoutMs - Optional abandonment guard (ms, default 0 = disabled):
25
+ * if the body is neither pulled nor cancelled within this window the pooled
26
+ * buffer is released and the stream closed. Opt-in — lets zero-copy
27
+ * responses bound memory under abandoned (unread) responses.
28
+ * @returns A `Response` that releases `handle` once its body is consumed.
29
+ *
30
+ * @remarks
31
+ * **Zero-copy caveat**: the body slice is served without copying — it aliases
32
+ * the pooled buffer. Consumers that hold a response open for a long time keep
33
+ * that buffer out of circulation. This helper is only intended for callers that
34
+ * explicitly opt into zero-copy (e.g. `INGRESS_UNSAFE_ZERO_COPY`); the default
35
+ * safe path copies the slice and releases the handle eagerly.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * const handle = pool.acquire(size);
40
+ * const used = handle.buffer.subarray(0, written);
41
+ * return pooledBodyResponse(handle, used, { status: 200, headers });
42
+ * ```
43
+ */
44
+ export function pooledBodyResponse(
45
+ handle: PooledBuffer,
46
+ bytes: Uint8Array,
47
+ init: ResponseInit = {},
48
+ timeoutMs = 0,
49
+ ): Response {
50
+ if (bytes.byteLength === 0) {
51
+ // Nothing to serve: release immediately.
52
+ handle.release()
53
+ return new Response(null, init)
54
+ }
55
+
56
+ let released = false
57
+ let timer: ReturnType<typeof setTimeout> | undefined
58
+ const release = (): void => {
59
+ if (released) {
60
+ return
61
+ }
62
+ released = true
63
+ if (timer !== undefined) {
64
+ clearTimeout(timer)
65
+ }
66
+ handle.release()
67
+ }
68
+
69
+ const body = new ReadableStream<Uint8Array>({
70
+ start(controller) {
71
+ // Abandonment guard (opt-in): if the body is neither pulled nor
72
+ // cancelled within `timeoutMs`, release the pooled buffer so it can be
73
+ // reused and close the stream. Without this, an abandoned Response would
74
+ // hold the pooled buffer in flight forever (bounded only by maxInFlight).
75
+ // `pull()` re-checks `released` so a late read never serves the reused
76
+ // (potentially overwritten) bytes.
77
+ if (timeoutMs > 0) {
78
+ timer = setTimeout(() => {
79
+ if (released) {
80
+ return
81
+ }
82
+ release()
83
+ controller.close()
84
+ }, timeoutMs)
85
+ }
86
+ },
87
+ pull(controller) {
88
+ if (released) {
89
+ // Released by the abandonment guard (or a racing cancel): the pooled
90
+ // bytes may already be reused — never serve them. End-of-stream.
91
+ controller.close()
92
+ return
93
+ }
94
+ controller.enqueue(bytes)
95
+ controller.close()
96
+ release()
97
+ },
98
+ cancel() {
99
+ release()
100
+ },
101
+ })
102
+
103
+ return new Response(body, init)
104
+ }
@@ -0,0 +1,34 @@
1
+ // src/shared/runtime.ts — runtime detection helpers.
2
+ //
3
+ // castrum is Bun-first but must also run under Node.js. This module is the
4
+ // single place that detects the host runtime, so the rest of the codebase can
5
+ // branch on `isBun()` / `isNode()` without sprinkling `typeof Bun` checks
6
+ // everywhere. Importing this module has zero side effects.
7
+
8
+ export type RuntimeName = 'bun' | 'node' | 'unknown'
9
+
10
+ /** Whether the current runtime is Bun (the primary target). */
11
+ export function isBun(): boolean {
12
+ // Bun defines the global `Bun` object; Node does not.
13
+ return typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined'
14
+ }
15
+
16
+ /** Whether the current runtime is Node.js. */
17
+ export function isNode(): boolean {
18
+ return !isBun() && typeof process !== 'undefined' && process.versions?.node != null
19
+ }
20
+
21
+ /** Best-effort name of the current runtime. */
22
+ export function runtimeName(): RuntimeName {
23
+ if (isBun()) return 'bun'
24
+ if (isNode()) return 'node'
25
+ return 'unknown'
26
+ }
27
+
28
+ /** The Node.js major version when running under Node, otherwise `null`. */
29
+ export function nodeMajorVersion(): number | null {
30
+ if (!isNode()) return null
31
+ const v = process.versions.node
32
+ const major = Number(v?.split('.')[0])
33
+ return Number.isFinite(major) ? major : null
34
+ }
@@ -0,0 +1,81 @@
1
+ // src/shared/trace.ts — W3C Trace Context helpers (zero-dep).
2
+ //
3
+ // Parses the `traceparent` request header and generates span ids so ingress
4
+ // logs/hooks can correlate requests across services. This is the "tracing"
5
+ // primitive — wire `parseTraceContext(req)` in an `onRequest` hook and pass the
6
+ // traceId/spanId into your structured logger (pino/otel), or use it directly.
7
+ //
8
+ // Format: `version-traceid-spanid-flags` (e.g. `00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01`).
9
+
10
+ /** A parsed W3C trace context (valid per the spec's length/hex rules). */
11
+ export interface TraceContext {
12
+ /** Version, always `"00"` for accepted headers. */
13
+ version: string
14
+ /** 32 hex chars — the trace id (shared across the whole trace). */
15
+ traceId: string
16
+ /** 16 hex chars — the current span id. */
17
+ spanId: string
18
+ /** The flags byte (bit 0 = sampled). */
19
+ flags: number
20
+ /** True when the `sampled` (bit 0) flag is set. */
21
+ sampled: boolean
22
+ }
23
+
24
+ const HEX = /^[0-9a-fA-F]+$/
25
+
26
+ /**
27
+ * Parse a `traceparent` header value into a {@link TraceContext}, or `null`
28
+ * when malformed (wrong version / length / non-hex). Invalid contexts are
29
+ * ignored per the spec — callers should generate a fresh trace then.
30
+ */
31
+ export function parseTraceParent(header: string | null | undefined): TraceContext | null {
32
+ if (!header) return null
33
+ const parts = header.trim().split('-')
34
+ if (parts.length !== 4) return null
35
+ const [version, traceId, spanId, flagsStr] = parts
36
+ // Only version 00 is currently defined.
37
+ if (version !== '00') return null
38
+ if (traceId === undefined || traceId.length !== 32 || !HEX.test(traceId)) return null
39
+ if (spanId === undefined || spanId.length !== 16 || !HEX.test(spanId)) return null
40
+ if (flagsStr === undefined || flagsStr.length !== 2 || !HEX.test(flagsStr)) return null
41
+ const flags = Number.parseInt(flagsStr, 16)
42
+ return {
43
+ version,
44
+ traceId: traceId.toLowerCase(),
45
+ spanId: spanId.toLowerCase(),
46
+ flags,
47
+ sampled: (flags & 0x01) === 0x01,
48
+ }
49
+ }
50
+
51
+ /** Random 32-hex trace id (crypto-backed where available). */
52
+ export function createTraceId(): string {
53
+ return randomHex(16)
54
+ }
55
+
56
+ /** Random 16-hex span id. */
57
+ export function createSpanId(): string {
58
+ return randomHex(8)
59
+ }
60
+
61
+ /** Generate `n` random bytes as lowercase hex. */
62
+ function randomHex(bytes: number): string {
63
+ const buf = new Uint8Array(bytes)
64
+ const c = globalThis.crypto
65
+ if (c?.getRandomValues) {
66
+ c.getRandomValues(buf)
67
+ } else {
68
+ for (let i = 0; i < bytes; i++) buf[i] = Math.floor(Math.random() * 256)
69
+ }
70
+ let out = ''
71
+ for (let i = 0; i < bytes; i++) {
72
+ out += (buf[i] ?? 0).toString(16).padStart(2, '0')
73
+ }
74
+ return out
75
+ }
76
+
77
+ /** Serialize a trace context back to a `traceparent` header value. */
78
+ export function serializeTraceParent(ctx: TraceContext): string {
79
+ const flags = ctx.flags.toString(16).padStart(2, '0')
80
+ return `${ctx.version}-${ctx.traceId}-${ctx.spanId}-${flags}`
81
+ }
@@ -0,0 +1,31 @@
1
+ // src/shared/uuid.ts — UUIDv7 generation.
2
+ //
3
+ // Deliberately does NOT implement UUIDv7 in Rust: the decision matrix
4
+ // (docs/bun-builtins-decision-matrix.md) measured Bun.randomUUIDv7 ~2x faster
5
+ // than the FFI-crossing rust random-token path, so this delegates to the Bun
6
+ // built-in and falls back to crypto.randomUUID (RFC 4122 v4) under Node.
7
+
8
+ /** Generate a UUIDv7 (time-ordered) string, preferring Bun's native built-in. */
9
+ export function uuidv7(): string {
10
+ const bun = (globalThis as { Bun?: { randomUUIDv7?: () => string } }).Bun
11
+ if (typeof bun?.randomUUIDv7 === 'function') {
12
+ return bun.randomUUIDv7()
13
+ }
14
+ // Node / non-Bun fallback: crypto.randomUUID (v4 — order not guaranteed).
15
+ return globalThis.crypto?.randomUUID?.() ?? fallbackRandomUuid()
16
+ }
17
+
18
+ /** Last-resort synchronous UUIDv4 (only if crypto.randomUUID is unavailable). */
19
+ function fallbackRandomUuid(): string {
20
+ const bytes = new Uint8Array(16)
21
+ const c = globalThis.crypto
22
+ if (c?.getRandomValues) {
23
+ c.getRandomValues(bytes)
24
+ } else {
25
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256)
26
+ }
27
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40 // version 4
28
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80 // variant 10xx
29
+ const hex = [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('')
30
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
31
+ }
package/Cargo.toml DELETED
@@ -1,73 +0,0 @@
1
- [package]
2
- name = "castrum"
3
- # Keep in sync with package.json "version" (single source of truth: the npm package).
4
- version = "0.7.0"
5
- edition = "2021"
6
-
7
- [lib]
8
- name = "castrum"
9
- crate-type = ["cdylib"]
10
- path = "rust/lib.rs"
11
-
12
- [dependencies]
13
- napi = { version = "3", features = ["napi10"] }
14
- napi-derive = "3"
15
-
16
- serde = { version = "1", features = ["derive"] }
17
- serde_json = "1"
18
- sonic-rs = "0.5"
19
-
20
- # default-features=false: drop resolve-http + tls-aws-lc-rs which pull in
21
- # reqwest -> hyper/h2/tokio/rustls (large binary, attack surface). Format
22
- # validators (email etc.) are built-in and NOT feature-gated in 0.48.
23
- jsonschema = { version = "0.48", default-features = false, features = ["resolve-file"] }
24
- json-patch = "4"
25
- aws-lc-rs = "1"
26
- base64 = "0.23"
27
- crc32fast = "1"
28
- httparse = "1"
29
- mime_guess = "2"
30
- phf = { version = "0.14", features = ["macros"] }
31
- getrandom = "0.4"
32
- memchr = "2"
33
- simdutf8 = "0.1"
34
- email_address = "0.2"
35
- fast_chemail = { version = "0.9", optional = true }
36
- rayon = "1"
37
- # mimalloc is set as the Rust #[global_allocator] (see rust/lib.rs). We DELIBERATELY
38
- # do NOT enable the "override" feature: it interposes libc malloc/free at the
39
- # process level, which is an embedding hazard when this cdylib is dlopen'd into
40
- # Bun (which itself embeds mimalloc -> symbol clashes). The Rust-level global
41
- # allocator keeps most of the allocation win without replacing the host malloc.
42
- mimalloc = { version = "0.1", features = ["local_dynamic_tls"] }
43
- core_affinity = "0.8"
44
-
45
- parking_lot = "0.12"
46
- xxhash-rust = { version = "0.8", features = ["xxh3"] }
47
- ipnet = "2"
48
- lru = "0.12"
49
-
50
- [build-dependencies]
51
- napi-build = "2"
52
-
53
- [features]
54
- default = ["fast-email"]
55
- fast-email = ["dep:fast_chemail"]
56
-
57
- [profile.release]
58
- opt-level = 3
59
- lto = "fat"
60
- codegen-units = 1
61
- strip = "symbols"
62
- debug = 0
63
- incremental = false
64
- # Safety: this is an embedded cdylib loaded into the Bun process. panic = "unwind"
65
- # (the default) lets napi's catch_unwind convert Rust panics into JS exceptions
66
- # instead of aborting the host process. overflow-checks = true catches integer
67
- # overflow in release; the cost is negligible relative to the safety gain.
68
- overflow-checks = true
69
- debug-assertions = false
70
-
71
- [profile.release.package."*"]
72
- opt-level = 3
73
- codegen-units = 1
package/bench.ts DELETED
@@ -1,3 +0,0 @@
1
- import { runBenchmark } from "./src/bench";
2
-
3
- runBenchmark();
package/rust/batch.rs DELETED
@@ -1,195 +0,0 @@
1
- // rust/batch.rs — v2: DIRECT WRITE, NO INTERMEDIATE Vec<Vec<u8>>
2
- use napi::bindgen_prelude::*;
3
- use napi_derive::napi;
4
-
5
- use crate::json_ops::{json_sum_ids_bytes, json_valid_bytes};
6
- use crate::util::{
7
- count_batch, should_parallelize, sum_batch_i64, total_bytes, unpack,
8
- };
9
- use crate::validation::{
10
- validate_email_bytes, validate_ipv4_bytes, validate_ipv6_bytes, validate_uuid_bytes,
11
- };
12
-
13
- // ── Internal helpers (direct-write variants) ──
14
-
15
- /// Direct-write JSON sum batch — eliminates intermediate Vec<i64>.
16
- #[inline]
17
- fn json_sum_batch_direct(items: &[&[u8]], out: &mut Vec<u8>) {
18
- let n = items.len();
19
- out.reserve(4 + n * 8);
20
- out.extend_from_slice(&(n as u32).to_le_bytes());
21
- let start = out.len();
22
- out.resize(start + n * 8, 0);
23
-
24
- // Slice the output buffer to write directly into it.
25
- let out_slice = &mut out[start..];
26
-
27
- if should_parallelize(items.len(), total_bytes(items)) {
28
- use rayon::prelude::*;
29
- let chunk_size = 256;
30
-
31
- // Use par_chunks_mut to get mutable disjoint slices of the output buffer.
32
- out_slice.par_chunks_mut(chunk_size * 8).enumerate().for_each(|(ci, out_chunk)| {
33
- let num_items = out_chunk.len() / 8;
34
- let item_start = ci * chunk_size;
35
- let item_end = (item_start + num_items).min(items.len());
36
-
37
- for (i, item) in items[item_start..item_end].iter().enumerate() {
38
- let sum = json_sum_ids_bytes(item).unwrap_or(0);
39
- out_chunk[i * 8..(i + 1) * 8].copy_from_slice(&sum.to_le_bytes());
40
- }
41
- });
42
- } else {
43
- for (i, item) in items.iter().enumerate() {
44
- let sum = json_sum_ids_bytes(item).unwrap_or(0);
45
- out_slice[i * 8..(i + 1) * 8].copy_from_slice(&sum.to_le_bytes());
46
- }
47
- }
48
- }
49
-
50
- /// Direct-write parse batch — eliminates intermediate Vec<Vec<u8>>.
51
- #[inline]
52
- fn parse_batch_direct<F>(items: &[&[u8]], out: &mut Vec<u8>, f: F)
53
- where
54
- F: Fn(&[u8]) -> Result<Vec<u8>> + Sync,
55
- {
56
- let n = items.len();
57
- out.reserve(4 + n * 32);
58
- out.extend_from_slice(&(n as u32).to_le_bytes());
59
- for item in items {
60
- let r = f(item).unwrap_or_default();
61
- out.extend_from_slice(&(r.len() as u32).to_le_bytes());
62
- out.extend_from_slice(&r);
63
- }
64
- }
65
-
66
- #[inline]
67
- fn query_parse_packed_vec(input: &[u8]) -> Result<Vec<u8>> {
68
- crate::query_parser::query_parse_packed_vec(input)
69
- }
70
-
71
- #[inline]
72
- fn cookie_parse_packed_vec(input: &[u8]) -> Vec<u8> {
73
- crate::cookie_parser::cookie_parse_packed_vec(input)
74
- }
75
-
76
- #[inline]
77
- fn http_parse_request_packed_vec(input: &[u8]) -> Result<Vec<u8>> {
78
- crate::http_parser::http_parse_request_packed_vec(input)
79
- }
80
-
81
- #[inline]
82
- fn count_batch_data(data: &[u8], f: impl Fn(&[u8]) -> bool + Sync, chunk_items: usize) -> Result<u32> {
83
- let items = unpack(data)?;
84
- Ok(count_batch(&items, f, chunk_items) as u32)
85
- }
86
-
87
- #[inline]
88
- fn sum_ids_batch_data(data: &[u8], chunk_items: usize) -> Result<i64> {
89
- let items = unpack(data)?;
90
- Ok(sum_batch_i64(&items, |item| json_sum_ids_bytes(item).unwrap_or(0), chunk_items))
91
- }
92
-
93
- #[inline]
94
- fn total_len_batch_data(data: &[u8], f: impl Fn(&[u8]) -> Result<Vec<u8>> + Sync, chunk_items: usize) -> Result<u32> {
95
- let items = unpack(data)?;
96
- let chunk_items = chunk_items.max(64);
97
- let total: usize = if should_parallelize(items.len(), total_bytes(&items)) {
98
- use rayon::prelude::*;
99
- items.par_chunks(chunk_items)
100
- .map(|chunk| chunk.iter().map(|item| f(item).map(|v| v.len()).unwrap_or(0)).sum::<usize>())
101
- .sum()
102
- } else {
103
- items.iter().map(|item| f(item).map(|v| v.len()).unwrap_or(0)).sum()
104
- };
105
- Ok(total as u32)
106
- }
107
-
108
- // ── Packed input → packed output ──
109
-
110
- #[inline]
111
- fn json_valid_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
112
- let items = unpack(data)?;
113
- Ok(crate::util::validation_bitset_chunked(&items, json_valid_bytes, 512))
114
- }
115
-
116
- #[inline]
117
- fn validate_email_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
118
- let items = unpack(data)?;
119
- Ok(crate::util::validation_bitset_chunked(&items, validate_email_bytes, 4096))
120
- }
121
-
122
- #[inline]
123
- fn validate_uuid_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
124
- let items = unpack(data)?;
125
- Ok(crate::util::validation_bitset_chunked(&items, validate_uuid_bytes, 4096))
126
- }
127
-
128
- #[inline]
129
- fn validate_ipv4_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
130
- let items = unpack(data)?;
131
- Ok(crate::util::validation_bitset_chunked(&items, validate_ipv4_bytes, 4096))
132
- }
133
-
134
- #[inline]
135
- fn validate_ipv6_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
136
- let items = unpack(data)?;
137
- Ok(crate::util::validation_bitset_chunked(&items, validate_ipv6_bytes, 4096))
138
- }
139
-
140
- #[inline]
141
- fn json_sum_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
142
- let items = unpack(data)?;
143
- let mut out = Vec::with_capacity(4 + items.len() * 8);
144
- json_sum_batch_direct(&items, &mut out);
145
- Ok(out)
146
- }
147
-
148
- #[inline]
149
- fn query_parse_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
150
- let items = unpack(data)?;
151
- let mut out = Vec::with_capacity(items.len() * 32);
152
- parse_batch_direct(&items, &mut out, query_parse_packed_vec);
153
- Ok(out)
154
- }
155
-
156
- #[inline]
157
- fn cookie_parse_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
158
- let items = unpack(data)?;
159
- let mut out = Vec::with_capacity(items.len() * 32);
160
- parse_batch_direct(&items, &mut out, |i| Ok(cookie_parse_packed_vec(i)));
161
- Ok(out)
162
- }
163
-
164
- #[inline]
165
- fn http_parse_request_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
166
- let items = unpack(data)?;
167
- let mut out = Vec::with_capacity(items.len() * 64);
168
- parse_batch_direct(&items, &mut out, http_parse_request_packed_vec);
169
- Ok(out)
170
- }
171
-
172
- // ── Sync packed batch APIs ──
173
-
174
- #[napi] pub fn json_valid_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(json_valid_batch_bytes(input.as_ref())?)) }
175
- #[napi] pub fn validate_email_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(validate_email_batch_bytes(input.as_ref())?)) }
176
- #[napi] pub fn validate_uuid_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(validate_uuid_batch_bytes(input.as_ref())?)) }
177
- #[napi] pub fn validate_ipv4_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(validate_ipv4_batch_bytes(input.as_ref())?)) }
178
- #[napi] pub fn validate_ipv6_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(validate_ipv6_batch_bytes(input.as_ref())?)) }
179
- #[napi] pub fn json_sum_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(json_sum_batch_bytes(input.as_ref())?)) }
180
- #[napi] pub fn query_parse_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(query_parse_batch_bytes(input.as_ref())?)) }
181
- #[napi] pub fn cookie_parse_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(cookie_parse_batch_bytes(input.as_ref())?)) }
182
- #[napi] pub fn http_parse_request_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(http_parse_request_batch_bytes(input.as_ref())?)) }
183
-
184
- // ── Aggregate sync packed batch APIs ──
185
-
186
- #[napi] pub fn json_valid_batch_count_packed(input: Uint8Array) -> Result<u32> { count_batch_data(input.as_ref(), json_valid_bytes, 512) }
187
- #[napi] pub fn validate_email_batch_count_packed(input: Uint8Array) -> Result<u32> { count_batch_data(input.as_ref(), validate_email_bytes, 4096) }
188
- #[napi] pub fn validate_uuid_batch_count_packed(input: Uint8Array) -> Result<u32> { count_batch_data(input.as_ref(), validate_uuid_bytes, 4096) }
189
- #[napi] pub fn validate_ipv4_batch_count_packed(input: Uint8Array) -> Result<u32> { count_batch_data(input.as_ref(), validate_ipv4_bytes, 4096) }
190
- #[napi] pub fn validate_ipv6_batch_count_packed(input: Uint8Array) -> Result<u32> { count_batch_data(input.as_ref(), validate_ipv6_bytes, 4096) }
191
- #[napi] pub fn json_sum_batch_total_packed(input: Uint8Array) -> Result<i64> { sum_ids_batch_data(input.as_ref(), 512) }
192
- #[napi] pub fn query_parse_batch_total_len_packed(input: Uint8Array) -> Result<u32> { total_len_batch_data(input.as_ref(), query_parse_packed_vec, 256) }
193
- #[napi] pub fn cookie_parse_batch_total_len_packed(input: Uint8Array) -> Result<u32> { total_len_batch_data(input.as_ref(), |i| Ok(cookie_parse_packed_vec(i)), 256) }
194
- #[napi] pub fn http_parse_request_batch_total_len_packed(input: Uint8Array) -> Result<u32> { total_len_batch_data(input.as_ref(), http_parse_request_packed_vec, 256) }
195
-
@@ -1,70 +0,0 @@
1
- // rust/cookie_parser.rs — Unified zero-alloc cookie parser
2
- // Eliminated the _vec variant; all callers use the single _into_slice path.
3
- // Upper-bound pre-allocation for callers that need a Vec.
4
-
5
- use crate::util::{trim_ascii_whitespace, write_bytes, write_u32_le};
6
- use napi::bindgen_prelude::*;
7
- use napi_derive::napi;
8
-
9
- /// Parse cookies into a caller-provided slice (zero-alloc).
10
- /// Output format: [u32 count] repeated { [u32 name_len] [name] [u32 value_len] [value] }
11
- #[inline]
12
- pub fn cookie_parse_packed_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
13
- let mut pos = 0usize;
14
- write_u32_le(out, &mut pos, 0)?;
15
-
16
- let mut count = 0u32;
17
-
18
- for pair in input.split(|&b| b == b';') {
19
- let pair = trim_ascii_whitespace(pair);
20
- if pair.is_empty() { continue; }
21
-
22
- let (name, value) = match pair.iter().position(|&b| b == b'=') {
23
- Some(eq) => (&pair[..eq], &pair[eq + 1..]),
24
- None => (pair, &[][..]),
25
- };
26
-
27
- let name = trim_ascii_whitespace(name);
28
- let value = trim_ascii_whitespace(value);
29
-
30
- if name.is_empty() { continue; }
31
-
32
- write_u32_le(out, &mut pos, name.len() as u32)?;
33
- write_bytes(out, &mut pos, name)?;
34
-
35
- write_u32_le(out, &mut pos, value.len() as u32)?;
36
- write_bytes(out, &mut pos, value)?;
37
-
38
- count += 1;
39
- }
40
-
41
- out[0..4].copy_from_slice(&count.to_le_bytes());
42
- Ok(pos)
43
- }
44
-
45
- /// Allocate and parse cookies. Uses conservative upper bound to avoid a pre-scan.
46
- #[inline]
47
- pub fn cookie_parse_packed_vec(input: &[u8]) -> Vec<u8> {
48
- let upper_bound = input.len().saturating_mul(9).saturating_add(16);
49
- let mut out = vec![0u8; upper_bound];
50
- match cookie_parse_packed_into_slice(input, &mut out) {
51
- Ok(written) => {
52
- out.truncate(written);
53
- out
54
- }
55
- Err(_) => {
56
- // Fall back to VecWriter if slice is too small (rare).
57
- vec![0u8; 4] // empty result
58
- }
59
- }
60
- }
61
-
62
- #[napi]
63
- pub fn cookie_parse_packed(input: Uint8Array) -> Result<Buffer> {
64
- Ok(Buffer::from(cookie_parse_packed_vec(input.as_ref())))
65
- }
66
-
67
- #[napi]
68
- pub fn cookie_parse_packed_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
69
- crate::util::run_packed_into(&input, &mut output, cookie_parse_packed_into_slice)
70
- }