import Foundation
/// Backoff for poll loops: `interval` doubles up to `maxInterval` between attempts
/// until `timeout` has elapsed. The first attempt always runs, even with a zero or
/// negative timeout.
public struct PollOptions: Sendable {
public var interval: Duration = .seconds(1)
public var maxInterval: Duration = .seconds(8)
public var timeout: Duration = .seconds(120)
public var message: String = "Timed out polling"
public init(interval: Duration = .seconds(1), maxInterval: Duration = .seconds(8),
timeout: Duration = .seconds(120), message: String = "Timed out polling") {
self.interval = interval
self.maxInterval = maxInterval
self.timeout = timeout
self.message = message
}
public static let jobs = PollOptions(message: "Timed out waiting for the job")
public static let uploads = PollOptions(timeout: .seconds(300), message: "Timed out waiting for the upload")
public static let actions = PollOptions(message: "Timed out waiting for the action")
}
/// Runs `step` until `done`, sleeping with doubling backoff between attempts; throws
/// `IrdError.timeout` carrying the last result once the deadline has passed.
public func pollUntil<T: Encodable & Sendable>(
_ options: PollOptions, clock: any IrdClock,
step: @Sendable () async throws -> T, done: @Sendable (T) -> Bool
) async throws -> T {
let deadline = clock.now + options.timeout
var interval = options.interval
while true {
let result = try await step()
if done(result) { return result }
if clock.now >= deadline {
throw IrdError.timeout(message: options.message, last: try? JSONValue(encoding: result))
}
try await clock.sleep(for: interval)
interval = min(options.maxInterval, interval * 2)
}
}