Skip to main content

pond/
lib.rs

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