Skip to content

Commit 264ef92

Browse files
committed
feat: add Swift Duration support for retry strategies
1 parent 931cc04 commit 264ef92

17 files changed

+478
-138
lines changed

README.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -99,21 +99,21 @@ Typhoon provides three powerful retry strategies to handle different failure sce
9999

100100
```swift
101101
/// A retry strategy with a constant number of attempts and fixed duration between retries.
102-
case constant(retry: UInt, duration: DispatchTimeInterval)
102+
case constant(retry: UInt, dispatchDuration: DispatchTimeInterval)
103103

104104
/// A retry strategy with a linearly increasing delay.
105-
case linear(retry: UInt, duration: DispatchTimeInterval)
105+
case linear(retry: UInt, dispatchDuration: DispatchTimeInterval)
106106

107107
/// A retry strategy with a Fibonacci-based delay progression.
108-
case fibonacci(retry: UInt, duration: DispatchTimeInterval)
108+
case fibonacci(retry: UInt, dispatchDuration: DispatchTimeInterval)
109109

110110
/// A retry strategy with exponential increase in duration between retries and added jitter.
111111
case exponential(
112112
retry: UInt,
113113
jitterFactor: Double = 0.1,
114114
maxInterval: DispatchTimeInterval? = .seconds(60),
115115
multiplier: Double = 2.0,
116-
duration: DispatchTimeInterval
116+
dispatchDuration: DispatchTimeInterval
117117
)
118118

119119
/// A custom retry strategy defined by a user-provided delay calculator.
@@ -129,7 +129,7 @@ import Typhoon
129129

130130
// Retry up to 5 times with 2 seconds between each attempt
131131
let service = RetryPolicyService(
132-
strategy: .constant(retry: 4, duration: .seconds(2))
132+
strategy: .constant(retry: 4, dispatchDuration: .seconds(2))
133133
)
134134

135135
do {
@@ -157,7 +157,7 @@ import Typhoon
157157

158158
// Retry up to 4 times with linearly increasing delays
159159
let service = RetryPolicyService(
160-
strategy: .linear(retry: 3, duration: .seconds(1))
160+
strategy: .linear(retry: 3, dispatchDuration: .seconds(1))
161161
)
162162
```
163163

@@ -175,7 +175,7 @@ Delays follow the Fibonacci sequence — grows faster than linear but slower tha
175175
import Typhoon
176176

177177
let service = RetryPolicyService(
178-
strategy: .fibonacci(retry: 5, duration: .seconds(1))
178+
strategy: .fibonacci(retry: 5, dispatchDuration: .seconds(1))
179179
)
180180
```
181181

@@ -200,7 +200,7 @@ let service = RetryPolicyService(
200200
retry: 3,
201201
jitterFactor: 0,
202202
multiplier: 2.0,
203-
duration: .seconds(1)
203+
dispatchDuration: .seconds(1)
204204
)
205205
)
206206

@@ -233,7 +233,7 @@ let service = RetryPolicyService(
233233
jitterFactor: 0.2, // Add ±20% randomization
234234
maxInterval: .seconds(30), // Cap at 30 seconds
235235
multiplier: 2.0,
236-
duration: .seconds(1)
236+
dispatchDuration: .seconds(1)
237237
)
238238
)
239239

@@ -280,10 +280,10 @@ import Typhoon
280280
let service = RetryPolicyService(
281281
strategy: .chain([
282282
// Phase 1: 3 quick attempts with constant delay
283-
.init(retries: 3, strategy: ConstantDelayStrategy(duration: .milliseconds(100))),
283+
.init(retries: 3, strategy: ConstantDelayStrategy(dispatchDuration: .milliseconds(100))),
284284
// Phase 2: 3 slower attempts with exponential backoff
285285
.init(retries: 3, strategy: ExponentialDelayStrategy(
286-
duration: .seconds(1),
286+
dispatchDuration: .seconds(1),
287287
multiplier: 2.0,
288288
jitterFactor: 0.1,
289289
maxInterval: .seconds(60)
@@ -325,7 +325,7 @@ import Typhoon
325325

326326
class APIClient {
327327
private let retryService = RetryPolicyService(
328-
strategy: .exponential(retry: 3, duration: .milliseconds(500))
328+
strategy: .exponential(retry: 3, dispatchDuration: .milliseconds(500))
329329
)
330330

331331
func fetchUser(id: String) async throws -> User {
@@ -350,7 +350,7 @@ class DatabaseManager {
350350
retry: 5,
351351
jitterFactor: 0.15,
352352
maxInterval: .seconds(60),
353-
duration: .seconds(1)
353+
dispatchDuration: .seconds(1)
354354
)
355355
)
356356

@@ -369,7 +369,7 @@ import Typhoon
369369

370370
class FileService {
371371
private let retryService = RetryPolicyService(
372-
strategy: .constant(retry: 3, duration: .milliseconds(100))
372+
strategy: .constant(retry: 3, dispatchDuration: .milliseconds(100))
373373
)
374374

375375
func writeFile(data: Data, to path: String) async throws {
@@ -390,7 +390,7 @@ class PaymentService {
390390
strategy: .exponential(
391391
retry: 4,
392392
multiplier: 1.5,
393-
duration: .seconds(2)
393+
dispatchDuration: .seconds(2)
394394
)
395395
)
396396

Sources/Typhoon/Classes/Extensions/DispatchTimeInterval+Double.swift

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,32 @@
66
import Foundation
77

88
extension DispatchTimeInterval {
9-
/// Converts a `DispatchTimeInterval` value into seconds represented as `Double`.
9+
/// Converts a `DispatchTimeInterval` value into nanoseconds represented as `UInt64`.
1010
///
11-
/// This computed property normalizes all supported `DispatchTimeInterval` cases
12-
/// (`seconds`, `milliseconds`, `microseconds`, `nanoseconds`) into a single
13-
/// unit — **seconds** — which simplifies time calculations and conversions.
14-
///
15-
/// For example:
16-
/// - `.seconds(2)` → `2.0`
17-
/// - `.milliseconds(500)` → `0.5`
18-
/// - `.microseconds(1_000)` → `0.001`
19-
/// - `.nanoseconds(1_000_000_000)` → `1.0`
20-
///
21-
/// - Returns: The interval expressed in seconds as `Double`,
11+
/// - Returns: The interval expressed in nanoseconds,
2212
/// or `nil` if the interval represents `.never` or an unknown case.
23-
var double: Double? {
13+
var nanoseconds: UInt64? {
2414
switch self {
2515
case let .seconds(value):
26-
return Double(value)
16+
return UInt64(value) * 1_000_000_000
2717
case let .milliseconds(value):
28-
return Double(value) * 1e-3
18+
return UInt64(value) * 1_000_000
2919
case let .microseconds(value):
30-
return Double(value) * 1e-6
20+
return UInt64(value) * 1000
3121
case let .nanoseconds(value):
32-
return Double(value) * 1e-9
22+
return UInt64(value)
3323
case .never:
3424
return nil
3525
@unknown default:
3626
return nil
3727
}
3828
}
29+
30+
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
31+
static func from(_ duration: Duration) -> DispatchTimeInterval {
32+
let seconds = duration.components.seconds
33+
let nanos = duration.components.attoseconds / 1_000_000_000
34+
let totalNanos = Int(seconds) * 1_000_000_000 + Int(nanos)
35+
return .nanoseconds(totalNanos)
36+
}
3937
}

Sources/Typhoon/Classes/Extensions/Double+Nanosec.swift

Lines changed: 0 additions & 10 deletions
This file was deleted.

Sources/Typhoon/Classes/Extensions/Double+SafeUInt64.swift

Lines changed: 0 additions & 12 deletions
This file was deleted.

Sources/Typhoon/Classes/RetryPolicyService/RetryPolicyService.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ extension RetryPolicyService: IRetryPolicyService {
9494

9595
var iterator = RetrySequence(strategy: effectiveStrategy).makeIterator()
9696

97-
let deadline = maxTotalDuration?.double.map { Date().addingTimeInterval($0) }
97+
let deadline = maxTotalDuration?.nanoseconds.map {
98+
Date().addingTimeInterval(TimeInterval($0) / 1_000_000_000)
99+
}
98100

99101
while true {
100102
if let deadline, Date() > deadline {

Sources/Typhoon/Classes/RetrySequence/Strategies/ConstantDelayStrategy.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ struct ConstantDelayStrategy: IRetryDelayStrategy {
2020
/// - Parameter retries: The current retry attempt index (ignored).
2121
/// - Returns: The delay in nanoseconds.
2222
func delay(forRetry _: UInt) -> UInt64? {
23-
guard let seconds = duration.double else { return .zero }
24-
return (seconds * .nanosec).safeUInt64
23+
guard let nanos = duration.nanoseconds else { return .zero }
24+
return nanos
2525
}
2626
}

Sources/Typhoon/Classes/RetrySequence/Strategies/ExponentialDelayStrategy.swift

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ struct ExponentialDelayStrategy: IRetryDelayStrategy {
3131
/// If specified, the computed delay will never exceed this value.
3232
let maxInterval: DispatchTimeInterval?
3333

34+
// MARK: Initialization
35+
36+
init(
37+
duration: DispatchTimeInterval,
38+
multiplier: Double = 2.0,
39+
jitterFactor: Double = 0.1,
40+
maxInterval: DispatchTimeInterval? = .seconds(60)
41+
) {
42+
self.duration = duration
43+
self.multiplier = multiplier
44+
self.jitterFactor = jitterFactor
45+
self.maxInterval = maxInterval
46+
}
47+
3448
// MARK: - IRetryDelayStrategy
3549

3650
/// Calculates the delay for a given retry attempt using
@@ -39,22 +53,21 @@ struct ExponentialDelayStrategy: IRetryDelayStrategy {
3953
/// - Parameter retries: The current retry attempt index (starting at `0`).
4054
/// - Returns: The delay in nanoseconds, or `nil` if it cannot be computed.
4155
func delay(forRetry retries: UInt) -> UInt64? {
42-
guard let seconds = duration.double else { return .zero }
56+
guard let baseNanos = duration.nanoseconds else { return .zero }
4357

44-
let maxDelayNanos = maxInterval.flatMap(\.double)
45-
.map { min($0 * .nanosec, Double(UInt64.max)) } ?? Double(UInt64.max)
58+
let maxDelayNanos = maxInterval?.nanoseconds.map { UInt64($0) } ?? UInt64.max
4659

47-
let base = seconds * .nanosec * pow(multiplier, Double(retries))
60+
let base = Double(baseNanos) * pow(multiplier, Double(retries))
4861

49-
guard base < maxDelayNanos, base < Double(UInt64.max) else {
50-
return maxDelayNanos.safeUInt64
62+
guard base < Double(maxDelayNanos) else {
63+
return maxDelayNanos
5164
}
5265

5366
let jitterRange = base * jitterFactor
5467
let jittered = Double.random(
55-
in: max(0, base - jitterRange) ... min(base + jitterRange, maxDelayNanos)
68+
in: max(0, base - jitterRange) ... min(base + jitterRange, Double(maxDelayNanos))
5669
)
5770

58-
return min(jittered, maxDelayNanos).safeUInt64
71+
return UInt64(jittered)
5972
}
6073
}

Sources/Typhoon/Classes/RetrySequence/Strategies/FibonacciDelayStrategy.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,16 @@ struct FibonacciDelayStrategy: IRetryDelayStrategy {
2626
/// - Parameter retries: The current retry attempt index (starting from `0`).
2727
/// - Returns: The delay in nanoseconds.
2828
func delay(forRetry retries: UInt) -> UInt64? {
29-
guard let seconds = duration.double else { return .zero }
30-
31-
let fib = fibonacci(retries + 1)
32-
let delay = seconds * Double(fib)
33-
34-
return (delay * .nanosec).safeUInt64
29+
guard let nanos = duration.nanoseconds else { return .zero }
30+
return nanos * fibonacci(retries + 1)
3531
}
3632

3733
// MARK: - Private
3834

3935
/// Returns the Fibonacci number for a given index.
4036
///
4137
/// Uses an iterative approach to avoid recursion overhead.
42-
private func fibonacci(_ n: UInt) -> UInt {
38+
private func fibonacci(_ n: UInt) -> UInt64 {
4339
guard n > 1 else { return 1 }
4440

4541
var previous: UInt = 1
@@ -51,6 +47,6 @@ struct FibonacciDelayStrategy: IRetryDelayStrategy {
5147
current = next
5248
}
5349

54-
return current
50+
return UInt64(current)
5551
}
5652
}

Sources/Typhoon/Classes/RetrySequence/Strategies/LinearDelayStrategy.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ struct LinearDelayStrategy: IRetryDelayStrategy {
2626
/// The formula used:
2727
/// `baseDuration * (retries + 1)`
2828
func delay(forRetry retries: UInt) -> UInt64? {
29-
guard let seconds = duration.double else { return .zero }
30-
let delay = seconds * Double(retries + 1)
31-
return (delay * .nanosec).safeUInt64
29+
guard let nanos = duration.nanoseconds else { return .zero }
30+
return nanos * UInt64(retries + 1)
3231
}
3332
}

0 commit comments

Comments
 (0)