use std::cell::{Cell, RefCell};
use std::rc::Rc;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::{JsCast, JsValue};
use crate::error::Error;
struct Handles {
window: web_sys::Window,
document: web_sys::Document,
live_socket: JsValue,
exec_js: js_sys::Function,
}
pub(crate) type ResetHookId = u64;
thread_local! {
static HANDLES: RefCell<Option<Handles>> = const { RefCell::new(None) };
static RESET_HOOKS: RefCell<Vec<(ResetHookId, Rc<dyn Fn()>)>> =
const { RefCell::new(Vec::new()) };
static NEXT_HOOK_ID: Cell<ResetHookId> = const { Cell::new(0) };
static RESET_LISTENER_INSTALLED: Cell<bool> = const { Cell::new(false) };
static RESETTING: Cell<bool> = const { Cell::new(false) };
}
struct ResetGuard;
impl ResetGuard {
fn try_acquire() -> Option<Self> {
RESETTING.with(|cell| {
if cell.replace(true) { None } else { Some(Self) }
})
}
}
impl Drop for ResetGuard {
fn drop(&mut self) {
RESETTING.with(|cell| cell.set(false));
}
}
fn ensure_handles(slot: &mut Option<Handles>) -> Result<(), Error> {
if slot.is_some() {
return Ok(());
}
let window = web_sys::window().ok_or(Error::NoWindow)?;
let document = window.document().ok_or(Error::NoDocument)?;
let live_socket = js_sys::Reflect::get(&window, &JsValue::from_str("liveSocket"))
.map_err(|_| Error::NoLiveSocket)?;
if live_socket.is_undefined() || live_socket.is_null() {
return Err(Error::NoLiveSocket);
}
let exec_js_val = js_sys::Reflect::get(&live_socket, &JsValue::from_str("execJS"))
.map_err(|_| Error::NoLiveSocket)?;
let exec_js: js_sys::Function = exec_js_val.dyn_into().map_err(|_| Error::NoLiveSocket)?;
*slot = Some(Handles {
window,
document,
live_socket,
exec_js,
});
install_reset_listener(&slot.as_ref().unwrap().window);
Ok(())
}
fn install_reset_listener(window: &web_sys::Window) {
RESET_LISTENER_INSTALLED.with(|installed| {
if installed.get() {
return;
}
let closure = Closure::<dyn Fn()>::new(reset);
match window.add_event_listener_with_callback(
"phx:page-loading-stop",
closure.as_ref().unchecked_ref(),
) {
Ok(()) => {
closure.forget();
}
Err(error) => {
web_sys::console::error_1(&JsValue::from_str(&format!(
"wasm_liveview::cache: could not install phx:page-loading-stop listener: {error:?}"
)));
}
}
installed.set(true);
});
}
fn reset() {
let _guard = match ResetGuard::try_acquire() {
Some(guard) => guard,
None => return,
};
HANDLES.with(|cell| {
cell.borrow_mut().take();
});
let hooks: Vec<Rc<dyn Fn()>> = RESET_HOOKS.with(|registry| {
registry
.borrow()
.iter()
.map(|(_, hook)| Rc::clone(hook))
.collect()
});
for hook in hooks {
hook();
}
}
pub(crate) fn register_reset_hook(hook: Rc<dyn Fn()>) -> ResetHookId {
let id = NEXT_HOOK_ID.with(|cell| {
let id = cell.get();
cell.set(id.wrapping_add(1));
id
});
RESET_HOOKS.with(|registry| registry.borrow_mut().push((id, hook)));
id
}
pub(crate) fn unregister_reset_hook(id: ResetHookId) {
RESET_HOOKS.with(|registry| {
registry.borrow_mut().retain(|(hook_id, _)| *hook_id != id);
});
}
pub fn window() -> Result<web_sys::Window, Error> {
HANDLES.with(|cell| {
let mut slot = cell.borrow_mut();
ensure_handles(&mut slot)?;
Ok(slot.as_ref().unwrap().window.clone())
})
}
pub fn document() -> Result<web_sys::Document, Error> {
HANDLES.with(|cell| {
let mut slot = cell.borrow_mut();
ensure_handles(&mut slot)?;
Ok(slot.as_ref().unwrap().document.clone())
})
}
pub fn with_live_view<Return, Callback>(callback: Callback) -> Result<Return, Error>
where
Callback: FnOnce(&js_sys::Function, &JsValue, &web_sys::Element) -> Result<Return, Error>,
{
HANDLES.with(|cell| {
let mut slot = cell.borrow_mut();
ensure_handles(&mut slot)?;
let handles = slot.as_ref().unwrap();
let root = handles
.document
.query_selector("[data-phx-session]")
.ok()
.flatten()
.ok_or(Error::NoLiveViewRoot)?;
callback(&handles.exec_js, &handles.live_socket, &root)
})
}