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    /// Stderr counterpart to [`line`]. Per rust-cli/book stdout-vs-stderr
108    /// discipline, anything that is not "the result" (progress, prompts,
109    /// disclaimers, hints, interim status) belongs here so piping stdout to a
110    /// file or another command yields the machine-readable view alone.
111    pub fn line_err(message: &str) -> anyhow::Result<()> {
112        let mut stderr = io::stderr().lock();
113        writeln!(stderr, "{message}").context("failed to write command meta")
114    }
115}
116
117#[derive(Debug, thiserror::Error)]
118pub enum Error {
119    #[error("validation failed: {message}")]
120    Validation {
121        message: String,
122        field: Option<String>,
123        value: Option<Value>,
124        expected: Option<String>,
125    },
126    #[error("not found: {message}")]
127    NotFound {
128        message: String,
129        kind: String,
130        pk: Value,
131    },
132    #[error("namespace unknown: {namespace}")]
133    NamespaceUnknown { namespace: String },
134    #[error("commit conflict after {attempts} attempt(s)")]
135    Conflict { attempts: u8 },
136    #[error("storage unavailable: {0}")]
137    Storage(#[from] anyhow::Error),
138    #[error("internal error: {0}")]
139    Internal(String),
140}
141
142impl Error {
143    pub fn validation(message: impl Into<String>) -> Self {
144        Self::Validation {
145            message: message.into(),
146            field: None,
147            value: None,
148            expected: None,
149        }
150    }
151
152    pub fn validation_field(
153        message: impl Into<String>,
154        field: impl Into<String>,
155        value: Option<Value>,
156        expected: Option<String>,
157    ) -> Self {
158        Self::Validation {
159            message: message.into(),
160            field: Some(field.into()),
161            value,
162            expected,
163        }
164    }
165
166    pub fn not_found(kind: impl Into<String>, pk: Value, message: impl Into<String>) -> Self {
167        Self::NotFound {
168            message: message.into(),
169            kind: kind.into(),
170            pk,
171        }
172    }
173
174    pub fn namespace_unknown(namespace: impl Into<String>) -> Self {
175        Self::NamespaceUnknown {
176            namespace: namespace.into(),
177        }
178    }
179
180    pub fn conflict(attempts: u8) -> Self {
181        Self::Conflict { attempts }
182    }
183
184    pub fn internal(message: impl Into<String>) -> Self {
185        Self::Internal(message.into())
186    }
187}