refactor(api): put a SessionApi seam between the commands, the TUI, and the client
Commands and the TUI now hold an Arc<dyn SessionApi> instead of an Arc<Client>. The trait is implemented on Client itself, so the local path is the same client behind one vtable and no wrapper type exists to keep in sync. Daemon mode plugs a remote implementation into the same handle.
This commit is contained in:
@@ -18,6 +18,7 @@ use crate::output::{CliError, CliResult, Exit, Out, PortmapRecord};
|
||||
use crate::port_mapping::{self, PortMapper};
|
||||
use soulseek_rs::types::{RoomEvent, RoomInfo};
|
||||
use soulseek_rs::{Client, ClientSettings, SessionLoss};
|
||||
use crate::api::SessionApi;
|
||||
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -31,7 +32,7 @@ pub struct Ctx {
|
||||
pub search_timeout: Duration,
|
||||
/// The client of the session this command opened, once it has one, so the
|
||||
/// verdict can be checked against the session that produced it.
|
||||
pub session: OnceLock<Arc<Client>>,
|
||||
pub session: OnceLock<Arc<dyn SessionApi>>,
|
||||
}
|
||||
|
||||
impl Ctx {
|
||||
@@ -75,7 +76,7 @@ fn session_lost(loss: SessionLoss) -> CliError {
|
||||
/// A logged-in client plus, while the listener is enabled, the port mapping
|
||||
/// that makes us reachable. Both live until the command ends.
|
||||
pub struct Session {
|
||||
pub client: Arc<Client>,
|
||||
pub client: Arc<dyn SessionApi>,
|
||||
_mapper: Option<PortMapper>,
|
||||
}
|
||||
|
||||
@@ -90,7 +91,8 @@ impl Session {
|
||||
ctx.out.status(&format!("connecting to {address}…"));
|
||||
reachable(&address, REACH_TIMEOUT)?;
|
||||
|
||||
let client = Arc::new(login(ctx.settings.clone(), &address)?);
|
||||
let client: Arc<dyn SessionApi> =
|
||||
Arc::new(login(ctx.settings.clone(), &address)?);
|
||||
ctx.out
|
||||
.status(&format!("logged in as {}", ctx.settings.username));
|
||||
let _ = ctx.session.set(Arc::clone(&client));
|
||||
@@ -184,7 +186,7 @@ fn login(settings: ClientSettings, address: &str) -> CliResult<Client> {
|
||||
/// Events already queued are discarded first: servers push a room list at
|
||||
/// login, and counting that one would answer a question we had not asked yet.
|
||||
pub fn await_room_list(
|
||||
client: &Client,
|
||||
client: &dyn SessionApi,
|
||||
timeout: Duration,
|
||||
poll: Duration,
|
||||
) -> Option<Vec<RoomInfo>> {
|
||||
@@ -215,7 +217,7 @@ pub fn await_room_list(
|
||||
/// message reached the socket. The room list is the cheapest such request and
|
||||
/// every server answers it, which lets `message send` and `room say` report
|
||||
/// delivery honestly instead of sleeping and hoping.
|
||||
pub fn confirm_flush(client: &Client, timeout: Duration) -> bool {
|
||||
pub fn confirm_flush(client: &dyn SessionApi, timeout: Duration) -> bool {
|
||||
await_room_list(client, timeout, Duration::from_millis(50)).is_some()
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn whoami(ctx: &Ctx) -> CliResult {
|
||||
shared_folders: folders,
|
||||
shared_files: files,
|
||||
download_dir: ctx.download_dir.clone(),
|
||||
privilege_seconds: own_privileges(&session.client),
|
||||
privilege_seconds: own_privileges(session.client.as_ref()),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -40,7 +40,7 @@ pub fn whoami(ctx: &Ctx) -> CliResult {
|
||||
///
|
||||
/// A server that does not answer leaves the field null rather than reporting a
|
||||
/// zero, because "no privileges" and "did not say" are different facts.
|
||||
fn own_privileges(client: &soulseek_rs::Client) -> Option<u32> {
|
||||
fn own_privileges(client: &dyn crate::api::SessionApi) -> Option<u32> {
|
||||
if client.check_privileges().is_err() {
|
||||
return None;
|
||||
}
|
||||
@@ -159,7 +159,7 @@ pub fn serve(
|
||||
}
|
||||
}
|
||||
if let Some(sweeper) = sweeper.as_mut() {
|
||||
sweeper.tick(ctx, &session.client);
|
||||
sweeper.tick(ctx, session.client.as_ref());
|
||||
}
|
||||
std::thread::sleep(POLL);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ use crate::output::{
|
||||
RoomEventRecord, RoomMemberRecord, RoomRecord,
|
||||
};
|
||||
use soulseek_rs::types::RoomEvent;
|
||||
use soulseek_rs::{Client, SharedDirectory};
|
||||
use crate::api::SessionApi;
|
||||
use soulseek_rs::SharedDirectory;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// How long to wait for the server to acknowledge something we sent.
|
||||
@@ -30,7 +31,7 @@ pub fn full_path(directory: &str, basename: &str) -> String {
|
||||
|
||||
/// Ask a peer for its share listing and wait for the answer.
|
||||
pub fn fetch_listing(
|
||||
client: &Client,
|
||||
client: &dyn SessionApi,
|
||||
user: &str,
|
||||
timeout: Duration,
|
||||
) -> CliResult<Vec<SharedDirectory>> {
|
||||
@@ -57,7 +58,7 @@ pub fn browse(ctx: &Ctx, args: &BrowseArgs) -> CliResult {
|
||||
.status(&format!("requesting {}'s shared files…", args.user));
|
||||
|
||||
let directories = fetch_listing(
|
||||
&session.client,
|
||||
session.client.as_ref(),
|
||||
&args.user,
|
||||
Duration::from_secs(args.timeout),
|
||||
)?;
|
||||
@@ -89,7 +90,7 @@ pub fn room_list(ctx: &Ctx, timeout: Duration) -> CliResult {
|
||||
ctx.out.status("fetching the room list…");
|
||||
|
||||
let Some(mut rooms) =
|
||||
super::await_room_list(&session.client, timeout, POLL)
|
||||
super::await_room_list(session.client.as_ref(), timeout, POLL)
|
||||
else {
|
||||
return Err(CliError::timeout(format!(
|
||||
"the server sent no room list within {}s",
|
||||
@@ -123,7 +124,7 @@ pub fn room_say(ctx: &Ctx, room: &str, message: &str) -> CliResult {
|
||||
CliError::connection(format!("cannot post to {room}: {e}"))
|
||||
})?;
|
||||
|
||||
if confirm_flush(&session.client, FLUSH_TIMEOUT) {
|
||||
if confirm_flush(session.client.as_ref(), FLUSH_TIMEOUT) {
|
||||
ctx.out.status(&format!("posted to {room}"));
|
||||
return Ok(());
|
||||
}
|
||||
@@ -248,7 +249,7 @@ pub fn message_send(ctx: &Ctx, user: &str, message: &str) -> CliResult {
|
||||
CliError::connection(format!("cannot send to {user}: {e}"))
|
||||
})?;
|
||||
|
||||
if confirm_flush(&session.client, FLUSH_TIMEOUT) {
|
||||
if confirm_flush(session.client.as_ref(), FLUSH_TIMEOUT) {
|
||||
ctx.out.status(&format!("sent to {user}"));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ use crate::output::{
|
||||
CliError, CliResult, DownloadRecord, Exit, Out, SearchRecord,
|
||||
};
|
||||
use soulseek_rs::utils::path::expand_tilde;
|
||||
use soulseek_rs::{Client, DownloadStatus, SearchResult};
|
||||
use crate::api::SessionApi;
|
||||
use soulseek_rs::{DownloadStatus, SearchResult};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::io::BufRead;
|
||||
use std::path::PathBuf;
|
||||
@@ -241,7 +242,7 @@ pub fn download(ctx: &Ctx, args: &DownloadArgs) -> CliResult {
|
||||
|
||||
ensure_download_dir(&ctx.download_dir)?;
|
||||
let session = Session::open(ctx)?;
|
||||
let requests = resolve_sizes(ctx, &session.client, requests)?;
|
||||
let requests = resolve_sizes(ctx, session.client.as_ref(), requests)?;
|
||||
download_all(
|
||||
ctx,
|
||||
&session.client,
|
||||
@@ -283,7 +284,7 @@ pub fn get(ctx: &Ctx, args: &GetArgs) -> CliResult {
|
||||
/// late responders are often the better sources.
|
||||
fn collect(
|
||||
ctx: &Ctx,
|
||||
client: &Arc<Client>,
|
||||
client: &Arc<dyn SessionApi>,
|
||||
query: &str,
|
||||
stop_early: bool,
|
||||
) -> CliResult<Vec<SearchResult>> {
|
||||
@@ -365,7 +366,7 @@ fn dedupe(
|
||||
/// and saved empty, so look it up in the peer's share listing first.
|
||||
fn resolve_sizes(
|
||||
ctx: &Ctx,
|
||||
client: &Client,
|
||||
client: &dyn SessionApi,
|
||||
requests: Vec<Request>,
|
||||
) -> CliResult<Vec<Request>> {
|
||||
if requests.iter().all(|request| request.size > 0) {
|
||||
@@ -434,7 +435,7 @@ fn ensure_download_dir(download_dir: &str) -> CliResult {
|
||||
/// each result as it lands. Any failure fails the command.
|
||||
fn download_all(
|
||||
ctx: &Ctx,
|
||||
client: &Arc<Client>,
|
||||
client: &Arc<dyn SessionApi>,
|
||||
requests: Vec<Request>,
|
||||
timeout: Duration,
|
||||
) -> CliResult {
|
||||
@@ -452,7 +453,7 @@ fn download_all(
|
||||
queue.lock().ok().and_then(|mut q| q.pop_front())
|
||||
{
|
||||
let outcome = download_one(
|
||||
client,
|
||||
client.as_ref(),
|
||||
&ctx.out,
|
||||
&request,
|
||||
&ctx.download_dir,
|
||||
@@ -499,7 +500,7 @@ fn download_all(
|
||||
/// on the status channel rather than through the call that starts the
|
||||
/// transfer, so the channel is the only thing worth believing.
|
||||
fn download_one(
|
||||
client: &Client,
|
||||
client: &dyn SessionApi,
|
||||
out: &Out,
|
||||
request: &Request,
|
||||
download_dir: &str,
|
||||
|
||||
@@ -9,6 +9,7 @@ use super::{Ctx, Session, settings::Store, transfer};
|
||||
use crate::cli::{SortKey, WishRunArgs};
|
||||
use crate::output::{CliError, CliResult, Out, WishRecord};
|
||||
use crate::persist::config::FileConfig;
|
||||
use crate::api::SessionApi;
|
||||
use soulseek_rs::Client;
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -56,7 +57,7 @@ pub fn run(ctx: &Ctx, store: &Store, args: &WishRunArgs) -> CliResult {
|
||||
let wishes = selected(&store.config, args.only.as_deref())?;
|
||||
let session = Session::open(ctx)?;
|
||||
|
||||
if !sweep(ctx, &session.client, &wishes, &Sweep::from(args)) {
|
||||
if !sweep(ctx, session.client.as_ref(), &wishes, &Sweep::from(args)) {
|
||||
return Err(CliError::no_results(
|
||||
"no wish matched anything on the network",
|
||||
));
|
||||
@@ -121,7 +122,7 @@ impl Sweeper {
|
||||
/// unreported and `--duration` overshoots by that much. Move the sweep to its
|
||||
/// own thread with a channel of records if that ever matters; what mattered
|
||||
/// was that it used to be one window *per wish*.
|
||||
pub fn tick(&mut self, ctx: &Ctx, client: &Client) {
|
||||
pub fn tick(&mut self, ctx: &Ctx, client: &dyn SessionApi) {
|
||||
if Instant::now() < self.next {
|
||||
return;
|
||||
}
|
||||
@@ -139,7 +140,12 @@ impl Sweeper {
|
||||
/// All the searches go out before anything is waited for: peers answer whenever
|
||||
/// they get round to it, so one shared window covers every wish, where waiting
|
||||
/// per wish would cost a full window each.
|
||||
fn sweep(ctx: &Ctx, client: &Client, wishes: &[String], how: &Sweep) -> bool {
|
||||
fn sweep(
|
||||
ctx: &Ctx,
|
||||
client: &dyn SessionApi,
|
||||
wishes: &[String],
|
||||
how: &Sweep,
|
||||
) -> bool {
|
||||
let mut sent = Vec::new();
|
||||
for wish in wishes {
|
||||
match client.start_wishlist_search(wish) {
|
||||
|
||||
Reference in New Issue
Block a user