use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use sl_map_apis::map_tiles::{MapProgressEvent, TileOutcome};
use sl_types::map::MapTileDescriptor;
use tokio::sync::{Mutex, broadcast, watch};
use uuid::Uuid;
pub type JobId = Uuid;
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ProgressDto {
PlanComputed {
zoom_level: u8,
total_tiles: u32,
},
TileStarted {
zoom: u8,
x: u32,
y: u32,
},
TileFinished {
zoom: u8,
x: u32,
y: u32,
outcome: &'static str,
},
RegionCheckPlanned {
total_regions: u32,
},
RegionChecked {
x: u32,
y: u32,
exists: bool,
},
RoutePlanned {
total_waypoints: usize,
},
RouteWaypointResolved {
index: usize,
total: usize,
region: String,
},
RegionNamesPlanned {
total_regions: u32,
},
RegionNameResolved {
index: u32,
total: u32,
},
Done,
Error {
message: String,
},
}
impl From<MapProgressEvent> for ProgressDto {
fn from(value: MapProgressEvent) -> Self {
match value {
MapProgressEvent::PlanComputed {
zoom_level,
total_tiles,
} => Self::PlanComputed {
zoom_level: zoom_level.into_inner(),
total_tiles,
},
MapProgressEvent::TileStarted { descriptor } => {
let (zoom, x, y) = split_descriptor(&descriptor);
Self::TileStarted { zoom, x, y }
}
MapProgressEvent::TileFinished {
descriptor,
outcome,
} => {
let (zoom, x, y) = split_descriptor(&descriptor);
Self::TileFinished {
zoom,
x,
y,
outcome: outcome_str(outcome),
}
}
MapProgressEvent::RegionCheckPlanned { total_regions } => {
Self::RegionCheckPlanned { total_regions }
}
MapProgressEvent::RegionChecked { x, y, exists } => {
Self::RegionChecked { x, y, exists }
}
MapProgressEvent::RoutePlanned { total_waypoints } => {
Self::RoutePlanned { total_waypoints }
}
MapProgressEvent::RouteWaypointResolved {
index,
total,
region,
} => Self::RouteWaypointResolved {
index,
total,
region: region.into_inner(),
},
MapProgressEvent::RegionNamesPlanned { total_regions } => {
Self::RegionNamesPlanned { total_regions }
}
MapProgressEvent::RegionNameResolved { index, total } => {
Self::RegionNameResolved { index, total }
}
}
}
}
fn split_descriptor(descriptor: &MapTileDescriptor) -> (u8, u32, u32) {
let zoom = (*descriptor.zoom_level()).into_inner();
let corner = descriptor.lower_left_corner();
(zoom, corner.x(), corner.y())
}
const fn outcome_str(outcome: TileOutcome) -> &'static str {
match outcome {
TileOutcome::LoadedFromMemoryCache => "memory",
TileOutcome::LoadedFromDiskCache => "disk",
TileOutcome::FetchedFromNetwork => "network",
TileOutcome::Missing => "missing",
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metadata {
pub aspect_x: u32,
pub aspect_y: u32,
pub aspect_ratio: f64,
pub pps_hud_config: String,
}
#[derive(Debug, Clone)]
pub enum JobOutcome {
Ok {
image: Bytes,
image_without_route: Option<Bytes>,
content_type: &'static str,
metadata: Metadata,
glw_data_id: Option<uuid::Uuid>,
},
Err(String),
}
#[derive(Debug)]
pub struct JobState {
pub created: Instant,
pub events: Mutex<Vec<ProgressDto>>,
pub ping: broadcast::Sender<()>,
pub outcome: watch::Sender<Option<Arc<JobOutcome>>>,
}
impl JobState {
#[must_use]
pub fn new() -> Self {
let (ping, _) = broadcast::channel(64);
let (outcome, _) = watch::channel(None);
Self {
created: Instant::now(),
events: Mutex::new(Vec::new()),
ping,
outcome,
}
}
}
impl Default for JobState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default)]
pub struct JobStore {
inner: Mutex<HashMap<JobId, Arc<JobState>>>,
}
impl JobStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub async fn create(&self) -> (JobId, Arc<JobState>) {
self.create_with_id(Uuid::new_v4()).await
}
pub async fn create_with_id(&self, id: JobId) -> (JobId, Arc<JobState>) {
let state = Arc::new(JobState::new());
{
let mut guard = self.inner.lock().await;
drop(guard.insert(id, Arc::clone(&state)));
}
(id, state)
}
pub async fn get(&self, id: JobId) -> Option<Arc<JobState>> {
let guard = self.inner.lock().await;
guard.get(&id).map(Arc::clone)
}
pub async fn evict_older_than(&self, max_age: Duration) {
let now = Instant::now();
let mut guard = self.inner.lock().await;
guard.retain(|_, state| {
let age = now.saturating_duration_since(state.created);
let finished = state.outcome.borrow().is_some();
!finished || age <= max_age
});
}
}
pub async fn record_event(state: &JobState, event: ProgressDto) {
{
let mut events = state.events.lock().await;
events.push(event);
}
drop(state.ping.send(()));
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn region_name_progress_events_serialise_for_the_client()
-> Result<(), Box<dyn std::error::Error>> {
let planned = ProgressDto::from(MapProgressEvent::RegionNamesPlanned { total_regions: 42 });
assert_eq!(
serde_json::to_value(&planned)?,
serde_json::json!({ "type": "region_names_planned", "total_regions": 42 })
);
let resolved = ProgressDto::from(MapProgressEvent::RegionNameResolved {
index: 7,
total: 42,
});
assert_eq!(
serde_json::to_value(&resolved)?,
serde_json::json!({ "type": "region_name_resolved", "index": 7, "total": 42 })
);
Ok(())
}
}