Skip to content

Commit

Permalink
add back crux_time::Duration and modify api instead
Browse files Browse the repository at this point in the history
  • Loading branch information
StuartHarris committed Feb 26, 2025
1 parent f92f93e commit f6d7dd9
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 66 deletions.
10 changes: 6 additions & 4 deletions crux_time/src/command.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use std::{future::Future, marker::PhantomData};
use std::{future::Future, marker::PhantomData, time::Duration};

use crux_core::{command::RequestBuilder, Command, Request};
use futures::{
channel::oneshot::{self, Sender},
select, FutureExt,
};

use crate::{get_timer_id, Duration, Instant, TimeRequest, TimeResponse, TimerId};
use crate::{get_timer_id, Instant, TimeRequest, TimeResponse, TimerId};

pub struct Time<Effect, Event> {
// Allow impl level trait bounds to avoid repetition
Expand Down Expand Up @@ -65,7 +65,7 @@ where

let builder = RequestBuilder::new(move |ctx| async move {
select! {
response = ctx.request_from_shell(TimeRequest::NotifyAfter { id, duration }).fuse() => return response,
response = ctx.request_from_shell(TimeRequest::NotifyAfter { id, duration: duration.into() }).fuse() => return response,
cleared = receiver => {
// The Err variant would mean the sender was dropped, but `receiver` is a fused future,
// which signals `is_terminated` true in that case, so this branch of the select will
Expand Down Expand Up @@ -104,10 +104,12 @@ impl TimerHandle {

#[cfg(test)]
mod tests {
use std::time::Duration;

use crux_core::Request;

use super::Time;
use crate::{Duration, TimeRequest, TimeResponse};
use crate::{TimeRequest, TimeResponse};

enum Effect {
Time(Request<TimeRequest>),
Expand Down
57 changes: 17 additions & 40 deletions crux_time/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,8 @@
pub mod command;
pub mod error;
pub mod instant;
pub mod protocol;

pub use error::TimeError;
pub use instant::Instant;

use serde::{Deserialize, Serialize};

use crux_core::capability::{CapabilityContext, Operation};
use std::{
collections::HashSet,
future::Future,
Expand All @@ -23,39 +17,18 @@ use std::{
LazyLock, Mutex,
},
task::Poll,
time::Duration,
};

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

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TimerId(pub usize);
pub use error::TimeError;
pub use protocol::{duration::Duration, instant::Instant, TimeRequest, TimeResponse, TimerId};

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: Instant },
InstantArrived { id: TimerId },
DurationElapsed { id: TimerId },
Cleared { id: TimerId },
}

impl Operation for TimeRequest {
type Output = TimeResponse;
}

/// The Time capability API
///
/// This capability provides access to the current time and allows the app to ask for
Expand All @@ -79,6 +52,9 @@ impl<Ev> crux_core::Capability<Ev> for Time<Ev> {

#[cfg(feature = "typegen")]
fn register_types(generator: &mut crux_core::typegen::TypeGen) -> crux_core::typegen::Result {
use crux_core::capability::Operation;
use protocol::{duration::Duration, instant::Instant};

generator.register_type::<Instant>()?;
generator.register_type::<Duration>()?;
generator.register_type::<Self::Operation>()?;
Expand Down Expand Up @@ -153,8 +129,8 @@ where
(TimerFuture::new(id, future), id)
}

/// Ask to receive a notification when the specified duration has elapsed.
pub fn notify_after<F>(&self, duration: Duration, callback: F) -> TimerId
/// Ask to receive a notification when the specified [`Duration`](std::time::Duration) has elapsed.
pub fn notify_after<F>(&self, duration: std::time::Duration, callback: F) -> TimerId
where
F: FnOnce(TimeResponse) -> Ev + Send + Sync + 'static,
{
Expand All @@ -168,16 +144,17 @@ where
id
}

/// Ask to receive a notification when the specified duration has elapsed.
/// Ask to receive a notification when the specified [`Duration`](std::time::Duration) has elapsed.
/// This is an async call to use with [`crux_core::compose::Compose`].
pub fn notify_after_async(
&self,
duration: Duration,
duration: std::time::Duration,
) -> (TimerFuture<impl Future<Output = TimeResponse>>, TimerId) {
let id = get_timer_id();
let future = self
.context
.request_from_shell(TimeRequest::NotifyAfter { id, duration });
let future = self.context.request_from_shell(TimeRequest::NotifyAfter {
id,
duration: duration.into(),
});
(TimerFuture::new(id, future), id)
}

Expand Down Expand Up @@ -287,13 +264,13 @@ mod test {

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

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

let deserialized: TimeRequest = serde_json::from_str(&serialized).unwrap();
Expand Down
133 changes: 133 additions & 0 deletions crux_time/src/protocol/duration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use serde::{Deserialize, Serialize};

use crate::{error::TimeResult, TimeError};

/// The number of nanoseconds in seconds.
pub(crate) const NANOS_PER_SEC: u32 = 1_000_000_000;
/// The number of nanoseconds in a millisecond.
const NANOS_PER_MILLI: u32 = 1_000_000;

/// Represents a duration of time, internally stored as nanoseconds
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Duration {
nanos: u64,
}

impl Duration {
/// Create a new `Duration` from the given number of nanoseconds.
pub fn new(nanos: u64) -> Self {
Self { nanos }
}

/// Create a new `Duration` from the given number of milliseconds.
///
/// Errors with [`TimeError::InvalidDuration`] if the number of milliseconds
/// would overflow when converted to nanoseconds.
pub fn from_millis(millis: u64) -> TimeResult<Self> {
let nanos = millis
.checked_mul(NANOS_PER_MILLI as u64)
.ok_or(TimeError::InvalidDuration)?;
Ok(Self { nanos })
}

/// Create a new `Duration` from the given number of seconds.
///
/// Errors with [`TimeError::InvalidDuration`] if the number of seconds
/// would overflow when converted to nanoseconds.
pub fn from_secs(seconds: u64) -> TimeResult<Self> {
let nanos = seconds
.checked_mul(NANOS_PER_SEC as u64)
.ok_or(TimeError::InvalidDuration)?;
Ok(Self { nanos })
}
}

impl From<std::time::Duration> for Duration {
fn from(duration: std::time::Duration) -> Self {
Duration {
nanos: duration.as_nanos() as u64,
}
}
}

impl From<Duration> for std::time::Duration {
fn from(duration: Duration) -> Self {
std::time::Duration::from_nanos(duration.nanos)
}
}

#[cfg(feature = "chrono")]
impl TryFrom<chrono::TimeDelta> for Duration {
type Error = TimeError;

fn try_from(value: chrono::TimeDelta) -> Result<Self, Self::Error> {
let nanos = value.num_nanoseconds().ok_or(TimeError::InvalidDuration)? as u64;
Ok(Self { nanos })
}
}

#[cfg(feature = "chrono")]
impl TryFrom<Duration> for chrono::TimeDelta {
type Error = TimeError;

fn try_from(value: Duration) -> Result<Self, Self::Error> {
let nanos = value
.nanos
.try_into()
.map_err(|_| TimeError::InvalidDuration)?;
Ok(chrono::TimeDelta::nanoseconds(nanos))
}
}

#[cfg(test)]
mod test {
use super::Duration;
use std::time::Duration as StdDuration;

#[test]
fn duration_from_millis() {
let duration = Duration::from_millis(1_000).unwrap();
assert_eq!(duration.nanos, 1_000_000_000);
}

#[test]
fn duration_from_secs() {
let duration = Duration::from_secs(1).unwrap();
assert_eq!(duration.nanos, 1_000_000_000);
}

#[test]
fn std_into_duration() {
let actual: Duration = StdDuration::from_millis(100).into();
let expected = Duration { nanos: 100_000_000 };
assert_eq!(actual, expected);
}

#[test]
fn duration_into_std() {
let actual: StdDuration = Duration { nanos: 100_000_000 }.into();
let expected = StdDuration::from_nanos(100_000_000);
assert_eq!(actual, expected);
}
}

#[cfg(feature = "chrono")]
#[cfg(test)]
mod chrono_test {
use super::*;

#[test]
fn duration_to_timedelta() {
let duration = Duration::new(1_000_000_000);
let chrono_duration: chrono::TimeDelta = duration.try_into().unwrap();
assert_eq!(chrono_duration.num_nanoseconds().unwrap(), 1_000_000_000);
}

#[test]
fn timedelta_to_duration() {
let chrono_duration = chrono::TimeDelta::nanoseconds(1_000_000_000);
let duration: Duration = chrono_duration.try_into().unwrap();
assert_eq!(duration.nanos, 1_000_000_000);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use crate::{error::TimeResult, TimeError};

/// The number of nanoseconds in seconds.
pub const NANOS_PER_SEC: u32 = 1_000_000_000;
const NANOS_PER_SEC: u32 = 1_000_000_000;

/// Represents a point in time (UTC):
///
Expand Down
33 changes: 33 additions & 0 deletions crux_time/src/protocol/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
pub mod duration;
pub mod instant;

use crux_core::capability::Operation;
use serde::{Deserialize, Serialize};

use duration::Duration;
use instant::Instant;

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

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

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

impl Operation for TimeRequest {
type Output = TimeResponse;
}
9 changes: 6 additions & 3 deletions crux_time/tests/time_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ mod shared {
use chrono::{DateTime, Utc};
use crux_core::render::Render;
use crux_core::{macros::Effect, Command};
use crux_time::{Time, TimeResponse, TimerId};
use crux_time::{
protocol::{TimeResponse, TimerId},
Time,
};
use serde::{Deserialize, Serialize};

#[derive(Default)]
Expand Down Expand Up @@ -141,7 +144,7 @@ mod shell {
use super::shared::{App, Effect, Event};
use chrono::{DateTime, Utc};
use crux_core::{Core, Request};
use crux_time::{Instant, TimeRequest, TimeResponse};
use crux_time::{protocol::TimeRequest, protocol::TimeResponse, Instant};
use std::collections::VecDeque;

pub enum Outcome {
Expand Down Expand Up @@ -191,7 +194,7 @@ mod tests {
};
use chrono::{DateTime, Utc};
use crux_core::{testing::AppTester, Core};
use crux_time::TimeResponse;
use crux_time::protocol::TimeResponse;

#[test]
pub fn test_time() {
Expand Down
8 changes: 0 additions & 8 deletions examples/notes/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit f6d7dd9

Please sign in to comment.