make reactivity system pluggable

This commit is contained in:
2026-05-04 23:07:32 -06:00
parent bc20cf825d
commit 801a182e7c
11 changed files with 80 additions and 49 deletions
Generated
-1
View File
@@ -4235,7 +4235,6 @@ dependencies = [
"dasp_ring_buffer",
"deep_filter",
"dioxus-asset-resolver",
"dioxus-signals",
"etcetera",
"futures",
"futures-channel",
-1
View File
@@ -66,7 +66,6 @@ etcetera = { version = "0.10.0", optional = true }
# Base Dependencies
# ================
dioxus-signals = "0.7.2"
manganis = "0.7.2"
once_cell = "1.19.0"
asynchronous-codec = { workspace = true }
+17 -9
View File
@@ -1,8 +1,8 @@
use dioxus_signals::{ReadableExt as _, Signal};
use mime_guess::Mime;
use mumble_web2_common::ProxyOverrides;
use ordermap::OrderSet;
use std::collections::{HashMap, HashSet};
use std::ops::{Deref, DerefMut};
use std::{fmt, sync::Arc};
pub type ChannelId = u32;
@@ -197,19 +197,27 @@ impl ServerState {
}
}
pub struct State {
pub status: Signal<ConnectionState>,
pub server: Signal<ServerState>,
pub audio: Signal<AudioSettings>,
pub trait Reactivity {
type Signal<T>;
fn new<T: 'static>(value: T) -> Self::Signal<T>;
fn read<T: 'static>(signal: &Self::Signal<T>) -> impl Deref<Target = T>;
fn write<T: 'static>(signal: &Self::Signal<T>) -> impl DerefMut<Target = T>;
}
impl fmt::Debug for State {
pub struct State<R: Reactivity> {
pub status: R::Signal<ConnectionState>,
pub server: R::Signal<ServerState>,
pub audio: R::Signal<AudioSettings>,
}
impl<R: Reactivity> fmt::Debug for State<R> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("State")
.field("status", &self.status.read())
.field("server", &self.server.read())
.field("status", &*R::read(&self.status))
.field("server", &*R::read(&self.server))
.finish()
}
}
pub type SharedState = Arc<State>;
pub type SharedState<R> = Arc<State<R>>;
+2 -1
View File
@@ -1,4 +1,5 @@
use crate::app::{Command, SharedState};
use crate::Reactivity;
use color_eyre::eyre::Error;
use futures_channel::mpsc::UnboundedReceiver;
use mumble_protocol::control::ClientControlCodec;
@@ -74,7 +75,7 @@ pub async fn network_connect(
username: String,
event_rx: &mut UnboundedReceiver<Command>,
overrides: &ProxyOverrides,
state: SharedState,
state: SharedState<impl Reactivity>,
) -> Result<(), Error> {
info!("connecting");
+2 -1
View File
@@ -1,4 +1,5 @@
use crate::app::{Command, SharedState};
use crate::Reactivity;
use color_eyre::eyre::Error;
use futures_channel::mpsc::UnboundedReceiver;
use mumble_web2_common::{ProxyOverrides, ServerStatus};
@@ -28,7 +29,7 @@ impl super::PlatformInterface for DesktopPlatform {
username: String,
event_rx: &mut UnboundedReceiver<Command>,
overrides: &ProxyOverrides,
state: SharedState,
state: SharedState<impl Reactivity>,
) -> Result<(), Error> {
super::connect::network_connect(address, username, event_rx, overrides, state).await
}
+2 -1
View File
@@ -1,4 +1,5 @@
use crate::app::{Command, SharedState};
use crate::Reactivity;
use color_eyre::eyre::Error;
use futures_channel::mpsc::UnboundedReceiver;
use mumble_web2_common::{ProxyOverrides, ServerStatus};
@@ -24,7 +25,7 @@ impl super::PlatformInterface for MobilePlatform {
username: String,
event_rx: &mut UnboundedReceiver<Command>,
overrides: &ProxyOverrides,
state: SharedState,
state: SharedState<impl Reactivity>,
) -> Result<(), Error> {
super::connect::network_connect(address, username, event_rx, overrides, state).await
}
+2 -1
View File
@@ -6,6 +6,7 @@
use crate::app::{Command, SharedState};
use crate::effects::AudioProcessor;
use crate::Reactivity;
use color_eyre::eyre::Error;
use futures_channel::mpsc::UnboundedReceiver;
use mumble_web2_common::{ProxyOverrides, ServerStatus};
@@ -83,7 +84,7 @@ pub trait PlatformInterface {
username: String,
event_rx: &mut UnboundedReceiver<Command>,
proxy_overrides: &ProxyOverrides,
state: SharedState,
state: SharedState<impl Reactivity>,
) -> impl Future<Output = Result<(), Error>>;
/// Get server status (user count, version, etc.) for the given address.
+2 -2
View File
@@ -1,6 +1,6 @@
/// Stub implementation of the platform interface, so that we can
/// `cargo check` without any --feature flags.
use crate::{app::SharedState, effects::AudioProcessor};
use crate::{app::SharedState, effects::AudioProcessor, Reactivity};
use color_eyre::eyre::Error;
use futures_channel::mpsc::UnboundedReceiver;
use mumble_web2_common::{ProxyOverrides, ServerStatus};
@@ -25,7 +25,7 @@ impl super::PlatformInterface for StubPlatform {
_username: String,
_event_rx: &mut UnboundedReceiver<crate::app::Command>,
_overrides: &ProxyOverrides,
_state: SharedState,
_state: SharedState<impl Reactivity>,
) -> impl Future<Output = Result<(), Error>> {
async { panic!("stubbed platform") }
}
+3 -2
View File
@@ -1,5 +1,6 @@
use crate::app::{Command, SharedState};
use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState};
use crate::Reactivity;
use color_eyre::eyre::{bail, eyre, Error};
use crossbeam::atomic::AtomicCell;
use futures_channel::mpsc::UnboundedReceiver;
@@ -112,7 +113,7 @@ impl super::PlatformInterface for WebPlatform {
username: String,
event_rx: &mut UnboundedReceiver<Command>,
overrides: &ProxyOverrides,
state: SharedState,
state: SharedState<impl Reactivity>,
) -> Result<(), Error> {
network_connect(address, username, event_rx, overrides, state).await
}
@@ -442,7 +443,7 @@ pub async fn network_connect(
username: String,
event_rx: &mut UnboundedReceiver<Command>,
overrides: &ProxyOverrides,
state: SharedState,
state: SharedState<impl Reactivity>,
) -> Result<(), Error> {
info!("connecting");
+26 -24
View File
@@ -3,11 +3,10 @@ use crate::AudioSettings;
use crate::Chat;
use crate::Command;
use crate::ConnectionState;
use crate::Reactivity;
use asynchronous_codec::FramedRead;
use asynchronous_codec::FramedWrite;
use color_eyre::eyre::{bail, Error};
use dioxus_signals::ReadableExt as _;
use dioxus_signals::WritableExt as _;
use futures::select;
use futures::AsyncRead;
use futures::AsyncWrite;
@@ -36,7 +35,10 @@ use crate::imp::{
Platform, PlatformInterface as _,
};
pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>, state: SharedState) {
pub async fn network_entrypoint<X: Reactivity>(
mut event_rx: UnboundedReceiver<Command>,
state: SharedState<X>,
) {
loop {
let Some(Command::Connect {
address,
@@ -47,16 +49,16 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>, state:
panic!("did not receive connect command")
};
*state.server.write_unchecked() = Default::default();
*state.status.write_unchecked() = ConnectionState::Connecting;
*X::write(&state.server) = Default::default();
*X::write(&state.status) = ConnectionState::Connecting;
if let Err(error) =
Platform::network_connect(address, username, &mut event_rx, &config, state.clone())
.await
{
error!("could not connect {:?}", error);
*state.status.write_unchecked() = ConnectionState::Failed(error.to_string());
*X::write(&state.status) = ConnectionState::Failed(error.to_string());
} else {
*state.status.write_unchecked() = ConnectionState::Disconnected;
*X::write(&state.status) = ConnectionState::Disconnected;
}
}
}
@@ -76,14 +78,14 @@ pub(crate) async fn sender_loop<W: AsyncWrite + Unpin + 'static>(
}
}
pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static>(
pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static, X: Reactivity>(
username: String,
state: SharedState,
state: SharedState<X>,
event_rx: &mut UnboundedReceiver<Command>,
mut outgoing: UnboundedSender<ControlPacket<Serverbound>>,
mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>,
) -> Result<(), Error> {
let audio_settings = state.audio.read().clone();
let audio_settings = X::read(&state.audio).clone();
// Get version packet
let version = match reader.next().await {
@@ -190,14 +192,14 @@ pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static>(
Ok(())
}
fn accept_command(
fn accept_command<X: Reactivity>(
command: Command,
send_chan: &mut UnboundedSender<ControlPacket<mumble_protocol::Serverbound>>,
audio: &mut AudioSystem,
state: &State,
state: &State<X>,
) -> Result<(), Error> {
use Command::*;
let Some(session) = state.server.read().session else {
let Some(session) = X::read(&state.server).session else {
bail!("no session id")
};
@@ -220,7 +222,7 @@ fn accept_command(
};
{
let mut server = state.server.write_unchecked();
let mut server = X::write(&state.server);
let Some(me) = server.session else {
bail!("not signed in with a session id")
};
@@ -261,7 +263,7 @@ fn accept_command(
};
{
let mut server = state.server.write_unchecked();
let mut server = X::write(&state.server);
let Some(me) = server.session else {
bail!("not signed in with a session id")
};
@@ -304,11 +306,11 @@ fn accept_command(
Ok(())
}
fn accept_packet(
fn accept_packet<X: Reactivity>(
msg: ControlPacket<mumble_protocol::Clientbound>,
audio_context: &mut AudioSystem,
player_map: &mut HashMap<u32, AudioPlayer>,
state: &State,
state: &State<X>,
) -> Result<(), Error> {
match msg {
ControlPacket::UDPTunnel(u) => {
@@ -345,15 +347,15 @@ fn accept_packet(
}
}
ControlPacket::ChannelState(u) => {
let mut server = state.server.write_unchecked();
let mut server = X::write(&state.server);
server.channels_state.update_from_channel_state(&u);
}
ControlPacket::ChannelRemove(u) => {
let mut server = state.server.write_unchecked();
let mut server = X::write(&state.server);
server.channels_state.update_from_channel_remove(&u);
}
ControlPacket::UserState(u) => {
let mut server = state.server.write_unchecked();
let mut server = X::write(&state.server);
let server = &mut *server;
let id = u.get_session();
@@ -397,7 +399,7 @@ fn accept_packet(
}
}
ControlPacket::UserRemove(u) => {
let mut server = state.server.write_unchecked();
let mut server = X::write(&state.server);
let id = u.get_session();
if let Some(state) = server.users.remove(&id) {
if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) {
@@ -406,7 +408,7 @@ fn accept_packet(
}
}
ControlPacket::TextMessage(u) => {
let mut server = state.server.write_unchecked();
let mut server = X::write(&state.server);
if u.has_message() {
let text = u.get_message().to_string();
server.chat.push(Chat {
@@ -421,8 +423,8 @@ fn accept_packet(
}
}
ControlPacket::ServerSync(u) => {
*state.status.write_unchecked() = ConnectionState::Connected;
let mut server = state.server.write_unchecked();
*X::write(&state.status) = ConnectionState::Connected;
let mut server = X::write(&state.server);
if u.has_welcome_text() {
let text = u.get_welcome_text().to_string();
server.chat.push(Chat {
+24 -6
View File
@@ -3,15 +3,34 @@
use dioxus::prelude::*;
use mumble_web2_client::{
network_entrypoint, reqwest, AudioSettings, ChannelId, Command, ConfigSystem,
ConfigSystemInterface as _, ConnectionState, Platform, PlatformInterface as _, ServerState,
SharedState, State, UserId, UserState, VERSION,
ConfigSystemInterface as _, ConnectionState, Platform, PlatformInterface as _, UserId,
UserState, VERSION,
};
use mumble_web2_common::{ProxyOverrides, ServerStatus};
use std::collections::{HashMap, HashSet};
use std::{fmt, sync::Arc};
use Command::*;
use ConnectionState::*;
pub struct DioxusReactivity;
impl mumble_web2_client::Reactivity for DioxusReactivity {
type Signal<T> = Signal<T>;
fn new<T: 'static>(value: T) -> Signal<T> {
Signal::new(value)
}
fn read<T: 'static>(signal: &Signal<T>) -> impl std::ops::Deref<Target = T> {
signal.read_unchecked()
}
fn write<T: 'static>(signal: &Signal<T>) -> impl std::ops::DerefMut<Target = T> {
signal.write_unchecked()
}
}
pub type SharedState = mumble_web2_client::SharedState<DioxusReactivity>;
pub type State = mumble_web2_client::State<DioxusReactivity>;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum UserIcon {
Normal,
@@ -519,8 +538,7 @@ pub fn LoginView(overrides: Resource<ProxyOverrides>) -> Element {
async move {
let client = reqwest::Client::new();
loop {
*last_status.write_unchecked() =
Some(Platform::get_status(&client, &addr).await);
*last_status.write_unchecked() = Some(Platform::get_status(&client, &addr).await);
Platform::sleep(std::time::Duration::from_secs_f32(1.0)).await;
}
}