#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
#![doc = include_str!("../README.md")]
use std::borrow::Cow;
use json_patch::Patch;
use leptos::{create_signal, ReadSignal};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use wasm_bindgen::JsValue;
cfg_if::cfg_if! {
if #[cfg(all(feature = "actix", feature = "ssr"))] {
mod actix;
pub use crate::actix::*;
}
}
cfg_if::cfg_if! {
if #[cfg(all(feature = "axum", feature = "ssr"))] {
mod axum;
pub use crate::axum::*;
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerSignalUpdate {
name: Cow<'static, str>,
patch: Patch,
}
impl ServerSignalUpdate {
pub fn new<T>(
name: impl Into<Cow<'static, str>>,
old: &T,
new: &T,
) -> Result<Self, serde_json::Error>
where
T: Serialize,
{
let left = serde_json::to_value(old)?;
let right = serde_json::to_value(new)?;
let patch = json_patch::diff(&left, &right);
Ok(ServerSignalUpdate {
name: name.into(),
patch,
})
}
pub fn new_from_json<T>(name: impl Into<Cow<'static, str>>, old: &Value, new: &Value) -> Self {
let patch = json_patch::diff(old, new);
ServerSignalUpdate {
name: name.into(),
patch,
}
}
}
#[allow(unused_variables)]
pub fn provide_sse(url: &str) -> Result<(), JsValue> {
provide_sse_inner(url)
}
#[allow(unused_variables)]
pub fn create_sse_signal<T>(name: impl Into<Cow<'static, str>>) -> ReadSignal<T>
where
T: Default + Serialize + for<'de> Deserialize<'de>,
{
let name = name.into();
let (get, set) = create_signal(T::default());
cfg_if::cfg_if! {
if #[cfg(target_arch = "wasm32")] {
use leptos::{use_context, create_effect, create_rw_signal, SignalSet, SignalGet};
let signal = create_rw_signal(serde_json::to_value(T::default()).unwrap());
if let Some(ServerSignalEventSourceContext { state_signals, .. }) = use_context::<ServerSignalEventSourceContext>() {
let name: Cow<'static, str> = name.into();
state_signals.borrow_mut().insert(name.clone(), signal);
create_effect(move |_| {
let name = name.clone();
let new_value = serde_json::from_value(signal.get()).unwrap();
set.set(new_value);
});
} else {
leptos::logging::error!(
r#"server signal was used without a SSE being provided.
Ensure you call `leptos_sse::provide_sse("http://localhost:3000/sse")` at the highest level in your app."#
);
}
}
}
get
}
cfg_if::cfg_if! {
if #[cfg(target_arch = "wasm32")] {
use std::cell::RefCell;
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use web_sys::EventSource;
use leptos::{provide_context, RwSignal};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ServerSignalEventSource(pub EventSource);
impl Deref for ServerSignalEventSource {
type Target = EventSource;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ServerSignalEventSource {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ServerSignalEventSourceContext {
inner: EventSource,
state_signals: Rc<RefCell<HashMap<Cow<'static, str>, RwSignal<Value>>>>,
delayed_updates: Rc<RefCell<HashMap<Cow<'static, str>, Vec<Patch>>>>,
}
#[inline]
fn provide_sse_inner(url: &str) -> Result<(), JsValue> {
use web_sys::MessageEvent;
use wasm_bindgen::{prelude::Closure, JsCast};
use leptos::{use_context, SignalUpdate};
use js_sys::{Function, JsString};
if use_context::<ServerSignalEventSourceContext>().is_none() {
let es = EventSource::new(url)?;
provide_context(ServerSignalEventSource(es.clone()));
provide_context(ServerSignalEventSourceContext { inner: es, state_signals: Default::default(), delayed_updates: Default::default() });
}
let es = use_context::<ServerSignalEventSourceContext>().unwrap();
let handlers = es.state_signals.clone();
let delayed_updates = es.delayed_updates.clone();
let callback = Closure::wrap(Box::new(move |event: MessageEvent| {
let ws_string = event.data().dyn_into::<JsString>().unwrap().as_string().unwrap();
if let Ok(update_signal) = serde_json::from_str::<ServerSignalUpdate>(&ws_string) {
let handler_map = (*handlers).borrow();
let name = &update_signal.name;
let mut delayed_map = (*delayed_updates).borrow_mut();
if let Some(signal) = handler_map.get(name) {
if let Some(delayed_patches) = delayed_map.remove(name) {
signal.update(|doc| {
for patch in delayed_patches {
json_patch::patch(doc, &patch).unwrap();
}
});
}
signal.update(|doc| {
json_patch::patch(doc, &update_signal.patch).unwrap();
});
} else {
leptos::logging::warn!("No local state for update to {}. Queuing patch.", name);
delayed_map.entry(name.clone()).or_default().push(update_signal.patch.clone());
}
}
}) as Box<dyn FnMut(_)>);
let function: &Function = callback.as_ref().unchecked_ref();
es.inner.set_onmessage(Some(function));
callback.forget();
Ok(())
}
} else {
#[inline]
fn provide_sse_inner(_url: &str) -> Result<(), JsValue> {
Ok(())
}
}
}