feat(dashboard): extract inline script to standalone dashboard.ts/js, add UI build/check recipes, and add route unit tests

This commit is contained in:
Riz Ashraf committed 2026-10-05 12:48:36 +01:00
1 parent 4b307b55b9
commit 19f9c7ace7
24 files changed
+2343 -1819

No files matched your search

+14 -19
View File
@@ -6,6 +6,7 @@ pub const STORE_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("stor
pub struct Store<T> {
pub cache: Arc<RwLock<T>>,
pub flushed: Arc<tokio::sync::Notify>,
tx: tokio::sync::mpsc::Sender<()>,
}
@@ -13,21 +14,22 @@ impl<T: DeserializeOwned + Default + Serialize + Clone + Send + Sync + 'static>
pub fn new(key: &str, db: Arc<Database>) -> Self {
let initial_data = Self::load_from_db(key, &db);
let cache = Arc::new(RwLock::new(initial_data));
let flushed = Arc::new(tokio::sync::Notify::new());
let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(1);
let db_clone = db.clone();
let key_clone = key.to_string();
let cache_clone = cache.clone();
let flushed_clone = flushed.clone();
tokio::spawn(async move {
while rx.recv().await.is_some() {
// Debounce window: wait 150ms to batch rapid sequential mutations
tokio::time::sleep(tokio::time::Duration::from_millis(150)).await;
// Drain any pending notifications accumulated during the debounce window
// Drain any pending notifications accumulated
while rx.try_recv().is_ok() {}
let db_inner = db_clone.clone();
let key_inner = key_clone.clone();
let flushed_inner = flushed_clone.clone();
let json_data = {
let lock = cache_clone.read().unwrap_or_else(|e| e.into_inner());
serde_json::to_vec(&*lock)
@@ -37,7 +39,7 @@ impl<T: DeserializeOwned + Default + Serialize + Clone + Send + Sync + 'static>
if let Some(json_data) = json_data {
let _ = tokio::task::spawn_blocking(move || {
// Retry up to 10 times with 30ms backoff if another Store holds write transaction
// Retry up to 10 times if another Store holds write transaction
for _ in 0..10 {
match db_inner.begin_write() {
Ok(write_txn) => {
@@ -48,17 +50,18 @@ impl<T: DeserializeOwned + Default + Serialize + Clone + Send + Sync + 'static>
break;
}
Err(_) => {
std::thread::sleep(std::time::Duration::from_millis(30));
std::thread::yield_now();
}
}
}
})
.await;
flushed_inner.notify_waiters();
}
}
});
Self { cache, tx }
Self { cache, flushed, tx }
}
fn load_from_db(key: &str, db: &Database) -> T {
@@ -123,18 +126,10 @@ mod tests {
data.value = 42;
});
// Wait and poll for persistence completion
let mut store2 = None;
for _ in 0..20 {
let s = Store::<TestData>::new("test_key", db.clone());
if s.read_with(|data| data.value) == 42 {
store2 = Some(s);
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}
// Event-driven wait for persistence completion
store.flushed.notified().await;
let store2 = store2.expect("Timed out waiting for async store persistence");
let store2 = Store::<TestData>::new("test_key", db.clone());
assert_eq!(
store2.read_with(|s| s.clone()),
TestData {
@@ -172,8 +167,8 @@ mod tests {
h.await.unwrap();
}
// Wait for all blocking writes to flush
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Event-driven wait for blocking writes to flush
store.flushed.notified().await;
assert_eq!(store.read_with(|s| s.value), 50);
}