use std::cell::RefCell;
use std::collections::HashMap;
use std::mem::ManuallyDrop;
use reactive_core::RwSignal;
use serde::Serialize;
use serde::de::DeserializeOwned;
type HotReader = Box<dyn Fn() -> Option<String>>;
thread_local! {
static REGISTRY: ManuallyDrop<RefCell<HashMap<String, HotReader>>> =
ManuallyDrop::new(RefCell::new(HashMap::new()));
static PENDING: ManuallyDrop<RefCell<HashMap<String, String>>> =
ManuallyDrop::new(RefCell::new(HashMap::new()));
}
pub fn hot_signal<T>(key: &str, init: T) -> RwSignal<T>
where
T: Clone + Serialize + DeserializeOwned + 'static,
{
let restored = PENDING
.with(|p| p.borrow_mut().remove(key))
.and_then(|raw| serde_json::from_str::<T>(&raw).ok());
let sig = reactive_core::signal(restored.unwrap_or(init));
let reader = sig.clone();
REGISTRY.with(|r| {
r.borrow_mut().insert(
key.to_string(),
Box::new(move || serde_json::to_string(&reader.peek()).ok()),
);
});
sig
}
const THEME_MODE_KEY: &str = "@telar/theme.mode";
pub fn hot_snapshot_json() -> String {
let mut map: HashMap<String, String> = REGISTRY.with(|r| {
r.borrow()
.iter()
.filter_map(|(key, read)| read().map(|value| (key.clone(), value)))
.collect()
});
if let Some(mode) = theme_core::active_mode() {
map.insert(THEME_MODE_KEY.to_string(), mode);
}
serde_json::to_string(&map).unwrap_or_default()
}
pub fn hot_restore_json(blob: &str) {
if let Ok(mut map) = serde_json::from_str::<HashMap<String, String>>(blob) {
if let Some(mode) = map.remove(THEME_MODE_KEY) {
theme_core::set_mode(mode);
}
PENDING.with(|p| p.borrow_mut().extend(map));
}
}
#[doc(hidden)]
pub mod probe {
use super::*;
pub struct Probe<'a, T>(pub &'a T);
pub struct SerdeTag;
pub struct PlainTag;
pub trait SerdeKind {
fn kind(&self) -> SerdeTag;
}
impl<'a, T: Clone + Serialize + DeserializeOwned + 'static> SerdeKind for Probe<'a, T> {
fn kind(&self) -> SerdeTag {
SerdeTag
}
}
pub trait PlainKind {
fn kind(&self) -> PlainTag;
}
impl<'a, T> PlainKind for &Probe<'a, T> {
fn kind(&self) -> PlainTag {
PlainTag
}
}
impl SerdeTag {
pub fn make<T: Clone + Serialize + DeserializeOwned + 'static>(
self,
key: &str,
init: T,
) -> RwSignal<T> {
hot_signal(key, init)
}
}
impl PlainTag {
pub fn make<T: 'static>(self, key: &str, init: T) -> RwSignal<T> {
let _ = key;
reactive_core::signal(init)
}
}
}
#[macro_export]
macro_rules! hot_signal_auto {
($key:expr, $init:expr) => {{
#[allow(unused_imports)]
use $crate::probe::{PlainKind as _, SerdeKind as _};
let __rsx_hot_init = $init;
(&$crate::probe::Probe(&__rsx_hot_init))
.kind()
.make($key, __rsx_hot_init)
}};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snapshot_and_restore_roundtrip() {
let a = hot_signal("t::a", 1i32);
let b = hot_signal("t::b", String::from("hi"));
a.set(41);
b.set("hola".to_string());
let blob = hot_snapshot_json();
hot_restore_json(&blob);
let a2 = hot_signal("t::a", 0i32);
let b2 = hot_signal("t::b", String::new());
assert_eq!(a2.peek(), 41);
assert_eq!(b2.peek(), "hola");
}
#[test]
fn missing_or_corrupt_values_fall_back_to_init() {
hot_restore_json("{\"t2::x\": \"not-an-int\"}");
let x = hot_signal("t2::x", 7i32);
assert_eq!(x.peek(), 7);
let y = hot_signal("t2::y", 3i32);
assert_eq!(y.peek(), 3);
}
#[test]
fn theme_mode_survives_snapshot_restore() {
theme_core::register_mode("midnight", || {});
theme_core::set_mode("midnight");
let blob = hot_snapshot_json();
theme_core::register_mode("modern", || {});
theme_core::set_mode("modern");
hot_restore_json(&blob);
assert_eq!(theme_core::active_mode().as_deref(), Some("midnight"));
}
#[test]
fn auto_macro_falls_back_for_non_serde_types() {
struct NotSerde(i32);
let plain = crate::hot_signal_auto!("t3::plain", NotSerde(5));
plain.update(|v| v.0 += 1);
assert_eq!(plain.with(|v| v.0), 6);
let kept = crate::hot_signal_auto!("t3::kept", 9i32);
assert_eq!(kept.peek(), 9);
assert!(hot_snapshot_json().contains("t3::kept"));
}
}