use std::sync::Arc;
use futures::StreamExt;
use reqwest_eventsource::{Event, RequestBuilderExt};
use tokio::sync::{Mutex, watch};
use super::{FileTreeEvent, FileTreeNode};
pub type OnChange = Box<dyn Fn(&[FileTreeNode]) + Send + Sync>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("connect laboratory filetree: {0}")]
Connect(#[from] reqwest_eventsource::CannotCloneRequestError),
#[error("laboratory filetree http client: {0}")]
Client(#[from] reqwest::Error),
}
struct Shared {
state: Mutex<Vec<FileTreeNode>>,
changes: watch::Sender<u64>,
on_change: Option<OnChange>,
}
pub struct FileTreeBuilder {
url: String,
query: Vec<(String, String)>,
signature: Option<String>,
on_change: Option<OnChange>,
}
impl FileTreeBuilder {
pub fn path(mut self, path: impl Into<String>) -> Self {
self.query.push(("path".to_string(), path.into()));
self
}
pub fn signature(mut self, signature: impl Into<String>) -> Self {
self.signature = Some(signature.into());
self
}
pub fn host(
mut self,
machine: impl Into<String>,
machine_state: impl Into<String>,
) -> Self {
self.query.push(("machine".to_string(), machine.into()));
self
.query
.push(("machine_state".to_string(), machine_state.into()));
self
}
pub fn on_change(
mut self,
callback: impl Fn(&[FileTreeNode]) + Send + Sync + 'static,
) -> Self {
self.on_change = Some(Box::new(callback));
self
}
pub async fn connect(self) -> Result<FileTree, Error> {
let client = reqwest::Client::builder().build()?;
let mut request = client
.get(&self.url)
.header("Accept", "text/event-stream");
if !self.query.is_empty() {
request = request.query(&self.query);
}
if let Some(signature) = &self.signature {
request = request.header("X-OBJECTIVEAI-SIGNATURE", signature);
}
let source = request.eventsource()?;
let shared = Arc::new(Shared {
state: Mutex::new(Vec::new()),
changes: watch::channel(0u64).0,
on_change: self.on_change,
});
let pump = tokio::spawn(pump(source, shared.clone()));
Ok(FileTree { shared, pump })
}
}
pub struct FileTree {
shared: Arc<Shared>,
pump: tokio::task::JoinHandle<()>,
}
impl FileTree {
pub fn laboratory(base_url: impl Into<String>) -> FileTreeBuilder {
FileTreeBuilder {
url: format!("{}/filetree", base_url.into()),
query: Vec::new(),
signature: None,
on_change: None,
}
}
pub fn daemon(
base_url: impl Into<String>,
laboratory_id: impl AsRef<str>,
) -> FileTreeBuilder {
FileTreeBuilder {
url: format!(
"{}/laboratories/{}/filetree",
base_url.into(),
laboratory_id.as_ref(),
),
query: Vec::new(),
signature: None,
on_change: None,
}
}
pub async fn tree(&self) -> Vec<FileTreeNode> {
self.shared.state.lock().await.clone()
}
pub async fn subscribe(&self) {
let mut rx = self.shared.changes.subscribe();
let _ = rx.changed().await;
}
}
impl Drop for FileTree {
fn drop(&mut self) {
self.pump.abort();
}
}
async fn pump(mut source: reqwest_eventsource::EventSource, shared: Arc<Shared>) {
while let Some(event) = source.next().await {
match event {
Ok(Event::Open) => continue,
Ok(Event::Message(message)) => {
let Ok(event) = serde_json::from_str::<FileTreeEvent>(&message.data)
else {
continue;
};
let snapshot = {
let mut state = shared.state.lock().await;
event.apply(&mut state);
shared.on_change.as_ref().map(|_| state.clone())
};
if let (Some(callback), Some(snapshot)) =
(&shared.on_change, &snapshot)
{
callback(snapshot);
}
shared.changes.send_modify(|version| {
*version = version.wrapping_add(1);
});
}
Err(_) => break,
}
}
}