Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
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;
|
||||
@@ -0,0 +1,83 @@
|
||||
use super::{Event, Heartbeat};
|
||||
use std::time::Duration;
|
||||
use warpui::r#async::Timer;
|
||||
use warpui::App;
|
||||
|
||||
#[test]
|
||||
#[ignore = "Flakes in CI"]
|
||||
fn test_periodic_ping() {
|
||||
App::test((), |mut app| async move {
|
||||
let heartbeat =
|
||||
app.add_model(|_| Heartbeat::default().with_ping_frequency(Duration::from_millis(100)));
|
||||
let (tx, rx) = async_channel::unbounded();
|
||||
let tx_clone = tx.clone();
|
||||
app.update(|ctx| {
|
||||
ctx.subscribe_to_model(&heartbeat, move |_, event, _| {
|
||||
if matches!(event, Event::Ping) {
|
||||
tx_clone.try_send(()).expect("can send over channel");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Start the heartbeat.
|
||||
heartbeat.update(&mut app, |heartbeat, ctx| heartbeat.start(ctx));
|
||||
|
||||
// After 50ms, there shouldn't have been any pings.
|
||||
Timer::after(Duration::from_millis(50)).await;
|
||||
assert_eq!(rx.len(), 0);
|
||||
|
||||
// After 150ms, there should have been 1 ping.
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
assert_eq!(rx.len(), 1);
|
||||
|
||||
// After 175ms, there still should have only been 1 ping.
|
||||
Timer::after(Duration::from_millis(25)).await;
|
||||
assert_eq!(rx.len(), 1);
|
||||
|
||||
// After 250ms, there should have been 2 pings in total.
|
||||
Timer::after(Duration::from_millis(75)).await;
|
||||
assert_eq!(rx.len(), 2);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Flakes in CI"]
|
||||
fn test_idle_timeout() {
|
||||
App::test((), |mut app| async move {
|
||||
let heartbeat =
|
||||
app.add_model(|_| Heartbeat::default().with_idle_timeout(Duration::from_millis(100)));
|
||||
let (tx, rx) = async_channel::unbounded();
|
||||
app.update(|ctx| {
|
||||
ctx.subscribe_to_model(&heartbeat, move |_, event, _| {
|
||||
if matches!(event, Event::Idle) {
|
||||
tx.try_send(()).expect("can send over channel");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Start the heartbeat.
|
||||
heartbeat.update(&mut app, |heartbeat, ctx| heartbeat.start(ctx));
|
||||
|
||||
// The idle timeout should not have expired yet.
|
||||
Timer::after(Duration::from_millis(50)).await;
|
||||
assert_eq!(rx.len(), 0);
|
||||
|
||||
// Reset the idle timeout.
|
||||
heartbeat.update(
|
||||
&mut app,
|
||||
|heartbeat, ctx: &mut warpui::ModelContext<Heartbeat>| {
|
||||
heartbeat.reset_idle_timeout(ctx)
|
||||
},
|
||||
);
|
||||
|
||||
// If the idle timeout was reset properly, then there should not be a idle event yet
|
||||
// even though one full idle timeout has elapsed since `start`.
|
||||
Timer::after(Duration::from_millis(75)).await;
|
||||
assert_eq!(rx.len(), 0);
|
||||
|
||||
// One full idle timeout has elapsed since the last reset, so
|
||||
// we should have received an idle event.
|
||||
Timer::after(Duration::from_millis(75)).await;
|
||||
assert_eq!(rx.len(), 1);
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod heartbeat;
|
||||
Reference in New Issue
Block a user