Skip to content

Commit 7f511ec

Browse files
committed
feat: implement typed metadata storage for rooms and clients (limited, but work for now)
1 parent 86d9000 commit 7f511ec

9 files changed

Lines changed: 284 additions & 39 deletions

File tree

examples/bot.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use wincode::{SchemaRead, SchemaWrite};
1212
const ADDR: &str = "127.0.0.1:7777";
1313
const ROOM: &str = "test-room";
1414
const BOT_COUNT: usize = 144;
15-
const TICK_RATE: u64 = 24;
15+
const TICK_RATE: u64 = 8;
1616
const MAX_SPEED: f32 = 400.0;
1717
const SCREEN: f32 = 600.0;
1818

examples/daemon.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ impl DaemonHandler {
3636
}
3737
}
3838

39+
#[allow(dead_code)]
40+
#[derive(Debug)]
41+
struct RoomMeta {
42+
foo: String,
43+
bar: String,
44+
}
45+
3946
impl ServerHandler for DaemonHandler {
4047
fn on_connect(&self, addr: SocketAddr) -> bool {
4148
let count = self
@@ -310,6 +317,13 @@ async fn main() -> Result<()> {
310317
}
311318

312319
server_handle.create_room("test-room")?;
320+
server_handle.set_room_meta(
321+
"test-room",
322+
RoomMeta {
323+
foo: "foo".to_string(),
324+
bar: "bar".to_string(),
325+
},
326+
);
313327

314328
let ctrl_listener = TcpListener::bind("0.0.0.0:8888").await?;
315329
info!("Control server listening on 0.0.0.0:8888");

src/client.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use tokio::io::{BufReader, BufWriter};
22
use tokio::net::TcpStream;
3+
use uuid::Uuid;
34

45
use crate::protocol;
56
use crate::types::{ClientWire, ServerEvent, ServerWire, SyncError};
@@ -9,6 +10,7 @@ pub struct Client {
910
reader: BufReader<tokio::net::tcp::OwnedReadHalf>,
1011
writer: BufWriter<tokio::net::tcp::OwnedWriteHalf>,
1112
max_payload: usize,
13+
client_id: Option<Uuid>,
1214
}
1315

1416
impl Client {
@@ -22,6 +24,14 @@ impl Client {
2224
ClientBuilder::new()
2325
}
2426

27+
/// Get the client's UUID assigned by the server.
28+
///
29+
/// Returns `None` before the first successful [`join`](Self::join) + [`recv`](Self::recv)
30+
/// (the server assigns the ID and sends it in the `Joined` event).
31+
pub fn client_id(&self) -> Option<Uuid> {
32+
self.client_id
33+
}
34+
2535
/// Join a room. If room does not exist, returns [`SyncError::RoomNotFound`]
2636
pub async fn join(&mut self, room_id: &str) -> Result<(), SyncError> {
2737
let msg = ClientWire::JoinRoom {
@@ -36,7 +46,7 @@ impl Client {
3646
}
3747

3848
/// Send a ping (keep-alive).
39-
#[inline(always)]
49+
#[inline]
4050
pub async fn ping(&mut self) -> Result<(), SyncError> {
4151
self.send(&ClientWire::Ping).await
4252
}
@@ -53,7 +63,7 @@ impl Client {
5363
/// Receive the next server event.
5464
/// Ping/pong keepalive is handled internally
5565
/// Returns `Ok(None)` on clean disconnect.
56-
#[inline]
66+
#[inline(always)]
5767
pub async fn recv(&mut self) -> Result<Option<ServerEvent>, SyncError> {
5868
loop {
5969
let payload = match protocol::read_frame_raw(&mut self.reader, self.max_payload).await {
@@ -78,14 +88,21 @@ impl Client {
7888
continue;
7989
}
8090

91+
// Capture client_id from Joined event
92+
if let ServerWire::Joined { client_id, .. } = &wire {
93+
self.client_id = Some(*client_id);
94+
}
95+
8196
return Ok(Some(Self::wire_to_event(wire)));
8297
}
8398
}
8499

100+
#[inline(always)]
85101
async fn send(&mut self, msg: &ClientWire) -> Result<(), SyncError> {
86102
protocol::write_frame(&mut self.writer, msg).await
87103
}
88104

105+
#[inline]
89106
fn wire_to_event(wire: ServerWire) -> ServerEvent {
90107
match wire {
91108
ServerWire::Joined { client_id, room_id } => ServerEvent::Joined { client_id, room_id },
@@ -141,6 +158,7 @@ impl ClientBuilder {
141158
reader: BufReader::with_capacity(2 * 1024 * 1024, read_half),
142159
writer: BufWriter::with_capacity(1024 * 1024, write_half),
143160
max_payload: self.max_payload,
161+
client_id: None,
144162
})
145163
}
146164
}

src/handler.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub trait ServerHandler: Send + Sync + 'static {
4747

4848
/// Called when a broadcast message is relayed.
4949
/// client_id is the sender, and the message has already been relayed to all peers.
50+
#[inline]
5051
fn on_broadcast(&self, _client_id: Uuid, _room_id: &str, _data: &[u8]) {}
5152

5253
/// Called when a frame is dropped because a client's write channel is full.
@@ -55,6 +56,7 @@ pub trait ServerHandler: Send + Sync + 'static {
5556
/// NOTE: For auto-kicking or pausing slow clients, you need to monitor channel length via [`ServerHandle::get_client_channel_len`](crate::ServerHandle::get_client_channel_len)
5657
/// or [`ServerHandle::get_room_channel_lens`](crate::ServerHandle::get_room_channel_lens) and implement your own logic in this hook.
5758
/// The library does not auto-kick or pause clients on backpressure, as some games may prefer to drop frames silently.
59+
#[inline]
5860
fn on_backpressure(&self, _client_id: Uuid, _room_id: &str) {}
5961

6062
/// Called when server is shutdown via ctrl-c (shutdown signals)

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod log;
66
mod protocol;
77
mod room;
88
mod server;
9+
mod storage;
910
mod types;
1011

1112
pub use client::{Client, ClientBuilder};

src/room.rs

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
use std::collections::HashMap;
1+
use std::any::Any;
22

33
use bytes::Bytes;
44
use dashmap::DashMap;
55
use tokio::sync::mpsc;
66
use uuid::Uuid;
77

8+
use crate::storage::Storage;
89
use crate::types::{Result, ServerWire, SyncError};
910

1011
/// A broadcast frame to be sent to a client. `Bytes` is reference-counted,
@@ -17,20 +18,20 @@ pub type BroadcastFrame = Bytes;
1718
/// connected clients identified by their UUIDs. Each client has a
1819
/// write channel for outgoing frames.
1920
///
20-
/// Rooms carry a string metadata map. The library does not interpret metadata —
21+
/// Rooms carry a typed metadata storage. The library does not interpret metadata —
2122
/// users store whatever they need (passwords, max players, game mode, etc.)
2223
/// and check it in their [`ServerHandler`](crate::ServerHandler) hooks.
2324
pub struct Room {
2425
clients: DashMap<Uuid, mpsc::Sender<BroadcastFrame>>,
25-
metadata: DashMap<String, String>,
26+
pub(crate) metadata: Storage,
2627
}
2728

2829
#[allow(dead_code)]
2930
impl Room {
3031
fn new() -> Self {
3132
Self {
32-
clients: DashMap::with_capacity(64),
33-
metadata: DashMap::with_capacity(128),
33+
clients: DashMap::with_capacity(128),
34+
metadata: Storage::new(),
3435
}
3536
}
3637

@@ -58,7 +59,7 @@ impl Room {
5859
///
5960
/// Returns `None` if the client is not in this room.
6061
/// Higher values indicate the client's writer task is falling behind.
61-
#[inline(always)]
62+
#[inline]
6263
pub fn channel_len(&self, id: &Uuid) -> Option<usize> {
6364
self.clients
6465
.get(id)
@@ -69,7 +70,7 @@ impl Room {
6970
///
7071
/// Returns `(uuid, channel_len)` pairs. Useful for monitoring
7172
/// backpressure and identifying slow clients.
72-
#[inline(always)]
73+
#[inline]
7374
pub fn all_channel_lens(&self) -> Vec<(Uuid, usize)> {
7475
self.clients
7576
.iter()
@@ -122,31 +123,34 @@ impl Room {
122123
self.clients.iter().map(|e| *e.key()).collect()
123124
}
124125

125-
/// Get a metadata value by key.
126+
/// Store a typed metadata value. Replaces any previous value.
126127
#[inline]
127-
pub fn get_meta(&self, key: &str) -> Option<String> {
128-
self.metadata.get(key).map(|v| v.clone())
128+
pub fn set_meta<T: Any + Send + Sync + 'static>(&self, value: T) {
129+
self.metadata.set(value);
129130
}
130131

131-
/// Set a metadata key-value pair.
132+
/// Read the stored metadata via a callback.
133+
///
134+
/// Returns `None` if no metadata is set or the stored type doesn't match `T`.
135+
/// The callback receives `&T` and can extract whatever it needs — no `Clone` required.
132136
#[inline]
133-
pub fn set_meta(&self, key: impl Into<String>, value: impl Into<String>) {
134-
self.metadata.insert(key.into(), value.into());
137+
pub fn get_meta<T: Any + Send + Sync + 'static, R>(
138+
&self,
139+
f: impl FnOnce(&T) -> R,
140+
) -> Option<R> {
141+
self.metadata.get(f)
135142
}
136143

137-
/// Remove a metadata key.
144+
/// Remove and return the stored metadata, downcasted to `T`.
138145
#[inline]
139-
pub fn remove_meta(&self, key: &str) -> bool {
140-
self.metadata.remove(key).is_some()
146+
pub fn take_meta<T: Any + Send + Sync + 'static>(&self) -> Option<T> {
147+
self.metadata.take()
141148
}
142149

143-
/// Get all metadata as a HashMap.
150+
/// Check if metadata is set on this room.
144151
#[inline]
145-
pub fn get_all_meta(&self) -> HashMap<String, String> {
146-
self.metadata
147-
.iter()
148-
.map(|e| (e.key().clone(), e.value().clone()))
149-
.collect()
152+
pub fn has_meta(&self) -> bool {
153+
self.metadata.is_set()
150154
}
151155
}
152156

src/server.rs

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::HashMap;
1+
use std::any::Any;
22
use std::net::SocketAddr;
33
use std::sync::Arc;
44
use std::time::Duration;
@@ -12,13 +12,15 @@ use uuid::Uuid;
1212
use crate::handler::{NoopHandler, ServerHandler};
1313
use crate::protocol;
1414
use crate::room::RoomManager;
15+
use crate::storage::Storage;
1516
use crate::types::{ClientWire, Result, ServerConfig, ServerWire, SyncError};
1617
use crate::{error, info, warn};
1718

1819
/// Tracks per-client state on the server side.
1920
struct ClientState {
2021
room_id: Option<String>,
2122
addr: SocketAddr,
23+
metadata: Storage,
2224
}
2325

2426
/// A broadcast relay game server.
@@ -74,7 +76,7 @@ impl ServerHandle {
7476
}
7577

7678
/// Create a room at runtime. Fails if the room already exists.
77-
#[inline(always)]
79+
#[inline]
7880
pub fn create_room(&self, id: &str) -> Result<()> {
7981
self.rooms.create(id)?;
8082
self.handler.on_room_create(id);
@@ -85,7 +87,7 @@ impl ServerHandle {
8587
///
8688
/// Soft delete: connected clients are not kicked, but their next
8789
/// broadcast or join will fail with a [`SyncError::RoomNotFound`].
88-
#[inline(always)]
90+
#[inline]
8991
pub fn delete_room(&self, id: &str) -> bool {
9092
let existed = self.rooms.delete(id);
9193
if existed {
@@ -135,24 +137,80 @@ impl ServerHandle {
135137
self.rooms.get(room_id).map(|r| r.all_channel_lens())
136138
}
137139

138-
/// Get a metadata value from a room.
139-
pub fn get_room_meta(&self, room_id: &str, key: &str) -> Option<String> {
140-
self.rooms.get(room_id)?.get_meta(key)
140+
/// Store typed metadata on a room. Replaces any previous metadata.
141+
/// Returns `false` if the room doesn't exist.
142+
pub fn set_room_meta<T: Any + Send + Sync + 'static>(&self, room_id: &str, value: T) -> bool {
143+
if let Some(room) = self.rooms.get(room_id) {
144+
room.metadata.set(value);
145+
true
146+
} else {
147+
false
148+
}
141149
}
142150

143-
/// Set a metadata value on a room. Returns `false` if the room doesn't exist.
144-
pub fn set_room_meta(&self, room_id: &str, key: &str, value: &str) -> bool {
145-
if let Some(room) = self.rooms.get(room_id) {
146-
room.set_meta(key, value);
151+
/// Read room metadata via a callback.
152+
///
153+
/// The callback receives `&T` if metadata is set and the type matches.
154+
/// Returns `None` if the room doesn't exist or has no metadata of type `T`.
155+
///
156+
/// This avoids needing `Clone` on your metadata type.
157+
pub fn with_room_meta<T: Any + Send + Sync + 'static, R>(
158+
&self,
159+
room_id: &str,
160+
f: impl FnOnce(&T) -> R,
161+
) -> Option<R> {
162+
self.rooms.get(room_id)?.metadata.get(f)
163+
}
164+
165+
/// Remove and return room metadata, downcasted to `T`.
166+
/// Returns `None` if the room doesn't exist or has no metadata of type `T`.
167+
pub fn take_room_meta<T: Any + Send + Sync + 'static>(&self, room_id: &str) -> Option<T> {
168+
self.rooms.get(room_id)?.metadata.take()
169+
}
170+
171+
/// Check if a room has metadata set.
172+
pub fn room_has_meta(&self, room_id: &str) -> bool {
173+
self.rooms.get(room_id).is_some_and(|r| r.metadata.is_set())
174+
}
175+
176+
/// Store typed metadata on a connected client. Replaces any previous metadata.
177+
/// Returns `false` if the client doesn't exist.
178+
pub fn set_client_meta<T: Any + Send + Sync + 'static>(
179+
&self,
180+
client_id: &Uuid,
181+
value: T,
182+
) -> bool {
183+
if let Some(state) = self.clients.get(client_id) {
184+
state.metadata.set(value);
147185
true
148186
} else {
149187
false
150188
}
151189
}
152190

153-
/// Get all metadata from a room as a HashMap. Returns `None` if room doesn't exist.
154-
pub fn get_room_meta_all(&self, room_id: &str) -> Option<HashMap<String, String>> {
155-
self.rooms.get(room_id).map(|r| r.get_all_meta())
191+
/// Read client metadata via a callback.
192+
///
193+
/// The callback receives `&T` if metadata is set and the type matches.
194+
/// Returns `None` if the client doesn't exist or has no metadata of type `T`.
195+
pub fn with_client_meta<T: Any + Send + Sync + 'static, R>(
196+
&self,
197+
client_id: &Uuid,
198+
f: impl FnOnce(&T) -> R,
199+
) -> Option<R> {
200+
self.clients.get(client_id)?.metadata.get(f)
201+
}
202+
203+
/// Remove and return client metadata, downcasted to `T`.
204+
/// Returns `None` if the client doesn't exist or has no metadata of type `T`.
205+
pub fn take_client_meta<T: Any + Send + Sync + 'static>(&self, client_id: &Uuid) -> Option<T> {
206+
self.clients.get(client_id)?.metadata.take()
207+
}
208+
209+
/// Check if a client has metadata set.
210+
pub fn client_has_meta(&self, client_id: &Uuid) -> bool {
211+
self.clients
212+
.get(client_id)
213+
.is_some_and(|c| c.metadata.is_set())
156214
}
157215

158216
/// Room a client is currently in. Returns `None` if not in a room or not connected.
@@ -358,6 +416,7 @@ impl Server {
358416
ClientState {
359417
room_id: None,
360418
addr,
419+
metadata: Storage::new(),
361420
},
362421
);
363422

@@ -540,7 +599,7 @@ impl Server {
540599
Ok(())
541600
}
542601

543-
#[inline]
602+
#[inline(always)]
544603
async fn cleanup_client(self: &Arc<Self>, client_id: Uuid, room_id: &str) {
545604
let notify = ServerWire::PlayerLeft { client_id };
546605
if let Some(room) = self.rooms.get(room_id) {

0 commit comments

Comments
 (0)