[bugfix] harden countdown snapshot serialization and error messaging

- guard instant-to-epoch conversion against pre-boot instants
- serialize snapshot duration as milliseconds (u128) in DTO/commands
- derive Display/Error for CountdownError and return user-facing error strings
- remove unused command result alias and minor snapshot lock cleanup
This commit is contained in:
2026-02-26 17:44:07 +01:00
parent 8f8db9233b
commit 74c9ded46b
5 changed files with 15 additions and 13 deletions

View File

@@ -17,7 +17,10 @@ impl ClockAnchor {
}
pub fn instant_to_epoch_ms(&self, instant: tokio::time::Instant) -> u128 {
instant.duration_since(self.boot_instant).as_millis() + self.boot_epoch_ms
match instant.checked_duration_since(self.boot_instant) {
Some(duration) => duration.as_millis() + self.boot_epoch_ms,
None => return 0,
}
}
}
//TODO: implement the handling of multiple countdowns

View File

@@ -4,15 +4,13 @@ use crate::AppState;
use tauri::{command, State};
use tokio::time::Instant;
type CmdResult<T> = Result<T, String>;
#[command]
pub async fn countdown_start(state: State<'_, AppState>) -> Result<(), String> {
state
.countdown_service
.start(Instant::now())
.await
.map_err(|e: CountdownError| format!("{e:?}"))?;
.map_err(|e: CountdownError| e.to_string())?;
Ok(())
}
@@ -28,7 +26,7 @@ pub async fn countdown_pause(state: State<'_, AppState>) -> Result<(), String> {
.countdown_service
.pause(Instant::now())
.await
.map_err(|e: CountdownError| format!("{e:?}"))?;
.map_err(|e: CountdownError| e.to_string())?;
Ok(())
}
@@ -38,7 +36,7 @@ pub async fn countdown_resume(state: State<'_, AppState>) -> Result<(), String>
.countdown_service
.resume(Instant::now())
.await
.map_err(|e: CountdownError| format!("{e:?}"))?;
.map_err(|e: CountdownError| e.to_string())?;
Ok(())
}
@@ -58,7 +56,7 @@ pub async fn countdown_snapshot(
Ok(CountdownSnapshotDto {
id: countdown_snapshot.id,
label: countdown_snapshot.label,
duration: countdown_snapshot.duration,
duration: countdown_snapshot.duration.as_millis(),
state: countdown_snapshot.state,
start_epoch_ms: start,
target_epoch_ms: target,

View File

@@ -1,12 +1,10 @@
use tokio::time::Duration;
use crate::countdown::model::CountdownState;
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct CountdownSnapshotDto {
pub id: u64,
pub label: String,
pub duration: Duration,
pub duration: u128,
pub state: CountdownState,
pub start_epoch_ms: Option<u128>,
pub target_epoch_ms: Option<u128>,
@@ -16,7 +14,7 @@ impl CountdownSnapshotDto {
pub fn new(
id: u64,
label: String,
duration: Duration,
duration: u128,
state: CountdownState,
start_epoch_ms: Option<u128>,
target_epoch_ms: Option<u128>,

View File

@@ -1,10 +1,13 @@
use crate::countdown::model::CountdownState;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum CountdownError {
#[error("Invalid transition from {from:?} to {action:?}")]
InvalidTransition {
from: CountdownState,
action: &'static str,
},
#[error("Time overflow")]
TimeOverflow,
}

View File

@@ -30,7 +30,7 @@ impl CountdownService {
}
pub async fn snapshot(&self, now: Instant) -> CountdownSnapshot {
let countdown = self.countdown.lock().await.clone();
let countdown = self.countdown.lock().await;
CountdownSnapshot {
id: countdown.id(),
label: countdown.label().to_string(),