Trading card city builder game?

connect custom websocket from client side

eldridge.cam bf288b4b 77e541ee

Waiting for spindle ...
+508 -112
+214 -23
Cargo.lock
··· 149 149 "bytes", 150 150 "form_urlencoded", 151 151 "futures-util", 152 - "http", 152 + "http 1.4.0", 153 153 "http-body", 154 154 "http-body-util", 155 155 "hyper", ··· 183 183 dependencies = [ 184 184 "bytes", 185 185 "futures-core", 186 - "http", 186 + "http 1.4.0", 187 187 "http-body", 188 188 "http-body-util", 189 189 "mime", ··· 205 205 "bytes", 206 206 "futures-util", 207 207 "headers", 208 - "http", 208 + "http 1.4.0", 209 209 "http-body", 210 210 "http-body-util", 211 211 "mime", ··· 245 245 version = "1.8.3" 246 246 source = "registry+https://github.com/rust-lang/crates.io-index" 247 247 checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" 248 + 249 + [[package]] 250 + name = "bincode" 251 + version = "1.3.3" 252 + source = "registry+https://github.com/rust-lang/crates.io-index" 253 + checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 254 + dependencies = [ 255 + "serde", 256 + ] 248 257 249 258 [[package]] 250 259 name = "bitflags" ··· 335 344 name = "cartography" 336 345 version = "0.1.0" 337 346 dependencies = [ 347 + "anyhow", 338 348 "axum", 339 349 "derive_more 2.1.1", 340 350 "dioxus", 341 351 "futures", 342 352 "futures-rx", 353 + "gloo", 343 354 "kameo", 344 355 "rmp-serde", 345 356 "serde", ··· 947 958 checksum = "c0161af1d3cfc8ff31503ff1b7ee0068c97771fc38d0cc6566e23483142ddf4f" 948 959 dependencies = [ 949 960 "dioxus-cli-config", 950 - "http", 961 + "http 1.4.0", 951 962 "infer", 952 963 "jni", 953 964 "js-sys", ··· 1163 1174 "futures", 1164 1175 "futures-channel", 1165 1176 "futures-util", 1166 - "gloo-net", 1177 + "gloo-net 0.6.0", 1167 1178 "headers", 1168 - "http", 1179 + "http 1.4.0", 1169 1180 "http-body", 1170 1181 "http-body-util", 1171 1182 "inventory", ··· 1215 1226 "futures-channel", 1216 1227 "futures-util", 1217 1228 "generational-box", 1218 - "http", 1229 + "http 1.4.0", 1219 1230 "inventory", 1220 1231 "parking_lot", 1221 1232 "serde", ··· 1446 1457 "futures-channel", 1447 1458 "futures-util", 1448 1459 "generational-box", 1449 - "http", 1460 + "http 1.4.0", 1450 1461 "http-body-util", 1451 1462 "hyper", 1452 1463 "hyper-util", ··· 2272 2283 ] 2273 2284 2274 2285 [[package]] 2286 + name = "gloo" 2287 + version = "0.11.0" 2288 + source = "registry+https://github.com/rust-lang/crates.io-index" 2289 + checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" 2290 + dependencies = [ 2291 + "gloo-console", 2292 + "gloo-dialogs", 2293 + "gloo-events", 2294 + "gloo-file", 2295 + "gloo-history", 2296 + "gloo-net 0.5.0", 2297 + "gloo-render", 2298 + "gloo-storage", 2299 + "gloo-timers", 2300 + "gloo-utils", 2301 + "gloo-worker", 2302 + ] 2303 + 2304 + [[package]] 2305 + name = "gloo-console" 2306 + version = "0.3.0" 2307 + source = "registry+https://github.com/rust-lang/crates.io-index" 2308 + checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" 2309 + dependencies = [ 2310 + "gloo-utils", 2311 + "js-sys", 2312 + "serde", 2313 + "wasm-bindgen", 2314 + "web-sys", 2315 + ] 2316 + 2317 + [[package]] 2318 + name = "gloo-dialogs" 2319 + version = "0.2.0" 2320 + source = "registry+https://github.com/rust-lang/crates.io-index" 2321 + checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df" 2322 + dependencies = [ 2323 + "wasm-bindgen", 2324 + "web-sys", 2325 + ] 2326 + 2327 + [[package]] 2328 + name = "gloo-events" 2329 + version = "0.2.0" 2330 + source = "registry+https://github.com/rust-lang/crates.io-index" 2331 + checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" 2332 + dependencies = [ 2333 + "wasm-bindgen", 2334 + "web-sys", 2335 + ] 2336 + 2337 + [[package]] 2338 + name = "gloo-file" 2339 + version = "0.3.0" 2340 + source = "registry+https://github.com/rust-lang/crates.io-index" 2341 + checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" 2342 + dependencies = [ 2343 + "gloo-events", 2344 + "js-sys", 2345 + "wasm-bindgen", 2346 + "web-sys", 2347 + ] 2348 + 2349 + [[package]] 2350 + name = "gloo-history" 2351 + version = "0.2.2" 2352 + source = "registry+https://github.com/rust-lang/crates.io-index" 2353 + checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" 2354 + dependencies = [ 2355 + "getrandom 0.2.17", 2356 + "gloo-events", 2357 + "gloo-utils", 2358 + "serde", 2359 + "serde-wasm-bindgen", 2360 + "serde_urlencoded", 2361 + "thiserror 1.0.69", 2362 + "wasm-bindgen", 2363 + "web-sys", 2364 + ] 2365 + 2366 + [[package]] 2367 + name = "gloo-net" 2368 + version = "0.5.0" 2369 + source = "registry+https://github.com/rust-lang/crates.io-index" 2370 + checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" 2371 + dependencies = [ 2372 + "futures-channel", 2373 + "futures-core", 2374 + "futures-sink", 2375 + "gloo-utils", 2376 + "http 0.2.12", 2377 + "js-sys", 2378 + "pin-project", 2379 + "serde", 2380 + "serde_json", 2381 + "thiserror 1.0.69", 2382 + "wasm-bindgen", 2383 + "wasm-bindgen-futures", 2384 + "web-sys", 2385 + ] 2386 + 2387 + [[package]] 2275 2388 name = "gloo-net" 2276 2389 version = "0.6.0" 2277 2390 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2281 2394 "futures-core", 2282 2395 "futures-sink", 2283 2396 "gloo-utils", 2284 - "http", 2397 + "http 1.4.0", 2285 2398 "js-sys", 2286 2399 "pin-project", 2287 2400 "serde", ··· 2293 2406 ] 2294 2407 2295 2408 [[package]] 2409 + name = "gloo-render" 2410 + version = "0.2.0" 2411 + source = "registry+https://github.com/rust-lang/crates.io-index" 2412 + checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833" 2413 + dependencies = [ 2414 + "wasm-bindgen", 2415 + "web-sys", 2416 + ] 2417 + 2418 + [[package]] 2419 + name = "gloo-storage" 2420 + version = "0.3.0" 2421 + source = "registry+https://github.com/rust-lang/crates.io-index" 2422 + checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" 2423 + dependencies = [ 2424 + "gloo-utils", 2425 + "js-sys", 2426 + "serde", 2427 + "serde_json", 2428 + "thiserror 1.0.69", 2429 + "wasm-bindgen", 2430 + "web-sys", 2431 + ] 2432 + 2433 + [[package]] 2296 2434 name = "gloo-timers" 2297 2435 version = "0.3.0" 2298 2436 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2318 2456 ] 2319 2457 2320 2458 [[package]] 2459 + name = "gloo-worker" 2460 + version = "0.5.0" 2461 + source = "registry+https://github.com/rust-lang/crates.io-index" 2462 + checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" 2463 + dependencies = [ 2464 + "bincode", 2465 + "futures", 2466 + "gloo-utils", 2467 + "gloo-worker-macros", 2468 + "js-sys", 2469 + "pinned", 2470 + "serde", 2471 + "thiserror 1.0.69", 2472 + "wasm-bindgen", 2473 + "wasm-bindgen-futures", 2474 + "web-sys", 2475 + ] 2476 + 2477 + [[package]] 2478 + name = "gloo-worker-macros" 2479 + version = "0.1.0" 2480 + source = "registry+https://github.com/rust-lang/crates.io-index" 2481 + checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" 2482 + dependencies = [ 2483 + "proc-macro-crate 1.3.1", 2484 + "proc-macro2", 2485 + "quote", 2486 + "syn 2.0.114", 2487 + ] 2488 + 2489 + [[package]] 2321 2490 name = "gobject-sys" 2322 2491 version = "0.18.0" 2323 2492 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2391 2560 "fnv", 2392 2561 "futures-core", 2393 2562 "futures-sink", 2394 - "http", 2563 + "http 1.4.0", 2395 2564 "indexmap", 2396 2565 "slab", 2397 2566 "tokio", ··· 2456 2625 "base64", 2457 2626 "bytes", 2458 2627 "headers-core", 2459 - "http", 2628 + "http 1.4.0", 2460 2629 "httpdate", 2461 2630 "mime", 2462 2631 "sha1", ··· 2468 2637 source = "registry+https://github.com/rust-lang/crates.io-index" 2469 2638 checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" 2470 2639 dependencies = [ 2471 - "http", 2640 + "http 1.4.0", 2472 2641 ] 2473 2642 2474 2643 [[package]] ··· 2530 2699 2531 2700 [[package]] 2532 2701 name = "http" 2702 + version = "0.2.12" 2703 + source = "registry+https://github.com/rust-lang/crates.io-index" 2704 + checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 2705 + dependencies = [ 2706 + "bytes", 2707 + "fnv", 2708 + "itoa", 2709 + ] 2710 + 2711 + [[package]] 2712 + name = "http" 2533 2713 version = "1.4.0" 2534 2714 source = "registry+https://github.com/rust-lang/crates.io-index" 2535 2715 checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" ··· 2545 2725 checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 2546 2726 dependencies = [ 2547 2727 "bytes", 2548 - "http", 2728 + "http 1.4.0", 2549 2729 ] 2550 2730 2551 2731 [[package]] ··· 2556 2736 dependencies = [ 2557 2737 "bytes", 2558 2738 "futures-core", 2559 - "http", 2739 + "http 1.4.0", 2560 2740 "http-body", 2561 2741 "pin-project-lite", 2562 2742 ] ··· 2590 2770 "futures-channel", 2591 2771 "futures-core", 2592 2772 "h2", 2593 - "http", 2773 + "http 1.4.0", 2594 2774 "http-body", 2595 2775 "httparse", 2596 2776 "httpdate", ··· 2608 2788 source = "registry+https://github.com/rust-lang/crates.io-index" 2609 2789 checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 2610 2790 dependencies = [ 2611 - "http", 2791 + "http 1.4.0", 2612 2792 "hyper", 2613 2793 "hyper-util", 2614 2794 "rustls", ··· 2629 2809 "bytes", 2630 2810 "futures-channel", 2631 2811 "futures-util", 2632 - "http", 2812 + "http 1.4.0", 2633 2813 "http-body", 2634 2814 "hyper", 2635 2815 "ipnet", ··· 3319 3499 "bytes", 3320 3500 "encoding_rs", 3321 3501 "futures-util", 3322 - "http", 3502 + "http 1.4.0", 3323 3503 "httparse", 3324 3504 "memchr", 3325 3505 "mime", ··· 3859 4039 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 3860 4040 3861 4041 [[package]] 4042 + name = "pinned" 4043 + version = "0.1.0" 4044 + source = "registry+https://github.com/rust-lang/crates.io-index" 4045 + checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b" 4046 + dependencies = [ 4047 + "futures", 4048 + "rustversion", 4049 + "thiserror 1.0.69", 4050 + ] 4051 + 4052 + [[package]] 3862 4053 name = "pkcs1" 3863 4054 version = "0.7.5" 3864 4055 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4280 4471 "cookie_store", 4281 4472 "futures-core", 4282 4473 "futures-util", 4283 - "http", 4474 + "http 1.4.0", 4284 4475 "http-body", 4285 4476 "http-body-util", 4286 4477 "hyper", ··· 5578 5769 "bytes", 5579 5770 "futures-core", 5580 5771 "futures-util", 5581 - "http", 5772 + "http 1.4.0", 5582 5773 "http-body", 5583 5774 "http-body-util", 5584 5775 "http-range-header", ··· 5711 5902 dependencies = [ 5712 5903 "bytes", 5713 5904 "data-encoding", 5714 - "http", 5905 + "http 1.4.0", 5715 5906 "httparse", 5716 5907 "log", 5717 5908 "native-tls", ··· 5730 5921 dependencies = [ 5731 5922 "bytes", 5732 5923 "data-encoding", 5733 - "http", 5924 + "http 1.4.0", 5734 5925 "httparse", 5735 5926 "log", 5736 5927 "rand 0.9.2", ··· 6666 6857 "dunce", 6667 6858 "gtk", 6668 6859 "html5ever", 6669 - "http", 6860 + "http 1.4.0", 6670 6861 "javascriptcore-rs", 6671 6862 "jni", 6672 6863 "kuchikiki",
+5 -2
Cargo.toml
··· 7 7 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 8 9 9 [dependencies] 10 + anyhow = "1.0.101" 10 11 axum = { version = "0.8.8", features = ["ws"], optional = true } 11 - derive_more = "2.1.1" 12 + derive_more = { version = "2.1.1", features = ["error"] } 12 13 dioxus = { version = "0.7.1", features = ["router", "fullstack"] } 13 14 futures = "0.3.31" 14 15 futures-rx = "0.2.1" 16 + gloo = { version = "0.11.0", optional = true } 15 17 kameo = "0.19.2" 16 18 rmp-serde = "1.3.1" 17 19 serde = { version = "1.0.228", features = ["derive"] } ··· 24 26 25 27 [features] 26 28 default = ["web", "server"] 27 - web = ["dioxus/web", "tokio/rt", "uuid/js"] 29 + web = ["dioxus/web", "tokio/rt", "uuid/js", "dep:gloo"] 28 30 desktop = ["dioxus/desktop", "tokio/rt", "uuid/js"] 29 31 mobile = ["dioxus/mobile", "tokio/rt", "uuid/js"] 30 32 server = ["dioxus/server", "tokio/rt-multi-thread", "dep:sqlx", "dep:axum"] 33 + gloo = ["dep:gloo"]
+2 -2
Justfile
··· 19 19 shadow_database_name := if SHADOW_DATABASE_URL != "" { file_stem(SHADOW_DATABASE_URL) } else { "" } 20 20 21 21 [group: "run"] 22 - dev: up 23 - dx serve 22 + dev +args="--web": up 23 + dx serve {{args}} 24 24 25 25 [group: "dev"] 26 26 init:
-61
src/actor/player_socket.rs
··· 1 - use futures::Stream; 2 - use serde::{Deserialize, Serialize}; 3 - use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; 4 - use tokio_stream::wrappers::UnboundedReceiverStream; 5 - 6 - #[cfg(feature = "server")] 7 - use kameo::prelude::*; 8 - 9 - #[cfg(feature = "server")] 10 - #[derive(Actor, Default)] 11 - pub struct PlayerSocket { 12 - account_id: Option<String>, 13 - } 14 - 15 - #[cfg(feature = "server")] 16 - impl PlayerSocket { 17 - pub async fn push( 18 - actor: ActorRef<Self>, 19 - request: Request, 20 - ) -> Result<impl Stream<Item = Response>, SendError<PlayerSocketMessage>> { 21 - let (tx, rx) = unbounded_channel(); 22 - actor.tell(PlayerSocketMessage { tx, request }).await?; 23 - Ok(UnboundedReceiverStream::new(rx)) 24 - } 25 - } 26 - 27 - #[cfg(feature = "server")] 28 - pub struct PlayerSocketMessage { 29 - tx: UnboundedSender<Response>, 30 - request: Request, 31 - } 32 - 33 - #[derive(Serialize, Deserialize, Clone, Debug)] 34 - #[serde(tag = "type", content = "data")] 35 - pub enum Request { 36 - Authenticate(String), 37 - } 38 - 39 - #[derive(Serialize, Deserialize, Clone, Debug)] 40 - #[serde(tag = "type", content = "data")] 41 - pub enum Response { 42 - Authenticated(String), 43 - } 44 - 45 - #[cfg(feature = "server")] 46 - impl Message<PlayerSocketMessage> for PlayerSocket { 47 - type Reply = (); 48 - 49 - async fn handle( 50 - &mut self, 51 - PlayerSocketMessage { tx, request }: PlayerSocketMessage, 52 - _ctx: &mut Context<Self, Self::Reply>, 53 - ) -> Self::Reply { 54 - match request { 55 - Request::Authenticate(name) => { 56 - self.account_id = Some(name.clone()); 57 - let _ = tx.send(Response::Authenticated(name)); 58 - } 59 - } 60 - } 61 - }
+75
src/actor/player_socket/mod.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + #[derive(Serialize, Deserialize, Clone, Debug)] 4 + #[serde(tag = "type", content = "data")] 5 + pub enum Request { 6 + Authenticate(String), 7 + } 8 + 9 + #[derive(Serialize, Deserialize, Clone, Debug)] 10 + #[serde(tag = "type", content = "data")] 11 + pub enum Response { 12 + Authenticated(String), 13 + } 14 + 15 + #[cfg(feature = "server")] 16 + pub use server::PlayerSocket; 17 + 18 + #[cfg(feature = "server")] 19 + mod server { 20 + mod authenticate; 21 + 22 + use super::{Request, Response}; 23 + use futures::Stream; 24 + use kameo::prelude::*; 25 + use sqlx::PgPool; 26 + use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; 27 + use tokio_stream::wrappers::UnboundedReceiverStream; 28 + 29 + #[derive(Actor)] 30 + pub struct PlayerSocket { 31 + pub(super) db: PgPool, 32 + pub(super) account_id: Option<String>, 33 + } 34 + 35 + impl PlayerSocket { 36 + pub fn build(db: PgPool) -> Self { 37 + Self { 38 + db, 39 + account_id: None, 40 + } 41 + } 42 + 43 + pub async fn push( 44 + actor: ActorRef<Self>, 45 + request: Request, 46 + ) -> Result<impl Stream<Item = Response>, SendError<PlayerSocketMessage>> { 47 + let (tx, rx) = unbounded_channel(); 48 + actor.tell(PlayerSocketMessage { tx, request }).await?; 49 + Ok(UnboundedReceiverStream::new(rx)) 50 + } 51 + } 52 + 53 + pub struct PlayerSocketMessage { 54 + tx: UnboundedSender<Response>, 55 + request: Request, 56 + } 57 + 58 + impl Message<PlayerSocketMessage> for PlayerSocket { 59 + type Reply = (); 60 + 61 + async fn handle( 62 + &mut self, 63 + PlayerSocketMessage { tx, request }: PlayerSocketMessage, 64 + ctx: &mut Context<Self, Self::Reply>, 65 + ) -> Self::Reply { 66 + let result = match request { 67 + Request::Authenticate(account_id) => self.authenticate(tx, account_id).await, 68 + }; 69 + if let Err(error) = result { 70 + tracing::error!("error handling player socket message: {}", error); 71 + ctx.stop() 72 + } 73 + } 74 + } 75 + }
+34
src/actor/player_socket/server/authenticate.rs
··· 1 + use super::{PlayerSocket, Response}; 2 + use tokio::sync::mpsc::UnboundedSender; 3 + 4 + impl PlayerSocket { 5 + pub(super) async fn authenticate( 6 + &mut self, 7 + tx: UnboundedSender<Response>, 8 + account_id: String, 9 + ) -> anyhow::Result<()> { 10 + struct Account { 11 + id: String, 12 + } 13 + let mut conn = self.db.begin().await?; 14 + let account = sqlx::query_as!(Account, "SELECT id FROM accounts WHERE id = $1", account_id) 15 + .fetch_optional(&mut *conn) 16 + .await?; 17 + let account = match account { 18 + Some(account) => account, 19 + None => { 20 + sqlx::query_as!( 21 + Account, 22 + "INSERT INTO accounts (id) VALUES ($1) RETURNING id", 23 + account_id 24 + ) 25 + .fetch_one(&mut *conn) 26 + .await? 27 + } 28 + }; 29 + conn.commit().await?; 30 + self.account_id = Some(account.id.clone()); 31 + tx.send(Response::Authenticated(account.id))?; 32 + Ok(()) 33 + } 34 + }
-1
src/api.rs
··· 1 1 use dioxus::prelude::*; 2 2 3 - #[cfg(feature = "server")] 4 3 pub mod ws; 5 4 6 5 /// Echo the user input on the server.
+20 -19
src/api/ws.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + use uuid::Uuid; 3 + 4 + #[cfg(feature = "server")] 1 5 use axum::extract::ws::{CloseFrame, Message, WebSocket, WebSocketUpgrade}; 6 + #[cfg(feature = "server")] 7 + use axum::Extension; 8 + #[cfg(feature = "server")] 2 9 use futures::StreamExt; 10 + #[cfg(feature = "server")] 3 11 use kameo::prelude::*; 4 - use serde::{Deserialize, Serialize}; 12 + #[cfg(feature = "server")] 13 + use sqlx::PgPool; 14 + #[cfg(feature = "server")] 5 15 use tracing::Instrument; 6 - use uuid::Uuid; 7 - 16 + #[cfg(feature = "server")] 8 17 use crate::actor::player_socket::{PlayerSocket, Request, Response}; 9 18 10 19 #[derive(Serialize, Deserialize, Clone, Debug)] ··· 14 23 data: T, 15 24 } 16 25 17 - const JSON_PROTOCOL: &str = "v1-json.cartography.app"; 18 - const MESSAGEPACK_PROTOCOL: &str = "v1-messagepack.cartography.app"; 26 + pub const JSON_PROTOCOL: &str = "v1-json.cartography.app"; 27 + pub const MESSAGEPACK_PROTOCOL: &str = "v1-messagepack.cartography.app"; 19 28 20 - impl std::error::Error for ProtocolV1Error { 21 - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 22 - match self { 23 - Self::InvalidJson(error) => Some(error), 24 - Self::InvalidMessagepack(error) => Some(error), 25 - _ => None, 26 - } 27 - } 28 - } 29 - 30 - #[derive(Debug, derive_more::Display)] 29 + #[cfg(feature = "server")] 30 + #[derive(Debug, derive_more::Error, derive_more::Display)] 31 31 enum ProtocolV1Error { 32 32 #[display("invalid JSON payload: {_0}")] 33 33 InvalidJson(serde_json::Error), ··· 38 38 #[display("client disconnected")] 39 39 Disconnected, 40 40 #[display("client closed connection: {_0:?}")] 41 - Closed(CloseFrame), 41 + Closed(#[error(not(source))] CloseFrame), 42 42 } 43 43 44 - pub async fn v1(ws: WebSocketUpgrade) -> axum::response::Response { 44 + #[cfg(feature = "server")] 45 + pub async fn v1(ws: WebSocketUpgrade, db: Extension<PgPool>) -> axum::response::Response { 45 46 let ws = ws.protocols([JSON_PROTOCOL, MESSAGEPACK_PROTOCOL]); 46 47 let protocol = ws 47 48 .selected_protocol() ··· 54 55 let (ws_sender, ws_receiver) = socket.split(); 55 56 futures::pin_mut!(ws_sender); 56 57 57 - let actor = PlayerSocket::spawn_default(); 58 + let actor = PlayerSocket::spawn(PlayerSocket::build((*db).clone())); 58 59 let result = ws_receiver 59 60 .filter_map(|msg| async move { msg.ok() }) 60 61 .map(|msg| match msg {
+1
src/app.rs src/app/mod.rs
··· 1 1 use dioxus::prelude::*; 2 2 3 + mod hooks; 3 4 mod menu; 4 5 mod play; 5 6
+1
src/app/hooks/mod.rs
··· 1 + pub mod use_custom_websocket;
+142
src/app/hooks/use_custom_websocket.rs
··· 1 + use crate::actor::player_socket::{Request, Response}; 2 + use crate::api::{self, ws::ProtocolV1Message}; 3 + use dioxus::fullstack::{get_server_url, WebsocketState}; 4 + use dioxus::prelude::*; 5 + use futures::stream::{SplitSink, SplitStream}; 6 + use futures::{SinkExt, StreamExt}; 7 + #[cfg(feature = "web")] 8 + use gloo::net::websocket::futures::WebSocket; 9 + #[cfg(feature = "web")] 10 + use gloo::net::websocket::Message; 11 + use tokio::sync::Mutex; 12 + 13 + #[cfg(feature = "web")] 14 + pub struct CustomWebSocket { 15 + protocol: String, 16 + sender: Mutex<SplitSink<WebSocket, Message>>, 17 + receiver: Mutex<SplitStream<WebSocket>>, 18 + } 19 + 20 + #[derive(Copy, Clone)] 21 + pub struct UseCustomWebsocket { 22 + waker: UseWaker<()>, 23 + #[cfg(feature = "web")] 24 + connection: Resource<anyhow::Result<CustomWebSocket>>, 25 + status: Signal<WebsocketState>, 26 + status_read: ReadSignal<WebsocketState>, 27 + } 28 + 29 + pub fn use_custom_websocket(path: &'static str) -> UseCustomWebsocket { 30 + let mut waker = use_waker(); 31 + #[cfg(feature = "web")] 32 + let mut status = use_signal(|| WebsocketState::Connecting); 33 + #[cfg(not(feature = "web"))] 34 + let mut status = use_signal(|| WebsocketState::FailedToConnect); 35 + let status_read = use_hook(|| ReadSignal::new(status)); 36 + 37 + #[cfg(feature = "web")] 38 + let connection = use_resource(move || async move { 39 + let socket = match gloo::net::websocket::futures::WebSocket::open_with_protocols( 40 + &format!("{}/{}", get_server_url(), path), 41 + &[api::ws::JSON_PROTOCOL, api::ws::MESSAGEPACK_PROTOCOL], 42 + ) { 43 + Ok(socket) => { 44 + status.set(WebsocketState::Open); 45 + socket 46 + } 47 + Err(error) => { 48 + status.set(WebsocketState::FailedToConnect); 49 + return Err(error.into()); 50 + } 51 + }; 52 + 53 + let protocol = socket.protocol(); 54 + let (sender, receiver) = socket.split(); 55 + 56 + waker.wake(()); 57 + 58 + Ok(CustomWebSocket { 59 + protocol, 60 + sender: Mutex::new(sender), 61 + receiver: Mutex::new(receiver), 62 + }) 63 + }); 64 + 65 + UseCustomWebsocket { 66 + waker, 67 + #[cfg(feature = "web")] 68 + connection, 69 + status, 70 + status_read, 71 + } 72 + } 73 + 74 + impl UseCustomWebsocket { 75 + #[cfg(not(feature = "web"))] 76 + pub async fn connect(&self) -> WebsocketState { 77 + WebsocketState::FailedToConnect 78 + } 79 + 80 + #[cfg(feature = "web")] 81 + pub async fn connect(&self) -> WebsocketState { 82 + while !self.connection.finished() { 83 + _ = self.waker.wait().await; 84 + } 85 + self.status.cloned() 86 + } 87 + 88 + pub fn status(&self) -> ReadSignal<WebsocketState> { 89 + self.status_read 90 + } 91 + 92 + #[cfg(not(feature = "web"))] 93 + pub async fn send(&self, msg: ProtocolV1Message<Request>) -> anyhow::Result<()> { 94 + Err(anyhow::anyhow!("not implemented on this platform")) 95 + } 96 + 97 + #[cfg(not(feature = "web"))] 98 + pub async fn recv(&self) -> anyhow::Result<ProtocolV1Message<Response>> { 99 + Err(anyhow::anyhow!("not implemented on this platform")) 100 + } 101 + 102 + #[cfg(feature = "web")] 103 + pub async fn send(&self, msg: ProtocolV1Message<Request>) -> anyhow::Result<()> { 104 + self.connect().await; 105 + 106 + let connection = self.connection.as_ref(); 107 + let connection = connection 108 + .as_deref() 109 + .ok_or_else(|| anyhow::anyhow!("websocket closed away"))? 110 + .as_ref() 111 + .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 + 118 + connection.sender.lock().await.send(msg).await?; 119 + 120 + Ok(()) 121 + } 122 + 123 + #[cfg(feature = "web")] 124 + pub async fn recv(&self) -> anyhow::Result<ProtocolV1Message<Response>> { 125 + self.connect().await; 126 + 127 + let connection = self.connection.as_ref(); 128 + let connection = connection 129 + .as_deref() 130 + .ok_or_else(|| anyhow::anyhow!("websocket closed away"))? 131 + .as_ref() 132 + .map_err(|err| anyhow::anyhow!("{err}"))?; 133 + 134 + let mut recv = connection.receiver.lock().await; 135 + 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)?), 138 + Some(Err(e)) => Err(e.into()), 139 + None => anyhow::bail!("closed away"), 140 + } 141 + } 142 + }
+4 -3
src/app/menu.rs
··· 1 + use crate::app::Route; 1 2 use dioxus::prelude::*; 2 3 3 4 #[component] ··· 13 14 gap: "1rem", 14 15 15 16 h1 { font_size: "2rem", "This game?" } 16 - a { href: "/cards", "See the cards" } 17 - a { href: "/play", "Just Play" } 18 - a { href: "/demo", "Try Demo" } 17 + Link { to: Route::Menu {}, "See the cards" } 18 + Link { to: Route::Play {}, "Just Play" } 19 + Link { to: Route::Menu {}, "Try Demo" } 19 20 } 20 21 } 21 22 }
+9
src/app/play.rs
··· 1 + use crate::app::hooks::use_custom_websocket::use_custom_websocket; 1 2 use dioxus::prelude::*; 2 3 3 4 #[component] 4 5 pub fn Play() -> Element { 6 + let socket = use_custom_websocket("api/ws"); 7 + 8 + use_future(move || async move { 9 + while let Ok(msg) = socket.recv().await { 10 + dbg!(msg); 11 + } 12 + }); 13 + 5 14 rsx! { 6 15 main { display: "flex", 7 16 div {
+1 -1
src/main.rs
··· 6 6 7 7 fn main() { 8 8 #[cfg(not(feature = "server"))] 9 - dioxus::fullstack::set_server_url(""); 9 + dioxus::fullstack::set_server_url("http://localhost:8080"); 10 10 11 11 #[cfg(not(feature = "server"))] 12 12 dioxus::launch(App);