use std::future::Future;
use futures::{Stream, StreamExt};
use crate::hooks::{HookSlot, Hooks};
impl<'a> Hooks<'a> {
pub fn use_future<F, Fut>(&mut self, make: F)
where
F: FnOnce() -> Fut,
Fut: Future<Output = ()> + Send + 'static,
{
let mut make = Some(make);
let slot = self.next_slot(move || HookSlot::Task(tokio::spawn((make.take().unwrap())())));
let HookSlot::Task(_) = slot else {
self.hook_mismatch("use_future")
};
}
pub fn use_task<D, F, Fut>(&mut self, deps: D, make: F)
where
D: PartialEq + 'static,
F: FnOnce() -> Fut + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
self.use_effect(deps, move || {
let handle = tokio::spawn(make());
move || handle.abort()
});
}
pub fn use_stream<S, F>(&mut self, make: impl FnOnce() -> S + Send + 'static, mut on_item: F)
where
S: Stream + Send + 'static,
S::Item: Send,
F: FnMut(S::Item) + Send + 'static,
{
self.use_future(move || async move {
let mut s = std::pin::pin!(make());
while let Some(item) = s.next().await {
on_item(item);
}
});
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use crate::component::Component;
use crate::element::Element;
use crate::hooks::Hooks;
use crate::hooks::state::State;
use crate::props::TextProps;
use crate::test_util::Shared;
use crate::testing::TestTerminal;
struct Timer;
#[derive(Clone, PartialEq, Default)]
struct TimerProps {
fired: Shared<bool>,
}
impl Component for Timer {
type Props = TimerProps;
fn render(props: &TimerProps, hooks: &mut Hooks) -> Element {
let done = hooks.use_state(|| false);
let d = done.clone();
let fired = props.fired.clone();
hooks.use_future(move || async move {
tokio::time::sleep(Duration::from_millis(100)).await;
*fired.lock() = true;
d.set(true);
});
Element::text(TextProps {
content: done.get().to_string(),
..Default::default()
})
}
}
struct Gate;
#[derive(Clone, PartialEq, Default)]
struct GateProps {
fired: Shared<bool>,
show: Shared<Option<State<bool>>>,
}
impl Component for Gate {
type Props = GateProps;
fn render(props: &GateProps, hooks: &mut Hooks) -> Element {
let show = hooks.use_state(|| true);
*props.show.lock() = Some(show.clone());
if show.get() {
Element::fragment(vec![Element::component::<Timer>(TimerProps {
fired: props.fired.clone(),
})])
} else {
Element::fragment(vec![])
}
}
}
#[tokio::test(start_paused = true)]
async fn future_completes_and_sets_state() {
let props = TimerProps::default();
let mut t = TestTerminal::new(10, 1, Element::component::<Timer>(props.clone())).unwrap();
assert!(t.frame_text().contains("false"));
tokio::time::sleep(Duration::from_millis(150)).await; t.tick().await.unwrap();
assert!(t.frame_text().contains("true"));
}
#[tokio::test(start_paused = true)]
async fn unmount_aborts_task() {
let props = GateProps::default();
let mut t = TestTerminal::new(10, 1, Element::component::<Gate>(props.clone())).unwrap();
props.show.lock().clone().unwrap().set(false); t.tick().await.unwrap();
tokio::time::sleep(Duration::from_millis(300)).await;
t.tick().await.unwrap();
assert!(!*props.fired.lock());
}
struct Streamer;
#[derive(Clone, PartialEq, Default)]
struct StreamerProps {
items: Shared<Vec<i32>>,
}
impl Component for Streamer {
type Props = StreamerProps;
fn render(props: &StreamerProps, hooks: &mut Hooks) -> Element {
let items = props.items.clone();
hooks.use_stream(
|| futures::stream::iter(vec![1, 2, 3]),
move |item| items.lock().push(item),
);
Element::text(TextProps {
content: "s".into(),
..Default::default()
})
}
}
#[tokio::test(start_paused = true)]
async fn use_stream_forwards_all_items_in_order() {
let props = StreamerProps::default();
let mut t =
TestTerminal::new(10, 1, Element::component::<Streamer>(props.clone())).unwrap();
tokio::time::sleep(Duration::from_millis(50)).await; t.tick().await.unwrap();
assert_eq!(*props.items.lock(), vec![1, 2, 3]);
}
struct SlowStreamer;
#[derive(Clone, PartialEq, Default)]
struct SlowStreamerProps {
items: Shared<Vec<i32>>,
}
impl Component for SlowStreamer {
type Props = SlowStreamerProps;
fn render(props: &SlowStreamerProps, hooks: &mut Hooks) -> Element {
let items = props.items.clone();
hooks.use_stream(
|| {
use futures::StreamExt;
futures::stream::iter(vec![1, 2, 3]).then(|i| async move {
tokio::time::sleep(Duration::from_millis(100)).await;
i
})
},
move |item| items.lock().push(item),
);
Element::text(TextProps {
content: "s".into(),
..Default::default()
})
}
}
struct StreamGate;
#[derive(Clone, PartialEq, Default)]
struct StreamGateProps {
items: Shared<Vec<i32>>,
show: Shared<Option<State<bool>>>,
}
impl Component for StreamGate {
type Props = StreamGateProps;
fn render(props: &StreamGateProps, hooks: &mut Hooks) -> Element {
let show = hooks.use_state(|| true);
*props.show.lock() = Some(show.clone());
if show.get() {
Element::fragment(vec![Element::component::<SlowStreamer>(
SlowStreamerProps {
items: props.items.clone(),
},
)])
} else {
Element::fragment(vec![])
}
}
}
struct Keyed;
#[derive(Clone, PartialEq, Default)]
struct KeyedProps {
key: Shared<Option<State<u32>>>,
fired: Shared<Vec<u32>>,
}
impl Component for Keyed {
type Props = KeyedProps;
fn render(props: &KeyedProps, hooks: &mut Hooks) -> Element {
let key = hooks.use_state(|| 0u32);
*props.key.lock() = Some(key.clone());
let k = key.get();
let fired = props.fired.clone();
hooks.use_task(k, move || async move {
tokio::time::sleep(Duration::from_millis(100)).await;
fired.lock().push(k);
});
Element::text(TextProps {
content: k.to_string(),
..Default::default()
})
}
}
#[tokio::test(start_paused = true)]
async fn use_task_restarts_on_deps_change_aborting_the_old_task() {
let props = KeyedProps::default();
let mut t = TestTerminal::new(10, 1, Element::component::<Keyed>(props.clone())).unwrap();
tokio::time::sleep(Duration::from_millis(50)).await;
props.key.lock().clone().unwrap().set(1);
t.tick().await.unwrap();
tokio::time::sleep(Duration::from_millis(300)).await;
t.tick().await.unwrap();
assert_eq!(*props.fired.lock(), vec![1]);
}
#[tokio::test(start_paused = true)]
async fn use_task_with_stable_deps_spawns_exactly_once() {
let props = KeyedProps::default();
let mut t = TestTerminal::new(10, 1, Element::component::<Keyed>(props.clone())).unwrap();
let key = props.key.lock().clone().unwrap();
tokio::time::sleep(Duration::from_millis(60)).await;
key.set(0);
t.tick().await.unwrap();
tokio::time::sleep(Duration::from_millis(60)).await;
t.tick().await.unwrap();
assert_eq!(*props.fired.lock(), vec![0]);
}
#[tokio::test(start_paused = true)]
async fn use_stream_aborts_on_unmount() {
let props = StreamGateProps::default();
let mut t =
TestTerminal::new(10, 1, Element::component::<StreamGate>(props.clone())).unwrap();
tokio::time::sleep(Duration::from_millis(150)).await;
t.tick().await.unwrap();
props.show.lock().clone().unwrap().set(false); t.tick().await.unwrap();
let at_unmount = props.items.lock().len();
assert!(at_unmount < 3, "stream should still be draining at unmount");
tokio::time::sleep(Duration::from_millis(500)).await;
t.tick().await.unwrap();
assert_eq!(
props.items.lock().len(),
at_unmount,
"no further items after the aborted stream"
);
}
}