Files
SoulStream/crates/soulseek-client/src/distributed.rs
T
KeelrLeek a667b4b28e Baseline: full tree snapshot after git repository loss
The previous .git directory was found gutted (only info/ remained), so
history is unrecoverable locally. This commit restores version control
with the tree exactly as it stands: the working app, the preview-tracks
launcher on the artist hero, the scrollable-track-previews design bundle
(docs + approved mockups), and the Phase 0 validation artifacts.
2026-07-22 17:57:00 -05:00

107 lines
3.9 KiB
Rust

//! Distributed network: we join as a leaf child. The server sends parent
//! candidates via NetInfo; we connect to one (type "D"), complete the
//! branch handshake, and receive relayed `DistributedSearchRequest`
//! messages, which we answer from our shares.
//!
//! We do not accept children (leaf only) — that keeps the implementation
//! tractable while still making us a search-answering participant.
use std::net::SocketAddrV4;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use soulseek_proto::{dist, peer, server};
use crate::{conn, responder, Inner};
const CONNECT_TIMEOUT: Duration = Duration::from_secs(8);
const READ_TIMEOUT: Duration = Duration::from_secs(120);
/// Try each parent candidate until one connects; on success, run the
/// distributed read loop until it drops, then allow re-solicitation.
pub(crate) fn on_net_info(inner: Arc<Inner>, parents: Vec<(String, SocketAddrV4)>) {
if inner.has_parent.load(Ordering::Relaxed) || parents.is_empty() {
return;
}
tokio::spawn(async move {
for (username, addr) in parents {
if inner.has_parent.load(Ordering::Relaxed) {
return;
}
match connect_parent(&inner, &username, addr).await {
Ok(stream) => {
inner.has_parent.store(true, Ordering::Relaxed);
log::info!("distributed parent: {username} ({addr})");
let _ = run_parent(&inner, stream).await;
inner.has_parent.store(false, Ordering::Relaxed);
// Ask the server for fresh parents next time.
let _ = inner.send_server(server::have_no_parents(true));
return;
}
Err(e) => log::debug!("parent {username} ({addr}) failed: {e}"),
}
}
});
}
async fn connect_parent(
inner: &Arc<Inner>,
_username: &str,
addr: SocketAddrV4,
) -> std::io::Result<TcpStream> {
let mut stream = tokio::time::timeout(CONNECT_TIMEOUT, TcpStream::connect(addr))
.await
.map_err(|_| std::io::Error::other("connect timeout"))??;
stream.set_nodelay(true).ok();
// Identify as a distributed peer.
let token = inner.next_token();
let init = peer::peer_init(&inner.own_username(), peer::CONN_TYPE_DISTRIBUTED, token);
stream.write_all(&init).await?;
// Announce ourselves as a leaf: branch level 0, root = us.
stream.write_all(&dist::branch_level(0)).await?;
stream
.write_all(&dist::branch_root(&inner.own_username()))
.await?;
Ok(stream)
}
async fn run_parent(inner: &Arc<Inner>, mut stream: TcpStream) -> std::io::Result<()> {
loop {
let frame = match tokio::time::timeout(READ_TIMEOUT, conn::read_frame(&mut stream)).await {
Ok(f) => f?,
Err(_) => {
// Keepalive ping; if it fails the connection is dead.
if stream.write_all(&dist::ping()).await.is_err() {
return Ok(());
}
continue;
}
};
let Some(frame) = frame else { return Ok(()) };
match dist::decode(frame) {
Ok(dist::DistributedEvent::SearchRequest {
username,
token,
query,
}) => {
responder::respond_to_search(inner.clone(), username, token, query);
}
Ok(dist::DistributedEvent::BranchLevel(level)) => {
// Report our level (parent + 1) to the server.
let _ = inner.send_server(server::branch_level(level + 1));
}
Ok(dist::DistributedEvent::BranchRoot(root)) => {
let _ = inner.send_server(server::branch_root(&root));
}
Ok(_) => {}
Err(e) => log::trace!("bad distributed message: {e}"),
}
}
}