1use std::path::Path;
11
12use rusqlite::Connection;
13use thiserror::Error;
14
15use crate::events::SessionId;
16
17pub const DELIVERY_HORIZON: u64 = 40;
20
21#[derive(Debug, Error)]
22pub enum DeliveryError {
23 #[error("delivery index storage error: {0}")]
24 Storage(#[from] rusqlite::Error),
25 #[error("delivery index schema error: {0}")]
26 Schema(String),
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Delivery {
32 Full,
35 Pointer,
37}
38
39#[derive(Debug)]
43pub struct DeliveryRequest<'a> {
44 pub session: &'a SessionId,
45 pub cwd: &'a str,
46 pub slice_key: &'a str,
47 pub complete: bool,
48}
49
50pub struct DeliveryIndex {
51 conn: Connection,
52}
53
54impl DeliveryIndex {
55 pub fn open(path: impl AsRef<Path>) -> Result<Self, DeliveryError> {
61 let conn = Connection::open(path)?;
62 crate::events::apply_migrations(&conn)
63 .map_err(|error| DeliveryError::Schema(error.to_string()))?;
64 Ok(Self { conn })
65 }
66
67 pub fn decide(&self, request: &DeliveryRequest<'_>) -> Result<Delivery, DeliveryError> {
73 let scope = (request.session.as_str(), request.cwd);
74 self.conn.execute(
75 "INSERT INTO delivery_counters (session, cwd, emissions) VALUES (?1, ?2, 1)
76 ON CONFLICT (session, cwd) DO UPDATE SET emissions = emissions + 1",
77 scope,
78 )?;
79 let emission: u64 = self.conn.query_row(
80 "SELECT emissions FROM delivery_counters WHERE session = ?1 AND cwd = ?2",
81 scope,
82 |row| row.get(0),
83 )?;
84 let delivered_at: Option<u64> = self
85 .conn
86 .query_row(
87 "SELECT delivered_at_emission FROM delivered_slices
88 WHERE session = ?1 AND cwd = ?2 AND slice_key = ?3",
89 (scope.0, scope.1, request.slice_key),
90 |row| row.get(0),
91 )
92 .map(Some)
93 .or_else(ignore_missing_row)?;
94 if let Some(at) = delivered_at {
95 if emission.saturating_sub(at) < DELIVERY_HORIZON {
96 return Ok(Delivery::Pointer);
97 }
98 }
99 if request.complete {
100 self.conn.execute(
101 "INSERT INTO delivered_slices (session, cwd, slice_key, delivered_at_emission)
102 VALUES (?1, ?2, ?3, ?4)
103 ON CONFLICT (session, cwd, slice_key)
104 DO UPDATE SET delivered_at_emission = ?4",
105 (scope.0, scope.1, request.slice_key, emission),
106 )?;
107 }
108 Ok(Delivery::Full)
109 }
110
111 pub fn clear(&self, session: &SessionId, cwd: &str) -> Result<(), DeliveryError> {
117 let scope = (session.as_str(), cwd);
118 self.conn.execute(
119 "DELETE FROM delivered_slices WHERE session = ?1 AND cwd = ?2",
120 scope,
121 )?;
122 self.conn.execute(
123 "DELETE FROM delivery_counters WHERE session = ?1 AND cwd = ?2",
124 scope,
125 )?;
126 Ok(())
127 }
128}
129
130fn ignore_missing_row(error: rusqlite::Error) -> Result<Option<u64>, rusqlite::Error> {
132 if matches!(error, rusqlite::Error::QueryReturnedNoRows) {
133 Ok(None)
134 } else {
135 Err(error)
136 }
137}