commit 8332f0dc68b28b3a002d47b09e1931b191c3242d
parent 3147dbe18ae29b4a7513d57dff3c5ef2f9b2f18a
Author: triesap <tyson@radroots.org>
Date: Tue, 4 Aug 2026 00:10:05 +0000
runtime: preserve local startup without relays
- open the local account store before relay availability
- project invalid packaged relay configuration as degraded
- keep actor snapshots and observer changes lifecycle-consistent
- cover empty local-first relay fallback without network access
Diffstat:
2 files changed, 43 insertions(+), 6 deletions(-)
diff --git a/crates/studio_ffi/src/commands.rs b/crates/studio_ffi/src/commands.rs
@@ -8,7 +8,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
use directories::ProjectDirs;
use radroots_studio_application::{
- Clock, DurableRequestId, GeneratedKeyRecoveryHandle, RelayRuntimeMode,
+ Clock, DurableRequestId, GeneratedKeyRecoveryHandle, RelayConfiguration, RelayRuntimeMode,
RemovalConfirmationToken, SdkNostrClient, relay_configuration_from_environment,
};
use radroots_studio_domain::{PublicKey, SafeError, SecretKeyInput, UnixTimestamp};
@@ -185,18 +185,29 @@ pub(crate) struct RuntimeCore {
BTreeMap<radroots_studio_application::ChangeSubscriptionId, tokio::task::JoinHandle<()>>,
>,
pub(crate) closed: AtomicBool,
+ pub(crate) startup_relay_problem: Option<SafeError>,
}
impl RuntimeCore {
pub(crate) fn snapshot_dto(&self) -> AppSnapshotDto {
- AppSnapshotDto::from_runtime(&self.actor.snapshot(), self.actor.lifecycle())
+ AppSnapshotDto::from_runtime(&self.actor.snapshot(), self.effective_lifecycle())
}
pub(crate) fn dto_for(
&self,
snapshot: &radroots_studio_application::AppSnapshot,
) -> AppSnapshotDto {
- AppSnapshotDto::from_runtime(snapshot, self.actor.lifecycle())
+ AppSnapshotDto::from_runtime(snapshot, self.effective_lifecycle())
+ }
+
+ pub(crate) fn effective_lifecycle(&self) -> radroots_studio_application::RuntimeLifecycle {
+ let lifecycle = self.actor.lifecycle();
+ match (lifecycle, self.startup_relay_problem) {
+ (radroots_studio_application::RuntimeLifecycle::Ready, Some(problem)) => {
+ radroots_studio_application::RuntimeLifecycle::Degraded(problem)
+ }
+ _ => lifecycle,
+ }
}
}
@@ -484,7 +495,8 @@ impl StudioAppCore {
} else {
RelayRuntimeMode::Packaged
};
- let relays = relay_configuration_from_environment(mode)?;
+ let (relays, startup_relay_problem) =
+ local_first_relay_configuration(relay_configuration_from_environment(mode));
let actor = RuntimeActorHandle::open(
path,
relays,
@@ -499,11 +511,21 @@ impl StudioAppCore {
actor,
observers: Mutex::new(BTreeMap::new()),
closed: AtomicBool::new(false),
+ startup_relay_problem,
}),
}))
}
}
+fn local_first_relay_configuration(
+ configured: Result<RelayConfiguration, SafeError>,
+) -> (RelayConfiguration, Option<SafeError>) {
+ match configured {
+ Ok(relays) => (relays, None),
+ Err(problem) => (RelayConfiguration::default(), Some(problem)),
+ }
+}
+
#[derive(Clone, Copy)]
pub(crate) struct SystemClock;
@@ -596,6 +618,7 @@ mod tests {
use std::sync::Arc;
use radroots_studio_application::{InMemorySecretStore, RelayConfiguration, SdkNostrClient};
+ use radroots_studio_domain::SafeError;
use radroots_studio_storage::RuntimeActorHandle;
use radroots_studio_storage::{CREDENTIAL_SERVICE, CURRENT_SCHEMA_VERSION};
@@ -604,7 +627,7 @@ mod tests {
ACTOR_MAILBOX_CAPACITY, CompatibilityExpectation, DATABASE_APPLICATION, DATABASE_FILENAME,
DATABASE_ORGANIZATION, DATABASE_QUALIFIER, FFI_CONTRACT_HASH, FFI_CONTRACT_MAJOR,
FFI_CONTRACT_MINOR, RequestContextDto, RuntimeCore, StudioAppCore, SystemClock,
- compatibility_descriptor, runtime, verify_compatibility,
+ compatibility_descriptor, local_first_relay_configuration, runtime, verify_compatibility,
};
fn in_memory_core() -> Arc<StudioAppCore> {
@@ -622,6 +645,7 @@ mod tests {
actor,
observers: std::sync::Mutex::new(std::collections::BTreeMap::new()),
closed: std::sync::atomic::AtomicBool::new(false),
+ startup_relay_problem: None,
}),
})
}
@@ -778,4 +802,16 @@ mod tests {
assert!(!observer.contains(&forbidden));
}
}
+
+ #[test]
+ fn invalid_relay_configuration_preserves_local_startup_as_degraded() {
+ let problem = SafeError::new(
+ radroots_studio_domain::SafeErrorCode::InvalidRelayConfiguration,
+ radroots_studio_domain::SafeMessage::new("The Nostr relay configuration is invalid."),
+ );
+ let (relays, degraded) = local_first_relay_configuration(Err(problem));
+
+ assert!(relays.relays().is_empty());
+ assert_eq!(degraded, Some(problem));
+ }
}
diff --git a/crates/studio_ffi/src/observer.rs b/crates/studio_ffi/src/observer.rs
@@ -95,7 +95,7 @@ impl StudioAppCore {
observer.on_change(SnapshotChangeDto {
snapshot: AppSnapshotDto::from_runtime(
change.snapshot(),
- runtime_core.actor.lifecycle(),
+ runtime_core.effective_lifecycle(),
),
previous_revision: change
.previous_revision()
@@ -207,6 +207,7 @@ mod tests {
actor,
observers: Mutex::new(std::collections::BTreeMap::new()),
closed: std::sync::atomic::AtomicBool::new(false),
+ startup_relay_problem: None,
}),
})
}