use std::path::Path;
use rusqlite::Connection;
use thiserror::Error;
use crate::events::SessionId;
pub const DELIVERY_HORIZON: u64 = 40;
#[derive(Debug, Error)]
pub enum DeliveryError {
#[error("delivery index storage error: {0}")]
Storage(#[from] rusqlite::Error),
#[error("delivery index schema error: {0}")]
Schema(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Delivery {
Full,
Pointer,
}
#[derive(Debug)]
pub struct DeliveryRequest<'a> {
pub session: &'a SessionId,
pub cwd: &'a str,
pub slice_key: &'a str,
pub complete: bool,
}
pub struct DeliveryIndex {
conn: Connection,
}
impl DeliveryIndex {
pub fn open(path: impl AsRef<Path>) -> Result<Self, DeliveryError> {
let conn = Connection::open(path)?;
crate::events::apply_migrations(&conn)
.map_err(|error| DeliveryError::Schema(error.to_string()))?;
Ok(Self { conn })
}
pub fn decide(&self, request: &DeliveryRequest<'_>) -> Result<Delivery, DeliveryError> {
let scope = (request.session.as_str(), request.cwd);
self.conn.execute(
"INSERT INTO delivery_counters (session, cwd, emissions) VALUES (?1, ?2, 1)
ON CONFLICT (session, cwd) DO UPDATE SET emissions = emissions + 1",
scope,
)?;
let emission: u64 = self.conn.query_row(
"SELECT emissions FROM delivery_counters WHERE session = ?1 AND cwd = ?2",
scope,
|row| row.get(0),
)?;
let delivered_at: Option<u64> = self
.conn
.query_row(
"SELECT delivered_at_emission FROM delivered_slices
WHERE session = ?1 AND cwd = ?2 AND slice_key = ?3",
(scope.0, scope.1, request.slice_key),
|row| row.get(0),
)
.map(Some)
.or_else(ignore_missing_row)?;
if let Some(at) = delivered_at {
if emission.saturating_sub(at) < DELIVERY_HORIZON {
return Ok(Delivery::Pointer);
}
}
if request.complete {
self.conn.execute(
"INSERT INTO delivered_slices (session, cwd, slice_key, delivered_at_emission)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT (session, cwd, slice_key)
DO UPDATE SET delivered_at_emission = ?4",
(scope.0, scope.1, request.slice_key, emission),
)?;
}
Ok(Delivery::Full)
}
pub fn clear(&self, session: &SessionId, cwd: &str) -> Result<(), DeliveryError> {
let scope = (session.as_str(), cwd);
self.conn.execute(
"DELETE FROM delivered_slices WHERE session = ?1 AND cwd = ?2",
scope,
)?;
self.conn.execute(
"DELETE FROM delivery_counters WHERE session = ?1 AND cwd = ?2",
scope,
)?;
Ok(())
}
}
fn ignore_missing_row(error: rusqlite::Error) -> Result<Option<u64>, rusqlite::Error> {
if matches!(error, rusqlite::Error::QueryReturnedNoRows) {
Ok(None)
} else {
Err(error)
}
}