{"version":3,"file":"resilience.cjs","names":[],"sources":["../../src/ws/resilience.ts"],"sourcesContent":["/** Why a socket stopped trying to come back. */\nexport type WebSocketLostReason = \"rejected\" | \"exhausted\";\n\n/**\n * First close code in the range a server uses to refuse a peer outright.\n *\n * `tempest-fastapi-sdk` closes with 4400 (invalid room), 4401 (unauthorized),\n * 4403 (forbidden) and 4409 (full) — none of which get better by trying again,\n * so retrying only reproduces the rejection while the person watches a spinner.\n */\nexport const REJECTION_CLOSE_MIN = 4400;\n\n/** Last close code in the rejection range. */\nexport const REJECTION_CLOSE_MAX = 4499;\n\n/**\n * Heartbeat timeout, which sits inside the rejection range but is not one.\n *\n * `tempest-fastapi-sdk` closes with 4408 when no `pong` arrived within\n * `WS_HEARTBEAT_TIMEOUT_SECONDS`. That is the *link* failing, not the server\n * refusing the peer — exactly the case reconnection exists for. Treating the\n * whole 4400–4499 range as fatal, which is the obvious reading, makes one\n * missed pong permanent.\n */\nexport const HEARTBEAT_CLOSE_CODE = 4408;\n\n/**\n * Clean close codes that still deserve a retry.\n *\n * A close is normally taken as final when the closing handshake completed\n * (`wasClean`), because that means the server said so on purpose. These four\n * say the opposite: the server is going away for a reason that ends —\n * 1001 going away, 1011 internal error, 1012 service restart (a deploy),\n * 1013 try again later — and the socket comes back if anyone asks again.\n */\nconst RETRYABLE_CLEAN_CLOSE_CODES: ReadonlySet<number> = new Set([1001, 1011, 1012, 1013]);\n\n/**\n * Whether a close code means the server refused this client for good.\n *\n * @param code - The `CloseEvent.code`.\n * @returns `true` when reconnecting can only reproduce the refusal.\n *\n * @example\n * isRejectionCloseCode(4401); // true — unauthorized\n * isRejectionCloseCode(4408); // false — heartbeat timeout, retry it\n */\nexport function isRejectionCloseCode(code: number): boolean {\n    return (\n        code >= REJECTION_CLOSE_MIN && code <= REJECTION_CLOSE_MAX && code !== HEARTBEAT_CLOSE_CODE\n    );\n}\n\n/**\n * Whether a closed socket should be reopened.\n *\n * An unclean close is always retried: the connection died rather than ended.\n * A clean one is retried only for the codes that describe a server which is\n * temporarily unavailable, plus the heartbeat timeout — a server that closes\n * cleanly with 1000 meant it.\n *\n * @param code - The `CloseEvent.code`.\n * @param wasClean - The `CloseEvent.wasClean` flag.\n * @returns `true` when the connection is worth reopening.\n */\nexport function shouldRetryClose(code: number, wasClean: boolean): boolean {\n    if (isRejectionCloseCode(code)) return false;\n    if (!wasClean) return true;\n    return code === HEARTBEAT_CLOSE_CODE || RETRYABLE_CLEAN_CLOSE_CODES.has(code);\n}\n\n/** Shape of the backoff schedule. */\nexport interface BackoffOptions {\n    /** Delay before the first retry (ms), doubled each attempt. */\n    initialBackoff: number;\n    /** Ceiling for the doubling (ms). */\n    maxBackoff: number;\n    /** Fraction of the delay added at random, 0–1. */\n    jitter: number;\n}\n\n/**\n * Delay before retry number `attempt` (0-based), with jitter.\n *\n * The jitter matters when the *server* is what went down: every client that was\n * connected to it wakes on the same schedule and retries in the same\n * millisecond, so the box comes back up into a synchronized stampede and drops\n * the connections again. Spreading each delay by a fraction of itself breaks\n * the alignment, and it only ever adds time — the floor stays predictable.\n *\n * @param attempt - Zero-based retry index.\n * @param options - Schedule shape.\n * @param random - Source of randomness, injectable for tests. Defaults to `Math.random`.\n * @returns Delay in milliseconds.\n *\n * @example\n * backoffDelay(0, { initialBackoff: 1000, maxBackoff: 30000, jitter: 0.3 });\n * // 1000–1300\n */\nexport function backoffDelay(\n    attempt: number,\n    { initialBackoff, maxBackoff, jitter }: BackoffOptions,\n    random: () => number = Math.random,\n): number {\n    const base = Math.min(initialBackoff * 2 ** attempt, maxBackoff);\n    if (jitter <= 0) return base;\n    return base + random() * base * jitter;\n}\n"],"mappings":"AAUA,IAAa,EAAsB,KAGtB,EAAsB,KAWtB,EAAuB,KAW9B,EAAmD,IAAI,IAAI,CAAC,KAAM,KAAM,KAAM,IAAI,CAAC,EAYzF,SAAgB,EAAqB,EAAuB,CACxD,OACI,GAAA,MAA+B,GAAA,MAA+B,IAAA,IAEtE,CAcA,SAAgB,EAAiB,EAAc,EAA4B,CAGvE,OAFI,EAAqB,CAAI,EAAU,GACvC,CAAK,GACE,IAAA,MAAiC,EAA4B,IAAI,CAAI,CAChF,CA8BA,SAAgB,EACZ,EACA,CAAE,iBAAgB,aAAY,UAC9B,EAAuB,KAAK,OACtB,CACN,IAAM,EAAO,KAAK,IAAI,EAAiB,GAAK,EAAS,CAAU,EAE/D,OADI,GAAU,EAAU,EACjB,EAAO,EAAO,EAAI,EAAO,CACpC"}