mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-09 22:35:56 +05:45
142 lines
5.3 KiB
Swift
142 lines
5.3 KiB
Swift
//
|
|
// HTTPRequestTimeoutManager.swift
|
|
// RevenueCat
|
|
//
|
|
// Created by Rick van der Linden on 06/11/2025.
|
|
// Copyright © 2025 RevenueCat, Inc. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
protocol HTTPRequestTimeoutManagerType: AnyObject {
|
|
|
|
/// Determines the timeout to be used by an HTTP request attempt.
|
|
///
|
|
/// - Parameters:
|
|
/// - host: The resolved host string of the attempt, used to look up the per-host fail-fast memory.
|
|
/// - isFallbackHostRequest: Whether this attempt targets a fallback host.
|
|
/// - endpointSupportsFallbackURLs: Whether the endpoint has fallback-URL support.
|
|
/// - isProxied: Whether a proxy URL is set.
|
|
/// - Returns: The timeout interval in seconds.
|
|
func timeout(host: String?,
|
|
isFallbackHostRequest: Bool,
|
|
endpointSupportsFallbackURLs: Bool,
|
|
isProxied: Bool) -> TimeInterval
|
|
|
|
/// Updates the internal state in response to the result of an HTTP request attempt.
|
|
///
|
|
/// - Parameters:
|
|
/// - host: The resolved host string of the attempt.
|
|
/// - result: The result of the HTTP request.
|
|
func recordRequestResult(host: String?, _ result: HTTPRequestTimeoutManager.RequestResult)
|
|
}
|
|
|
|
class HTTPRequestTimeoutManager: HTTPRequestTimeoutManagerType {
|
|
|
|
enum RequestResult {
|
|
|
|
/// Request succeeded on the main source.
|
|
case successOnMainBackend
|
|
|
|
/// Request timed out on the main source.
|
|
case mainSourceTimedOut
|
|
|
|
/// Any other result (fallback-host request, non-timeout errors, etc.)
|
|
case other
|
|
}
|
|
|
|
/// Timeout tiers, in seconds.
|
|
enum Timeout {
|
|
|
|
/// Main-source request to an endpoint with no fallback-URL support.
|
|
static let mainSourceNoFallback: TimeInterval = 15
|
|
|
|
/// Main-source request to an endpoint with no fallback-URL support, when the source recently timed out.
|
|
static let mainSourceNoFallbackReduced: TimeInterval = 5
|
|
|
|
/// Main-source request to an endpoint with fallback-URL support.
|
|
static let mainSourceSupportingFallback: TimeInterval = 5
|
|
|
|
/// Main-source request to an endpoint with fallback-URL support, when the source recently timed out.
|
|
static let mainSourceSupportingFallbackReduced: TimeInterval = 2
|
|
|
|
/// Fallback-host and proxied requests (flat, never reduced).
|
|
static let flat: TimeInterval = 30
|
|
}
|
|
|
|
// The amount of time after which a per-host timeout entry expires.
|
|
private static let timeoutResetInterval: TimeInterval = 600 // 10 minutes
|
|
|
|
// The last time a timeout was recorded, keyed by resolved host string. Guarded by `lock`.
|
|
private var lastTimeoutByHost: [String: Date] = [:]
|
|
|
|
// When the developer sets a custom `networkTimeout`, it replaces the built-in base/flat tiers.
|
|
private let networkTimeout: NetworkTimeout
|
|
|
|
private let dateProvider: DateProvider
|
|
private let lock = Lock()
|
|
|
|
init(
|
|
networkTimeout: NetworkTimeout,
|
|
dateProvider: DateProvider = .init()
|
|
) {
|
|
self.networkTimeout = networkTimeout
|
|
self.dateProvider = dateProvider
|
|
}
|
|
|
|
func timeout(host: String?,
|
|
isFallbackHostRequest: Bool,
|
|
endpointSupportsFallbackURLs: Bool,
|
|
isProxied: Bool) -> TimeInterval {
|
|
// Fallback-host and proxied requests use a flat timeout and never consult the per-host memory.
|
|
guard !isFallbackHostRequest, !isProxied else {
|
|
return self.baseTimeout(default: Timeout.flat)
|
|
}
|
|
|
|
let sourceRecentlyTimedOut = host.map { self.hasRecentTimeout(forHost: $0) } ?? false
|
|
|
|
// Base tiers honor a custom `networkTimeout`; the reduced fail-fast tiers stay fixed.
|
|
switch (endpointSupportsFallbackURLs, sourceRecentlyTimedOut) {
|
|
case (true, false): return self.baseTimeout(default: Timeout.mainSourceSupportingFallback)
|
|
case (true, true): return Timeout.mainSourceSupportingFallbackReduced
|
|
case (false, false): return self.baseTimeout(default: Timeout.mainSourceNoFallback)
|
|
case (false, true): return Timeout.mainSourceNoFallbackReduced
|
|
}
|
|
}
|
|
|
|
/// The base/flat timeout to use: the developer's custom value when set, otherwise the built-in tier.
|
|
private func baseTimeout(default defaultValue: TimeInterval) -> TimeInterval {
|
|
switch self.networkTimeout {
|
|
case .default: return defaultValue
|
|
case let .custom(value): return value
|
|
}
|
|
}
|
|
|
|
func recordRequestResult(host: String?, _ result: RequestResult) {
|
|
guard let host else { return }
|
|
|
|
switch result {
|
|
case .successOnMainBackend:
|
|
self.lock.perform { _ = self.lastTimeoutByHost.removeValue(forKey: host) }
|
|
case .mainSourceTimedOut:
|
|
self.lock.perform { self.lastTimeoutByHost[host] = self.dateProvider.now() }
|
|
case .other:
|
|
break
|
|
}
|
|
}
|
|
|
|
/// Whether `host` has a non-expired timeout entry. Prunes the entry if it has expired.
|
|
private func hasRecentTimeout(forHost host: String) -> Bool {
|
|
return self.lock.perform {
|
|
guard let lastTimeout = self.lastTimeoutByHost[host] else { return false }
|
|
|
|
if self.dateProvider.now().timeIntervalSince(lastTimeout) >= Self.timeoutResetInterval {
|
|
self.lastTimeoutByHost.removeValue(forKey: host)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
}
|
|
}
|