Skip to content

Commit 8eb0d90

Browse files
authored
Merge pull request #13 from CurtisTechSolutions/feat/live-match-stream
feat: stream committed match states over WebSockets
2 parents d64d463 + 0c2f7ec commit 8eb0d90

12 files changed

Lines changed: 760 additions & 24 deletions

File tree

‎Cargo.lock‎

Lines changed: 125 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,14 @@ sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio
4242
tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync", "net", "signal", "time"] }
4343
tempfile = "3"
4444

45-
axum = { version = "0.8.9", default-features = false, features = ["http1", "json", "query", "tokio"] }
45+
axum = { version = "0.8.9", default-features = false, features = ["http1", "json", "query", "tokio", "ws"] }
4646
tower = { version = "0.5", features = ["util"] }
4747

4848
uuid = { version = "1", features = ["v4"] }
4949

50+
tokio-tungstenite = { version = "0.28", default-features = false, features = ["connect", "handshake"] }
51+
futures-util = { version = "0.3", features = ["sink"] }
52+
5053
[workspace.lints.rust]
5154
unsafe_code = "forbid"
5255
missing_docs = "warn"

‎crates/adapters/gfa-http/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ gfa-service.workspace = true
1111
axum.workspace = true
1212
serde.workspace = true
1313
serde_json.workspace = true
14+
tokio.workspace = true
1415

1516
[dev-dependencies]
16-
tokio.workspace = true
1717
tower.workspace = true
1818

1919
[lints]

‎crates/adapters/gfa-http/src/lib.rs‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
//! It does not issue seat credentials and must not be mounted behind a public proxy.
66
mod access;
77
mod error;
8+
mod stream;
9+
10+
pub use stream::LiveUpdates;
811

912
use axum::{
1013
extract::{
@@ -33,14 +36,34 @@ pub const MAX_BODY_BYTES: usize = 64 * 1024;
3336
/// Serve using `into_make_service_with_connect_info::<SocketAddr>()`; requests
3437
/// without peer information are refused. The address must already be loopback.
3538
pub fn local_router(service: Arc<GameService>, address: SocketAddr) -> Result<Router, ApiError> {
39+
build_router(service, address, None)
40+
}
41+
42+
/// Construct local REST and WebSocket routes using the service's commit observer.
43+
///
44+
/// The host must install this same LiveUpdates instance with GameService::with_observer.
45+
/// Periodic reconciliation also catches commits from other hosts sharing the store.
46+
pub fn local_router_with_updates(
47+
service: Arc<GameService>,
48+
address: SocketAddr,
49+
updates: Arc<LiveUpdates>,
50+
) -> Result<Router, ApiError> {
51+
build_router(service, address, Some(updates))
52+
}
53+
54+
fn build_router(
55+
service: Arc<GameService>,
56+
address: SocketAddr,
57+
updates: Option<Arc<LiveUpdates>>,
58+
) -> Result<Router, ApiError> {
3659
if !address.ip().is_loopback() || address.port() == 0 {
3760
return Err(ApiError::new(
3861
"INVALID_CONFIG",
3962
"A bound loopback listener is required",
4063
"Bind 127.0.0.1 or ::1, then pass listener.local_addr().",
4164
));
4265
}
43-
Ok(Router::new()
66+
let mut router = Router::new()
4467
.route("/healthz", get(health))
4568
.route("/v1/games", get(games))
4669
.route("/v1/games/{game_id}", get(game))
@@ -50,7 +73,14 @@ pub fn local_router(service: Arc<GameService>, address: SocketAddr) -> Result<Ro
5073
.route("/v1/matches/{id}/actions", post(make_move))
5174
.route("/v1/matches/{id}/replay", get(replay))
5275
.fallback(not_found)
53-
.method_not_allowed_fallback(method_not_allowed)
76+
.method_not_allowed_fallback(method_not_allowed);
77+
if let Some(updates) = updates {
78+
router = router.route(
79+
"/v1/matches/{id}/stream",
80+
get(stream::upgrade).layer(axum::Extension(updates)),
81+
);
82+
}
83+
Ok(router
5484
.layer(DefaultBodyLimit::max(MAX_BODY_BYTES))
5585
.layer(middleware::from_fn_with_state(
5686
access::LocalAccess::new(address),

0 commit comments

Comments
 (0)