import { D as Decision, S as Store, C as Clock, d as Strategy, L as Limiter } from './types-DKirIBQt.cjs'; import { R as Region } from './types-BSVR-zyA.cjs'; import { R as RedisClientLike } from './store-BZNM-FbH.cjs'; /** A lease-size policy: commit a size for the next refill, then learn from the realised demand. */ interface LeaseSizer { /** The integer lease size to use for the next refill (`>= minSize >= 1`). */ size(): number; /** Feed the demand the node saw this window; updates the size for subsequent windows. */ observe(demand: number): void; /** The continuous internal size (before rounding/clamping), for introspection. */ readonly continuous: number; } /** Options for {@link leaseSizer}. */ interface LeaseSizerOptions { /** Order cost `c`: cost charged per lease (one L2 round trip). Must be `> 0`. */ orderCost: number; /** Strand penalty `h`: cost per leased-but-unused credit forfeited at the window boundary. Must be `> 0`. */ strandPenalty: number; /** Lower clamp on lease size (`>= 1`). Default `1`. */ minSize?: number; /** Upper clamp on lease size. Default `1e6`. */ maxSize?: number; /** Initial lease size. Default `minSize`. */ initialSize?: number; /** * AdaGrad step scale `η₀` in `η₀/√(ε + Σg²)`. Default `min(1, ln(maxSize) − ln(minSize))` — capped at * one e-fold so the first step (where `Σg² ≈ g²`) can't traverse the whole range and pin to a clamp. */ stepScale?: number; /** AdaGrad numerical floor `ε`. Default `1e-8`. */ epsilon?: number; } /** * The Economic Order Quantity optimum `b* = √(2·orderCost·demand/strandPenalty)` — the per-window * minimiser of the lease-sizing cost, and the size {@link leaseSizer} descends onto under stationary * demand. */ declare function eoqOptimum(orderCost: number, strandPenalty: number, demand: number): number; /** * **Online adaptive lease sizer** (GALE Pillar 2) — learn the L2 lease `batch` that minimises the * coordination-vs-stranding EOQ cost, online, as a node's demand drifts. * * Each refill it commits the current {@link LeaseSizer.size}; each window you feed it the realised * demand via {@link LeaseSizer.observe}, and it takes one **AdaGrad step in log-space** on the convex * EOQ loss (log-space because the optimum spans orders of magnitude and the gradient is unbounded and * smooth — AdaGrad's adaptivity earns the scale-freedom; contrast the bounded pinball subgradient of * `learnedReservation`, where plain OGD is optimal). It attains `O(√T)` regret versus the best fixed * batch in hindsight and tracks a drifting optimum. * * @example * const sizer = leaseSizer({ orderCost: 20, strandPenalty: 1 }); * // per window on a leasing node: * const batch = sizer.size(); // use this as twoTier lease.batch * // …serve the window, counting how many credits this node actually demanded… * sizer.observe(demandThisWindow); // learn for next window * * @experimental Excluded from the 1.x SemVer guarantee (may change in a minor). See STABILITY.md. */ declare function leaseSizer(options: LeaseSizerOptions): LeaseSizer; /** Options for {@link predictiveLeaseSizer}. */ interface PredictiveLeaseSizerOptions extends LeaseSizerOptions { /** Hedge learning rate `η` (expert weights ∝ `exp(−η · cumulative expert loss)`). Default `0.01`. */ learningRate?: number; } /** A predictions-with-safety lease sizer: blend a per-window demand hint against the robust learner. */ interface PredictiveLeaseSizer { /** Commit a lease size for the upcoming window, given its predicted demand. */ size(predictedDemand: number): number; /** Learn from the realised demand: update both experts' weights and the robust learner. */ observe(demand: number): void; /** Current expert weights `[followPrediction, robust]` (sum to 1), for introspection. */ readonly weights: readonly [number, number]; } /** * **Learning-augmented lease sizer** (GALE Pillar 3) — like {@link leaseSizer}, but able to exploit a * per-window *demand prediction* when one is available, without trusting it. * * Two experts each window: "follow the prediction" plays the {@link eoqOptimum} size for the predicted * demand; "robust" is the {@link leaseSizer} AdaGrad learner. A **Hedge** meta-learner sets convex * weights from each expert's realised window cost and plays the weighted-average size. Convexity + * Jensen give `cost(blend) ≤ weighted-average expert cost`, and Hedge drives weight onto the better * expert: * * - **accurate predictions ⇒ weight → follow ⇒ cost → the offline optimum** (consistency); * - **bad predictions ⇒ weight → robust ⇒ cost → the no-regret bound** (robustness). * * Safety is untouched: the size is a number GALE Pillar 1 gates, so no prediction can breach the cap. * * @example * const sizer = predictiveLeaseSizer({ orderCost: 20, strandPenalty: 1 }); * const batch = sizer.size(predictedDemandNextWindow); // blends the hint with the robust learner * // …serve the window… * sizer.observe(realisedDemand); * * @experimental Excluded from the 1.x SemVer guarantee (may change in a minor). See STABILITY.md. */ declare function predictiveLeaseSizer(options: PredictiveLeaseSizerOptions): PredictiveLeaseSizer; /** * **Weighted Fair Escrow** — GALE Pillar 4 graduated to production. A weighted, work-conserving * fair-allocation limiter that splits one shared budget `L` across tenants in proportion to weight, * with idle tenants' surplus reclaimed by backlogged ones — neither stranded nor first-come. * * Design + rationale: `research/bigger-bets/pillar4-wfe/DESIGN.md`. Proofs of the four theorems * (T1 safety, T2 sharing-incentive, T3 work-conservation, T4 bounded unfairness) live in * `research/gale/PILLAR4-fairness.md`; the pure batch algebra is machine-checked at 20 000 random * trials in `test/gale/fair-escrow.test.ts`. * * ## Algorithm (the streaming realisation) * * Each tenant has a *dynamic guaranteed share* `gᵢ = ⌊wᵢ/W·L_effective⌋` recomputed from the * current active set on every check (`W` = total weight of active tenants; * `L_effective` = the credits visible to this process — see "L1 vs L2" below). The check is * hierarchical: * * 1. **Within guarantee** (`used + cost ≤ gᵢ`): always allowed, subject to the hard cap * `Σ used + cost ≤ L_effective`. This is the T2 sharing-incentive promise. * 2. **Borrow phase** (`used + cost > gᵢ`): the asker tries to grow into surplus that other tenants * have not yet claimed against their own guarantee. The pessimistic surplus available is * * ```text * borrowAvailable = max(0, (L_effective − Σ used) − Σⱼ≠ᵢ max(0, gⱼ − usedⱼ)) * ``` * * i.e. the unallocated budget minus what would still need to be served to *other* backlogged * tenants' guarantees. If `cost ≤ borrowAvailable`, the request is admitted; otherwise denied. * * ## L1 vs L2: what `L_effective` means * * Two configurations share the same fairness algorithm but differ in where `L_effective` comes * from: * * - **L1-only (single-process)** — `L_effective` is just `options.limit`; the whole budget is * visible to this one process. The bound is `Σ used ≤ L` within the process. * - **L2-backed (multi-process)** — when an L2 `Store` is configured, `L_effective` starts at 0 * each window and grows lazily: when a check needs more credits than the local pool holds, the * WFE leases `quantum` credits (or `cost` if larger) atomically from a shared L2 counter * (a `fixedWindow({ limit: L, windowMs })` against the same key on every process). The L2 * counter's atomicity bounds the *global* total at `L` across processes; within a process the * leased-and-used credits feed the same fairness math. See DESIGN.md §6.3 / §6.4 for the * multi-process T1/T2/T4 bounds (each cross-process bound picks up a `quantum`-scaled slack). * * ## What the streaming algorithm does and does not promise vs the batch ideal * * The batch `weightedMaxMin(d, w, L)` (in `src/admission/`) gives the exact lexicographically- * maximal split given the *complete* demand vector. Streaming WFE can only know each tenant's * declared demand at their check sites; the asymptotic behaviour matches: * * - **T1 safety** — `Σ used ≤ L` always (guarantee floor + L_remaining cap; L2 atomicity for the * cross-process case). ✓ * - **T2 sharing-incentive** — every active tenant is admissible up to `gᵢ` before any other * tenant can borrow beyond their `gⱼ`, within a process and against the process's `L_effective`. * Across processes, T2 scales by the process's leased share; see DESIGN.md §6.4. ✓ * - **T3 work-conservation** — surplus from idle tenants flows to backlogged ones, but only when * `gⱼ − usedⱼ` for all `j` ≠ asker has been pessimistically reserved. A tenant who stops mid- * window keeps their guaranteed reserve until the window rolls — the safe choice when we cannot * distinguish "stopped" from "about to ask again." Work-conservation is therefore realised * between *truly absent* tenants (who never join the active set), not between *paused* ones; the * gap is the documented streaming-vs-batch trade. End-of-window T3 holds. * - **T4 bounded unfairness** — `0` at the L1 layer (exact integer guarantees per check). * `Σₚ Q⁽ᵖ⁾ · (1/wᵢ + 1/wⱼ)` across processes in L2 mode (the DRR quantum bound, scaled by the * number of processes contending for the shared pool). * * The cost is `O(N)` per check (L1 mode) or `O(N) + 1 RTT` per check that triggers a lease (L2 * mode), with `N` = active tenants this window. For the bounded-`N` production case (`N ≤ 1024` * via `l1.maxKeys`), the L1 bookkeeping is sub-microsecond per call. * * ## What it is not * * - **Not strategy-proof.** Per FairRide (Pu et al., NSDI'16): no shared-cache primitive can be * sharing-incentive, work-conserving, AND strategy-proof at once. WFE takes the first two and * concedes the third honestly. A tenant *can* over-declare demand to claim surplus; window- * coupling bounds the gain to one window. * - **Single-region.** This primitive splits one budget across tenants in one region. For * cross-region pooling that composes to a GLOBAL weighted-max-min guarantee, use * {@link federatedWeightedFairEscrow} + `regionFairPool` (TK-1404 — DR-P4-7/DR-P4-8 shipped): a * weighted-fair reservation layer over regions (region weight = Σ active tenant weights) composed * with this in-region split. Proof: `research/gale/PILLAR4-fairness.md` §"Federated composition". * * ## When to use this vs `weightedFairShare` * * `weightedFairShare` (`src/admission/index.ts`) ships an *equal-share approximation* — surplus from * idle tenants is first-come, not redistributed by weight. WFE is the work-conserving sibling: * under skewed demand, idle tenants' shares flow to backlogged ones in proportion to weight, * dominating the equal-share variant on utilisation while keeping the same hard `Δ = 0` per-window * cap (Pillar 1 inheritance). Pick `weightedFairShare` when single-process equal-share is enough; * pick `weightedFairEscrow` when work-conservation under skew matters (the LLM-gateway multi-tenant * overload case in Workload C of EVALUATION.md). */ /** Options for {@link weightedFairEscrow}. */ interface WeightedFairEscrowOptions { /** Global per-window budget `L`. Floored to an integer; must be > 0. */ limit: number; /** Window width in ms. Windows are epoch-aligned: `floor(now/windowMs)·windowMs`. Must be > 0. */ windowMs: number; /** * Per-tenant weight `wᵢ`. Returns `> 0` for any tenant string. Default `() => 1` (equal share — * the exact work-conserving generalisation of {@link fairShare} / {@link weightedFairShare}). */ weightOf?: (tenant: string) => number; /** * **L2 backing (multi-process)** — when provided, the shared budget lives in this `Store` * (any distributed store: Redis, Postgres, MemoryStore for tests). Each process atomically * leases credits from the shared counter via the existing `fixedWindow({ limit: L, windowMs })` * strategy (DR-P4-5 — no new Lua); per-tenant fairness arithmetic stays in-process. When omitted, * the limiter is single-process and the full `limit` is visible immediately. */ l2?: Store; /** * L2-only: the per-process **lease size** — how many credits to acquire from the shared store * at a time. Larger quantum = fewer round trips, looser cross-process T4 bound * (`Σₚ Q⁽ᵖ⁾ · (1/wᵢ + 1/wⱼ)`). Must be a positive integer. Required when `l2` is set; ignored * when omitted. There is no default — tune it to your latency-vs-fairness budget. */ quantum?: number; /** * L2-only: the shared store's key for this WFE instance. All processes sharing a budget MUST * use the same key. Default `"tk:wfe:pool"`. */ l2Key?: string; /** * Bounded tenant set. Same role as `twoTier.l1.maxKeys`: caps the in-process per-tenant state * map to prevent unbounded growth on untrusted tenant input. Default unbounded; set on public * surfaces to a value that comfortably exceeds the expected tenant count. */ l1?: { maxKeys?: number; }; /** Injected clock. Default {@link systemClock}. */ clock?: Clock; } /** * A weighted-fair-escrow limiter: split a shared budget `L` across tenants by weight, with * surplus from idle tenants reclaimed to backlogged ones. See {@link weightedFairEscrow}. */ interface WeightedFairEscrowLimiter { /** * Check `tenant` for the given `cost` (default 1). Returns a {@link Decision}; `limit` and * `remaining` describe **this tenant's** current fair-share ceiling and remaining headroom, not * the global pool — matches {@link weightedFairShare}'s contract so client-facing 429-rendering * is consistent. * * Synchronous only when L1-only (no `l2` configured); with `l2` configured the lease step is * async and {@link WeightedFairEscrowLimiter.checkSync} throws. */ checkSync(tenant: string, cost?: number): Decision; /** Promise-returning form; required path when `l2` is configured. */ check(tenant: string, cost?: number): Promise; /** * Reset one tenant's per-window usage (it leaves the active set), or — with no argument — the * whole window. The freed `used` is returned to the unallocated pool, so other backlogged * tenants can grow into it on subsequent checks. **L2 note:** in L2 mode this does NOT reset * the shared store — it only resets in-process accounting. The shared store rolls itself at the * next window boundary; call `store.reset(l2Key)` explicitly to force a global reset. */ reset(tenant?: string): void; /** * Read-only snapshot of the current window's tenant state, for metrics / introspection. The * returned object is a copy; mutating it does not affect the live state. */ stats(): WeightedFairEscrowStats; } /** A point-in-time read of the WFE's current window for metrics / introspection. */ interface WeightedFairEscrowStats { /** Window start (epoch-ms, `floor(now/windowMs)·windowMs`); -Infinity if no check has happened. */ readonly windowStart: number; /** Configured per-window budget `L` (constant). */ readonly limit: number; /** * Effective `L_effective` visible to this process. In L1-only mode = `limit`; in L2 mode it * grows lazily as the process leases from the shared store, capped at `limit`. */ readonly effectiveLimit: number; /** Effective unallocated pool: `effectiveLimit − Σ used`. */ readonly pool: number; /** Total used across all tenants this window (in this process). */ readonly totalUsed: number; /** Per-tenant snapshot: weight + used (current cumulative consumption in this window). */ readonly tenants: ReadonlyArray<{ readonly tenant: string; readonly weight: number; readonly used: number; }>; } /** * **Weighted Fair Escrow** — split a shared per-window budget across tenants in weighted-max-min- * fair proportion, with idle tenants' surplus reclaimed by backlogged ones. * * @example * // Single-process WFE (no `l2`): * import { weightedFairEscrow } from "throttlekit/twotier"; * * const escrow = weightedFairEscrow({ * limit: 10_000, // L * windowMs: 60_000, * weightOf: (tenant) => tenantWeights[tenant] ?? 1, * }); * const d = await escrow.check("tenant-A", 5); * * @example * // Multi-process WFE — one shared L2 counter, atomic leases: * import { weightedFairEscrow } from "throttlekit/twotier"; * import { RedisStore } from "throttlekit/redis"; * * const escrow = weightedFairEscrow({ * limit: 10_000, * windowMs: 60_000, * weightOf: (t) => tenantWeights[t] ?? 1, * l2: new RedisStore({ client }), * quantum: 100, // per-process lease size * l2Key: "tk:wfe:my-gateway", // same on every process * }); * * @example * // Composes with unifiedAdmission's cost axis: * import { unifiedAdmission, rateLimit, gcra } from "throttlekit"; * * const admit = unifiedAdmission({ * rate: rateLimit({ strategy: gcra({ limit: 500, periodMs: 60_000 }) }), * cost: weightedFairEscrow({ limit: 200_000, windowMs: 60_000, weightOf: ... }), * }); */ declare function weightedFairEscrow(options: WeightedFairEscrowOptions): WeightedFairEscrowLimiter; /** * **Federated Weighted Fair Escrow** — GALE Pillar 4 lifted across regions (TK-1404, #176). * * `weightedFairEscrow` splits ONE budget `L` across tenants in one process. This composes it across * REGIONS so that the **per-tenant GLOBAL total** — summed over every region a tenant is active in — * is the weighted-max-min-fair allocation a single, flat, global WFE would produce. The regions are * plumbing, not a fairness boundary: a tenant is neither helped nor hurt by *which* region it lives in. * * ## The composition (why naive pooling is not enough, and what works) * * Hierarchical max-min fairness is in general **not** flat max-min fairness — running WFE per region * over a budget shared by a plain first-come-first-served counter gives *in-region* weighted fairness * but *cross-region* FCFS pooling: a heavily-weighted region that arrives late is starved by a * lightly-weighted one (HLS isolation, Saeed et al. 2021; the same gap a shared counter leaves). Flat * global fairness emerges only under the Parekh-Gallager GPS-decomposition conditions, which this * mechanism realises by composing **two levels of WFE**: * * 1. **Cross-region WFE (the {@link RegionFairPool}).** A weighted-fair *reservation* layer over * regions: region `r`'s weight is its DYNAMIC active aggregate tenant weight `W_r = Σ w_{t,r}`, * and the pool grants region `r` at least its guaranteed share `⌊W_r/ΣW·L⌋` (reserved — a busy * region cannot steal it) and lets it borrow idle regions' surplus (work-conservation). A plain * counter cannot reserve, which is exactly why cross-region weights need this layer. * 2. **In-region WFE (per tenant).** The same `weightedFairEscrow` arithmetic (T1 cap, T2 guarantee, * T4 DRR-bounded borrow) splits each region's pool-granted budget among its tenants by weight. * 3. **Demand-proportional weight-split.** A tenant active in several regions must have its global * weight `w_t` SPLIT, `w_{t,r}=w_t·d_{t,r}/d_t` (so `Σ_r w_{t,r}=w_t`); returning the full `w_t` * in every region double-counts it and over-serves ≈k×. `weightOf` returns the region-local * split weight (for a region-local tenant, just `w_t`). * * Setting the region weight to `Σ` of its tenants' weights is the GPS-decomposition condition that * collapses the two-level hierarchy to a single global water-fill: the per-tenant global total equals * the flat global weighted-max-min ideal **exactly in the fluid limit**, and within a two-level DRR * residual under discrete granting. Proof + the machine-checked gate: * `research/bigger-bets/federation/federated-wfe-gate.ts`; theorem write-up (T-FED-1 safety, T-FED-2 * fluid exactness, T-FED-3 bound): `research/gale/PILLAR4-fairness.md`. * * ## Safety * * The pool grants `Σ_r (region budget) ≤ L` and in-region WFE serves `Σ_t used ≤` the region budget, * so `Σ admitted ≤ L` globally regardless of region count — `Δ = 0`. Both levels only *reorder* who * gets a credit; neither raises the total. * * ## Topology / distribution * * The {@link RegionFairPool} is the shared cross-region authority. {@link regionFairPool} is the * in-process implementation: correct + complete for a single arbiter process that all regions consult * (e.g. a central rate-limit service), and the substrate the tests verify against the flat oracle. * Distributing the pool across separate region processes needs the same per-region accounting in a * shared store (a Redis hash of region→{weight,used}, the weighted analog of `RegionalEscrow`'s Lua) — * the documented production path (DR-FWFE-1), staged exactly as `weightedFairEscrow` shipped L1 then L2. * * ## What it is / is not * * - **`checkSync` available** (the in-process pool is synchronous); `check` is the Promise form. * - **Not strategy-proof** (inherited from WFE T5 / FairRide): a tenant can over-declare demand to * claim surplus; window-coupling bounds the gain to one window. * * @example * import { regionFairPool, federatedWeightedFairEscrow } from "throttlekit/twotier"; * * const pool = regionFairPool({ limit: 1_000_000, windowMs: 60_000 }); // global L, shared * const us = federatedWeightedFairEscrow({ region: "us-east", pool, weightOf: (t) => weights[t] ?? 1 }); * const eu = federatedWeightedFairEscrow({ region: "eu-west", pool, weightOf: (t) => weights[t] ?? 1 }); * us.checkSync("tenant-A", 5); */ /** Options for {@link regionFairPool}. */ interface RegionFairPoolOptions { /** Global per-window budget `L`, shared across ALL regions. Floored to an integer; must be > 0. */ limit: number; /** Window width in ms. Epoch-aligned. Must be > 0. All regions on this pool share it. */ windowMs: number; /** Injected clock. Default {@link systemClock}. */ clock?: Clock; } /** Per-region snapshot for {@link RegionFairPool.stats}. */ interface RegionFairPoolStats { readonly windowStart: number; readonly limit: number; readonly totalGranted: number; readonly regions: ReadonlyArray<{ readonly region: string; readonly weight: number; readonly granted: number; }>; } /** * The shared cross-region weighted-fair reservation layer — a WFE whose "tenants" are regions. One * pool instance is shared by all {@link federatedWeightedFairEscrow} regions drawing from one global * budget. See the module doc for why a plain shared counter is insufficient (no reservation). */ interface RegionFairPool { /** Global budget `L`. */ readonly limit: number; /** Window width in ms. */ readonly windowMs: number; /** The pool's clock (regions couple their tenant windows to it). */ readonly clock: Clock; /** * Grant region `region` (current active aggregate weight `weight`) up to `wantTotal` total credits * for the active window, respecting cross-region weighted-max-min: the region is guaranteed at least * `⌊weight/ΣW·L⌋` (reserved) and may borrow idle regions' surplus, with `Σ_r granted ≤ L`. Returns * the region's new total grant (monotonic within a window). `now` is the caller's clock reading. */ grant(region: string, weight: number, wantTotal: number, now: number): number; /** Drop a region from the active set (its grant returns to the pool for others). */ release(region: string, now: number): void; /** Read-only snapshot of the current window. */ stats(): RegionFairPoolStats; } /** * In-process {@link RegionFairPool}: cross-region weighted-max-min with reservation + borrow. This is * the shipped, tested substrate; distributing it across processes is the store-backed production path * (DR-FWFE-1). The grant arithmetic is the region-level analog of `weightedFairEscrow.decide`. */ declare function regionFairPool(options: RegionFairPoolOptions): RegionFairPool; /** * The **async** face of {@link RegionFairPool} — the same weighted-max-min reservation, but where the * region→{weight,granted} state lives in a **shared store** so the pool is the single authority across * **separate region processes** (the production "DR-FWFE-1" path: a Redis hash, the weighted analog of * {@link RegionalEscrow}'s Lua). The grant arithmetic is identical to {@link regionFairPool}; only the * transport differs (a round-trip per grant). Because every grant is a network call, the methods are * `Promise`-returning — `federatedWeightedFairEscrow` consumes an async pool through `check()` (its * `checkSync()` becomes unavailable, exactly as a store-backed two-tier limiter's does). * * The `isAsync: true` marker discriminates it from the synchronous in-process {@link RegionFairPool} at * runtime without widening (and so breaking) that frozen interface. */ interface AsyncRegionFairPool { /** Discriminant: this pool's `grant`/`release` are asynchronous (a shared-store round-trip each). */ readonly isAsync: true; /** Global budget `L`. */ readonly limit: number; /** Window width in ms. */ readonly windowMs: number; /** The pool's clock (regions couple their tenant windows to it). */ readonly clock: Clock; /** Async {@link RegionFairPool.grant}: grant region `region` up to `wantTotal` total credits this window. */ grant(region: string, weight: number, wantTotal: number, now: number): Promise; /** Async {@link RegionFairPool.release}: drop a region from the active set. */ release(region: string, now: number): Promise; /** Async {@link RegionFairPool.stats}: read-only snapshot of the current window. */ stats(): Promise; } /** Narrow a pool to the async (store-backed) variant. */ declare function isAsyncRegionFairPool(pool: RegionFairPool | AsyncRegionFairPool): pool is AsyncRegionFairPool; /** * In-memory **async** {@link RegionFairPool} for tests + a single-process async deployment — it simply * wraps an in-process {@link regionFairPool} behind a `Promise`-returning surface, so its grants are * **byte-identical to the synchronous pool** (it *is* the same arithmetic). That makes it the conformance * bridge: a `federatedWeightedFairEscrow` over `testRegionFairPool` admits exactly what one over * `regionFairPool` does, and N regions sharing **one** instance hold `Σ granted ≤ L` — the property a * production {@link AsyncRegionFairPool} (e.g. `RedisRegionFairPool`) must replicate atomically in its store. */ declare function testRegionFairPool(options: RegionFairPoolOptions): AsyncRegionFairPool; /** Options for {@link federatedWeightedFairEscrow}. */ interface FederatedWeightedFairEscrowOptions { /** This region's identity. */ region: Region; /** * The shared cross-region pool (the global budget authority). All regions drawing from one global * budget `L` MUST share one instance. `limit`/`windowMs`/`clock` come from it. A synchronous * {@link RegionFairPool} (in-process, single arbiter) keeps `checkSync` available; an * {@link AsyncRegionFairPool} (store-backed, multi-process — DR-FWFE-1) routes through `check()` and * makes `checkSync` throw, exactly as a store-backed two-tier limiter does. */ pool: RegionFairPool | AsyncRegionFairPool; /** * Per-tenant weight `w_{t,r}` as seen in this region. For a region-local tenant, return its global * weight `w_t`. For a tenant active in MULTIPLE regions, return the demand-proportional SPLIT * `w_t·d_{t,r}/d_t` (so `Σ_r w_{t,r} = w_t`) — full `w_t` in every region double-counts it. `> 0`. * Default `() => 1`. */ weightOf?: (tenant: string) => number; /** * Bounded tenant set — caps the in-process per-tenant map (same role as `weightedFairEscrow.l1`). * Default unbounded; set on public surfaces above the expected tenant count. */ l1?: { maxKeys?: number; }; } /** A point-in-time read of a federated WFE region's current window. */ interface FederatedWeightedFairEscrowStats { readonly region: Region; readonly windowStart: number; /** Global budget `L`. */ readonly limit: number; /** Budget the pool has granted THIS region this window (its `L_r`). */ readonly regionBudget: number; /** This region's active aggregate weight `W_r = Σ wᵢ`. */ readonly activeWeight: number; /** Total used across this region's tenants this window. */ readonly totalUsed: number; readonly tenants: ReadonlyArray<{ readonly tenant: string; readonly weight: number; readonly used: number; }>; } /** A federated weighted-fair-escrow limiter for one region. See {@link federatedWeightedFairEscrow}. */ interface FederatedWeightedFairEscrowLimiter { /** Synchronous check (the in-process pool is sync). `cost` default 1. */ checkSync(tenant: string, cost?: number): Decision; /** Promise form of {@link FederatedWeightedFairEscrowLimiter.checkSync}. */ check(tenant: string, cost?: number): Promise; /** Reset one tenant's in-region usage, or — with no argument — the whole region (releases it from the pool). */ reset(tenant?: string): void; /** Read-only snapshot of this region's current window. */ stats(): FederatedWeightedFairEscrowStats; } /** * **Federated Weighted Fair Escrow** — per-region WFE composing (via a shared {@link RegionFairPool}) * to a GLOBAL weighted-max-min guarantee. See the module doc for the composition theorem. */ declare function federatedWeightedFairEscrow(options: FederatedWeightedFairEscrowOptions): FederatedWeightedFairEscrowLimiter; /** * `RedisRegionFairPool` — the production {@link AsyncRegionFairPool} (DR-FWFE-1): the cross-region * weighted-fair reservation pool with its region→{weight,granted} state in a **shared Redis hash**, so a * fleet of separate region processes draws from ONE global budget `L`. It is the weighted analog of * {@link RedisRegionalEscrow}, using the same atomic-EVALSHA-with-EVAL-fallback pattern one layer up. * * The grant is a single atomic Lua script that runs **the exact arithmetic of the in-process * {@link regionFairPool}** — weighted-max-min with reservation + borrow — so `Σ_r granted ≤ L` holds across * the fleet regardless of region count or interleaving. The conformance test pins it grant-for-grant against * the in-process oracle. * * Layout — one HASH per pool key: * * ws : the active window's start (epoch-ms, epoch-aligned) * w: : that region's current active aggregate weight * g: : that region's total grant this window (monotonic) * * The HASH carries PEXPIRE to the window boundary, so a rolled window auto-drops the prior state (and the * GRANT script clears it explicitly on the first touch after a roll, mirroring `regionFairPool.rollWindow`). * `useServerTime: true` (the default) makes the script read Redis `TIME` so node-clock skew never moves a * window boundary on the shared state — exactly as `RedisCoordinator` / `RedisRegionalEscrow` do. */ /** Options for {@link RedisRegionFairPool}. */ interface RedisRegionFairPoolOptions { /** An `ioredis` (or compatible) client. Use the adapters in `throttlekit/redis` for other clients. */ client: RedisClientLike; /** Global per-window budget `L`, shared across ALL regions. Floored to an integer; must be > 0. */ limit: number; /** Window width in ms (epoch-aligned). Must be > 0. All regions on this pool share it. */ windowMs: number; /** * The single pool key the regions share (the federation key — e.g. the policy name). Every region's * `federatedWeightedFairEscrow` on this pool MUST resolve the same key, or they hold separate budgets. */ key: string; /** Redis key prefix. Default `"tk:rfp"`. */ prefix?: string; /** * Use the Redis server clock (`TIME`) for the `now` in the grant's window math. Default `true` — protects * the shared window boundary from node clock skew. Set `false` in deterministic tests passing an explicit * `now` (the conformance suite does this with a `ManualClock`). NOTE: `now === 0` is the shared `LUA_NOW` * "use server TIME" sentinel, so a deterministic `now` must be **non-zero** (use a window-aligned epoch). */ useServerTime?: boolean; /** * The clock {@link federatedWeightedFairEscrow} uses for its OWN per-tenant window + decide timing. Default * {@link systemClock}. Independent of `useServerTime` (which governs only the shared region-window math); * with NTP the two stay within a few ms, as the coordinator/escrow layers already assume. */ clock?: Clock; } /** A store-backed {@link AsyncRegionFairPool}: the in-process pool's arithmetic, atomic over a Redis hash. */ declare class RedisRegionFairPool implements AsyncRegionFairPool { #private; readonly isAsync: true; readonly limit: number; readonly windowMs: number; readonly clock: Clock; constructor(options: RedisRegionFairPoolOptions); grant(region: string, weight: number, wantTotal: number, now: number): Promise; release(region: string, now: number): Promise; stats(): Promise; } /** * `LeaseSpender` — the **Tier-2 client-side spend** of a window-coupled lease, extracted verbatim from * the `twoTier(mode: "leased", lease: { windowCoupled: true })` L1 path (see `src/twotier/index.ts`: * `synthAllow` + the window-coupled discard + the local credit decrement). * * **The one-oracle line.** A high-throughput client leases a chunk of a global budget from the service * (the gRPC `Fleet.Reserve` door) and serves requests locally, round-tripping only to refresh — killing * the per-request network hop. The **server** (the core, behind the door) computes the grant *size* via * its coordinator/pool; the client may only **subtract from the granted balance and synthesize an * allow**. It never invents a denial: when local credits run short it returns {@link LeaseSpend.needsRefresh}, * and the *server's* authoritative `Decision` is surfaced verbatim when a refresh can't be granted. One * oracle therefore holds **iff** this local spend is byte-identical to the core's L1 spend — which the * golden lease vectors (`wire/vectors`) pin, and which the `lease-spender` conformance test proves against * the shipped `twoTier` leased path. * * **Why window-coupled.** Cross-window carryover of leased-but-unspent credits is the sole source of * leased overshoot. Coupling a credit's lifetime to the window that granted it — discarding the remainder * once `now >= expiresAt` — removes that source, holding the per-window global total to exactly the limit, * independent of how many clients lease concurrently. The grant's `expiresAt` is the **server/store** * window boundary; a client treats it as authoritative and never extends it (the clock-skew safety line). * * The spend is pure and synchronous — `now` is injected per call, like every core algorithm — so it is * deterministic and portable to any language a polyglot client is written in. */ /** Options for a {@link LeaseSpender}. Language-neutral: a port maps these to its own constructor. */ interface LeaseSpenderOptions { /** * The effective ceiling reported on a synthesized allow (the strategy's `limit` — the global * per-window budget). Surfaced as `Decision.limit`; does not bound local spend (the granted * `capacity` does that). */ limit: number; /** * Fallback for a synthesized allow's `resetAt` when no lease has been applied yet (mirrors the core * `synthAllow`'s `e.lastDecision?.resetAt ?? now + strategy.ttlMs`). In normal use a lease is always * applied before a credit is spent, so `expiresAt` drives `resetAt` and this is never read. Default 0. */ ttlMs?: number; /** * Discard a key's remaining credits once the window that granted them has rolled (`now >= expiresAt`), * rather than carrying them across the boundary. Default **true** — the safe, bound-tightening posture * the Tier-2 lease is designed around. Set false only to reproduce the legacy carry-over behaviour. */ windowCoupled?: boolean; } /** A grant the service door returned: `capacity` units valid until the `expiresAt` window boundary (epoch-ms). */ interface LeaseGrant { /** The **granted** units (may be `< wants` — a partial grant is legitimate). Never the requested amount. */ readonly capacity: number; /** Epoch-ms window boundary the grant is coupled to; the grant is invalid after this instant. */ readonly expiresAt: number; } /** A refusal the service door returned: its authoritative `Decision` (surfaced verbatim — never synthesized). */ interface LeaseDenied { readonly denied: Decision; } /** The outcome of a {@link LeaseSpender.spend}. */ type LeaseSpend = /** Served from local credits — a client-synthesized allow byte-identical to the core L1 path. */ { readonly needsRefresh: false; readonly decision: Decision; } /** Out of local credits — the caller must `Reserve` a refresh (or surface the server's denial). */ | { readonly needsRefresh: true; }; /** What the caller's refresh round-trip (`Fleet.Reserve`) yields: a grant, or the server's denial. */ type ReserveResult = LeaseGrant | LeaseDenied; /** A refresh round-trip: ask the service for up to `wants` units; resolve to a grant or the server's denial. */ type ReserveFn = (wants: number) => Promise; /** * Spends a window-coupled lease locally, synthesizing an allow per request and signalling when a refresh * is needed. One instance tracks one key's lease state (credits + the window they are coupled to). * * @example * ```ts * const spender = new LeaseSpender({ limit: 1000, ttlMs: 60_000 }); // windowCoupled defaults to true * // `reserve` performs the gRPC Fleet.Reserve round-trip (transport lives in the client, not here): * const decision = await spender.spendOrRefresh(now, 1, reserve); * if (!decision.allowed) backOff(decision.retryAfterMs); * ``` */ declare class LeaseSpender { #private; private readonly limit; private readonly ttlMs; private readonly windowCoupled; /** Local leased credits available to spend without a round trip. */ private _credits; /** Epoch-ms window boundary the current credits are coupled to; undefined until the first grant. */ private _expiresAt; constructor(options: LeaseSpenderOptions); /** Local leased credits currently available (post-discard is applied lazily on the next {@link spend}). */ get credits(): number; /** Epoch-ms window boundary the current credits are coupled to, or undefined before the first grant. */ get expiresAt(): number | undefined; /** * Apply a granted lease: add its `capacity` to local credits and couple them to its `expiresAt` window. * Mirrors the core leased path's `credits += leaseAmount; lastDecision = d` on an admitted lease. */ applyLease(grant: LeaseGrant): void; /** * Discard credits whose granting window has rolled (`now >= expiresAt`). Idempotent; folded into every * {@link spend}, exposed for a caller that wants to reclaim eagerly. */ private expireIfRolled; /** * Try to serve one request of `cost` (default 1) from local credits at `now`. * * Returns a client-synthesized allow when credits suffice (byte-identical to the core L1 `synthAllow`), * else `{ needsRefresh: true }` — the caller must `Reserve` more budget (and surface the server's denial * if none is granted). Never synthesizes a denial; never performs I/O. */ spend(now: number, cost?: number): LeaseSpend; /** * The full client loop: spend locally, and on a shortfall `Reserve` a refresh and retry. Returns a * `Decision` — a local allow, or the **server's** denial verbatim when the global budget is spent. The * `reserve` callback owns the transport (gRPC `Fleet.Reserve`); this method owns only the spend. * * A grant always makes progress (the server grants `>= 1` or denies), so the loop converges within a * window; `maxRounds` is a defensive backstop against a misbehaving `reserve` that neither grants nor denies. */ spendOrRefresh(now: number, cost: number, reserve: ReserveFn, maxRounds?: number): Promise; /** Forget all local credits and the current window coupling (e.g. on a hard reset / reconnect). */ reset(): void; } /** L1/L2 coordination mode. See docs and THROTTLEKIT.md §8. */ type TwoTierMode = "strict" | "cached-deny" | "leased"; interface LeaseOptions { /** * Tokens leased from L2 per refill. Larger batch ⇒ fewer round trips, larger overshoot bound. * Required unless {@link LeaseOptions.adaptive} is set — with adaptive sizing this is an optional * per-key warm-start size; the online learner takes over from there. */ batch?: number; /** * When the local budget is at or below this level, refill asynchronously (so requests never * block on the network). Default 0, which disables proactive refill — purely lease-on-demand, * giving the tightest overshoot bound (≤ L×batch). Set > 0 to hide lease latency at the cost of * a looser bound (≤ L×(batch+lowWater)). */ lowWater?: number; /** Drop a key's idle local credits after this many ms. Capacity self-heals via L2 refill. */ returnIdleAfterMs?: number; /** * Couple leased-credit lifetime to the L2 window: when the L2 window that granted a key's local * credits has rolled over (i.e. `now >= the lease's resetAt`), discard those credits instead of * carrying them across the boundary. Cross-window carryover is the *sole* source of leased * overshoot, so this tightens the global per-window bound from `Limit + L×(batch−1)` to exactly * `Limit` — independent of the node count `L` — at the cost of one re-lease per node just after * each boundary. Intended for a fixed-window L2 strategy (the case the bound is proven for, in * `spec/GaleWindowCoupledLeasing.tla`). Default false (credits carry over — the legacy behaviour). */ windowCoupled?: boolean; /** * **Adaptive (online) lease sizing — GALE Pillar 2.** Instead of a fixed {@link LeaseOptions.batch}, * size each key's batch online with a {@link leaseSizer}: every L2 window the limiter feeds the * learner the demand that key actually served and reads back the batch for the next window, descending * onto the EOQ optimum `√(2·orderCost·demand/strandPenalty)` and tracking drift. One independent * learner per key (cold keys evict with their entry — bound them with {@link L1Options.maxKeys}). * * Pass {@link LeaseSizerOptions} (the limiter builds a `leaseSizer` per key) or a `() => LeaseSizer` * factory for a custom per-key learner. Safety is untouched: by Pillar 1 the per-window global bound * holds for *any* batch the learner emits (exactly `Limit` under {@link LeaseOptions.windowCoupled}), * so adaptive sizing only trades coordination against stranding — it can never loosen the cap. */ adaptive?: LeaseSizerOptions | (() => LeaseSizer); } interface L1Options { /** * Max distinct keys held locally before approximate (CLOCK-style) eviction. **Unbounded when * omitted** — set this on public-facing endpoints so a flood of unique keys can't grow the local * `credits`/`lastDecision`/`lastUse` maps without limit (the same stance as `MemoryStore`'s * `maxKeys`). The `cached-deny` deny-cache is bounded by the same value. */ maxKeys?: number; } interface TwoTierOptions { /** The algorithm enforced at L2 (and, for leasing, the unit of the leased budget). */ strategy: Strategy; /** The distributed store (e.g. RedisStore). */ l2: Store; /** Coordination mode. */ mode: TwoTierMode; /** Required for `leased` mode. */ lease?: LeaseOptions; /** Local-tier tuning. */ l1?: L1Options; /** Injected clock. */ clock?: Clock; /** Key namespace. */ prefix?: string; } /** * A two-tier limiter: a local in-process tier (L1) fronting a distributed tier (L2), with a * selectable consistency/throughput trade-off. * * - `strict`: every check consults L2 (exact, 1 round trip / request). * - `cached-deny`: denials are cached locally for their `retryAfterMs`, so an abusive client can't * translate a flood into L2 load; allowed traffic stays globally exact. * - `leased`: each node leases a batch of tokens from L2 and serves them locally, driving * steady-state network cost toward ~1 round trip per `batch` requests, with a bounded global * overshoot (≤ L×batch with the default `lowWater: 0`). */ declare function twoTier(options: TwoTierOptions): Limiter; export { type AsyncRegionFairPool, type FederatedWeightedFairEscrowLimiter, type FederatedWeightedFairEscrowOptions, type FederatedWeightedFairEscrowStats, type L1Options, type LeaseDenied, type LeaseGrant, type LeaseOptions, type LeaseSizer, type LeaseSizerOptions, type LeaseSpend, LeaseSpender, type LeaseSpenderOptions, type PredictiveLeaseSizer, type PredictiveLeaseSizerOptions, RedisRegionFairPool, type RedisRegionFairPoolOptions, type RegionFairPool, type RegionFairPoolOptions, type RegionFairPoolStats, type ReserveFn, type ReserveResult, type TwoTierMode, type TwoTierOptions, type WeightedFairEscrowLimiter, type WeightedFairEscrowOptions, type WeightedFairEscrowStats, eoqOptimum, federatedWeightedFairEscrow, isAsyncRegionFairPool, leaseSizer, predictiveLeaseSizer, regionFairPool, testRegionFairPool, twoTier, weightedFairEscrow };