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> = 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) { let Ok(ws) = WebSocket::new(url) else { return }; let cb = on_state.clone(); let onmessage = Closure::::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::::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(url: &str) -> Option { let url = endpoint(url); Request::get(&url).send().await.ok()?.json().await.ok() } pub async fn get_text(url: &str) -> Option { 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::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" }); } }