agentmux_srv/backend/storage/
widget_storage.rs1use rusqlite::{params, OptionalExtension};
10
11use super::error::StoreError;
12use super::store::Store;
13
14impl Store {
15 pub fn widget_storage_get(&self, widget_id: &str, key: &str) -> Result<Option<String>, StoreError> {
16 let conn = self.conn.lock().unwrap();
17 Ok(conn
18 .query_row(
19 "SELECT value FROM db_widget_storage WHERE widget_id = ?1 AND key = ?2",
20 params![widget_id, key],
21 |row| row.get(0),
22 )
23 .optional()?)
24 }
25
26 pub fn widget_storage_set(&self, widget_id: &str, key: &str, value: &str, max_total: usize) -> Result<bool, StoreError> {
29 let mut conn = self.conn.lock().unwrap();
30 let tx = conn.transaction()?;
31 let others: i64 = tx.query_row(
32 "SELECT COALESCE(SUM(length(CAST(key AS BLOB)) + length(CAST(value AS BLOB))), 0)
33 FROM db_widget_storage WHERE widget_id = ?1 AND key <> ?2",
34 params![widget_id, key],
35 |row| row.get(0),
36 )?;
37 if others as usize + key.len() + value.len() > max_total {
38 return Ok(false);
39 }
40 tx.execute(
41 "INSERT OR REPLACE INTO db_widget_storage (widget_id, key, value) VALUES (?1, ?2, ?3)",
42 params![widget_id, key, value],
43 )?;
44 tx.commit()?;
45 Ok(true)
46 }
47
48 pub fn widget_storage_delete(&self, widget_id: &str, key: &str) -> Result<(), StoreError> {
49 let conn = self.conn.lock().unwrap();
50 conn.execute("DELETE FROM db_widget_storage WHERE widget_id = ?1 AND key = ?2", params![widget_id, key])?;
51 Ok(())
52 }
53
54 pub fn widget_storage_keys(&self, widget_id: &str, prefix: &str) -> Result<Vec<String>, StoreError> {
56 let conn = self.conn.lock().unwrap();
57 let mut stmt = conn.prepare(
58 "SELECT key FROM db_widget_storage WHERE widget_id = ?1 AND substr(key, 1, length(?2)) = ?2 ORDER BY key",
59 )?;
60 let keys = stmt.query_map(params![widget_id, prefix], |row| row.get(0))?.collect::<Result<Vec<String>, _>>()?;
61 Ok(keys)
62 }
63
64 pub fn widget_storage_purge(&self, widget_id: &str) -> Result<(), StoreError> {
66 let conn = self.conn.lock().unwrap();
67 conn.execute("DELETE FROM db_widget_storage WHERE widget_id = ?1", params![widget_id])?;
68 Ok(())
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use std::path::Path;
75
76 use super::*;
77
78 #[test]
79 fn keeps_each_package_apart_within_its_budget() {
80 let store = Store::open(Path::new(":memory:")).unwrap();
81 assert!(store.widget_storage_set("acme.a", "note:1", "\"hi\"", 100).unwrap());
82 assert!(store.widget_storage_set("acme.a", "note:2", "\"yo\"", 100).unwrap());
83 assert!(store.widget_storage_set("acme.b", "note:1", "\"other\"", 100).unwrap());
84 assert_eq!(store.widget_storage_get("acme.a", "note:1").unwrap().as_deref(), Some("\"hi\""));
85 assert_eq!(store.widget_storage_keys("acme.a", "note:").unwrap(), vec!["note:1", "note:2"]);
86 assert!(store.widget_storage_keys("acme.a", "x").unwrap().is_empty());
87 assert!(!store.widget_storage_set("acme.a", "big", &"x".repeat(90), 100).unwrap());
89 assert_eq!(store.widget_storage_get("acme.a", "big").unwrap(), None);
90 assert!(store.widget_storage_set("acme.a", "note:1", &"y".repeat(70), 100).unwrap());
92 store.widget_storage_delete("acme.a", "note:2").unwrap();
93 assert_eq!(store.widget_storage_keys("acme.a", "").unwrap(), vec!["note:1"]);
94 store.widget_storage_purge("acme.a").unwrap();
95 assert!(store.widget_storage_keys("acme.a", "").unwrap().is_empty());
96 assert_eq!(store.widget_storage_get("acme.b", "note:1").unwrap().as_deref(), Some("\"other\""));
97 }
98}