Typhoon is a modern, lightweight Swift framework that provides elegant and robust retry policies for asynchronous operations. Built with Swift's async/await concurrency model, it helps you handle transient failures gracefully with configurable retry strategies.
✨ Multiple Retry Strategies - Constant, exponential, and exponential with jitter
⚡ Async/Await Native - Built for modern Swift concurrency
🎯 Type-Safe - Leverages Swift's type system for compile-time safety
🔧 Configurable - Flexible retry parameters for any use case
📱 Cross-Platform - Works on iOS, macOS, tvOS, watchOS, and visionOS
⚡ Lightweight - Minimal footprint with zero dependencies
🧪 Well Tested - Comprehensive test coverage
- Requirements
- Installation
- Quick Start
- Usage
- Common Use Cases
- Communication
- Documentation
- Contributing
- Author
- License
| Platform | Minimum Version |
|---|---|
| iOS | 13.0+ |
| macOS | 10.15+ |
| tvOS | 13.0+ |
| watchOS | 6.0+ |
| visionOS | 1.0+ |
| Xcode | 15.3+ |
| Swift | 5.10+ |
Add the following dependency to your Package.swift:
dependencies: [
.package(url: "https://github.com/space-code/typhoon.git", from: "1.4.0")
]Or add it through Xcode:
- File > Add Package Dependencies
- Enter package URL:
https://github.com/space-code/typhoon.git - Select version requirements
import Typhoon
let retryService = RetryPolicyService(
strategy: .constant(retry: 3, duration: .seconds(1))
)
do {
let result = try await retryService.retry {
try await fetchDataFromAPI()
}
print("✅ Success: \(result)")
} catch {
print("❌ Failed after retries: \(error)")
}Typhoon provides three powerful retry strategies to handle different failure scenarios:
/// A retry strategy with a constant number of attempts and fixed duration between retries.
case constant(retry: UInt, duration: DispatchTimeInterval)
/// A retry strategy with a linearly increasing delay.
case linear(retry: UInt, duration: DispatchTimeInterval)
/// A retry strategy with a Fibonacci-based delay progression.
case fibonacci(retry: UInt, duration: DispatchTimeInterval)
/// A retry strategy with exponential increase in duration between retries and added jitter.
case exponential(
retry: UInt,
jitterFactor: Double = 0.1,
maxInterval: DispatchTimeInterval? = .seconds(60),
multiplier: Double = 2.0,
duration: DispatchTimeInterval
)
/// A custom retry strategy defined by a user-provided delay calculator.
case custom(retry: UInt, strategy: IRetryDelayStrategy)Best for scenarios where you want predictable, fixed delays between retries:
import Typhoon
// Retry up to 5 times with 2 seconds between each attempt
let service = RetryPolicyService(
strategy: .constant(retry: 4, duration: .seconds(2))
)
do {
let data = try await service.retry {
try await URLSession.shared.data(from: url)
}
} catch {
print("Failed after 5 attempts")
}Retry Timeline:
- Attempt 1: Immediate
- Attempt 2: After 2 seconds
- Attempt 3: After 2 seconds
- Attempt 4: After 2 seconds
- Attempt 5: After 2 seconds
Delays grow proportionally with each attempt — a middle ground between constant and exponential:
import Typhoon
// Retry up to 4 times with linearly increasing delays
let service = RetryPolicyService(
strategy: .linear(retry: 3, duration: .seconds(1))
)Retry Timeline:
- Attempt 1: Immediate
- Attempt 2: After 1 second (1 × 1)
- Attempt 3: After 2 seconds (1 × 2)
- Attempt 4: After 3 seconds (1 × 3)
Delays follow the Fibonacci sequence — grows faster than linear but slower than exponential:
import Typhoon
let service = RetryPolicyService(
strategy: .fibonacci(retry: 5, duration: .seconds(1))
)Retry Timeline:
- Attempt 1: Immediate
- Attempt 2: After 1 second
- Attempt 3: After 1 second
- Attempt 4: After 2 seconds
- Attempt 5: After 3 seconds
- Attempt 6: After 5 seconds
Ideal for avoiding overwhelming a failing service by progressively increasing wait times:
import Typhoon
// Retry up to 4 times with exponentially increasing delays
let service = RetryPolicyService(
strategy: .exponential(
retry: 3,
jitterFactor: 0,
multiplier: 2.0,
duration: .seconds(1)
)
)
do {
let response = try await service.retry {
try await performNetworkRequest()
}
} catch {
print("Request failed after exponential backoff")
}Retry Timeline:
- Attempt 1: Immediate
- Attempt 2: After 1 second (1 × 2⁰)
- Attempt 3: After 2 seconds (1 × 2¹)
- Attempt 4: After 4 seconds (1 × 2²)
The most sophisticated strategy, adding randomization to prevent thundering herd problems:
import Typhoon
// Retry with exponential backoff, jitter, and maximum interval cap
let service = RetryPolicyService(
strategy: .exponential(
retry: 5,
jitterFactor: 0.2, // Add ±20% randomization
maxInterval: .seconds(30), // Cap at 30 seconds
multiplier: 2.0,
duration: .seconds(1)
)
)
do {
let result = try await service.retry {
try await connectToDatabase()
}
} catch {
print("Connection failed after sophisticated retry attempts")
}Benefits of Jitter:
- Prevents multiple clients from retrying simultaneously
- Reduces load spikes on recovering services
- Improves overall system resilience
Provide your own delay logic by implementing IRetryDelayStrategy:
import Typhoon
struct QuadraticDelayStrategy: IRetryDelayStrategy {
func delay(forRetry retries: UInt) -> UInt64? {
let seconds = Double(retries * retries) // 0s, 1s, 4s, 9s...
return UInt64(seconds * 1_000_000_000)
}
}
let service = RetryPolicyService(
strategy: .custom(retry: 4, strategy: QuadraticDelayStrategy())
)Combines multiple strategies executed sequentially. Each strategy runs independently with its own delay logic, making it ideal for phased retry approaches — e.g. react quickly first, then back off gradually.
import Typhoon
let service = RetryPolicyService(
strategy: .chain([
// Phase 1: 3 quick attempts with constant delay
.init(retries: 3, strategy: ConstantDelayStrategy(duration: .milliseconds(100))),
// Phase 2: 3 slower attempts with exponential backoff
.init(retries: 3, strategy: ExponentialDelayStrategy(
duration: .seconds(1),
multiplier: 2.0,
jitterFactor: 0.1,
maxInterval: .seconds(60)
))
])
)
do {
let result = try await service.retry {
try await fetchDataFromAPI()
}
} catch {
print("Failed after all phases")
}Retry Timeline:
Attempt 1: immediate
Attempt 2: 100ms ┐
Attempt 3: 100ms ├─ Phase 1: Constant
Attempt 4: 100ms ┘
Attempt 5: 1s ┐
Attempt 6: 2s ├─ Phase 2: Exponential
Attempt 7: 4s ┘
The total retry count is calculated automatically from the sum of all entries — no need to specify it manually.
Each strategy in the chain uses local indexing, meaning every phase starts its delay calculation from zero. This ensures each strategy behaves predictably regardless of its position in the chain.
import Typhoon
class APIClient {
private let retryService = RetryPolicyService(
strategy: .exponential(retry: 3, duration: .milliseconds(500))
)
func fetchUser(id: String) async throws -> User {
try await retryService.retry {
let (data, _) = try await URLSession.shared.data(
from: URL(string: "https://api.example.com/users/\(id)")!
)
return try JSONDecoder().decode(User.self, from: data)
}
}
}import Typhoon
class DatabaseManager {
private let retryService = RetryPolicyService(
strategy: .exponential(
retry: 5,
jitterFactor: 0.15,
maxInterval: .seconds(60),
duration: .seconds(1)
)
)
func saveRecord(_ record: Record) async throws {
try await retryService.retry {
try await database.insert(record)
}
}
}import Typhoon
class FileService {
private let retryService = RetryPolicyService(
strategy: .constant(retry: 3, duration: .milliseconds(100))
)
func writeFile(data: Data, to path: String) async throws {
try await retryService.retry {
try data.write(to: URL(fileURLWithPath: path))
}
}
}import Typhoon
class PaymentService {
private let retryService = RetryPolicyService(
strategy: .exponential(
retry: 4,
multiplier: 1.5,
duration: .seconds(2)
)
)
func processPayment(amount: Decimal) async throws -> PaymentResult {
try await retryService.retry {
try await paymentGateway.charge(amount: amount)
}
}
}- 🐛 Found a bug? Open an issue
- 💡 Have a feature request? Open an issue
- ❓ Questions? Start a discussion
- 🔒 Security issue? Email nv3212@gmail.com
Comprehensive documentation is available: Typhoon Documentation
We love contributions! Please feel free to help out with this project. If you see something that could be made better or want a new feature, open up an issue or send a Pull Request.
Bootstrap the development environment:
mise installNikita Vasilev
- Email: nv3212@gmail.com
- GitHub: @ns-vasilev
Typhoon is released under the MIT license. See LICENSE for details.
Made with ❤️ by space-code
