use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, OnceLock};
use reactive_graph::signal::RwSignal;
use reactive_graph::traits::Set;
use crate::ReactiveRuntime;
use crate::executor::is_ui_thread;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DeepLink {
pub url: String,
pub sequence: u64,
}
impl DeepLink {
pub fn new(url: impl Into<String>) -> Self {
let sequence = SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
Self {
url: url.into(),
sequence,
}
}
}
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
#[derive(Clone)]
pub struct DeepLinks {
pub initial: Option<String>,
pub latest: RwSignal<Option<DeepLink>>,
}
struct Slot {
initial: Mutex<Option<String>>,
latest: RwSignal<Option<DeepLink>>,
}
static SLOT: OnceLock<Slot> = OnceLock::new();
fn slot() -> &'static Slot {
SLOT.get_or_init(|| {
let rt = ReactiveRuntime::get().expect(
"frust-reactive: deep_links() was called before ReactiveRuntime::init — an app \
must run under the Frust facade's entry point (which initializes the reactive \
runtime) before reading deep links",
);
Slot {
initial: Mutex::new(None),
latest: rt.with_owner(|| RwSignal::new(None)),
}
})
}
#[cfg(test)]
fn reset_initial_for_test() {
*slot()
.initial
.lock()
.expect("frust-reactive: deep_link initial mutex poisoned") = None;
}
pub fn push_deep_link(url: impl Into<String>) {
let url = url.into();
if !is_ui_thread() {
panic!(
"frust-reactive: push_deep_link was called off the UI thread. Deep links can \
only be pushed from the UI thread (the one `ReactiveRuntime::init` ran on) — this \
is a wiring bug: route the platform delivery through the UI thread before pushing, \
the same contract `Executor::spawn_local` enforces."
);
}
if ReactiveRuntime::get().is_none() {
eprintln!(
"frust-reactive: push_deep_link(\"{url}\") dropped — ReactiveRuntime::init has \
not run yet. A shell should queue a link platform-side until its native handle \
exists; reaching this indicates an odd init-ordering race, not normal \
operation."
);
return;
}
let slot = slot();
{
let mut initial = slot
.initial
.lock()
.expect("frust-reactive: deep_link initial mutex poisoned");
if initial.is_none() {
*initial = Some(url.clone());
}
}
slot.latest.set(Some(DeepLink::new(url)));
}
pub fn deep_links() -> DeepLinks {
let slot = slot();
let initial = slot
.initial
.lock()
.expect("frust-reactive: deep_link initial mutex poisoned")
.clone();
DeepLinks {
initial,
latest: slot.latest,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{FrameWaker, TrackedScope};
use reactive_graph::traits::{Get, GetUntracked};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
fn recording_waker() -> (FrameWaker, Arc<AtomicUsize>) {
let counter = Arc::new(AtomicUsize::new(0));
let seen = counter.clone();
let waker: FrameWaker = Arc::new(move || {
counter.fetch_add(1, Ordering::SeqCst);
});
(waker, seen)
}
#[test]
fn sequence_increments_on_identical_urls() {
let _guard = crate::WAKER_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let (waker, _) = recording_waker();
let _rt = ReactiveRuntime::init(waker);
let url = "frust-test://identical".to_string();
push_deep_link(url.clone());
let link1 = deep_links().latest.get_untracked().unwrap();
push_deep_link(url.clone());
let link2 = deep_links().latest.get_untracked().unwrap();
assert_eq!(link1.url, link2.url);
assert_eq!(link1.sequence + 1, link2.sequence);
}
#[test]
fn initial_sequence_lower_than_later_push() {
let _guard = crate::WAKER_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let (waker, _) = recording_waker();
let _rt = ReactiveRuntime::init(waker);
let url1 = "frust-test://initial".to_string();
push_deep_link(url1.clone());
let links1 = deep_links();
let initial_seq = links1
.latest
.get_untracked()
.expect("initial push should have produced a link")
.sequence;
let url2 = "frust-test://later".to_string();
push_deep_link(url2);
let links2 = deep_links();
let later_seq = links2
.latest
.get_untracked()
.expect("second push should have produced a link")
.sequence;
assert!(
initial_seq < later_seq,
"initial sequence {} should be lower than later sequence {}",
initial_seq,
later_seq
);
}
#[test]
fn sequences_strictly_increasing_across_mixed_urls() {
let _guard = crate::WAKER_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let (waker, _) = recording_waker();
let _rt = ReactiveRuntime::init(waker);
let urls = vec![
"frust-test://a",
"frust-test://b",
"frust-test://a", "frust-test://c",
"frust-test://b", ];
let mut sequences = Vec::new();
for url in urls {
push_deep_link(url);
let seq = deep_links()
.latest
.get_untracked()
.expect("push should produce a link")
.sequence;
sequences.push(seq);
}
for i in 1..sequences.len() {
assert!(
sequences[i - 1] < sequences[i],
"sequence {} should be strictly less than {}",
sequences[i - 1],
sequences[i]
);
}
}
#[test]
fn deep_link_push_and_wake_bridge() {
let _guard = crate::WAKER_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let (waker, wakes) = recording_waker();
let _rt = ReactiveRuntime::init(waker);
reset_initial_for_test();
let url1 = "frust-test://a/1".to_string();
push_deep_link(url1.clone());
let links = deep_links();
assert_eq!(links.initial.as_deref(), Some(url1.as_str()));
let link1 = links
.latest
.get_untracked()
.expect("should have pushed a link");
assert_eq!(link1.url, url1);
assert!(link1.sequence > 0, "sequence should be assigned");
let scope = TrackedScope::new();
let seen = scope.track(|| deep_links().latest.get());
let seen_link = seen.expect("should have a link");
assert_eq!(seen_link.url, url1);
assert_eq!(
seen_link.sequence, link1.sequence,
"sequence should be stable"
);
assert!(!scope.is_dirty(), "a fresh track starts clean");
let before = wakes.load(Ordering::SeqCst);
let url2 = "frust-test://b/2".to_string();
push_deep_link(url2.clone());
assert!(
scope.is_dirty(),
"push_deep_link must dirty a scope tracking `latest`"
);
assert_eq!(
wakes.load(Ordering::SeqCst) - before,
1,
"a push must fire the waker exactly once"
);
let links2 = deep_links();
assert_eq!(
links2.initial.as_deref(),
Some(url1.as_str()),
"initial is set once and stays"
);
let link2 = links2
.latest
.get_untracked()
.expect("should have pushed a second link");
assert_eq!(link2.url, url2);
assert!(
link2.sequence > link1.sequence,
"second sequence should be higher"
);
}
#[test]
#[should_panic(expected = "wiring bug")]
fn push_off_ui_thread_panics() {
let _guard = crate::WAKER_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
std::thread::spawn(|| {
push_deep_link("frust-test://off-thread");
})
.join()
.unwrap_or_else(|e| std::panic::resume_unwind(e));
}
}