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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// src/loader/batch.ts — Microtask coalescer + bounded LRU cache.
|
|
2
|
+
//
|
|
3
|
+
// The coalescer is the DataLoader-style engine: N `load()` calls made within
|
|
4
|
+
// the same event-loop tick are grouped by op and flushed together in ONE
|
|
5
|
+
// packed native batch call (reducing N native crossings → 1). The cache is a
|
|
6
|
+
// bounded LRU (default key = fnv1a64 of the input) for repeated-key hot loads.
|
|
7
|
+
|
|
8
|
+
/** A pending `load()` request inside a coalescing tick. */
|
|
9
|
+
export interface LoadRequest {
|
|
10
|
+
input: Uint8Array
|
|
11
|
+
resolve(value: unknown): void
|
|
12
|
+
reject(err: unknown): void
|
|
13
|
+
/** Cache key (only set when caching is enabled for this request). */
|
|
14
|
+
key?: string | bigint
|
|
15
|
+
/**
|
|
16
|
+
* The default cache key should be computed lazily at flush time (coalesce
|
|
17
|
+
* mode deferral) so all keys are computed in ONE packed native crossing.
|
|
18
|
+
*/
|
|
19
|
+
needsDefaultKey?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Bounded LRU cache used by the loader (Hot Function Cache). */
|
|
23
|
+
export interface LoaderCache {
|
|
24
|
+
get(key: string | bigint): unknown
|
|
25
|
+
set(key: string | bigint, value: unknown): void
|
|
26
|
+
clear(): void
|
|
27
|
+
readonly size: number
|
|
28
|
+
readonly evictions: number
|
|
29
|
+
readonly hits: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A cache that never stores anything (maxKeys <= 0). */
|
|
33
|
+
const NULL_CACHE: LoaderCache = {
|
|
34
|
+
get: () => undefined,
|
|
35
|
+
set: () => {},
|
|
36
|
+
clear: () => {},
|
|
37
|
+
size: 0,
|
|
38
|
+
evictions: 0,
|
|
39
|
+
hits: 0,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Bounded LRU cache. `maxKeys <= 0` disables caching entirely.
|
|
44
|
+
* LRU order is maintained by delete+re-insert on access (Map order).
|
|
45
|
+
*/
|
|
46
|
+
export function createLruCache(maxKeys: number): LoaderCache {
|
|
47
|
+
if (maxKeys <= 0) return NULL_CACHE
|
|
48
|
+
const map = new Map<string | bigint, unknown>()
|
|
49
|
+
let evictions = 0
|
|
50
|
+
let hits = 0
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
get(key) {
|
|
54
|
+
const value = map.get(key)
|
|
55
|
+
if (value === undefined) return undefined
|
|
56
|
+
hits++
|
|
57
|
+
map.delete(key)
|
|
58
|
+
map.set(key, value)
|
|
59
|
+
return value
|
|
60
|
+
},
|
|
61
|
+
set(key, value) {
|
|
62
|
+
if (map.has(key)) map.delete(key)
|
|
63
|
+
map.set(key, value)
|
|
64
|
+
if (map.size > maxKeys) {
|
|
65
|
+
const oldest = map.keys().next().value
|
|
66
|
+
if (oldest !== undefined) {
|
|
67
|
+
map.delete(oldest)
|
|
68
|
+
evictions++
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
clear() {
|
|
73
|
+
map.clear()
|
|
74
|
+
},
|
|
75
|
+
get size() {
|
|
76
|
+
return map.size
|
|
77
|
+
},
|
|
78
|
+
get evictions() {
|
|
79
|
+
return evictions
|
|
80
|
+
},
|
|
81
|
+
get hits() {
|
|
82
|
+
return hits
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Per-tick coalescer. `enqueue` schedules a single `queueMicrotask` flush that
|
|
89
|
+
* drains ALL pending ops; the flush callback receives one group per op.
|
|
90
|
+
*/
|
|
91
|
+
export interface TickCoalescer {
|
|
92
|
+
enqueue(op: string, request: LoadRequest): void
|
|
93
|
+
/** Number of requests not yet flushed. */
|
|
94
|
+
readonly pending: number
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createTickCoalescer(
|
|
98
|
+
flush: (op: string, requests: LoadRequest[]) => void,
|
|
99
|
+
): TickCoalescer {
|
|
100
|
+
let buffer = new Map<string, LoadRequest[]>()
|
|
101
|
+
let scheduled = false
|
|
102
|
+
let pending = 0
|
|
103
|
+
|
|
104
|
+
function schedule() {
|
|
105
|
+
if (scheduled) return
|
|
106
|
+
scheduled = true
|
|
107
|
+
queueMicrotask(() => {
|
|
108
|
+
scheduled = false
|
|
109
|
+
const snapshot = buffer
|
|
110
|
+
buffer = new Map()
|
|
111
|
+
pending = 0
|
|
112
|
+
for (const [op, requests] of snapshot) {
|
|
113
|
+
if (requests.length > 0) flush(op, requests)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
enqueue(op, request) {
|
|
120
|
+
let list = buffer.get(op)
|
|
121
|
+
if (!list) {
|
|
122
|
+
list = []
|
|
123
|
+
buffer.set(op, list)
|
|
124
|
+
}
|
|
125
|
+
list.push(request)
|
|
126
|
+
pending++
|
|
127
|
+
schedule()
|
|
128
|
+
},
|
|
129
|
+
get pending() {
|
|
130
|
+
return pending
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// src/loader/cost.ts — Adaptive scalar-vs-batch cost model.
|
|
2
|
+
//
|
|
3
|
+
// Each op owns one cost model. It learns from ACTUAL dispatch timings (EWMA)
|
|
4
|
+
// and decides, at runtime, whether a bulk of `n` items should go through the
|
|
5
|
+
// packed batch path (ONE native crossing) or a scalar loop (`n` crossings).
|
|
6
|
+
//
|
|
7
|
+
// - `n === 1` always routes to the scalar path.
|
|
8
|
+
// - `n >= batchMin` routes to the batch path.
|
|
9
|
+
// - `batchMin` starts at the configured value (default 2) and adapts in
|
|
10
|
+
// [2, 8]: if measured batch-per-item cost stays above the scalar-per-item
|
|
11
|
+
// cost, `batchMin` rises so tiny batches fall back to the scalar loop; if
|
|
12
|
+
// batching wins, `batchMin` sinks back to 2.
|
|
13
|
+
//
|
|
14
|
+
// Timing is sampled (every `sampleEvery` dispatches) so the hot path does NOT
|
|
15
|
+
// pay a `hrtime` call on every invocation.
|
|
16
|
+
|
|
17
|
+
/** A per-op adaptive cost model. */
|
|
18
|
+
export interface LoaderCostModel {
|
|
19
|
+
/**
|
|
20
|
+
* Should a bulk of `n` items use the packed batch path?
|
|
21
|
+
* `false` → scalar loop (`n` crossings).
|
|
22
|
+
*/
|
|
23
|
+
decide(n: number): boolean
|
|
24
|
+
/** Record a scalar-path dispatch of `n` items taking `elapsedNs`. */
|
|
25
|
+
recordScalar(n: number, elapsedNs: number): void
|
|
26
|
+
/** Record a batch-path dispatch of `n` items taking `elapsedNs`. */
|
|
27
|
+
recordBatch(n: number, elapsedNs: number): void
|
|
28
|
+
/** Current learned batch threshold. */
|
|
29
|
+
readonly batchMin: number
|
|
30
|
+
/** Snapshot for observability/tests. */
|
|
31
|
+
readonly stats: {
|
|
32
|
+
batchMin: number
|
|
33
|
+
scalarPerItemNs: number
|
|
34
|
+
batchPerItemNs: number
|
|
35
|
+
samples: number
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const MIN_BATCH_MIN = 2
|
|
40
|
+
const MAX_BATCH_MIN = 8
|
|
41
|
+
const EWMA_ALPHA = 0.3
|
|
42
|
+
|
|
43
|
+
function clampBatchMin(v: number): number {
|
|
44
|
+
return Math.max(MIN_BATCH_MIN, Math.min(MAX_BATCH_MIN, v))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** High-resolution nanosecond clock (Bun/Node). */
|
|
48
|
+
export const nowNs: () => number =
|
|
49
|
+
typeof process !== 'undefined' && typeof process.hrtime === 'function'
|
|
50
|
+
? () => Number(process.hrtime.bigint())
|
|
51
|
+
: () => performance.now() * 1e6
|
|
52
|
+
|
|
53
|
+
export interface CostModelOptions {
|
|
54
|
+
/** Allow the model to adjust `batchMin` from measurement. Default true. */
|
|
55
|
+
adaptive?: boolean
|
|
56
|
+
/** Initial batch threshold in [2, 8]. Default 2 (batch wins for n >= 2). */
|
|
57
|
+
batchMin?: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Build a per-op cost model. */
|
|
61
|
+
export function createCostModel(options: CostModelOptions = {}): LoaderCostModel {
|
|
62
|
+
const adaptive = options.adaptive !== false
|
|
63
|
+
let batchMin = clampBatchMin(options.batchMin ?? MIN_BATCH_MIN)
|
|
64
|
+
let scalarPerItemNs = 0
|
|
65
|
+
let batchPerItemNs = 0
|
|
66
|
+
let samples = 0
|
|
67
|
+
|
|
68
|
+
function ewma(prev: number, next: number): number {
|
|
69
|
+
return prev === 0 ? next : prev * (1 - EWMA_ALPHA) + next * EWMA_ALPHA
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
decide(n) {
|
|
74
|
+
if (n <= 1) return false
|
|
75
|
+
return n >= batchMin
|
|
76
|
+
},
|
|
77
|
+
recordScalar(n, elapsedNs) {
|
|
78
|
+
samples++
|
|
79
|
+
const perItem = n > 0 ? elapsedNs / n : elapsedNs
|
|
80
|
+
scalarPerItemNs = ewma(scalarPerItemNs, perItem)
|
|
81
|
+
if (adaptive && n >= 2 && batchPerItemNs > 0 && perItem < batchPerItemNs * 0.8) {
|
|
82
|
+
batchMin = clampBatchMin(batchMin + 1)
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
recordBatch(n, elapsedNs) {
|
|
86
|
+
samples++
|
|
87
|
+
const perItem = n > 0 ? elapsedNs / n : elapsedNs
|
|
88
|
+
batchPerItemNs = ewma(batchPerItemNs, perItem)
|
|
89
|
+
if (adaptive && scalarPerItemNs > 0) {
|
|
90
|
+
if (perItem > scalarPerItemNs * 1.2) {
|
|
91
|
+
batchMin = clampBatchMin(batchMin + 1)
|
|
92
|
+
} else if (perItem < scalarPerItemNs * 0.8) {
|
|
93
|
+
batchMin = clampBatchMin(batchMin - 1)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
get batchMin() {
|
|
98
|
+
return batchMin
|
|
99
|
+
},
|
|
100
|
+
get stats() {
|
|
101
|
+
return { batchMin, scalarPerItemNs, batchPerItemNs, samples }
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
}
|