sdk-runtime

    API-agnostic client runtime for MoonBit SDKs: error taxonomy, Retry-After and backoff, tri-state JSON fields, open enums. Built on gaato/http; no async runtime dependency.

    sdk
    http
    retry
    rate-limit
    pagination
    json
    Download zip
    Author
    Version
    0.2.1
    License
    Apache-2.0
    Last updated
    7 hours ago
    Downloads
    39

    Dependencies

    #gaato/sdk-runtime

    API-agnostic client runtime for MoonBit SDKs, built on gaato/http: an error taxonomy (SdkError), Retry-After / retry-after-ms parsing, backoff and retry policy, a Client that resolves URLs, applies default headers and auth, runs a rate limiter and middleware, classifies responses and retries; a window rate limiter, a paginator, tri-state JSON fields (Presence), open enums and a multipart writer (gaato/sdk-runtime/json, gaato/sdk-runtime/multipart). No async runtime dependency: time comes through the Clock trait.

    Unofficial and experimental. Source, issues and design notes: https://github.com/gaato/mbt-sdk

    RateLimiter

    pub(open) trait RateLimiter {
    async fn acquire(Self, String, global_exempt~ : Bool) -> Unit
    async fn release(Self, String, status~ : Int, headers~ :
    Headers
    ) -> Unit
    }

    A per-bucket admission policy updated from HTTP responses.

    acquire and release come in pairs: the client calls release exactly once for every acquire that returned, with the response's status and headers, or with status 0 and no headers when the attempt produced no response (a transport failure or a cancellation). A limiter that holds per-bucket state across the exchange, such as a gate that serialises one bucket, relies on that pairing; a stateless window limiter ignores the status-0 calls.

    A cancelled task still gets its release, but inside cancellation any further asynchronous work is cut short, so an implementation that talks to another process must recover on its own if that call does not complete (dropping its connection so the peer can clean up, for example).

    RetryDecider

    pub(open) trait RetryDecider {
    fn next_delay_ms(Self, SdkError,
    Request
    , Int, Double) -> Int?
    }

    Decides, after a failed attempt, whether the client sends the request again and how long it waits first.

    RetryPolicy is the general-purpose implementation. An API with its own rules — one that must never resend a timed-out request because it may already have been processed, or that reads the delay from a response body — supplies its own implementation to Client::new.

    SdkError

    pub(all) suberror SdkError {
    Transport(
    HttpError
    )
    Status(status~ : Int, headers~ :
    Headers
    , body~ : Bytes)
    RateLimited(retry_after_ms~ : Int?, headers~ :
    Headers
    , body~ : Bytes)
    Decode(message~ : String, body~ : Bytes)
    Config(String)
    } derive(
    Debug
    )

    Failures distinguished by transport, HTTP status, decoding, or configuration.

    SdkError::is_retryable

    fn SdkError::is_retryable(self : SdkError) -> Bool

    Reports transient failures; request idempotency remains the caller's concern.

    SdkError::status

    fn SdkError::status(self : SdkError) -> Int?

    Returns an HTTP status only for failures with an HTTP status.

    SdkError::to_repr

    Formats the classified failure for debugging.

    Attempt

    pub(all) struct Attempt {
    bucket : String
    request :
    Request

    attempt : Int
    status : Int
    headers :
    Headers

    body : Bytes
    duration_ms : Int64
    error : SdkError?
    }

    One network attempt of a Client call, as reported to the observer.

    status, headers and body are those of the response (the body is empty for a streaming response, whose body belongs to the caller); an attempt that produced no response (a transport failure or a cancellation) reports status 0, empty headers and body, and the failure in error when there is one. attempt counts from 0, so a request that succeeded after two retries reports its last attempt as 2.

    Auth

    pub(all) enum Auth {
    NoAuth
    Bearer(String)
    Header(String, String)
    }

    Request authentication. Debug output always redacts credential values.
    impl Debug for Auth

    Auth::apply

    Adds authentication only when the request has no header with the same name.

    test {
    let request = @runtime.Bearer("secret").apply(@http.Request::get("/"))
    assert_eq(request.headers.get("authorization"), Some("Bearer secret"))
    }

    Auth::to_repr

    Redacted debug representation of authentication settings.

    Backoff

    pub(all) struct Backoff {
    base_ms : Int
    max_ms : Int
    factor : Double
    jitter : Double
    } derive(Eq,
    Debug
    )

    Parameters for pure exponential backoff with symmetric jitter.

    Backoff::default

    fn Backoff::default() -> Backoff

    Uses 500ms base, 30000ms cap, factor 2, and jitter 0.2.

    Backoff::delay_ms

    fn Backoff::delay_ms(self : Backoff, attempt : Int, random : Double) -> Int

    Computes a capped delay, truncating fractional milliseconds. Negative attempts behave as zero; random is clamped into [0, 1).

    test {
    assert_eq(@runtime.Backoff::default().delay_ms(2, 0.5), 2000)
    }

    Backoff::equal

    fn Backoff::equal(Backoff, Backoff) -> Bool

    Compares backoff parameters.

    Backoff::not_equal

    fn Backoff::not_equal(x : Backoff, y : Backoff) -> Bool

    Compares backoff parameters.

    Backoff::to_repr

    Formats backoff parameters for debugging.

    Client

    pub struct Client {
    // private fields
    }

    A transport-independent SDK client with authentication, retry, and limits.

    Client::new

    fn Client::new(transport : &
    Transport
    , clock : &
    Clock
    , base_url~ : String, default_headers? :
    Headers
    , auth? : Auth, retry? : &RetryDecider, limiter? : &RateLimiter, middleware? : Array[async (
    Request
    , async (
    Request
    ) ->
    Response
    raise
    HttpError
    ) ->
    Response
    raise
    HttpError
    ], observer? : (Attempt) -> Unit, random? : () -> Double) -> Client

    Creates a client from injected transport and clock implementations.

    retry decides after each failed attempt whether to send again; the default is RetryPolicy::default(). observer is called once per attempt, after the limiter's release and before that decision, including for an attempt that fails or is cancelled; it must not raise and should return promptly.

    Client::send

    async fn Client::send(self : Client, request :
    Request
    , bucket? : String, global_exempt? : Bool, body? : () -> Bytes) ->
    Response
    raise SdkError

    Sends a buffered request, retrying only as directed by the retry decider.

    bucket names the rate-limit bucket the request is admitted through and global_exempt keeps it out of an account-wide limit; both are passed to the limiter as they are.

    body, when given, replaces the request's body: it is called once per attempt, after the limiter admits that attempt, so a request waiting for admission holds no encoded body (a multipart upload can keep only the segments from @multipart.encode_segments and join them here). The retry decider sees the request without it; the observer sees what was sent.

    Client::send_json

    async fn Client::send_json(self : Client, request :
    Request
    , bucket? : String, global_exempt? : Bool, body? : () -> Bytes) -> Json raise SdkError

    Sends and decodes JSON; an empty successful body is JSON null. body is as for send.

    Client::send_stream

    async fn Client::send_stream(self : Client, request :
    Request
    , bucket? : String, global_exempt? : Bool, body? : () -> Bytes) -> (
    ResponseHead
    , &
    BodyStream
    ) raise SdkError

    Sends a streaming request. A successful body is returned to the caller; an unsuccessful body is read up to 1 MiB, closed, classified, and retried. body is as for send.

    NoLimiter

    pub struct NoLimiter {
    }

    A limiter that never delays requests and ignores releases.

    NoLimiter::acquire

    async fn NoLimiter::acquire(_self : NoLimiter, _bucket : String, global_exempt~ : Bool) -> Unit

    Explicit no-op limiter methods.

    NoLimiter::new

    fn NoLimiter::new() -> NoLimiter

    Creates a limiter that admits every request.

    NoLimiter::release

    async fn NoLimiter::release(_self : NoLimiter, _bucket : String, status~ : Int, headers~ :
    Headers
    ) -> Unit

    Explicit no-op limiter methods.

    Page

    pub(all) struct Page[T] {
    items : Array[T]
    next : String?
    }

    One decoded page and its optional cursor for the following page, for Paginator::with_cursor.

    Paginator

    pub struct Paginator[T, E] {
    // private fields
    }

    A stateful async paginator.

    E is the error the fetch raises, so an SDK exposes its own error type through the paginator instead of SdkError.

    Paginator::collect

    async fn[T, E : Error] Paginator::collect(self : Paginator[T, E], max~ : Int) -> Array[T] raise E

    Collects at most max remaining items. The unused rest of the last page is kept for the next call, so nothing fetched is lost. A non-positive maximum performs no requests and returns an empty array.

    Paginator::each

    async fn[T, E : Error] Paginator::each(self : Paginator[T, E], f : async (T) -> Unit raise E) -> Unit raise E

    Visits every remaining item in page order.

    Paginator::new

    fn[T, E : Error] Paginator::new(fetch : async () -> Array[T]? raise E) -> Paginator[T, E]

    Creates a paginator over a fetch that owns its own cursor.

    fetch returns the next page, or None once the sequence has ended; an empty page ends it as well. The fetch keeps whatever cursor the API uses (a snowflake, a timestamp, a URL) and must advance it only after a page has arrived, so that a fetch that raised is asked for the same page again. For an API whose cursor is one string, with_cursor keeps that state here.

    Paginator::next_page

    async fn[T, E : Error] Paginator::next_page(self : Paginator[T, E]) -> Array[T]? raise E

    Fetches the next non-empty page, or None once the sequence has ended. Items that a previous collect did not use come back first.

    Paginator::with_cursor

    fn[T, E : Error] Paginator::with_cursor(fetch : async (String?) -> Page[T] raise E) -> Paginator[T, E]

    Creates a paginator over a string cursor. The first fetch receives None; each page names the cursor of the following one, and a page without a cursor is the last. A fetch that raised leaves the cursor unchanged.

    RateLimitState

    pub(all) struct RateLimitState {
    remaining : Int
    reset_at_unix_ms : Int64
    } derive(Eq,
    Debug
    )

    Remaining requests and the Unix millisecond instant when a window resets.

    RateLimitState::equal

    Equality for rate-limit states.

    RateLimitState::not_equal

    fn RateLimitState::not_equal(x : RateLimitState, y : RateLimitState) -> Bool

    Equality for rate-limit states.

    RateLimitState::to_repr

    Debug representation of rate-limit states.

    RetryPolicy

    pub(all) struct RetryPolicy {
    max_retries : Int
    backoff : Backoff
    max_retry_after_ms : Int
    retry_non_idempotent : Bool
    } derive(Eq,
    Debug
    )

    Retry limits, backoff, Retry-After cap, and non-idempotent policy.

    RetryPolicy::default

    fn RetryPolicy::default() -> RetryPolicy

    Uses two retries, default backoff, a 60-second server-delay cap, and only retries non-idempotent requests when the failure proves they were not run.

    RetryPolicy::equal

    fn RetryPolicy::equal(RetryPolicy, RetryPolicy) -> Bool

    Equality for retry policy values.

    RetryPolicy::next_delay_ms

    fn RetryPolicy::next_delay_ms(self : RetryPolicy, error : SdkError, request :
    Request
    , attempt : Int, random : Double) -> Int?

    The policy's decision as a dot-callable method.

    RetryPolicy::none

    fn RetryPolicy::none() -> RetryPolicy

    Disables retries while retaining the other default settings.

    RetryPolicy::not_equal

    fn RetryPolicy::not_equal(x : RetryPolicy, y : RetryPolicy) -> Bool

    Equality for retry policy values.

    RetryPolicy::to_repr

    Debug representation of retry policy values.

    WindowLimiter

    pub struct WindowLimiter {
    // private fields
    }

    A mutable per-bucket fixed-window limiter driven by an injected clock.

    WindowLimiter::acquire

    async fn WindowLimiter::acquire(self : WindowLimiter, bucket : String, global_exempt~ : Bool) -> Unit

    Explicit fixed-window limiter methods.

    WindowLimiter::new

    Creates a limiter using an API-specific response-header parser.

    WindowLimiter::release

    async fn WindowLimiter::release(self : WindowLimiter, bucket : String, status~ : Int, headers~ :
    Headers
    ) -> Unit

    Explicit fixed-window limiter methods.

    WindowLimiter::state

    fn WindowLimiter::state(self : WindowLimiter, bucket : String) -> RateLimitState?

    Returns the currently recorded state for a bucket.

    classify

    fn classify(response :
    Response
    , now_unix_ms? : Int64) ->
    Response
    raise SdkError

    Returns successful responses unchanged and classifies all other statuses.

    test {
    let response : @http.Response = {
    status: 204,
    headers: @http.Headers::new(),
    body: b"",
    }
    assert_eq(@runtime.classify(response), response)
    let error = @runtime.Transport(@http.Timeout(500))
    assert_eq(error.status(), None)
    assert_true(error.is_retryable())
    }

    is_idempotent

    fn is_idempotent(request :
    Request
    ) -> Bool

    Reports whether the method is idempotent or an idempotency key is present.
    fn parse_link_next(headers :
    Headers
    ) -> String?

    Returns the target of the first RFC 8288 Link value whose relation includes next, across repeated headers and comma-separated link values.

    parse_retry_after

    fn parse_retry_after(value : String, now_unix_ms? : Int64) -> Int?

    Parses nonnegative seconds (including decimals) or an IMF-fixdate with a clock. Results are truncated to milliseconds and saturated at Int max.

    test {
    assert_eq(@runtime.parse_retry_after("1.2349"), Some(1234))
    }

    rate_limit_headers

    fn rate_limit_headers(remaining~ : String, reset_after_seconds? : String, reset_unix_seconds? : String) -> ((
    Headers
    , Int64) -> RateLimitState?)

    Builds a parser for conventional remaining/reset response headers.

    retry_after_ms

    fn retry_after_ms(headers :
    Headers
    , now_unix_ms? : Int64) -> Int?

    Prefers the first retry-after-ms value; falls back to retry-after when that header is absent or unparseable, as the official OpenAI SDKs do.