Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Sendable support #19

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 9 additions & 5 deletions Sources/Get/APIClient.swift
Expand Up @@ -2,9 +2,13 @@
//
// Copyright (c) 2021-2022 Alexander Grebenyuk (github.com/kean).

#if compiler(>=5.6)
@preconcurrency import Foundation
#else
import Foundation
#endif

public protocol APIClientDelegate {
public protocol APIClientDelegate: Sendable {
func client(_ client: APIClient, willSendRequest request: inout URLRequest) async throws
func shouldClientRetry(_ client: APIClient, withError error: Error) async throws -> Bool
func client(_ client: APIClient, didReceiveInvalidResponse response: HTTPURLResponse, data: Data) -> Error
Expand All @@ -17,7 +21,7 @@ public actor APIClient {
private let delegate: APIClientDelegate
private let loader = DataLoader()

public struct Configuration {
public struct Configuration: Sendable {
public var host: String
public var port: Int?
/// If `true`, uses `http` instead of `https`.
Expand Down Expand Up @@ -60,7 +64,7 @@ public actor APIClient {
}

/// Sends the given request and returns a response with a decoded response value.
public func send<T: Decodable>(_ request: Request<T?>) async throws -> Response<T?> {
public func send<T: Decodable>(_ request: Request<T?>) async throws -> Response<T?> where T: Sendable {
try await send(request) { data in
if data.isEmpty {
return nil
Expand All @@ -71,11 +75,11 @@ public actor APIClient {
}

/// Sends the given request and returns a response with a decoded response value.
public func send<T: Decodable>(_ request: Request<T>) async throws -> Response<T> {
public func send<T: Decodable>(_ request: Request<T>) async throws -> Response<T> where T: Sendable {
try await send(request, decode)
}

private func decode<T: Decodable>(_ data: Data) async throws -> T {
private func decode<T: Decodable>(_ data: Data) async throws -> T where T: Sendable {
if T.self == Data.self {
return data as! T
} else if T.self == String.self {
Expand Down
37 changes: 34 additions & 3 deletions Sources/Get/Helpers.swift
Expand Up @@ -2,7 +2,11 @@
//
// Copyright (c) 2021-2022 Alexander Grebenyuk (github.com/kean).

#if compiler(>=5.6)
@preconcurrency import Foundation
#else
import Foundation
#endif

struct AnyEncodable: Encodable {
private let value: Encodable
Expand Down Expand Up @@ -67,13 +71,40 @@ actor Serializer {
}

// A simple URLSession wrapper adding async/await APIs compatible with older platforms.
final class DataLoader: NSObject, URLSessionDataDelegate {
private var handlers = [URLSessionTask: TaskHandler]()
final class DataLoader: NSObject, URLSessionDataDelegate, @unchecked Sendable {
private var _handlers = [URLSessionTask: TaskHandler]()
private var handlers: [URLSessionTask: TaskHandler] {
get { safe { _handlers } }
set { safe { _handlers = newValue } }
}

private let lock = NSRecursiveLock()
private func safe<Value>(_ action: () -> Value) -> Value {
defer { lock.unlock() }
lock.lock()
return action()
}

private typealias Completion = (Result<(Data, URLResponse, URLSessionTaskMetrics?), Error>) -> Void

/// Loads data with the given request.
func data(for request: URLRequest, session: URLSession) async throws -> (Data, URLResponse, URLSessionTaskMetrics?) {
final class Box { var task: URLSessionTask? }
final class Box: @unchecked Sendable {
private let lock = NSRecursiveLock()
private var _task: URLSessionTask?

var task: URLSessionTask? {
get { safe { _task } }
set { safe { _task = newValue } }
}

private func safe<Value>(_ action: () -> Value) -> Value {
defer { lock.unlock() }
lock.lock()
return action()
}
}

let box = Box()
return try await withTaskCancellationHandler(handler: {
box.task?.cancel()
Expand Down
8 changes: 7 additions & 1 deletion Sources/Get/Request.swift
Expand Up @@ -2,9 +2,13 @@
//
// Copyright (c) 2021-2022 Alexander Grebenyuk (github.com/kean).

#if compiler(>=5.6)
@preconcurrency import Foundation
#else
import Foundation
#endif

public struct Request<Response> {
public struct Request<Response>: Sendable {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be extension Response: Sendable where T: Sendable {}. We should restrict the users to only using Sendable types are responses.

public var method: String
public var path: String
public var query: [(String, String?)]?
Expand Down Expand Up @@ -99,3 +103,5 @@ public struct Response<T> {
Response<U>(value: closure(value), data: data, request: request, response: response, metrics: metrics)
}
}

extension Response: Sendable where T: Sendable {}