Skip to main content

rx_rust/operators/utility/
timeout.rs

1use crate::disposable::{
2    Disposable, bound_drop_disposal::BoundDropDisposal, option_disposal::OptionDisposal,
3};
4use crate::observable::{Observable, Subscription};
5use crate::observer::{Flow, Observer, Termination};
6use crate::scheduler::{RecursionAction, Scheduler};
7use crate::utils::serialized_delivery::UpdateOutcome;
8use crate::utils::subscribe_with_context::{
9    self, SubscriptionContext, subscribe_with_context_owning_source,
10};
11use crate::utils::types::{MarkerType, MaybeSend};
12use educe::Educe;
13use std::time::{Duration, Instant};
14
15#[derive(Educe)]
16#[educe(Debug, Clone, PartialEq, Eq)]
17pub enum Error<E> {
18    Timeout,
19    SourceError(E),
20}
21
22/// Mirrors the source Observable, but issues an error if a specified duration elapses between emissions.
23/// See <https://reactivex.io/documentation/operators/timeout.html>
24///
25/// # Examples
26/// ```rust
27/// # #[cfg(not(feature = "tokio-scheduler"))]
28/// # fn main() {}
29/// # #[cfg(feature = "tokio-scheduler")]
30/// #[tokio::main]
31/// async fn main() {
32///     use rx_rust::{
33///         observable::ObservableExt,
34///         observer::{Observer, Termination},
35///         operators::utility::timeout::{Error, Timeout},
36///         subject::publish_subject::PublishSubject,
37///     };
38///     use std::{convert::Infallible, sync::{Arc, Mutex}};
39///     use tokio::time::{sleep, Duration};
40///
41///     let handle = tokio::runtime::Handle::current();
42///     let values = Arc::new(Mutex::new(Vec::new()));
43///     let terminations = Arc::new(Mutex::new(Vec::new()));
44///     let values_observer = Arc::clone(&values);
45///     let terminations_observer = Arc::clone(&terminations);
46///     let mut subject: PublishSubject<'static, i32, Infallible> = PublishSubject::default();
47///
48///     let subscription = Timeout::new(subject.clone(), Duration::from_millis(5), handle.clone())
49///         .subscribe_with_callback(
50///             move |value| values_observer.lock().unwrap().push(value),
51///             move |termination| terminations_observer
52///                 .lock()
53///                 .unwrap()
54///                 .push(termination),
55///         );
56///
57///     subject.on_next(1);
58///     sleep(Duration::from_millis(10)).await;
59///     drop(subscription);
60///
61///     assert_eq!(&*values.lock().unwrap(), &[1]);
62///     assert_eq!(
63///         &*terminations.lock().unwrap(),
64///         &[Termination::Error(Error::Timeout)]
65///     );
66/// }
67/// ```
68#[derive(Educe)]
69#[educe(Debug, Clone)]
70pub struct Timeout<'or, OE, S> {
71    source: OE,
72    duration: Duration,
73    scheduler: S,
74    _marker: MarkerType<&'or ()>,
75}
76
77impl<'or, OE, S> Timeout<'or, OE, S> {
78    pub fn new(source: OE, duration: Duration, scheduler: S) -> Self {
79        Self {
80            source,
81            duration,
82            scheduler,
83            _marker: Default::default(),
84        }
85    }
86}
87
88impl<'or, T, E, OE, S> Observable<'static, T, Error<E>> for Timeout<'or, OE, S>
89where
90    T: MaybeSend + 'static,
91    E: MaybeSend + 'static,
92    OE: Observable<'or, T, E>,
93    OE::D: MaybeSend + 'static,
94    S: Scheduler + Clone + MaybeSend + 'static,
95{
96    type D = subscribe_with_context::OwningDisposal<'static>;
97
98    fn subscribe(
99        self,
100        observer: impl Observer<T, Error<E>> + MaybeSend + 'static,
101    ) -> Subscription<Self::D> {
102        let model = Model {
103            deadline: Instant::now() + self.duration,
104        };
105        subscribe_with_context_owning_source(observer, model, |context| {
106            let source_subscription = self.source.subscribe(TimeoutObserver {
107                context: context.clone(),
108                duration: self.duration,
109            });
110            let timer = setup_timer(context, &self.scheduler);
111            source_subscription.preceded_by(timer)
112        })
113    }
114}
115
116struct Model {
117    deadline: Instant,
118}
119
120struct TimeoutObserver<T, E, OR, D: Disposable> {
121    context: SubscriptionContext<T, Error<E>, OR, Model, D>,
122    duration: Duration,
123}
124
125impl<T, E, OR, D> Observer<T, E> for TimeoutObserver<T, E, OR, D>
126where
127    T: MaybeSend + 'static,
128    E: MaybeSend + 'static,
129    OR: Observer<T, Error<E>> + MaybeSend + 'static,
130    D: Disposable + MaybeSend + 'static,
131{
132    fn on_next(&mut self, value: T) -> Flow {
133        self.context.update_flow(|model| {
134            model.deadline = Instant::now() + self.duration;
135            UpdateOutcome::empty().with_next_event(value)
136        })
137    }
138
139    fn on_termination(self, termination: Termination<E>) {
140        self.context.send_termination(match termination {
141            Termination::Completed => Termination::Completed,
142            Termination::Error(error) => Termination::Error(Error::SourceError(error)),
143        });
144    }
145}
146
147/// Drives the timeout with one long-lived recursive scheduler task.
148///
149/// Source values only move `deadline` forward. If the task wakes at an obsolete deadline, it
150/// continues at the latest one; no scheduler task needs to be cancelled or spawned per value.
151fn setup_timer<T, E, OR, S, D>(
152    context: SubscriptionContext<T, Error<E>, OR, Model, D>,
153    scheduler: &S,
154) -> OptionDisposal<BoundDropDisposal<S::D>>
155where
156    T: MaybeSend + 'static,
157    E: MaybeSend + 'static,
158    OR: Observer<T, Error<E>> + MaybeSend + 'static,
159    S: Scheduler + Clone + MaybeSend + 'static,
160    D: Disposable + MaybeSend + 'static,
161{
162    let deadline = context.update(|model| UpdateOutcome::new(model.deadline).without_events());
163    let Ok(deadline) = deadline else {
164        // The source terminated synchronously while it was being subscribed.
165        return OptionDisposal::none();
166    };
167
168    let weak_context = context.downgrade();
169    let timer = scheduler.schedule_recursively(
170        move |_| {
171            let Some(context) = weak_context.upgrade() else {
172                return RecursionAction::Stop;
173            };
174            context
175                .update(|model| {
176                    if Instant::now() < model.deadline {
177                        return UpdateOutcome::new(RecursionAction::ContinueAt(model.deadline))
178                            .without_events();
179                    }
180                    UpdateOutcome::new(RecursionAction::Stop)
181                        .with_termination_event(Termination::Error(Error::Timeout))
182                })
183                .unwrap_or(RecursionAction::Stop)
184        },
185        Some(deadline.saturating_duration_since(Instant::now())),
186    );
187    OptionDisposal::some(timer)
188}