{"version":3,"sources":["../src/protocol.ts"],"names":[],"mappings":";;;AAUO,IAAM,gBAAA,GAAmB;AAGzB,IAAM,YAAA,GAAe;AAGrB,IAAM,mBAAA,GAAsB;AAG5B,IAAM,oBAAA,GAAuB;AAM7B,IAAM,QAAA,GAAW;AAAA;AAAA,EAEtB,WAAA,EAAa,CAAA;AAAA;AAAA,EAEb,eAAA,EAAiB,CAAA;AAAA;AAAA,EAEjB,iBAAA,EAAmB,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAKnB,gBAAA,EAAkB,IAAA;AAAA;AAAA,EAElB,eAAA,EAAiB,GAAA;AAAA;AAAA,EAEjB,eAAA,EAAiB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAKjB,cAAA,EAAgB,IAAA;AAAA;AAAA,EAEhB,WAAA,EAAa;AACf;AAGO,IAAM,uBAAA,GAA0B,CAAC,QAAA,EAAU,UAAU;AA6SrD,SAAS,mBAAmB,GAAA,EAAmC;AACpE,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC1B,IAAA,OAAO,OAAO,GAAA,EAAK,IAAA,KAAS,QAAA,GAAW,GAAA,GAAM,IAAA;AAAA,EAC/C,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAGO,SAAS,mBAAmB,GAAA,EAAmC;AACpE,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC1B,IAAA,IAAI,OAAO,GAAA,EAAK,IAAA,KAAS,QAAA,EAAU,OAAO,IAAA;AAC1C,IAAA,IAAI,GAAA,CAAI,IAAA,CAAK,UAAA,CAAW,QAAQ,GAAG,OAAO,IAAA;AAC1C,IAAA,OAAO,GAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAGO,SAAS,wBAAwB,GAAA,EAAwC;AAC9E,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC1B,IAAA,IAAI,OAAO,GAAA,EAAK,IAAA,KAAS,QAAA,IAAY,CAAC,IAAI,IAAA,CAAK,UAAA,CAAW,QAAQ,CAAA,EAAG,OAAO,IAAA;AAC5E,IAAA,OAAO,GAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAGO,SAAS,wBAAwB,GAAA,EAAwC;AAC9E,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC1B,IAAA,IAAI,OAAO,GAAA,EAAK,IAAA,KAAS,QAAA,IAAY,CAAC,IAAI,IAAA,CAAK,UAAA,CAAW,QAAQ,CAAA,EAAG,OAAO,IAAA;AAC5E,IAAA,OAAO,GAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF","file":"protocol.cjs","sourcesContent":["/**\n * Shared wire protocol for `@reactor-team/queue`.\n *\n * Both the PartyKit server (`@reactor-team/queue/server`) and the browser\n * client (`@reactor-team/queue`) import these types so the messages they\n * exchange over the WebSocket stay in lockstep. Nothing here depends on\n * PartyKit, the Reactor SDK, React, or the DOM — it is plain data.\n */\n\n/** Current protocol version. Bumped only on breaking wire changes. */\nexport const PROTOCOL_VERSION = 2 as const;\n\n/** Default PartyKit room id. A single room is the source of truth for one queue. */\nexport const DEFAULT_ROOM = \"reactor-queue\";\n\n/** Query-string key used to carry the stable per-browser id on connect. */\nexport const CLIENT_ID_QUERY_KEY = \"rqClientId\";\n\n/** Set to `1` on the WebSocket URL to open an admin connection (not queued). */\nexport const ADMIN_MODE_QUERY_KEY = \"rqAdmin\";\n\n/**\n * Default tunables. Every one of these is overridable from server config and/or\n * environment variables (see `@reactor-team/queue/server`).\n */\nexport const DEFAULTS = {\n  /** Max concurrent Reactor sessions (GPU ceiling). */\n  maxSessions: 1,\n  /** Members per session (default 1 = today's behavior; >1 when platform allows N). */\n  usersPerSession: 1,\n  /** Full session budget once a user has `claim()`ed their slot. */\n  sessionDurationMs: 120_000,\n  /**\n   * Grace window an admitted user gets to actually start (claim) their session\n   * before the slot is reclaimed. Prevents an idle admit from wasting a slot.\n   */\n  admissionGraceMs: 45_000,\n  /** How long before expiry to emit a `time_warning`. */\n  warningBeforeMs: 30_000,\n  /** Lifetime requested for each minted Reactor JWT. Deliberately short. */\n  tokenTtlSeconds: 60,\n  /**\n   * How often the server re-checks tracked live sessions against the Reactor\n   * API to catch sessions that ended without a clean `session_ended`/close.\n   */\n  pollIntervalMs: 15_000,\n  /** Client-side skew: refresh the JWT this long before it actually expires. */\n  tokenSkewMs: 10_000,\n} as const;\n\n/** Reactor session states that mean \"the slot is free again\". */\nexport const TERMINAL_SESSION_STATES = [\"CLOSED\", \"INACTIVE\"] as const;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Server → Client messages\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** You are waiting in line. `position` is 1-based. */\nexport interface QueuePositionMessage {\n  type: \"queue_position\";\n  position: number;\n  total: number;\n  active: number;\n  capacity: number;\n}\n\n/**\n * You reached the front and a capacity slot is reserved for you. No Reactor\n * session exists yet — the server creates it only when you `claim()`, so an\n * abandoned grace never leaves an orphaned GPU session. You have until the\n * admission grace expires to `claim()`.\n */\nexport interface AdmittedMessage {\n  type: \"admitted\";\n  active: number;\n  /** Total live users = maxSessions * usersPerSession. */\n  capacity: number;\n  /** ms the client has to `claim()` before the slot is reclaimed. */\n  graceMs: number;\n  /** Full session budget (ms) the client receives once it `claim()`s. For countdown UI. */\n  sessionDurationMs: number;\n}\n\n/**\n * Sent after `claim()`: the server has created (or reused) the Reactor session\n * and minted a WebRTC connection under it for this member. Attach with\n * `connect({ sessionId, connectionId })` — the server owns both, so the client\n * never creates or stops anything.\n */\nexport interface SessionReadyMessage {\n  type: \"session_ready\";\n  /** Reactor session id created by the server — pass to connect({ sessionId }). */\n  sessionId: string;\n  /**\n   * Server-minted WebRTC connection id for this member — pass to\n   * connect({ connectionId }). The server registered it under `sessionId`, so\n   * the client adopts it instead of registering its own.\n   */\n  connectionId: number;\n  /** Full session budget (ms). */\n  sessionDurationMs: number;\n  /** Unix epoch ms when the session ends. */\n  expiresAt: number;\n}\n\n/** A freshly minted, short-lived Reactor JWT. Sent on admission and on each `request_token`. */\nexport interface TokenMessage {\n  type: \"token\";\n  jwt: string;\n  /** Unix epoch seconds at which the JWT expires. */\n  expiresAt: number;\n}\n\n/** Your session is about to end. */\nexport interface TimeWarningMessage {\n  type: \"time_warning\";\n  secondsLeft: number;\n  /** Unix epoch ms when the session ends. */\n  expiresAt: number;\n}\n\n/** Your session ended (time ran out, or the server reclaimed the slot). */\nexport interface ExpiredMessage {\n  type: \"expired\";\n  reason: \"timeout\" | \"grace_timeout\" | \"server\";\n}\n\n/** You were refused entry. */\nexport interface RejectedMessage {\n  type: \"rejected\";\n  reason: \"already_connected\" | \"server_error\" | \"forbidden_origin\" | string;\n}\n\n/** A non-fatal error (e.g. token mint failed); the client may retry. */\nexport interface ErrorMessage {\n  type: \"error\";\n  message: string;\n}\n\nexport type ServerMessage =\n  | QueuePositionMessage\n  | AdmittedMessage\n  | SessionReadyMessage\n  | TokenMessage\n  | TimeWarningMessage\n  | ExpiredMessage\n  | RejectedMessage\n  | ErrorMessage;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Client → Server messages\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** \"I'm actually entering the demo\" — upgrades the grace window to the full session. */\nexport interface ClaimMessage {\n  type: \"claim\";\n}\n\n/** Ask for a fresh JWT. The server only answers if you currently hold a slot. */\nexport interface RequestTokenMessage {\n  type: \"request_token\";\n}\n\n/** The user ended the Reactor session from the client; free the slot now. */\nexport interface SessionEndedMessage {\n  type: \"session_ended\";\n}\n\n/** Leave the queue / release the slot without intending to rejoin. */\nexport interface LeaveMessage {\n  type: \"leave\";\n}\n\nexport type ClientMessage = ClaimMessage | RequestTokenMessage | SessionEndedMessage | LeaveMessage;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Admin mode (server → admin client)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Read-only server tunables included in every admin snapshot. */\nexport interface AdminConfigSnapshot {\n  maxSessions: number;\n  usersPerSession: number;\n  capacity: number;\n  model: string;\n  webrtcVersion: string;\n  sessionDurationMs: number;\n  admissionGraceMs: number;\n  warningBeforeMs: number;\n  tokenTtlSeconds: number;\n  pollIntervalMs: number;\n  coordinatorUrl: string;\n  apiVersion: number;\n  stopSessionsOnExpiry: boolean;\n  allowDuplicateConnections: boolean;\n  /** \"default\" = queue creates/stops sessions; \"custom\" = acquire/release overridden. */\n  sessionSource: \"default\" | \"custom\";\n}\n\n/** One person waiting in the FIFO queue. */\nexport interface AdminQueuedUserSnapshot {\n  connId: string;\n  /** 1-based position in line. */\n  position: number;\n  clientId: string | null;\n}\n\n/** One admitted member (may or may not have claimed yet). */\nexport interface AdminMemberSnapshot {\n  connId: string;\n  /** Reactor session id once claimed; null while still in grace (no session yet). */\n  sessionId: string | null;\n  /** Server-minted WebRTC connection id once claimed; null while still in grace. */\n  connectionId: number | null;\n  clientId: string | null;\n  claimed: boolean;\n  expiresAt: number;\n  msLeft: number;\n}\n\n/** One capacity slot and its member connection ids. */\nexport interface AdminSessionSnapshot {\n  /** Reactor session id, or null while the slot is reserved but unclaimed (no GPU session yet). */\n  sessionId: string | null;\n  members: string[];\n  createdAt: number;\n  msSinceCreated: number;\n}\n\n/** Full room state pushed to authenticated admin connections. */\nexport interface AdminSnapshotMessage {\n  type: \"admin_snapshot\";\n  at: number;\n  activeCount: number;\n  sessionCount: number;\n  config: AdminConfigSnapshot;\n  queue: AdminQueuedUserSnapshot[];\n  sessions: AdminSessionSnapshot[];\n  members: AdminMemberSnapshot[];\n}\n\n/** Admin WebSocket authenticated; snapshots follow on changes. */\nexport interface AdminReadyMessage {\n  type: \"admin_ready\";\n}\n\nexport interface AdminRejectedMessage {\n  type: \"admin_rejected\";\n  reason: \"admin_disabled\" | \"invalid_password\" | \"auth_required\" | \"forbidden_origin\";\n}\n\nexport interface AdminActionResultMessage {\n  type: \"admin_action_result\";\n  action: \"kick_member\" | \"kick_queued\" | \"close_session\";\n  ok: boolean;\n  message?: string;\n}\n\n/** Severity of an {@link AdminLogEntry}. Mirrors `console.log`/`warn`/`error`. */\nexport type AdminLogLevel = \"info\" | \"warn\" | \"error\";\n\n/**\n * One structured server event. The queue server emits these for every notable\n * thing that happens in a room — a user joining, an admission, a session being\n * created or closed, and crucially the **reason an API call failed** (e.g. a\n * Coordinator quota rejection, with its HTTP status and body in `data`). They\n * are streamed live to admins and kept in a bounded server-side ring buffer so\n * a freshly-connected admin sees recent history.\n */\nexport interface AdminLogEntry {\n  /** Stable unique id (also usable as a React key). */\n  id: string;\n  /** Unix epoch ms when the event happened. */\n  at: number;\n  level: AdminLogLevel;\n  /** Machine-readable event code, e.g. `\"user_admitted\"`, `\"session_create_failed\"`. */\n  event: string;\n  /** Human-readable, already-formatted summary line. */\n  message: string;\n  /** The connection this event concerns, when applicable. */\n  connId?: string;\n  /** The Reactor session this event concerns, when applicable. */\n  sessionId?: string;\n  /** Extra structured context (HTTP status, response body, reason, …). */\n  data?: Record<string, unknown>;\n}\n\n/** A single new log line, pushed live to authenticated admins as it happens. */\nexport interface AdminLogMessage {\n  type: \"admin_log\";\n  entry: AdminLogEntry;\n}\n\n/** Recent log history (oldest → newest), sent once right after admin auth. */\nexport interface AdminLogHistoryMessage {\n  type: \"admin_log_history\";\n  entries: AdminLogEntry[];\n}\n\nexport type AdminServerMessage =\n  | AdminReadyMessage\n  | AdminRejectedMessage\n  | AdminSnapshotMessage\n  | AdminActionResultMessage\n  | AdminLogMessage\n  | AdminLogHistoryMessage;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Admin mode (admin client → server)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** First message on an admin connection; password must match `RQ_ADMIN_PASSWORD`. */\nexport interface AdminAuthMessage {\n  type: \"admin_auth\";\n  password: string;\n}\n\n/** Remove a member from their session and free capacity (same as forced expiry). */\nexport interface AdminKickMemberMessage {\n  type: \"admin_kick_member\";\n  connId: string;\n}\n\n/** Drop a still-waiting connection from the queue and close its socket. */\nexport interface AdminKickQueuedMessage {\n  type: \"admin_kick_queued\";\n  connId: string;\n}\n\n/** Stop the Reactor session and evict all members. */\nexport interface AdminCloseSessionMessage {\n  type: \"admin_close_session\";\n  sessionId: string;\n}\n\n/** Request a fresh snapshot (also sent automatically on room changes). */\nexport interface AdminRefreshMessage {\n  type: \"admin_refresh\";\n}\n\nexport type AdminClientMessage =\n  | AdminAuthMessage\n  | AdminKickMemberMessage\n  | AdminKickQueuedMessage\n  | AdminCloseSessionMessage\n  | AdminRefreshMessage;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Narrowing parse for an inbound server message. Returns null on garbage. */\nexport function parseServerMessage(raw: string): ServerMessage | null {\n  try {\n    const msg = JSON.parse(raw) as ServerMessage;\n    return typeof msg?.type === \"string\" ? msg : null;\n  } catch {\n    return null;\n  }\n}\n\n/** Narrowing parse for an inbound client message. Returns null on garbage. */\nexport function parseClientMessage(raw: string): ClientMessage | null {\n  try {\n    const msg = JSON.parse(raw) as ClientMessage;\n    if (typeof msg?.type !== \"string\") return null;\n    if (msg.type.startsWith(\"admin_\")) return null;\n    return msg;\n  } catch {\n    return null;\n  }\n}\n\n/** Parse an admin client message. Returns null on garbage or non-admin types. */\nexport function parseAdminClientMessage(raw: string): AdminClientMessage | null {\n  try {\n    const msg = JSON.parse(raw) as AdminClientMessage;\n    if (typeof msg?.type !== \"string\" || !msg.type.startsWith(\"admin_\")) return null;\n    return msg;\n  } catch {\n    return null;\n  }\n}\n\n/** Parse a server message sent to an admin connection. */\nexport function parseAdminServerMessage(raw: string): AdminServerMessage | null {\n  try {\n    const msg = JSON.parse(raw) as AdminServerMessage;\n    if (typeof msg?.type !== \"string\" || !msg.type.startsWith(\"admin_\")) return null;\n    return msg;\n  } catch {\n    return null;\n  }\n}\n"]}