Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ identifier_name:
excluded:
- id
- URL

- n
analyzer_rules:
- unused_import
- unused_declaration
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ case constant(retry: Int, 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: Int,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// Typhoon
// Copyright © 2026 Space Code. All rights reserved.
//

import Foundation

/// A retry delay strategy that increases the delay
/// following the Fibonacci sequence.
///
/// The delay is calculated as:
/// `baseDuration * fibonacci(retryIndex + 1)`
struct FibonacciDelayStrategy: IRetryDelayStrategy {
// MARK: - Properties

/// The base delay interval.
///
/// Each retry multiplies this value by
/// the corresponding Fibonacci number.
let duration: DispatchTimeInterval

// MARK: - IRetryDelayStrategy

/// Calculates a delay based on the Fibonacci sequence.
///
/// - Parameter retries: The current retry attempt index (starting from `0`).
/// - Returns: The delay in nanoseconds.
func delay(forRetry retries: UInt) -> UInt64? {
guard let seconds = duration.double else { return .zero }

let fib = fibonacci(retries + 1)
let delay = seconds * Double(fib)

return (delay * .nanosec).safeUInt64
}

// MARK: - Private

/// Returns the Fibonacci number for a given index.
///
/// Uses an iterative approach to avoid recursion overhead.
private func fibonacci(_ n: UInt) -> UInt {
guard n > 1 else { return 1 }

var previous: UInt = 1
var current: UInt = 1

for _ in 2 ..< n {
let next = previous + current
previous = current
current = next
}

return current
}
}
17 changes: 17 additions & 0 deletions Sources/Typhoon/Classes/Strategy/RetryPolicyStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ public enum RetryPolicyStrategy: Sendable {
/// the linear backoff interval.
case linear(retry: UInt, duration: DispatchTimeInterval)

/// A retry strategy with a Fibonacci-based delay progression.
///
/// The delay grows according to the Fibonacci sequence:
/// `duration * fibonacci(retryIndex + 1)`.
///
/// - Parameters:
/// - retry: The maximum number of retry attempts.
/// - duration: The base delay used to calculate
/// the Fibonacci backoff interval.
case fibonacci(retry: UInt, duration: DispatchTimeInterval)

/// A retry strategy with exponential increase in duration between retries and added jitter.
///
/// - Parameters:
Expand All @@ -52,6 +63,8 @@ public enum RetryPolicyStrategy: Sendable {
retry
case let .linear(retry, _):
retry
case let .fibonacci(retry, _):
retry
}
}

Expand All @@ -64,6 +77,8 @@ public enum RetryPolicyStrategy: Sendable {
duration
case let .linear(_, duration):
duration
case let .fibonacci(_, duration):
duration
}
}
}
Expand All @@ -82,6 +97,8 @@ extension RetryPolicyStrategy {
ConstantDelayStrategy(duration: duration)
case let .linear(_, duration):
LinearDelayStrategy(duration: duration)
case let .fibonacci(_, duration):
FibonacciDelayStrategy(duration: duration)
}
}
}
8 changes: 8 additions & 0 deletions Tests/TyphoonTests/UnitTests/RetryPolicyStrategyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ final class RetryPolicyStrategyTests: XCTestCase {
// then
XCTAssertEqual(duration, .second)
}

func test_thatRetryPolicyStrategyReturnsDuration_whenTypeIsFibonacci() {
// when
let duration = RetryPolicyStrategy.fibonacci(retry: .retry, duration: .second).duration

// then
XCTAssertEqual(duration, .second)
}
}

// MARK: Constants
Expand Down
11 changes: 11 additions & 0 deletions Tests/TyphoonTests/UnitTests/RetrySequenceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ final class RetrySequenceTests: XCTestCase {
XCTAssertEqual(result, [1, 2, 3, 4, 5, 6, 7, 8])
}

func test_thatRetrySequenceCreatesASequence_whenStrategyIsFibonacci() {
// given
let sequence = RetrySequence(strategy: .fibonacci(retry: .retry, duration: .nanosecond))

// when
let result: [UInt64] = sequence.map { $0 }

// then
XCTAssertEqual(result, [1, 1, 2, 3, 5, 8, 13, 21])
}

func test_thatRetrySequenceCreatesASequence_whenStrategyIsExponential() {
// given
let sequence = RetrySequence(strategy: .exponential(retry: .retry, jitterFactor: .zero, duration: .nanosecond))
Expand Down
Loading