millennium 1.0.0-beta.3

Create consistent, light, & secure apps that work on all platforms, using HTML, CSS, and JavaScript
Documentation
// Copyright 2022 pyke.io
//           2019-2021 Tauri Programme within The Commons Conservancy
//                     [https://tauri.studio/]
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::{
	boxed::Box,
	cell::Cell,
	collections::HashMap,
	fmt,
	hash::Hash,
	sync::{Arc, Mutex}
};

use uuid::Uuid;

/// Checks if an event name is valid.
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 `_`.");
}

/// Represents an event handler.
#[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)
	}
}

/// An event that was triggered.
#[derive(Debug, Clone)]
pub struct Event {
	id: EventHandler,
	data: Option<String>
}

impl Event {
	/// The [`EventHandler`] that was triggered.
	pub fn id(&self) -> EventHandler {
		self.id
	}

	/// The event payload.
	pub fn payload(&self) -> Option<&str> {
		self.data.as_deref()
	}
}

/// What to do with the pending handler when resolving it?
enum Pending {
	Unlisten(EventHandler),
	Listen(EventHandler, String, Handler),
	Trigger(String, Option<String>, Option<String>)
}

/// Stored in [`Listeners`] to be called upon when the event that stored it is
/// triggered.
struct Handler {
	window: Option<String>,
	callback: Box<dyn Fn(Event) + Send>
}

/// Holds event handlers and pending event handlers, along with the salts
/// associating them.
struct InnerListeners {
	handlers: Mutex<HashMap<String, HashMap<EventHandler, Handler>>>,
	pending: Mutex<Vec<Pending>>,
	function_name: Uuid,
	listeners_object_name: Uuid
}

/// A self-contained event manager.
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 {
	/// Randomly generated function name to represent the JavaScript event
	/// function.
	pub(crate) fn function_name(&self) -> String {
		self.inner.function_name.to_string()
	}

	/// Randomly generated listener object name to represent the JavaScript
	/// event listener object.
	pub(crate) fn listeners_object_name(&self) -> String {
		self.inner.listeners_object_name.to_string()
	}

	/// Insert a pending event action to the queue.
	fn insert_pending(&self, action: Pending) {
		self.inner.pending.lock().expect("poisoned pending event queue").push(action)
	}

	/// Finish all pending event actions.
	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);
			}
		}
	}

	/// Adds an event listener for JS events.
	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
	}

	/// Listen to a JS event and immediately unlisten.
	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)
		})
	}

	/// Removes an event listener.
	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);
			})
		}
	}

	/// Triggers the given global event with its payload.
	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 proptest::prelude::*;

	use super::*;

	// dummy event handler function
	fn event_fn(s: Event) {
		println!("{:?}", s);
	}

	proptest! {
		#![proptest_config(ProptestConfig::with_cases(10000))]

		// check to see if listen() is properly passing keys into the LISTENERS map
		#[test]
		fn listeners_check_key(e in "[a-z]+") {
			let listeners: Listeners = Default::default();
			// clone e as the key
			let key = e.clone();
			// pass e and an dummy func into listen
			listeners.listen(e, None, event_fn);

			// lock mutex
			let l = listeners.inner.handlers.lock().unwrap();

			// check if the generated key is in the map
			assert!(l.contains_key(&key));
		}

		// check to see if listen inputs a handler function properly into the LISTENERS map.
		#[test]
		fn listeners_check_fn(e in "[a-z]+") {
			let listeners: Listeners = Default::default();
			// clone e as the key
			let key = e.clone();
			// pass e and an dummy func into listen
			listeners.listen(e, None, event_fn);

			// lock mutex
			let mut l = listeners.inner.handlers.lock().unwrap();

			// check if l contains key
			if l.contains_key(&key) {
				// grab key if it exists
				let handler = l.get_mut(&key);
				// check to see if we get back a handler or not
				match handler {
					// pass on Some(handler)
					Some(_) => {},
					// Fail on None
					None => panic!("handler is None")
				}
			}
		}

		// check to see if on_event properly grabs the stored function from listen.
		#[test]
		fn check_on_event(e in "[a-z]+", d in "[a-z]+") {
			let listeners: Listeners = Default::default();
			// clone e as the key
			let key = e.clone();
			// call listen with e and the event_fn dummy func
			listeners.listen(e.clone(), None, event_fn);
			// call on event with e and d.
			listeners.trigger(&e, None, Some(d));

			// lock the mutex
			let l = listeners.inner.handlers.lock().unwrap();

			// assert that the key is contained in the listeners map
			assert!(l.contains_key(&key));
		}
	}
}

pub fn unlisten_js(listeners_object_name: String, event_name: String, event_id: u64) -> String {
	format!(
		"(function() {{
			const listeners = (window['{listeners}'] || {{}})['{event_name}'];
			if (listeners) {{
				const index = window['{listeners}']['{event_name}'].findIndex(e => e.id === {event_id});
				if (index > -1) {{
					window['{listeners}']['{event_name}'].splice(index, 1);
				}}
			}}
		}})()",
		listeners = listeners_object_name,
		event_name = event_name,
		event_id = event_id,
	)
}

pub fn listen_js(listeners_object_name: String, event: String, event_id: u64, window_label: Option<String>, handler: String) -> String {
	format!(
		"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: [] }});
		}}
		window['{listeners}'][{event}].push({{
			id: {event_id},
			windowLabel: {window_label},
			handler: {handler}
		}});",
		listeners = listeners_object_name,
		event = event,
		event_id = event_id,
		window_label = if let Some(l) = window_label {
			crate::runtime::window::assert_label_is_valid(&l);
			format!("'{}'", l)
		} else {
			"null".to_owned()
		},
		handler = handler
	)
}