/** * Bandwidth governor — process-wide pacing for S3 payload transfers. * * The desktop app spawns `hq-sync-runner` on customer machines; on slow links * an unthrottled sync saturates the connection. The runner honors three env * vars (set by the desktop app, all optional): * * - `HQ_SYNC_MAX_BYTES_PER_SEC` — integer; total S3 payload rate cap, * uploads + downloads combined. 0 / unset / invalid ⇒ unlimited. * - `HQ_SYNC_MAX_CONCURRENCY` — integer; max simultaneous S3 object * transfers (clamps the transfer pool). 0 / unset / invalid ⇒ the * existing adaptive default. * - `HQ_SYNC_BANDWIDTH_PERCENT` — integer 1..100; adaptive mode. The * governor keeps a rolling estimate of link capacity (max observed * aggregate throughput over the last 10 minutes, measured during actual * transfers) and paces to percent/100 × that estimate, floored at * 64 KiB/s so sync always progresses. When both PERCENT and * MAX_BYTES_PER_SEC are set, the smaller resulting cap wins. * * Only DATA-plane transfers are paced (upload bodies, download streams). * Control-plane calls (list, head, delete, presign mints) are never * throttled — see the wiring in object-io.ts. * * Mechanism: a single token bucket shared by every transfer in the process, * refilled continuously at the effective rate with a one-second burst * allowance. Transfers acquire tokens per chunk; when the bucket is empty the * acquire sleeps for exactly the deficit, so at most one chunk is ever in * flight beyond the bucket (backpressure, no extra buffering). Deliberately * NOT a delay-based congestion controller — dead simple, deterministic, and * testable under virtual time. */ export declare const MAX_BYTES_PER_SEC_ENV = "HQ_SYNC_MAX_BYTES_PER_SEC"; export declare const MAX_CONCURRENCY_ENV = "HQ_SYNC_MAX_CONCURRENCY"; export declare const BANDWIDTH_PERCENT_ENV = "HQ_SYNC_BANDWIDTH_PERCENT"; /** Adaptive-mode floor: sync always progresses at ≥ 64 KiB/s. */ export declare const MIN_ADAPTIVE_BYTES_PER_SEC: number; export interface BandwidthPolicy { /** Fixed aggregate cap in bytes/sec; undefined = no fixed cap. */ maxBytesPerSec?: number; /** Adaptive percent of observed link capacity (1..100); undefined = off. */ percent?: number; /** Transfer-pool clamp; undefined = keep the adaptive default. */ maxConcurrency?: number; } /** * Parse the env contract. Invalid, zero, or unset values disable the * corresponding knob rather than erroring — a misconfigured desktop app must * never break sync, only fail open to today's behavior. */ export declare function parseBandwidthPolicy(env?: Record): BandwidthPolicy; /** * Rolling link-capacity estimate: max observed aggregate throughput over the * last {@link ESTIMATOR_WINDOW_MS}. Bytes are recorded into per-second * buckets as transfers deliver them; capacity is the largest bucket in the * window (bytes in one second = B/s). A tiny always-current accumulator — * at most 600 live entries, pruned on every touch. */ export declare class ThroughputEstimator { private readonly now; private readonly buckets; constructor(now?: () => number); private prune; record(bytes: number): void; /** Max observed B/s in the window; undefined before any observation. */ capacityBps(): number | undefined; } interface ClockOpts { now?: () => number; sleep?: (ms: number) => Promise; } /** * Continuous-refill token bucket with a one-second burst allowance and a * dynamic rate (re-read on every acquire, so adaptive mode retunes live). * `acquire(n)` debits n tokens and sleeps for exactly the deficit when the * bucket goes negative — the caller's chunk proceeds, and the NEXT chunk * waits, so no more than one chunk is ever in flight beyond the bucket. */ export declare class TokenBucket { private readonly rateBps; private tokens; private lastRefillMs; private readonly now; private readonly sleep; constructor(rateBps: () => number | undefined, opts?: ClockOpts); acquire(bytes: number): Promise; } /** * Process-wide governor: one bucket + one estimator shared by every transfer. * Inert (all pass-throughs) when the policy carries no rate knobs. */ export declare class BandwidthGovernor { readonly policy: BandwidthPolicy; private readonly estimator; private readonly bucket; constructor(policy: BandwidthPolicy, opts?: ClockOpts); /** True when any payload pacing is configured. */ isPacing(): boolean; /** * The rate the bucket refills at right now. Fixed cap, adaptive cap * (percent × rolling max, floored at {@link MIN_ADAPTIVE_BYTES_PER_SEC}), * or — when both are set — the smaller. Adaptive mode with no observation * yet is unlimited: the first transfers measure the link. */ effectiveRateBps(): number | undefined; /** * Pace `bytes` of payload through the shared bucket and feed the capacity * estimator. Instant when pacing is off. */ acquire(bytes: number): Promise; /** * Wrap a download body so each chunk acquires tokens before it is yielded. * Delivers every byte unmodified; returns the original iterable when * pacing is off (zero overhead on the default path). */ throttleBody(body: AsyncIterable): AsyncIterable; /** Clamp the transfer pool size with HQ_SYNC_MAX_CONCURRENCY (if set). */ clampConcurrency(poolSize: number): number; /** One-line human description of the effective policy for startup logs. */ describePolicy(): string; } export declare function getBandwidthGovernor(): BandwidthGovernor; /** * Test seam: install a governor (or null to drop the singleton so the next * get re-reads the env). Production never calls this. */ export declare function setBandwidthGovernorForTesting(governor: BandwidthGovernor | null): void; export {}; //# sourceMappingURL=bandwidth.d.ts.map