import type { FetchObservation } from "./fetch-observer.js"; /** * In-flight backpressure watchdog for audit crawls. * * Goal: protect the user's origin (and their bill) when our crawl triggers a * cascade of expensive requests — the paperforge case, where each fetch fanned * out into a chain of uncached DB queries that exhausted the Neon free-tier * egress quota. * * Design: * - First `warmupSize` origin fetches establish a baseline p95 latency. * During warmup, the monitor never aborts — we don't know what "normal" * looks like yet. * - After warmup, every new observation updates a rolling window. If the * rolling p95 exceeds `max(baseline × multiplier, absoluteP95Ms)` OR the * 5xx ratio exceeds `errorRatioThreshold`, the monitor votes for abort. * - Pure cache hits (no origin round-trip) are ignored — they don't stress * the origin and would otherwise mask degradation (a cached page looks * "fast" even when the origin is collapsing). * * The monitor is passive: it returns a decision but does not touch the * AbortController itself. Callers hook it into their fetch pipeline and * trigger the abort on their end so tests don't have to mock signals. */ export interface BackpressureOptions { /** Origin fetches (excluding cache hits) required before trips are allowed. * During this window the monitor records but never aborts. */ warmupSize: number; /** Absolute p95 cap in milliseconds. p95 ≥ this → abort. */ absoluteP95Ms: number; /** Post-warmup p95 must exceed `baseline × multiplier` before tripping on * relative slowdown. Catches "origin was fine, now it's 3× slower". */ baselineMultiplier: number; /** Abort when 5xx ratio in the rolling window ≥ this. */ errorRatioThreshold: number; /** Size of the rolling window for post-warmup stats. Default 20. */ rollingWindowSize?: number; } export interface BackpressureDecision { shouldAbort: boolean; /** Populated when `shouldAbort` is true — human-readable reason. */ reason?: string; /** Snapshot of the metrics that drove the decision (for error reporting). */ snapshot?: BackpressureSnapshot; } export interface BackpressureSnapshot { /** Rolling p95 in ms (post-warmup). */ p95Ms: number; /** Baseline p95 computed during warmup. 0 if not yet established. */ baselineP95Ms: number; /** 5xx ratio in the rolling window. */ errorRatio: number; /** Total origin (non-cached) fetches seen. */ liveFetchCount: number; } export declare class OriginDegradedError extends Error { readonly diagnostics: BackpressureSnapshot; constructor(reason: string, diagnostics: BackpressureSnapshot); } export declare class BackpressureMonitor { private readonly opts; private readonly liveDurations; private readonly liveStatuses; private baselineP95Ms; constructor(opts: BackpressureOptions); record(o: FetchObservation): BackpressureDecision; } //# sourceMappingURL=backpressure.d.ts.map