From 78999174ab102634b17ec81105146404aac800b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asger=20Juul=20Brunsh=C3=B8j?= Date: Tue, 13 Jun 2023 11:27:00 +0200 Subject: [PATCH] persistent websocket --- Cargo.lock | 1 + crates/backend/src/main.rs | 57 ++++--- crates/common/Cargo.toml | 1 + crates/common/src/lib.rs | 20 +++ crates/frontend/index.html | 2 + crates/frontend/src/bin/event_bus.rs | 2 +- crates/frontend/src/components/achievement.rs | 5 +- .../src/components/create_achievement.rs | 121 ++++++------- .../src/components/create_milestone.rs | 83 ++++----- .../src/components/error/error_component.rs | 10 +- crates/frontend/src/components/milestone.rs | 28 ++- crates/frontend/src/components/root.rs | 160 +++++++----------- crates/frontend/src/event_bus.rs | 28 ++- crates/frontend/src/lib.rs | 30 +++- crates/frontend/src/services/confirm.rs | 2 +- crates/frontend/src/services/rest.rs | 46 ++--- crates/frontend/src/services/websocket.rs | 42 +++-- crates/frontend/static/css/styles.css | 5 + 18 files changed, 334 insertions(+), 309 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cad306d..957ca06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -167,6 +167,7 @@ name = "common" version = "0.1.0" dependencies = [ "serde", + "thiserror", "uuid", ] diff --git a/crates/backend/src/main.rs b/crates/backend/src/main.rs index 73a6c87..1f213ef 100644 --- a/crates/backend/src/main.rs +++ b/crates/backend/src/main.rs @@ -16,6 +16,7 @@ use common::CreateMilestone; use common::DeleteAchievement; use common::DeleteMilestone; use common::Milestone; +use common::RestResponse; use common::ToggleAchievement; use serde::Deserialize; use serde::Serialize; @@ -37,6 +38,20 @@ struct SharedStateParts { watcher_tx: tokio::sync::watch::Sender, } +type Response = Result<(StatusCode, Json>), HandlerError>; +// TODO: still needed? +#[derive(Debug, thiserror::Error)] +enum HandlerError { + // #[error("Failed to lock state")] + // LockAppStateError, +} +impl IntoResponse for HandlerError { + fn into_response(self) -> axum::response::Response { + let error_message = format!("{self}"); + (StatusCode::INTERNAL_SERVER_ERROR, error_message).into_response() + } +} + #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); @@ -168,7 +183,7 @@ async fn handle_socket(mut socket: WebSocket, state_watch_rx: tokio::sync::watch async fn create_achievement( Extension(app_state): Extension, Json(create_achievement): Json, -) -> Result<(StatusCode, ()), HandlerError> { +) -> Response<()> { tracing::debug!("Creating achievement: {create_achievement:?}."); let achievement = Achievement { goal: create_achievement.goal, @@ -180,17 +195,21 @@ async fn create_achievement( lock.watcher_tx .send(lock.app_state.state.clone()) .expect("watch channel is closed, every receiver was dropped."); - Ok((StatusCode::CREATED, ())) + Ok((StatusCode::CREATED, Json(Ok(())))) } async fn create_milestone( Extension(app_state): Extension, Json(create_milestone): Json, -) -> Result<(StatusCode, ()), HandlerError> { +) -> Response<()> { tracing::debug!("Creating milestone: {create_milestone:?}."); - if create_milestone.goal > 100 { - return Ok((StatusCode::BAD_REQUEST, ())); + let goal_max = 100; + if create_milestone.goal > goal_max { + return Ok(( + StatusCode::BAD_REQUEST, + Json(Err(format!("Max goal allowed: {goal_max}.").into())), + )); } let milestone = Milestone { @@ -202,13 +221,13 @@ async fn create_milestone( lock.watcher_tx .send(lock.app_state.state.clone()) .expect("watch channel is closed, every receiver was dropped."); - Ok((StatusCode::CREATED, ())) + Ok((StatusCode::CREATED, Json(Ok(())))) } async fn delete_milestone( Extension(app_state): Extension, Json(delete_milestone): Json, -) -> Result<(StatusCode, ()), HandlerError> { +) -> Response<()> { tracing::debug!("Deleting milestone: {delete_milestone:?}."); let mut lock = app_state.write().await; if let Some(pos) = lock @@ -223,13 +242,13 @@ async fn delete_milestone( .send(lock.app_state.state.clone()) .expect("watch channel is closed, every receiver was dropped."); } - Ok((StatusCode::OK, ())) + Ok((StatusCode::OK, Json(Ok(())))) } async fn toggle_achievement( Extension(app_state): Extension, Json(toggle_achievement): Json, -) -> Result<(StatusCode, ()), HandlerError> { +) -> Response<()> { tracing::debug!("Toggling achievement: {toggle_achievement:?}."); let mut lock = app_state.write().await; if let Some(achievement) = lock @@ -244,13 +263,13 @@ async fn toggle_achievement( .send(lock.app_state.state.clone()) .expect("watch channel is closed, every receiver was dropped."); } - Ok((StatusCode::OK, ())) + Ok((StatusCode::OK, Json(Ok(())))) } async fn delete_achievement( Extension(app_state): Extension, Json(delete_achievement): Json, -) -> Result<(StatusCode, ()), HandlerError> { +) -> Response<()> { tracing::debug!("Deleting achievement: {delete_achievement:?}."); let mut lock = app_state.write().await; if let Some(pos) = lock @@ -265,7 +284,7 @@ async fn delete_achievement( .send(lock.app_state.state.clone()) .expect("watch channel is closed, every receiver was dropped."); } - Ok((StatusCode::OK, ())) + Ok((StatusCode::OK, Json(Ok(())))) } async fn shutdown_signal() { @@ -335,17 +354,3 @@ pub enum AppStateWriteError { #[error("Failed to serialize the state")] SerializationError(#[from] serde_json::Error), } - -// TODO: still needed? -#[derive(Debug, thiserror::Error)] -enum HandlerError { - // #[error("Failed to lock state")] - // LockAppStateError, -} - -impl IntoResponse for HandlerError { - fn into_response(self) -> axum::response::Response { - let error_message = format!("{self}"); - (StatusCode::INTERNAL_SERVER_ERROR, error_message).into_response() - } -} diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 275f276..a42c5c9 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -6,4 +6,5 @@ edition.workspace = true [dependencies] serde.workspace = true +thiserror = "1" uuid.workspace = true diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 8c7b8b8..f4b41b9 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -2,6 +2,26 @@ use serde::Deserialize; use serde::Serialize; pub type WebSocketMessage = State; +pub type RestResponse = Result; + +#[derive(thiserror::Error, Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[error("{error}")] +pub struct RestError { + pub error: String, +} +impl RestError { + pub fn new(err: impl Into) -> Self { + Self { error: err.into() } + } +} +impl From for RestError +where + T: Into, +{ + fn from(value: T) -> Self { + RestError::new(value) + } +} #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct State { diff --git a/crates/frontend/index.html b/crates/frontend/index.html index bbaa7c6..ee2fa71 100644 --- a/crates/frontend/index.html +++ b/crates/frontend/index.html @@ -23,8 +23,10 @@ + diff --git a/crates/frontend/src/bin/event_bus.rs b/crates/frontend/src/bin/event_bus.rs index c839061..1258a9d 100644 --- a/crates/frontend/src/bin/event_bus.rs +++ b/crates/frontend/src/bin/event_bus.rs @@ -2,5 +2,5 @@ use frontend::event_bus::EventBus; use yew_agent::PublicWorker; fn main() { - EventBus::register(); + EventBus::::register(); } diff --git a/crates/frontend/src/components/achievement.rs b/crates/frontend/src/components/achievement.rs index 12c93e6..39ca894 100644 --- a/crates/frontend/src/components/achievement.rs +++ b/crates/frontend/src/components/achievement.rs @@ -7,7 +7,6 @@ use wasm_bindgen_futures::spawn_local; use yew::classes; use yew::function_component; use yew::html; -use yew::use_memo; use yew::Callback; use yew::Html; use yew::Properties; @@ -25,8 +24,6 @@ pub fn AchievementComponent(props: &Props) -> Html { uuid, } = &props.achievement; - let confirm_service = use_memo(|_| ConfirmService, ()); - let uuid = *uuid; let onclick_toggle = Callback::from(move |_| { @@ -40,7 +37,7 @@ pub fn AchievementComponent(props: &Props) -> Html { }); let onclick_delete = Callback::from(move |_| { - if !confirm_service.confirm("Are you sure you want to delete?") { + if !ConfirmService::confirm("Are you sure you want to delete?") { return; } log::info!("Delete achievement confirmed."); diff --git a/crates/frontend/src/components/create_achievement.rs b/crates/frontend/src/components/create_achievement.rs index 05967a3..c695cec 100644 --- a/crates/frontend/src/components/create_achievement.rs +++ b/crates/frontend/src/components/create_achievement.rs @@ -1,4 +1,6 @@ +use super::error::error_provider::ErrorContext; use crate::services::rest::RestService; +use crate::services::rest::RestServiceError; use common::CreateAchievement; use wasm_bindgen::JsCast; use wasm_bindgen_futures::spawn_local; @@ -13,15 +15,16 @@ use yew_router::scope_ext::RouterScopeExt; #[derive(Debug)] pub enum Msg { Submit, - Reset, + SubmitResult(Result<(), RestServiceError>), UpdateInput(String), } #[derive(Default)] pub struct CreateAchievementComponent { input_value: String, - submitted: bool, input_ref: NodeRef, + + awaiting_response: bool, } impl Component for CreateAchievementComponent { type Message = Msg; @@ -42,27 +45,37 @@ impl Component for CreateAchievementComponent { } } - fn update(&mut self, _ctx: &yew::Context, msg: Self::Message) -> bool { + fn update(&mut self, ctx: &yew::Context, msg: Self::Message) -> bool { match msg { - Msg::Reset => { - self.input_value.clear(); - self.submitted = false; - true - } Msg::Submit => { log::info!("Creating achievement"); let payload = CreateAchievement { goal: self.input_value.clone(), }; + let link = ctx.link().clone(); spawn_local(async move { - match RestService::create_achievement(payload).await { - Ok(_response) => {} - Err(_err) => {} - } + let res = RestService::create_achievement(payload).await; + link.send_message(Msg::SubmitResult(res)); }); + self.awaiting_response = true; - self.submitted = true; - + true + } + Msg::SubmitResult(result) => { + match result { + Ok(_response) => { + let nav = ctx.link().navigator().unwrap(); + nav.push(&crate::Route::Root); + } + Err(err) => { + let (error_context, _context_listener) = ctx + .link() + .context(Callback::from(|_: ErrorContext| ())) + .expect("No Error Context Provided"); + error_context.dispatch(err.to_string()); + } + }; + self.awaiting_response = false; true } Msg::UpdateInput(value) => { @@ -76,65 +89,39 @@ impl Component for CreateAchievementComponent { let link = ctx.link().clone(); let nav = ctx.link().navigator().unwrap(); - if self.submitted { - let onclick_go_back = Callback::from(move |_: web_sys::MouseEvent| { - nav.push(&crate::Route::Root); - }); + let onclick_go_back = Callback::from(move |_: web_sys::MouseEvent| { + nav.push(&crate::Route::Root); + }); - let onclick_add_another = link.callback(|_: web_sys::MouseEvent| Msg::Reset); + let onsubmit = link.callback(|e: web_sys::SubmitEvent| { + e.prevent_default(); + Msg::Submit + }); - html! { - <> -
-
-

{"Submitted!"}

-
-
-
-
- -
-
- -
-
- - } - } else { - let onclick_go_back = Callback::from(move |_: web_sys::MouseEvent| { - nav.push(&crate::Route::Root); - }); - - let onsubmit = link.callback(|e: web_sys::SubmitEvent| { - e.prevent_default(); - Msg::Submit - }); - - let oninput = link.callback(|e: web_sys::InputEvent| { - let Some(input) = e + let oninput = link.callback(|e: web_sys::InputEvent| { + let Some(input) = e .target() .and_then(|t| t.dyn_into::().ok()) else { unreachable!() }; - Msg::UpdateInput(input.value()) - }); + Msg::UpdateInput(input.value()) + }); - html! { - <> -
- + html! { + <> +
+ +
+ +
+
+
+
+ +
- - -
-
-
- - -
-
- - - - } +
+ + + } } } diff --git a/crates/frontend/src/components/create_milestone.rs b/crates/frontend/src/components/create_milestone.rs index 09fed0c..c9a4c94 100644 --- a/crates/frontend/src/components/create_milestone.rs +++ b/crates/frontend/src/components/create_milestone.rs @@ -22,8 +22,10 @@ pub enum Msg { #[derive(Default)] pub struct CreateMilestoneComponent { input_value: String, - submitted: bool, input_ref: NodeRef, + + /// Submitted and awaiting response + awaiting_response: bool, } impl Component for CreateMilestoneComponent { type Message = Msg; @@ -57,7 +59,7 @@ impl Component for CreateMilestoneComponent { let res = RestService::create_milestone(payload).await; link.send_message(Msg::SubmitResult(res)); }); - self.submitted = true; + self.awaiting_response = true; true } @@ -72,10 +74,10 @@ impl Component for CreateMilestoneComponent { .link() .context(Callback::from(|_: ErrorContext| ())) .expect("No Error Context Provided"); - log::info!("dispatching error"); error_context.dispatch(err.to_string()); } }; + self.awaiting_response = false; true } Msg::UpdateInput(value) => { @@ -89,60 +91,39 @@ impl Component for CreateMilestoneComponent { let link = ctx.link().clone(); let nav = ctx.link().navigator().unwrap(); - if self.submitted { - let onclick_go_back = Callback::from(move |_: web_sys::MouseEvent| { - nav.push(&crate::Route::Root); - }); + let onclick_go_back = Callback::from(move |_: web_sys::MouseEvent| { + nav.push(&crate::Route::Root); + }); - html! { - <> -
-
-

{"Submitted!"}

-
-
-
-
- -
-
- - } - } else { - let onclick_go_back = Callback::from(move |_: web_sys::MouseEvent| { - nav.push(&crate::Route::Root); - }); + let onsubmit = link.callback(|e: web_sys::SubmitEvent| { + e.prevent_default(); + Msg::Submit + }); - let onsubmit = link.callback(|e: web_sys::SubmitEvent| { - e.prevent_default(); - Msg::Submit - }); - - let oninput = link.callback(|e: web_sys::InputEvent| { - let Some(input) = e + let oninput = link.callback(|e: web_sys::InputEvent| { + let Some(input) = e .target() .and_then(|t| t.dyn_into::().ok()) else { unreachable!() }; - Msg::UpdateInput(input.value()) - }); + Msg::UpdateInput(input.value()) + }); - html! { - <> -
- + html! { + <> +
+ +
+ +
+
+
+
+ +
- - -
-
-
- - -
-
- - - - } +
+ + + } } } diff --git a/crates/frontend/src/components/error/error_component.rs b/crates/frontend/src/components/error/error_component.rs index 529eb51..41ffd6b 100644 --- a/crates/frontend/src/components/error/error_component.rs +++ b/crates/frontend/src/components/error/error_component.rs @@ -54,12 +54,16 @@ impl ErrorComponent { match &self.error { Some(error_msg) => { html! { -
- { error_msg } -
+
+ } } None => html! {}, diff --git a/crates/frontend/src/components/milestone.rs b/crates/frontend/src/components/milestone.rs index 90a53c3..07c9fbb 100644 --- a/crates/frontend/src/components/milestone.rs +++ b/crates/frontend/src/components/milestone.rs @@ -13,8 +13,6 @@ pub struct Props { #[function_component(MilestoneComponent)] pub fn milestone_component(props: &Props) -> Html { - let confirm_service = use_memo(|_| ConfirmService, ()); - let unfilled = props.milestone.goal - props.completed_achievements.min(props.milestone.goal); let filled = props.completed_achievements.min(props.milestone.goal); @@ -32,7 +30,7 @@ pub fn milestone_component(props: &Props) -> Html { let uuid = props.milestone.uuid; let onclick_delete = Callback::from(move |_| { - if !confirm_service.confirm("Are you sure you want to delete?") { + if !ConfirmService::confirm("Are you sure you want to delete?") { return; } log::info!("Delete achievement confirmed."); @@ -48,7 +46,29 @@ pub fn milestone_component(props: &Props) -> Html { html! {

- {format!("{} / {}", props.completed_achievements, props.milestone.goal)} + { + if unfilled == 0 { + // html! { } + html! { + <> + {"🎉"} + + + } + } else { html! {} } + } + { format!("{} / {}", filled, filled + unfilled) } + { + if unfilled == 0 { + // html! { } + html! { + <> + + {"🎉"} + + } + } else { html! {} } + }
{filled_stars} {unfilled_stars} diff --git a/crates/frontend/src/components/root.rs b/crates/frontend/src/components/root.rs index 6a97c51..9e8acb4 100644 --- a/crates/frontend/src/components/root.rs +++ b/crates/frontend/src/components/root.rs @@ -1,121 +1,77 @@ use crate::components::achievement::AchievementComponent; use crate::components::milestone::MilestoneComponent; -use crate::event_bus::EventBus; -use crate::services::websocket::WebsocketService; -use std::rc::Rc; +use yew::functional::*; use yew::prelude::*; -use yew_agent::Bridge; -use yew_agent::Bridged; -use yew_router::scope_ext::RouterScopeExt; +use yew_router::prelude::*; -pub struct Root { - _wss: WebsocketService, - _producer: Box>, +#[function_component] +pub fn Root() -> Html { + let nav = use_navigator().expect("cannot get navigator"); + let app_state = use_context::().expect("no app state ctx found"); - /// None until the first websocket message is received. - state: Option, -} + let onclick_create_achievement = { + let nav = nav.clone(); + Callback::from(move |_: web_sys::MouseEvent| { + nav.push(&crate::Route::CreateAchievement); + }) + }; -pub enum Msg { - HandleMsg(String), -} + let onclick_create_milestone = Callback::from(move |_: web_sys::MouseEvent| { + nav.push(&crate::Route::CreateMilestone); + }); -impl Component for Root { - type Message = Msg; - type Properties = (); - - fn create(ctx: &Context) -> Self { - let wss = WebsocketService::new(); - - let cb = { - let link = ctx.link().clone(); - move |e| link.send_message(Msg::HandleMsg(e)) - }; - - Self { - _wss: wss, - _producer: EventBus::bridge(Rc::new(cb)), - state: None, - } - } - - fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { - match msg { - Msg::HandleMsg(s) => { - let msg: common::WebSocketMessage = serde_json::from_str(&s).unwrap(); - self.state = Some(msg); - true + let achievements = app_state + .state + .achievements + .iter() + .cloned() + .map(|a| { + html! { + } - } - } + }) + .collect::(); - fn view(&self, ctx: &Context) -> Html { - let nav = ctx.link().navigator().unwrap(); + let completed_achievements = app_state + .state + .achievements + .iter() + .filter(|a| a.completed) + .count(); - let onclick_create_achievement = { - let nav = nav.clone(); - Callback::from(move |_: web_sys::MouseEvent| { - nav.push(&crate::Route::CreateAchievement); - }) - }; + let mut milestones = app_state.state.milestones.clone(); + milestones.sort_by_key(|m| m.goal); + let milestones = milestones.into_iter().map(|m| html! { }).collect::(); - let onclick_create_milestone = Callback::from(move |_: web_sys::MouseEvent| { - nav.push(&crate::Route::CreateMilestone); - }); + html! { + <> - let Some(state) = &self.state else { - return html! { -

{"loading..."}

- }; - }; +

{"Polter A"}{"bend"}{"chievements!"}

- let achievements = state - .achievements - .iter() - .cloned() - .map(|a| { - html! { - - } - }) - .collect::(); +
+
+
+

{"Milestones"}

+
- let completed_achievements = state.achievements.iter().filter(|a| a.completed).count(); +
+ +
+
+ {milestones} - let mut milestones = state.milestones.clone(); - milestones.sort_by_key(|m| m.goal); - let milestones = milestones.into_iter().map(|m| html! { }).collect::(); +
+
+
+

{"Achievements"}

+
- html! { - <> +
+ +
+
+ {achievements} -

{"Polter A"}{"bend"}{"chievements!"}

- -
-
-
-

{"Milestones"}

-
- -
- -
-
- {milestones} - -
-
-
-

{"Achievements"}

-
- -
- -
-
- {achievements} - - - } + } } diff --git a/crates/frontend/src/event_bus.rs b/crates/frontend/src/event_bus.rs index 9841f16..f1cf165 100644 --- a/crates/frontend/src/event_bus.rs +++ b/crates/frontend/src/event_bus.rs @@ -1,3 +1,4 @@ +use serde::de::DeserializeOwned; use serde::Deserialize; use serde::Serialize; use std::collections::HashSet; @@ -5,23 +6,30 @@ use yew_agent::HandlerId; use yew_agent::Public; use yew_agent::WorkerLink; -pub struct EventBus { +pub struct EventBus +where + T: 'static + Serialize + DeserializeOwned + Clone, +{ link: WorkerLink, subscribers: HashSet, } #[derive(Serialize, Deserialize)] -pub enum EventBusInput { - EventBusMsg(String), +pub enum EventBusInput { + EventBusMsg(T), } -impl yew_agent::Worker for EventBus { +impl yew_agent::Worker for EventBus +where + T: 'static + Serialize + DeserializeOwned + Clone, +{ type Reach = Public; - type Input = EventBusInput; - type Output = String; + type Input = EventBusInput; + type Output = T; type Message = (); fn create(link: WorkerLink) -> Self { + log::debug!("Creating Event Bus."); Self { link, subscribers: HashSet::new(), @@ -32,9 +40,13 @@ impl yew_agent::Worker for EventBus { fn handle_input(&mut self, msg: Self::Input, _id: HandlerId) { match msg { - EventBusInput::EventBusMsg(s) => { + EventBusInput::EventBusMsg(msg) => { + log::debug!( + "Event bus received message. Responding to {} subscribers.", + self.subscribers.len() + ); for sub in &self.subscribers { - self.link.respond(*sub, s.clone()); + self.link.respond(*sub, msg.clone()); } } } diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index c11be58..9b54af5 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -1,11 +1,14 @@ use crate::components::error::error_component::ErrorComponent; use crate::components::error::error_provider::ErrorProvider; +use crate::event_bus::EventBus; +use crate::services::websocket::WebsocketService; use components::admin::Admin; use components::create_achievement::CreateAchievementComponent; use components::create_milestone::CreateMilestoneComponent; use components::root::Root; use std::rc::Rc; use yew::prelude::*; +use yew_agent::Bridged; use yew_router::BrowserRouter; use yew_router::Routable; use yew_router::Switch; @@ -41,16 +44,39 @@ fn switch(selected_route: Route) -> Html { #[derive(Default, Clone, Debug, PartialEq, Eq)] struct AppStateInner { + /// The part of the state that comes from the server state: common::State, } +impl Reducible for AppStateInner { + type Action = common::WebSocketMessage; + fn reduce(self: Rc, action: Self::Action) -> Rc { + Rc::new(Self { state: action }) + } +} type AppState = Rc; #[function_component] pub fn App() -> Html { - let ctx = use_state(|| Rc::new(AppStateInner::default())); + let app_state = use_reducer(AppStateInner::default); + let app_state_dispatcher = app_state.dispatcher(); + + let _event_bus = use_memo( + |_| { + log::info!("Creating event bus bridge."); + EventBus::::bridge(Rc::new(move |ws_msg: common::WebSocketMessage| { + log::debug!("dispatching websocket msg to reducer"); + app_state_dispatcher.dispatch(ws_msg); + })) + }, + (), + ); + + let _wss = use_memo(|_| Rc::new(WebsocketService::connect()), ()); + + let rc_app_state = Rc::new((*app_state).clone()); html! { - context={(*ctx).clone()}> + context={rc_app_state}>
diff --git a/crates/frontend/src/services/confirm.rs b/crates/frontend/src/services/confirm.rs index 3751875..0b4ff23 100644 --- a/crates/frontend/src/services/confirm.rs +++ b/crates/frontend/src/services/confirm.rs @@ -2,7 +2,7 @@ pub struct ConfirmService; impl ConfirmService { - pub fn confirm(&self, message: &str) -> bool { + pub fn confirm(message: &str) -> bool { web_sys::window() .expect("no global `window` exists") .confirm_with_message(message) diff --git a/crates/frontend/src/services/rest.rs b/crates/frontend/src/services/rest.rs index 058ae65..b9c0213 100644 --- a/crates/frontend/src/services/rest.rs +++ b/crates/frontend/src/services/rest.rs @@ -2,8 +2,10 @@ use common::CreateAchievement; use common::CreateMilestone; use common::DeleteAchievement; use common::DeleteMilestone; +use common::RestResponse; use common::ToggleAchievement; use reqwasm::http::Request; +use serde::de::DeserializeOwned; use serde::Serialize; pub struct RestService; @@ -13,11 +15,14 @@ pub enum RestServiceError { #[error("Failed to serialize/deserialize data: {0}")] SerdeError(#[from] serde_json::Error), + #[error(transparent)] + RestError(#[from] common::RestError), + + #[error("Failed request: {0}")] + ReqwasmError(#[from] reqwasm::Error), + #[error("Unexpected response")] UnexpectedResponse(reqwasm::http::Response), - - #[error("Failed request")] - ReqwasmError(#[from] reqwasm::Error), } impl RestService { @@ -29,31 +34,26 @@ impl RestService { format!("http://{}:4000{endpoint}", Self::hostname()) } - async fn post_json(payload: T, path: &str) -> Result<(), RestServiceError> { + async fn post_json( + payload: P, + path: &str, + ) -> Result { let req = Request::post(&Self::url_to(path)) .header("Content-Type", "application/json") .body(serde_json::to_string(&payload)?); - let res = req.send().await; - match res { - Ok(response) => { - let status_code = response.status(); - if (200..300).contains(&status_code) { - Ok(()) - } else { - log::warn!( - "Received unexpected status code: {}. Response: {:?}", - status_code, - response - ); - Err(RestServiceError::UnexpectedResponse(response)) - } - } - Err(err) => { - log::error!("Request failed: {:?}", err); - Err(err.into()) - } + let response = req.send().await.map_err(|err| { + log::error!("Request failed: {:?}", err); + err + })?; + + if let Ok(rest_response) = response.json::>().await { + return rest_response.map_err(Into::into); } + + let err = RestServiceError::UnexpectedResponse(response); + log::warn!("{err:?}"); + Err(err) } pub async fn toggle_achievement(payload: ToggleAchievement) -> Result<(), RestServiceError> { diff --git a/crates/frontend/src/services/websocket.rs b/crates/frontend/src/services/websocket.rs index 78081a8..cfb215e 100644 --- a/crates/frontend/src/services/websocket.rs +++ b/crates/frontend/src/services/websocket.rs @@ -1,49 +1,57 @@ use crate::event_bus::EventBus; use crate::event_bus::EventBusInput; use futures::channel::mpsc::Sender; -use futures::SinkExt; use futures::StreamExt; use reqwasm::websocket::futures::WebSocket; use reqwasm::websocket::Message; use wasm_bindgen_futures::spawn_local; use yew_agent::Dispatched; +#[derive(Debug)] pub struct WebsocketService { - pub tx: Sender, + // No messages sent from app to server on websocket at the moment. + pub tx: Sender<()>, } impl WebsocketService { - pub fn new() -> Self { + pub fn connect() -> Self { let hostname = web_sys::window().unwrap().location().hostname().unwrap(); let ws_addr = format!("ws://{hostname}:4000/ws"); let ws = WebSocket::open(&ws_addr).unwrap(); + log::info!("Opened websocket connection to {ws_addr}"); - let (mut write, mut read) = ws.split(); + let (_write, mut read) = ws.split(); - let (in_tx, mut in_rx) = futures::channel::mpsc::channel::(1000); - let mut event_bus = EventBus::dispatcher(); + let (in_tx, mut in_rx) = futures::channel::mpsc::channel::<()>(1000); + let mut event_bus = EventBus::::dispatcher(); + // App to Server spawn_local(async move { - while let Some(s) = in_rx.next().await { - log::debug!("got event from channel! {}", s); - write.send(Message::Text(s)).await.unwrap(); + while let Some(()) = in_rx.next().await { + // write.send(Message::Text(s)).await.unwrap(); } }); + // Server to App spawn_local(async move { while let Some(msg) = read.next().await { match msg { Ok(Message::Text(data)) => { - event_bus.send(EventBusInput::EventBusMsg(data)); - } - Ok(Message::Bytes(b)) => { - let decoded = std::str::from_utf8(&b); - if let Ok(val) = decoded { - event_bus.send(EventBusInput::EventBusMsg(val.into())); + match serde_json::from_str::(&data) { + Ok(ws_msg) => { + log::debug!("Received ws message. Dispatching to event bus."); + event_bus.send(EventBusInput::EventBusMsg(ws_msg)); + } + Err(err) => { + log::error!("{err:?}"); + } } } - Err(e) => { - log::error!("ws: {:?}", e); + Ok(Message::Bytes(_)) => { + log::warn!("Received binary data on websocket. This is unhandled"); + } + Err(err) => { + log::error!("ws error: {err:?}"); } } } diff --git a/crates/frontend/static/css/styles.css b/crates/frontend/static/css/styles.css index a8f87ba..0443eaa 100644 --- a/crates/frontend/static/css/styles.css +++ b/crates/frontend/static/css/styles.css @@ -73,3 +73,8 @@ .flex-grow { flex-grow: 1; } + +input:disabled { + filter: brightness(75%); + cursor: not-allowed; +}