sim/ui/src/api.rs

133 lines
3.8 KiB
Rust
Raw Normal View History

use gloo_net::http::Request;
use gloo_timers::future::TimeoutFuture;
use leptos::task::spawn_local;
use serde::de::DeserializeOwned;
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::Closure;
use web_sys::{MessageEvent, WebSocket};
thread_local! {
static SOCKET: RefCell<Option<WebSocket>> = const { RefCell::new(None) };
}
fn endpoint(path: &str) -> String {
let admin = web_sys::window()
.and_then(|window| window.location().pathname().ok())
.is_some_and(|pathname| pathname == "/admin" || pathname.starts_with("/admin/"));
if admin {
format!("/admin{path}")
} else {
path.to_owned()
}
}
/// Opens the live socket and calls `on_state` with each pushed snapshot; reconnects after a drop.
pub fn connect(on_state: impl Fn(String) + 'static) {
let Some(loc) = web_sys::window().map(|w| w.location()) else {
return;
};
let on_state = Rc::new(on_state);
let proto = if loc.protocol().is_ok_and(|p| p == "https:") {
"wss"
} else {
"ws"
};
let Ok(host) = loc.host() else { return };
open(
&format!("{proto}://{host}{}", endpoint("/ws")),
on_state.clone(),
);
spawn_local(async move {
loop {
TimeoutFuture::new(1000).await;
let connected = SOCKET.with(|socket| {
socket
.borrow()
.as_ref()
.is_some_and(|socket| socket.ready_state() == WebSocket::OPEN)
});
if connected {
continue;
}
if let Some(state) = get_text("/api/state").await {
on_state(state);
}
}
});
}
fn open(url: &str, on_state: Rc<dyn Fn(String)>) {
let Ok(ws) = WebSocket::new(url) else { return };
let cb = on_state.clone();
let onmessage = Closure::<dyn FnMut(MessageEvent)>::new(move |e: MessageEvent| {
if let Some(text) = e.data().as_string() {
cb(text);
}
});
ws.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
onmessage.forget();
let url = url.to_string();
let onclose = Closure::<dyn FnMut()>::new(move || {
SOCKET.with(|s| *s.borrow_mut() = None);
let (url, on_state) = (url.clone(), on_state.clone());
spawn_local(async move {
TimeoutFuture::new(1000).await;
open(&url, on_state);
});
});
ws.set_onclose(Some(onclose.as_ref().unchecked_ref()));
onclose.forget();
SOCKET.with(|s| *s.borrow_mut() = Some(ws));
}
pub async fn get_json<T: DeserializeOwned>(url: &str) -> Option<T> {
let url = endpoint(url);
Request::get(&url).send().await.ok()?.json().await.ok()
}
pub async fn get_text(url: &str) -> Option<String> {
let url = endpoint(url);
Request::get(&url).send().await.ok()?.text().await.ok()
}
/// Returns (success, response body).
pub async fn post(url: &str, body: String) -> Option<(bool, String)> {
let url = endpoint(url);
let r = Request::post(&url).body(body).ok()?.send().await.ok()?;
Some((r.ok(), r.text().await.ok()?))
}
pub fn edit(cmd: String) {
let sent = SOCKET.with(|s| {
s.borrow()
.as_ref()
.filter(|w| w.ready_state() == WebSocket::OPEN)
.is_some_and(|w| w.send_with_str(&cmd).is_ok())
});
if !sent {
spawn_local(async move {
let _ = post("/api/edit", cmd).await;
});
}
}
fn storage() -> Option<web_sys::Storage> {
web_sys::window()?.local_storage().ok().flatten()
}
pub fn vim_pref() -> bool {
storage()
.and_then(|s| s.get_item("vim").ok().flatten())
.is_none_or(|v| v != "0")
}
pub fn set_vim_pref(on: bool) {
if let Some(s) = storage() {
let _ = s.set_item("vim", if on { "1" } else { "0" });
}
}