Add update subscription endpoint

This commit is contained in:
D. Scott Boggs 2023-06-18 08:11:11 -04:00
parent 3553743a9a
commit c22241008e
5 changed files with 123 additions and 23 deletions

View file

@ -4,25 +4,48 @@ mod groups;
mod import;
mod ticks;
mod tracks;
pub(crate) mod update;
use std::default::default;
use std::net::{IpAddr, Ipv4Addr};
use std::{
default::default,
net::{IpAddr, Ipv4Addr},
sync::Arc,
};
use crate::error::Error;
use crate::rocket::{Build, Rocket};
use rocket::fs::{FileServer, NamedFile};
use rocket::{routes, Config};
use rocket::{
fs::{FileServer, NamedFile},
response::stream::{Event, EventStream},
routes, Build, Config, Rocket, State,
};
use sea_orm::DatabaseConnection;
pub(crate) use error::ErrorResponder;
use tokio::sync::{
broadcast::{self, error::RecvError, Receiver},
RwLock,
};
use self::error::ApiResult;
use self::{error::ApiResult, update::Update};
#[get("/status")]
fn status() -> &'static str {
"Ok"
}
#[get("/updates")]
async fn stream_updates(rx: &State<Arc<RwLock<Receiver<Update>>>>) -> EventStream![Event + '_] {
let rx: Arc<RwLock<Receiver<Update>>> = (rx as &Arc<RwLock<Receiver<Update>>>).clone();
EventStream![loop {
let mut rx = rx.write().await;
match rx.recv().await {
Ok(update) => yield update.to_event(),
Err(RecvError::Closed) => break,
Err(RecvError::Lagged(count)) => yield Update::lagged(count).to_event(),
}
}]
}
#[catch(404)]
async fn spa_index_redirect() -> ApiResult<NamedFile> {
Ok(NamedFile::open("/src/public/index.html")
@ -34,6 +57,7 @@ pub(crate) fn start_server(db: DatabaseConnection) -> Rocket<Build> {
use groups::*;
use ticks::*;
use tracks::*;
let (tx, rx) = broadcast::channel::<Update>(8);
let it = rocket::build()
.configure(Config {
address: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
@ -41,7 +65,9 @@ pub(crate) fn start_server(db: DatabaseConnection) -> Rocket<Build> {
})
.register("/", catchers![spa_index_redirect])
.manage(db)
.mount("/api/v1", routes![status])
.manage(tx)
.manage(rx)
.mount("/api/v1", routes![status, stream_updates])
.mount(
"/api/v1/tracks",
routes![
@ -50,7 +76,8 @@ pub(crate) fn start_server(db: DatabaseConnection) -> Rocket<Build> {
ticks_for_track,
insert_track,
update_track,
delete_track
delete_track,
ticked,
],
)
.mount(

View file

@ -1,13 +1,14 @@
use either::{Either, Left, Right};
use rocket::{http::Status, serde::json::Json, State};
use sea_orm::{prelude::*, DatabaseConnection};
use tokio::sync::broadcast::Sender;
use crate::{
entities::{prelude::*, *},
error::Error,
};
use super::error::ApiResult;
use super::{error::ApiResult, update::Update};
#[get("/")]
pub(super) async fn all_ticks(
@ -59,10 +60,18 @@ pub(super) async fn update_tick(
}
#[delete("/<id>")]
pub(super) async fn delete_tick(db: &State<DatabaseConnection>, id: i32) -> ApiResult<Status> {
Ticks::delete_by_id(id)
.exec(db as &DatabaseConnection)
.await
.map_err(Error::from)?;
Ok(Status::Ok)
pub(super) async fn delete_tick(
db: &State<DatabaseConnection>,
tx: &State<Sender<Update>>,
id: i32,
) -> ApiResult<Status> {
let db = db as &DatabaseConnection;
let tick = Ticks::find_by_id(id).one(db).await.map_err(Error::from)?;
if let Some(tick) = tick {
tick.clone().delete(db).await.map_err(Error::from)?;
tx.send(Update::tick_cancelled(tick)).map_err(Error::from)?;
Ok(Status::Ok)
} else {
Ok(Status::NotFound)
}
}

View file

@ -6,6 +6,9 @@ use rocket::http::Status;
use rocket::{serde::json::Json, State};
use sea_orm::{prelude::*, DatabaseConnection};
use std::default::default;
use tokio::sync::broadcast::Sender;
use super::update::Update;
#[get("/")]
pub(super) async fn all_tracks(
@ -90,13 +93,16 @@ pub(super) async fn delete_track(db: &State<DatabaseConnection>, id: i32) -> Api
#[patch("/<id>/ticked")]
pub(super) async fn ticked(
db: &State<DatabaseConnection>,
tx: &State<Sender<Update>>,
id: i32,
) -> ApiResult<Json<ticks::Model>> {
let tick = ticks::ActiveModel::now(id);
Ok(Json(
tick.insert(db as &DatabaseConnection)
.await
.map_err(Error::from)?
.to_owned(),
))
let tick = tick
.insert(db as &DatabaseConnection)
.await
.map_err(Error::from)?
.to_owned();
tx.send(Update::tick_added(tick.clone()))
.map_err(Error::from)?;
Ok(Json(tick))
}

56
server/src/api/update.rs Normal file
View file

@ -0,0 +1,56 @@
use rocket::response::stream::Event;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::entities::ticks;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum Update {
TickChanged {
kind: UpdateType,
tick: ticks::Model,
},
Lagged {
kind: UpdateType,
count: u64,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum UpdateType {
TickAdded,
TickDropped,
Error,
}
impl Update {
pub(crate) fn lagged(count: u64) -> Update {
Update::Lagged {
kind: UpdateType::Error,
count,
}
}
pub(crate) fn tick_added(tick: ticks::Model) -> Self {
Self::TickChanged {
kind: UpdateType::TickAdded,
tick,
}
}
pub(crate) fn tick_cancelled(tick: ticks::Model) -> Self {
Self::TickChanged {
kind: UpdateType::TickDropped,
tick,
}
}
pub(crate) fn to_event(&self) -> Event {
use Update::*;
match self {
TickChanged { kind, tick } => Event::json(tick).event(format!("{kind:?}")),
Lagged { kind, count } => {
Event::json(&json! {{"message": "error: lagged", "count": count}})
.event(format!("{kind:?}"))
}
}
}
}

View file

@ -3,7 +3,7 @@ use std::string;
use derive_builder::UninitializedFieldError;
#[derive(Debug, thiserror::Error)]
pub enum Error {
pub(crate) enum Error {
#[error(transparent)]
Builder(#[from] UninitializedFieldError),
#[error(transparent)]
@ -16,6 +16,8 @@ pub enum Error {
Unreachable,
#[error(transparent)]
Utf8(#[from] string::FromUtf8Error),
#[error(transparent)]
ChannelSendError(#[from] tokio::sync::broadcast::error::SendError<crate::api::update::Update>),
}
pub type Result<T> = std::result::Result<T, Error>;
pub(crate) type Result<T> = std::result::Result<T, Error>;