Skip to main content

pond/
lib.rs

1pub mod adapter;
2pub mod config;
3pub mod embed;
4pub mod handlers;
5pub mod sessions;
6pub mod substrate;
7pub mod transport;
8pub mod wire;
9
10pub const PROTOCOL_VERSION: u16 = 1;
11
12use std::time::Duration;
13
14use chrono::{DateTime, Utc};
15use serde_json::Value;
16
17pub trait Clock: Send + Sync {
18    fn now(&self) -> DateTime<Utc>;
19}
20
21#[derive(Debug, Clone, Copy, Default)]
22pub struct SystemClock;
23
24impl Clock for SystemClock {
25    fn now(&self) -> DateTime<Utc> {
26        Utc::now()
27    }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct RetryPolicy {
32    pub attempts: u8,
33    pub initial_backoff: Duration,
34    pub max_backoff: Duration,
35    /// Symmetric jitter factor applied to the exponential backoff: the sleep
36    /// is multiplied by `1.0 + jitter * uniform(-1.0, 1.0)` before clamping
37    /// to `max_backoff`. De-correlates concurrent retriers on a contended
38    /// Lance manifest (spec.md#lance-retry-jitter).
39    pub jitter: f64,
40}
41
42impl Default for RetryPolicy {
43    fn default() -> Self {
44        Self {
45            attempts: 3,
46            initial_backoff: Duration::from_millis(300),
47            max_backoff: Duration::from_secs(5),
48            jitter: 0.2,
49        }
50    }
51}
52
53pub mod output {
54    use std::{
55        io::{self, IsTerminal, Write},
56        sync::OnceLock,
57    };
58
59    use anstyle::{AnsiColor, Style};
60    use anyhow::Context;
61
62    /// Whether the user-facing CLI surface should emit ANSI styling. Honors
63    /// `NO_COLOR` (no-color.org) and falls back to plain text when stdout is
64    /// not a TTY (piped to a file, captured by tests, ...). Cached so per-call
65    /// overhead is a single pointer load.
66    fn use_color() -> bool {
67        static USE: OnceLock<bool> = OnceLock::new();
68        *USE.get_or_init(|| std::env::var_os("NO_COLOR").is_none() && io::stdout().is_terminal())
69    }
70
71    /// Wrap `text` in `style`'s SGR sequence when color is enabled; return the
72    /// raw text otherwise. The caller writes the result to stdout via
73    /// [`line`].
74    pub fn paint(text: &str, style: Style) -> String {
75        if use_color() {
76            format!("{}{text}{}", style.render(), style.render_reset())
77        } else {
78            text.to_owned()
79        }
80    }
81
82    pub fn bold() -> Style {
83        Style::new().bold()
84    }
85    pub fn dim() -> Style {
86        Style::new().dimmed()
87    }
88    pub fn green() -> Style {
89        Style::new().fg_color(Some(AnsiColor::Green.into()))
90    }
91    pub fn yellow() -> Style {
92        Style::new().fg_color(Some(AnsiColor::Yellow.into()))
93    }
94    pub fn red() -> Style {
95        Style::new().fg_color(Some(AnsiColor::Red.into()))
96    }
97    pub fn cyan() -> Style {
98        Style::new().fg_color(Some(AnsiColor::Cyan.into()))
99    }
100
101    #[allow(clippy::print_stdout)]
102    pub fn line(message: &str) -> anyhow::Result<()> {
103        let mut stdout = io::stdout().lock();
104        writeln!(stdout, "{message}").context("failed to write command output")
105    }
106}
107
108#[derive(Debug, thiserror::Error)]
109pub enum Error {
110    #[error("validation failed: {message}")]
111    Validation {
112        message: String,
113        field: Option<String>,
114        value: Option<Value>,
115        expected: Option<String>,
116    },
117    #[error("not found: {message}")]
118    NotFound {
119        message: String,
120        kind: String,
121        pk: Value,
122    },
123    #[error("namespace unknown: {namespace}")]
124    NamespaceUnknown { namespace: String },
125    #[error("commit conflict after {attempts} attempt(s)")]
126    Conflict { attempts: u8 },
127    #[error("storage unavailable: {0}")]
128    Storage(#[from] anyhow::Error),
129    #[error("internal error: {0}")]
130    Internal(String),
131}
132
133impl Error {
134    pub fn validation(message: impl Into<String>) -> Self {
135        Self::Validation {
136            message: message.into(),
137            field: None,
138            value: None,
139            expected: None,
140        }
141    }
142
143    pub fn validation_field(
144        message: impl Into<String>,
145        field: impl Into<String>,
146        value: Option<Value>,
147        expected: Option<String>,
148    ) -> Self {
149        Self::Validation {
150            message: message.into(),
151            field: Some(field.into()),
152            value,
153            expected,
154        }
155    }
156
157    pub fn not_found(kind: impl Into<String>, pk: Value, message: impl Into<String>) -> Self {
158        Self::NotFound {
159            message: message.into(),
160            kind: kind.into(),
161            pk,
162        }
163    }
164
165    pub fn namespace_unknown(namespace: impl Into<String>) -> Self {
166        Self::NamespaceUnknown {
167            namespace: namespace.into(),
168        }
169    }
170
171    pub fn conflict(attempts: u8) -> Self {
172        Self::Conflict { attempts }
173    }
174
175    pub fn internal(message: impl Into<String>) -> Self {
176        Self::Internal(message.into())
177    }
178}