diff --git a/PubNubMembership/Sources/Membership+PubNub.swift b/PubNubMembership/Sources/Membership+PubNub.swift index 2c7f79f0..3f08a419 100644 --- a/PubNubMembership/Sources/Membership+PubNub.swift +++ b/PubNubMembership/Sources/Membership+PubNub.swift @@ -18,7 +18,6 @@ import PubNubUser public protocol PubNubMembershipInterface { /// A copy of the configuration object used for this session var configuration: PubNubConfiguration { get } - /// Session used for performing request/response REST calls var networkSession: SessionReplaceable { get } @@ -268,6 +267,7 @@ public extension PubNubMembershipInterface { (requestConfig.customSession ?? networkSession) .route( router, + requestOperator: configuration.automaticRetry?[.fetchMemberships], responseDecoder: FetchMultipleValueResponseDecoder(), responseQueue: requestConfig.responseQueue ) { result in @@ -320,6 +320,7 @@ public extension PubNubMembershipInterface { (requestConfig.customSession ?? networkSession) .route( router, + requestOperator: configuration.automaticRetry?[.fetchMemberships], responseDecoder: FetchMultipleValueResponseDecoder(), responseQueue: requestConfig.responseQueue ) { result in @@ -365,6 +366,7 @@ public extension PubNubMembershipInterface { (requestConfig.customSession ?? networkSession) .route( router, + requestOperator: configuration.automaticRetry?[.addMemberships], responseDecoder: FetchStatusResponseDecoder(), responseQueue: requestConfig.responseQueue ) { result in @@ -401,6 +403,7 @@ public extension PubNubMembershipInterface { (requestConfig.customSession ?? networkSession) .route( router, + requestOperator: configuration.automaticRetry?[.addMemberships], responseDecoder: FetchStatusResponseDecoder(), responseQueue: requestConfig.responseQueue ) { result in @@ -463,6 +466,7 @@ public extension PubNubMembershipInterface { (requestConfig.customSession ?? networkSession) .route( router, + requestOperator: configuration.automaticRetry?[.removeMemberships], responseDecoder: FetchStatusResponseDecoder(), responseQueue: requestConfig.responseQueue ) { result in @@ -499,6 +503,7 @@ public extension PubNubMembershipInterface { (requestConfig.customSession ?? networkSession) .route( router, + requestOperator: configuration.automaticRetry?[.removeMemberships], responseDecoder: FetchStatusResponseDecoder(), responseQueue: requestConfig.responseQueue ) { result in diff --git a/PubNubSpace/Sources/Space+PubNub.swift b/PubNubSpace/Sources/Space+PubNub.swift index 8954f8ae..23e28020 100644 --- a/PubNubSpace/Sources/Space+PubNub.swift +++ b/PubNubSpace/Sources/Space+PubNub.swift @@ -16,7 +16,6 @@ import PubNub public protocol PubNubSpaceInterface { /// A copy of the configuration object used for this session var configuration: PubNubConfiguration { get } - /// Session used for performing request/response REST calls var networkSession: SessionReplaceable { get } @@ -213,6 +212,7 @@ public extension PubNubSpaceInterface { (requestConfig.customSession ?? networkSession) .route( router, + requestOperator: configuration.automaticRetry?[.fetchSpaces], responseDecoder: FetchMultipleValueResponseDecoder(), responseQueue: requestConfig.responseQueue ) { result in @@ -237,6 +237,7 @@ public extension PubNubSpaceInterface { (requestConfig.customSession ?? networkSession) .route( router, + requestOperator: configuration.automaticRetry?[.fetchSpaces], responseDecoder: FetchSingleValueResponseDecoder(), responseQueue: requestConfig.responseQueue ) { result in @@ -273,6 +274,7 @@ public extension PubNubSpaceInterface { (requestConfig.customSession ?? networkSession) .route( router, + requestOperator: configuration.automaticRetry?[.createSpace], responseDecoder: FetchSingleValueResponseDecoder(), responseQueue: requestConfig.responseQueue ) { result in @@ -317,6 +319,7 @@ public extension PubNubSpaceInterface { (requestConfig.customSession ?? networkSession) .route( router, + requestOperator: configuration.automaticRetry?[.removeSpace], responseDecoder: FetchStatusResponseDecoder(), responseQueue: requestConfig.responseQueue ) { result in diff --git a/PubNubUser/Sources/User+PubNub.swift b/PubNubUser/Sources/User+PubNub.swift index 2ebf20a4..6724cd1f 100644 --- a/PubNubUser/Sources/User+PubNub.swift +++ b/PubNubUser/Sources/User+PubNub.swift @@ -9,14 +9,12 @@ // import Foundation - import PubNub /// Protocol interface to manage `PubNubUser` entities using closures public protocol PubNubUserInterface { /// A copy of the configuration object used for this session var configuration: PubNubConfiguration { get } - /// Session used for performing request/response REST calls var networkSession: SessionReplaceable { get } @@ -221,6 +219,7 @@ public extension PubNubUserInterface { (requestConfig.customSession ?? networkSession)? .route( router, + requestOperator: configuration.automaticRetry?[.fetchUsers], responseDecoder: FetchMultipleValueResponseDecoder(), responseQueue: requestConfig.responseQueue ) { result in @@ -248,6 +247,7 @@ public extension PubNubUserInterface { (requestConfig.customSession ?? networkSession) .route( router, + requestOperator: configuration.automaticRetry?[.fetchUsers], responseDecoder: FetchSingleValueResponseDecoder(), responseQueue: requestConfig.responseQueue ) { @@ -288,6 +288,7 @@ public extension PubNubUserInterface { (requestConfig.customSession ?? networkSession) .route( router, + requestOperator: configuration.automaticRetry?[.createUser], responseDecoder: FetchSingleValueResponseDecoder(), responseQueue: requestConfig.responseQueue ) { result in @@ -336,6 +337,7 @@ public extension PubNubUserInterface { (requestConfig.customSession ?? networkSession) .route( router, + requestOperator: configuration.automaticRetry?[.removeUser], responseDecoder: FetchStatusResponseDecoder(), responseQueue: requestConfig.responseQueue ) { result in diff --git a/Sources/PubNub/APIs/File+PubNub.swift b/Sources/PubNub/APIs/File+PubNub.swift index f99b1d14..329d44bd 100644 --- a/Sources/PubNub/APIs/File+PubNub.swift +++ b/Sources/PubNub/APIs/File+PubNub.swift @@ -29,6 +29,7 @@ public extension PubNub { ) { route( FileManagementRouter(.list(channel: channel, limit: limit, next: next), configuration: configuration), + requestOperator: configuration.automaticRetry?[.listFiles], responseDecoder: FileListResponseDecoder(), custom: requestConfig ) { result in @@ -60,6 +61,7 @@ public extension PubNub { ) { route( FileManagementRouter(.delete(channel: channel, fileId: fileId, filename: filename), configuration: configuration), + requestOperator: configuration.automaticRetry?[.removeFile], responseDecoder: FileGeneralSuccessResponseDecoder(), custom: requestConfig ) { result in @@ -137,6 +139,7 @@ public extension PubNub { .generateURL(channel: channel, body: .init(name: remoteFilename)), configuration: configuration ), + requestOperator: configuration.automaticRetry?[.generateFileUploadURL], responseDecoder: FileGenerateResponseDecoder(), custom: requestConfig ) { [configuration] result in @@ -225,9 +228,12 @@ public extension PubNub { configuration: configuration ) - route(router, - responseDecoder: PublishResponseDecoder(), - custom: request.customRequestConfig) { result in + route( + router, + requestOperator: configuration.automaticRetry?[.publishFile], + responseDecoder: PublishResponseDecoder(), + custom: request.customRequestConfig + ) { result in completion?(result.map { $0.payload.timetoken }) } } diff --git a/Sources/PubNub/Helpers/Constants.swift b/Sources/PubNub/Helpers/Constants.swift index aa29d619..e1cd1ac8 100644 --- a/Sources/PubNub/Helpers/Constants.swift +++ b/Sources/PubNub/Helpers/Constants.swift @@ -162,6 +162,10 @@ public extension Constant { /// Produces a `User-Agent` header according to /// [RFC7231 section 5.5.3](https://tools.ietf.org/html/rfc7231#section-5.5.3) static let userAgentHeaderKey = "User-Agent" + + /// A header indicating how long to wait before making a new request + /// [RFC6585 section 4](https://datatracker.ietf.org/doc/html/rfc6585#section-4) + static let retryAfterHeaderKey = "Retry-After" internal static let defaultUserAgentHeader: String = { let userAgent: String = { diff --git a/Sources/PubNub/Networking/Replaceables+PubNub.swift b/Sources/PubNub/Networking/Replaceables+PubNub.swift index ae597ec8..f86ed392 100644 --- a/Sources/PubNub/Networking/Replaceables+PubNub.swift +++ b/Sources/PubNub/Networking/Replaceables+PubNub.swift @@ -126,6 +126,7 @@ public protocol SessionReplaceable { func route( _ router: HTTPRouter, + requestOperator: RequestOperator?, responseDecoder: Decoder, responseQueue: DispatchQueue, completion: @escaping (Result, Error>) -> Void @@ -135,11 +136,12 @@ public protocol SessionReplaceable { public extension SessionReplaceable { func route( _ router: HTTPRouter, + requestOperator: RequestOperator? = nil, responseDecoder: Decoder, responseQueue: DispatchQueue = .main, completion: @escaping (Result, Error>) -> Void ) where Decoder: ResponseDecoder { - request(with: router, requestOperator: nil) + request(with: router, requestOperator: requestOperator) .validate() .response( on: responseQueue, diff --git a/Sources/PubNub/Networking/Request/Operators/AutomaticRetry.swift b/Sources/PubNub/Networking/Request/Operators/AutomaticRetry.swift index 0c433b87..8b44ee61 100644 --- a/Sources/PubNub/Networking/Request/Operators/AutomaticRetry.swift +++ b/Sources/PubNub/Networking/Request/Operators/AutomaticRetry.swift @@ -16,41 +16,46 @@ public struct AutomaticRetry: RequestOperator, Hashable { public static var `default` = AutomaticRetry() /// No retry will be performed public static var none = AutomaticRetry(retryLimit: 1) - /// Retry immediately twice on lost network connection - public static var connectionLost = AutomaticRetry(policy: .immediately, - retryableURLErrorCodes: [.networkConnectionLost]) + /// Retry on lost network connection + public static var connectionLost = AutomaticRetry( + policy: .defaultLinear, + retryableURLErrorCodes: [.networkConnectionLost] + ) /// Exponential backoff twice when no internet connection is detected - public static var noInternet = AutomaticRetry(policy: .defaultExponential, - retryableURLErrorCodes: [.notConnectedToInternet]) - + public static var noInternet = AutomaticRetry( + policy: .defaultExponential, + retryableURLErrorCodes: [.notConnectedToInternet] + ) + // The minimum value allowed between retries + static let minDelay: UInt = 2 + /// Provides the action taken when a retry is to be performed public enum ReconnectionPolicy: Hashable { - /// Exponential backoff with base/scale factor of 2, and a 300s max delay - public static let defaultExponential: ReconnectionPolicy = .exponential(base: 2, scale: 2, maxDelay: 300) - - /// Linear reconnect every 3 seconds - public static let defaultLinear: ReconnectionPolicy = .linear(delay: 3) + /// Exponential backoff with base/scale factor of 2, and a 150s max delay + public static let defaultExponential: ReconnectionPolicy = .exponential(minDelay: minDelay, maxDelay: 150) + /// Linear reconnect every 2 seconds + public static let defaultLinear: ReconnectionPolicy = .linear(delay: Double(minDelay)) - /// Attempt to reconnect immediately - case immediately /// Reconnect with an exponential backoff - case exponential(base: UInt, scale: Double, maxDelay: UInt) + case exponential(minDelay: UInt, maxDelay: UInt) /// Attempt to reconnect every X seconds case linear(delay: Double) func delay(for retryAttempt: Int) -> TimeInterval { + /// Generates a random interval that's added to the final value + /// Mitigates receiving 429 status code that's the result of too many requests in a given amount of time + let randomDelay = Double.random(in: 0...1) + switch self { - case .immediately: - return 0.0 - case let .exponential(base, scale, maxDelay): - return exponentialBackoffDelay(for: base, scale: scale, maxDelay: maxDelay, current: retryAttempt) + case let .exponential(minDelay, maxDelay): + return exponentialBackoffDelay(minDelay: minDelay, maxDelay: maxDelay, current: retryAttempt) + randomDelay case let .linear(delay): - return delay + return delay + randomDelay } } - func exponentialBackoffDelay(for base: UInt, scale: Double, maxDelay: UInt, current retryCount: Int) -> Double { - return min(pow(Double(base), Double(retryCount)) * scale, Double(maxDelay)) + func exponentialBackoffDelay(minDelay: UInt, maxDelay: UInt, current retryCount: Int) -> Double { + return min(Double(maxDelay), Double(minDelay) * pow(2, Double(retryCount))) } } @@ -80,43 +85,93 @@ public struct AutomaticRetry: RequestOperator, Hashable { public let retryableHTTPStatusCodes: Set /// Collection of returned `URLError.Code` objects that will trigger a retry public let retryableURLErrorCodes: Set + /// The list of endpoints excluded from retrying + public let excluded: [AutomaticRetry.Endpoint] public init( - retryLimit: UInt = 2, + retryLimit: UInt = 6, policy: ReconnectionPolicy = .defaultExponential, - retryableHTTPStatusCodes: Set = [500], - retryableURLErrorCodes: Set = AutomaticRetry.defaultRetryableURLErrorCodes + retryableHTTPStatusCodes: Set = [500, 429], + retryableURLErrorCodes: Set = AutomaticRetry.defaultRetryableURLErrorCodes, + excluded endpoints: [AutomaticRetry.Endpoint] = [ + .addChannelsToGroup, + .removeChannelsFromGroup, + .listChannelsForGroup, + .listChannelGroups, + .removeChannelGroup, + .publish, + .fire, + .signal, + .time, + .whereNow, + .hereNow, + .setPresence, + .getPresence, + .fetchMessageActions, + .addMessageAction, + .removeMessageAction, + .fetchMessageHistory, + .deleteMessageHistory, + .messageCounts, + .fetchMemberships, + .addMemberships, + .removeMemberships, + .fetchUsers, + .createUser, + .removeUser, + .fetchSpaces, + .createSpace, + .removeSpace, + .listPushChannels, + .managePushChannels, + .listAPNSPushChannels, + .manageAPNSDevices, + .listFiles, + .generateFileUploadURL, + .publishFile, + .removeFile + ] ) { switch policy { - case let .exponential(base, scale, max): - switch (true, true) { - case (base < 2, scale < 0): - PubNub.log.warn("The `exponential.base` must be a minimum of 2.") - PubNub.log.warn("The `exponential.scale` must be a positive value.") - self.policy = .exponential(base: 2, scale: 0, maxDelay: max) - case (base < 2, scale >= 0): - PubNub.log.warn("The `exponential.base` must be a minimum of 2.") - self.policy = .exponential(base: 2, scale: scale, maxDelay: max) - case (base >= 2, scale < 0): - PubNub.log.warn("The `exponential.scale` must be a positive value.") - self.policy = .exponential(base: base, scale: 0, maxDelay: max) - default: - self.policy = policy + case let .exponential(minDelay, maxDelay): + var finalMinDelay: UInt = minDelay + var finalMaxDelay: UInt = maxDelay + var finalRetryLimit: UInt = retryLimit + + if finalRetryLimit > 10 { + PubNub.log.warn("The `retryLimit` for exponential policy must be less than or equal 10") + finalRetryLimit = 10 + } + if finalMinDelay < Self.minDelay { + PubNub.log.warn("The `minDelay` must be a minimum of \(Self.minDelay)") + finalMinDelay = Self.minDelay } + if finalMinDelay > finalMaxDelay { + PubNub.log.warn("The `minDelay` \"\(minDelay)\" must be greater or equal `maxDelay` \"\(maxDelay)\"") + finalMaxDelay = minDelay + } + self.retryLimit = finalRetryLimit + self.policy = .exponential(minDelay: finalMinDelay, maxDelay: finalMaxDelay) + case let .linear(delay): - if delay < 0 { - PubNub.log.warn("The `linear.delay` must be a positive value.") - self.policy = .linear(delay: 0) - } else { - self.policy = policy + var finalRetryLimit = retryLimit + var finalDelay = delay + + if finalRetryLimit > 10 { + PubNub.log.warn("The `retryLimit` for linear policy must be less than or equal 10") + finalRetryLimit = 10 + } + if finalDelay < 0 || UInt(finalDelay) < Self.minDelay { + PubNub.log.warn("The `linear.delay` must be greater than or equal \(Self.minDelay).") + finalDelay = Double(Self.minDelay) } - case .immediately: - self.policy = policy + self.retryLimit = finalRetryLimit + self.policy = .linear(delay: finalDelay) } - - self.retryLimit = retryLimit + self.retryableHTTPStatusCodes = retryableHTTPStatusCodes self.retryableURLErrorCodes = retryableURLErrorCodes + self.excluded = endpoints } public func retry( @@ -129,20 +184,109 @@ public struct AutomaticRetry: RequestOperator, Hashable { completion(.failure(error)) return } - - return completion(.success(policy.delay(for: request.retryCount))) + + let urlResponse = request.urlResponse + let retryAfterValue = urlResponse?.allHeaderFields[Constant.retryAfterHeaderKey] + + if let retryAfterValue = retryAfterValue as? TimeInterval { + return completion(.success(retryAfterValue)) + } else { + return completion(.success(policy.delay(for: request.retryCount))) + } } func shouldRetry(response: HTTPURLResponse?, error: Error) -> Bool { - if let statusCode = response?.statusCode, retryableHTTPStatusCodes.contains(statusCode) { - return true + if let statusCode = response?.statusCode { + return retryableHTTPStatusCodes.contains(statusCode) } else if let errorCode = error.urlError?.code, retryableURLErrorCodes.contains(errorCode) { return true - } else if let errorCode = error.pubNubError?.underlying?.urlError?.code, - retryableURLErrorCodes.contains(errorCode) { + } else if let errorCode = error.pubNubError?.underlying?.urlError?.code, retryableURLErrorCodes.contains(errorCode) { return true } - return false } + + public subscript(endpoint: AutomaticRetry.Endpoint) -> RequestOperator? { + excluded.contains(endpoint) ? nil : self + } + + /// List of endpoints possible to retry + public enum Endpoint { + /// Adding a channel to the channel group + case addChannelsToGroup + /// Removing a channel from the channel group + case removeChannelsFromGroup + /// Listing all the channels of the channel group + case listChannelsForGroup + /// Listing all the channel groups + case listChannelGroups + /// Removing the channel group + case removeChannelGroup + /// Publishing a message to the channel + case publish + /// Publishing a message to PubNub Functions Event Handlers + case fire + /// Publish a message to PubNub Functions Event Handlers + case signal + /// Getting current `Timetoken` from System + case time + /// Subscribing to channels and/or channel groups + case subscribe + /// Informing Presence that a user is still active + case heartbeat + /// Obtaining information about the current list of channels a UUID is subscribed to + case whereNow + /// Obtaining information about the current state of a channel + case hereNow + /// Setting state dictionary pairs specific to a subscriber UUID + case setPresence + /// Getting state dictionary pairs from a specific subscriber uuid + case getPresence + /// Fetching a list of Message Actions for a channel + case fetchMessageActions + /// Add an Action to a Message + case addMessageAction + /// Removes a Message Action from a published Message + case removeMessageAction + /// Fetching historical messages of a channel + case fetchMessageHistory + /// Removing the messages from the history of a specific channel + case deleteMessageHistory + /// Returning the number of messages published for one or more channels + case messageCounts + /// Fetching all `PubNubMembership` linked to a specific `PubNubUser.id` + case fetchMemberships + /// Adding a `PubNubMembership` relationship between a `PubNubSpace` and one or more `PubNubUser` + case addMemberships + /// Removing the `PubNubMembership` relationship + case removeMemberships + /// Fetching one or all `PubNubUser` that exist on a keyset + case fetchUsers + /// Creating a new `PubNubUser` + case createUser + /// Removing a previously created `PubNubUser` (if it existed) + case removeUser + /// Fetching one or all `PubNubSpace` that exist on a keyset + case fetchSpaces + /// Creating a new `PubNubSpace` + case createSpace + /// Updating an existing`PubNubSpace` + case removeSpace + /// Getting channels on which push notification has been enabled using specified push token + case listPushChannels + /// Getting channels on which APNS push notification has been enabled using specified device token and topic + case listAPNSPushChannels + /// Adding/removing push notification functionality on provided set of channels + case managePushChannels + /// Adding/removing APNS push notification functionality on provided set of channels for a given topic + case manageAPNSDevices + /// Retrieve list of files uploaded to a channel + case listFiles + /// Generating a File Upload URL + case generateFileUploadURL + /// Publishing the `PubNubFile` representing the uploaded File + case publishFile + /// Removing file from specified `Channel` + case removeFile + } } diff --git a/Sources/PubNub/PubNub.swift b/Sources/PubNub/PubNub.swift index 9f05505e..9c8d3cf7 100644 --- a/Sources/PubNub/PubNub.swift +++ b/Sources/PubNub/PubNub.swift @@ -94,6 +94,7 @@ public class PubNub { func route( _ router: HTTPRouter, + requestOperator: RequestOperator? = nil, responseDecoder: Decoder, custom requestConfig: RequestConfiguration, completion: @escaping (Result, Error>) -> Void @@ -101,6 +102,7 @@ public class PubNub { (requestConfig.customSession ?? networkSession) .route( router, + requestOperator: requestOperator, responseDecoder: responseDecoder, responseQueue: requestConfig.responseQueue, completion: completion @@ -199,9 +201,12 @@ public extension PubNub { custom requestConfig: RequestConfiguration = RequestConfiguration(), completion: ((Result) -> Void)? ) { - route(TimeRouter(.time, configuration: requestConfig.customConfiguration ?? configuration), - responseDecoder: TimeResponseDecoder(), - custom: requestConfig) { result in + route( + TimeRouter(.time, configuration: requestConfig.customConfiguration ?? configuration), + requestOperator: configuration.automaticRetry?[.time], + responseDecoder: TimeResponseDecoder(), + custom: requestConfig + ) { result in completion?(result.map { $0.payload.timetoken }) } } @@ -242,27 +247,34 @@ public extension PubNub { let router: PublishRouter if shouldCompress { router = PublishRouter( - .compressedPublish(message: message.codableValue, - channel: channel, - shouldStore: shouldStore, - ttl: storeTTL, - meta: meta?.codableValue), + .compressedPublish( + message: message.codableValue, + channel: channel, + shouldStore: shouldStore, + ttl: storeTTL, + meta: meta?.codableValue + ), configuration: requestConfig.customConfiguration ?? configuration ) } else { router = PublishRouter( - .publish(message: message.codableValue, - channel: channel, - shouldStore: shouldStore, - ttl: storeTTL, - meta: meta?.codableValue), + .publish( + message: message.codableValue, + channel: channel, + shouldStore: shouldStore, + ttl: storeTTL, + meta: meta?.codableValue + ), configuration: requestConfig.customConfiguration ?? configuration ) } - route(router, - responseDecoder: PublishResponseDecoder(), - custom: requestConfig) { result in + route( + router, + requestOperator: configuration.automaticRetry?[.publish], + responseDecoder: PublishResponseDecoder(), + custom: requestConfig + ) { result in completion?(result.map { $0.payload.timetoken }) } } @@ -294,10 +306,15 @@ public extension PubNub { custom requestConfig: RequestConfiguration = RequestConfiguration(), completion: ((Result) -> Void)? ) { - route(PublishRouter(.fire(message: message.codableValue, channel: channel, meta: meta?.codableValue), - configuration: requestConfig.customConfiguration ?? configuration), - responseDecoder: PublishResponseDecoder(), - custom: requestConfig) { result in + route( + PublishRouter( + .fire(message: message.codableValue, channel: channel, meta: meta?.codableValue), + configuration: requestConfig.customConfiguration ?? configuration + ), + requestOperator: configuration.automaticRetry?[.fire], + responseDecoder: PublishResponseDecoder(), + custom: requestConfig + ) { result in completion?(result.map { $0.payload.timetoken }) } } @@ -318,10 +335,15 @@ public extension PubNub { custom requestConfig: RequestConfiguration = RequestConfiguration(), completion: ((Result) -> Void)? ) { - route(PublishRouter(.signal(message: message.codableValue, channel: channel), - configuration: requestConfig.customConfiguration ?? configuration), - responseDecoder: PublishResponseDecoder(), - custom: requestConfig) { result in + route( + PublishRouter( + .signal(message: message.codableValue, channel: channel), + configuration: requestConfig.customConfiguration ?? configuration + ), + requestOperator: configuration.automaticRetry?[.signal], + responseDecoder: PublishResponseDecoder(), + custom: requestConfig + ) { result in completion?(result.map { $0.payload.timetoken }) } } @@ -348,10 +370,12 @@ public extension PubNub { ) { subscription.filterExpression = filterOverride - subscription.subscribe(to: channels, - and: channelGroups, - at: SubscribeCursor(timetoken: timetoken), - withPresence: withPresence) + subscription.subscribe( + to: channels, + and: channelGroups, + at: SubscribeCursor(timetoken: timetoken), + withPresence: withPresence + ) } /// Unsubscribe from channels and/or channel groups @@ -445,9 +469,12 @@ public extension PubNub { configuration: requestConfig.customConfiguration ?? configuration ) - route(router, - responseDecoder: PresenceResponseDecoder>(), - custom: requestConfig) { result in + route( + router, + requestOperator: configuration.automaticRetry?[.setPresence], + responseDecoder: PresenceResponseDecoder>(), + custom: requestConfig + ) { result in completion?(result.map { $0.payload.payload }) } } @@ -472,9 +499,12 @@ public extension PubNub { configuration: requestConfig.customConfiguration ?? configuration ) - route(router, - responseDecoder: GetPresenceStateResponseDecoder(), - custom: requestConfig) { result in + route( + router, + requestOperator: configuration.automaticRetry?[.getPresence], + responseDecoder: GetPresenceStateResponseDecoder(), + custom: requestConfig + ) { result in completion?(result.map { (uuid: $0.payload.uuid, stateByChannel: $0.payload.channels) }) } } @@ -504,8 +534,10 @@ public extension PubNub { ) { let router: PresenceRouter if channels.isEmpty, groups.isEmpty { - router = PresenceRouter(.hereNowGlobal(includeUUIDs: includeUUIDs, includeState: includeState), - configuration: requestConfig.customConfiguration ?? configuration) + router = PresenceRouter( + .hereNowGlobal(includeUUIDs: includeUUIDs, includeState: includeState), + configuration: requestConfig.customConfiguration ?? configuration + ) } else { router = PresenceRouter( .hereNow(channels: channels, groups: groups, includeUUIDs: includeUUIDs, includeState: includeState), @@ -515,9 +547,12 @@ public extension PubNub { let decoder = HereNowResponseDecoder(channels: channels, groups: groups) - route(router, - responseDecoder: decoder, - custom: requestConfig) { result in + route( + router, + requestOperator: configuration.automaticRetry?[.hereNow], + responseDecoder: decoder, + custom: requestConfig + ) { result in completion?(result.map { $0.payload.asPubNubPresenceBase }) } } @@ -534,9 +569,12 @@ public extension PubNub { custom requestConfig: RequestConfiguration = RequestConfiguration(), completion: ((Result<[String: [String]], Error>) -> Void)? ) { - route(PresenceRouter(.whereNow(uuid: uuid), configuration: requestConfig.customConfiguration ?? configuration), - responseDecoder: PresenceResponseDecoder>(), - custom: requestConfig) { result in + route( + PresenceRouter(.whereNow(uuid: uuid), configuration: requestConfig.customConfiguration ?? configuration), + requestOperator: configuration.automaticRetry?[.whereNow], + responseDecoder: PresenceResponseDecoder>(), + custom: requestConfig + ) { result in completion?(result.map { [uuid: $0.payload.payload.channels] }) } } @@ -555,9 +593,12 @@ public extension PubNub { custom requestConfig: RequestConfiguration = RequestConfiguration(), completion: ((Result<[String], Error>) -> Void)? ) { - route(ChannelGroupsRouter(.channelGroups, configuration: requestConfig.customConfiguration ?? configuration), - responseDecoder: ChannelGroupResponseDecoder(), - custom: requestConfig) { result in + route( + ChannelGroupsRouter(.channelGroups, configuration: requestConfig.customConfiguration ?? configuration), + requestOperator: configuration.automaticRetry?[.listChannelGroups], + responseDecoder: ChannelGroupResponseDecoder(), + custom: requestConfig + ) { result in completion?(result.map { $0.payload.payload.groups }) } } @@ -580,6 +621,7 @@ public extension PubNub { .deleteGroup(group: channelGroup), configuration: requestConfig.customConfiguration ?? configuration ), + requestOperator: configuration.automaticRetry?[.removeChannelGroup], responseDecoder: GenericServiceResponseDecoder(), custom: requestConfig ) { result in @@ -604,6 +646,7 @@ public extension PubNub { .channelsForGroup(group: group), configuration: requestConfig.customConfiguration ?? configuration ), + requestOperator: configuration.automaticRetry?[.listChannelsForGroup], responseDecoder: ChannelGroupResponseDecoder(), custom: requestConfig ) { result in @@ -630,6 +673,7 @@ public extension PubNub { .addChannelsToGroup(group: group, channels: channels), configuration: requestConfig.customConfiguration ?? configuration ), + requestOperator: configuration.automaticRetry?[.addChannelsToGroup], responseDecoder: GenericServiceResponseDecoder(), custom: requestConfig ) { result in @@ -637,7 +681,7 @@ public extension PubNub { } } - /// Rremoves the channels from the channel group. + /// Removes the channels from the channel group. /// - Parameters: /// - channels: List of channels to remove from the group /// - from: The Channel Group to remove the list of channels from @@ -656,6 +700,7 @@ public extension PubNub { .removeChannelsForGroup(group: group, channels: channels), configuration: requestConfig.customConfiguration ?? configuration ), + requestOperator: configuration.automaticRetry?[.removeChannelsFromGroup], responseDecoder: GenericServiceResponseDecoder(), custom: requestConfig ) { result in @@ -686,6 +731,7 @@ public extension PubNub { .listPushChannels(pushToken: deviceToken, pushType: pushType), configuration: requestConfig.customConfiguration ?? configuration ), + requestOperator: configuration.automaticRetry?[.listPushChannels], responseDecoder: RegisteredPushChannelsResponseDecoder(), custom: requestConfig ) { result in @@ -716,9 +762,12 @@ public extension PubNub { configuration: requestConfig.customConfiguration ?? configuration ) - route(router, - responseDecoder: ModifyPushResponseDecoder(), - custom: requestConfig) { result in + route( + router, + requestOperator: configuration.automaticRetry?[.managePushChannels], + responseDecoder: ModifyPushResponseDecoder(), + custom: requestConfig + ) { result in completion?(result.map { (added: $0.payload.added, removed: $0.payload.removed) }) } } @@ -788,6 +837,7 @@ public extension PubNub { .removeAllPushChannels(pushToken: deviceToken, pushType: pushType), configuration: requestConfig.customConfiguration ?? configuration ), + requestOperator: configuration.automaticRetry?[.managePushChannels], responseDecoder: ModifyPushResponseDecoder(), custom: requestConfig ) { result in @@ -813,11 +863,10 @@ public extension PubNub { ) { route( PushRouter( - .manageAPNS( - pushToken: deviceToken, environment: environment, topic: topic, adding: [], removing: [] - ), + .manageAPNS(pushToken: deviceToken, environment: environment, topic: topic, adding: [], removing: []), configuration: requestConfig.customConfiguration ?? configuration ), + requestOperator: configuration.automaticRetry?[.listAPNSPushChannels], responseDecoder: RegisteredPushChannelsResponseDecoder(), custom: requestConfig ) { result in @@ -846,20 +895,28 @@ public extension PubNub { completion: ((Result<(added: [String], removed: [String]), Error>) -> Void)? ) { let router = PushRouter( - .manageAPNS(pushToken: token, environment: environment, - topic: topic, adding: additions, removing: removals), + .manageAPNS( + pushToken: token, environment: environment, topic: topic, + adding: additions, removing: removals + ), configuration: requestConfig.customConfiguration ?? configuration ) if removals.isEmpty, additions.isEmpty { completion?( - .failure(PubNubError(.missingRequiredParameter, - router: router, - additional: [ErrorDescription.missingChannelsAnyGroups]))) + .failure(PubNubError( + .missingRequiredParameter, + router: router, + additional: [ErrorDescription.missingChannelsAnyGroups] + )) + ) } else { - route(router, - responseDecoder: ModifyPushResponseDecoder(), - custom: requestConfig) { result in + route( + router, + requestOperator: configuration.automaticRetry?[.manageAPNSDevices], + responseDecoder: ModifyPushResponseDecoder(), + custom: requestConfig + ) { result in completion?(result.map { (added: $0.payload.added, removed: $0.payload.removed) }) } } @@ -931,10 +988,15 @@ public extension PubNub { custom requestConfig: RequestConfiguration = RequestConfiguration(), completion: ((Result) -> Void)? ) { - route(PushRouter(.removeAllAPNS(pushToken: deviceToken, environment: environment, topic: topic), - configuration: requestConfig.customConfiguration ?? configuration), - responseDecoder: ModifyPushResponseDecoder(), - custom: requestConfig) { result in + route( + PushRouter( + .removeAllAPNS(pushToken: deviceToken, environment: environment, topic: topic), + configuration: requestConfig.customConfiguration ?? configuration + ), + requestOperator: configuration.automaticRetry?[.manageAPNSDevices], + responseDecoder: ModifyPushResponseDecoder(), + custom: requestConfig + ) { result in completion?(result.map { _ in () }) } } @@ -1009,6 +1071,7 @@ public extension PubNub { route( router, + requestOperator: configuration.automaticRetry?[.fetchMessageHistory], responseDecoder: MessageHistoryResponseDecoder(), custom: requestConfig ) { result in @@ -1042,6 +1105,7 @@ public extension PubNub { .delete(channel: channel, start: start, end: end), configuration: requestConfig.customConfiguration ?? configuration ), + requestOperator: configuration.automaticRetry?[.deleteMessageHistory], responseDecoder: GenericServiceResponseDecoder(), custom: requestConfig ) { result in @@ -1066,9 +1130,12 @@ public extension PubNub { configuration: requestConfig.customConfiguration ?? configuration ) - route(router, - responseDecoder: MessageCountsResponseDecoder(), - custom: requestConfig) { result in + route( + router, + requestOperator: configuration.automaticRetry?[.messageCounts], + responseDecoder: MessageCountsResponseDecoder(), + custom: requestConfig + ) { result in completion?(result.map { $0.payload.channels }) } } @@ -1092,9 +1159,12 @@ public extension PubNub { configuration: requestConfig.customConfiguration ?? configuration ) - route(router, - responseDecoder: MessageCountsResponseDecoder(), - custom: requestConfig) { result in + route( + router, + requestOperator: configuration.automaticRetry?[.messageCounts], + responseDecoder: MessageCountsResponseDecoder(), + custom: requestConfig + ) { result in completion?(result.map { $0.payload.channels }) } } @@ -1122,6 +1192,7 @@ public extension PubNub { .fetch(channel: channel, start: page?.start, end: page?.end, limit: page?.limit), configuration: requestConfig.customConfiguration ?? configuration ), + requestOperator: configuration.automaticRetry?[.fetchMessageActions], responseDecoder: MessageActionsResponseDecoder(), custom: requestConfig ) { result in @@ -1162,12 +1233,14 @@ public extension PubNub { configuration: requestConfig.customConfiguration ?? configuration ) - route(router, - responseDecoder: MessageActionResponseDecoder(), - custom: requestConfig) { result in + route( + router, + requestOperator: configuration.automaticRetry?[.addMessageAction], + responseDecoder: MessageActionResponseDecoder(), + custom: requestConfig + ) { result in switch result { case let .success(response): - if let errorPayload = response.payload.error { let error = PubNubError( reason: errorPayload.message.pubnubReason, router: router, @@ -1205,9 +1278,12 @@ public extension PubNub { configuration: requestConfig.customConfiguration ?? configuration ) - route(router, - responseDecoder: DeleteResponseDecoder(), - custom: requestConfig) { result in + route( + router, + requestOperator: configuration.automaticRetry?[.removeMessageAction], + responseDecoder: DeleteResponseDecoder(), + custom: requestConfig + ) { result in switch result { case let .success(response): if let errorPayload = response.payload.error { diff --git a/Sources/PubNub/Subscription/SubscriptionSession+Presence.swift b/Sources/PubNub/Subscription/SubscriptionSession+Presence.swift index 8a752602..7128b8fc 100644 --- a/Sources/PubNub/Subscription/SubscriptionSession+Presence.swift +++ b/Sources/PubNub/Subscription/SubscriptionSession+Presence.swift @@ -55,7 +55,7 @@ extension SubscriptionSession { ) nonSubscribeSession - .request(with: router, requestOperator: configuration.automaticRetry) + .request(with: router, requestOperator: configuration.automaticRetry?[.heartbeat]) .validate() .response(on: .main, decoder: GenericServiceResponseDecoder()) { [weak self] result in switch result { diff --git a/Sources/PubNub/Subscription/SubscriptionSession.swift b/Sources/PubNub/Subscription/SubscriptionSession.swift index c8164aa6..ff715908 100644 --- a/Sources/PubNub/Subscription/SubscriptionSession.swift +++ b/Sources/PubNub/Subscription/SubscriptionSession.swift @@ -208,11 +208,13 @@ public class SubscriptionSession { stopSubscribeLoop(.longPollingRestart) // Will compre this in the error response to see if we need to restart - let nextSubscribe = longPollingSession - .request(with: router, requestOperator: configuration.automaticRetry) + let nextSubscribe = longPollingSession.request( + with: router, + requestOperator: configuration.automaticRetry?[.subscribe] + ) let currentSubscribeID = nextSubscribe.requestID + request = nextSubscribe - request? .validate() .response(on: .main, decoder: SubscribeDecoder()) { [weak self] result in diff --git a/Tests/PubNubTests/Networking/Operators/AutomaticRetryTests.swift b/Tests/PubNubTests/Networking/Operators/AutomaticRetryTests.swift index d7b474a4..b83da655 100644 --- a/Tests/PubNubTests/Networking/Operators/AutomaticRetryTests.swift +++ b/Tests/PubNubTests/Networking/Operators/AutomaticRetryTests.swift @@ -18,7 +18,7 @@ class AutomaticRetryTests: XCTestCase { func testReconnectionPolicy_DefaultLinearPolicy() { switch defaultLinearPolicy { case let .linear(delay): - XCTAssertEqual(delay, 3) + XCTAssertEqual(delay, 2) default: XCTFail("Default Linear Policy should only match to linear case") } @@ -26,10 +26,9 @@ class AutomaticRetryTests: XCTestCase { func testReconnectionPolicy_DefaultExponentialPolicy() { switch defaultExpoentialPolicy { - case let .exponential(base, scale, max): - XCTAssertEqual(base, 2) - XCTAssertEqual(scale, 2) - XCTAssertEqual(max, 300) + case let .exponential(minDelay, maxDelay): + XCTAssertEqual(minDelay, 2) + XCTAssertEqual(maxDelay, 150) default: XCTFail("Default Exponential Policy should only match to linear case") } @@ -39,77 +38,101 @@ class AutomaticRetryTests: XCTestCase { func testEquatable_Init_Valid_() { let testPolicy = AutomaticRetry.default - let policy = AutomaticRetry() + let automaticRetry = AutomaticRetry() - XCTAssertEqual(testPolicy, policy) + XCTAssertEqual(testPolicy, automaticRetry) } - func testEquatable_Init_Exponential_InvalidBase() { - let invalidBasePolicy = AutomaticRetry.ReconnectionPolicy.exponential(base: 0, scale: 3.0, maxDelay: 1) - let validBasePolicy = AutomaticRetry.ReconnectionPolicy.exponential(base: 2, scale: 3.0, maxDelay: 1) - let testPolicy = AutomaticRetry(retryLimit: 2, - policy: invalidBasePolicy, - retryableHTTPStatusCodes: [], - retryableURLErrorCodes: []) - - XCTAssertNotEqual(testPolicy.policy, invalidBasePolicy) - XCTAssertEqual(testPolicy.policy, validBasePolicy) + func testEquatable_Init_Exponential_InvalidMinDelay() { + let invalidBasePolicy = AutomaticRetry.ReconnectionPolicy.exponential(minDelay: 0, maxDelay: 30) + let validBasePolicy = AutomaticRetry.ReconnectionPolicy.exponential(minDelay: 2, maxDelay: 30) + let automaticRetry = AutomaticRetry( + retryLimit: 2, + policy: invalidBasePolicy, + retryableHTTPStatusCodes: [], + retryableURLErrorCodes: [] + ) + + XCTAssertNotEqual(automaticRetry.policy, invalidBasePolicy) + XCTAssertEqual(automaticRetry.policy, validBasePolicy) } - - func testEquatable_Init_Exponential_InvalidScale() { - let invalidBasePolicy = AutomaticRetry.ReconnectionPolicy.exponential(base: 2, scale: -1.0, maxDelay: 1) - let validBasePolicy = AutomaticRetry.ReconnectionPolicy.exponential(base: 2, scale: 0.0, maxDelay: 1) - let testPolicy = AutomaticRetry(retryLimit: 2, - policy: invalidBasePolicy, - retryableHTTPStatusCodes: [], - retryableURLErrorCodes: []) - - XCTAssertNotEqual(testPolicy.policy, invalidBasePolicy) - XCTAssertEqual(testPolicy.policy, validBasePolicy) + + func testEquatable_Init_Exponential_MinDelayGreaterThanMaxDelay() { + let invalidBasePolicy = AutomaticRetry.ReconnectionPolicy.exponential(minDelay: 10, maxDelay: 5) + let validBasePolicy = AutomaticRetry.ReconnectionPolicy.exponential(minDelay: 10, maxDelay: 10) + let automaticRetry = AutomaticRetry( + retryLimit: 2, + policy: invalidBasePolicy, + retryableHTTPStatusCodes: [], + retryableURLErrorCodes: [] + ) + + XCTAssertNotEqual(automaticRetry.policy, invalidBasePolicy) + XCTAssertEqual(automaticRetry.policy, validBasePolicy) } - - func testEquatable_Init_Exponential_InvalidBaseAndScale() { - let invalidBasePolicy = AutomaticRetry.ReconnectionPolicy.exponential(base: 0, scale: -1.0, maxDelay: 1) - let validBasePolicy = AutomaticRetry.ReconnectionPolicy.exponential(base: 2, scale: 0.0, maxDelay: 1) - let testPolicy = AutomaticRetry(retryLimit: 2, - policy: invalidBasePolicy, - retryableHTTPStatusCodes: [], - retryableURLErrorCodes: []) - - XCTAssertNotEqual(testPolicy.policy, invalidBasePolicy) - XCTAssertEqual(testPolicy.policy, validBasePolicy) + + func testEquatable_Init_Exponential_TooHighRetryLimit() { + let policy = AutomaticRetry.ReconnectionPolicy.exponential(minDelay: 5, maxDelay: 60) + let automaticRetry = AutomaticRetry( + retryLimit: 12, + policy: policy, + retryableHTTPStatusCodes: [], + retryableURLErrorCodes: [] + ) + + XCTAssertEqual(automaticRetry.policy, policy) + XCTAssertEqual(automaticRetry.retryLimit, 10) } func testEquatable_Init_Linear_InvalidDelay() { let invalidBasePolicy = AutomaticRetry.ReconnectionPolicy.linear(delay: -1.0) - let validBasePolicy = AutomaticRetry.ReconnectionPolicy.linear(delay: 0.0) - let testPolicy = AutomaticRetry(retryLimit: 2, - policy: invalidBasePolicy, - retryableHTTPStatusCodes: [], - retryableURLErrorCodes: []) - - XCTAssertNotEqual(testPolicy.policy, invalidBasePolicy) - XCTAssertEqual(testPolicy.policy, validBasePolicy) + let validBasePolicy = AutomaticRetry.ReconnectionPolicy.linear(delay: 2.0) + let automaticRetry = AutomaticRetry( + retryLimit: 2, + policy: invalidBasePolicy, + retryableHTTPStatusCodes: [], + retryableURLErrorCodes: [] + ) + + XCTAssertNotEqual(automaticRetry.policy, invalidBasePolicy) + XCTAssertEqual(automaticRetry.policy, validBasePolicy) + } + + func testEquatable_Init_Linear_TooHighRetryLimit() { + let policy = AutomaticRetry.ReconnectionPolicy.linear(delay: 3.0) + let automaticRetry = AutomaticRetry( + retryLimit: 12, + policy: policy, + retryableHTTPStatusCodes: [], + retryableURLErrorCodes: [] + ) + + XCTAssertEqual(automaticRetry.policy, policy) + XCTAssertEqual(automaticRetry.retryLimit, 10) } func testEquatable_Init_Linear_Valid() { - let validLinearPolicy = AutomaticRetry.ReconnectionPolicy.linear(delay: 1.0) - let testPolicy = AutomaticRetry(retryLimit: 2, - policy: validLinearPolicy, - retryableHTTPStatusCodes: [], - retryableURLErrorCodes: []) - - XCTAssertEqual(testPolicy.policy, validLinearPolicy) + let validLinearPolicy = AutomaticRetry.ReconnectionPolicy.linear(delay: 3.0) + let automaticRetry = AutomaticRetry( + retryLimit: 2, + policy: validLinearPolicy, + retryableHTTPStatusCodes: [], + retryableURLErrorCodes: [] + ) + + XCTAssertEqual(automaticRetry.policy, validLinearPolicy) } func testEquatable_Init_Other() { - let immediateasePolicy = AutomaticRetry.ReconnectionPolicy.immediately - let testPolicy = AutomaticRetry(retryLimit: 2, - policy: immediateasePolicy, - retryableHTTPStatusCodes: [], - retryableURLErrorCodes: []) - - XCTAssertEqual(testPolicy.policy, immediateasePolicy) + let linearPolicy = AutomaticRetry.ReconnectionPolicy.linear(delay: 3.0) + let automaticRetry = AutomaticRetry( + retryLimit: 2, + policy: linearPolicy, + retryableHTTPStatusCodes: [], + retryableURLErrorCodes: [] + ) + + XCTAssertEqual(automaticRetry.policy, linearPolicy) } // MARK: - retry(:session:for:dueTo:completion:) @@ -136,71 +159,75 @@ class AutomaticRetryTests: XCTestCase { } let testStatusCode = 500 - let testPolicy = AutomaticRetry(retryLimit: 2, - policy: .immediately, - retryableHTTPStatusCodes: [testStatusCode], - retryableURLErrorCodes: []) - let testResponse = HTTPURLResponse(url: url, - statusCode: testStatusCode, - httpVersion: nil, - headerFields: [:]) - - XCTAssertTrue(testPolicy.shouldRetry(response: testResponse, - error: PubNubError(.unknown))) + let testPolicy = AutomaticRetry( + retryLimit: 2, + policy: .linear(delay: 3.0), + retryableHTTPStatusCodes: [testStatusCode], + retryableURLErrorCodes: [] + ) + let testResponse = HTTPURLResponse( + url: url, + statusCode: testStatusCode, + httpVersion: nil, + headerFields: [:] + ) + + XCTAssertTrue(testPolicy.shouldRetry(response: testResponse, error: PubNubError(.unknown))) } func testShouldRetry_True_ErrorCodeMatch() { let testURLErrorCode = URLError.Code.timedOut let testError = URLError(testURLErrorCode) - let testPolicy = AutomaticRetry(retryLimit: 2, - policy: .immediately, - retryableHTTPStatusCodes: [], - retryableURLErrorCodes: [testURLErrorCode]) - - XCTAssertTrue(testPolicy.shouldRetry(response: nil, - error: testError)) + let testPolicy = AutomaticRetry( + retryLimit: 2, + policy: .linear(delay: 3.0), + retryableHTTPStatusCodes: [], + retryableURLErrorCodes: [testURLErrorCode] + ) + + XCTAssertTrue(testPolicy.shouldRetry(response: nil, error: testError)) } func testShouldRetry_False() { let testError = URLError(.timedOut) - let testPolicy = AutomaticRetry(retryLimit: 2, - policy: .immediately, - retryableHTTPStatusCodes: [], - retryableURLErrorCodes: []) - - XCTAssertFalse(testPolicy.shouldRetry(response: nil, - error: testError)) + let testPolicy = AutomaticRetry( + retryLimit: 2, + policy: .linear(delay: 3.0), + retryableHTTPStatusCodes: [], + retryableURLErrorCodes: [] + ) + + XCTAssertFalse(testPolicy.shouldRetry(response: nil, error: testError)) } // MARK: - exponentialBackoffDelay(for:scale:current:) func testExponentialBackoffDelay_DefaultScale() { let maxRetryCount = 5 - let scale = 2.0 - let base: UInt = 2 let maxDelay = UInt.max - - let delayForRetry = [4.0, 8.0, 16.0, 32.0, 64.0] - - for count in 1 ... maxRetryCount { - XCTAssertEqual(AutomaticRetry.ReconnectionPolicy - .exponential(base: base, scale: scale, maxDelay: maxDelay).delay(for: count), - delayForRetry[count - 1]) + // Usage of Range due to random delay (0...1) that's always added to the final value + let delayForRetry: [ClosedRange] = [2.0...3.0, 4.0...5.0, 8.0...9.0, 16.0...17.0, 32.0...33.0] + + for count in 0..] = [2.0...3.0, 3.0...4.0, 3.0...4.0, 3.0...4.0, 3.0...4.0] let maxRetryCount = 5 - let scale = 2.0 - let base: UInt = 2 - let maxDelay: UInt = 0 - - let delayForRetry = [0.0, 0.0, 0.0, 0.0, 0.0] - for count in 1 ... maxRetryCount { - XCTAssertEqual(AutomaticRetry.ReconnectionPolicy - .exponential(base: base, scale: scale, maxDelay: maxDelay).delay(for: count), - delayForRetry[count - 1]) + for count in 0..