use {
crate::connection::Disconnect,
alloc::{string::String, vec::Vec},
bevy_app::prelude::*,
bevy_ecs::prelude::*,
bevy_platform::time::Instant,
bevy_reflect::prelude::*,
log::debug,
};
pub(crate) struct ServerPlugin;
impl Plugin for ServerPlugin {
fn build(&self, app: &mut App) {
app.register_type::<ServerEndpoint>()
.register_type::<Server>()
.add_observer(on_opening)
.add_observer(on_opened)
.add_observer(on_close)
.add_observer(on_closed);
}
}
#[derive(Debug, Clone, Copy, Default, Component, Reflect)]
pub struct ServerEndpoint;
#[derive(Debug, Component, Reflect)]
#[reflect(from_reflect = false, Component)]
#[require(ServerEndpoint)]
pub struct Server {
opened_at: Instant,
}
impl Server {
#[must_use]
pub const fn new(opened_at: Instant) -> Self {
Self { opened_at }
}
}
#[derive(Debug, Clone, PartialEq, Eq, EntityEvent)]
pub struct Close {
pub entity: Entity,
pub reason: String,
}
impl Close {
#[must_use]
pub fn new(entity: Entity, reason: impl Into<String>) -> Self {
Self {
entity,
reason: reason.into(),
}
}
}
#[derive(Debug, EntityEvent)]
pub struct Closed {
pub entity: Entity,
pub reason: CloseReason,
}
#[derive(Debug)]
pub enum CloseReason {
ByUser(String),
ByError(anyhow::Error),
}
impl CloseReason {
#[must_use]
pub fn by_user(reason: impl Into<String>) -> Self {
Self::ByUser(reason.into())
}
#[must_use]
pub fn by_error(reason: impl Into<anyhow::Error>) -> Self {
Self::ByError(reason.into())
}
#[must_use]
pub fn map_err(self, f: impl FnOnce(anyhow::Error) -> anyhow::Error) -> Self {
match self {
Self::ByUser(reason) => Self::ByUser(reason),
Self::ByError(err) => Self::ByError(f(err)),
}
}
}
impl<E: Into<anyhow::Error>> From<E> for CloseReason {
fn from(value: E) -> Self {
Self::by_error(value)
}
}
fn on_opening(trigger: On<Add, ServerEndpoint>) {
let target = trigger.event_target();
debug!("{target} opening");
}
fn on_opened(trigger: On<Add, Server>) {
let target = trigger.event_target();
debug!("{target} opened");
}
fn on_close(trigger: On<Close>, mut commands: Commands) {
let entity = trigger.event_target();
commands.trigger(Closed {
entity,
reason: CloseReason::by_user(&trigger.reason),
});
}
fn on_closed(trigger: On<Closed>, children: Query<&Children>, mut commands: Commands) {
let target = trigger.event_target();
let children = children
.get(target)
.map(|children| children.iter().collect::<Vec<_>>())
.unwrap_or_default();
match &trigger.reason {
CloseReason::ByUser(reason) => {
debug!("{target} closed by user: {reason}");
for child in children {
commands.trigger(Disconnect::new(child, reason));
}
}
CloseReason::ByError(err) => {
debug!("{target} closed due to error: {err:#}");
}
}
commands.entity(target).try_despawn();
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::{
AeronetIoPlugin,
connection::{DisconnectReason, Disconnected},
},
};
#[test]
fn disconnect_clients_on_close() {
const REASON: &str = "disconnect reason";
#[derive(Resource)]
struct HasClosed(bool);
#[derive(Resource)]
struct HasDisconnected(bool);
let mut app = App::new();
app.add_plugins(AeronetIoPlugin)
.insert_resource(HasClosed(false))
.insert_resource(HasDisconnected(false));
let client = app.world_mut().spawn_empty().id();
app.world_mut().entity_mut(client).observe(
|trigger: On<Disconnected>, mut has_disconnected: ResMut<HasDisconnected>| {
assert!(matches!(
&trigger.reason,
DisconnectReason::ByUser(reason) if reason == REASON
));
has_disconnected.0 = true;
},
);
let server = app.world_mut().spawn_empty().id();
app.world_mut()
.entity_mut(server)
.add_child(client)
.observe(|trigger: On<Closed>, mut has_closed: ResMut<HasClosed>| {
assert!(matches!(
&trigger.reason,
CloseReason::ByUser(reason) if reason == REASON
));
has_closed.0 = true;
});
app.world_mut().trigger(Close::new(server, REASON));
app.update();
assert!(app.world().get_entity(client).is_err());
assert!(app.world().resource::<HasDisconnected>().0);
assert!(app.world().get_entity(server).is_err());
assert!(app.world().resource::<HasClosed>().0);
}
}