| | |
| | | import Foundation |
| | | |
| | | /// Stores all state associated with a `URLRequest` being adapted. |
| | | public struct RequestAdapterState { |
| | | public struct RequestAdapterState: Sendable { |
| | | /// The `UUID` of the `Request` associated with the `URLRequest` to adapt. |
| | | public let requestID: UUID |
| | | |
| | |
| | | // MARK: - |
| | | |
| | | /// A type that can inspect and optionally adapt a `URLRequest` in some manner if necessary. |
| | | public protocol RequestAdapter { |
| | | public protocol RequestAdapter: Sendable { |
| | | /// Inspects and adapts the specified `URLRequest` in some manner and calls the completion handler with the Result. |
| | | /// |
| | | /// - Parameters: |
| | | /// - urlRequest: The `URLRequest` to adapt. |
| | | /// - session: The `Session` that will execute the `URLRequest`. |
| | | /// - completion: The completion handler that must be called when adaptation is complete. |
| | | func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) |
| | | func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping @Sendable (_ result: Result<URLRequest, any Error>) -> Void) |
| | | |
| | | /// Inspects and adapts the specified `URLRequest` in some manner and calls the completion handler with the Result. |
| | | /// |
| | |
| | | /// - urlRequest: The `URLRequest` to adapt. |
| | | /// - state: The `RequestAdapterState` associated with the `URLRequest`. |
| | | /// - completion: The completion handler that must be called when adaptation is complete. |
| | | func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void) |
| | | func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping @Sendable (_ result: Result<URLRequest, any Error>) -> Void) |
| | | } |
| | | |
| | | extension RequestAdapter { |
| | | public func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | @preconcurrency |
| | | public func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping @Sendable (_ result: Result<URLRequest, any Error>) -> Void) { |
| | | adapt(urlRequest, for: state.session, completion: completion) |
| | | } |
| | | } |
| | |
| | | // MARK: - |
| | | |
| | | /// Outcome of determination whether retry is necessary. |
| | | public enum RetryResult { |
| | | public enum RetryResult: Sendable { |
| | | /// Retry should be attempted immediately. |
| | | case retry |
| | | /// Retry should be attempted after the associated `TimeInterval`. |
| | |
| | | /// Do not retry. |
| | | case doNotRetry |
| | | /// Do not retry due to the associated `Error`. |
| | | case doNotRetryWithError(Error) |
| | | case doNotRetryWithError(any Error) |
| | | } |
| | | |
| | | extension RetryResult { |
| | | var retryRequired: Bool { |
| | | switch self { |
| | | case .retry, .retryWithDelay: return true |
| | | default: return false |
| | | case .retry, .retryWithDelay: true |
| | | default: false |
| | | } |
| | | } |
| | | |
| | | var delay: TimeInterval? { |
| | | switch self { |
| | | case let .retryWithDelay(delay): return delay |
| | | default: return nil |
| | | case let .retryWithDelay(delay): delay |
| | | default: nil |
| | | } |
| | | } |
| | | |
| | | var error: Error? { |
| | | var error: (any Error)? { |
| | | guard case let .doNotRetryWithError(error) = self else { return nil } |
| | | return error |
| | | } |
| | |
| | | |
| | | /// A type that determines whether a request should be retried after being executed by the specified session manager |
| | | /// and encountering an error. |
| | | public protocol RequestRetrier { |
| | | public protocol RequestRetrier: Sendable { |
| | | /// Determines whether the `Request` should be retried by calling the `completion` closure. |
| | | /// |
| | | /// This operation is fully asynchronous. Any amount of time can be taken to determine whether the request needs |
| | |
| | | /// - session: `Session` that produced the `Request`. |
| | | /// - error: `Error` encountered while executing the `Request`. |
| | | /// - completion: Completion closure to be executed when a retry decision has been determined. |
| | | func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) |
| | | func retry(_ request: Request, for session: Session, dueTo error: any Error, completion: @escaping @Sendable (RetryResult) -> Void) |
| | | } |
| | | |
| | | // MARK: - |
| | |
| | | public protocol RequestInterceptor: RequestAdapter, RequestRetrier {} |
| | | |
| | | extension RequestInterceptor { |
| | | public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | @preconcurrency |
| | | public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) { |
| | | completion(.success(urlRequest)) |
| | | } |
| | | |
| | | @preconcurrency |
| | | public func retry(_ request: Request, |
| | | for session: Session, |
| | | dueTo error: Error, |
| | | completion: @escaping (RetryResult) -> Void) { |
| | | dueTo error: any Error, |
| | | completion: @escaping @Sendable (RetryResult) -> Void) { |
| | | completion(.doNotRetry) |
| | | } |
| | | } |
| | | |
| | | /// `RequestAdapter` closure definition. |
| | | public typealias AdaptHandler = (URLRequest, Session, _ completion: @escaping (Result<URLRequest, Error>) -> Void) -> Void |
| | | public typealias AdaptHandler = @Sendable (_ request: URLRequest, |
| | | _ session: Session, |
| | | _ completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) -> Void |
| | | |
| | | /// `RequestRetrier` closure definition. |
| | | public typealias RetryHandler = (Request, Session, Error, _ completion: @escaping (RetryResult) -> Void) -> Void |
| | | public typealias RetryHandler = @Sendable (_ request: Request, |
| | | _ session: Session, |
| | | _ error: any Error, |
| | | _ completion: @escaping @Sendable (RetryResult) -> Void) -> Void |
| | | |
| | | // MARK: - |
| | | |
| | | /// Closure-based `RequestAdapter`. |
| | | open class Adapter: RequestInterceptor { |
| | | open class Adapter: @unchecked Sendable, RequestInterceptor { |
| | | private let adaptHandler: AdaptHandler |
| | | |
| | | /// Creates an instance using the provided closure. |
| | | /// |
| | | /// - Parameter adaptHandler: `AdaptHandler` closure to be executed when handling request adaptation. |
| | | /// |
| | | @preconcurrency |
| | | public init(_ adaptHandler: @escaping AdaptHandler) { |
| | | self.adaptHandler = adaptHandler |
| | | } |
| | | |
| | | open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | @preconcurrency |
| | | open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) { |
| | | adaptHandler(urlRequest, session, completion) |
| | | } |
| | | |
| | | open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | @preconcurrency |
| | | open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) { |
| | | adaptHandler(urlRequest, state.session, completion) |
| | | } |
| | | } |
| | |
| | | /// |
| | | /// - Parameter closure: `AdaptHandler` to use to adapt the request. |
| | | /// - Returns: The `Adapter`. |
| | | @preconcurrency |
| | | public static func adapter(using closure: @escaping AdaptHandler) -> Adapter { |
| | | Adapter(closure) |
| | | } |
| | |
| | | // MARK: - |
| | | |
| | | /// Closure-based `RequestRetrier`. |
| | | open class Retrier: RequestInterceptor { |
| | | open class Retrier: @unchecked Sendable, RequestInterceptor { |
| | | private let retryHandler: RetryHandler |
| | | |
| | | /// Creates an instance using the provided closure. |
| | | /// |
| | | /// - Parameter retryHandler: `RetryHandler` closure to be executed when handling request retry. |
| | | @preconcurrency |
| | | public init(_ retryHandler: @escaping RetryHandler) { |
| | | self.retryHandler = retryHandler |
| | | } |
| | | |
| | | @preconcurrency |
| | | open func retry(_ request: Request, |
| | | for session: Session, |
| | | dueTo error: Error, |
| | | completion: @escaping (RetryResult) -> Void) { |
| | | dueTo error: any Error, |
| | | completion: @escaping @Sendable (RetryResult) -> Void) { |
| | | retryHandler(request, session, error, completion) |
| | | } |
| | | } |
| | |
| | | /// |
| | | /// - Parameter closure: `RetryHandler` to use to retry the request. |
| | | /// - Returns: The `Retrier`. |
| | | @preconcurrency |
| | | public static func retrier(using closure: @escaping RetryHandler) -> Retrier { |
| | | Retrier(closure) |
| | | } |
| | |
| | | // MARK: - |
| | | |
| | | /// `RequestInterceptor` which can use multiple `RequestAdapter` and `RequestRetrier` values. |
| | | open class Interceptor: RequestInterceptor { |
| | | open class Interceptor: @unchecked Sendable, RequestInterceptor { |
| | | /// All `RequestAdapter`s associated with the instance. These adapters will be run until one fails. |
| | | public let adapters: [RequestAdapter] |
| | | public let adapters: [any RequestAdapter] |
| | | /// All `RequestRetrier`s associated with the instance. These retriers will be run one at a time until one triggers retry. |
| | | public let retriers: [RequestRetrier] |
| | | public let retriers: [any RequestRetrier] |
| | | |
| | | /// Creates an instance from `AdaptHandler` and `RetryHandler` closures. |
| | | /// |
| | |
| | | /// - Parameters: |
| | | /// - adapter: `RequestAdapter` value to be used. |
| | | /// - retrier: `RequestRetrier` value to be used. |
| | | public init(adapter: RequestAdapter, retrier: RequestRetrier) { |
| | | public init(adapter: any RequestAdapter, retrier: any RequestRetrier) { |
| | | adapters = [adapter] |
| | | retriers = [retrier] |
| | | } |
| | |
| | | /// - adapters: `RequestAdapter` values to be used. |
| | | /// - retriers: `RequestRetrier` values to be used. |
| | | /// - interceptors: `RequestInterceptor`s to be used. |
| | | public init(adapters: [RequestAdapter] = [], retriers: [RequestRetrier] = [], interceptors: [RequestInterceptor] = []) { |
| | | public init(adapters: [any RequestAdapter] = [], |
| | | retriers: [any RequestRetrier] = [], |
| | | interceptors: [any RequestInterceptor] = []) { |
| | | self.adapters = adapters + interceptors |
| | | self.retriers = retriers + interceptors |
| | | } |
| | | |
| | | open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | @preconcurrency |
| | | open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) { |
| | | adapt(urlRequest, for: session, using: adapters, completion: completion) |
| | | } |
| | | |
| | | private func adapt(_ urlRequest: URLRequest, |
| | | for session: Session, |
| | | using adapters: [RequestAdapter], |
| | | completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | using adapters: [any RequestAdapter], |
| | | completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) { |
| | | var pendingAdapters = adapters |
| | | |
| | | guard !pendingAdapters.isEmpty else { completion(.success(urlRequest)); return } |
| | | |
| | | let adapter = pendingAdapters.removeFirst() |
| | | |
| | | adapter.adapt(urlRequest, for: session) { result in |
| | | adapter.adapt(urlRequest, for: session) { [pendingAdapters] result in |
| | | switch result { |
| | | case let .success(urlRequest): |
| | | self.adapt(urlRequest, for: session, using: pendingAdapters, completion: completion) |
| | |
| | | } |
| | | } |
| | | |
| | | open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | @preconcurrency |
| | | open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) { |
| | | adapt(urlRequest, using: state, adapters: adapters, completion: completion) |
| | | } |
| | | |
| | | private func adapt(_ urlRequest: URLRequest, |
| | | using state: RequestAdapterState, |
| | | adapters: [RequestAdapter], |
| | | completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | adapters: [any RequestAdapter], |
| | | completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) { |
| | | var pendingAdapters = adapters |
| | | |
| | | guard !pendingAdapters.isEmpty else { completion(.success(urlRequest)); return } |
| | | |
| | | let adapter = pendingAdapters.removeFirst() |
| | | |
| | | adapter.adapt(urlRequest, using: state) { result in |
| | | adapter.adapt(urlRequest, using: state) { [pendingAdapters] result in |
| | | switch result { |
| | | case let .success(urlRequest): |
| | | self.adapt(urlRequest, using: state, adapters: pendingAdapters, completion: completion) |
| | |
| | | } |
| | | } |
| | | |
| | | @preconcurrency |
| | | open func retry(_ request: Request, |
| | | for session: Session, |
| | | dueTo error: Error, |
| | | completion: @escaping (RetryResult) -> Void) { |
| | | dueTo error: any Error, |
| | | completion: @escaping @Sendable (RetryResult) -> Void) { |
| | | retry(request, for: session, dueTo: error, using: retriers, completion: completion) |
| | | } |
| | | |
| | | private func retry(_ request: Request, |
| | | for session: Session, |
| | | dueTo error: Error, |
| | | using retriers: [RequestRetrier], |
| | | completion: @escaping (RetryResult) -> Void) { |
| | | dueTo error: any Error, |
| | | using retriers: [any RequestRetrier], |
| | | completion: @escaping @Sendable (RetryResult) -> Void) { |
| | | var pendingRetriers = retriers |
| | | |
| | | guard !pendingRetriers.isEmpty else { completion(.doNotRetry); return } |
| | | |
| | | let retrier = pendingRetriers.removeFirst() |
| | | |
| | | retrier.retry(request, for: session, dueTo: error) { result in |
| | | retrier.retry(request, for: session, dueTo: error) { [pendingRetriers] result in |
| | | switch result { |
| | | case .retry, .retryWithDelay, .doNotRetryWithError: |
| | | completion(result) |
| | |
| | | /// - adapter: `AdapterHandler`to use to adapt the request. |
| | | /// - retrier: `RetryHandler` to use to retry the request. |
| | | /// - Returns: The `Interceptor`. |
| | | @preconcurrency |
| | | public static func interceptor(adapter: @escaping AdaptHandler, retrier: @escaping RetryHandler) -> Interceptor { |
| | | Interceptor(adaptHandler: adapter, retryHandler: retrier) |
| | | } |
| | |
| | | /// - adapter: `RequestAdapter` to use to adapt the request |
| | | /// - retrier: `RequestRetrier` to use to retry the request. |
| | | /// - Returns: The `Interceptor`. |
| | | public static func interceptor(adapter: RequestAdapter, retrier: RequestRetrier) -> Interceptor { |
| | | @preconcurrency |
| | | public static func interceptor(adapter: any RequestAdapter, retrier: any RequestRetrier) -> Interceptor { |
| | | Interceptor(adapter: adapter, retrier: retrier) |
| | | } |
| | | |
| | |
| | | /// a retry is triggered. |
| | | /// - interceptors: `RequestInterceptor`s to use to intercept the request. |
| | | /// - Returns: The `Interceptor`. |
| | | public static func interceptor(adapters: [RequestAdapter] = [], |
| | | retriers: [RequestRetrier] = [], |
| | | interceptors: [RequestInterceptor] = []) -> Interceptor { |
| | | @preconcurrency |
| | | public static func interceptor(adapters: [any RequestAdapter] = [], |
| | | retriers: [any RequestRetrier] = [], |
| | | interceptors: [any RequestInterceptor] = []) -> Interceptor { |
| | | Interceptor(adapters: adapters, retriers: retriers, interceptors: interceptors) |
| | | } |
| | | } |