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:
1 parent
4b307b55b9
commit
19f9c7ace7
24 files changed
+2343
-1819
No files matched your search
+14
-19
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user