121 lines
3.8 KiB
Swift
121 lines
3.8 KiB
Swift
// Sources/Core/Sessions/SessionRegistry.swift
|
|
// T-2.6: Session listing, creation, and deletion via the sidecar REST API.
|
|
|
|
import Foundation
|
|
|
|
// MARK: - SessionInfo
|
|
|
|
struct SessionInfo: Identifiable, Hashable, Sendable {
|
|
let id: String
|
|
let name: String
|
|
let state: String // "running", "idle", etc.
|
|
}
|
|
|
|
// MARK: - SessionRegistry
|
|
|
|
@MainActor
|
|
final class SessionRegistry: ObservableObject {
|
|
@Published var sessions: [SessionInfo] = []
|
|
@Published var isLoading = false
|
|
@Published var error: String? = nil
|
|
|
|
init() {}
|
|
|
|
// MARK: - Private helpers
|
|
|
|
private func baseURL(credential: SidecarCredential) -> URL {
|
|
URL(string: "http://\(credential.host):\(credential.port)")!
|
|
}
|
|
|
|
private func authorizedRequest(url: URL, credential: SidecarCredential) -> URLRequest {
|
|
var req = URLRequest(url: url)
|
|
req.setValue("Bearer \(credential.bearerToken)", forHTTPHeaderField: "Authorization")
|
|
return req
|
|
}
|
|
|
|
// MARK: - GET /sessions
|
|
|
|
/// Fetches the current session list from the sidecar and updates `sessions`.
|
|
func refresh(credential: SidecarCredential) async {
|
|
isLoading = true
|
|
error = nil
|
|
defer { isLoading = false }
|
|
|
|
let url = baseURL(credential: credential).appendingPathComponent("sessions")
|
|
let req = authorizedRequest(url: url, credential: credential)
|
|
|
|
do {
|
|
let (data, _) = try await URLSession.shared.data(for: req)
|
|
let items = try JSONDecoder().decode([SessionItem].self, from: data)
|
|
sessions = items.map { SessionInfo(id: $0.id, name: $0.name, state: $0.state) }
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
// MARK: - POST /sessions
|
|
|
|
/// Creates a new session with the given name and returns the created `SessionInfo`.
|
|
@discardableResult
|
|
func spawnSession(name: String, credential: SidecarCredential) async throws -> SessionInfo {
|
|
let url = baseURL(credential: credential).appendingPathComponent("sessions")
|
|
var req = authorizedRequest(url: url, credential: credential)
|
|
req.httpMethod = "POST"
|
|
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
req.httpBody = try JSONEncoder().encode(["name": name])
|
|
|
|
let (data, response) = try await URLSession.shared.data(for: req)
|
|
|
|
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
|
|
throw SessionRegistryError.unexpectedStatus
|
|
}
|
|
|
|
let item = try JSONDecoder().decode(SessionItem.self, from: data)
|
|
let info = SessionInfo(id: item.id, name: item.name, state: item.state)
|
|
|
|
// Append optimistically; a follow-up refresh will normalise order.
|
|
sessions.append(info)
|
|
return info
|
|
}
|
|
|
|
// MARK: - DELETE /sessions/<id>
|
|
|
|
/// Deletes the session with the given id.
|
|
func deleteSession(id: String, credential: SidecarCredential) async throws {
|
|
let url = baseURL(credential: credential)
|
|
.appendingPathComponent("sessions")
|
|
.appendingPathComponent(id)
|
|
var req = authorizedRequest(url: url, credential: credential)
|
|
req.httpMethod = "DELETE"
|
|
|
|
let (_, response) = try await URLSession.shared.data(for: req)
|
|
|
|
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
|
|
throw SessionRegistryError.unexpectedStatus
|
|
}
|
|
|
|
sessions.removeAll { $0.id == id }
|
|
}
|
|
}
|
|
|
|
// MARK: - Private Codable helper
|
|
|
|
private struct SessionItem: Decodable {
|
|
let id: String
|
|
let name: String
|
|
let state: String
|
|
}
|
|
|
|
// MARK: - Errors
|
|
|
|
enum SessionRegistryError: LocalizedError {
|
|
case unexpectedStatus
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .unexpectedStatus:
|
|
return "Unexpected HTTP status from sidecar."
|
|
}
|
|
}
|
|
}
|