import { storeCreateSession, storeGetSession, storeIsSessionOwner, storeGetSessionOwners, storeBindSession, storeUpdateSession, storeListSessions, storeListSessionsByUsername, storeListSessionsByEnvironment, storeListSessionsByOwnerUuid, } from "../store"; import { randomUUID } from "node:crypto"; import { getAllEventBuses, removeEventBus } from "../transport/event-bus"; import type { CreateSessionRequest, CreateCodeSessionRequest, SessionResponse, SessionSummaryResponse } from "../types/api"; const CODE_SESSION_PREFIX = "cse_"; const WEB_SESSION_PREFIX = "session_"; const CLOSED_SESSION_STATUSES = new Set(["archived", "inactive"]); function toResponse(row: { id: string; environmentId: string | null; title: string | null; status: string; source: string; permissionMode: string | null; workerEpoch: number; username: string | null; createdAt: Date; updatedAt: Date }): SessionResponse { return { id: row.id, environment_id: row.environmentId, title: row.title, status: row.status, source: row.source, permission_mode: row.permissionMode, worker_epoch: row.workerEpoch, username: row.username, created_at: row.createdAt.getTime() / 1000, updated_at: row.updatedAt.getTime() / 1000, }; } export function toWebSessionId(sessionId: string): string { if (!sessionId.startsWith(CODE_SESSION_PREFIX)) return sessionId; return `${WEB_SESSION_PREFIX}${sessionId.slice(CODE_SESSION_PREFIX.length)}`; } function toCompatibleCodeSessionId(sessionId: string): string | null { if (!sessionId.startsWith(WEB_SESSION_PREFIX)) return null; return `${CODE_SESSION_PREFIX}${sessionId.slice(WEB_SESSION_PREFIX.length)}`; } export function toWebSessionResponse(session: SessionResponse): SessionResponse { return { ...session, id: toWebSessionId(session.id) }; } function toWebSessionSummaryResponse(session: SessionSummaryResponse): SessionSummaryResponse { return { ...session, id: toWebSessionId(session.id) }; } export function createSession(req: CreateSessionRequest & { username?: string }): SessionResponse { const record = storeCreateSession({ environmentId: req.environment_id, title: req.title, source: req.source, permissionMode: req.permission_mode, username: req.username, }); return toResponse(record); } export function createCodeSession(req: CreateCodeSessionRequest): SessionResponse { const record = storeCreateSession({ idPrefix: "cse_", title: req.title, source: req.source, permissionMode: req.permission_mode, }); return toResponse(record); } export function getSession(sessionId: string): SessionResponse | null { const record = storeGetSession(sessionId); return record ? toResponse(record) : null; } export function isSessionClosedStatus(status: string | null | undefined): boolean { return !!status && CLOSED_SESSION_STATUSES.has(status); } export function resolveExistingSessionId(sessionId: string): string | null { if (storeGetSession(sessionId)) { return sessionId; } const compatibleCodeSessionId = toCompatibleCodeSessionId(sessionId); if (compatibleCodeSessionId && storeGetSession(compatibleCodeSessionId)) { return compatibleCodeSessionId; } return null; } export function resolveExistingWebSessionId(sessionId: string): string | null { return resolveExistingSessionId(sessionId); } export function resolveOwnedWebSessionId(sessionId: string, uuid: string): string | null { if (storeIsSessionOwner(sessionId, uuid)) { return sessionId; } const compatibleCodeSessionId = toCompatibleCodeSessionId(sessionId); if (compatibleCodeSessionId && storeIsSessionOwner(compatibleCodeSessionId, uuid)) { return compatibleCodeSessionId; } // Auto-bind: if the session exists but has no owner, claim it for the requesting user const existingId = resolveExistingSessionId(sessionId); if (existingId) { const owners = storeGetSessionOwners(existingId); if (!owners || owners.size === 0) { storeBindSession(existingId, uuid); return existingId; } } return null; } export function listWebSessionsByOwnerUuid(uuid: string): SessionResponse[] { return storeListSessionsByOwnerUuid(uuid) .filter((session) => !isSessionClosedStatus(session.status)) .map(toResponse) .map(toWebSessionResponse); } export function listWebSessionSummariesByOwnerUuid(uuid: string): SessionSummaryResponse[] { return storeListSessionsByOwnerUuid(uuid) .filter((session) => !isSessionClosedStatus(session.status)) .map(toSummaryResponse) .map(toWebSessionSummaryResponse); } export function updateSessionTitle(sessionId: string, title: string) { storeUpdateSession(sessionId, { title }); } export function updateSessionStatus(sessionId: string, status: string) { storeUpdateSession(sessionId, { status }); const bus = getAllEventBuses().get(sessionId); if (!bus) return; bus.publish({ id: randomUUID(), sessionId, type: "session_status", payload: { status }, direction: "inbound", }); } export function touchSession(sessionId: string) { storeUpdateSession(sessionId, {}); } export function archiveSession(sessionId: string) { updateSessionStatus(sessionId, "archived"); removeEventBus(sessionId); } export function incrementEpoch(sessionId: string): number { const record = storeGetSession(sessionId); if (!record) throw new Error("Session not found"); const newEpoch = record.workerEpoch + 1; storeUpdateSession(sessionId, { workerEpoch: newEpoch }); return newEpoch; } export function listSessions() { return storeListSessions().map(toResponse); } function toSummaryResponse(row: { id: string; title: string | null; status: string; username: string | null; updatedAt: Date }): SessionSummaryResponse { return { id: row.id, title: row.title, status: row.status, username: row.username, updated_at: row.updatedAt.getTime() / 1000, }; } export function listSessionSummaries(): SessionSummaryResponse[] { return storeListSessions().map(toSummaryResponse); } export function listSessionSummariesByOwnerUuid(uuid: string): SessionSummaryResponse[] { return storeListSessionsByOwnerUuid(uuid).map(toSummaryResponse); } export function listSessionSummariesByUsername(username: string): SessionSummaryResponse[] { return storeListSessionsByUsername(username).map(toSummaryResponse); } export function listSessionsByEnvironment(envId: string) { return storeListSessionsByEnvironment(envId).map(toResponse); }