commit 63feea28db87214b1de6a4da14f4d7e3c6b9d587
parent 00711e6f3bb309936e2ad22c2a4aa3a60de6364e
Author: triesap <tyson@radroots.org>
Date: Mon, 25 May 2026 02:29:59 +0000
runtime: require configured relay set
- replace the single default relay env with RADROOTS_APP_NOSTR_RELAY_URLS
- normalize configured relay URLs before app startup and direct publish
- pass the relay set through desktop runtime and direct sync transport
- cover relay-set config and direct publish behavior with focused tests
Diffstat:
6 files changed, 145 insertions(+), 95 deletions(-)
diff --git a/crates/launchers/desktop/src/app.rs b/crates/launchers/desktop/src/app.rs
@@ -27,10 +27,8 @@ pub fn launch() -> Result<(), AppLaunchError> {
bootstrap_logging(&snapshot, runtime_config.local_log_root.as_path())?;
install_panic_hook();
- let runtime = DesktopAppRuntime::bootstrap(
- runtime_config.default_nostr_relay_url.clone(),
- snapshot.clone(),
- );
+ let runtime =
+ DesktopAppRuntime::bootstrap(runtime_config.nostr_relay_urls.clone(), snapshot.clone());
if let Err(error) = runtime.sync_on_app_launch() {
error!(
target: "sync",
diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs
@@ -134,13 +134,10 @@ struct SdkDirectRelayAppSyncTransport {
}
impl SdkDirectRelayAppSyncTransport {
- fn new(
- accounts_manager: RadrootsNostrAccountsManager,
- default_nostr_relay_url: String,
- ) -> Self {
+ fn new(accounts_manager: RadrootsNostrAccountsManager, nostr_relay_urls: Vec<String>) -> Self {
Self {
accounts_manager,
- relay_urls: vec![default_nostr_relay_url],
+ relay_urls: nostr_relay_urls,
timeout_ms: APP_DIRECT_RELAY_SYNC_TIMEOUT_MS,
}
}
@@ -231,29 +228,27 @@ pub struct DesktopAppRuntime {
}
impl DesktopAppRuntime {
- pub fn bootstrap(
- default_nostr_relay_url: String,
- runtime_snapshot: AppRuntimeSnapshot,
- ) -> Self {
- let state = match DesktopAppRuntimeState::try_bootstrap(
- default_nostr_relay_url,
- runtime_snapshot.clone(),
- ) {
- Ok(state) => state,
- Err(error) => DesktopAppRuntimeState::degraded_with_snapshot(error, runtime_snapshot),
- };
+ pub fn bootstrap(nostr_relay_urls: Vec<String>, runtime_snapshot: AppRuntimeSnapshot) -> Self {
+ let state =
+ match DesktopAppRuntimeState::try_bootstrap(nostr_relay_urls, runtime_snapshot.clone())
+ {
+ Ok(state) => state,
+ Err(error) => {
+ DesktopAppRuntimeState::degraded_with_snapshot(error, runtime_snapshot)
+ }
+ };
Self::from_state(state)
}
pub fn bootstrap_with_paths(
paths: AppDesktopRuntimePaths,
- default_nostr_relay_url: String,
+ nostr_relay_urls: Vec<String>,
) -> Self {
let runtime_snapshot = default_runtime_snapshot();
let state = match DesktopAppRuntimeState::bootstrap_from_paths(
paths,
- default_nostr_relay_url,
+ nostr_relay_urls,
runtime_snapshot.clone(),
) {
Ok(state) => state,
@@ -297,8 +292,8 @@ impl DesktopAppRuntime {
}
}
- pub fn default_nostr_relay_url(&self) -> String {
- self.lock_state().default_nostr_relay_url.clone()
+ pub fn nostr_relay_urls(&self) -> Vec<String> {
+ self.lock_state().nostr_relay_urls.clone()
}
pub fn selected_settings_section(&self) -> SettingsSection {
@@ -1011,7 +1006,7 @@ struct DesktopPreparedSyncRequest {
struct DesktopAppRuntimeState {
state_store: AppStateStore<AppStatePersistenceRepository>,
- default_nostr_relay_url: String,
+ nostr_relay_urls: Vec<String>,
shared_accounts_paths: Option<AppSharedAccountsPaths>,
remote_signer_paths: Option<DesktopRemoteSignerPaths>,
accounts_manager: Option<RadrootsNostrAccountsManager>,
@@ -1061,16 +1056,16 @@ impl fmt::Debug for DesktopAppRuntimeState {
impl DesktopAppRuntimeState {
fn try_bootstrap(
- default_nostr_relay_url: String,
+ nostr_relay_urls: Vec<String>,
runtime_snapshot: AppRuntimeSnapshot,
) -> Result<Self, DesktopAppRuntimeBootstrapError> {
let paths = AppDesktopRuntimePaths::current_desktop()?;
- Self::bootstrap_from_paths(paths, default_nostr_relay_url, runtime_snapshot)
+ Self::bootstrap_from_paths(paths, nostr_relay_urls, runtime_snapshot)
}
fn bootstrap_from_paths(
paths: AppDesktopRuntimePaths,
- default_nostr_relay_url: String,
+ nostr_relay_urls: Vec<String>,
runtime_snapshot: AppRuntimeSnapshot,
) -> Result<Self, DesktopAppRuntimeBootstrapError> {
if let Err(error) = cleanup_prepared_customer_label_asset_root() {
@@ -1127,13 +1122,13 @@ impl DesktopAppRuntimeState {
match accounts_bootstrap.accounts_manager.as_ref() {
Some(accounts_manager) => Box::new(SdkDirectRelayAppSyncTransport::new(
accounts_manager.clone(),
- default_nostr_relay_url.clone(),
+ nostr_relay_urls.clone(),
)),
None => default_sync_transport(),
};
let mut state = Self {
state_store,
- default_nostr_relay_url,
+ nostr_relay_urls,
shared_accounts_paths: Some(paths.shared_accounts.clone()),
remote_signer_paths: Some(remote_signer_paths),
accounts_manager: accounts_bootstrap.accounts_manager,
@@ -1174,7 +1169,7 @@ impl DesktopAppRuntimeState {
Self {
state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
.expect("in-memory state store should load"),
- default_nostr_relay_url: String::new(),
+ nostr_relay_urls: Vec::new(),
shared_accounts_paths: None,
remote_signer_paths: None,
accounts_manager: None,
@@ -3621,7 +3616,7 @@ impl DesktopAppRuntimeState {
order_document_json: Some(local_work.payload.clone()),
listing_addr: export.listing_addr,
listing_event_id: export.listing_event_id,
- listing_relays: vec![self.default_nostr_relay_url.clone()],
+ listing_relays: self.nostr_relay_urls.clone(),
buyer_pubkey: export.buyer_pubkey,
seller_pubkey: export.seller_pubkey,
items: order
@@ -4746,10 +4741,17 @@ fn current_runtime_time_ms() -> Result<i64, AppSqliteError> {
fn normalized_app_sync_relay_urls(
relay_urls: &[String],
) -> Result<Vec<String>, AppSyncTransportError> {
+ let mut seen = BTreeSet::new();
let normalized = relay_urls
.iter()
- .map(|relay| relay.trim().to_owned())
- .filter(|relay| !relay.is_empty())
+ .filter_map(|relay| {
+ let relay = relay.trim();
+ if relay.is_empty() || !seen.insert(relay.to_owned()) {
+ None
+ } else {
+ Some(relay.to_owned())
+ }
+ })
.collect::<Vec<_>>();
if normalized.is_empty() {
return Err(AppSyncTransportError::unavailable(
@@ -7051,7 +7053,8 @@ mod tests {
#[test]
fn runtime_direct_relay_transport_publishes_typed_farm_work() {
- let relay = ThreadedAckRelay::spawn();
+ let relay_a = ThreadedAckRelay::spawn();
+ let relay_b = ThreadedAckRelay::spawn();
let manager = RadrootsNostrAccountsManager::new_in_memory();
let account_id = manager
.generate_identity(Some("Farmer".to_owned()), true)
@@ -7066,8 +7069,10 @@ mod tests {
});
let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
.expect("typed farm publish work should serialize");
- let mut transport =
- SdkDirectRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
+ let mut transport = SdkDirectRelayAppSyncTransport::with_relay_urls(
+ manager,
+ vec![relay_a.url().to_owned(), relay_b.url().to_owned()],
+ );
let result = transport
.sync(AppSyncRequest {
@@ -7090,7 +7095,23 @@ mod tests {
);
assert_eq!(
result.published_receipts[0].relay_delivery_json["acknowledged_relays"],
- json!([relay.url()])
+ json!([relay_a.url(), relay_b.url()])
+ );
+ }
+
+ #[test]
+ fn runtime_direct_relay_transport_normalizes_configured_relay_set() {
+ let relay_urls = super::normalized_app_sync_relay_urls(&[
+ " ws://127.0.0.1:8081 ".to_owned(),
+ "ws://127.0.0.1:8080".to_owned(),
+ "ws://127.0.0.1:8081".to_owned(),
+ " ".to_owned(),
+ ])
+ .expect("relay set should normalize");
+
+ assert_eq!(
+ relay_urls,
+ vec!["ws://127.0.0.1:8081", "ws://127.0.0.1:8080"]
);
}
@@ -7175,7 +7196,7 @@ mod tests {
let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
.expect("in-memory state store should load"),
- default_nostr_relay_url: "ws://127.0.0.1:8080".to_owned(),
+ nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
shared_accounts_paths: None,
remote_signer_paths: None,
accounts_manager: None,
@@ -7229,7 +7250,7 @@ mod tests {
let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
.expect("in-memory state store should load"),
- default_nostr_relay_url: "ws://127.0.0.1:8080".to_owned(),
+ nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
shared_accounts_paths: None,
remote_signer_paths: None,
accounts_manager: None,
@@ -8659,7 +8680,7 @@ mod tests {
let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
.expect("in-memory state store should load"),
- default_nostr_relay_url: "ws://127.0.0.1:8080".to_owned(),
+ nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
shared_accounts_paths: None,
remote_signer_paths: Some(paths.clone()),
accounts_manager: None,
@@ -8690,7 +8711,7 @@ mod tests {
let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
.expect("in-memory state store should load"),
- default_nostr_relay_url: "ws://127.0.0.1:8080".to_owned(),
+ nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
shared_accounts_paths: None,
remote_signer_paths: Some(paths.clone()),
accounts_manager: None,
@@ -9099,7 +9120,7 @@ mod tests {
let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
.expect("in-memory state store should load"),
- default_nostr_relay_url: "ws://127.0.0.1:8080".to_owned(),
+ nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
shared_accounts_paths: None,
remote_signer_paths: None,
accounts_manager: None,
@@ -9201,7 +9222,7 @@ mod tests {
let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
.expect("in-memory state store should load"),
- default_nostr_relay_url: "ws://127.0.0.1:8080".to_owned(),
+ nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
shared_accounts_paths: None,
remote_signer_paths: None,
accounts_manager: None,
@@ -9254,7 +9275,7 @@ mod tests {
let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
.expect("in-memory state store should load"),
- default_nostr_relay_url: "ws://127.0.0.1:8080".to_owned(),
+ nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
shared_accounts_paths: None,
remote_signer_paths: None,
accounts_manager: None,
@@ -9291,7 +9312,7 @@ mod tests {
let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
.expect("in-memory state store should load"),
- default_nostr_relay_url: "ws://127.0.0.1:8080".to_owned(),
+ nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
shared_accounts_paths: None,
remote_signer_paths: None,
accounts_manager: None,
@@ -9326,7 +9347,7 @@ mod tests {
let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
.expect("in-memory state store should load"),
- default_nostr_relay_url: "ws://127.0.0.1:8080".to_owned(),
+ nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
shared_accounts_paths: None,
remote_signer_paths: None,
accounts_manager: None,
@@ -13490,7 +13511,7 @@ mod tests {
let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
.expect("in-memory state store should load"),
- default_nostr_relay_url: "ws://127.0.0.1:8080".to_owned(),
+ nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
shared_accounts_paths: Some(paths),
remote_signer_paths: None,
accounts_manager: None,
@@ -13519,7 +13540,7 @@ mod tests {
DesktopAppRuntime::from_state(DesktopAppRuntimeState {
state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
.expect("in-memory state store should load"),
- default_nostr_relay_url: "ws://127.0.0.1:8080".to_owned(),
+ nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
shared_accounts_paths: None,
remote_signer_paths: None,
accounts_manager: Some(
@@ -13550,7 +13571,7 @@ mod tests {
DesktopAppRuntime::from_state(DesktopAppRuntimeState {
state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
.expect("in-memory state store should load"),
- default_nostr_relay_url: "ws://127.0.0.1:8080".to_owned(),
+ nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
shared_accounts_paths: Some(paths.clone()),
remote_signer_paths: None,
accounts_manager: Some(
@@ -13586,7 +13607,7 @@ mod tests {
DesktopAppRuntime::from_state(
DesktopAppRuntimeState::bootstrap_from_paths(
paths,
- "ws://127.0.0.1:8080".to_owned(),
+ vec!["ws://127.0.0.1:8080".to_owned()],
super::default_runtime_snapshot(),
)
.expect("runtime bootstrap should succeed"),
diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs
@@ -559,12 +559,12 @@ impl HomeView {
}
self.startup_view.clear_notice();
- let relay_url = self.runtime.default_nostr_relay_url();
+ let relay_urls = self.runtime.nostr_relay_urls();
cx.notify();
cx.spawn_in(window, async move |this, cx| {
let startup_task = cx
.background_executor()
- .spawn(run_startup_app_init(relay_url));
+ .spawn(run_startup_app_init(relay_urls));
Timer::after(Duration::from_secs(1)).await;
let startup_result = startup_task.await;
let _ = this.update(cx, |this, cx| {
@@ -9357,12 +9357,14 @@ fn startup_home_body(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement {
div().w_full().text_center().child(home_body_text(body))
}
-async fn connect_default_relay(relay_url: String) -> Result<RadrootsNostrClient, String> {
+async fn connect_configured_relays(relay_urls: Vec<String>) -> Result<RadrootsNostrClient, String> {
let client = RadrootsNostrClient::new_signerless();
- client
- .add_relay(relay_url.as_str())
- .await
- .map_err(|error| format!("failed to add relay `{relay_url}`: {error}"))?;
+ for relay_url in relay_urls {
+ client
+ .add_relay(relay_url.as_str())
+ .await
+ .map_err(|error| format!("failed to add relay `{relay_url}`: {error}"))?;
+ }
client.connect().await;
Ok(client)
}
@@ -9371,8 +9373,8 @@ struct StartupAppInitResult {
relay_client: RadrootsNostrClient,
}
-async fn run_startup_app_init(relay_url: String) -> Result<StartupAppInitResult, String> {
- let relay_client = connect_default_relay(relay_url).await?;
+async fn run_startup_app_init(relay_urls: Vec<String>) -> Result<StartupAppInitResult, String> {
+ let relay_client = connect_configured_relays(relay_urls).await?;
Ok(StartupAppInitResult { relay_client })
}
@@ -13117,7 +13119,7 @@ mod tests {
.expect("desktop runtime paths should resolve");
let runtime = crate::runtime::DesktopAppRuntime::bootstrap_with_paths(
paths.clone(),
- "wss://relay.example".to_owned(),
+ vec!["wss://relay.example".to_owned()],
);
(HomeView::new(runtime), paths, home_dir)
diff --git a/crates/shared/core/src/lib.rs b/crates/shared/core/src/lib.rs
@@ -28,7 +28,7 @@ pub use paths::{
SHARED_LOCAL_EVENTS_NAMESPACE_VALUE, shared_local_events_database_path_from_shared_accounts,
};
pub use runtime::{
- APP_DEFAULT_NOSTR_RELAY_URL_ENV, APP_HOST_PLATFORM, APP_ID, APP_LOCAL_LOG_ROOT_ENV, APP_NAME,
+ APP_HOST_PLATFORM, APP_ID, APP_LOCAL_LOG_ROOT_ENV, APP_NAME, APP_NOSTR_RELAY_URLS_ENV,
APP_PLATFORM_RUNTIME, APP_PROJECTION_SOURCE, APP_RUNTIME_MODE_ENV, APP_RUNTIME_ORIGIN,
AppBuildIdentity, AppCoreRuntimeMetadata, AppHostRuntimeMetadata, AppRuntimeCapture,
AppRuntimeConfig, AppRuntimeConfigError, AppRuntimeMode, AppRuntimeSnapshot,
diff --git a/crates/shared/core/src/runtime.rs b/crates/shared/core/src/runtime.rs
@@ -1,4 +1,5 @@
use std::{
+ collections::BTreeSet,
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
@@ -15,7 +16,7 @@ pub const APP_PROJECTION_SOURCE: &str = "gpui-native";
pub const APP_RUNTIME_ORIGIN: &str = "gpui://localhost";
pub const APP_HOST_PLATFORM: &str = "desktop";
pub const APP_RUNTIME_MODE_ENV: &str = "RADROOTS_APP_RUNTIME_MODE";
-pub const APP_DEFAULT_NOSTR_RELAY_URL_ENV: &str = "RADROOTS_APP_DEFAULT_NOSTR_RELAY_URL";
+pub const APP_NOSTR_RELAY_URLS_ENV: &str = "RADROOTS_APP_NOSTR_RELAY_URLS";
pub const APP_LOCAL_LOG_ROOT_ENV: &str = "RADROOTS_APP_LOCAL_LOG_ROOT";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -28,7 +29,7 @@ pub enum AppRuntimeMode {
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AppRuntimeConfig {
pub runtime_mode: AppRuntimeMode,
- pub default_nostr_relay_url: String,
+ pub nostr_relay_urls: Vec<String>,
pub local_log_root: PathBuf,
}
@@ -106,8 +107,10 @@ impl AppRuntimeConfig {
{
let runtime_mode =
parse_config_runtime_mode(&require_env_value(&mut read_env, APP_RUNTIME_MODE_ENV)?)?;
- let default_nostr_relay_url =
- require_env_value(&mut read_env, APP_DEFAULT_NOSTR_RELAY_URL_ENV)?;
+ let nostr_relay_urls = parse_relay_url_set(
+ APP_NOSTR_RELAY_URLS_ENV,
+ require_env_value(&mut read_env, APP_NOSTR_RELAY_URLS_ENV)?,
+ )?;
let local_log_root = read_env(APP_LOCAL_LOG_ROOT_ENV)
.map(|value| require_path_value(APP_LOCAL_LOG_ROOT_ENV, value))
.transpose()?;
@@ -121,7 +124,7 @@ impl AppRuntimeConfig {
Ok(Self {
runtime_mode,
- default_nostr_relay_url,
+ nostr_relay_urls,
local_log_root,
})
}
@@ -214,6 +217,26 @@ fn parse_config_runtime_mode(value: &str) -> Result<AppRuntimeMode, AppRuntimeCo
}
}
+fn parse_relay_url_set(
+ field: &'static str,
+ value: String,
+) -> Result<Vec<String>, AppRuntimeConfigError> {
+ let mut seen = BTreeSet::new();
+ let mut relays = Vec::new();
+ for relay in value.split(',') {
+ let relay = relay.trim();
+ if !relay.is_empty() && seen.insert(relay.to_owned()) {
+ relays.push(relay.to_owned());
+ }
+ }
+
+ if relays.is_empty() {
+ return Err(AppRuntimeConfigError::MissingField(field));
+ }
+
+ Ok(relays)
+}
+
fn require_path_value(
field: &'static str,
value: String,
@@ -283,10 +306,10 @@ mod tests {
use std::{collections::BTreeMap, path::PathBuf};
use super::{
- APP_DEFAULT_NOSTR_RELAY_URL_ENV, APP_HOST_PLATFORM, APP_ID, APP_LOCAL_LOG_ROOT_ENV,
- APP_NAME, APP_PROJECTION_SOURCE, APP_RUNTIME_MODE_ENV, APP_RUNTIME_ORIGIN,
- AppBuildIdentity, AppRuntimeCapture, AppRuntimeConfig, AppRuntimeConfigError,
- AppRuntimeMode, AppRuntimeSnapshot, runtime_mode_label,
+ APP_HOST_PLATFORM, APP_ID, APP_LOCAL_LOG_ROOT_ENV, APP_NAME, APP_NOSTR_RELAY_URLS_ENV,
+ APP_PROJECTION_SOURCE, APP_RUNTIME_MODE_ENV, APP_RUNTIME_ORIGIN, AppBuildIdentity,
+ AppRuntimeCapture, AppRuntimeConfig, AppRuntimeConfigError, AppRuntimeMode,
+ AppRuntimeSnapshot, runtime_mode_label,
};
fn test_build_identity() -> AppBuildIdentity {
@@ -304,8 +327,8 @@ mod tests {
BTreeMap::from([
(APP_RUNTIME_MODE_ENV, "localhost-dev".to_owned()),
(
- APP_DEFAULT_NOSTR_RELAY_URL_ENV,
- "ws://127.0.0.1:8080".to_owned(),
+ APP_NOSTR_RELAY_URLS_ENV,
+ " ws://127.0.0.1:8080 , ws://127.0.0.1:8081 , ws://127.0.0.1:8080 ".to_owned(),
),
])
}
@@ -343,10 +366,7 @@ mod tests {
#[test]
fn runtime_config_requires_explicit_runtime_mode_env() {
- let env = BTreeMap::from([(
- APP_DEFAULT_NOSTR_RELAY_URL_ENV,
- "ws://127.0.0.1:8080".to_owned(),
- )]);
+ let env = BTreeMap::from([(APP_NOSTR_RELAY_URLS_ENV, "ws://127.0.0.1:8080".to_owned())]);
let error = AppRuntimeConfig::from_env_with(
|name| env.get(name).cloned(),
Some(PathBuf::from("/tmp/default-logs")),
@@ -363,10 +383,7 @@ mod tests {
fn runtime_config_surfaces_explicit_local_log_root() {
let env = BTreeMap::from([
(APP_RUNTIME_MODE_ENV, "localhost-dev".to_owned()),
- (
- APP_DEFAULT_NOSTR_RELAY_URL_ENV,
- "ws://127.0.0.1:8080".to_owned(),
- ),
+ (APP_NOSTR_RELAY_URLS_ENV, "ws://127.0.0.1:8080".to_owned()),
(APP_LOCAL_LOG_ROOT_ENV, "/tmp/radroots/logs".to_owned()),
]);
let config = AppRuntimeConfig::from_env_with(
@@ -376,11 +393,26 @@ mod tests {
.expect("valid env config");
assert_eq!(config.runtime_mode, AppRuntimeMode::LocalhostDev);
- assert_eq!(config.default_nostr_relay_url, "ws://127.0.0.1:8080");
+ assert_eq!(config.nostr_relay_urls, vec!["ws://127.0.0.1:8080"]);
assert_eq!(config.local_log_root, PathBuf::from("/tmp/radroots/logs"));
}
#[test]
+ fn runtime_config_normalizes_configured_nostr_relay_urls() {
+ let env = test_runtime_env();
+ let config = AppRuntimeConfig::from_env_with(
+ |name| env.get(name).cloned(),
+ Some(PathBuf::from("/tmp/default-logs")),
+ )
+ .expect("valid env config");
+
+ assert_eq!(
+ config.nostr_relay_urls,
+ vec!["ws://127.0.0.1:8080", "ws://127.0.0.1:8081"]
+ );
+ }
+
+ #[test]
fn runtime_config_defaults_local_log_root_from_runtime_paths() {
let env = test_runtime_env();
let config = AppRuntimeConfig::from_env_with(
@@ -396,10 +428,7 @@ mod tests {
fn runtime_config_accepts_explicit_log_root_without_default_runtime_paths() {
let env = BTreeMap::from([
(APP_RUNTIME_MODE_ENV, "localhost-dev".to_owned()),
- (
- APP_DEFAULT_NOSTR_RELAY_URL_ENV,
- "ws://127.0.0.1:8080".to_owned(),
- ),
+ (APP_NOSTR_RELAY_URLS_ENV, "ws://127.0.0.1:8080".to_owned()),
(APP_LOCAL_LOG_ROOT_ENV, "/tmp/explicit-logs".to_owned()),
]);
let config = AppRuntimeConfig::from_env_with(|name| env.get(name).cloned(), None)
@@ -451,7 +480,7 @@ mod tests {
fn runtime_config_rejects_empty_required_fields() {
let env = BTreeMap::from([
(APP_RUNTIME_MODE_ENV, "localhost-dev".to_owned()),
- (APP_DEFAULT_NOSTR_RELAY_URL_ENV, "".to_owned()),
+ (APP_NOSTR_RELAY_URLS_ENV, "".to_owned()),
]);
let error = AppRuntimeConfig::from_env_with(
|name| env.get(name).cloned(),
@@ -462,25 +491,25 @@ mod tests {
assert!(
matches!(
error,
- AppRuntimeConfigError::MissingField(APP_DEFAULT_NOSTR_RELAY_URL_ENV)
+ AppRuntimeConfigError::MissingField(APP_NOSTR_RELAY_URLS_ENV)
),
"unexpected error: {error}"
);
}
#[test]
- fn runtime_config_rejects_missing_default_nostr_relay_url() {
+ fn runtime_config_rejects_missing_nostr_relay_urls() {
let env = BTreeMap::from([(APP_RUNTIME_MODE_ENV, "localhost-dev".to_owned())]);
let error = AppRuntimeConfig::from_env_with(
|name| env.get(name).cloned(),
Some(PathBuf::from("/tmp/default-logs")),
)
- .expect_err("missing default relay url should fail");
+ .expect_err("missing relay urls should fail");
assert!(
matches!(
error,
- AppRuntimeConfigError::MissingEnv(APP_DEFAULT_NOSTR_RELAY_URL_ENV)
+ AppRuntimeConfigError::MissingEnv(APP_NOSTR_RELAY_URLS_ENV)
),
"unexpected error: {error}"
);
diff --git a/scripts/run.sh b/scripts/run.sh
@@ -7,16 +7,16 @@ repo_root="$(git -C "${script_dir}" rev-parse --show-toplevel)"
cd "${repo_root}"
runtime_mode="${RADROOTS_APP_RUNTIME_MODE:-localhost-dev}"
-default_nostr_relay_url="${RADROOTS_APP_DEFAULT_NOSTR_RELAY_URL:-}"
+nostr_relay_urls="${RADROOTS_APP_NOSTR_RELAY_URLS:-}"
local_log_root="${RADROOTS_APP_LOCAL_LOG_ROOT:-${repo_root}/logs}"
-if [[ -z "${default_nostr_relay_url}" ]]; then
- echo "missing required env: RADROOTS_APP_DEFAULT_NOSTR_RELAY_URL" >&2
+if [[ -z "${nostr_relay_urls}" ]]; then
+ echo "missing required env: RADROOTS_APP_NOSTR_RELAY_URLS" >&2
exit 1
fi
export RADROOTS_APP_RUNTIME_MODE="${runtime_mode}"
-export RADROOTS_APP_DEFAULT_NOSTR_RELAY_URL="${default_nostr_relay_url}"
+export RADROOTS_APP_NOSTR_RELAY_URLS="${nostr_relay_urls}"
export RADROOTS_APP_LOCAL_LOG_ROOT="${local_log_root}"
export RUST_LOG="${RADROOTS_APP_RUST_LOG:-info}"