use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex as StdMutex};
use tokio::sync::{broadcast, RwLock};
use super::client_id_store::{ClientIdStore, InMemoryClientIdStore};
use super::runtime::{spawn, HostHandleTx};
use super::types::{
generate_client_id, HostClientHandle, HostConfig, HostError, HostEvent, HostEventStream,
HostHandle, HostId, HostState, HostSubscriptionEvent, HostSubscriptionStream, HostedAgent,
HostedSessionSummary,
};
const DEFAULT_EVENT_BUFFER: usize = 1024;
#[derive(Clone)]
pub struct MultiHostClient {
inner: Arc<MultiInner>,
}
struct MultiInner {
hosts: RwLock<HashMap<HostId, HostHandleTx>>,
pending_host_ids: StdMutex<HashSet<HostId>>,
fan_out: broadcast::Sender<HostSubscriptionEvent>,
host_events: broadcast::Sender<HostEvent>,
client_id_store: Arc<dyn ClientIdStore>,
}
impl MultiHostClient {
pub fn new() -> Self {
Self::with_client_id_store(Arc::new(InMemoryClientIdStore::new()))
}
pub fn with_client_id_store(client_id_store: Arc<dyn ClientIdStore>) -> Self {
let (fan_out, _) = broadcast::channel(DEFAULT_EVENT_BUFFER);
let (host_events, _) = broadcast::channel(DEFAULT_EVENT_BUFFER);
Self {
inner: Arc::new(MultiInner {
hosts: RwLock::new(HashMap::new()),
pending_host_ids: StdMutex::new(HashSet::new()),
fan_out,
host_events,
client_id_store,
}),
}
}
pub async fn single(config: HostConfig) -> Result<(Self, HostHandle), HostError> {
let multi = Self::new();
let handle = multi.add_host(config).await?;
Ok((multi, handle))
}
pub async fn add_host(&self, config: HostConfig) -> Result<HostHandle, HostError> {
let id = config.id.clone();
let mut pending_guard = {
let hosts = self.inner.hosts.read().await;
let mut pending = self
.inner
.pending_host_ids
.lock()
.expect("pending_host_ids mutex poisoned");
if hosts.contains_key(&id) || pending.contains(&id) {
return Err(HostError::DuplicateHost(id));
}
pending.insert(id.clone());
PendingHostGuard {
inner: self.inner.clone(),
id: id.clone(),
armed: true,
}
};
let resolved = self
.resolve_client_id(&id, config.client_id.as_deref())
.await?;
let shared = {
let mut hosts = self.inner.hosts.write().await;
if hosts.contains_key(&id) {
return Err(HostError::DuplicateHost(id));
}
let tx = spawn(
config,
resolved,
self.inner.fan_out.clone(),
self.inner.host_events.clone(),
);
let shared = tx.shared.clone();
hosts.insert(id.clone(), tx);
shared
};
pending_guard.commit();
let snapshot = shared.lock().await.snapshot();
Ok(snapshot)
}
async fn resolve_client_id(
&self,
host_id: &HostId,
explicit: Option<&str>,
) -> Result<String, HostError> {
let store = &self.inner.client_id_store;
let resolved = if let Some(value) = explicit {
value.to_owned()
} else {
match store.load(host_id.clone()).await {
Ok(Some(stored)) => stored,
Ok(None) => generate_client_id(),
Err(error) => {
return Err(HostError::ClientIdStore {
host: host_id.clone(),
error,
})
}
}
};
if let Err(error) = store.store(host_id.clone(), resolved.clone()).await {
return Err(HostError::ClientIdStore {
host: host_id.clone(),
error,
});
}
Ok(resolved)
}
pub async fn remove_host(&self, id: &HostId) -> Result<(), HostError> {
let entry = {
let mut hosts = self.inner.hosts.write().await;
hosts.remove(id)
};
match entry {
Some(tx) => {
tx.shutdown().await;
let _ = self.inner.host_events.send(HostEvent::Removed {
host_id: id.clone(),
});
Ok(())
}
None => Err(HostError::UnknownHost(id.clone())),
}
}
pub async fn reconnect_host(&self, id: &HostId) -> Result<(), HostError> {
let tx = self
.inner
.hosts
.read()
.await
.get(id)
.map(|entry| entry.cmd_tx.clone())
.ok_or_else(|| HostError::UnknownHost(id.clone()))?;
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
tx.send(super::runtime::HostCommand::Reconnect { reply: reply_tx })
.await
.map_err(|_| HostError::HostShutDown(id.clone()))?;
reply_rx
.await
.map_err(|_| HostError::HostShutDown(id.clone()))?;
Ok(())
}
pub async fn reconnect_all_unavailable(&self) -> HashMap<HostId, HostError> {
let snapshots: Vec<(HostId, Arc<super::types::HostShared>)> = {
let hosts = self.inner.hosts.read().await;
let mut out = Vec::with_capacity(hosts.len());
for (id, entry) in hosts.iter() {
out.push((id.clone(), entry.shared.clone()));
}
out
};
let mut targets: Vec<HostId> = Vec::new();
for (id, shared) in snapshots {
let state = shared.lock().await.state.clone();
match state {
HostState::Connected | HostState::Connecting => continue,
HostState::Disconnected
| HostState::Reconnecting { .. }
| HostState::Failed { .. } => {
targets.push(id);
}
}
}
if targets.is_empty() {
return HashMap::new();
}
let mut handles: Vec<(HostId, tokio::task::JoinHandle<Result<(), HostError>>)> =
Vec::with_capacity(targets.len());
for id in targets {
let multi = self.clone();
let task_id = id.clone();
handles.push((
id,
tokio::spawn(async move { multi.reconnect_host(&task_id).await }),
));
}
let mut errors = HashMap::new();
for (id, handle) in handles {
match handle.await {
Ok(Ok(())) => {}
Ok(Err(err)) => {
errors.insert(id, err);
}
Err(join_err) => {
tracing::error!(
host_id = %id,
error = ?join_err,
"reconnect task failed to join"
);
errors.insert(id.clone(), HostError::HostShutDown(id));
}
}
}
errors
}
pub async fn host(&self, id: &HostId) -> Option<HostHandle> {
let shared = self
.inner
.hosts
.read()
.await
.get(id)
.map(|e| e.shared.clone())?;
let snapshot = shared.lock().await.snapshot();
Some(snapshot)
}
pub async fn hosts(&self) -> Vec<HostHandle> {
let shareds: Vec<_> = self
.inner
.hosts
.read()
.await
.values()
.map(|e| e.shared.clone())
.collect();
let mut out = Vec::with_capacity(shareds.len());
for shared in shareds {
out.push(shared.lock().await.snapshot());
}
out
}
pub async fn client(&self, id: &HostId) -> Option<HostClientHandle> {
let shared = self
.inner
.hosts
.read()
.await
.get(id)
.map(|e| e.shared.clone())?;
let state = shared.lock().await;
let client = state.current_client.clone()?;
Some(HostClientHandle {
host_id: id.clone(),
generation: state.generation,
client,
shared: shared.clone(),
})
}
pub async fn subscribe(
&self,
host_id: &HostId,
uri: String,
) -> Result<ahp_types::commands::SubscribeResult, HostError> {
let entry = self.host_entry(host_id).await?;
entry.subscribe(uri).await
}
pub async fn unsubscribe(&self, host_id: &HostId, uri: String) -> Result<(), HostError> {
let entry = self.host_entry(host_id).await?;
entry.unsubscribe(uri).await
}
pub async fn dispatch(
&self,
host_id: &HostId,
channel: ahp_types::common::Uri,
action: ahp_types::actions::StateAction,
) -> Result<crate::DispatchHandle, HostError> {
let entry = self.host_entry(host_id).await?;
entry.dispatch(channel, action).await
}
pub fn events(&self) -> HostSubscriptionStream {
HostSubscriptionStream {
rx: self.inner.fan_out.subscribe(),
}
}
pub fn host_events(&self) -> HostEventStream {
HostEventStream {
rx: self.inner.host_events.subscribe(),
}
}
pub async fn aggregated_sessions(&self) -> Vec<HostedSessionSummary> {
let mut out = Vec::new();
for handle in self.hosts().await {
for summary in handle.session_summaries.iter().cloned() {
out.push(HostedSessionSummary {
host_id: handle.id.clone(),
host_label: handle.label.clone(),
summary,
});
}
}
out.sort_by_key(|item| std::cmp::Reverse(item.summary.modified_at.clone()));
out
}
pub async fn aggregated_agents(&self) -> Vec<HostedAgent> {
let mut out = Vec::new();
for handle in self.hosts().await {
for agent in handle.agents.iter().cloned() {
out.push(HostedAgent {
host_id: handle.id.clone(),
host_label: handle.label.clone(),
agent,
});
}
}
out
}
async fn host_entry(&self, id: &HostId) -> Result<HostHandleTxRef, HostError> {
self.inner
.hosts
.read()
.await
.get(id)
.map(|e| HostHandleTxRef {
cmd_tx: e.cmd_tx.clone(),
shared: e.shared.clone(),
})
.ok_or_else(|| HostError::UnknownHost(id.clone()))
}
}
impl Default for MultiHostClient {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for MultiHostClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MultiHostClient").finish_non_exhaustive()
}
}
struct PendingHostGuard {
inner: Arc<MultiInner>,
id: HostId,
armed: bool,
}
impl PendingHostGuard {
fn commit(&mut self) {
if self.armed {
self.remove_pending();
self.armed = false;
}
}
fn remove_pending(&self) {
let mut pending = self
.inner
.pending_host_ids
.lock()
.expect("pending_host_ids mutex poisoned");
pending.remove(&self.id);
}
}
impl Drop for PendingHostGuard {
fn drop(&mut self) {
if self.armed {
self.remove_pending();
}
}
}
struct HostHandleTxRef {
cmd_tx: tokio::sync::mpsc::Sender<super::runtime::HostCommand>,
shared: Arc<super::types::HostShared>,
}
impl HostHandleTxRef {
async fn subscribe(
&self,
uri: String,
) -> Result<ahp_types::commands::SubscribeResult, HostError> {
let (tx, rx) = tokio::sync::oneshot::channel();
let host_id = self.host_id().await;
self.cmd_tx
.send(super::runtime::HostCommand::Subscribe { uri, reply: tx })
.await
.map_err(|_| HostError::HostShutDown(host_id.clone()))?;
rx.await.map_err(|_| HostError::HostShutDown(host_id))?
}
async fn unsubscribe(&self, uri: String) -> Result<(), HostError> {
let (tx, rx) = tokio::sync::oneshot::channel();
let host_id = self.host_id().await;
self.cmd_tx
.send(super::runtime::HostCommand::Unsubscribe { uri, reply: tx })
.await
.map_err(|_| HostError::HostShutDown(host_id.clone()))?;
rx.await.map_err(|_| HostError::HostShutDown(host_id))?
}
async fn dispatch(
&self,
channel: ahp_types::common::Uri,
action: ahp_types::actions::StateAction,
) -> Result<crate::DispatchHandle, HostError> {
let (tx, rx) = tokio::sync::oneshot::channel();
let host_id = self.host_id().await;
self.cmd_tx
.send(super::runtime::HostCommand::Dispatch {
channel,
action: Box::new(action),
reply: tx,
})
.await
.map_err(|_| HostError::HostShutDown(host_id.clone()))?;
rx.await.map_err(|_| HostError::HostShutDown(host_id))?
}
async fn host_id(&self) -> HostId {
self.shared.lock().await.id.clone()
}
}
impl HostState {
pub fn is_connected(&self) -> bool {
matches!(self, HostState::Connected)
}
pub fn is_failed(&self) -> bool {
matches!(self, HostState::Failed { .. })
}
}