app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 028fb8f50c54fe717e8bb24bbb56a53e9ff4c9e3
parent 7f2afb5b635dea2ed63e697a37ddc3674a3a00e7
Author: triesap <triesap@radroots.dev>
Date:   Mon, 19 Jan 2026 23:46:12 +0000

app: flush buffered logs after init

- add log buffer flush helper with retention pruning

- flush buffered entries after backends initialize

- emit debug logs for init stage transitions

- add unit test coverage for buffer flush behavior

Diffstat:
Mapp/src/app.rs | 32++++++++++++++++++++++++++------
Mapp/src/lib.rs | 1+
Mapp/src/logging.rs | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++----
3 files changed, 82 insertions(+), 10 deletions(-)

diff --git a/app/src/app.rs b/app/src/app.rs @@ -14,6 +14,8 @@ use crate::{ app_init_stage_set, app_init_total_add, app_init_total_unknown, + app_log_buffer_flush, + app_log_debug_emit, app_log_error_emit, app_log_error_store, app_config_default, @@ -58,6 +60,10 @@ fn active_key_label(value: Option<String>) -> String { format!("{head}...{tail}") } +fn log_init_stage(stage: AppInitStage) { + let _ = app_log_debug_emit("log.app.init.stage", stage.as_str(), None); +} + fn spawn_health_checks( config: AppConfig, health_report: RwSignal<AppHealthReport, LocalStorage>, @@ -108,7 +114,9 @@ fn HomePage() -> impl IntoView { provide_context(init_state); Effect::new(move || { spawn_local(async move { - init_state.update(|state| app_init_stage_set(state, AppInitStage::Storage)); + let stage = AppInitStage::Storage; + init_state.update(|state| app_init_stage_set(state, stage)); + log_init_stage(stage); let config = app_config_default(); if !app_init_has_completed() { init_state.update(|state| { @@ -117,7 +125,10 @@ fn HomePage() -> impl IntoView { }); let assets_result = app_init_assets( &config, - |stage| init_state.update(|state| app_init_stage_set(state, stage)), + |stage| { + init_state.update(|state| app_init_stage_set(state, stage)); + log_init_stage(stage); + }, |loaded, total| { init_state.update(|state| { app_init_progress_add(state, loaded); @@ -133,21 +144,30 @@ fn HomePage() -> impl IntoView { let init_err = AppInitError::Assets(err); let _ = app_log_error_emit(&init_err); init_error.set(Some(init_err)); - init_state.update(|state| app_init_stage_set(state, AppInitStage::Error)); + let stage = AppInitStage::Error; + init_state.update(|state| app_init_stage_set(state, stage)); + log_init_stage(stage); return; } - init_state.update(|state| app_init_stage_set(state, AppInitStage::Storage)); + let stage = AppInitStage::Storage; + init_state.update(|state| app_init_stage_set(state, stage)); + log_init_stage(stage); } match app_init_backends(config).await { Ok(value) => { + let _ = app_log_buffer_flush(&value.datastore, &value.config.datastore.key_maps).await; backends.set(Some(value)); app_init_mark_completed(); - init_state.update(|state| app_init_stage_set(state, AppInitStage::Ready)); + let stage = AppInitStage::Ready; + init_state.update(|state| app_init_stage_set(state, stage)); + log_init_stage(stage); } Err(err) => { let _ = app_log_error_emit(&err); init_error.set(Some(err)); - init_state.update(|state| app_init_stage_set(state, AppInitStage::Error)); + let stage = AppInitStage::Error; + init_state.update(|state| app_init_stage_set(state, stage)); + log_init_stage(stage); } } }) diff --git a/app/src/lib.rs b/app/src/lib.rs @@ -54,6 +54,7 @@ pub use logging::{ app_log_entry_record, app_log_entry_store, app_log_buffer_drain, + app_log_buffer_flush, app_log_buffer_push, app_log_entries_dump, app_log_entries_load, diff --git a/app/src/logging.rs b/app/src/logging.rs @@ -329,6 +329,27 @@ pub fn app_log_buffer_drain() -> Vec<AppLogEntry> { entries.drain(..).collect() } +pub async fn app_log_buffer_flush<T: RadrootsClientDatastore>( + datastore: &T, + key_maps: &AppKeyMapConfig, +) -> AppLogResult<usize> { + let entries = app_log_buffer_drain(); + let mut stored = 0; + let mut iter = entries.into_iter(); + while let Some(entry) = iter.next() { + if let Err(err) = app_log_entry_store(datastore, key_maps, &entry).await { + app_log_buffer_push(entry); + for remaining in iter { + app_log_buffer_push(remaining); + } + return Err(err); + } + stored += 1; + } + let _ = app_log_entries_prune(datastore, key_maps, APP_LOG_MAX_ENTRIES).await?; + Ok(stored) +} + pub fn app_log_entry_prefix(key_maps: &AppKeyMapConfig) -> AppLogResult<String> { let param = app_datastore_param_key(key_maps, "log_entry")?; Ok(param("")) @@ -450,6 +471,7 @@ mod tests { app_log_entry_key, app_log_entry_prefix, app_log_buffer_drain, + app_log_buffer_flush, app_log_buffer_push, app_log_metadata, app_log_timestamp_ms, @@ -475,6 +497,8 @@ mod tests { use serde::{de::DeserializeOwned, Serialize}; use std::sync::Mutex; + static LOG_TEST_LOCK: Mutex<()> = Mutex::new(()); + struct TestDatastore { entries: Mutex<Vec<RadrootsClientDatastoreEntry>>, } @@ -515,13 +539,21 @@ mod tests { async fn set_obj<T>( &self, - _key: &str, - _value: &T, + key: &str, + value: &T, ) -> RadrootsClientDatastoreResult<T> where T: Serialize + DeserializeOwned + Clone, { - Err(RadrootsClientDatastoreError::IdbUndefined) + let encoded = serde_json::to_string(value) + .map_err(|_| RadrootsClientDatastoreError::NoResult)?; + let mut entries = self.entries.lock().unwrap_or_else(|err| err.into_inner()); + entries.retain(|entry| entry.key != key); + entries.push(RadrootsClientDatastoreEntry::new( + key.to_string(), + Some(encoded), + )); + Ok(value.clone()) } async fn update_obj<T>( @@ -653,6 +685,7 @@ mod tests { #[test] fn log_buffer_drains_entries() { + let _guard = LOG_TEST_LOCK.lock().unwrap_or_else(|err| err.into_inner()); let _ = app_log_buffer_drain(); let entry = app_log_entry_new(AppLogLevel::Debug, "log.code.test", "buf", None); app_log_buffer_push(entry.clone()); @@ -737,8 +770,26 @@ mod tests { let datastore = TestDatastore::new(stored); let removed = futures::executor::block_on(app_log_entries_prune(&datastore, &key_maps, 2)) - .expect("prune"); + .expect("prune"); assert_eq!(removed, 1); assert_eq!(datastore.len(), 2); } + + #[test] + fn log_buffer_flush_stores_entries() { + let _guard = LOG_TEST_LOCK.lock().unwrap_or_else(|err| err.into_inner()); + let _ = app_log_buffer_drain(); + let key_maps = app_key_maps_default(); + let datastore = TestDatastore::new(Vec::new()); + app_log_buffer_push(app_log_entry_new( + AppLogLevel::Info, + "log.code.flush", + "flush", + None, + )); + let stored = futures::executor::block_on(app_log_buffer_flush(&datastore, &key_maps)) + .expect("flush"); + assert_eq!(stored, 1); + assert_eq!(datastore.len(), 1); + } }