Files
Outpost/OutlineKit/Sources/OutlineKit/Support/RetryPolicy.swift
T
Puranjay Savar Mattas 5d5cda9cea fix: RetryPolicy.withRetry closure label, missing Foundation import
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.
2026-08-21 00:48:17 +01:00

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
}
}