import Foundation /// What a native client needs from the network. `NativeHTTP` is the real one; /// `RastrilloTestSupport.ScriptedTransport` answers from a script, so offline, /// lost responses and ended sessions can be tested without a fixture server. /// /// Anything that can outlive a session should take a supplier, /// `@MainActor () -> NativeTransport?`, rather than capturing one transport: /// signing in again replaces the transport under it. /// /// The requirements take every argument. The shorter forms, with defaults, /// are extensions below; they have different signatures from the /// requirements, so no conformer can pick one up as a default /// implementation that calls itself. public protocol NativeTransport: Sendable { var origin: URL { get } /// Returns any status. Throws `NativeClientError.offline` when the request /// never reached the server or its answer never came back. func responseDetails(_ path: String, method: String, body: Data?, contentType: String, maxBytes: Int) async throws -> NativeResponse /// Throws the typed case for a refused stream: a 401 reads as `.unauthorized`. func stream(_ path: String, onOpen: @Sendable () async throws -> Void, onLine: @Sendable (String) async throws -> Void) async throws func download(_ path: String, to destination: URL) async throws } /// The default response bound, 4 MiB. public let nativeDefaultMaxBytes = 4 * 1024 * 1024 public extension NativeTransport { func responseDetails(_ path: String, method: String = "GET", body: Data? = nil, contentType: String = "application/json") async throws -> NativeResponse { try await responseDetails(path, method: method, body: body, contentType: contentType, maxBytes: nativeDefaultMaxBytes) } func responseDetails(_ path: String, method: String = "GET", body: Data? = nil, maxBytes: Int) async throws -> NativeResponse { try await responseDetails(path, method: method, body: body, contentType: "application/json", maxBytes: maxBytes) } func response(_ path: String, method: String = "GET", body: Data? = nil, contentType: String = "application/json", maxBytes: Int = nativeDefaultMaxBytes) async throws -> (Data, Int) { let result = try await responseDetails(path, method: method, body: body, contentType: contentType, maxBytes: maxBytes) return (result.data, result.status) } /// Decodes a 2xx body; any other status throws its typed case. func request(_ path: String, method: String = "GET", body: Data? = nil, contentType: String = "application/json", maxBytes: Int = nativeDefaultMaxBytes) async throws -> T { let result = try await responseDetails(path, method: method, body: body, contentType: contentType, maxBytes: maxBytes) try result.requireSuccess() do { return try JSONDecoder().decode(T.self, from: result.data) } catch { throw NativeClientError.message("The server returned an unsupported response.") } } func stream(_ path: String, onLine: @Sendable (String) async throws -> Void) async throws { try await stream(path, onOpen: {}, onLine: onLine) } } /// What `NativeHTTP` tells its outcome observer. A response of any status /// proves the server is reachable; an offline-class failure suggests it is not. public enum NativeTransportOutcome: Sendable, Equatable { case responded(status: Int) case offline(URLError.Code) }