///|
/// Wraps a transport so that every request carries a deadline.
///
/// The library never performs I/O itself, so it cannot enforce a timeout on
/// its own: cancelling a socket read is a property of the HTTP stack, not of
/// the request. This wrapper makes the promise explicit instead. It stamps
/// `x-chroma-timeout-ms` on each request and hands it to the inner transport,
/// so a real transport can honour the value; a transport that ignores the
/// header still works, it just loses the guarantee. The bundled async
/// transport does honour it (see `AsyncHttpTransport`).
///
/// ```text
/// let client = ChromaClient::new(TimeoutTransport::new(http_stack, 5000))
/// ```
pub(all) struct TimeoutTransport[T] {
inner : T
timeout_ms : Int32
}
///|
/// Wraps `inner`, limiting each request to `timeout_ms` milliseconds.
///
/// A non-positive value means "no deadline", in which case only the header
/// stamping is skipped and requests pass through untouched.
pub fn[T] TimeoutTransport::new(
inner : T,
timeout_ms : Int32,
) -> TimeoutTransport[T] {
{ inner, timeout_ms, }
}
///|
/// Builds a timeout transport from a client's own configuration, so
/// `Config::timeout_ms` gets a consumer.
pub fn[T] TimeoutTransport::from_config(
inner : T,
config : Config,
) -> TimeoutTransport[T] {
{ inner, timeout_ms: config.timeout_ms, }
}
///|
/// The header a transport reads to learn its deadline.
pub const TIMEOUT_HEADER : String = "x-chroma-timeout-ms"
///|
pub impl[T : Transport] Transport for TimeoutTransport[T] with fn send(
self,
request,
) {
if self.timeout_ms <= 0 {
return self.inner.send(request)
}
let headers = request.headers.copy()
headers[TIMEOUT_HEADER] = self.timeout_ms.to_string()
self.inner.send({ ..request, headers, })
}
///|
pub extend TimeoutTransport with Transport::{send}
///|
/// The deadline a request asks for, if it carries one.
///
/// Both the header and an explicit value are read by `AsyncTransport`, so the
/// parsing lives here next to the header's definition rather than in every
/// transport. A malformed value is treated as "no deadline": the header is an
/// internal contract, and a garbage one means the wrapper was misused rather
/// than that the caller wants a zero deadline.
pub fn request_timeout_ms(request : Request) -> Int32? {
match request.headers.get(TIMEOUT_HEADER) {
Some(value) => parse_delay_ms(value)
None => None
}
}
///|
/// The wait a `RetryTransport` asks for before a re-send, if any. Same rules
/// as `request_timeout_ms`.
pub fn request_retry_delay_ms(request : Request) -> Int32? {
match request.headers.get(RETRY_DELAY_HEADER) {
Some(value) => parse_delay_ms(value)
None => None
}
}
///|
/// Reads a non-negative millisecond count; `None` when the value is not one.
fn parse_delay_ms(value : String) -> Int32? {
let ms = @string.parse_int(value) catch { _ => return None }
if ms > 0 {
Some(ms)
} else {
None
}
}
///|
/// Strips the two client-internal headers from a request.
///
/// They are addressed to the transport, not to Chroma, and a server is free to
/// reject an unknown header (some proxies answer 400). Every real transport
/// sends the stripped copy.
pub fn without_internal_headers(request : Request) -> Request {
let headers = request.headers.copy()
ignore(headers.remove(TIMEOUT_HEADER))
ignore(headers.remove(RETRY_DELAY_HEADER))
{ ..request, headers, }
}