//// Polling helper for Smithy `@waitable` operations. //// //// The codegen-emitted `wait_until_` functions are thin //// wrappers over `wait`. Each wrapper: //// //// 1. Invokes the underlying typed operation in a `step` closure. //// 2. Matches the result against the waiter's acceptors (a list //// of `state` + `matcher` rules lifted from the Smithy trait). //// Returns `Settled` when an acceptor with `state: success` //// matches, `FailedNow(err)` when `state: failure` matches, //// `Continue` otherwise. //// 3. Calls `wait(step, max_attempts, min_delay, max_delay)` to //// drive the loop with exponential backoff between attempts. //// //// Backoff doubles the previous delay (starting at `min_delay_ms`) //// up to `max_delay_ms` — Smithy's standard waiter cadence. The //// helper sleeps between attempts via `process.sleep`; tests pass //// zero delays so they don't block. //// //// `step` carries the 1-indexed attempt number. The codegen //// generally ignores it but specialised callers can use it for //// dynamic adjustments (e.g. emit a log line per attempt). import gleam/erlang/process /// What a single waiter step produced. The codegen-emitted /// `wait_until_` wrapper turns the typed operation's /// `Result(Output, Error)` plus the configured acceptors into one /// of these three states. pub type Step(error) { /// At least one `state: success` acceptor matched — the wait /// returns `Ok(Nil)` immediately. Settled /// No acceptor matched yet — sleep for the current backoff and /// re-invoke `step` (unless we've hit `max_attempts`). Continue /// A `state: failure` acceptor matched — the wait returns /// `Error(Failed(_))` immediately, no further attempts. FailedNow(error) } /// What can go wrong driving a waiter to completion. pub type WaiterError(error) { /// A `state: failure` acceptor matched on attempt `attempts`. Failed(error: error) /// Reached the configured `max_attempts` without ever settling /// or failing. `attempts` is the actual number of attempts made. MaxAttemptsExceeded(attempts: Int) } /// Drive a waiter step closure to completion, sleeping between /// attempts with exponential backoff capped at `max_delay_ms`. /// Returns `Ok(Nil)` on settle, `Error(Failed(_))` on a /// `state: failure` acceptor match, `Error(MaxAttemptsExceeded(_))` /// when the attempt budget is exhausted. `max_attempts == 0` is a /// defensive guard that returns immediately without invoking /// `step`. pub fn wait( step step: fn(Int) -> Step(error), max_attempts max_attempts: Int, min_delay_ms min_delay_ms: Int, max_delay_ms max_delay_ms: Int, ) -> Result(Nil, WaiterError(error)) { case max_attempts > 0 { False -> Error(MaxAttemptsExceeded(attempts: 0)) True -> loop(step, 1, max_attempts, min_delay_ms, max_delay_ms) } } fn loop( step: fn(Int) -> Step(error), attempt: Int, max_attempts: Int, current_delay_ms: Int, max_delay_ms: Int, ) -> Result(Nil, WaiterError(error)) { case step(attempt) { Settled -> Ok(Nil) FailedNow(e) -> Error(Failed(error: e)) Continue -> case attempt >= max_attempts { True -> Error(MaxAttemptsExceeded(attempts: max_attempts)) False -> { // The Gleam OTP scheduler treats `process.sleep(0)` as a // no-op; the loop drives attempts back-to-back in that // case (used by tests). sleep(current_delay_ms) loop( step, attempt + 1, max_attempts, next_delay(current_delay_ms, max_delay_ms), max_delay_ms, ) } } } } /// Smithy's waiter backoff: double the previous delay, capped at /// `max_delay_ms`. Starts at `min_delay_ms` (the first sleep) and /// climbs from there. fn next_delay(current_ms: Int, max_delay_ms: Int) -> Int { let doubled = current_ms * 2 case doubled > max_delay_ms { True -> max_delay_ms False -> doubled } } fn sleep(ms: Int) -> Nil { case ms <= 0 { True -> Nil False -> process.sleep(ms) } }