feat(commands): relay-fallback search + set_relay_discoverable command

search_directory falls back to relay when local results < 5.
Relay stubs upserted with INSERT OR IGNORE to preserve existing data.
set_relay_discoverable persists setting and notifies running node.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
cloudwithax 2026-02-19 12:35:48 -05:00
parent cb45e2e463
commit 72a82c1a11
3 changed files with 109 additions and 2 deletions

View File

@ -254,12 +254,15 @@ pub async fn search_directory(
query: String, query: String,
) -> Result<Vec<DirectoryEntry>, String> { ) -> Result<Vec<DirectoryEntry>, String> {
ipc_log!("search_directory", { ipc_log!("search_directory", {
let query_trimmed = query.trim().to_string();
// local search first
let entries = state let entries = state
.storage .storage
.load_directory() .load_directory()
.map_err(|e| format!("failed to load directory: {}", e))?; .map_err(|e| format!("failed to load directory: {}", e))?;
let query_lower = query.to_lowercase(); let query_lower = query_trimmed.to_lowercase();
let mut results: Vec<DirectoryEntry> = entries let mut results: Vec<DirectoryEntry> = entries
.into_values() .into_values()
.filter(|entry| { .filter(|entry| {
@ -267,8 +270,63 @@ pub async fn search_directory(
|| entry.peer_id.to_lowercase().contains(&query_lower) || entry.peer_id.to_lowercase().contains(&query_lower)
}) })
.collect(); .collect();
results.sort_by(|a, b| b.last_seen.cmp(&a.last_seen)); results.sort_by(|a, b| b.last_seen.cmp(&a.last_seen));
// relay fallback when local results are sparse
if results.len() < 5 && !query_trimmed.is_empty() {
let node_handle = state.node_handle.lock().await;
if let Some(ref handle) = *node_handle {
let (tx, rx) = tokio::sync::oneshot::channel();
let _ = handle
.command_tx
.send(crate::node::NodeCommand::DirectorySearch {
query: query_trimmed.clone(),
reply: tx,
})
.await;
drop(node_handle);
// wait up to 5 seconds for relay response
if let Ok(Ok(Ok(relay_entries))) =
tokio::time::timeout(std::time::Duration::from_secs(5), rx).await
{
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
for entry in relay_entries {
// upsert as stub — empty bio/public_key means never directly connected
let stub = DirectoryEntry {
peer_id: entry.peer_id.clone(),
display_name: entry.display_name,
bio: String::new(),
public_key: String::new(),
last_seen: entry.last_seen.saturating_mul(1000).max(now - 86_400_000),
is_friend: false,
};
// preserve existing local data if we already know this peer
let _ = state.storage.save_directory_entry_if_new(&stub);
}
// re-run local search to get merged results
let entries2 = state
.storage
.load_directory()
.unwrap_or_default();
let mut results2: Vec<DirectoryEntry> = entries2
.into_values()
.filter(|entry| {
entry.display_name.to_lowercase().contains(&query_lower)
|| entry.peer_id.to_lowercase().contains(&query_lower)
})
.collect();
results2.sort_by(|a, b| b.last_seen.cmp(&a.last_seen));
return Ok(results2);
}
}
}
Ok(results) Ok(results)
}) })
} }
@ -329,6 +387,33 @@ pub async fn discover_global_peers(state: State<'_, AppState>) -> Result<(), Str
Ok(()) Ok(())
} }
// toggle relay discoverability at runtime and sync the setting to disk
#[tauri::command]
pub async fn set_relay_discoverable(
state: State<'_, AppState>,
enabled: bool,
) -> Result<(), String> {
ipc_log!("set_relay_discoverable", {
// persist setting
let mut settings = state.storage.load_settings().unwrap_or_default();
settings.relay_discoverable = enabled;
state
.storage
.save_settings(&settings)
.map_err(|e| format!("failed to save settings: {}", e))?;
// notify running node
let node_handle = state.node_handle.lock().await;
if let Some(ref handle) = *node_handle {
let _ = handle
.command_tx
.send(crate::node::NodeCommand::SetRelayDiscoverable { enabled })
.await;
}
Ok(())
})
}
// change relay address and restart the node // change relay address and restart the node
// used when default relay is unreachable or at capacity // used when default relay is unreachable or at capacity
#[tauri::command] #[tauri::command]

View File

@ -114,6 +114,7 @@ pub fn run() {
commands::identity::add_friend, commands::identity::add_friend,
commands::identity::remove_friend, commands::identity::remove_friend,
commands::identity::discover_global_peers, commands::identity::discover_global_peers,
commands::identity::set_relay_discoverable,
commands::identity::set_relay_address, commands::identity::set_relay_address,
commands::identity::reset_identity, commands::identity::reset_identity,
commands::identity::cache_avatar_icon, commands::identity::cache_avatar_icon,

View File

@ -744,6 +744,27 @@ impl DiskStorage {
Ok(()) Ok(())
} }
// save a directory entry only if the peer is not already known
// preserves existing data (bio, public_key) when upserting relay stubs
pub fn save_directory_entry_if_new(&self, entry: &DirectoryEntry) -> Result<(), io::Error> {
let conn = self.open_conn()?;
conn.execute(
"INSERT OR IGNORE INTO directory_entries (
peer_id, display_name, bio, public_key, last_seen, is_friend
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![
entry.peer_id,
entry.display_name,
entry.bio,
entry.public_key,
entry.last_seen as i64,
if entry.is_friend { 1_i64 } else { 0_i64 }
],
)
.map_err(sqlite_to_io_error)?;
Ok(())
}
// load the entire peer directory // load the entire peer directory
pub fn load_directory(&self) -> Result<HashMap<String, DirectoryEntry>, io::Error> { pub fn load_directory(&self) -> Result<HashMap<String, DirectoryEntry>, io::Error> {
let conn = self.open_conn()?; let conn = self.open_conn()?;