Trading card city builder game?

experimentation and refactoring of client side websocket

eldridge.cam 57207b58 0b1768bb

verified
Waiting for spindle ...
+143 -26
+3 -2
Cargo.toml
··· 21 21 serde_json = "1.0.149" 22 22 sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres"], optional = true } 23 23 tokio = { version = "1.49.0", features = ["macros"] } 24 - tokio-stream = "0.1.18" 24 + tokio-stream = { version = "0.1.18", features = ["sync"] } 25 25 tracing = "0.1.44" 26 26 uuid = { version = "1.20.0", features = ["serde", "v7"] } 27 27 ··· 31 31 desktop = ["dioxus/desktop", "tokio/rt", "uuid/js"] 32 32 mobile = ["dioxus/mobile", "tokio/rt", "uuid/js"] 33 33 server = ["dioxus/server", "tokio/rt-multi-thread", "dep:sqlx", "dep:axum"] 34 - gloo = ["dep:gloo"] 34 + 35 + messagepack = []
+17 -2
src/api/ws.rs
··· 18 18 19 19 #[derive(Serialize, Deserialize, Clone, Debug)] 20 20 pub struct ProtocolV1Message<T> { 21 - id: Uuid, 21 + pub id: Uuid, 22 22 #[serde(flatten)] 23 - data: T, 23 + pub data: T, 24 + } 25 + 26 + impl<T> ProtocolV1Message<T> { 27 + pub fn new(data: T) -> Self { 28 + Self { 29 + id: Uuid::now_v7(), 30 + data, 31 + } 32 + } 33 + } 34 + 35 + impl<T> From<T> for ProtocolV1Message<T> { 36 + fn from(value: T) -> Self { 37 + Self::new(value) 38 + } 24 39 } 25 40 26 41 pub const JSON_PROTOCOL: &str = "v1-json.cartography.app";
+1
src/app/hooks/mod.rs
··· 1 1 pub mod use_custom_websocket; 2 + pub mod use_game_websocket;
+92 -19
src/app/hooks/use_custom_websocket.rs
··· 1 - use crate::actor::player_socket::{Request, Response}; 2 - use crate::api::{self, ws::ProtocolV1Message}; 1 + use std::marker::PhantomData; 2 + 3 3 use dioxus::fullstack::{get_server_url, WebsocketState}; 4 4 use dioxus::prelude::*; 5 5 use futures::stream::{SplitSink, SplitStream}; ··· 8 8 use gloo::net::websocket::futures::WebSocket; 9 9 #[cfg(feature = "web")] 10 10 use gloo::net::websocket::Message; 11 + use serde::de::DeserializeOwned; 12 + use serde::Serialize; 11 13 use tokio::sync::Mutex; 12 14 13 15 #[cfg(feature = "web")] ··· 17 19 receiver: Mutex<SplitStream<WebSocket>>, 18 20 } 19 21 20 - #[derive(Copy, Clone)] 21 - pub struct UseCustomWebsocket { 22 + pub struct UseCustomWebsocket<In, Out, Enc> { 22 23 waker: UseWaker<()>, 23 24 #[cfg(feature = "web")] 24 25 connection: Resource<anyhow::Result<CustomWebSocket>>, 25 26 status: Signal<WebsocketState>, 26 27 status_read: ReadSignal<WebsocketState>, 28 + _pd: PhantomData<(In, Out, Enc)>, 27 29 } 28 30 29 - pub fn use_custom_websocket(path: &'static str) -> UseCustomWebsocket { 31 + pub struct WebsocketSender<In, Enc> { 32 + #[cfg(feature = "web")] 33 + sender: Mutex<SplitSink<WebSocket, Message>>, 34 + status: Signal<WebsocketState>, 35 + status_read: ReadSignal<WebsocketState>, 36 + _pd: PhantomData<(In, Enc)>, 37 + } 38 + 39 + pub struct WebsocketReceiver<Out, Enc> { 40 + #[cfg(feature = "web")] 41 + receiver: Mutex<SplitStream<WebSocket>>, 42 + status: Signal<WebsocketState>, 43 + status_read: ReadSignal<WebsocketState>, 44 + _pd: PhantomData<(Out, Enc)>, 45 + } 46 + 47 + impl<In, Out, Enc> Copy for UseCustomWebsocket<In, Out, Enc> {} 48 + impl<In, Out, Enc> Clone for UseCustomWebsocket<In, Out, Enc> { 49 + fn clone(&self) -> Self { 50 + *self 51 + } 52 + } 53 + 54 + pub fn use_custom_websocket< 55 + In: Serialize + DeserializeOwned, 56 + Out: Serialize + DeserializeOwned, 57 + Enc: Encoding, 58 + >( 59 + path: &'static str, 60 + protocols: &'static [&'static str], 61 + ) -> UseCustomWebsocket<In, Out, Enc> { 30 62 let mut waker = use_waker(); 31 63 #[cfg(feature = "web")] 32 64 let mut status = use_signal(|| WebsocketState::Connecting); ··· 38 70 let connection = use_resource(move || async move { 39 71 let socket = match gloo::net::websocket::futures::WebSocket::open_with_protocols( 40 72 &format!("{}/{}", get_server_url(), path), 41 - &[api::ws::JSON_PROTOCOL, api::ws::MESSAGEPACK_PROTOCOL], 73 + protocols, 42 74 ) { 43 75 Ok(socket) => { 44 76 status.set(WebsocketState::Open); ··· 68 100 connection, 69 101 status, 70 102 status_read, 103 + _pd: PhantomData, 71 104 } 72 105 } 73 106 74 - impl UseCustomWebsocket { 107 + pub trait Encoding { 108 + #[cfg(feature = "web")] 109 + fn encode<T: Serialize>(message: &T) -> anyhow::Result<Message>; 110 + 111 + #[cfg(feature = "web")] 112 + fn decode<T: DeserializeOwned>(message: Message) -> anyhow::Result<T>; 113 + } 114 + 115 + #[allow(dead_code)] 116 + pub struct Json; 117 + 118 + impl Encoding for Json { 119 + #[cfg(feature = "web")] 120 + fn encode<T: Serialize>(message: &T) -> anyhow::Result<Message> { 121 + Ok(serde_json::to_string(message).map(Message::Text)?) 122 + } 123 + 124 + #[cfg(feature = "web")] 125 + fn decode<T: DeserializeOwned>(message: Message) -> anyhow::Result<T> { 126 + match message { 127 + Message::Text(text) => Ok(serde_json::from_str(&text)?), 128 + Message::Bytes(_) => anyhow::bail!("expected text message"), 129 + } 130 + } 131 + } 132 + 133 + #[allow(dead_code)] 134 + pub struct MessagePack; 135 + 136 + impl Encoding for MessagePack { 137 + #[cfg(feature = "web")] 138 + fn encode<T: Serialize>(message: &T) -> anyhow::Result<Message> { 139 + Ok(rmp_serde::to_vec(message).map(Message::Bytes)?) 140 + } 141 + 142 + #[cfg(feature = "web")] 143 + fn decode<T: DeserializeOwned>(message: Message) -> anyhow::Result<T> { 144 + match message { 145 + Message::Bytes(bytes) => Ok(rmp_serde::from_slice(&bytes)?), 146 + Message::Text(_) => anyhow::bail!("expected binary message"), 147 + } 148 + } 149 + } 150 + 151 + impl<In: Serialize + DeserializeOwned, Out: Serialize + DeserializeOwned, Enc: Encoding> 152 + UseCustomWebsocket<In, Out, Enc> 153 + { 75 154 #[cfg(not(feature = "web"))] 76 155 pub async fn connect(&self) -> WebsocketState { 77 156 WebsocketState::FailedToConnect ··· 90 169 } 91 170 92 171 #[cfg(not(feature = "web"))] 93 - pub async fn send(&self, msg: ProtocolV1Message<Request>) -> anyhow::Result<()> { 172 + pub async fn send(&self, msg: In) -> anyhow::Result<()> { 94 173 Err(anyhow::anyhow!("not implemented on this platform")) 95 174 } 96 175 97 176 #[cfg(not(feature = "web"))] 98 - pub async fn recv(&self) -> anyhow::Result<ProtocolV1Message<Response>> { 177 + pub async fn recv(&self) -> anyhow::Result<Out> { 99 178 Err(anyhow::anyhow!("not implemented on this platform")) 100 179 } 101 180 102 181 #[cfg(feature = "web")] 103 - pub async fn send(&self, msg: ProtocolV1Message<Request>) -> anyhow::Result<()> { 182 + pub async fn send(&self, msg: In) -> anyhow::Result<()> { 104 183 self.connect().await; 105 184 106 185 let connection = self.connection.as_ref(); ··· 109 188 .ok_or_else(|| anyhow::anyhow!("websocket closed away"))? 110 189 .as_ref() 111 190 .map_err(|err| anyhow::anyhow!("{err}"))?; 112 - 113 - let msg = match connection.protocol.as_str() { 114 - api::ws::MESSAGEPACK_PROTOCOL => rmp_serde::to_vec(&msg).map(Message::Bytes)?, 115 - _ => serde_json::to_string(&msg).map(Message::Text)?, 116 - }; 117 - 191 + let msg = Enc::encode(&msg)?; 118 192 connection.sender.lock().await.send(msg).await?; 119 193 120 194 Ok(()) 121 195 } 122 196 123 197 #[cfg(feature = "web")] 124 - pub async fn recv(&self) -> anyhow::Result<ProtocolV1Message<Response>> { 198 + pub async fn recv(&self) -> anyhow::Result<Out> { 125 199 self.connect().await; 126 200 127 201 let connection = self.connection.as_ref(); ··· 133 207 134 208 let mut recv = connection.receiver.lock().await; 135 209 match recv.next().await { 136 - Some(Ok(Message::Text(text))) => Ok(serde_json::from_str(&text)?), 137 - Some(Ok(Message::Bytes(bytes))) => Ok(rmp_serde::from_read(&*bytes)?), 210 + Some(Ok(msg)) => Ok(Enc::decode(msg)?), 138 211 Some(Err(e)) => Err(e.into()), 139 212 None => anyhow::bail!("closed away"), 140 213 }
+18
src/app/hooks/use_game_websocket.rs
··· 1 + use crate::actor::player_socket::{Request, Response}; 2 + use crate::api::ws::{self, ProtocolV1Message}; 3 + use crate::app::hooks::use_custom_websocket::{self, UseCustomWebsocket, use_custom_websocket}; 4 + 5 + #[cfg(not(feature = "messagepack"))] 6 + type Encoding = use_custom_websocket::Json; 7 + #[cfg(not(feature = "messagepack"))] 8 + const PROTOCOL: &str = ws::JSON_PROTOCOL; 9 + 10 + #[cfg(feature = "messagepack")] 11 + type Encoding = use_custom_websocket::MessagePack; 12 + #[cfg(feature = "messagepack")] 13 + const PROTOCOL: &str = ws::MESSAGEPACK_PROTOCOL; 14 + 15 + pub fn use_game_websocket() 16 + -> UseCustomWebsocket<ProtocolV1Message<Request>, ProtocolV1Message<Response>, Encoding> { 17 + use_custom_websocket("play/ws", &[PROTOCOL]) 18 + }
+12 -3
src/app/play.rs
··· 1 - use crate::app::hooks::use_custom_websocket::use_custom_websocket; 1 + use crate::actor::player_socket::Request; 2 + use crate::app::hooks::use_game_websocket::use_game_websocket; 2 3 use dioxus::prelude::*; 3 4 4 5 #[component] 5 6 pub fn Play() -> Element { 6 - let socket = use_custom_websocket("play/ws"); 7 + let socket = use_game_websocket(); 8 + 9 + use_future(move || async move { 10 + socket 11 + .send(Request::Authenticate("foxfriends".to_owned()).into()) 12 + .await 13 + .unwrap(); 14 + }); 7 15 16 + #[cfg(feature = "web")] 8 17 use_future(move || async move { 9 18 while let Ok(msg) = socket.recv().await { 10 - dbg!(msg); 19 + gloo::console::log!("msg:", format!("{:?}", msg)); 11 20 } 12 21 }); 13 22