use std::{
boxed::Box,
cell::Cell,
collections::HashMap,
fmt,
hash::Hash,
sync::{Arc, Mutex},
};
use uuid::Uuid;
pub fn is_event_name_valid(event: &str) -> bool {
event
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '/' || c == ':' || c == '_')
}
pub fn assert_event_name_is_valid(event: &str) {
assert!(
is_event_name_valid(event),
"Event name must include only alphanumeric characters, `-`, `/`, `:` and `_`."
);
}
#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct EventHandler(Uuid);
impl fmt::Display for EventHandler {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone)]
pub struct Event {
id: EventHandler,
data: Option<String>,
}
impl Event {
pub fn id(&self) -> EventHandler {
self.id
}
pub fn payload(&self) -> Option<&str> {
self.data.as_deref()
}
}
enum Pending {
Unlisten(EventHandler),
Listen(EventHandler, String, Handler),
Trigger(String, Option<String>, Option<String>),
}
struct Handler {
window: Option<String>,
callback: Box<dyn Fn(Event) + Send>,
}
struct InnerListeners {
handlers: Mutex<HashMap<String, HashMap<EventHandler, Handler>>>,
pending: Mutex<Vec<Pending>>,
function_name: Uuid,
listeners_object_name: Uuid,
}
pub(crate) struct Listeners {
inner: Arc<InnerListeners>,
}
impl Default for Listeners {
fn default() -> Self {
Self {
inner: Arc::new(InnerListeners {
handlers: Mutex::default(),
pending: Mutex::default(),
function_name: Uuid::new_v4(),
listeners_object_name: Uuid::new_v4(),
}),
}
}
}
impl Clone for Listeners {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl Listeners {
pub(crate) fn function_name(&self) -> String {
self.inner.function_name.to_string()
}
pub(crate) fn listeners_object_name(&self) -> String {
self.inner.listeners_object_name.to_string()
}
fn insert_pending(&self, action: Pending) {
self
.inner
.pending
.lock()
.expect("poisoned pending event queue")
.push(action)
}
fn flush_pending(&self) {
let pending = {
let mut lock = self
.inner
.pending
.lock()
.expect("poisoned pending event queue");
std::mem::take(&mut *lock)
};
for action in pending {
match action {
Pending::Unlisten(id) => self.unlisten(id),
Pending::Listen(id, event, handler) => self.listen_(id, event, handler),
Pending::Trigger(ref event, window, payload) => self.trigger(event, window, payload),
}
}
}
fn listen_(&self, id: EventHandler, event: String, handler: Handler) {
match self.inner.handlers.try_lock() {
Err(_) => self.insert_pending(Pending::Listen(id, event, handler)),
Ok(mut lock) => {
lock.entry(event).or_default().insert(id, handler);
}
}
}
pub(crate) fn listen<F: Fn(Event) + Send + 'static>(
&self,
event: String,
window: Option<String>,
handler: F,
) -> EventHandler {
let id = EventHandler(Uuid::new_v4());
let handler = Handler {
window,
callback: Box::new(handler),
};
self.listen_(id, event, handler);
id
}
pub(crate) fn once<F: FnOnce(Event) + Send + 'static>(
&self,
event: String,
window: Option<String>,
handler: F,
) -> EventHandler {
let self_ = self.clone();
let handler = Cell::new(Some(handler));
self.listen(event, window, move |event| {
self_.unlisten(event.id);
let handler = handler
.take()
.expect("attempted to call handler more than once");
handler(event)
})
}
pub(crate) fn unlisten(&self, handler_id: EventHandler) {
match self.inner.handlers.try_lock() {
Err(_) => self.insert_pending(Pending::Unlisten(handler_id)),
Ok(mut lock) => lock.values_mut().for_each(|handler| {
handler.remove(&handler_id);
}),
}
}
pub(crate) fn trigger(&self, event: &str, window: Option<String>, payload: Option<String>) {
let mut maybe_pending = false;
match self.inner.handlers.try_lock() {
Err(_) => self.insert_pending(Pending::Trigger(event.to_owned(), window, payload)),
Ok(lock) => {
if let Some(handlers) = lock.get(event) {
for (&id, handler) in handlers {
if handler.window.is_none() || window == handler.window {
maybe_pending = true;
(handler.callback)(self::Event {
id,
data: payload.clone(),
})
}
}
}
}
}
if maybe_pending {
self.flush_pending();
}
}
}
#[cfg(test)]
mod test {
use super::*;
use proptest::prelude::*;
fn event_fn(s: Event) {
println!("{s:?}");
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(10000))]
#[test]
fn listeners_check_key(e in "[a-z]+") {
let listeners: Listeners = Default::default();
let key = e.clone();
listeners.listen(e, None, event_fn);
let l = listeners.inner.handlers.lock().unwrap();
assert!(l.contains_key(&key));
}
#[test]
fn listeners_check_fn(e in "[a-z]+") {
let listeners: Listeners = Default::default();
let key = e.clone();
listeners.listen(e, None, event_fn);
let mut l = listeners.inner.handlers.lock().unwrap();
if l.contains_key(&key) {
let handler = l.get_mut(&key);
match handler {
Some(_) => {},
None => panic!("handler is None")
}
}
}
#[test]
fn check_on_event(key in "[a-z]+", d in "[a-z]+") {
let listeners: Listeners = Default::default();
listeners.listen(key.clone(), None, event_fn);
listeners.trigger(&key, None, Some(d));
let l = listeners.inner.handlers.lock().unwrap();
assert!(l.contains_key(&key));
}
}
}
pub fn unlisten_js(listeners_object_name: String, event_name: String, event_id: u32) -> String {
format!(
"
(function () {{
const listeners = (window['{listeners_object_name}'] || {{}})['{event_name}']
if (listeners) {{
const index = window['{listeners_object_name}']['{event_name}'].findIndex(e => e.id === {event_id})
if (index > -1) {{
window['{listeners_object_name}']['{event_name}'].splice(index, 1)
}}
}}
}})()
",
)
}
pub fn listen_js(
listeners_object_name: String,
event: String,
event_id: u32,
window_label: Option<String>,
handler: String,
) -> String {
format!(
"
(function () {{
if (window['{listeners}'] === void 0) {{
Object.defineProperty(window, '{listeners}', {{ value: Object.create(null) }});
}}
if (window['{listeners}'][{event}] === void 0) {{
Object.defineProperty(window['{listeners}'], {event}, {{ value: [] }});
}}
const eventListeners = window['{listeners}'][{event}]
const listener = {{
id: {event_id},
windowLabel: {window_label},
handler: {handler}
}};
eventListeners.push(listener);
}})()
",
listeners = listeners_object_name,
window_label = if let Some(l) = window_label {
crate::runtime::window::assert_label_is_valid(&l);
format!("'{l}'")
} else {
"null".to_owned()
},
)
}