{"version":3,"sources":["../src/protocol.ts","../src/admin-types.ts","../src/admin-client.ts"],"names":["PartySocket"],"mappings":"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","file":"admin-client.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","import type { AdminActionResultMessage, AdminLogEntry, AdminSnapshotMessage } from \"./protocol\";\n\nexport type AdminPhase = \"idle\" | \"connecting\" | \"ready\" | \"rejected\" | \"disconnected\";\n\nexport interface AdminState {\n  phase: AdminPhase;\n  snapshot: AdminSnapshotMessage | null;\n  reason: string | null;\n  lastAction: AdminActionResultMessage | null;\n  /**\n   * Activity log, oldest → newest. Seeded with recent history on connect, then\n   * appended live as the server streams events. Capped client-side (newest kept).\n   */\n  logs: AdminLogEntry[];\n}\n\nexport const INITIAL_ADMIN_STATE: AdminState = {\n  phase: \"idle\",\n  snapshot: null,\n  reason: null,\n  lastAction: null,\n  logs: [],\n};\n\n/** Max log entries retained client-side. The newest are kept past this. */\nexport const MAX_CLIENT_LOGS = 300;\n\nexport type AdminPasswordSource = string | (() => string | Promise<string>);\n\nexport interface ReactorQueueAdminClientOptions {\n  host: string;\n  room?: string;\n  party?: string;\n  /** How the client authenticates. You supply the secret (env, input, hardcoded, etc.). */\n  password: AdminPasswordSource;\n  autoConnect?: boolean;\n  /**\n   * Poll a fresh snapshot every N ms once authenticated. The server also pushes\n   * on every room change; polling keeps the `msLeft` countdowns live. 0 disables.\n   */\n  refreshIntervalMs?: number;\n}\n","import PartySocket from \"partysocket\";\nimport {\n  ADMIN_MODE_QUERY_KEY,\n  DEFAULT_ROOM,\n  parseAdminServerMessage,\n  type AdminClientMessage,\n} from \"./protocol\";\nimport {\n  INITIAL_ADMIN_STATE,\n  MAX_CLIENT_LOGS,\n  type AdminPasswordSource,\n  type AdminState,\n  type ReactorQueueAdminClientOptions,\n} from \"./admin-types\";\n\ntype Listener = (state: AdminState) => void;\n\nasync function resolvePassword(source: AdminPasswordSource): Promise<string> {\n  if (typeof source === \"function\") return source();\n  return source;\n}\n\n/**\n * WebSocket client for queue admin mode. Does not join the FIFO queue; after\n * auth it receives {@link AdminSnapshotMessage} updates and can kick members or\n * close sessions.\n */\nexport class ReactorQueueAdminClient {\n  private readonly opts: Required<\n    Omit<ReactorQueueAdminClientOptions, \"password\" | \"room\" | \"party\">\n  > & { room: string; party?: string; password: AdminPasswordSource };\n\n  private socket: PartySocket | null = null;\n  private destroyed = false;\n  private state: AdminState = { ...INITIAL_ADMIN_STATE };\n  private listeners = new Set<Listener>();\n  private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n  constructor(options: ReactorQueueAdminClientOptions) {\n    this.opts = {\n      host: options.host,\n      room: options.room ?? DEFAULT_ROOM,\n      party: options.party,\n      password: options.password,\n      autoConnect: options.autoConnect ?? false,\n      refreshIntervalMs: options.refreshIntervalMs ?? 0,\n    };\n    if (this.opts.autoConnect) this.connect();\n  }\n\n  getState(): AdminState {\n    return this.state;\n  }\n\n  subscribe(listener: Listener): () => void {\n    this.listeners.add(listener);\n    listener(this.state);\n    return () => this.listeners.delete(listener);\n  }\n\n  private setState(patch: Partial<AdminState>): void {\n    this.state = { ...this.state, ...patch };\n    for (const l of this.listeners) l(this.state);\n  }\n\n  connect(): void {\n    if (this.destroyed) return;\n    this.teardownSocket();\n    this.setState({ ...INITIAL_ADMIN_STATE, phase: \"connecting\" });\n\n    const socket = new PartySocket({\n      host: this.opts.host,\n      room: this.opts.room,\n      party: this.opts.party,\n      query: { [ADMIN_MODE_QUERY_KEY]: \"1\" },\n    });\n    this.socket = socket;\n\n    socket.addEventListener(\"open\", () => {\n      void this.authenticate();\n    });\n    socket.addEventListener(\"message\", (evt) => this.handleMessage(String(evt.data)));\n    socket.addEventListener(\"close\", () => {\n      if (this.state.phase === \"connecting\" || this.state.phase === \"ready\") {\n        this.setState({ phase: \"disconnected\" });\n      }\n    });\n  }\n\n  disconnect(): void {\n    this.teardownSocket();\n    this.setState({ phase: \"idle\" });\n  }\n\n  destroy(): void {\n    this.destroyed = true;\n    this.teardownSocket();\n    this.listeners.clear();\n  }\n\n  refresh(): void {\n    this.send({ type: \"admin_refresh\" });\n  }\n\n  kickMember(connId: string): void {\n    this.send({ type: \"admin_kick_member\", connId });\n  }\n\n  kickQueued(connId: string): void {\n    this.send({ type: \"admin_kick_queued\", connId });\n  }\n\n  closeSession(sessionId: string): void {\n    this.send({ type: \"admin_close_session\", sessionId });\n  }\n\n  private async authenticate(): Promise<void> {\n    try {\n      const password = await resolvePassword(this.opts.password);\n      this.send({ type: \"admin_auth\", password });\n    } catch (err) {\n      this.setState({\n        phase: \"rejected\",\n        reason: err instanceof Error ? err.message : \"password_resolve_failed\",\n      });\n      this.teardownSocket();\n    }\n  }\n\n  private handleMessage(raw: string): void {\n    const msg = parseAdminServerMessage(raw);\n    if (!msg) return;\n\n    switch (msg.type) {\n      case \"admin_ready\":\n        this.setState({ phase: \"ready\", reason: null });\n        this.startRefreshTimer();\n        break;\n      case \"admin_snapshot\":\n        this.setState({ phase: \"ready\", snapshot: msg });\n        break;\n      case \"admin_rejected\":\n        this.setState({ phase: \"rejected\", reason: msg.reason });\n        this.teardownSocket();\n        break;\n      case \"admin_action_result\":\n        this.setState({ lastAction: msg });\n        break;\n      case \"admin_log_history\":\n        this.setState({ logs: msg.entries.slice(-MAX_CLIENT_LOGS) });\n        break;\n      case \"admin_log\":\n        this.setState({ logs: [...this.state.logs, msg.entry].slice(-MAX_CLIENT_LOGS) });\n        break;\n    }\n  }\n\n  private send(msg: AdminClientMessage): void {\n    if (this.socket?.readyState === WebSocket.OPEN) {\n      this.socket.send(JSON.stringify(msg));\n    }\n  }\n\n  private startRefreshTimer(): void {\n    this.clearRefreshTimer();\n    if (this.opts.refreshIntervalMs <= 0) return;\n    this.refreshTimer = setInterval(() => {\n      if (this.state.phase === \"ready\") this.refresh();\n    }, this.opts.refreshIntervalMs);\n  }\n\n  private clearRefreshTimer(): void {\n    if (this.refreshTimer) {\n      clearInterval(this.refreshTimer);\n      this.refreshTimer = null;\n    }\n  }\n\n  private teardownSocket(): void {\n    this.clearRefreshTimer();\n    if (this.socket) {\n      try {\n        this.socket.close();\n      } catch {\n        /* already closing */\n      }\n      this.socket = null;\n    }\n  }\n}\n"]}