Skip to content

Commit

Permalink
Merge pull request #38 from kean/data-task-delegate
Browse files Browse the repository at this point in the history
Allow setting URLSessionDataDelegate on a per-request basis
  • Loading branch information
kean committed Jul 9, 2022
2 parents ea2bc71 + abbff49 commit 67c55ed
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 156 deletions.
4 changes: 2 additions & 2 deletions Package.swift
Expand Up @@ -7,10 +7,10 @@ let package = Package(
name: "Get",
platforms: [.iOS(.v13), .macCatalyst(.v13), .macOS(.v10_15), .watchOS(.v6), .tvOS(.v13)],
products: [
.library(name: "Get", targets: ["Get"]),
.library(name: "Get", targets: ["Get"])
],
targets: [
.target(name: "Get"),
.testTarget(name: "GetTests", dependencies: ["Get"], resources: [.process("Resources")]),
.testTarget(name: "GetTests", dependencies: ["Get"], resources: [.process("Resources")])
]
)
34 changes: 17 additions & 17 deletions Sources/Get/APIClient.swift
Expand Up @@ -32,14 +32,14 @@ public actor APIClient {
/// The (optional) URLSession delegate that allows you to monitor the underlying URLSession.
public var sessionDelegate: URLSessionDelegate?
#endif

public init(baseURL: URL?, sessionConfiguration: URLSessionConfiguration = .default, delegate: APIClientDelegate? = nil) {
self.baseURL = baseURL
self.sessionConfiguration = sessionConfiguration
self.delegate = delegate
}
}

/// Initializes the client with the given parameters.
///
/// - parameter baseURL: A base URL. For example, `"https://api.github.com"`.
Expand All @@ -66,8 +66,8 @@ 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?> {
try await send(request) { data in
public func send<T: Decodable>(_ request: Request<T?>, delegate: URLSessionDataDelegate? = nil) async throws -> Response<T?> {
try await send(request, delegate: delegate) { data in
if data.isEmpty {
return nil
} else {
Expand All @@ -77,8 +77,8 @@ 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> {
try await send(request, decode)
public func send<T: Decodable>(_ request: Request<T>, delegate: URLSessionDataDelegate? = nil) async throws -> Response<T> {
try await send(request, delegate: delegate, decode)
}

private func decode<T: Decodable>(_ data: Data) async throws -> T {
Expand All @@ -94,30 +94,30 @@ public actor APIClient {

/// Sends the given request.
@discardableResult
public func send(_ request: Request<Void>) async throws -> Response<Void> {
try await send(request) { _ in () }
public func send(_ request: Request<Void>, delegate: URLSessionDataDelegate? = nil) async throws -> Response<Void> {
try await send(request, delegate: delegate) { _ in () }
}

private func send<T>(_ request: Request<T>, _ decode: @escaping (Data) async throws -> T) async throws -> Response<T> {
private func send<T>(_ request: Request<T>, delegate: URLSessionDataDelegate?, _ decode: @escaping (Data) async throws -> T) async throws -> Response<T> {
let request = try await makeURLRequest(for: request)
let response = try await send(request)
let response = try await send(request, delegate: delegate)
let value = try await decode(response.value)
return response.map { _ in value } // Keep metadata
}

private func send(_ request: URLRequest) async throws -> Response<Data> {
private func send(_ request: URLRequest, delegate: URLSessionDataDelegate?) async throws -> Response<Data> {
do {
return try await actuallySend(request)
return try await actuallySend(request, delegate: delegate)
} catch {
guard try await delegate.shouldClientRetry(self, for: request, withError: error) else { throw error }
return try await actuallySend(request)
guard try await self.delegate.shouldClientRetry(self, for: request, withError: error) else { throw error }
return try await actuallySend(request, delegate: delegate)
}
}

private func actuallySend(_ request: URLRequest) async throws -> Response<Data> {
private func actuallySend(_ request: URLRequest, delegate: URLSessionDataDelegate?) async throws -> Response<Data> {
var request = request
try await delegate.client(self, willSendRequest: &request)
let (data, response, metrics) = try await loader.data(for: request, session: session)
try await self.delegate.client(self, willSendRequest: &request)
let (data, response, metrics) = try await loader.data(for: request, session: session, delegate: delegate)
try validate(response: response, data: data)
return Response(value: data, data: data, request: request, response: response, metrics: metrics)
}
Expand Down
188 changes: 188 additions & 0 deletions Sources/Get/DataLoader.swift
@@ -0,0 +1,188 @@
// The MIT License (MIT)
//
// Copyright (c) 2021-2022 Alexander Grebenyuk (github.com/kean).

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

// A simple URLSession wrapper adding async/await APIs compatible with older platforms.
final class DataLoader: NSObject, URLSessionDataDelegate {
private var handlers = [URLSessionTask: TaskHandler]()
private typealias Completion = (Result<(Data, URLResponse, URLSessionTaskMetrics?), Error>) -> Void

/// Loads data with the given request.
func data(for request: URLRequest, session: URLSession, delegate: URLSessionDataDelegate?) async throws -> (Data, URLResponse, URLSessionTaskMetrics?) {
final class Box { var task: URLSessionTask? }
let box = Box()
return try await withTaskCancellationHandler(handler: {
box.task?.cancel()
}, operation: {
try await withUnsafeThrowingContinuation { continuation in
box.task = self.loadData(with: request, session: session, delegate: delegate) { result in
continuation.resume(with: result)
}
}
})
}

private func loadData(with request: URLRequest, session: URLSession, delegate: URLSessionDataDelegate?, completion: @escaping Completion) -> URLSessionTask {
let task = session.dataTask(with: request)
session.delegateQueue.addOperation {
self.handlers[task] = TaskHandler(delegate: delegate, completion: completion)
}
task.resume()
return task
}

private final class TaskHandler {
let delegate: URLSessionDataDelegate?
let completion: Completion
var data: Data?
var metrics: URLSessionTaskMetrics?

init(delegate: URLSessionDataDelegate?, completion: @escaping Completion) {
self.delegate = delegate
self.completion = completion
}
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
guard let handler = handlers[dataTask] else { return }
#if os(Linux)
handler.delegate?.urlSession(session, dataTask: dataTask, didReceive: data)
#else
handler.delegate?.urlSession?(session, dataTask: dataTask, didReceive: data)
#endif
if handler.data == nil {
handler.data = Data()
}
handler.data!.append(data)
}

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let handler = handlers[task] else { return }
handlers[task] = nil
#if os(Linux)
handler.delegate?.urlSession(session, task: task, didCompleteWithError: error)
#else
handler.delegate?.urlSession?(session, task: task, didCompleteWithError: error)
#endif
if let response = task.response, error == nil {
handler.completion(.success((handler.data ?? Data(), response, handler.metrics)))
} else {
handler.completion(.failure(error ?? URLError(.unknown)))
}
}

func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
handlers[task]?.metrics = metrics
}
}

#if !os(Linux)
extension DataLoader {
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
if handlers[dataTask]?.delegate?.urlSession?(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler) != nil {
return
}
completionHandler(.allow)
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didBecome downloadTask: URLSessionDownloadTask) {
handlers[dataTask]?.delegate?.urlSession?(session, dataTask: dataTask, didBecome: downloadTask)
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didBecome streamTask: URLSessionStreamTask) {
handlers[dataTask]?.delegate?.urlSession?(session, dataTask: dataTask, didBecome: streamTask)
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) {
if handlers[dataTask]?.delegate?.urlSession?(session, dataTask: dataTask, willCacheResponse: proposedResponse, completionHandler: completionHandler) != nil {
// Do nothing, delegate called
} else {
completionHandler(proposedResponse)
}
}

func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
if handlers[task]?.delegate?.urlSession?(session, task: task, willPerformHTTPRedirection: response, newRequest: request, completionHandler: completionHandler) != nil {
// Do nothing, delegate called
} else {
completionHandler(request)
}
}

func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if handlers[task]?.delegate?.urlSession?(session, task: task, didReceive: challenge, completionHandler: completionHandler) != nil {
// Do nothing, delegate called
} else {
completionHandler(.performDefaultHandling, nil)
}
}

func urlSession(_ session: URLSession, task: URLSessionTask, willBeginDelayedRequest request: URLRequest, completionHandler: @escaping (URLSession.DelayedRequestDisposition, URLRequest?) -> Void) {
if handlers[task]?.delegate?.urlSession?(session, task: task, willBeginDelayedRequest: request, completionHandler: completionHandler) != nil {
// Do nothing, delegate called
} else {
completionHandler(.continueLoading, nil)
}
}

func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
handlers[task]?.delegate?.urlSession?(session, taskIsWaitingForConnectivity: task)
}

#if swift(>=5.7)
func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask) {
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) {
handlers[task]?.delegate?.urlSession?(session, didCreateTask: task)
} else {
// Doesn't exist on earlier versions
}
}
#endif
}
#else
extension DataLoader {
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
if handlers[dataTask]?.delegate?.urlSession(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler) != nil {
return
}
completionHandler(.allow)
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) {
if handlers[dataTask]?.delegate?.urlSession(session, dataTask: dataTask, willCacheResponse: proposedResponse, completionHandler: completionHandler) != nil {
// Do nothing, delegate called
} else {
completionHandler(proposedResponse)
}
}

func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
if handlers[task]?.delegate?.urlSession(session, task: task, willPerformHTTPRedirection: response, newRequest: request, completionHandler: completionHandler) != nil {
// Do nothing, delegate called
} else {
completionHandler(request)
}
}

func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if handlers[task]?.delegate?.urlSession(session, task: task, didReceive: challenge, completionHandler: completionHandler) != nil {
// Do nothing, delegate called
} else {
completionHandler(.performDefaultHandling, nil)
}
}

func urlSession(_ session: URLSession, task: URLSessionTask, willBeginDelayedRequest request: URLRequest, completionHandler: @escaping (URLSession.DelayedRequestDisposition, URLRequest?) -> Void) {
if handlers[task]?.delegate?.urlSession(session, task: task, willBeginDelayedRequest: request, completionHandler: completionHandler) != nil {
// Do nothing, delegate called
} else {
completionHandler(.continueLoading, nil)
}
}
}
#endif
64 changes: 0 additions & 64 deletions Sources/Get/Helpers.swift
Expand Up @@ -47,70 +47,6 @@ actor Serializer {
}
}

// A simple URLSession wrapper adding async/await APIs compatible with older platforms.
final class DataLoader: NSObject, URLSessionDataDelegate {
private var handlers = [URLSessionTask: TaskHandler]()
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? }
let box = Box()
return try await withTaskCancellationHandler(handler: {
box.task?.cancel()
}, operation: {
try await withUnsafeThrowingContinuation { continuation in
box.task = self.loadData(with: request, session: session) { result in
continuation.resume(with: result)
}
}
})
}

private func loadData(with request: URLRequest, session: URLSession, completion: @escaping Completion) -> URLSessionTask {
let task = session.dataTask(with: request)
session.delegateQueue.addOperation {
self.handlers[task] = TaskHandler(completion: completion)
}
task.resume()
return task
}

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let handler = handlers[task] else { return }
handlers[task] = nil
if let response = task.response, error == nil {
handler.completion(.success((handler.data ?? Data(), response, handler.metrics)))
} else {
handler.completion(.failure(error ?? URLError(.unknown)))
}
}

func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
handlers[task]?.metrics = metrics
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
guard let handler = handlers[dataTask] else {
return
}
if handler.data == nil {
handler.data = Data()
}
handler.data!.append(data)
}

private final class TaskHandler {
var data: Data?
var metrics: URLSessionTaskMetrics?
let completion: Completion

init(completion: @escaping Completion) {
self.completion = completion
}
}
}

#if !os(Linux)
/// Allows users to monitor URLSession.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
Expand Down

0 comments on commit 67c55ed

Please sign in to comment.