/** * Host-wide shared Telegram rate-limit pool for the threaded session surface. * * Multiple GJC sessions on one host share a single bot token and paired chat. * Telegram enforces per-bot/per-chat limits (~1 message/sec, bursts up to ~20), * so the singleton notifications daemon owns ONE pool that all per-session * threads draw from. The pool provides: * * - a token bucket (burst capacity + steady refill) modelling the chat limit; * - priority lanes (`ask` > `finalized` > `live` > `idle`) so urgent frames * win scarce tokens; * - per-session round-robin fairness within a lane so one session's live-edit * stream cannot starve other sessions; * - coalescing of live edits that share a `coalesceKey` (the latest rendered * text replaces the queued one) so throttled edit storms collapse. * * The core is a pull-based scheduler with an injectable clock so fairness, * starvation, and burst behaviour are deterministically unit-testable without * real time or a live Bot API. */ /** Delivery lanes in descending priority. */ export type RateLimitLane = "ask" | "finalized" | "live" | "idle"; /** Lanes ordered from highest to lowest priority. */ export declare const LANE_PRIORITY: readonly RateLimitLane[]; /** A unit of work competing for a send slot. */ export interface RateLimitItem { /** Owning session id (used for per-session fairness). */ sessionId: string; /** Priority lane. */ lane: RateLimitLane; /** * Optional coalesce key. Submitting another unidentifiable item with the * same `(sessionId, lane, coalesceKey)` replaces the queued payload with * the newer one instead of enqueuing a duplicate (used for live edits). */ coalesceKey?: string; /** Optional stable identifier for exact queued-item removal. Identified items never coalesce. */ itemId?: string; /** Absolute Unix timestamp in ms. The item expires when `now >= deadlineAt`. */ deadlineAt?: number; /** Opaque payload the caller maps to an actual Telegram send. */ payload: T; } export type RateLimitDisposition = "queued" | "sending" | "accepted" | "rejected" | "ambiguous" | "removed" | "expired"; export interface RateLimitHandle { itemId: string; settled: Promise>; } /** Options for {@link RateLimitPool}. */ export interface RateLimitPoolOptions { /** Burst capacity (max tokens). Default 20 (Telegram per-chat burst). */ capacity?: number; /** Steady refill rate in tokens per second. Default 1 (~1 msg/sec/chat). */ refillPerSec?: number; /** Injectable clock in ms. Default `Date.now`. */ now?: () => number; } /** The deterministic result of draining queued work at a point in time. */ export interface RateLimitDrainResult { /** Items granted a token and ready to send. */ granted: RateLimitItem[]; /** Items removed because their absolute deadline has elapsed. */ expired: RateLimitItem[]; } /** * A deterministic, pull-based shared rate-limit scheduler. * * Callers {@link submit} work and periodically {@link drain} (e.g. on a timer * or after each submit); `drain` returns the items granted a send slot, in the * order they should be sent. */ export declare class RateLimitPool { private readonly capacity; private readonly refillPerSec; private readonly now; /** Per-lane FIFO queues; each lane holds items across sessions. */ private readonly lanes; /** Rotating session cursor per lane for round-robin fairness. */ private readonly laneCursor; private tokens; private lastRefill; private readonly settlements; private seqCounter; constructor(options?: RateLimitPoolOptions); /** Number of items currently queued across all lanes. */ get pending(): number; /** Whether any queued item currently matches the predicate. */ someQueued(predicate: (item: RateLimitItem) => boolean): boolean; /** Current available token count (after refill at `now`). */ availableTokens(nowMs?: number): number; /** * Submit an item. Unidentified items with a `coalesceKey` matching a queued * item in the same `(sessionId, lane)` replace its payload (latest wins) * while preserving FIFO position; identified items are always appended. */ submit(item: RateLimitItem): RateLimitHandle; /** Settle a stable item after its external transport effect has a known outcome. */ settle(itemId: string, disposition: Exclude): void; /** Mark a granted item as owned by an external transport effect. */ markSending(itemId: string): void; private handle; /** * Grant as many queued items as tokens allow at `nowMs`. This compatibility * wrapper discards items that expired during the drain. */ drain(nowMs?: number): RateLimitItem[]; /** * Deterministically expire elapsed-deadline items, then grant as many live * items as tokens allow. Expired items never consume tokens or receive a * grant; both result lists preserve their deterministic queue ordering. */ drainWithExpired(nowMs?: number): RateLimitDrainResult; /** Remove queued items matching `predicate` without consuming tokens. Returns removed items in lane/FIFO order. */ removeWhere(predicate: (item: RateLimitItem) => boolean, disposition?: Exclude): RateLimitItem[]; /** Remove exactly one queued item by its stable id without consuming a token. */ removeById(itemId: string): RateLimitItem | undefined; private refill; /** Pop the next item by lane priority + per-session round-robin fairness. */ private takeNext; /** * Choose the index to serve from a lane queue using round-robin over the * distinct session ids present, starting just after the last-served * session. Falls back to FIFO (index 0) when only one session is queued. */ private pickFairIndex; }