/** * @module @arcis/node/middleware/overload * * Event-loop overload protection (sdk-vectors.md tier 1 #30, issue #51). * * Rate limiting caps per-client load; this middleware caps **total server * load** by sampling event-loop lag and shedding new requests with 503 * when the loop is saturated. Pairs with rate-limit (per-client) + * `methodAllowlist` (per-route) to give Arcis three orthogonal layers * of "this server is healthy enough to do work" gating. * * ```ts * import { eventLoopProtection } from '@arcis/node'; * * app.use(eventLoopProtection({ * maxLagMs: 500, // 503 above this smoothed lag * sampleIntervalMs: 250, // measure every 250ms * })); * ``` * * Sampling strategy: * - `setInterval(fn, sampleIntervalMs)` is scheduled; the callback * compares wall-clock elapsed against the configured interval. Lag * IS the difference: when the loop is busy, the timer fires late. * - The interval handle is `.unref()`-ed so the process can exit * cleanly even if the caller forgets to call `close()`. * - Exponential moving average (`smoothed = 0.7 * smoothed + 0.3 * * measured`) smooths out single-sample spikes — a 600ms GC pause * shouldn't cause every request to 503 for the next sample window. * * Implementation deliberately picks `setTimeout` measurement over * `perf_hooks.monitorEventLoopDelay` (Node 12+ alternative) for two * reasons: (1) it works on every supported Node version with no feature * detection branch, (2) the perf_hooks histogram returns nanosecond * lag from a 10ms-resolution sampler, but applying the spec's EMA * formula on that signal would skew toward unreal lag values that the * sampler smoothed away. Keeping the sampler the spec calls for keeps * the math testable. */ import type { RequestHandler } from 'express'; export interface EventLoopProtectionOptions { /** Smoothed lag threshold in ms above which the middleware returns 503. Default: 500. */ maxLagMs?: number; /** Sample frequency in ms. Default: 250. Lower = more responsive, higher = less overhead. */ sampleIntervalMs?: number; /** Status code to return when overloaded. Default: 503. */ statusCode?: number; /** Error message in the response body. Default: "Server overloaded, please retry". */ message?: string; /** Retry-After header value in seconds. Default: 5. */ retryAfterSeconds?: number; /** * EMA smoothing factor for the new measurement. Default: 0.3 (per * issue #51 spec). Must be in (0, 1]; lower values smooth harder * (longer memory of past samples), higher values track more reactively. */ alpha?: number; /** * When true, every response gets `X-EventLoop-Lag: ` so monitoring * can graph saturation independent of the deny decision. Off by default * because most apps don't need it and an extra header on every response * adds noise. */ exposeLagHeader?: boolean; } export interface EventLoopProtectionMiddleware extends RequestHandler { /** * Stop the sampler. Call from a SIGTERM handler so the interval doesn't * keep a misconfigured process alive. Idempotent: subsequent calls are * no-ops. */ close(): void; /** * Read the current smoothed lag in ms. Useful for tests that want to * assert the smoothing math without mocking timers, and for callers * who want to expose the value through a different surface (Prometheus, * dashboard panel, etc.). */ currentLagMs(): number; } /** * Build an event-loop protection middleware. Returns a request handler * with `close()` and `currentLagMs()` attached (Pattern 6 in the * monorepo's middleware conventions: factories return a callable * augmented with cleanup helpers). */ export declare function eventLoopProtection(options?: EventLoopProtectionOptions): EventLoopProtectionMiddleware; export default eventLoopProtection; export declare const __test: { ema(prior: number, measured: number, alpha: number): number; }; //# sourceMappingURL=overload.d.ts.map