diff --git a/GoodNetworking-Sample/GoodNetworking-Sample.xcodeproj/xcshareddata/xcschemes/GoodNetworking-Sample.xcscheme b/GoodNetworking-Sample/GoodNetworking-Sample.xcodeproj/xcshareddata/xcschemes/GoodNetworking-Sample.xcscheme new file mode 100644 index 0000000..8be713f --- /dev/null +++ b/GoodNetworking-Sample/GoodNetworking-Sample.xcodeproj/xcshareddata/xcschemes/GoodNetworking-Sample.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GoodNetworking-Sample/GoodNetworking-Sample/Managers/RequestManager/RequestManager.swift b/GoodNetworking-Sample/GoodNetworking-Sample/Managers/RequestManager/RequestManager.swift index 2f5d3ad..8f492de 100644 --- a/GoodNetworking-Sample/GoodNetworking-Sample/Managers/RequestManager/RequestManager.swift +++ b/GoodNetworking-Sample/GoodNetworking-Sample/Managers/RequestManager/RequestManager.swift @@ -24,8 +24,7 @@ final class RequestManager: RequestManagerType { } func fetchHero(heroId: Int) -> RequestPublisher { - return session.request(endpoint: Endpoint.hero(id: heroId)) - .goodify() + return session.execute(endpoint: Endpoint.hero(id: heroId)) .eraseToAnyPublisher() } diff --git a/GoodNetworking-Sample/GoodNetworking-Sample/Screens/Home/HomeViewModel.swift b/GoodNetworking-Sample/GoodNetworking-Sample/Screens/Home/HomeViewModel.swift index 170dc40..ef1b614 100644 --- a/GoodNetworking-Sample/GoodNetworking-Sample/Screens/Home/HomeViewModel.swift +++ b/GoodNetworking-Sample/GoodNetworking-Sample/Screens/Home/HomeViewModel.swift @@ -49,13 +49,18 @@ final class HomeViewModel { extension HomeViewModel { func fetchHero() { - di.requestManager.fetchHero(heroId: Int.random(in: Constants.Hero.range)) - .map{ HeroFetchingResultState.success($0) } - .catch { Just(HeroFetchingResultState.error($0)) } - .prepend(HeroFetchingResultState.loading) - .eraseToAnyPublisher() - .sink { [weak self] result in self?.heroResult.send(result) } - .store(in: &cancellables) +// di.requestManager.fetchHero(heroId: Int.random(in: Constants.Hero.range)) + let heroID = Int.random(in: Constants.Hero.range) + return Publishers.Merge( + di.requestManager.fetchHero(heroId: heroID), + di.requestManager.fetchHero(heroId: heroID) + ) + .map{ HeroFetchingResultState.success($0) } + .catch { Just(HeroFetchingResultState.error($0)) } + .prepend(HeroFetchingResultState.loading) + .eraseToAnyPublisher() + .sink { [weak self] result in self?.heroResult.send(result) } + .store(in: &cancellables) } func goToAbout() { diff --git a/Sources/GoodNetworking/Executor/ExecutorTask.swift b/Sources/GoodNetworking/Executor/ExecutorTask.swift new file mode 100644 index 0000000..546aa00 --- /dev/null +++ b/Sources/GoodNetworking/Executor/ExecutorTask.swift @@ -0,0 +1,30 @@ +// +// ExecutorTask.swift +// +// +// Created by Matus Klasovity on 06/08/2024. +// + +import Foundation +import Alamofire + +final class ExecutorTask { + + var finishDate: Date? + let taskID: String + let task: Task, Never> + + private let cacheTimeout: TimeInterval + + var exceedsTimeout: Bool { + guard let finishDate else { return false } + return Date().timeIntervalSince(finishDate) > cacheTimeout + } + + init(taskID: String, task: Task, Never>, cacheTimeout: TimeInterval) { + self.taskID = taskID + self.task = task + self.cacheTimeout = cacheTimeout + } + +} diff --git a/Sources/GoodNetworking/Executor/RequestExecutor.swift b/Sources/GoodNetworking/Executor/RequestExecutor.swift new file mode 100644 index 0000000..4f8a73c --- /dev/null +++ b/Sources/GoodNetworking/Executor/RequestExecutor.swift @@ -0,0 +1,96 @@ +// +// RequestExecutor.swift +// +// +// Created by Matus Klasovity on 06/08/2024. +// + +import Foundation +import Alamofire + +actor RequestExecutor { + + private let logger: SessionLogger = { + if #available(iOS 14, *) { + return OSLogLogger() + } else { + return PrintLogger() + } + }() + + private var runningRequestTasks: [String: ExecutorTask] = [:] + + func execute( + _ request: DataRequest, + taskID: String, + deduplicate: Bool, + validResponseCodes: Set, + emptyResponseCodes: Set, + emptyResponseMethods: Set, + cacheTimeout: TimeInterval + ) async -> DataResponse { + let randomUUID = UUID().uuidString + return await execute( + request, + taskID: deduplicate ? taskID : randomUUID, + validResponseCodes: validResponseCodes, + emptyResponseCodes: emptyResponseCodes, + emptyResponseMethods: emptyResponseMethods, + cacheTimeout: cacheTimeout + ) + } + + private func execute( + _ request: DataRequest, + taskID: String, + validResponseCodes: Set, + emptyResponseCodes: Set, + emptyResponseMethods: Set, + cacheTimeout: TimeInterval + ) async -> DataResponse { + runningRequestTasks = runningRequestTasks.filter { !$0.value.exceedsTimeout } + + if let runningTask = runningRequestTasks[taskID] { + logger.log(level: .info, message: "🚀 taskID: \(taskID) Cached value used") + return await runningTask.task.value.map { $0 as! SuccessType } + } else { + let requestTask = Task, Never> { + let result: DataResponse = await request.goodifyAsync( + validResponseCodes: validResponseCodes, + emptyResponseCodes: emptyResponseCodes, + emptyResponseMethods: emptyResponseMethods + ) + + return result.map { $0 as Decodable } + } + + logger.log(level: .info, message: "🚀 taskID: \(taskID): Task created") + let executorTask: ExecutorTask = ExecutorTask( + taskID: taskID, + task: requestTask, + cacheTimeout: cacheTimeout + ) + + runningRequestTasks[taskID] = executorTask + + let dataResponse = await requestTask.value + switch dataResponse.result { + case .success: + logger.log(level: .info, message: "🚀 taskID: \(taskID): Task finished successfully") + if cacheTimeout > 0 { + runningRequestTasks[taskID]?.finishDate = Date() + } else { + runningRequestTasks[taskID] = nil + } + + case .failure: + logger.log(level: .error, message: "🚀 taskID: \(taskID): Task finished with error") + runningRequestTasks[taskID] = nil + } + + return dataResponse.map { $0 as! SuccessType } + } + } + +} + diff --git a/Sources/GoodNetworking/Extensions/DataRequestExtensions.swift b/Sources/GoodNetworking/Extensions/DataRequestExtensions.swift index 34a4f69..b90c1c2 100644 --- a/Sources/GoodNetworking/Extensions/DataRequestExtensions.swift +++ b/Sources/GoodNetworking/Extensions/DataRequestExtensions.swift @@ -81,6 +81,81 @@ public extension DataRequest { .value() } + /// Returns a `T` response or thrown an error for this instance and uses a ``serializingDecodable`` method to serialize the + /// response. + /// + /// - Parameters: + /// - type: `Decodable` type to which to decode response `Data`. Inferred from the context by default. + /// - validResponseCodes: `Set` of acceptable HTTP status code in the default acceptable range of 200…299. + /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by + /// default. + /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless of + /// status code. `[.head]` by default. + /// - decoder: `JSONDecoder` instance used to decode response `Data`. For `Decodable` `JSONDecoder()` by default. + /// For `Decodable & WithCustomDecoder` custom `decoder` used by default. + /// + /// - Returns: The `DataResponsePublisher`. + func goodifyAsync( + type: T.Type = T.self, + validResponseCodes: Set = Set(200..<299), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, + decoder: JSONDecoder = (T.self as? WithCustomDecoder.Type)?.decoder ?? JSONDecoder() + ) async throws -> T { + do { + return try await self + .validate(statusCode: validResponseCodes) + .serializingDecodable( + T.self, + automaticallyCancelling: true, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyResponseMethods + ) + .value + } catch let afError as AFError { + // potentionally the place for alamofire non fatal crashes logger + throw afError + } catch { + throw error + } + } + + + /// Returns a `DataResponse` for this instance and uses a ``serializingDecodable`` method to serialize the + /// response. + /// + /// - Parameters: + /// - type: `Decodable` type to which to decode response `Data`. Inferred from the context by default. + /// - validResponseCodes: `Set` of acceptable HTTP status code in the default acceptable range of 200…299. + /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by + /// default. + /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless of + /// status code. `[.head]` by default. + /// - decoder: `JSONDecoder` instance used to decode response `Data`. For `Decodable` `JSONDecoder()` by default. + /// For `Decodable & WithCustomDecoder` custom `decoder` used by default. + /// + /// - Returns: The `DataResponsePublisher`. + func goodifyAsync( + type: T.Type = T.self, + validResponseCodes: Set = Set(200..<299), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, + decoder: JSONDecoder = (T.self as? WithCustomDecoder.Type)?.decoder ?? JSONDecoder() + ) async -> DataResponse { + await self + .validate(statusCode: validResponseCodes) + .serializingDecodable( + T.self, + automaticallyCancelling: true, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyResponseMethods + ) + .response + } + + } // MARK: - Private diff --git a/Sources/GoodNetworking/Extensions/FutureExtensions.swift b/Sources/GoodNetworking/Extensions/FutureExtensions.swift new file mode 100644 index 0000000..344e472 --- /dev/null +++ b/Sources/GoodNetworking/Extensions/FutureExtensions.swift @@ -0,0 +1,47 @@ +// +// FutureExtensions.swift +// +// +// Created by Matus Klasovity on 06/08/2024. +// + +import Foundation +import Combine + +extension Future where Failure == Error { + + static func create(asyncThrowableFunc: @Sendable @escaping () async throws -> Output) -> Self { + Self.init { promise in + nonisolated(unsafe) let promise = promise + Task { + do { + let result = try await asyncThrowableFunc() + await MainActor.run { + promise(.success(result)) + } + } catch { + await MainActor.run { + promise(.failure(error)) + } + } + } + } + } + +} + +extension Future where Failure == Never { + + static func create(asyncFunc: @Sendable @escaping () async -> Output) -> Self { + Self.init { promise in + nonisolated(unsafe) let promise = promise + Task { + let result = await asyncFunc() + await MainActor.run { + promise(.success(result)) + } + } + } + } + +} diff --git a/Sources/GoodNetworking/Session/NetworkSession.swift b/Sources/GoodNetworking/Session/NetworkSession.swift index d050216..e68cdfb 100644 --- a/Sources/GoodNetworking/Session/NetworkSession.swift +++ b/Sources/GoodNetworking/Session/NetworkSession.swift @@ -7,6 +7,7 @@ import Alamofire import Foundation +import Combine /// Executes network requests for the client app. public class NetworkSession { @@ -22,6 +23,8 @@ public class NetworkSession { public let baseUrl: String? + private let requestExecutor = RequestExecutor() + // MARK: - Initialization /// A public initializer that sets the baseURL and configuration properties, and initializes the underlying `Session` object. @@ -42,16 +45,17 @@ public class NetworkSession { } -// MARK: - Request +// MARK: - Build Request public extension NetworkSession { /// Builds a DataRequest object by constructing URL and Body parameters. /// /// - Parameters: - /// - endpoint: A GREndpoint instance representing the endpoint. + /// - endpoint: A Endpoint instance representing the endpoint. /// - base: An optional BaseURL instance representing the base URL. If not provided, the default `baseUrl` property will be used. /// - Returns: A DataRequest object that is ready to be executed. + @available(*, deprecated, renamed: "buildRequest", message: "Request method is deprecated, use buildRequest instead.") func request(endpoint: Endpoint, base: String? = nil) -> DataRequest { let baseUrl = base ?? baseUrl ?? "" @@ -64,6 +68,318 @@ public extension NetworkSession { ) } + /// Builds a DataRequest object by constructing URL and Body parameters. + /// + /// - Parameters: + /// - endpoint: A Endpoint instance representing the endpoint. + /// - base: An optional BaseURL instance representing the base URL. If not provided, the default `baseUrl` property will be used. + /// - Returns: A DataRequest object that is ready to be executed. + func buildRequest(endpoint: Endpoint, base: String? = nil) -> DataRequest { + let baseUrl = base ?? baseUrl ?? "" + + return session.request( + try? endpoint.url(on: baseUrl), + method: endpoint.method, + parameters: endpoint.parameters?.dictionary, + encoding: endpoint.encoding, + headers: endpoint.headers + ) + } + +} + +// MARK: - Execute request - Async + +public extension NetworkSession { + + /// Executes a data request and returns a `DataResponse` containing the result of the request. + /// + /// This method allows you to specify various parameters to control the request execution, + /// including deduplication, valid response codes, and empty response handling. + /// + /// - Parameters: + /// - request: The `DataRequest` to be executed. + /// - deduplicate: A boolean value indicating whether to deduplicate the request. Default is `true`. + /// - validResponseCodes: A set of valid HTTP response codes. Default is all codes from 200 to 299. + /// - emptyResponseCodes: A set of HTTP response codes that indicate an empty response. Default is the `DecodableResponseSerializer` default empty response codes for the specified `ResultType`. + /// - emptyResponseMethods: A set of HTTP methods that indicate an empty response. Default is the `DecodableResponseSerializer` default empty request methods for the specified `ResultType`. + /// - cacheTimeout: The time interval to cache the successful response. Default is `0` (no cache). + /// - Returns: A `DataResponse` containing the result of the request, which includes either the decoded result of type `ResultType` or an `AFError`. + func execute( + request: DataRequest, + deduplicate: Bool = true, + validResponseCodes: Set = Set(200..<300), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, + cacheTimeout: TimeInterval = 0 + ) async -> DataResponse { + let taskID = request.convertible.urlRequest?.url?.absoluteString ?? UUID().uuidString + + return await requestExecutor.execute( + request, + taskID: taskID, + deduplicate: deduplicate, + validResponseCodes: validResponseCodes, + emptyResponseCodes: emptyResponseCodes, + emptyResponseMethods: emptyResponseMethods, + cacheTimeout: cacheTimeout + ) + } + + /// Executes a request to the specified endpoint and returns a `DataResponse` containing the result. + /// + /// This method allows you to specify various parameters to control the request execution, + /// including the base URL, deduplication, valid response codes, and empty response handling. + /// + /// - Parameters: + /// - endpoint: The `Endpoint` representing the API endpoint to be requested. + /// - base: An optional base URL to be used for the request. If not provided, the default base URL is used. + /// - deduplicate: A boolean value indicating whether to deduplicate the request. Default is `true`. + /// - validResponseCodes: A set of valid HTTP response codes. Default is all codes from 200 to 299. + /// - emptyResponseCodes: A set of HTTP response codes that indicate an empty response. Default is the `DecodableResponseSerializer` default empty response codes for the specified `ResultType`. + /// - emptyResponseMethods: A set of HTTP methods that indicate an empty response. Default is the `DecodableResponseSerializer` default empty request methods for the specified `ResultType`. + /// - cacheTimeout: The time interval to cache the successful response. Default is `0` (no cache). + /// - Returns: A `DataResponse` containing the result of the request, which includes either the decoded result of type `ResultType` or an `AFError`. + func execute( + endpoint: Endpoint, + base: String? = nil, + deduplicate: Bool = true, + validResponseCodes: Set = Set(200..<300), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, + cacheTimeout: TimeInterval = 0 + ) async -> DataResponse { + let baseUrl = base ?? baseUrl ?? "" + let taskID = (try? endpoint.url(on: baseUrl).absoluteString) ?? UUID().uuidString + let request = self.buildRequest(endpoint: endpoint, base: base) + + return await requestExecutor.execute( + request, + taskID: taskID, + deduplicate: deduplicate, + validResponseCodes: validResponseCodes, + emptyResponseCodes: emptyResponseCodes, + emptyResponseMethods: emptyResponseMethods, + cacheTimeout: cacheTimeout + ) + } + +} + +// MARK: - Execute Request - Publisher DataResponse + +public extension NetworkSession { + + /// Executes a request to the specified endpoint and returns a `DataResponse` containing the result. + /// + /// This method allows you to specify various parameters to control the request execution, + /// including the base URL, deduplication, valid response codes, and empty response handling. + /// + /// - Parameters: + /// - endpoint: The `Endpoint` representing the API endpoint to be requested. + /// - base: An optional base URL to be used for the request. If not provided, the default base URL is used. + /// - deduplicate: A boolean value indicating whether to deduplicate the request. Default is `true`. + /// - validResponseCodes: A set of valid HTTP response codes. Default is all codes from 200 to 299. + /// - emptyResponseCodes: A set of HTTP response codes that indicate an empty response. Default is the `DecodableResponseSerializer` default empty response codes for the specified `ResultType`. + /// - emptyResponseMethods: A set of HTTP methods that indicate an empty response. Default is the `DecodableResponseSerializer` default empty request methods for the specified `ResultType`. + /// - cacheTimeout: The time interval to cache the successful response. Default is `0` (no cache). + /// - Returns: A Publisher of `DataResponse` containing the result of the request, which includes either the decoded result of type `ResultType` or an `AFError`. + func execute( + request: DataRequest, + deduplicate: Bool = true, + validResponseCodes: Set = Set(200..<300), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, + cacheTimeout: TimeInterval = 0 + ) -> AnyPublisher, Never> { + let taskID = request.convertible.urlRequest?.url?.absoluteString ?? UUID().uuidString + + return Future.create { [weak self] in + guard let self else { + return DataResponse( + request: nil, + response: nil, + data: nil, + metrics: nil, + serializationDuration: .nan, + result: .failure(AFError.sessionDeinitialized) + ) + } + + return await requestExecutor.execute( + request, + taskID: taskID, + deduplicate: deduplicate, + validResponseCodes: validResponseCodes, + emptyResponseCodes: emptyResponseCodes, + emptyResponseMethods: emptyResponseMethods, + cacheTimeout: cacheTimeout + ) + } + .eraseToAnyPublisher() + } + + /// Executes a request to the specified endpoint and returns a `DataResponse` containing the result. + /// + /// This method allows you to specify various parameters to control the request execution, + /// including the base URL, deduplication, valid response codes, and empty response handling. + /// + /// - Parameters: + /// - endpoint: The `Endpoint` representing the API endpoint to be requested. + /// - base: An optional base URL to be used for the request. If not provided, the default base URL is used. + /// - deduplicate: A boolean value indicating whether to deduplicate the request. Default is `true`. + /// - validResponseCodes: A set of valid HTTP response codes. Default is all codes from 200 to 299. + /// - emptyResponseCodes: A set of HTTP response codes that indicate an empty response. Default is the `DecodableResponseSerializer` default empty response codes for the specified `ResultType`. + /// - emptyResponseMethods: A set of HTTP methods that indicate an empty response. Default is the `DecodableResponseSerializer` default empty request methods for the specified `ResultType`. + /// - cacheTimeout: The time interval to cache the successful response. Default is `0` (no cache). + /// - Returns: A Publisher of `DataResponse` containing the result of the request, which includes either the decoded result of type `ResultType` or an `AFError`. + func execute( + endpoint: Endpoint, + base: String? = nil, + deduplicate: Bool = true, + validResponseCodes: Set = Set(200..<300), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, + cacheTimeout: TimeInterval = 0 + ) -> AnyPublisher, Never> { + let baseUrl = base ?? baseUrl ?? "" + let taskID = (try? endpoint.url(on: baseUrl).absoluteString) ?? UUID().uuidString + let request = self.buildRequest(endpoint: endpoint, base: base) + + return Future.create { [weak self] in + guard let self else { + return DataResponse( + request: nil, + response: nil, + data: nil, + metrics: nil, + serializationDuration: .nan, + result: .failure(AFError.sessionDeinitialized) + ) + } + + return await requestExecutor.execute( + request, + taskID: taskID, + deduplicate: deduplicate, + validResponseCodes: validResponseCodes, + emptyResponseCodes: emptyResponseCodes, + emptyResponseMethods: emptyResponseMethods, + cacheTimeout: cacheTimeout + ) + } + .eraseToAnyPublisher() + } + +} + +// MARK: - Execute Request - Publisher Response + +public extension NetworkSession { + + /// Executes a request to the specified endpoint and returns a `DataResponse` containing the result. + /// + /// This method allows you to specify various parameters to control the request execution, + /// including the base URL, deduplication, valid response codes, and empty response handling. + /// + /// - Parameters: + /// - endpoint: The `Endpoint` representing the API endpoint to be requested. + /// - base: An optional base URL to be used for the request. If not provided, the default base URL is used. + /// - deduplicate: A boolean value indicating whether to deduplicate the request. Default is `true`. + /// - validResponseCodes: A set of valid HTTP response codes. Default is all codes from 200 to 299. + /// - emptyResponseCodes: A set of HTTP response codes that indicate an empty response. Default is the `DecodableResponseSerializer` default empty response codes for the specified `ResultType`. + /// - emptyResponseMethods: A set of HTTP methods that indicate an empty response. Default is the `DecodableResponseSerializer` default empty request methods for the specified `ResultType`. + /// - cacheTimeout: The time interval to cache the successful response. Default is `0` (no cache). + /// - Returns: A Publisher of `ResultType` or an `AFError`. + func execute( + endpoint: Endpoint, + base: String? = nil, + deduplicate: Bool = true, + validResponseCodes: Set = Set(200..<300), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, + cacheTimeout: TimeInterval = 0 + ) -> AnyPublisher { + let baseUrl = base ?? baseUrl ?? "" + let taskID = (try? endpoint.url(on: baseUrl).absoluteString) ?? UUID().uuidString + let request = self.buildRequest(endpoint: endpoint, base: base) + + return Future.create { [weak self] in + guard let self else { + throw AFError.sessionDeinitialized + } + + let dataResponse: DataResponse = await requestExecutor.execute( + request, + taskID: taskID, + deduplicate: deduplicate, + validResponseCodes: validResponseCodes, + emptyResponseCodes: emptyResponseCodes, + emptyResponseMethods: emptyResponseMethods, + cacheTimeout: cacheTimeout + ) + + switch dataResponse.result { + case .success(let success): + return success + case .failure(let failure): + throw failure + } + } + .mapError { $0.asAFError(orFailWith: "") } + .eraseToAnyPublisher() + } + + /// Executes a request to the specified endpoint and returns a `DataResponse` containing the result. + /// + /// This method allows you to specify various parameters to control the request execution, + /// including the base URL, deduplication, valid response codes, and empty response handling. + /// + /// - Parameters: + /// - endpoint: The `Endpoint` representing the API endpoint to be requested. + /// - base: An optional base URL to be used for the request. If not provided, the default base URL is used. + /// - deduplicate: A boolean value indicating whether to deduplicate the request. Default is `true`. + /// - validResponseCodes: A set of valid HTTP response codes. Default is all codes from 200 to 299. + /// - emptyResponseCodes: A set of HTTP response codes that indicate an empty response. Default is the `DecodableResponseSerializer` default empty response codes for the specified `ResultType`. + /// - emptyResponseMethods: A set of HTTP methods that indicate an empty response. Default is the `DecodableResponseSerializer` default empty request methods for the specified `ResultType`. + /// - cacheTimeout: The time interval to cache the successful response. Default is `0` (no cache). + /// - Returns: A Publisher of `ResultType` or an `AFError`. + func execute( + request: DataRequest, + deduplicate: Bool = true, + validResponseCodes: Set = Set(200..<300), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, + cacheTimeout: TimeInterval = 0 + ) -> AnyPublisher { + let taskID = request.convertible.urlRequest?.url?.absoluteString ?? UUID().uuidString + + return Future.create { [weak self] in + guard let self else { + throw AFError.sessionDeinitialized + } + + let dataResponse: DataResponse = await requestExecutor.execute( + request, + taskID: taskID, + deduplicate: deduplicate, + validResponseCodes: validResponseCodes, + emptyResponseCodes: emptyResponseCodes, + emptyResponseMethods: emptyResponseMethods, + cacheTimeout: cacheTimeout + ) + + switch dataResponse.result { + case .success(let success): + return success + case .failure(let failure): + throw failure + } + } + .mapError { $0.asAFError(orFailWith: "") } + .eraseToAnyPublisher() + } + } // MARK: - Download