commit 16aae96c8af9e26af2ae6e19230161091dc95cad
parent 7c712692c252ba974209d0c41593e43395cdb568
Author: triesap <triesap@radroots.dev>
Date: Wed, 21 Jan 2026 18:15:55 +0000
app: add explicit state create and update helpers
- add create and update APIs for app state records
- prevent implicit creation when updating state data
- switch notification updates to the update helper
- extend bootstrap tests for create/update paths
Diffstat:
2 files changed, 91 insertions(+), 2 deletions(-)
diff --git a/app/src/bootstrap.rs b/app/src/bootstrap.rs
@@ -66,11 +66,36 @@ pub async fn app_datastore_write_state<T: RadrootsClientDatastore>(
key_maps: &RadrootsAppKeyMapConfig,
data: &RadrootsAppState,
) -> RadrootsAppInitResult<RadrootsAppState> {
+ app_datastore_update_state(datastore, key_maps, data).await
+}
+
+pub async fn app_datastore_create_state<T: RadrootsClientDatastore>(
+ datastore: &T,
+ key_maps: &RadrootsAppKeyMapConfig,
+ data: &RadrootsAppState,
+) -> RadrootsAppInitResult<RadrootsAppState> {
+ let now_ms = app_state_timestamp_ms();
+ match app_datastore_read_state_record(datastore, key_maps).await {
+ Ok(_) => Err(RadrootsAppInitError::State(RadrootsAppStateError::AlreadyExists)),
+ Err(RadrootsAppInitError::State(RadrootsAppStateError::Missing)) => {
+ let record = app_state_record_new(data.clone(), 1, now_ms);
+ let value = app_datastore_write_state_record(datastore, key_maps, &record).await?;
+ Ok(value.state)
+ }
+ Err(err) => Err(err),
+ }
+}
+
+pub async fn app_datastore_update_state<T: RadrootsClientDatastore>(
+ datastore: &T,
+ key_maps: &RadrootsAppKeyMapConfig,
+ data: &RadrootsAppState,
+) -> RadrootsAppInitResult<RadrootsAppState> {
let now_ms = app_state_timestamp_ms();
let record = match app_datastore_read_state_record(datastore, key_maps).await {
Ok(existing) => app_state_record_new(data.clone(), existing.revision + 1, now_ms),
Err(RadrootsAppInitError::State(RadrootsAppStateError::Missing)) => {
- app_state_record_new(data.clone(), 1, now_ms)
+ return Err(RadrootsAppInitError::State(RadrootsAppStateError::Missing));
}
Err(err) => return Err(err),
};
@@ -117,7 +142,7 @@ pub async fn app_state_set_notifications_permission<T: RadrootsClientDatastore>(
) -> RadrootsAppInitResult<RadrootsAppState> {
let mut data = app_datastore_read_state(datastore, key_maps).await?;
data.notifications_permission = Some(permission.to_string());
- let value = app_datastore_write_state(datastore, key_maps, &data).await?;
+ let value = app_datastore_update_state(datastore, key_maps, &data).await?;
Ok(value)
}
@@ -141,8 +166,10 @@ pub async fn app_state_set_notifications_permission_value<T: RadrootsClientDatas
mod tests {
use super::{
app_datastore_clear_bootstrap,
+ app_datastore_create_state,
app_datastore_has_state,
app_datastore_read_state,
+ app_datastore_update_state,
app_state_set_notifications_permission,
app_state_set_notifications_permission_value,
app_state_notifications_permission_value,
@@ -427,4 +454,64 @@ mod tests {
let record = record.as_ref().expect("record");
assert_eq!(record.state.notifications_permission.as_deref(), Some("granted"));
}
+
+ #[test]
+ fn create_state_writes_record() {
+ let mut state = RadrootsAppState::default();
+ state.active_key = "pub".to_string();
+ state.eula_date = "2025-01-01T00:00:00Z".to_string();
+ let datastore = TestDatastore {
+ state: None,
+ record: RefCell::new(None),
+ };
+ let key_maps = app_key_maps_default();
+ let created = futures::executor::block_on(app_datastore_create_state(
+ &datastore,
+ &key_maps,
+ &state,
+ ))
+ .expect("created");
+ assert_eq!(created.active_key, "pub");
+ let record = datastore.record.borrow();
+ let record = record.as_ref().expect("record");
+ assert_eq!(record.state.active_key, "pub");
+ }
+
+ #[test]
+ fn create_state_reports_existing() {
+ let mut state = RadrootsAppState::default();
+ state.active_key = "pub".to_string();
+ state.eula_date = "2025-01-01T00:00:00Z".to_string();
+ let datastore = TestDatastore {
+ state: Some(state.clone()),
+ record: RefCell::new(None),
+ };
+ let key_maps = app_key_maps_default();
+ let err = futures::executor::block_on(app_datastore_create_state(
+ &datastore,
+ &key_maps,
+ &state,
+ ))
+ .expect_err("exists");
+ assert_eq!(err, RadrootsAppInitError::State(RadrootsAppStateError::AlreadyExists));
+ }
+
+ #[test]
+ fn update_state_requires_existing_record() {
+ let mut state = RadrootsAppState::default();
+ state.active_key = "pub".to_string();
+ state.eula_date = "2025-01-01T00:00:00Z".to_string();
+ let datastore = TestDatastore {
+ state: None,
+ record: RefCell::new(None),
+ };
+ let key_maps = app_key_maps_default();
+ let err = futures::executor::block_on(app_datastore_update_state(
+ &datastore,
+ &key_maps,
+ &state,
+ ))
+ .expect_err("missing");
+ assert_eq!(err, RadrootsAppInitError::State(RadrootsAppStateError::Missing));
+ }
}
diff --git a/app/src/lib.rs b/app/src/lib.rs
@@ -17,8 +17,10 @@ mod entry;
pub use app::RadrootsApp;
pub use bootstrap::{
app_datastore_clear_bootstrap,
+ app_datastore_create_state,
app_datastore_has_state,
app_datastore_read_state,
+ app_datastore_update_state,
app_state_set_notifications_permission,
app_state_set_notifications_permission_value,
app_state_notifications_permission_value,