Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
@@ -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;