/** * 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 * throughput while network reads are pending over the last 10 minutes; * token-bucket waits are excluded) 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; } export interface BandwidthDiagnostics { effectiveRateBps: number | null; activeChoreTransfers: number; governorWaitMs: 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 raw throughput over the last * {@link ESTIMATOR_WINDOW_MS}. Network-read samples are bytes divided by the * time spent waiting for the read; governor waits are outside those samples. * A tiny always-current accumulator — at most 600 live entries, pruned on * every touch. */ export declare class ThroughputEstimator { private readonly now; private readonly buckets; private readonly rawRates; constructor(now?: () => number); private prune; record(bytes: number): void; /** Record bytes divided by the wall time spent in the network read. */ recordRawThroughput(bytes: number, readDurationMs: 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; private readonly now; private governorWaitMs; private activeChoreTransfers; /** Unmeasurable raw bytes from bodies that have not yet timed a sample. */ private unmeasuredRaw; private readonly choreTransferWaiters; 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. Instant when pacing is * off; time spent waiting here is excluded from the capacity estimate. */ acquire(bytes: number): Promise; /** Observe bytes as a Node readable receives them, before consumer pacing. */ observeRawBody(body: AsyncIterable): AsyncIterable; /** Build one transport-arrival sampler, independent of body consumer pulls. */ createRawReadObserver(): (bytes: number) => void; /** * Add one transport sample to the rolling capacity estimate. * * A chunk that arrives in the same clock tick as its observer's previous * mark has no measurable duration. Within one body that is the normal first * chunk and it is ignored once the same body records a measurable sample. * A body whose only chunks were unmeasurable (a small buffered response) is * carried forward and folded into the next sample from a different body, so * a workload of such bodies still yields an estimate instead of leaving a * percent policy unlimited. */ recordRawThroughputSample(bytes: number, durationMs: number, source?: object): void; /** Measure a successful upload request after token waits have completed. */ measureRawTransfer(bytes: number, work: () => Promise): Promise; /** Current transfer pressure and cumulative token-bucket wait for diagnostics. */ getDiagnostics(): BandwidthDiagnostics; private choreTransferLimit; private makeChoreTransferRelease; private admitWaitingChoreTransfers; private acquireChoreTransfer; /** Reserve a chore slot before opening its payload request, if configured. */ reserveChoreTransfer(): Promise<(() => void) | undefined>; /** * 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, reservedRelease?: () => void): AsyncIterable; /** Apply pacing when enabled, or count active GET bodies for cadence diagnostics. */ governBody(body: AsyncIterable, reservedRelease?: () => void): AsyncIterable; private trackChoreBody; /** Hold a chore transfer slot around an upload request or other payload leg. */ withChoreTransfer(work: () => Promise): Promise; private throttleBodyWithPacing; /** 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