space-code/typhoon
## Description
Description
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.
Features
β¨ 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 π§Ύ Pluggable Logging β Integrates with OSLog or custom loggers π URLSession Integration β Retry network requests with a single parameter π§ͺ Well Tested - Comprehensive test coverage
Table of Contents
- Retry Strategies - Constant Strategy - Exponential Strategy - Exponential with Jitter Strategy - Custom Strategy - Chain Strategy
Requirements
| Platform | Minimum Version | |-----------|----------------| | iOS | 13.0+ | | macOS | 10.15+ | | tvOS | 13.0+ | | watchOS | 6.0+ | | visionOS | 1.0+ | | Xcode | 15.3+ | | Swift | 5.10+ |
Installation
Swift Package Manager
Add the following dependency to your Package.swift:
dependencies: [
.package(url: "https://github.com/space-code/typhoon.git", from: "2.0.0")
]Or add it through Xcode:
- File > Add Package Dependencies
- Enter package URL:
https://github.com/space-code/typhoon.git - Select version requirements
Quick Start
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)")
}Usage
Retry Strategies
Typhoon provides six 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, dispatchDuration: DispatchTimeInterval)
/// A retry strategy with a linearly increasing delay.
case linear(retry: UInt, dispatchDuration: DispatchTimeInterval)
/// A retry strategy with a Fibonacci-based delay progression.
case fibonacci(retry: UInt, dispatchDuration: 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,
dispatchDuration: DispatchTimeInterval
)
/// A custom retry strategy defined by a user-provided delay calculator.
case custom(retry: UInt, strategy: IRetryDelayStrategy)Additionally, Typhoon allows composing multiple retry strategies into a single policy using a chained strategy:
RetryPolicyStrategy.chain([
.constant(retry: 2, dispatchDuration: .seconds(1)),
.exponential(retry: 3, dispatchDuration: .seconds(2))
])Constant Strategy
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, dispatchDuration: .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
Linear Strategy
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, dispatchDuration: .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)
Fibonacci Strategy
Delays follow the Fibonacci sequence β grows faster than linear but slower than exponential:
import Typhoon
let service = RetryPolicyService(
strategy: .fibonacci(retry: 5, dispatchDuration: .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
Exponential Strategy
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,
dispatchDuration: .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Β²)
Exponential with Jitter Strategy
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,
dispatchDuration: .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
Custom Strategy
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())
)Chain Strategy
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(dispatchDuration: .milliseconds(100))),
// Phase 2: 3 slower attempts with exponential backoff
.init(retries: 3, strategy: ExponentialDelayStrategy(
dispatchDuration: .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.
Logging
Typhoon provides a lightweight logging abstraction that allows you to integrate retry diagnostics into your existing logging system.
The framework defines a simple ILogger protocol:
public protocol ILogger: Sendable {
func info(_ message: String)
func warning(_ message: String)
func error(_ message: String)
}You can plug in any logging framework by implementing this protocol.
Using Apple's OSLog
Typhoon includes built-in support for Apple's OSLog system via Logger:
import Typhoon
import OSLog
let logger = Logger(subsystem: "com.example.network", category: "retry")
let retryService = RetryPolicyService(
strategy: .exponential(retry: 3, dispatchDuration: .seconds(1)),
logger: logger
)All retry attempts, failures, and final errors will be reported through the provided logger.
You can also integrate third-party loggers like SwiftLog or custom analytics systems.
URLSession Integration
Typhoon provides built-in integration with URLSession, allowing you to apply retry policies directly to network requests with minimal boilerplate.
Instead of wrapping network calls manually, you can call retry-enabled methods directly on URLSession.
Fetch Data with Retry
import Typhoon
let (data, response) = try await URLSession.shared.data(
from: URL(string: "https://api.example.com/users")!,
retryPolicy: .exponential(
retry: 3,
jitterFactor: 0.1,
dispatchDuration: .seconds(1)
)
)Using URLRequest
var request = URLRequest(url: URL(string: "https://api.example.com/users")!)
request.httpMethod = "GET"
let (data, response) = try await URLSession.shared.data(
for: request,
retryPolicy: .constant(retry: 3, dispatchDuration: .seconds(1))
)Upload Requests
let (data, response) = try await URLSession.shared.upload(
for: request,
from: bodyData,
retryPolicy: .exponential(retry: 3, dispatchDuration: .seconds(1))
)Download Requests
let (fileURL, response) = try await URLSession.shared.download(
for: request,
retryPolicy: .exponential(retry: 4, dispatchDuration: .seconds(2))
)Common Use Cases
Network Requests
import Typhoon
class APIClient {
private let retryService = RetryPolicyService(
strategy: .exponential(retry: 3, dispatchDuration: .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)
}
}
}Database Operations
import Typhoon
class DatabaseManager {
private let retryService = RetryPolicyService(
strategy: .exponential(
retry: 5,
jitterFactor: 0.15,
maxInterval: .seconds(60),
dispatchDuration: .seconds(1)
)
)
func saveRecord(_ record: Record) async throws {
try await retryService.retry {
try await database.insert(record)
}
}
}File Operations
import Typhoon
class FileService {
private let retryService = RetryPolicyService(
strategy: .constant(retry: 3, dispatchDuration: .milliseconds(100))
)
func writeFile(data: Data, to path: String) async throws {
try await retryService.retry {
try data.write(to: URL(fileURLWithPath: path))
}
}
}Third-Party Service Integration
import Typhoon
class PaymentService {
private let retryService = RetryPolicyService(
strategy: .exponential(
retry: 4,
multiplier: 1.5,
dispatchDuration: .seconds(2)
)
)
func processPayment(amount: Decimal) async throws -> PaymentResult {
try await retryService.retry {
try await paymentGateway.charge(amount: amount)
}
}
}Communication
- π Found a bug? Open an issue
- π‘ Have a feature request? Open an issue
- β Questions? Start a discussion
- π Security issue? Email nv3212@gmail.com
Documentation
Comprehensive documentation is available: Typhoon Documentation
Contributing
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.
Development Setup
Bootstrap the development environment:
mise installLicense
Typhoon is released under the MIT license. See LICENSE for details.
<div align="center">
Made with β€οΈ by space-code
</div>
Package Metadata
Repository: space-code/typhoon
Default branch: main
README: README.md