use std::fmt;
use std::sync::{Arc, Mutex, MutexGuard};
use super::path::{Location, RouteParams};
pub type NavWaker = Arc<dyn Fn() + Send + Sync>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NavRequest {
Go(String),
Push(String),
Replace(String),
Pop,
GoNamed {
name: String,
params: RouteParams,
},
PushNamed {
name: String,
params: RouteParams,
},
}
struct RouteNavInner {
queue: Mutex<Vec<NavRequest>>,
waker: Mutex<Option<NavWaker>>,
location: Mutex<Option<Location>>,
}
pub struct RouteNavigator {
inner: Arc<RouteNavInner>,
}
const _: fn() = || {
fn assert<T: Send + Sync + 'static>() {}
assert::<RouteNavigator>();
};
impl RouteNavigator {
pub fn new() -> Self {
RouteNavigator {
inner: Arc::new(RouteNavInner {
queue: Mutex::new(Vec::new()),
waker: Mutex::new(None),
location: Mutex::new(None),
}),
}
}
pub fn request(&self, request: NavRequest) {
lock(&self.inner.queue).push(request);
self.wake();
}
pub fn go(&self, location: impl Into<String>) {
self.request(NavRequest::Go(location.into()));
}
pub fn push(&self, location: impl Into<String>) {
self.request(NavRequest::Push(location.into()));
}
pub fn replace(&self, location: impl Into<String>) {
self.request(NavRequest::Replace(location.into()));
}
pub fn pop(&self) {
self.request(NavRequest::Pop);
}
pub fn go_named(&self, name: impl Into<String>, params: RouteParams) {
self.request(NavRequest::GoNamed {
name: name.into(),
params,
});
}
pub fn push_named(&self, name: impl Into<String>, params: RouteParams) {
self.request(NavRequest::PushNamed {
name: name.into(),
params,
});
}
pub fn location(&self) -> Option<Location> {
lock(&self.inner.location).clone()
}
pub fn set_waker(&self, waker: NavWaker) {
*lock(&self.inner.waker) = Some(waker);
}
pub fn drain(&self) -> Vec<NavRequest> {
std::mem::take(&mut *lock(&self.inner.queue))
}
pub(super) fn set_location(&self, location: Location) {
*lock(&self.inner.location) = Some(location);
}
fn wake(&self) {
let waker = lock(&self.inner.waker).clone();
if let Some(waker) = waker {
waker();
}
}
}
impl Clone for RouteNavigator {
fn clone(&self) -> Self {
RouteNavigator {
inner: Arc::clone(&self.inner),
}
}
}
impl Default for RouteNavigator {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for RouteNavigator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RouteNavigator")
.field("pending", &lock(&self.inner.queue).len())
.field("location", &*lock(&self.inner.location))
.finish_non_exhaustive()
}
}
fn lock<T>(slot: &Mutex<T>) -> MutexGuard<'_, T> {
slot.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
fn params(pairs: &[(&str, &str)]) -> RouteParams {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn queues_in_append_order_and_drains_once() {
let nav = RouteNavigator::new();
nav.go("/home");
nav.push("/detail");
nav.replace("/other");
nav.pop();
nav.go_named("user", params(&[("id", "7")]));
nav.push_named("user", params(&[("id", "8")]));
assert_eq!(
nav.drain(),
vec![
NavRequest::Go("/home".to_string()),
NavRequest::Push("/detail".to_string()),
NavRequest::Replace("/other".to_string()),
NavRequest::Pop,
NavRequest::GoNamed {
name: "user".to_string(),
params: params(&[("id", "7")]),
},
NavRequest::PushNamed {
name: "user".to_string(),
params: params(&[("id", "8")]),
},
]
);
assert!(nav.drain().is_empty());
}
#[test]
fn waker_fires_once_per_append() {
let nav = RouteNavigator::new();
let woke = Arc::new(AtomicUsize::new(0));
let counter = woke.clone();
nav.set_waker(Arc::new(move || {
counter.fetch_add(1, Ordering::SeqCst);
}));
nav.go("/a");
nav.pop();
assert_eq!(woke.load(Ordering::SeqCst), 2);
let _ = nav.drain();
assert_eq!(woke.load(Ordering::SeqCst), 2);
}
#[test]
fn appends_from_another_thread_are_total() {
let nav = RouteNavigator::new();
let woke = Arc::new(AtomicUsize::new(0));
let counter = woke.clone();
nav.set_waker(Arc::new(move || {
counter.fetch_add(1, Ordering::SeqCst);
}));
let off_thread = nav.clone();
std::thread::spawn(move || off_thread.push("/from-a-thread"))
.join()
.expect("the off-thread append must not panic");
assert_eq!(
nav.drain(),
vec![NavRequest::Push("/from-a-thread".to_string())]
);
assert_eq!(woke.load(Ordering::SeqCst), 1);
}
#[test]
fn clones_share_one_queue() {
let nav = RouteNavigator::new();
let other = nav.clone();
other.go("/shared");
assert_eq!(nav.drain(), vec![NavRequest::Go("/shared".to_string())]);
}
#[test]
fn location_starts_empty_and_reports_the_last_published() {
let nav = RouteNavigator::new();
assert!(nav.location().is_none());
nav.set_location(Location::parse("/users/42?tab=posts"));
let loc = nav.location().expect("published");
assert_eq!(loc.path, "/users/42");
assert_eq!(loc.query.get("tab").map(String::as_str), Some("posts"));
}
#[test]
fn a_poisoned_queue_still_works() {
let nav = RouteNavigator::new();
let poisoner = nav.clone();
let _ = std::thread::spawn(move || {
let _guard = poisoner.inner.queue.lock().expect("fresh mutex");
panic!("poison the queue");
})
.join();
nav.go("/after-poison");
assert_eq!(
nav.drain(),
vec![NavRequest::Go("/after-poison".to_string())]
);
}
}