| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754 |
- //
- // RequestInterceptorTests.swift
- //
- // Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/)
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
- //
- import Foundation
- import Testing
- @testable import Alamofire
- private struct MockError: Error {}
- private struct RetryError: Error {}
- // MARK: -
- @Suite
- struct RetryResultTests {
- @Test
- func retryRequiredProperty() async throws {
- // Given, When
- let retry = RetryResult.retry
- let retryWithDelay = RetryResult.retryWithDelay(1.0)
- let doNotRetry = RetryResult.doNotRetry
- let doNotRetryWithError = RetryResult.doNotRetryWithError(MockError())
- // Then
- #expect(retry.retryRequired == true)
- #expect(retryWithDelay.retryRequired == true)
- #expect(doNotRetry.retryRequired == false)
- #expect(doNotRetryWithError.retryRequired == false)
- }
- @Test
- func delayProperty() async throws {
- // Given, When
- let retry = RetryResult.retry
- let retryWithDelay = RetryResult.retryWithDelay(1.0)
- let doNotRetry = RetryResult.doNotRetry
- let doNotRetryWithError = RetryResult.doNotRetryWithError(MockError())
- // Then
- #expect(retry.delay == nil)
- #expect(retryWithDelay.delay == 1.0)
- #expect(doNotRetry.delay == nil)
- #expect(doNotRetryWithError.delay == nil)
- }
- @Test
- func errorProperty() {
- // Given, When
- let retry = RetryResult.retry
- let retryWithDelay = RetryResult.retryWithDelay(1.0)
- let doNotRetry = RetryResult.doNotRetry
- let doNotRetryWithError = RetryResult.doNotRetryWithError(MockError())
- // Then
- #expect(retry.error == nil)
- #expect(retryWithDelay.error == nil)
- #expect(doNotRetry.error == nil)
- #expect(doNotRetryWithError.error is MockError)
- }
- }
- // MARK: -
- @Suite
- struct AdapterTests {
- @Test
- func thatAdapterCallsAdaptHandler() async throws {
- // Given
- let urlRequest = Endpoint().urlRequest
- let session = Session()
- var adapted = false
- let adapter = Adapter { request, _, completion in
- adapted = true
- completion(.success(request))
- }
- var result: Result<URLRequest, any Error>!
- // When
- adapter.adapt(urlRequest, for: session) { result = $0 }
- // Then
- #expect(adapted)
- #expect(result.isSuccess)
- }
- @Test
- func thatAdapterCallsAdaptHandlerWithStateAPI() async throws {
- // Given
- let urlRequest = Endpoint().urlRequest
- let session = Session()
- let requestID = UUID()
- let interceptor = InspectorInterceptor(.adapter { @Sendable request, _, completionHandler in
- completionHandler(.success(request))
- })
- let state = RequestAdapterState(requestID: requestID, session: session)
- // When
- interceptor.adapt(urlRequest, using: state) { _ in }
- let adaptation = interceptor.adaptations.first
- // Then
- #expect(interceptor.adaptations.count == 1)
- #expect(adaptation?.result.isSuccess == true)
- #expect(adaptation?.urlRequest == urlRequest)
- #expect(adaptation?.state.requestID == requestID)
- #expect(adaptation.map(\.state.sessionID) == ObjectIdentifier(session))
- }
- @Test
- func thatAdapterCallsRequestRetrierDefaultImplementationInProtocolExtension() async throws {
- // Given
- let session = Session(startRequestsImmediately: false)
- let request = session.request(.default)
- let adapter = InspectorInterceptor(.adapter { @Sendable request, _, completion in
- completion(.success(request))
- })
- // When
- adapter.retry(request, for: session, dueTo: MockError()) { _ in }
- // Then
- #expect(adapter.retryResults.first == .doNotRetry)
- }
- @Test
- func thatAdapterCanBeImplementedAsynchronously() async throws {
- // Given
- let urlRequest = Endpoint().urlRequest
- let session = Session()
- let adapter = InspectorInterceptor(.adapter { @Sendable request, _, completion in
- DispatchQueue.main.async {
- completion(.success(request))
- }
- })
- // When
- await withCheckedContinuation { continuation in
- adapter.adapt(urlRequest, for: session) { _ in
- continuation.resume()
- }
- }
- // Then
- #expect(adapter.adaptations.count == 1)
- #expect(adapter.adaptations.first?.result.isSuccess == true)
- }
- }
- // MARK: -
- @Suite
- struct RetrierTests {
- @Test
- func thatRetrierCallsRetryHandler() async throws {
- // Given
- let session = Session(startRequestsImmediately: false)
- let request = session.request(.default)
- let retrier = InspectorInterceptor(Retrier { _, _, _, completion in
- completion(.retry)
- })
- // When
- retrier.retry(request, for: session, dueTo: MockError()) { _ in }
- // Then
- #expect(retrier.retryCalledCount == 1)
- #expect(retrier.retryResults.first == .retry)
- }
- @Test
- func thatRetrierCallsRequestAdapterDefaultImplementationInProtocolExtension() async throws {
- // Given
- let urlRequest = Endpoint().urlRequest
- let session = Session()
- let retrier = InspectorInterceptor(Retrier { _, _, _, completion in
- completion(.retry)
- })
- // When
- retrier.adapt(urlRequest, for: session) { _ in }
- // Then
- #expect(retrier.adaptations.first?.result.isSuccess == true)
- }
- @Test
- func thatRetrierCanBeImplementedAsynchronously() async throws {
- // Given
- let session = Session(startRequestsImmediately: false)
- let request = session.request(.default)
- let retrier = InspectorInterceptor(Retrier { _, _, _, completion in
- DispatchQueue.main.async {
- completion(.retry)
- }
- })
- // When
- await withCheckedContinuation { continuation in
- retrier.retry(request, for: session, dueTo: MockError()) { _ in
- continuation.resume()
- }
- }
- // Then
- #expect(retrier.retryCalledCount == 1)
- #expect(retrier.retryResults.first == .retry)
- }
- }
- // MARK: -
- @Suite
- struct InterceptorTests {
- @Test
- func adaptHandlerAndRetryHandlerDefaultInitializer() {
- // Given
- let adaptHandler: AdaptHandler = { urlRequest, _, completion in completion(.success(urlRequest)) }
- let retryHandler: RetryHandler = { _, _, _, completion in completion(.doNotRetry) }
- // When
- let interceptor = Interceptor(adaptHandler: adaptHandler, retryHandler: retryHandler)
- // Then
- #expect(interceptor.adapters.count == 1)
- #expect(interceptor.retriers.count == 1)
- }
- @Test
- func adapterAndRetrierDefaultInitializer() {
- // Given
- let adapter = Adapter { urlRequest, _, completion in completion(.success(urlRequest)) }
- let retrier = Retrier { _, _, _, completion in completion(.doNotRetry) }
- // When
- let interceptor = Interceptor(adapter: adapter, retrier: retrier)
- // Then
- #expect(interceptor.adapters.count == 1)
- #expect(interceptor.retriers.count == 1)
- }
- @Test
- func adaptersAndRetriersDefaultInitializer() {
- // Given
- let adapter = Adapter { urlRequest, _, completion in completion(.success(urlRequest)) }
- let retrier = Retrier { _, _, _, completion in completion(.doNotRetry) }
- // When
- let interceptor = Interceptor(adapters: [adapter, adapter], retriers: [retrier, retrier])
- // Then
- #expect(interceptor.adapters.count == 2)
- #expect(interceptor.retriers.count == 2)
- }
- @Test
- func thatInterceptorCanBeComposedOfMultipleRequestInterceptors() {
- // Given
- let adapter = Adapter { request, _, completion in completion(.success(request)) }
- let retrier = Retrier { _, _, _, completion in completion(.doNotRetry) }
- let inner = Interceptor(adapter: adapter, retrier: retrier)
- // When
- let interceptor = Interceptor(interceptors: [inner])
- // Then
- #expect(interceptor.adapters.count == 1)
- #expect(interceptor.retriers.count == 1)
- }
- @Test
- func thatInterceptorCanAdaptRequestWithNoAdapters() {
- // Given
- let urlRequest = Endpoint().urlRequest
- let session = Session()
- let interceptor = Interceptor()
- var result: Result<URLRequest, any Error>!
- // When
- interceptor.adapt(urlRequest, for: session) { result = $0 }
- // Then
- #expect(result.isSuccess == true)
- #expect(result.success == urlRequest)
- }
- @Test
- func thatInterceptorCanAdaptRequestWithOneAdapter() {
- // Given
- let urlRequest = Endpoint().urlRequest
- let session = Session()
- let adapter = Adapter { _, _, completion in completion(.failure(MockError())) }
- let interceptor = Interceptor(adapters: [adapter])
- var result: Result<URLRequest, any Error>!
- // When
- interceptor.adapt(urlRequest, for: session) { result = $0 }
- // Then
- #expect(result.isFailure)
- #expect(result.failure is MockError)
- }
- @Test
- func thatInterceptorCanAdaptRequestWithMultipleAdapters() {
- // Given
- let urlRequest = Endpoint().urlRequest
- let session = Session()
- let adapter1 = Adapter { urlRequest, _, completion in completion(.success(urlRequest)) }
- let adapter2 = Adapter { _, _, completion in completion(.failure(MockError())) }
- let interceptor = Interceptor(adapters: [adapter1, adapter2])
- var result: Result<URLRequest, any Error>!
- // When
- interceptor.adapt(urlRequest, for: session) { result = $0 }
- // Then
- #expect(result.isFailure)
- #expect(result.failure is MockError)
- }
- @Test
- func thatInterceptorCanAdaptRequestWithMultipleAdaptersUsingStateAPI() {
- // Given
- let urlRequest = Endpoint().urlRequest
- let session = Session()
- let adapter1 = Adapter { urlRequest, _, completion in completion(.success(urlRequest)) }
- let adapter2 = Adapter { _, _, completion in completion(.failure(MockError())) }
- let interceptor = Interceptor(adapters: [adapter1, adapter2])
- let state = RequestAdapterState(requestID: UUID(), session: session)
- var result: Result<URLRequest, any Error>!
- // When
- interceptor.adapt(urlRequest, using: state) { result = $0 }
- // Then
- #expect(result.isFailure)
- #expect(result.failure is MockError)
- }
- @Test
- func thatInterceptorCanAdaptRequestAsynchronously() async throws {
- // Given
- let urlRequest = Endpoint().urlRequest
- let session = Session()
- let adapter = Adapter { _, _, completion in
- DispatchQueue.main.async {
- completion(.failure(MockError()))
- }
- }
- let interceptor = InspectorInterceptor(Interceptor(adapters: [adapter]))
- // When
- await withCheckedContinuation { continuation in
- interceptor.adapt(urlRequest, for: session) { _ in
- continuation.resume()
- }
- }
- // Then
- #expect(interceptor.adaptations.first?.result.isFailure == true)
- #expect(interceptor.adaptations.first?.result.failure is MockError)
- }
- @Test
- func thatInterceptorCanRetryRequestWithNoRetriers() {
- // Given
- let session = Session(startRequestsImmediately: false)
- let request = session.request(.default)
- let interceptor = Interceptor()
- var result: RetryResult!
- // When
- interceptor.retry(request, for: session, dueTo: MockError()) { result = $0 }
- // Then
- #expect(result == .doNotRetry)
- }
- @Test
- func thatInterceptorCanRetryRequestWithOneRetrier() {
- // Given
- let session = Session(startRequestsImmediately: false)
- let request = session.request(.default)
- let retrier = Retrier { _, _, _, completion in completion(.retry) }
- let interceptor = Interceptor(retriers: [retrier])
- var result: RetryResult!
- // When
- interceptor.retry(request, for: session, dueTo: MockError()) { result = $0 }
- // Then
- #expect(result == .retry)
- }
- @Test
- func thatInterceptorCanRetryRequestWithMultipleRetriers() {
- // Given
- let session = Session(startRequestsImmediately: false)
- let request = session.request(.default)
- let retrier1 = Retrier { _, _, _, completion in completion(.doNotRetry) }
- let retrier2 = Retrier { _, _, _, completion in completion(.retry) }
- let interceptor = Interceptor(retriers: [retrier1, retrier2])
- var result: RetryResult!
- // When
- interceptor.retry(request, for: session, dueTo: MockError()) { result = $0 }
- // Then
- #expect(result == .retry)
- }
- @Test
- func thatInterceptorCanRetryRequestAsynchronously() async throws {
- // Given
- let session = Session(startRequestsImmediately: false)
- let request = session.request(.default)
- let retrier = Retrier { _, _, _, completion in
- DispatchQueue.main.async {
- completion(.retry)
- }
- }
- let interceptor = InspectorInterceptor(Interceptor(retriers: [retrier]))
- // When
- await withCheckedContinuation { continuation in
- interceptor.retry(request, for: session, dueTo: MockError()) { _ in
- continuation.resume()
- }
- }
- // Then
- #expect(interceptor.retryResults.first == .retry)
- }
- @Test
- func thatInterceptorStopsIteratingThroughPendingRetriersWithRetryResult() {
- // Given
- let session = Session(startRequestsImmediately: false)
- let request = session.request(.default)
- var retrier2Called = false
- let retrier1 = Retrier { _, _, _, completion in completion(.retry) }
- let retrier2 = Retrier { _, _, _, completion in retrier2Called = true; completion(.doNotRetry) }
- let interceptor = Interceptor(retriers: [retrier1, retrier2])
- var result: RetryResult!
- // When
- interceptor.retry(request, for: session, dueTo: MockError()) { result = $0 }
- // Then
- #expect(result == .retry)
- #expect(retrier2Called == false)
- }
- @Test
- func thatInterceptorStopsIteratingThroughPendingRetriersWithRetryWithDelayResult() {
- // Given
- let session = Session(startRequestsImmediately: false)
- let request = session.request(.default)
- var retrier2Called = false
- let retrier1 = Retrier { _, _, _, completion in completion(.retryWithDelay(1.0)) }
- let retrier2 = Retrier { _, _, _, completion in retrier2Called = true; completion(.doNotRetry) }
- let interceptor = Interceptor(retriers: [retrier1, retrier2])
- var result: RetryResult!
- // When
- interceptor.retry(request, for: session, dueTo: MockError()) { result = $0 }
- // Then
- #expect(result == .retryWithDelay(1.0))
- #expect(result.delay == 1.0)
- #expect(retrier2Called == false)
- }
- @Test
- func thatInterceptorStopsIteratingThroughPendingRetriersWithDoNotRetryResult() {
- // Given
- let session = Session(startRequestsImmediately: false)
- let request = session.request(.default)
- var retrier2Called = false
- let retrier1 = Retrier { _, _, _, completion in completion(.doNotRetryWithError(RetryError())) }
- let retrier2 = Retrier { _, _, _, completion in retrier2Called = true; completion(.doNotRetry) }
- let interceptor = Interceptor(retriers: [retrier1, retrier2])
- var result: RetryResult!
- // When
- interceptor.retry(request, for: session, dueTo: MockError()) { result = $0 }
- // Then
- #expect(result == RetryResult.doNotRetryWithError(RetryError()))
- #expect(result.error is RetryError)
- #expect(retrier2Called == false)
- }
- }
- // MARK: - Functional Tests
- @Suite
- struct InterceptorRequestTests {
- @Test
- func thatRetryPolicyRetriesRequestTimeout() async throws {
- // Given
- let interceptor = InspectorInterceptor(RetryPolicy(retryLimit: 1, exponentialBackoffScale: 0.1))
- let urlRequest = Endpoint.delay(1).modifying(\.timeout, to: 0.01)
- // When
- let request = AF.request(urlRequest, interceptor: interceptor)
- await request.finished()
- // Then
- #expect(request.tasks.count == 2, "There should be two tasks, one original, one retry.")
- #expect(interceptor.retryCalledCount == 2, "retry() should be called twice.")
- #expect(interceptor.retryResults == [.retryWithDelay(0.1), .doNotRetry], "RetryResults should be .retryWithDelay(0.1), .doNotRetry")
- }
- }
- extension DataRequest {
- func finished() async {
- await withCheckedContinuation { continuation in
- response { _ in continuation.resume() }
- }
- }
- }
- // MARK: - Static Accessors
- @Suite
- struct StaticAccessorTests {
- func consumeRequestAdapter(_ requestAdapter: any RequestAdapter) {
- _ = requestAdapter
- }
- func consumeRequestRetrier(_ requestRetrier: any RequestRetrier) {
- _ = requestRetrier
- }
- func consumeRequestInterceptor(_ requestInterceptor: any RequestInterceptor) {
- _ = requestInterceptor
- }
- @Test
- func thatAdapterCanBeCreatedStaticallyFromProtocol() {
- // Given, When, Then
- consumeRequestAdapter(.adapter { request, _, completion in completion(.success(request)) })
- }
- @Test
- func thatRetrierCanBeCreatedStaticallyFromProtocol() {
- // Given, When, Then
- consumeRequestRetrier(.retrier { _, _, _, completion in completion(.doNotRetry) })
- }
- @Test
- func thatInterceptorCanBeCreatedStaticallyFromProtocol() {
- // Given, When, Then
- consumeRequestInterceptor(.interceptor())
- }
- @Test
- func thatRetryPolicyCanBeCreatedStaticallyFromProtocol() {
- // Given, When, Then
- consumeRequestInterceptor(.retryPolicy())
- }
- @Test
- func thatConnectionLostRetryPolicyCanBeCreatedStaticallyFromProtocol() {
- // Given, When, Then
- consumeRequestInterceptor(.connectionLostRetryPolicy())
- }
- }
- // MARK: - Helpers
- /// Class which captures the output of any underlying `RequestInterceptor`.
- final class InspectorInterceptor<Interceptor: RequestInterceptor>: RequestInterceptor, Sendable {
- private struct State {
- let onAdaptation: ((_ result: Result<URLRequest, any Error>) -> Void)?
- let onRetry: ((_ retryResult: RetryResult) -> Void)?
- var adaptations: [Adaptation] = []
- var retries: [Retry] = []
- }
- private let state: Protected<State>
- struct Adaptation {
- var urlRequest: URLRequest
- var state: State
- var result: Result<URLRequest, any Error>
- var date: Date
- struct State {
- var requestID: UUID
- var sessionID: ObjectIdentifier
- }
- }
- struct Retry {
- var result: RetryResult
- var date: Date
- }
- /// Underlying interceptor.
- let interceptor: Interceptor
- /// Result of performed adaptations.
- var adaptations: [Adaptation] { state.read(\.adaptations) }
- /// Retry events.
- var retries: [Retry] { state.read(\.retries) }
- /// Result of performed retries.
- var retryResults: [RetryResult] { retries.map(\.result) }
- /// Number of times `retry` was called.
- var retryCalledCount: Int { state.read(\.retries.count) }
- init(_ interceptor: Interceptor,
- onAdaptation: ((_ result: Result<URLRequest, any Error>) -> Void)? = nil,
- onRetry: ((_ retryResult: RetryResult) -> Void)? = nil) {
- self.interceptor = interceptor
- state = Protected(State(onAdaptation: onAdaptation, onRetry: onRetry))
- }
- func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
- let sessionID = ObjectIdentifier(session)
- interceptor.adapt(urlRequest, for: session) { result in
- let onAdaptation = self.state.write { state in
- state.adaptations.append(.init(urlRequest: urlRequest, state: .init(requestID: .zero, sessionID: sessionID), result: result, date: .now))
- return state.onAdaptation
- }
- completion(result)
- onAdaptation?(result)
- }
- }
- func adapt(_ urlRequest: URLRequest, using adapterState: RequestAdapterState, completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) {
- let requestID = adapterState.requestID
- let sessionID = ObjectIdentifier(adapterState.session)
- interceptor.adapt(urlRequest, using: adapterState) { result in
- let onAdaptation = self.state.write { state in
- state.adaptations.append(.init(urlRequest: urlRequest, state: .init(requestID: requestID, sessionID: sessionID), result: result, date: .now))
- return state.onAdaptation
- }
- completion(result)
- onAdaptation?(result)
- }
- }
- func retry(_ request: Request, for session: Session, dueTo error: any Error, completion: @escaping @Sendable (RetryResult) -> Void) {
- interceptor.retry(request, for: session, dueTo: error) { result in
- let onRetry = self.state.write { state in
- state.retries.append(.init(result: result, date: .now))
- return state.onRetry
- }
- completion(result)
- onRetry?(result)
- }
- }
- }
- extension UUID {
- static let zero = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
- }
- /// Retry a request once, allowing the second to succeed using the method path.
- final class SingleRetrier: RequestInterceptor, Sendable {
- private let state = Protected(false)
- private var hasRetried: Bool { state.read(\.self) }
- func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) {
- if hasRetried {
- let method = urlRequest.method ?? .get
- let endpoint = Endpoint(path: .method(method),
- method: method,
- headers: urlRequest.headers)
- completion(.success(endpoint.urlRequest))
- } else {
- completion(.success(urlRequest))
- }
- }
- func retry(_ request: Request, for session: Session, dueTo error: any Error, completion: @escaping @Sendable (RetryResult) -> Void) {
- completion(hasRetried ? .doNotRetry : .retry)
- state.write(true)
- }
- }
- extension Alamofire.RetryResult: Swift.Equatable {
- public static func ==(lhs: RetryResult, rhs: RetryResult) -> Bool {
- switch (lhs, rhs) {
- case (.retry, .retry),
- (.doNotRetry, .doNotRetry),
- (.doNotRetryWithError, .doNotRetryWithError):
- true
- case let (.retryWithDelay(leftDelay), .retryWithDelay(rightDelay)):
- leftDelay == rightDelay
- default:
- false
- }
- }
- }
|