persistent websocket
This commit is contained in:
parent
70d99b0c6b
commit
78999174ab
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -167,6 +167,7 @@ name = "common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
@ -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<common::State>,
|
||||
}
|
||||
|
||||
type Response<T> = Result<(StatusCode, Json<RestResponse<T>>), 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<SharedState>,
|
||||
Json(create_achievement): Json<CreateAchievement>,
|
||||
) -> 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<SharedState>,
|
||||
Json(create_milestone): Json<CreateMilestone>,
|
||||
) -> 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<SharedState>,
|
||||
Json(delete_milestone): Json<DeleteMilestone>,
|
||||
) -> 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<SharedState>,
|
||||
Json(toggle_achievement): Json<ToggleAchievement>,
|
||||
) -> 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<SharedState>,
|
||||
Json(delete_achievement): Json<DeleteAchievement>,
|
||||
) -> 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()
|
||||
}
|
||||
}
|
||||
|
@ -6,4 +6,5 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
thiserror = "1"
|
||||
uuid.workspace = true
|
||||
|
@ -2,6 +2,26 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub type WebSocketMessage = State;
|
||||
pub type RestResponse<T> = Result<T, RestError>;
|
||||
|
||||
#[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<String>) -> Self {
|
||||
Self { error: err.into() }
|
||||
}
|
||||
}
|
||||
impl<T> From<T> for RestError
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
RestError::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct State {
|
||||
|
@ -23,8 +23,10 @@
|
||||
<link data-trunk rel="rust" href="Cargo.toml" data-bin="app" data-type="main" />
|
||||
<link data-trunk rel="rust" href="Cargo.toml" data-bin="event_bus" data-type="worker" />
|
||||
</head>
|
||||
<!--
|
||||
<body>
|
||||
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
|
||||
<script>eruda.init();</script>
|
||||
</body>
|
||||
-->
|
||||
</html>
|
||||
|
@ -2,5 +2,5 @@ use frontend::event_bus::EventBus;
|
||||
use yew_agent::PublicWorker;
|
||||
|
||||
fn main() {
|
||||
EventBus::register();
|
||||
EventBus::<common::WebSocketMessage>::register();
|
||||
}
|
||||
|
@ -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.");
|
||||
|
@ -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<Self>, msg: Self::Message) -> bool {
|
||||
fn update(&mut self, ctx: &yew::Context<Self>, 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,31 +89,6 @@ 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_add_another = link.callback(|_: web_sys::MouseEvent| Msg::Reset);
|
||||
|
||||
html! {
|
||||
<>
|
||||
<div class="row" style="margin-top: 15%">
|
||||
<div class="twelve columns">
|
||||
<h4>{"Submitted!"}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="six columns">
|
||||
<button onclick={onclick_go_back} class="button">{"Go back"}</button>
|
||||
</div>
|
||||
<div class="six columns">
|
||||
<button onclick={onclick_add_another} class="button">{"Add another"}</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
let onclick_go_back = Callback::from(move |_: web_sys::MouseEvent| {
|
||||
nav.push(&crate::Route::Root);
|
||||
});
|
||||
@ -131,10 +119,9 @@ impl Component for CreateAchievementComponent {
|
||||
<input ref={self.input_ref.clone()} {oninput} value={self.input_value.clone()} class="u-full-width" type="text" id="achievementInput" />
|
||||
</div>
|
||||
</div>
|
||||
<input class="button-primary" type="submit" value="Submit" />
|
||||
<input class="button-primary" type="submit" value="Submit" disabled={self.awaiting_response} />
|
||||
</form>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,26 +91,6 @@ 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);
|
||||
});
|
||||
|
||||
html! {
|
||||
<>
|
||||
<div class="row" style="margin-top: 15%">
|
||||
<div class="twelve columns">
|
||||
<h4>{"Submitted!"}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="six columns">
|
||||
<button onclick={onclick_go_back} class="button">{"Go back"}</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
let onclick_go_back = Callback::from(move |_: web_sys::MouseEvent| {
|
||||
nav.push(&crate::Route::Root);
|
||||
});
|
||||
@ -139,10 +121,9 @@ impl Component for CreateMilestoneComponent {
|
||||
<input ref={self.input_ref.clone()} {oninput} value={self.input_value.to_string()} class="u-full-width" type="number" id="milestoneInput" />
|
||||
</div>
|
||||
</div>
|
||||
<input class="button-primary" type="submit" value="Submit" />
|
||||
<input class="button-primary" type="submit" value="Submit" disabled={self.awaiting_response} />
|
||||
</form>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,12 +54,16 @@ impl ErrorComponent {
|
||||
match &self.error {
|
||||
Some(error_msg) => {
|
||||
html! {
|
||||
<div class="error-message">
|
||||
{ error_msg }
|
||||
<button onclick={onclick_hide}>
|
||||
<>
|
||||
<div class="row">
|
||||
<h5><i style="margin-right: 1rem" class="fas fa-triangle-exclamation" />{"Error"}</h5>
|
||||
<p>{ error_msg }</p>
|
||||
<button onclick={onclick_hide} class="button-primary">
|
||||
{ "Hide" }
|
||||
</button>
|
||||
</div>
|
||||
<hr />
|
||||
</>
|
||||
}
|
||||
}
|
||||
None => html! {},
|
||||
|
@ -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! {
|
||||
<div class="row flex">
|
||||
<p style="text-align: center" class="flex-grow">
|
||||
{format!("{} / {}", props.completed_achievements, props.milestone.goal)}
|
||||
{
|
||||
if unfilled == 0 {
|
||||
// html! { <i style="color: var(--secondary-color)" class="fas fa-gift" /> }
|
||||
html! {
|
||||
<>
|
||||
{"🎉"}
|
||||
<span style="display: inline-block; width: 0.5em"></span>
|
||||
</>
|
||||
}
|
||||
} else { html! {} }
|
||||
}
|
||||
{ format!("{} / {}", filled, filled + unfilled) }
|
||||
{
|
||||
if unfilled == 0 {
|
||||
// html! { <i style="color: var(--secondary-color)" class="fas fa-gift" /> }
|
||||
html! {
|
||||
<>
|
||||
<span style="display: inline-block; width: 0.5em"></span>
|
||||
{"🎉"}
|
||||
</>
|
||||
}
|
||||
} else { html! {} }
|
||||
}
|
||||
<br />
|
||||
{filled_stars}
|
||||
{unfilled_stars}
|
||||
|
@ -1,56 +1,13 @@
|
||||
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<dyn Bridge<EventBus>>,
|
||||
|
||||
/// None until the first websocket message is received.
|
||||
state: Option<common::State>,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
HandleMsg(String),
|
||||
}
|
||||
|
||||
impl Component for Root {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(ctx: &Context<Self>) -> 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<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::HandleMsg(s) => {
|
||||
let msg: common::WebSocketMessage = serde_json::from_str(&s).unwrap();
|
||||
self.state = Some(msg);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let nav = ctx.link().navigator().unwrap();
|
||||
#[function_component]
|
||||
pub fn Root() -> Html {
|
||||
let nav = use_navigator().expect("cannot get navigator");
|
||||
let app_state = use_context::<crate::AppState>().expect("no app state ctx found");
|
||||
|
||||
let onclick_create_achievement = {
|
||||
let nav = nav.clone();
|
||||
@ -63,13 +20,8 @@ impl Component for Root {
|
||||
nav.push(&crate::Route::CreateMilestone);
|
||||
});
|
||||
|
||||
let Some(state) = &self.state else {
|
||||
return html! {
|
||||
<p>{"loading..."}</p>
|
||||
};
|
||||
};
|
||||
|
||||
let achievements = state
|
||||
let achievements = app_state
|
||||
.state
|
||||
.achievements
|
||||
.iter()
|
||||
.cloned()
|
||||
@ -80,9 +32,14 @@ impl Component for Root {
|
||||
})
|
||||
.collect::<Html>();
|
||||
|
||||
let completed_achievements = state.achievements.iter().filter(|a| a.completed).count();
|
||||
let completed_achievements = app_state
|
||||
.state
|
||||
.achievements
|
||||
.iter()
|
||||
.filter(|a| a.completed)
|
||||
.count();
|
||||
|
||||
let mut milestones = state.milestones.clone();
|
||||
let mut milestones = app_state.state.milestones.clone();
|
||||
milestones.sort_by_key(|m| m.goal);
|
||||
let milestones = milestones.into_iter().map(|m| html! { <MilestoneComponent milestone={m} completed_achievements={completed_achievements} /> }).collect::<Html>();
|
||||
|
||||
@ -117,5 +74,4 @@ impl Component for Root {
|
||||
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<T>
|
||||
where
|
||||
T: 'static + Serialize + DeserializeOwned + Clone,
|
||||
{
|
||||
link: WorkerLink<Self>,
|
||||
subscribers: HashSet<HandlerId>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum EventBusInput {
|
||||
EventBusMsg(String),
|
||||
pub enum EventBusInput<T> {
|
||||
EventBusMsg(T),
|
||||
}
|
||||
|
||||
impl yew_agent::Worker for EventBus {
|
||||
impl<T> yew_agent::Worker for EventBus<T>
|
||||
where
|
||||
T: 'static + Serialize + DeserializeOwned + Clone,
|
||||
{
|
||||
type Reach = Public<Self>;
|
||||
type Input = EventBusInput;
|
||||
type Output = String;
|
||||
type Input = EventBusInput<T>;
|
||||
type Output = T;
|
||||
type Message = ();
|
||||
|
||||
fn create(link: WorkerLink<Self>) -> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Self>, action: Self::Action) -> Rc<Self> {
|
||||
Rc::new(Self { state: action })
|
||||
}
|
||||
}
|
||||
type AppState = Rc<AppStateInner>;
|
||||
|
||||
#[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::<common::WebSocketMessage>::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! {
|
||||
<ContextProvider<AppState> context={(*ctx).clone()}>
|
||||
<ContextProvider<AppState> context={rc_app_state}>
|
||||
<ErrorProvider>
|
||||
<div class="container" style="margin-top: 5%; margin-bottom: 25%">
|
||||
<ErrorComponent />
|
||||
|
@ -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)
|
||||
|
@ -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<T: Serialize>(payload: T, path: &str) -> Result<(), RestServiceError> {
|
||||
async fn post_json<P: Serialize, T: DeserializeOwned>(
|
||||
payload: P,
|
||||
path: &str,
|
||||
) -> Result<T, RestServiceError> {
|
||||
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) => {
|
||||
let response = req.send().await.map_err(|err| {
|
||||
log::error!("Request failed: {:?}", err);
|
||||
Err(err.into())
|
||||
}
|
||||
err
|
||||
})?;
|
||||
|
||||
if let Ok(rest_response) = response.json::<RestResponse<T>>().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> {
|
||||
|
@ -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<String>,
|
||||
// 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::<String>(1000);
|
||||
let mut event_bus = EventBus::dispatcher();
|
||||
let (in_tx, mut in_rx) = futures::channel::mpsc::channel::<()>(1000);
|
||||
let mut event_bus = EventBus::<common::WebSocketMessage>::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));
|
||||
match serde_json::from_str::<common::WebSocketMessage>(&data) {
|
||||
Ok(ws_msg) => {
|
||||
log::debug!("Received ws message. Dispatching to event bus.");
|
||||
event_bus.send(EventBusInput::EventBusMsg(ws_msg));
|
||||
}
|
||||
Ok(Message::Bytes(b)) => {
|
||||
let decoded = std::str::from_utf8(&b);
|
||||
if let Ok(val) = decoded {
|
||||
event_bus.send(EventBusInput::EventBusMsg(val.into()));
|
||||
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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,3 +73,8 @@
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
filter: brightness(75%);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user