Skip to main content

pond/
lib.rs

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