diff --git a/backend/src/api/live_simulation.rs b/backend/src/api/live_simulation.rs index 8c783da..3bc62f8 100644 --- a/backend/src/api/live_simulation.rs +++ b/backend/src/api/live_simulation.rs @@ -109,12 +109,21 @@ pub async fn stream( } let frame_step = metadata.frame_step_seconds.max(0.001); - let speed = query + let requested = query .speed .filter(|value| value.is_finite() && *value > 0.0) - .unwrap_or(1.0) - .clamp(live_simulation::MIN_SPEED, live_simulation::MAX_SPEED); - state.live_simulation.set_speed(session_key, speed).await; + .map(|value| value.clamp(live_simulation::MIN_SPEED, live_simulation::MAX_SPEED)); + let speed = match requested { + Some(speed) => { + state.live_simulation.set_speed(session_key, speed).await; + speed + } + None => state + .live_simulation + .speed(session_key) + .await + .unwrap_or(1.0), + }; let frame_delay = Duration::from_secs_f64((frame_step / speed).clamp(0.01, 2.0)); let replay_events = storage::get_replay_events(&state.pool, session_key).await?; let metadata_event = Event::default() diff --git a/backend/src/api/tests.rs b/backend/src/api/tests.rs index bb3b7c4..874962d 100644 --- a/backend/src/api/tests.rs +++ b/backend/src/api/tests.rs @@ -2013,6 +2013,40 @@ async fn live_simulation_start_accepts_lap_and_speed_and_speed_route_rebases() { .await; } +#[tokio::test] +async fn live_simulation_stream_without_speed_query_keeps_the_current_speed() { + let app = seeded_router().await; + post_json_body( + app.clone(), + "/api/sessions/9839/live-simulation/start", + json!({ "speed": 4.0 }), + StatusCode::OK, + ) + .await; + + let text = get_sse_prefix(app.clone(), "/api/sessions/9839/live-simulation/stream", |body| { + body.contains("event: snapshot") + }) + .await; + assert!(text.contains("event: metadata")); + + let before = get_json( + app.clone(), + "/api/sessions/9839/live-simulation/status", + StatusCode::OK, + ) + .await; + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + let after = get_json( + app, + "/api/sessions/9839/live-simulation/status", + StatusCode::OK, + ) + .await; + let advanced = after["current_t"].as_f64().unwrap() - before["current_t"].as_f64().unwrap(); + assert!(advanced >= 0.8, "simulation advanced only {advanced}s in 300ms; speed was reset"); +} + #[tokio::test] async fn live_simulation_requires_cached_replay() { let app = seeded_router().await; diff --git a/backend/src/live_simulation.rs b/backend/src/live_simulation.rs index c0e6763..4090dac 100644 --- a/backend/src/live_simulation.rs +++ b/backend/src/live_simulation.rs @@ -124,6 +124,14 @@ impl LiveSimulationRegistry { .unwrap_or_else(|| inactive_status(session_key)) } + pub async fn speed(&self, session_key: i64) -> Option { + self.sessions + .lock() + .await + .get(&session_key) + .map(|simulation| simulation.speed) + } + /// Rebases the clock so the current simulation time is continuous across the change. /// Returns `None` when no simulation is running for the session. pub async fn set_speed(&self, session_key: i64, speed: f64) -> Option { @@ -429,6 +437,19 @@ mod tests { assert!(registry.set_speed(42, 1.0).await.is_none()); } + #[tokio::test] + async fn speed_is_reported_for_running_simulations() { + let pool = demo_pool().await; + let registry = LiveSimulationRegistry::default(); + assert_eq!(registry.speed(9839).await, None); + let options = StartOptions { + speed: Some(4.0), + ..StartOptions::default() + }; + registry.start(&pool, 9839, options).await.unwrap(); + assert_eq!(registry.speed(9839).await, Some(4.0)); + } + #[tokio::test] async fn inactive_status_is_empty() { let registry = LiveSimulationRegistry::default();