Skip to content

Commit

Permalink
Merge pull request #279 from PatrykBuniX/feat/crux_time/cancellable-t…
Browse files Browse the repository at this point in the history
…imer

feat(crux_time): cancellable timer
  • Loading branch information
StuartHarris authored Oct 21, 2024
2 parents b56e6d5 + 0321cbd commit 0b2d1f2
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 27 deletions.
79 changes: 59 additions & 20 deletions crux_time/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,32 @@ pub use instant::Instant;
use serde::{Deserialize, Serialize};

use crux_core::capability::{CapabilityContext, Operation};
use std::sync::atomic::{AtomicUsize, Ordering};

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TimeRequest {
Now,
NotifyAt(Instant),
NotifyAfter(Duration),
NotifyAt { id: TimerId, instant: Instant },
NotifyAfter { id: TimerId, duration: Duration },
Clear { id: TimerId },
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TimerId(pub usize);

fn get_timer_id() -> TimerId {
static COUNTER: AtomicUsize = AtomicUsize::new(1);
TimerId(COUNTER.fetch_add(1, Ordering::Relaxed))
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TimeResponse {
Now(Instant),
InstantArrived,
DurationElapsed,
InstantArrived { id: TimerId },
DurationElapsed { id: TimerId },
Cleared { id: TimerId },
}

impl Operation for TimeRequest {
Expand Down Expand Up @@ -106,50 +117,66 @@ where
}

/// Ask to receive a notification when the specified [`Instant`] has arrived.
pub fn notify_at<F>(&self, instant: Instant, callback: F)
pub fn notify_at<F>(&self, instant: Instant, callback: F) -> TimerId
where
F: FnOnce(TimeResponse) -> Ev + Send + Sync + 'static,
{
let tid = get_timer_id();
self.context.spawn({
let context = self.context.clone();
let this = self.clone();

async move {
context.update_app(callback(this.notify_at_async(instant).await));
context.update_app(callback(this.notify_at_async(tid, instant).await));
}
});

tid
}

/// Ask to receive a notification when the specified [`Instant`] has arrived.
/// This is an async call to use with [`crux_core::compose::Compose`].
pub async fn notify_at_async(&self, instant: Instant) -> TimeResponse {
pub async fn notify_at_async(&self, id: TimerId, instant: Instant) -> TimeResponse {
self.context
.request_from_shell(TimeRequest::NotifyAt(instant))
.request_from_shell(TimeRequest::NotifyAt { id, instant })
.await
}

/// Ask to receive a notification when the specified duration has elapsed.
pub fn notify_after<F>(&self, duration: Duration, callback: F)
pub fn notify_after<F>(&self, duration: Duration, callback: F) -> TimerId
where
F: FnOnce(TimeResponse) -> Ev + Send + Sync + 'static,
{
let tid = get_timer_id();
self.context.spawn({
let context = self.context.clone();
let this = self.clone();

async move {
context.update_app(callback(this.notify_after_async(duration).await));
context.update_app(callback(this.notify_after_async(tid, duration).await));
}
});

tid
}

/// Ask to receive a notification when the specified duration has elapsed.
/// This is an async call to use with [`crux_core::compose::Compose`].
pub async fn notify_after_async(&self, duration: Duration) -> TimeResponse {
pub async fn notify_after_async(&self, id: TimerId, duration: Duration) -> TimeResponse {
self.context
.request_from_shell(TimeRequest::NotifyAfter(duration))
.request_from_shell(TimeRequest::NotifyAfter { id, duration })
.await
}

pub fn clear(&self, id: TimerId) {
self.context.spawn({
let context = self.context.clone();

async move {
context.notify_shell(TimeRequest::Clear { id }).await;
}
});
}
}

#[cfg(test)]
Expand All @@ -166,18 +193,30 @@ mod test {
let deserialized: TimeRequest = serde_json::from_str(&serialized).unwrap();
assert_eq!(now, deserialized);

let now = TimeRequest::NotifyAt(Instant::new(1, 2).expect("valid instant"));
let now = TimeRequest::NotifyAt {
id: TimerId(1),
instant: Instant::new(1, 2).expect("valid instant"),
};

let serialized = serde_json::to_string(&now).unwrap();
assert_eq!(&serialized, r#"{"notifyAt":{"seconds":1,"nanos":2}}"#);
assert_eq!(
&serialized,
r#"{"notifyAt":{"id":1,"instant":{"seconds":1,"nanos":2}}}"#
);

let deserialized: TimeRequest = serde_json::from_str(&serialized).unwrap();
assert_eq!(now, deserialized);

let now = TimeRequest::NotifyAfter(Duration::from_secs(1).expect("valid duration"));
let now = TimeRequest::NotifyAfter {
id: TimerId(2),
duration: Duration::from_secs(1).expect("valid duration"),
};

let serialized = serde_json::to_string(&now).unwrap();
assert_eq!(&serialized, r#"{"notifyAfter":{"nanos":1000000000}}"#);
assert_eq!(
&serialized,
r#"{"notifyAfter":{"id":2,"duration":{"nanos":1000000000}}}"#
);

let deserialized: TimeRequest = serde_json::from_str(&serialized).unwrap();
assert_eq!(now, deserialized);
Expand All @@ -193,18 +232,18 @@ mod test {
let deserialized: TimeResponse = serde_json::from_str(&serialized).unwrap();
assert_eq!(now, deserialized);

let now = TimeResponse::DurationElapsed;
let now = TimeResponse::DurationElapsed { id: TimerId(1) };

let serialized = serde_json::to_string(&now).unwrap();
assert_eq!(&serialized, r#""durationElapsed""#);
assert_eq!(&serialized, r#"{"durationElapsed":{"id":1}}"#);

let deserialized: TimeResponse = serde_json::from_str(&serialized).unwrap();
assert_eq!(now, deserialized);

let now = TimeResponse::InstantArrived;
let now = TimeResponse::InstantArrived { id: TimerId(2) };

let serialized = serde_json::to_string(&now).unwrap();
assert_eq!(&serialized, r#""instantArrived""#);
assert_eq!(&serialized, r#"{"instantArrived":{"id":2}}"#);

let deserialized: TimeResponse = serde_json::from_str(&serialized).unwrap();
assert_eq!(now, deserialized);
Expand Down
61 changes: 54 additions & 7 deletions crux_time/tests/time_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mod shared {
use chrono::{DateTime, Utc};
use crux_core::macros::Effect;
use crux_core::render::Render;
use crux_time::{Time, TimeResponse};
use crux_time::{Time, TimeResponse, TimerId};
use serde::{Deserialize, Serialize};

#[derive(Default)]
Expand Down Expand Up @@ -40,6 +40,7 @@ mod shared {
pub time: String,
debounce: Debounce,
pub debounce_complete: bool,
pub debounce_time_id: Option<TimerId>,
}

#[derive(Serialize, Deserialize, Default)]
Expand Down Expand Up @@ -73,16 +74,25 @@ mod shared {
Event::StartDebounce => {
let pending = model.debounce.start();

caps.time.notify_after(
let tid = caps.time.notify_after(
crux_time::Duration::from_millis(300).expect("valid duration"),
event_with_user_info(pending, Event::DurationElapsed),
);

model.debounce_time_id = Some(tid);
}
Event::DurationElapsed(pending, TimeResponse::DurationElapsed) => {
Event::DurationElapsed(pending, TimeResponse::DurationElapsed { id: _ }) => {
if model.debounce.resolve(pending) {
model.debounce_complete = true;
}
}
Event::DurationElapsed(_, TimeResponse::Cleared { id }) => {
if let Some(tid) = model.debounce_time_id {
if tid == id {
model.debounce_time_id = None;
}
}
}
Event::DurationElapsed(_, _) => {
panic!("Unexpected debounce event")
}
Expand Down Expand Up @@ -212,17 +222,54 @@ mod tests {
.expect_time();

// resolve and update
app.resolve_to_event_then_update(&mut request1, TimeResponse::DurationElapsed, &mut model)
.assert_empty();
app.resolve_to_event_then_update(
&mut request1,
TimeResponse::DurationElapsed {
id: model.debounce_time_id.unwrap(),
},
&mut model,
)
.assert_empty();

// resolving the first debounce should not set the debounce_complete flag
assert!(!model.debounce_complete);

// resolve and update
app.resolve_to_event_then_update(&mut request2, TimeResponse::DurationElapsed, &mut model)
.assert_empty();
app.resolve_to_event_then_update(
&mut request2,
TimeResponse::DurationElapsed {
id: model.debounce_time_id.unwrap(),
},
&mut model,
)
.assert_empty();

// resolving the second debounce should set the debounce_complete flag
assert!(model.debounce_complete);
}

#[test]
pub fn test_cancel_timer() {
let app = AppTester::<App, _>::default();
let mut model = Model::default();

let mut request1 = app
.update(Event::StartDebounce, &mut model)
.expect_one_effect()
.expect_time();

assert!(model.debounce_time_id.is_some());

app.resolve_to_event_then_update(
&mut request1,
TimeResponse::Cleared {
id: model.debounce_time_id.unwrap(),
},
&mut model,
)
.assert_empty();

assert!(!model.debounce_complete);
assert!(model.debounce_time_id.is_none());
}
}

0 comments on commit 0b2d1f2

Please sign in to comment.