99 lines
3.1 KiB
Rust
99 lines
3.1 KiB
Rust
use futures::stream::AbortHandle;
|
|
use std::time::Duration;
|
|
use warpui::r#async::Timer;
|
|
use warpui::{Entity, ModelContext};
|
|
|
|
const DEFAULT_PING_FREQUENCY: Duration = Duration::from_secs(5);
|
|
const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
|
|
|
|
/// A simple heartbeat mechanism to trigger ping notifications
|
|
/// on some cadence and to maintain a idle timer.
|
|
pub struct Heartbeat {
|
|
/// How often we want to trigger a [`Event::SendPing`] event.
|
|
ping_frequency: Duration,
|
|
|
|
/// The duration that we want to wait before firing a [`Event::Idle`] event.
|
|
/// To extend the timer by this duration, use [`Self::reset_idle_timeout`].
|
|
idle_timeout: Duration,
|
|
|
|
idle_timeout_abort_handle: Option<AbortHandle>,
|
|
periodic_ping_abort_handle: Option<AbortHandle>,
|
|
}
|
|
|
|
impl Default for Heartbeat {
|
|
fn default() -> Self {
|
|
Self {
|
|
idle_timeout_abort_handle: None,
|
|
periodic_ping_abort_handle: None,
|
|
ping_frequency: DEFAULT_PING_FREQUENCY,
|
|
idle_timeout: DEFAULT_IDLE_TIMEOUT,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Heartbeat {
|
|
pub fn with_idle_timeout(mut self, idle_timeout: Duration) -> Self {
|
|
self.idle_timeout = idle_timeout;
|
|
self
|
|
}
|
|
|
|
pub fn with_ping_frequency(mut self, ping_frequency: Duration) -> Self {
|
|
self.ping_frequency = ping_frequency;
|
|
self
|
|
}
|
|
|
|
/// Starts the periodic ping and the idle timeout tracker.
|
|
pub fn start(&mut self, ctx: &mut ModelContext<Self>) {
|
|
self.reset_idle_timeout(ctx);
|
|
self.periodic_ping(ctx);
|
|
}
|
|
|
|
/// Resets the idle timeout to expire after [`Self::idle_timeout`] from now.
|
|
pub fn reset_idle_timeout(&mut self, ctx: &mut ModelContext<Self>) {
|
|
if let Some(handle) = self.idle_timeout_abort_handle.take() {
|
|
handle.abort();
|
|
}
|
|
|
|
let idle_timeout = self.idle_timeout;
|
|
let handle = ctx.spawn(
|
|
async move { Timer::after(idle_timeout).await },
|
|
|me, _, ctx| {
|
|
// If the heartbeat has become idle, then don't ping anymore.
|
|
if let Some(handle) = me.periodic_ping_abort_handle.take() {
|
|
handle.abort();
|
|
}
|
|
ctx.emit(Event::Idle);
|
|
},
|
|
);
|
|
self.idle_timeout_abort_handle = Some(handle.abort_handle());
|
|
}
|
|
|
|
/// Emits a [`Event::SendPing`] event based on [`Self::ping_frequency`].
|
|
fn periodic_ping(&mut self, ctx: &mut ModelContext<Self>) {
|
|
// TODO: this would be simpler with a `spawn_stream_local`
|
|
// if our async timer supported an [`interval` API](https://docs.rs/async-io/latest/async_io/struct.Timer.html#method.interval).
|
|
let ping_frequency = self.ping_frequency;
|
|
let handle = ctx.spawn(
|
|
async move { Timer::after(ping_frequency).await },
|
|
|me, _, ctx| {
|
|
ctx.emit(Event::Ping);
|
|
me.periodic_ping(ctx);
|
|
},
|
|
);
|
|
self.periodic_ping_abort_handle = Some(handle.abort_handle());
|
|
}
|
|
}
|
|
|
|
pub enum Event {
|
|
Ping,
|
|
Idle,
|
|
}
|
|
|
|
impl Entity for Heartbeat {
|
|
type Event = Event;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "heartbeat_tests.rs"]
|
|
mod tests;
|