Skip to content

Repository files navigation

A powerful retry policy service for Swift

typhoon

LicenseSwift CompatibilityPlatform CompatibilityCI

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

Requirements

PlatformMinimum Version
iOS13.0+
macOS10.15+
tvOS13.0+
watchOS6.0+
visionOS1.0+
Xcode15.3+
Swift5.10+

Installation

Swift Package Manager

Add the following dependency to your Package.swift:

dependencies:[.package(url:"https://github.com/space-code/typhoon.git", from:"3.0.0")]

Or add it through Xcode:

  1. File > Add Package Dependencies
  2. Enter package URL: https://github.com/space-code/typhoon.git
  3. Select version requirements

Quick Start

import Typhoon
letretryService=RetryPolicyService(
strategy:.constant(retry:3, duration:.seconds(1)))do{letresult=tryawait retryService.retry{tryawaitfetchDataFromAPI()}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
letservice=RetryPolicyService(
strategy:.constant(retry:4, dispatchDuration:.seconds(2)))do{letdata=tryawait service.retry{tryawaitURLSession.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
letservice=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
letservice=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
letservice=RetryPolicyService(
strategy:.exponential(
retry:3,
jitterFactor:0,
multiplier:2.0,
dispatchDuration:.seconds(1)))do{letresponse=tryawait service.retry{tryawaitperformNetworkRequest()}}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
letservice=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{letresult=tryawait service.retry{tryawaitconnectToDatabase()}}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
structQuadraticDelayStrategy:IRetryDelayStrategy{func delay(forRetry retries:UInt)->UInt64?{letseconds=Double(retries * retries) // 0s, 1s, 4s, 9s...
returnUInt64(seconds *1_000_000_000)}}letservice=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
letservice=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{letresult=tryawait service.retry{tryawaitfetchDataFromAPI()}}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
classAPIClient{privateletretryService=RetryPolicyService(
strategy:.exponential(retry:3, dispatchDuration:.milliseconds(500)))func fetchUser(id:String)asyncthrows->User{tryawait retryService.retry{let(data, _)=tryawaitURLSession.shared.data(
from:URL(string:"https://api.example.com/users/\(id)")!
)returntryJSONDecoder().decode(User.self, from: data)}}}

Database Operations

import Typhoon
classDatabaseManager{privateletretryService=RetryPolicyService(
strategy:.exponential(
retry:5,
jitterFactor:0.15,
maxInterval:.seconds(60),
dispatchDuration:.seconds(1)))func saveRecord(_ record:Record)asyncthrows{tryawait retryService.retry{tryawait database.insert(record)}}}

File Operations

import Typhoon
classFileService{privateletretryService=RetryPolicyService(
strategy:.constant(retry:3, dispatchDuration:.milliseconds(100)))func writeFile(data:Data, to path:String)asyncthrows{tryawait retryService.retry{try data.write(to:URL(fileURLWithPath: path))}}}

Third-Party Service Integration

import Typhoon
classPaymentService{privateletretryService=RetryPolicyService(
strategy:.exponential(
retry:4,
multiplier:1.5,
dispatchDuration:.seconds(2)))func processPayment(amount:Decimal)asyncthrows->PaymentResult{tryawait retryService.retry{tryawait paymentGateway.charge(amount: amount)}}}

Communication

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 install

Author

Nikita Vasilev

License

Typhoon is released under the MIT license. See LICENSE for details.


⬆ back to top

Made with ❤️ by space-code

Releases

Packages

Used by

Contributors

Languages