use std::collections::BTreeSet;
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use cratefield_core::{Member, RealtimeError, RoomContext, RoomHandler};
use worker::send::IntoSendFuture;
use worker::{
Response, Result as WorkerResult, State, WebSocket, WebSocketIncomingMessage, WebSocketPair,
};
pub struct RoomDriver {
handler: Arc<dyn RoomHandler>,
}
impl RoomDriver {
#[must_use]
pub fn new(handler: Arc<dyn RoomHandler>) -> Self {
Self { handler }
}
#[must_use]
pub fn route(&self) -> &'static str {
self.handler.route()
}
pub async fn upgrade(&self, state: &State, member: &Member) -> WorkerResult<Response> {
let pair = WebSocketPair::new()?;
state.accept_websocket_with_tags(&pair.server, &[member.id.as_str()]);
let ctx = DurableRoomContext::new(state);
if let Err(error) = self.handler.on_join(&ctx, member).await {
let _ = pair.server.close(Some(1011), Some(error.to_string()));
return Err(worker::Error::RustError(error.to_string()));
}
Response::from_websocket(pair.client)
}
pub async fn message(
&self,
state: &State,
ws: &WebSocket,
message: WebSocketIncomingMessage,
) -> WorkerResult<()> {
let Some(member) = member_of(state, ws) else {
return Ok(());
};
let bytes = match message {
WebSocketIncomingMessage::String(text) => text.into_bytes(),
WebSocketIncomingMessage::Binary(bytes) => bytes,
};
let ctx = DurableRoomContext::new(state);
self.handler
.on_message(&ctx, &member, &bytes)
.await
.map_err(|error| into_worker(&error))
}
pub async fn close(&self, state: &State, ws: &WebSocket) -> WorkerResult<()> {
let Some(member) = member_of(state, ws) else {
return Ok(());
};
let ctx = DurableRoomContext::new(state);
let outcome = self.handler.on_leave(&ctx, &member).await;
if state.get_websockets().len() <= 1 {
let _ = state.storage().delete_alarm().into_send().await;
}
outcome.map_err(|error| into_worker(&error))
}
pub async fn error(&self, state: &State, ws: &WebSocket) -> WorkerResult<()> {
self.close(state, ws).await
}
pub async fn alarm(&self, state: &State) -> WorkerResult<Response> {
let ctx = DurableRoomContext::new(state);
if let Err(error) = self.handler.on_alarm(&ctx).await {
tracing::warn!(%error, "room alarm handler failed");
}
Response::empty()
}
}
fn member_of(state: &State, ws: &WebSocket) -> Option<Member> {
state.get_tags(ws).into_iter().next().map(Member::new)
}
fn into_worker(error: &RealtimeError) -> worker::Error {
worker::Error::RustError(error.to_string())
}
struct DurableRoomContext<'a> {
state: &'a State,
room_id: String,
}
impl<'a> DurableRoomContext<'a> {
fn new(state: &'a State) -> Self {
let id = state.id();
let room_id = id.name().unwrap_or_else(|| id.to_string());
Self { state, room_id }
}
}
#[async_trait]
impl RoomContext for DurableRoomContext<'_> {
fn room_id(&self) -> &str {
&self.room_id
}
async fn members(&self) -> Vec<Member> {
let ids: BTreeSet<String> = self
.state
.get_websockets()
.iter()
.filter_map(|ws| member_of(self.state, ws).map(|member| member.id))
.collect();
ids.into_iter().map(Member::new).collect()
}
async fn broadcast(&self, message: &[u8]) -> Result<(), RealtimeError> {
for ws in self.state.get_websockets() {
let _ = ws.send_with_bytes(message);
}
Ok(())
}
async fn send(&self, member_id: &str, message: &[u8]) -> Result<(), RealtimeError> {
for ws in self.state.get_websockets_with_tag(member_id) {
let _ = ws.send_with_bytes(message);
}
Ok(())
}
async fn set_alarm(&self, delay: Duration) -> Result<(), RealtimeError> {
self.state
.storage()
.set_alarm(delay)
.into_send()
.await
.map_err(|error| RealtimeError::Operation(error.to_string()))
}
}