coven_database/
database_local_bytes.rs1use super::{Database, Mode};
5use crate::{sqlite::DatabaseConnection, DbError, LiveQuery};
6use coven_foundation::task::CarriesBug;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct NamespaceBytes {
11 pub namespace: String,
13 pub budget: u64,
15 pub cached: u64,
17 pub pinned: u64,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct CacheBytes {
25 pub namespaces: Vec<NamespaceBytes>,
27 pub pinning: u64,
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct LocalBytes {
34 pub namespaces: Vec<NamespaceBytes>,
36 pub pinning: u64,
38 pub library: u64,
42 pub before_reset: Option<u64>,
44}
45
46fn unsigned(value: i64) -> Result<u64, DbError> {
47 u64::try_from(value).map_err(|_| DbError::DamagedDatabase)
48}
49
50#[expect(
52 clippy::map_err_ignore,
53 reason = "a stored value that won't convert or decode is DamagedDatabase"
54)]
55pub(crate) fn budgets(db: &DatabaseConnection) -> Result<Vec<NamespaceBytes>, DbError> {
56 db.query(
57 "SELECT namespace,bytes,cached,pinned FROM _coven_cache_budgets ORDER BY namespace",
58 [],
59 |r| {
60 Ok((
61 r.get::<_, String>(0)?,
62 r.get::<_, Vec<u8>>(1)?,
63 r.get::<_, i64>(2)?,
64 r.get::<_, i64>(3)?,
65 ))
66 },
67 )?
68 .into_iter()
69 .map(|(namespace, budget, cached, pinned)| {
70 Ok(NamespaceBytes {
71 namespace,
72 budget: u64::from_be_bytes(budget.try_into().map_err(|_| DbError::DamagedDatabase)?),
73 cached: unsigned(cached)?,
74 pinned: unsigned(pinned)?,
75 })
76 })
77 .collect()
78}
79
80pub(crate) fn partials(db: &DatabaseConnection) -> Result<u64, DbError> {
82 unsigned(db.query_row("SELECT coalesce(sum(size),0) FROM _coven_cache_partials", [], |r| {
83 r.get::<_, i64>(0)
84 })?)
85}
86
87impl<M: Mode> Database<M> {
88 pub async fn cache_bytes(&self) -> Result<CacheBytes, DbError> {
91 let declared: Vec<String> = self
92 .call(|open| {
93 Ok::<_, DbError>(
94 M::access(open)
95 .write_schema
96 .namespaces
97 .iter()
98 .map(|namespace| namespace.name.clone())
99 .collect(),
100 )
101 })
102 .await?;
103 let (mut kept, pinning) = self.read(|sql| sql.cache_bytes()).await?;
104 let namespaces = declared
105 .into_iter()
106 .filter_map(|name| {
107 kept.iter().position(|bytes| bytes.namespace == name).map(|at| kept.swap_remove(at))
108 })
109 .collect();
110 Ok(CacheBytes { namespaces, pinning })
111 }
112}
113
114impl Database {
115 pub fn subscribe_cache_budgets<E>(&self) -> LiveQuery<Vec<NamespaceBytes>, E>
119 where
120 E: From<DbError> + CarriesBug + Send + 'static,
121 {
122 self.subscribe(|sql| sql.cache_budgets().map_err(E::from))
123 }
124}
125
126impl Database {
127 pub async fn local_bytes(&self) -> Result<LocalBytes, DbError> {
132 let cache = self.cache_bytes().await?;
133 let directory = self.call(|open| Ok::<_, DbError>(open.access.directory.clone())).await?;
134 let walked = coven_foundation::task::blocking("measure the store's directory", move || {
135 directory.local_bytes()
136 })
137 .await??;
138 Ok(LocalBytes {
139 namespaces: cache.namespaces,
140 pinning: cache.pinning,
141 library: walked.library,
142 before_reset: walked.archives,
143 })
144 }
145
146 pub fn subscribe_local_bytes(&self) -> LocalBytesWatch {
151 let mut changed = self.writer.local_bytes_changes.subscribe();
152 changed.mark_unchanged();
153 LocalBytesWatch { database: self.clone(), budgets: self.subscribe_cache_budgets(), changed }
154 }
155
156 pub fn local_bytes_changed(&self) {
159 self.writer.local_bytes_changes.send_modify(|count| *count += 1);
160 }
161}
162
163pub struct LocalBytesWatch {
165 database: Database,
166 budgets: LiveQuery<Vec<NamespaceBytes>, DbError>,
167 changed: tokio::sync::watch::Receiver<u64>,
168}
169
170impl LocalBytesWatch {
171 pub async fn next(&mut self) -> Option<Result<LocalBytes, DbError>> {
175 tokio::select! {
176 run = self.budgets.next() => match run? {
177 Ok(_) => {}
178 Err(error) => return Some(Err(error)),
179 },
180 changed = self.changed.changed() => changed.ok()?,
181 }
182 Some(self.database.local_bytes().await)
183 }
184
185 pub fn cancel(self) {}
187}