diff --git a/clients/swift/Sources/Clawdforge/ForgeClient.swift b/clients/swift/Sources/Clawdforge/ForgeClient.swift index fe323d8..4ec6850 100644 --- a/clients/swift/Sources/Clawdforge/ForgeClient.swift +++ b/clients/swift/Sources/Clawdforge/ForgeClient.swift @@ -12,16 +12,68 @@ // `URLSession.data(for:)` / `URLSession.upload(for:fromFile:)`, so wrapping // a call in `Task { ... }.cancel()` cleanly aborts the in-flight request. // -// Linux: builds against swift-corelibs-foundation. The async URLSession -// methods used here (`data(for:)`, `upload(for:fromFile:)`) are available -// on Linux as of Swift 5.9. +// Linux: builds against swift-corelibs-foundation. As of Swift 5.9.2 the +// async `URLSession.data(for:)` / `URLSession.upload(for:fromFile:)` are +// **only** on Apple platforms (macOS 12+, iOS 15+, etc.) — Linux's +// FoundationNetworking exposes the callback-style URLSession API only. +// The `forgeData(for:)` / `forgeUpload(for:fromFile:)` helpers below +// bridge the callback API with `withCheckedThrowingContinuation` on Linux +// and forward to the native async API on Apple. +// +// `@preconcurrency` on the Foundation imports suppresses Sendable warnings +// for URL / URLSession / JSONEncoder / JSONDecoder which are not declared +// Sendable on swift-corelibs-foundation; the SDK still preserves Sendable +// semantics by the way it uses them (one shared session, immutable +// captured fields). -import Foundation +@preconcurrency import Foundation #if canImport(FoundationNetworking) -import FoundationNetworking +@preconcurrency import FoundationNetworking #endif +// MARK: - URLSession Linux/Apple bridge ----------------------------------- + +extension URLSession { + /// Async `(Data, URLResponse)` for a request — uses the native API on + /// Apple, bridges the callback API on Linux. Behaves identically. + fileprivate func forgeData(for request: URLRequest) async throws -> (Data, URLResponse) { + #if canImport(FoundationNetworking) + return try await withCheckedThrowingContinuation { cont in + let task = self.dataTask(with: request) { data, response, error in + if let error { cont.resume(throwing: error); return } + guard let data, let response else { + cont.resume(throwing: URLError(.unknown)); return + } + cont.resume(returning: (data, response)) + } + task.resume() + } + #else + return try await self.data(for: request) + #endif + } + + /// Async upload from a file — uses the native API on Apple, bridges + /// the callback API on Linux. + fileprivate func forgeUpload(for request: URLRequest, fromFile fileURL: URL) async throws -> (Data, URLResponse) { + #if canImport(FoundationNetworking) + return try await withCheckedThrowingContinuation { cont in + let task = self.uploadTask(with: request, fromFile: fileURL) { data, response, error in + if let error { cont.resume(throwing: error); return } + guard let data, let response else { + cont.resume(throwing: URLError(.unknown)); return + } + cont.resume(returning: (data, response)) + } + task.resume() + } + #else + return try await self.upload(for: request, fromFile: fileURL) + #endif + } +} + /// Thread-safe client for the clawdforge REST API. /// /// Construct once per (baseURL, token) pair and share across the app. @@ -164,7 +216,7 @@ public struct ForgeClient: Sendable { let (data, response): (Data, URLResponse) do { - (data, response) = try await session.upload(for: req, fromFile: tempURL) + (data, response) = try await session.forgeUpload(for: req, fromFile: tempURL) } catch let urlError as URLError { throw ForgeError.transport(urlError) } catch { @@ -275,7 +327,7 @@ public struct ForgeClient: Sendable { let (data, response): (Data, URLResponse) do { - (data, response) = try await session.data(for: req) + (data, response) = try await session.forgeData(for: req) } catch let urlError as URLError { throw ForgeError.transport(urlError) } catch { @@ -291,7 +343,7 @@ public struct ForgeClient: Sendable { let (data, response): (Data, URLResponse) do { - (data, response) = try await session.data(for: req) + (data, response) = try await session.forgeData(for: req) } catch let urlError as URLError { throw ForgeError.transport(urlError) } catch {