1use std::{cell::RefCell, collections::VecDeque, rc::Rc, sync::Mutex};
5static PENDING: Mutex<VecDeque<String>> = Mutex::new(VecDeque::new());
6day_reactive::tls_slots! {
7 urls;
8 static HANDLERS: RefCell<Vec<Rc<dyn Fn(&str) -> bool>>> = const { RefCell::new(Vec::new()) };
9}
10pub fn on_open_url(handler: impl Fn(&str) -> bool + 'static) {
18 let handler: Rc<dyn Fn(&str) -> bool> = Rc::new(handler);
19 HANDLERS.with(|h| h.borrow_mut().push(handler.clone()));
20 day_reactive::Scope::current().on_cleanup(move || {
21 let _ = HANDLERS.try_with(|h| {
22 h.borrow_mut()
23 .retain(|current| !Rc::ptr_eq(current, &handler));
24 });
25 });
26 if day_reactive::has_main_poster() {
27 day_reactive::on_main(drain);
28 }
29}
30pub fn request_open_url(url: &str) {
32 if url.is_empty() {
33 return;
34 }
35 PENDING
36 .lock()
37 .unwrap_or_else(|e| e.into_inner())
38 .push_back(url.into());
39 if day_reactive::has_main_poster() {
40 day_reactive::on_main(drain);
41 }
42}
43pub(crate) fn drain() {
44 drain_queue(&PENDING);
45}
46fn drain_queue(queue: &Mutex<VecDeque<String>>) {
47 loop {
48 let next = queue.lock().unwrap_or_else(|e| e.into_inner()).pop_front();
49 let Some(url) = next else { break };
50 let handlers: Vec<_> = HANDLERS.with(|h| h.borrow().iter().rev().cloned().collect());
52 let consumed = handlers.iter().any(|h| h(&url));
53 if !consumed {
54 crate::request_route(&day_spec::route_of_url(&url));
55 }
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 #[test]
63 fn original_urls_are_queued_reentrant_and_scoped() {
64 let seen = Rc::new(RefCell::new(Vec::new()));
65 let queue = std::sync::Arc::new(Mutex::new(VecDeque::from([
67 "feed:https://fixture.example/rss?q=a%26b".into(),
68 ])));
69 let again = queue.clone();
70 let scope = day_reactive::Scope::child();
71 let received = seen.clone();
72 scope.enter(|| {
73 on_open_url(move |url| {
74 received.borrow_mut().push(url.to_owned());
75 if url.starts_with("feed:") {
76 again
77 .lock()
78 .unwrap()
79 .push_back("atom:https://fixture.example/atom".into());
80 }
81 true
82 })
83 });
84 drain_queue(&queue);
85 queue
86 .lock()
87 .unwrap()
88 .push_back("rss://fixture.example/rss".into());
89 drain_queue(&queue);
90 assert_eq!(
91 *seen.borrow(),
92 [
93 "feed:https://fixture.example/rss?q=a%26b",
94 "atom:https://fixture.example/atom",
95 "rss://fixture.example/rss"
96 ]
97 );
98 scope.dispose();
99 assert!(HANDLERS.with(|h| h.borrow().is_empty()));
100 }
101
102 #[test]
105 fn handlers_stack_newest_first_and_leave_with_their_scope() {
106 let app_seen = Rc::new(RefCell::new(Vec::new()));
107 let part_seen = Rc::new(RefCell::new(Vec::new()));
108 let queue = std::sync::Arc::new(Mutex::new(VecDeque::new()));
109 let app_scope = day_reactive::Scope::child();
110 let received = app_seen.clone();
111 app_scope.enter(|| {
112 on_open_url(move |url| {
113 received.borrow_mut().push(url.to_owned());
114 true
115 })
116 });
117 let part_scope = day_reactive::Scope::child();
118 let received = part_seen.clone();
119 part_scope.enter(|| {
120 on_open_url(move |url| {
121 let mine = url.starts_with("myapp://auth");
122 if mine {
123 received.borrow_mut().push(url.to_owned());
124 }
125 mine
126 })
127 });
128 queue.lock().unwrap().extend([
129 "myapp://auth?code=1".to_string(),
130 "myapp://inbox".to_string(),
131 ]);
132 drain_queue(&queue);
133 assert_eq!(*part_seen.borrow(), ["myapp://auth?code=1"]);
134 assert_eq!(*app_seen.borrow(), ["myapp://inbox"]);
135 part_scope.dispose();
136 assert_eq!(HANDLERS.with(|h| h.borrow().len()), 1);
137 queue
138 .lock()
139 .unwrap()
140 .push_back("myapp://auth?code=2".into());
141 drain_queue(&queue);
142 assert_eq!(*app_seen.borrow(), ["myapp://inbox", "myapp://auth?code=2"]);
143 app_scope.dispose();
144 assert!(HANDLERS.with(|h| h.borrow().is_empty()));
145 }
146}