RetryPolicy.withRetry's operation param wasn't anonymous (_), so the
14 Outpost call sites that pass the closure in parens - RetryPolicy.
withRetry({ ... }) - rather than as a trailing closure failed to
compile ("Missing argument label 'operation:'" cascading into
nonsense errors about maxAttempts). OutlineKit's own internal call
sites all happened to use trailing-closure syntax, so this only
showed up once the app target actually got compiled. One-line fix at
the declaration (_ operation:) instead of touching every call site -
trailing-closure calls are unaffected either way.
APIFailureCenter.swift used Date/TimeInterval/URL/URLComponents/
URLQueryItem while only importing Observation and OutlineKit -
missing import Foundation. Other new files in the same commit escaped
this because they import SwiftUI, which re-exports Foundation
transitively; this one didn't.
OutlineKit: 92/92 still passing.
39 lines
1.4 KiB
Swift
39 lines
1.4 KiB
Swift
import Foundation
|
|
|
|
/// Automatic retry-with-backoff for API calls, so a single transient network
|
|
/// blip doesn't turn into a user-visible failure (or a silently swallowed
|
|
/// one) the way one `try?` used to.
|
|
///
|
|
/// Only retries `OutlineAPIError.transport` — a dropped connection or
|
|
/// timeout might succeed a second later. Everything else (`.decoding`,
|
|
/// `.unauthorized`, `.notFound`, `.server`, `.tokenUnavailable`) is retried
|
|
/// zero times: a response-shape mismatch or a 404 will look exactly the same
|
|
/// on attempt two, so retrying just burns the cooldown window for nothing —
|
|
/// callers should treat those as immediate failures instead.
|
|
public enum RetryPolicy {
|
|
public static func withRetry<T: Sendable>(
|
|
maxAttempts: Int = 3,
|
|
initialDelay: Duration = .seconds(1),
|
|
_ operation: () async throws -> T
|
|
) async throws -> T {
|
|
var attempt = 1
|
|
var delay = initialDelay
|
|
while true {
|
|
do {
|
|
return try await operation()
|
|
} catch {
|
|
guard attempt < maxAttempts, isRetryable(error) else { throw error }
|
|
attempt += 1
|
|
try? await Task.sleep(for: delay)
|
|
delay *= 2
|
|
}
|
|
}
|
|
}
|
|
|
|
static func isRetryable(_ error: Error) -> Bool {
|
|
guard let apiError = error as? OutlineAPIError else { return false }
|
|
if case .transport = apiError { return true }
|
|
return false
|
|
}
|
|
}
|