castrum 0.9.8 → 0.9.9

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 (55) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/castrum.darwin-arm64.node +0 -0
  4. package/castrum.darwin-x64.node +0 -0
  5. package/castrum.linux-arm64-gnu.node +0 -0
  6. package/castrum.linux-arm64-musl.node +0 -0
  7. package/castrum.linux-x64-gnu.node +0 -0
  8. package/castrum.linux-x64-musl.node +0 -0
  9. package/castrum.linux-x64-v3-gnu.node +0 -0
  10. package/castrum.win32-x64-msvc.node +0 -0
  11. package/dist/index.d.ts +164 -4
  12. package/dist/index.js +434 -70
  13. package/docs/ENVIRONMENT.md +7 -0
  14. package/docs/RND-CONCURRENCY.md +20 -4
  15. package/docs/superpowers/plans/2026-09-18-elysia-learnings-adoption.md +634 -0
  16. package/index.ts +5 -0
  17. package/package.json +2 -1
  18. package/rust/ffi/task.rs +19 -18
  19. package/rust/ingress/api.rs +21 -5
  20. package/rust/ingress/mod.rs +7 -0
  21. package/rust/ingress/native_route.rs +4 -2
  22. package/rust/ingress/schema_cache.rs +266 -0
  23. package/rust/task/mod.rs +1 -0
  24. package/rust/task/runtime.rs +55 -7
  25. package/rust/task/tests.rs +98 -1
  26. package/src/ingress/abort.ts +48 -0
  27. package/src/ingress/body.ts +104 -8
  28. package/src/ingress/handlers.ts +1 -0
  29. package/src/ingress/health.ts +22 -0
  30. package/src/ingress/index.ts +3 -1
  31. package/src/ingress/router.ts +36 -1
  32. package/src/ingress/routes/common.ts +9 -1
  33. package/src/ingress/routes/echo.ts +10 -0
  34. package/src/ingress/routes/json-write.ts +11 -0
  35. package/src/ingress/routes/native.ts +24 -5
  36. package/src/ingress/routes/options.ts +6 -2
  37. package/src/ingress/routes/responder.ts +25 -5
  38. package/src/ingress/routes/safe-record.ts +57 -0
  39. package/src/ingress/server-error.ts +111 -0
  40. package/src/ingress/server-node.ts +102 -23
  41. package/src/ingress/server.ts +140 -32
  42. package/src/integration/pipeline.ts +6 -0
  43. package/src/native/ffi/build/task.ts +2 -1
  44. package/src/native/ffi/types.ts +6 -1
  45. package/src/native/types.ts +6 -0
  46. package/src/runtime/detect.ts +14 -0
  47. package/src/rust-ffi/context.ts +46 -3
  48. package/src/rust-ffi/scalar/factories.ts +6 -0
  49. package/src/rust-ffi/scalar/interface.ts +14 -0
  50. package/src/rust-ffi/text.ts +5 -4
  51. package/src/shared/buffer-pool.ts +10 -0
  52. package/src/shared/memory.ts +68 -0
  53. package/src/shared/metrics.ts +86 -3
  54. package/src/shared/runtime.ts +7 -0
  55. package/src/task/runtime.ts +52 -9
package/Cargo.lock CHANGED
@@ -233,7 +233,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
233
233
 
234
234
  [[package]]
235
235
  name = "castrum"
236
- version = "0.9.8"
236
+ version = "0.9.9"
237
237
  dependencies = [
238
238
  "argon2",
239
239
  "aws-lc-rs",
package/Cargo.toml CHANGED
@@ -1,7 +1,7 @@
1
1
  [package]
2
2
  name = "castrum"
3
3
  # Keep in sync with package.json "version" (single source of truth: the npm package).
4
- version = "0.9.8"
4
+ version = "0.9.9"
5
5
  edition = "2021"
6
6
  # Keep in sync with package.json "license" (cargo-deny rejects a crate with no
7
7
  # license expression).
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/dist/index.d.ts CHANGED
@@ -553,8 +553,45 @@ export interface PathMatch {
553
553
  * adapter (Bun's native router handles matching; the ingress pipeline does
554
554
  * not echo params in its response). */
555
555
  type RouteHandler = (req: Request, srv?: unknown, params?: Record<string, string>) => Response | Promise<Response>;
556
+ /**
557
+ * A prebuilt constant response for static route promotion.
558
+ *
559
+ * Either a bare `Response` or a factory that builds a FRESH `Response` per
560
+ * request. Behavior differs by runtime:
561
+ *
562
+ * - **Bun**: a bare `Response` is snapshotted into `Bun.serve`'s native route
563
+ * table at bind time (verified on Bun 1.4.2 — the body is NOT consumed by the
564
+ * first request) and served on every request without entering JS.
565
+ * - **Node** (`createIngressServerNode`): the adapter reads the bare `Response`
566
+ * body ONCE and materializes a fresh `Response` per request (a web body is
567
+ * one-shot).
568
+ *
569
+ * A factory is invoked by the runtime on every request, so it MUST return a new
570
+ * `Response` each call. Returning the SAME already-sent instance fails on the
571
+ * second request with `ERR_BODY_ALREADY_USED` (Bun) / a consumed stream
572
+ * (Node) — use a factory only when the response genuinely varies per request.
573
+ */
574
+ export type StaticRoute = Response | (() => Response);
556
575
  /** Route spec for the Bun.serve builder. */
557
576
  export interface BakedRoute {
577
+ /**
578
+ * A prebuilt constant response served for GET, bypassing the ingress
579
+ * pipeline entirely. Takes precedence over `read`/`cookies` for GET (the
580
+ * pipeline handler is never wired); other methods (`write`/`echo`/`delete`)
581
+ * are unaffected. See {@link StaticRoute} for the one-shot-body contract.
582
+ *
583
+ * HEAD is intentionally NOT wired for `static` (a bare `Response` carries a
584
+ * body; static responses are probes/constants served over GET). This matches
585
+ * the raw-`read` probe behavior — but note that a route which previously
586
+ * served HEAD via a native `read` now 405s (Node) on HEAD once `static` is
587
+ * set. When `static` is combined with `write`/`echo`/`delete`, those dynamic
588
+ * methods remain wired.
589
+ *
590
+ * Runtime note: `createIngressRouter` treats `static` differently — there,
591
+ * like `raw`, it OWNS THE WHOLE SPEC (the router has no compiled ingress for
592
+ * the other methods; see `RouterRouteSpec.static` in src/ingress/router.ts).
593
+ */
594
+ static?: StaticRoute;
558
595
  /**
559
596
  * Wires GET + HEAD read handlers. Accepts an optimized ingress handler OR a
560
597
  * raw `RouteHandler` function (health/metrics probes are raw — see
@@ -631,14 +668,20 @@ export interface CreateIngressServerOptions {
631
668
  routes: Record<string, BakedRoute>;
632
669
  fallback?: OptimizedIngressHandler;
633
670
  /**
634
- * Invoked when an unhandled error escapes a request handler (currently wired
635
- * on the Node adapter; the Bun path uses runtime.onResponse). Never throws —
636
- * hook failures are swallowed.
671
+ * Invoked when an unhandled error escapes a request handler (raw `read`
672
+ * functions, `responder`/`native` responders, or the Bun/Node adapter
673
+ * catch-all). The error is masked to a generic 500 on the wire. Never
674
+ * throws — hook failures are swallowed.
637
675
  */
638
676
  onError?: (info: {
639
677
  error: Error;
640
678
  request?: Request;
641
679
  }) => void;
680
+ /**
681
+ * Optional structured-log sink for escaped handler errors (one JSON line per
682
+ * masked 500, message only). Never throws.
683
+ */
684
+ logger?: (line: string) => void;
642
685
  /** Node adapter only: ms to receive the complete request (headers + body).
643
686
  * Guards slowloris/trickling requests. Default: 30_000. */
644
687
  requestTimeoutMs?: number;
@@ -1372,18 +1415,23 @@ export interface BunFFI {
1372
1415
  regexEscapeStr(input: string): string | null;
1373
1416
  /** Start the shared native task pool; 0 = started, 1 = already running. */
1374
1417
  taskInit(threads: number): number;
1375
- /** Submit a task op with packed `args` under `taskId`; 1 = accepted. */
1418
+ /**
1419
+ * Submit a task op with packed `args` under `taskId`. Returns `1` accepted,
1420
+ * `2` overloaded (bounded queue full), `0` invalid args.
1421
+ */
1376
1422
  taskSubmit(op: number, args: Uint8Array, taskId: number): number;
1377
1423
  /**
1378
1424
  * Zero-copy submit: the op writes its result DIRECTLY into `output` on the
1379
1425
  * pool thread. `output` must be a stable, caller-owned buffer that stays
1380
1426
  * alive (and untouched) until the completion is drained — the runtime holds
1381
1427
  * the reference. The completion body is the 8-byte LE written length.
1428
+ * Returns `1` accepted, `2` overloaded, `0` invalid args.
1382
1429
  */
1383
1430
  taskSubmitOut(op: number, args: Uint8Array, taskId: number, output: Uint8Array): number;
1384
1431
  /**
1385
1432
  * Zero-copy INPUT: only `hdr` is copied; `data` is read in place by the pool
1386
1433
  * thread, so the caller must keep it alive (and untouched) until completion.
1434
+ * Returns `1` accepted, `2` overloaded, `0` invalid args.
1387
1435
  */
1388
1436
  taskSubmitSlice(op: number, hdr: Uint8Array, data: Uint8Array, taskId: number): number;
1389
1437
  /**
@@ -1448,6 +1496,41 @@ export declare function createTraceId(): string;
1448
1496
  export declare function createSpanId(): string;
1449
1497
  /** Serialize a trace context back to a `traceparent` header value. */
1450
1498
  export declare function serializeTraceParent(ctx: TraceContext): string;
1499
+ /**
1500
+ * The `err.code` value a body read carries when it is rejected because the
1501
+ * client disconnected (`Request.signal` aborted).
1502
+ */
1503
+ export declare const ABORT_CODE = "REQUEST_ABORTED";
1504
+ /**
1505
+ * Whether a thrown value is the cancellation error produced when `req.signal`
1506
+ * aborts during a body read (see `readBodyWithLimit` in `./body`).
1507
+ *
1508
+ * @param err - The caught value.
1509
+ * @returns `true` when `err.code === 'REQUEST_ABORTED'`.
1510
+ * @example
1511
+ * ```ts
1512
+ * try {
1513
+ * await readBodyWithLimit(req, 1 << 20, true)
1514
+ * } catch (err) {
1515
+ * if (isAbortError(err)) return abortResponse()
1516
+ * throw err
1517
+ * }
1518
+ * ```
1519
+ */
1520
+ export declare function isAbortError(err: unknown): boolean;
1521
+ /**
1522
+ * Build the cancelled-client response: status **499** ("Client Closed
1523
+ * Request", a de-facto nginx status) with no body. This is the only new
1524
+ * outcome added by cancellation — it never replaces an existing success or
1525
+ * error status.
1526
+ *
1527
+ * @returns A bodyless `Response` with status 499.
1528
+ * @example
1529
+ * ```ts
1530
+ * if (req.signal?.aborted) return abortResponse()
1531
+ * ```
1532
+ */
1533
+ export declare function abortResponse(): Response;
1451
1534
  /** Offset of the verdict byte (0 = accepted, nonzero = terminal error). */
1452
1535
  export declare const OUT_VERDICT: number;
1453
1536
  /** Offset of the error-code byte (see ERR_CODE_* below). */
@@ -1829,6 +1912,20 @@ export declare function createIngressHandler(options?: IngressHandlerOptions, ru
1829
1912
  * Never touches dependencies.
1830
1913
  */
1831
1914
  export declare function livenessHandler(): (_req: Request) => Response;
1915
+ /**
1916
+ * Liveness probe as a PREBUILT `Response`, for static route promotion.
1917
+ *
1918
+ * Wire it as `{ static: staticLiveness() }` so `Bun.serve` snapshots the
1919
+ * constant body into its native route table and serves `/livez` without
1920
+ * entering the JavaScript ingress pipeline at all (see `BakedRoute.static` in
1921
+ * src/ingress/server.ts). On the node:http adapter the bare `Response` is
1922
+ * re-materialized per request from cached body bytes, so the one-shot body is
1923
+ * never replayed. Unlike {@link readinessHandler}, it never runs a dependency
1924
+ * check.
1925
+ *
1926
+ * @returns A fresh 200 `{"status":"ok"}` `Response`.
1927
+ */
1928
+ export declare function staticLiveness(): Response;
1832
1929
  /**
1833
1930
  * Readiness probe: runs the optional async `check` (dependency readiness).
1834
1931
  * Returns `200 {"status":"ready"}` when the check passes (or no check is
@@ -1955,6 +2052,20 @@ export interface RouterRouteSpec {
1955
2052
  /** Terminal envelope style for this route's responder (default: router-level
1956
2053
  * `terminalStyle` or `'castrum'`). */
1957
2054
  terminalStyle?: TerminalStyle;
2055
+ /**
2056
+ * A prebuilt constant response served directly for GET, OUTSIDE the ingress
2057
+ * pipeline (static route promotion). Like `raw`, it OWNS THE WHOLE SPEC: the
2058
+ * router compiles no `IngressInner` for a `static` route, so `read`/`write`/
2059
+ * `echo`/etc. on the same spec are ignored.
2060
+ *
2061
+ * This differs from `BakedRoute.static` in src/ingress/server.ts, where
2062
+ * `static` only takes GET precedence and leaves `write`/`echo`/`delete`
2063
+ * wired — the router cannot mirror that because a `static`-only spec has no
2064
+ * compiled ingress to hand those methods. Do not unify the two semantics.
2065
+ *
2066
+ * See `BakedRoute.static` for the one-shot-body contract.
2067
+ */
2068
+ static?: StaticRoute;
1958
2069
  /**
1959
2070
  * A raw request→Response handler served directly for GET, OUTSIDE the
1960
2071
  * ingress pipeline (health/metrics probes, /metrics). When set, `read`/
@@ -3292,6 +3403,20 @@ export interface RustScalar {
3292
3403
  createMetricsRegistry(): MetricsRegistryInstance;
3293
3404
  initThreadPool(rayonThreads?: number): void;
3294
3405
  rayonNumThreads(): number;
3406
+ /**
3407
+ * Drop the process-wide compiled request-body schema cache. Identical schema
3408
+ * bytes are compiled once and shared across `Ingress` / `NativeRoute`
3409
+ * instances; this releases those compiled validators (already-constructed
3410
+ * instances keep their own shared reference and are unaffected). Idempotent,
3411
+ * and a safe no-op when the native addon is absent or predates the symbol
3412
+ * (the public `flushMemory()` relies on that).
3413
+ *
3414
+ * Maintenance-only escape hatch — NOT a benchmarked op, so it is intentionally
3415
+ * exempt from `PROVEN_SELECTION` / `selection.json` (there is no native-vs-JS
3416
+ * winner to classify). `test/unit/contract/proven.test.ts` only requires
3417
+ * registry coverage for selection.json ops + Bun built-ins.
3418
+ */
3419
+ clearSchemaCache(): void;
3295
3420
  jwtSign(claims: Record<string, unknown>, secret: Uint8Array, ttlSeconds?: number | null, nowSeconds?: number): Uint8Array;
3296
3421
  /**
3297
3422
  * Sign a JWT from pre-serialized claim JSON bytes — skips the napi
@@ -3694,6 +3819,41 @@ export declare const encoder: {
3694
3819
  export declare const decoder: {
3695
3820
  decode(bytes: Uint8Array | string): string;
3696
3821
  };
3822
+ /** Options for {@link flushMemory}. */
3823
+ export interface FlushMemoryOptions {
3824
+ /**
3825
+ * When true (the default), also request a full garbage collection from the
3826
+ * runtime: `Bun.gc(true)` on Bun, `globalThis.gc?.()` on Node when the
3827
+ * process runs with `--expose-gc`. Set false to flush caches only.
3828
+ */
3829
+ gc?: boolean;
3830
+ }
3831
+ /**
3832
+ * Flush process-level caches and (by default) request a full GC. Intended for
3833
+ * explicit memory-pressure handling — e.g. after a burst or before a snapshot.
3834
+ *
3835
+ * Clears:
3836
+ * - the global loader LRU cache (`loader.clear()`),
3837
+ * - the process-wide MIME caches (`clearMimeCaches()`),
3838
+ * - the native compiled-schema cache (`rust.clearSchemaCache()`).
3839
+ *
3840
+ * The metrics registry is intentionally left untouched.
3841
+ *
3842
+ * Idempotent, and safe to call before the native addon has ever been loaded
3843
+ * (every cache starts empty, the loader cache is a plain Map, and the native
3844
+ * schema-cache clear is a defensive no-op when the addon/symbol is absent).
3845
+ *
3846
+ * @param options - `gc: false` skips the runtime GC request. Defaults to `{}`
3847
+ * (GC requested).
3848
+ * @example
3849
+ * ```ts
3850
+ * import { flushMemory } from 'castrum'
3851
+ *
3852
+ * flushMemory() // drop caches + request GC
3853
+ * flushMemory({ gc: false }) // drop caches only
3854
+ * ```
3855
+ */
3856
+ export declare function flushMemory(options?: FlushMemoryOptions): void;
3697
3857
  /**
3698
3858
  * Generate a UUIDv7 (time-ordered) string, preferring Bun's native built-in.
3699
3859
  * Under Node this falls back to `crypto.randomUUID` (v4).