import * as Party from 'partykit/server'; export { ADMIN_MODE_QUERY_KEY, AdminActionResultMessage, AdminAuthMessage, AdminClientMessage, AdminCloseSessionMessage, AdminConfigSnapshot, AdminKickMemberMessage, AdminKickQueuedMessage, AdminLogEntry, AdminLogHistoryMessage, AdminLogLevel, AdminLogMessage, AdminMemberSnapshot, AdminQueuedUserSnapshot, AdminReadyMessage, AdminRefreshMessage, AdminRejectedMessage, AdminServerMessage, AdminSessionSnapshot, AdminSnapshotMessage, AdmittedMessage, CLIENT_ID_QUERY_KEY, ClaimMessage, ClientMessage, DEFAULTS, DEFAULT_ROOM, ErrorMessage, ExpiredMessage, LeaveMessage, PROTOCOL_VERSION, QueuePositionMessage, RejectedMessage, RequestTokenMessage, ServerMessage, SessionEndedMessage, SessionReadyMessage, TERMINAL_SESSION_STATES, TimeWarningMessage, TokenMessage, parseAdminClientMessage, parseAdminServerMessage, parseClientMessage, parseServerMessage } from '../protocol.js'; /** Passed to a custom {@link ReactorQueueServerConfig.acquireSession}. */ interface AcquireSessionContext { /** The configured model name. */ model: string; } /** Passed to a custom {@link ReactorQueueServerConfig.releaseSession}. */ interface ReleaseSessionContext { /** The Reactor session the user was in. */ sessionId: string; /** The connection id of the user who left. */ userId: string; /** Why they left: `timeout`, `grace_timeout`, or `server`. */ reason: string; /** True if they were the last member — the session is now empty. */ lastMember: boolean; } type AcquireSessionFn = (ctx: AcquireSessionContext) => Promise; type ReleaseSessionFn = (ctx: ReleaseSessionContext) => Promise; /** * Operator-facing configuration for the queue server. * * Every field is optional. Resolution order (last wins): built-in default → * value passed to {@link createReactorQueueServer} → environment variable * (PartyKit `room.env`). Putting env last lets you bake sensible defaults into * code and still tune a deployment without a redeploy. */ interface ReactorQueueServerConfig { /** Max concurrent Reactor sessions (GPU ceiling). Env: `RQ_MAX_SESSIONS`. */ maxSessions?: number; /** Members per session. Env: `RQ_USERS_PER_SESSION`. */ usersPerSession?: number; /** Model name for `POST /sessions`. Env: `RQ_MODEL`. Required. */ model?: string; /** WebRTC transport version for session create. Env: `RQ_WEBRTC_VERSION`. */ webrtcVersion?: string; /** Full session budget after claim, in ms. Env: `RQ_SESSION_DURATION_MS`. */ sessionDurationMs?: number; /** Grace window to claim an admitted slot, in ms. Env: `RQ_ADMISSION_GRACE_MS`. */ admissionGraceMs?: number; /** Lead time for the `time_warning`, in ms. Env: `RQ_WARNING_BEFORE_MS`. */ warningBeforeMs?: number; /** * Requested lifetime for each minted Reactor JWT, in seconds. Keep it short: * the client refreshes over `request_token` as needed. Env: * `RQ_TOKEN_TTL_SECONDS`. */ tokenTtlSeconds?: number; /** How often to reconcile tracked sessions with Reactor, in ms. Env: `RQ_POLL_INTERVAL_MS`. */ pollIntervalMs?: number; /** Reactor Coordinator base URL. Env: `RQ_COORDINATOR_URL`. Default `https://api.reactor.inc`. */ coordinatorUrl?: string; /** * Reactor API key (`rk_...`). **Server-side secret** — set it via a PartyKit * secret (`RQ_REACTOR_API_KEY`), never bake it into code that ships to * clients. Required for the server to mint JWTs and reap sessions. */ apiKey?: string; /** `Reactor-API-Version` header value. Env: `RQ_API_VERSION`. Default `1`. */ apiVersion?: number; /** * When true (default), the server calls `DELETE /sessions/{id}` the moment a * user's time runs out or they vanish, instead of waiting for Reactor's own * idle timeout. Env: `RQ_STOP_SESSIONS=false` to disable. */ stopSessionsOnExpiry?: boolean; /** * Password for admin dashboard connections (`rqAdmin=1` on the WebSocket URL). * Env: `RQ_ADMIN_PASSWORD`. When unset, admin mode is disabled. */ adminPassword?: string; /** * Allow the same browser (stable `clientId`) to hold multiple simultaneous * connections. Default false: a second tab is rejected with `already_connected`. * Env: `RQ_ALLOW_DUPLICATE_CONNECTIONS=true`. */ allowDuplicateConnections?: boolean; /** * Cross-origin allow-list for incoming WebSocket connections. Each entry is an * exact `Origin` header value (scheme + host + optional port, no path), e.g. * `"https://demo.example.com"`. A single `"*"` entry allows any origin. * * When empty/unset (the default) **all** origins are accepted — same as before * this option existed — so it is purely opt-in. Once set, a browser whose * `Origin` is not on the list (and any connection with no `Origin` header) is * rejected before it joins the queue. This is the cross-site abuse control: * without it, any page on the web can open a socket to your room and consume * GPU capacity. Env: `RQ_ALLOWED_ORIGINS` (comma-separated). */ allowedOrigins?: string[]; /** * Override how a session id is obtained when an admitted user `claim()`s. * Default: create one via the Reactor API (`POST /sessions`). Override to * source sessions from elsewhere — e.g. lease a pre-provisioned session from * another service that already has a different kind of client attached. * Called once per session (the first member's claim). Not configurable via * env — pass a function. */ acquireSession?: AcquireSessionFn; /** * Called when a user **leaves** a session (timeout, disconnect, end, or kick), * with their `userId` and whether they were the `lastMember`. Default: when * the last member leaves, delete the session via the Reactor API * (`DELETE /sessions/{id}`, subject to `stopSessionsOnExpiry`). Override to * keep the session alive and just react to the departure (e.g. hand it back to * the owning service to be reset and reused). Not configurable via env — pass * a function. */ releaseSession?: ReleaseSessionFn; /** Optional lifecycle hooks for logging / metrics. Not configurable via env. */ hooks?: ReactorQueueServerHooks; } interface ReactorQueueServerHooks { onUserConnected?: (connId: string) => void; onUserDisconnected?: (connId: string) => void; onUserEnteredSession?: (connId: string, sessionId: string) => void; onSessionCreated?: (sessionId: string) => void; onSessionClosed?: (sessionId: string, reason: string) => void; onError?: (where: string, error: unknown) => void; } /** Fully-resolved config with all values present. */ interface ResolvedConfig { maxSessions: number; usersPerSession: number; model: string; webrtcVersion: string; sessionDurationMs: number; admissionGraceMs: number; warningBeforeMs: number; tokenTtlSeconds: number; pollIntervalMs: number; coordinatorUrl: string; apiKey: string; apiVersion: number; stopSessionsOnExpiry: boolean; hooks: ReactorQueueServerHooks; /** Total live users = maxSessions * usersPerSession. */ capacity: number; /** When set, admin WebSocket connections may authenticate with this password. */ adminPassword: string | null; /** When true, the duplicate-tab (same `clientId`) rejection is disabled. */ allowDuplicateConnections: boolean; /** Allowed `Origin` values for WebSocket connections. Empty = allow all. `["*"]` = allow all explicitly. */ allowedOrigins: string[]; /** Custom session acquisition, or null to create via the Reactor API. */ acquireSession: AcquireSessionFn | null; /** Custom user-left handler, or null to delete via the Reactor API on last member. */ releaseSession: ReleaseSessionFn | null; } /** * Build a PartyKit `Server` class implementing the Reactor queue. Use it as the * default export of your PartyKit entrypoint: * * ```ts * // partykit/server.ts * import { createReactorQueueServer } from "@reactor-team/queue/server"; * export default createReactorQueueServer({ model: "helios" }); * ``` */ declare function createReactorQueueServer(config?: ReactorQueueServerConfig): new (room: Party.Room) => Party.Server; /** * Raised when the Coordinator answers a request with a non-OK status. Carries * the `endpoint`, HTTP `status`, and raw response `body` so callers (and the * admin log) can show *why* a call failed — a quota rejection, an expired key, * a bad model — instead of a generic "session create failed". */ declare class CoordinatorError extends Error { readonly endpoint: string; readonly status: number; readonly body: string; constructor(endpoint: string, status: number, body: string); } /** * Session authorization scope for a minted JWT. When passed to * {@link CoordinatorClient.mintToken}, the JWT is restricted to the named model * and to the sessions its grant holds — nothing else on the account. */ interface TokenScope { /** Model the token is confined to (fully-qualified `org/model`). */ model: string; /** * Existing sessions the grant starts bound to. Each must still be open and * owned by the API key doing the minting; the Coordinator answers `403` * otherwise, and refuses a session whose model falls outside `model`. */ sessions?: string[]; /** * How many sessions the grant may hold over its lifetime. Left unset it * resolves to the number of bound sessions, which leaves the token full on * arrival: it operates what it was given and cannot create more. */ maxSessions?: number; } /** * Thin server-side client for the Reactor Coordinator REST API. From inside the * trusted PartyKit server it: * * 1. mints short-lived client JWTs from the API key (`POST /tokens`), * optionally scoped to one model via `authorization_details`, * 2. creates sessions (`POST /sessions`), * 3. reads a session's state (`GET /sessions/{id}/runtime`), and * 4. stops a session (`DELETE /sessions/{id}`). * * (2)–(4) need a Bearer JWT. By default the client keeps its own cached * "server JWT" (unscoped, minted with a longer TTL) and reuses it across * calls; `createSession`/`createConnection` also accept an explicit `jwt` so * a session can be created *by* a scoped token, binding it to that token's * grant. */ declare class CoordinatorClient { private readonly baseUrl; private readonly apiKey; private readonly apiVersion; private readonly webrtcVersion; private serverJwt; /** TTL for the server's own admin JWT. Longer than client tokens; re-minted lazily. */ private static readonly SERVER_JWT_TTL_SECONDS; private static readonly SKEW_SECONDS; constructor(opts: { baseUrl: string; apiKey: string; apiVersion: number; webrtcVersion: string; }); private versionHeaders; /** * Exchange the API key for a JWT. `ttlSeconds` is passed as `expires_after`; * the Coordinator caps it at its server maximum. With a `scope`, the JWT * carries session `authorization_details`: it is confined to `scope.model` * and to the sessions on its grant, which `scope.sessions` can pre-populate * with sessions that already exist. The bound set is server state rather than * a claim, so it never appears in the token itself. */ mintToken(ttlSeconds: number, scope?: TokenScope): Promise<{ jwt: string; expiresAt: number; }>; private getServerJwt; /** * Returns the session's current state string, `"CLOSED"` if the session is * gone (404), or `null` if the lookup itself failed (so callers can avoid * freeing a slot on a transient network error). */ getSessionState(sessionId: string): Promise; /** True if a state string means the slot should be released. */ static isTerminal(state: string | null): boolean; /** * Create a Reactor session for the configured model. Returns the new * `session_id`. Runs billing/quota checks against the server's API key. */ createSession(opts: { model: string; webrtcVersion: string; }): Promise; /** * Register a WebRTC connection under an existing session and return the * server-minted `connection_id`. This is a transport call, so it carries * `Reactor-WebRTC-Version` rather than the API-version headers. * * A {@link CoordinatorError} with `status === 429` means the session hit its * `connections_per_session` cap; the caller falls back to another/new session. */ createConnection(sessionId: string): Promise; /** Force-close a session. Swallows "already gone" responses. */ stopSession(sessionId: string, reason?: string): Promise; } export { type AcquireSessionContext, type AcquireSessionFn, CoordinatorClient, CoordinatorError, type ReactorQueueServerConfig, type ReactorQueueServerHooks, type ReleaseSessionContext, type ReleaseSessionFn, type ResolvedConfig, createReactorQueueServer };