From 2a6cab3fd40a84a02648e9db86d3567237e67ad2 Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 11 Jul 2026 22:05:51 +0200 Subject: [PATCH 1/2] refactor(core): move AckStream, DropStream, ResponseHandlers and ChanStream into core behind remote-adapter feature Moves the duplicated stream constructs from all three remote adapters (redis, postgres, mongodb) into socketioxide-core behind the existing remote-adapter feature flag. Removes 737 lines of duplicated code. - DropStream: generic cleanup-on-drop stream wrapper - AckStream: generic ack stream merging local + remote - ResponseHandlers: generic type alias for HashMap> - ChanStream: generic mpsc::Receiver stream wrapper Each adapter now provides a decode function passed as a fn pointer to AckStream, avoiding orphan rule issues with trait impls on foreign types like Vec. Closes #756 --- Cargo.lock | 4 + crates/socketioxide-core/Cargo.toml | 12 +- crates/socketioxide-core/src/adapter/mod.rs | 2 + .../socketioxide-core/src/adapter/stream.rs | 272 ++++++++++++++++++ crates/socketioxide-mongodb/src/lib.rs | 13 +- crates/socketioxide-mongodb/src/stream.rs | 258 ++--------------- crates/socketioxide-postgres/src/lib.rs | 14 +- crates/socketioxide-postgres/src/stream.rs | 271 ++--------------- crates/socketioxide-redis/src/lib.rs | 18 +- crates/socketioxide-redis/src/stream.rs | 238 +-------------- 10 files changed, 365 insertions(+), 737 deletions(-) create mode 100644 crates/socketioxide-core/src/adapter/stream.rs diff --git a/Cargo.lock b/Cargo.lock index e3e1ec2e..14008782 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2617,11 +2617,15 @@ dependencies = [ "bytes", "engineioxide-core", "futures-core", + "futures-util", + "pin-project-lite", "rmp-serde", "serde", "serde_json", "smallvec", "thiserror", + "tokio", + "tracing", ] [[package]] diff --git a/crates/socketioxide-core/Cargo.toml b/crates/socketioxide-core/Cargo.toml index 09bf24cf..e85dd37f 100644 --- a/crates/socketioxide-core/Cargo.toml +++ b/crates/socketioxide-core/Cargo.toml @@ -13,7 +13,12 @@ license.workspace = true readme.workspace = true [features] -remote-adapter = [] +remote-adapter = [ + "dep:pin-project-lite", + "dep:futures-util", + "dep:tokio", + "dep:tracing", +] [dependencies] bytes.workspace = true @@ -21,7 +26,11 @@ engineioxide-core = { version = "0.2", path = "../engineioxide-core" } serde.workspace = true thiserror.workspace = true futures-core.workspace = true +futures-util = { workspace = true, optional = true } +pin-project-lite = { workspace = true, optional = true } smallvec = { workspace = true, features = ["serde"] } +tokio = { workspace = true, features = ["sync", "time"], optional = true } +tracing = { workspace = true, optional = true } [target."cfg(fuzzing)".dependencies] arbitrary = { version = "1.4.2", features = ["derive"] } @@ -29,6 +38,7 @@ arbitrary = { version = "1.4.2", features = ["derive"] } [dev-dependencies] serde_json.workspace = true rmp-serde.workspace = true +tokio = { workspace = true, features = ["macros", "rt"] } [lints] workspace = true diff --git a/crates/socketioxide-core/src/adapter/mod.rs b/crates/socketioxide-core/src/adapter/mod.rs index 9b9b5e87..55b3014e 100644 --- a/crates/socketioxide-core/src/adapter/mod.rs +++ b/crates/socketioxide-core/src/adapter/mod.rs @@ -27,6 +27,8 @@ use errors::{AdapterError, BroadcastError, SocketError}; pub mod errors; #[cfg(feature = "remote-adapter")] pub mod remote_packet; +#[cfg(feature = "remote-adapter")] +pub mod stream; /// A room identifier pub type Room = Cow<'static, str>; diff --git a/crates/socketioxide-core/src/adapter/stream.rs b/crates/socketioxide-core/src/adapter/stream.rs new file mode 100644 index 00000000..ef7e6bd7 --- /dev/null +++ b/crates/socketioxide-core/src/adapter/stream.rs @@ -0,0 +1,272 @@ +use std::{ + collections::HashMap, + fmt, + pin::Pin, + sync::{Arc, Mutex}, + task::{self, Poll}, + time::Duration, +}; + +use futures_core::{FusedStream, Stream}; +use futures_util::{StreamExt, stream::TakeUntil}; +use pin_project_lite::pin_project; +use serde::de::DeserializeOwned; +use tokio::{sync::mpsc, time}; + +use crate::Sid; +use crate::adapter::AckStreamItem; +use crate::adapter::remote_packet::{Response, ResponseType}; + +/// A map of request IDs to response senders, used to route responses to the correct awaiting stream. +pub type ResponseHandlers = HashMap>; + +pin_project! { + /// A [`Stream`] that wraps an [`mpsc::Receiver`]. + pub struct ChanStream { + #[pin] + rx: mpsc::Receiver, + } +} +impl ChanStream { + /// Create a new `ChanStream` from an [`mpsc::Receiver`]. + pub fn new(rx: mpsc::Receiver) -> Self { + Self { rx } + } +} +impl Stream for ChanStream { + type Item = T; + + fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + self.project().rx.poll_recv(cx) + } +} + +pin_project! { + /// A stream that unsubscribes from its source channel when dropped. + pub struct DropStream { + #[pin] + stream: S, + req_id: Sid, + handlers: Arc>>, + } + impl PinnedDrop for DropStream { + fn drop(this: Pin<&mut Self>) { + let stream = this.project(); + let chan = stream.req_id; + tracing::debug!(?chan, "dropping stream"); + stream.handlers.lock().unwrap().remove(chan); + } + } +} +impl DropStream { + /// Create a new `DropStream` that will remove the handler entry on drop. + pub fn new(stream: S, handlers: Arc>>, req_id: Sid) -> Self { + Self { + stream, + handlers, + req_id, + } + } +} +impl Stream for DropStream { + type Item = S::Item; + + fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + self.project().stream.poll_next(cx) + } +} +impl FusedStream for DropStream { + fn is_terminated(&self) -> bool { + self.stream.is_terminated() + } +} + +pin_project! { + /// A stream of acknowledgement messages received from the local and remote servers. + /// It merges the local ack stream with the remote ack stream from all the servers. + // The server_cnt is the number of servers that are expected to send a AckCount message. + // It is decremented each time a AckCount message is received. + // + // The ack_cnt is the number of acks that are expected to be received. It is the sum of all the the ack counts. + // And it is decremented each time an ack is received. + // + // Therefore an exhausted stream correspond to `ack_cnt == 0` and `server_cnt == 0`. + pub struct AckStream { + #[pin] + local: S, + #[pin] + remote: DropStream, T>, + decode: fn(R::Item) -> Result, Box>, + ack_cnt: u32, + total_ack_cnt: usize, + serv_cnt: u16, + } +} + +impl AckStream { + /// Create a new `AckStream` wrapping the given local and remote streams. + pub fn new( + local: S, + remote: R, + decode: fn(R::Item) -> Result, Box>, + timeout: Duration, + serv_cnt: u16, + req_id: Sid, + handlers: Arc>>, + ) -> Self { + let remote = remote.take_until(time::sleep(timeout)); + let remote = DropStream::new(remote, handlers, req_id); + Self { + local, + remote, + decode, + ack_cnt: 0, + total_ack_cnt: 0, + serv_cnt, + } + } + + /// Create a new `AckStream` for local-only use, with an empty remote stream. + pub fn new_empty_remote(local: S, empty_remote: R, decode: fn(R::Item) -> Result, Box>) -> Self { + let handlers = Arc::new(Mutex::new(ResponseHandlers::::new())); + let remote = empty_remote.take_until(time::sleep(Duration::ZERO)); + let remote = DropStream::new(remote, handlers, Sid::ZERO); + Self { + local, + remote, + decode, + ack_cnt: 0, + total_ack_cnt: 0, + serv_cnt: 0, + } + } +} + +impl AckStream +where + Err: DeserializeOwned + fmt::Debug, + S: Stream> + FusedStream, +{ + /// Poll the remote stream. First the count of acks is received, then the acks are received. + /// We expect `serv_cnt` of `BroadcastAckCount` messages to be received, then we expect + /// `ack_cnt` of `BroadcastAck` messages. + fn poll_remote( + self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + ) -> Poll>> { + if FusedStream::is_terminated(&self) { + return Poll::Ready(None); + } + let mut projection = self.project(); + loop { + match projection.remote.as_mut().poll_next(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(None) => return Poll::Ready(None), + Poll::Ready(Some(item)) => match (projection.decode)(item) { + Ok(Response { + node_id: uid, + r#type: ResponseType::BroadcastAckCount(count), + }) if *projection.serv_cnt > 0 => { + tracing::trace!(?uid, "receiving broadcast ack count {count}"); + *projection.ack_cnt += count; + *projection.total_ack_cnt += count as usize; + *projection.serv_cnt -= 1; + } + Ok(Response { + node_id: uid, + r#type: ResponseType::BroadcastAck((sid, res)), + }) if *projection.ack_cnt > 0 => { + tracing::trace!(?uid, "receiving broadcast ack {sid} {:?}", res); + *projection.ack_cnt -= 1; + return Poll::Ready(Some((sid, res))); + } + Ok(Response { node_id: uid, .. }) => { + tracing::warn!(?uid, "unexpected response type"); + } + Err(e) => { + tracing::warn!("error decoding ack response: {e}"); + } + }, + } + } + } +} + +impl Stream for AckStream +where + Err: DeserializeOwned + fmt::Debug, + S: Stream> + FusedStream, +{ + type Item = AckStreamItem; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + match self.as_mut().project().local.poll_next(cx) { + Poll::Pending => match self.poll_remote(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(Some(item)) => Poll::Ready(Some(item)), + Poll::Ready(None) => Poll::Pending, + }, + Poll::Ready(Some(item)) => Poll::Ready(Some(item)), + Poll::Ready(None) => self.poll_remote(cx), + } + } + + fn size_hint(&self) -> (usize, Option) { + let (lower, upper) = self.local.size_hint(); + (lower, upper.map(|upper| upper + self.total_ack_cnt)) + } +} + +impl FusedStream for AckStream +where + Err: DeserializeOwned + fmt::Debug, + S: Stream> + FusedStream, +{ + /// The stream is terminated if: + /// * The local stream is terminated. + /// * All the servers have sent the expected ack count. + /// * We have received all the expected acks. + fn is_terminated(&self) -> bool { + let remote_term = (self.ack_cnt == 0 && self.serv_cnt == 0) || self.remote.is_terminated(); + self.local.is_terminated() && remote_term + } +} + +impl fmt::Debug for AckStream { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AckStream") + .field("ack_cnt", &self.ack_cnt) + .field("total_ack_cnt", &self.total_ack_cnt) + .field("serv_cnt", &self.serv_cnt) + .finish() + } +} + +#[cfg(test)] +mod tests { + use futures_core::FusedStream; + use futures_util::StreamExt; + use crate::{Sid, Value}; + + use super::AckStream; + + #[tokio::test] + async fn local_ack_stream_should_have_a_closed_remote() { + let sid = Sid::new(); + let local = futures_util::stream::once(async move { + (sid, Ok::<_, ()>(Value::Str("local".into(), None))) + }); + let empty_remote = futures_util::stream::empty::<()>(); + let stream: AckStream<_, _, (), ()> = AckStream::new_empty_remote(local, empty_remote, |_| unreachable!()); + futures_util::pin_mut!(stream); + assert_eq!(stream.ack_cnt, 0); + assert_eq!(stream.total_ack_cnt, 0); + assert_eq!(stream.serv_cnt, 0); + let data = stream.next().await; + assert!( + matches!(data, Some((id, Ok(Value::Str(msg, None)))) if id == sid && msg == "local") + ); + assert_eq!(stream.next().await, None); + assert!(stream.is_terminated()); + } +} diff --git a/crates/socketioxide-mongodb/src/lib.rs b/crates/socketioxide-mongodb/src/lib.rs index d68a38f1..60c392a8 100644 --- a/crates/socketioxide-mongodb/src/lib.rs +++ b/crates/socketioxide-mongodb/src/lib.rs @@ -110,10 +110,10 @@ use socketioxide_core::{ adapter::{ BroadcastOptions, CoreAdapter, CoreLocalAdapter, DefinedAdapter, RemoteSocketData, Room, RoomParam, SocketEmitter, Spawnable, + stream::{AckStream, ChanStream, DropStream, ResponseHandlers}, }, packet::Packet, }; -use stream::{AckStream, ChanStream, DropStream}; use tokio::sync::mpsc; use drivers::{Driver, Item, ItemHeader}; @@ -303,8 +303,6 @@ impl From> for AdapterError { } } -pub(crate) type ResponseHandlers = HashMap>; - /// The mongodb adapter with the [mongodb](drivers::mongodb::mongodb_client) driver. #[cfg(feature = "mongodb")] pub type MongoDbAdapter = CustomMongoDbAdapter; @@ -326,14 +324,14 @@ pub struct CustomMongoDbAdapter { /// A map of nodes liveness, with the last time remote nodes were seen alive. nodes_liveness: Mutex>, /// A map of response handlers used to await for responses from the remote servers. - responses: Arc>, + responses: Arc>>, } impl DefinedAdapter for CustomMongoDbAdapter {} impl CoreAdapter for CustomMongoDbAdapter { type Error = Error; type State = MongoDbAdapterCtr; - type AckStream = AckStream; + type AckStream = AckStream, Item, E::AckError>; type InitRes = InitRes; fn new(state: &Self::State, local: CoreLocalAdapter) -> Self { @@ -429,7 +427,7 @@ impl CoreAdapter for CustomMongoDbAdapter if opts.is_local(self.uid) { tracing::debug!(?opts, "broadcast with ack is local"); let (local, _) = self.local.broadcast_with_ack(packet, opts, timeout); - let stream = AckStream::new_local(local); + let stream = AckStream::new_empty_remote(local, ChanStream::::new(mpsc::channel::(1).1), stream::decode_mongodb_ack); return Ok(stream); } let req = RequestOut::new(self.uid, RequestTypeOut::BroadcastWithAck(&packet), &opts); @@ -451,7 +449,8 @@ impl CoreAdapter for CustomMongoDbAdapter Ok(AckStream::new( local, - rx, + ChanStream::new(rx), + stream::decode_mongodb_ack, timeout, remote_serv_cnt, req_id, diff --git a/crates/socketioxide-mongodb/src/stream.rs b/crates/socketioxide-mongodb/src/stream.rs index eeb64467..02c5af79 100644 --- a/crates/socketioxide-mongodb/src/stream.rs +++ b/crates/socketioxide-mongodb/src/stream.rs @@ -1,241 +1,16 @@ -use std::{ - fmt, - pin::Pin, - sync::{Arc, Mutex}, - task::{self, Poll}, - time::Duration, -}; +use std::fmt; -use futures_core::{FusedStream, Stream}; -use futures_util::{StreamExt, stream::TakeUntil}; -use pin_project_lite::pin_project; use serde::de::DeserializeOwned; -use socketioxide_core::{ - Sid, - adapter::AckStreamItem, - adapter::remote_packet::{Response, ResponseType}, -}; -use tokio::{sync::mpsc, time}; +use socketioxide_core::adapter::remote_packet::Response; -use crate::{ResponseHandlers, drivers::Item}; +use crate::drivers::Item; -pin_project! { - /// A stream of acknowledgement messages received from the local and remote servers. - /// It merges the local ack stream with the remote ack stream from all the servers. - // The server_cnt is the number of servers that are expected to send a AckCount message. - // It is decremented each time a AckCount message is received. - // - // The ack_cnt is the number of acks that are expected to be received. It is the sum of all the the ack counts. - // And it is decremented each time an ack is received. - // - // Therefore an exhausted stream correspond to `ack_cnt == 0` and `server_cnt == 0`. - pub struct AckStream { - #[pin] - local: S, - #[pin] - remote: DropStream>, - ack_cnt: u32, - total_ack_cnt: usize, - serv_cnt: u16, - } -} - -impl AckStream { - pub fn new( - local: S, - rx: mpsc::Receiver, - timeout: Duration, - serv_cnt: u16, - req_id: Sid, - handlers: Arc>, - ) -> Self { - let remote = ChanStream::new(rx).take_until(time::sleep(timeout)); - let remote = DropStream::new(remote, handlers, req_id); - Self { - local, - remote, - ack_cnt: 0, - total_ack_cnt: 0, - serv_cnt, - } - } - pub fn new_local(local: S) -> Self { - let handlers = Arc::new(Mutex::new(ResponseHandlers::new())); - let rx = mpsc::channel(1).1; - let remote = ChanStream::new(rx).take_until(time::sleep(Duration::ZERO)); - let remote = DropStream::new(remote, handlers, Sid::ZERO); - Self { - local, - remote, - ack_cnt: 0, - total_ack_cnt: 0, - serv_cnt: 0, - } - } -} -impl AckStream -where - Err: DeserializeOwned + fmt::Debug, - S: Stream> + FusedStream, -{ - /// Poll the remote stream. First the count of acks is received, then the acks are received. - /// We expect `serv_cnt` of `BroadcastAckCount` messages to be received, then we expect - /// `ack_cnt` of `BroadcastAck` messages. - fn poll_remote( - self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - ) -> Poll>> { - // remote stream is not fused, so we need to check if it is terminated - if FusedStream::is_terminated(&self) { - return Poll::Ready(None); - } - let mut projection = self.project(); - loop { - match projection.remote.as_mut().poll_next(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(None) => return Poll::Ready(None), - Poll::Ready(Some(Item { header, data, .. })) => { - let res = rmp_serde::from_slice::>(&data); - match res { - Ok(Response { - node_id: uid, - r#type: ResponseType::BroadcastAckCount(count), - }) if *projection.serv_cnt > 0 => { - tracing::trace!(?uid, ?header, "receiving broadcast ack count {count}"); - *projection.ack_cnt += count; - *projection.total_ack_cnt += count as usize; - *projection.serv_cnt -= 1; - } - Ok(Response { - node_id: uid, - r#type: ResponseType::BroadcastAck((sid, res)), - }) if *projection.ack_cnt > 0 => { - tracing::trace!( - ?uid, - ?header, - "receiving broadcast ack {sid} {:?}", - res - ); - *projection.ack_cnt -= 1; - return Poll::Ready(Some((sid, res))); - } - Ok(Response { node_id: uid, .. }) => { - tracing::warn!(?uid, ?header, "unexpected response type"); - } - Err(e) => { - tracing::warn!("error decoding ack response: {e}"); - } - } - } - } - } - } -} -impl Stream for AckStream -where - E: DeserializeOwned + fmt::Debug, - S: Stream> + FusedStream, -{ - type Item = AckStreamItem; - fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - match self.as_mut().project().local.poll_next(cx) { - Poll::Pending => match self.poll_remote(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Some(item)) => Poll::Ready(Some(item)), - Poll::Ready(None) => Poll::Pending, - }, - Poll::Ready(Some(item)) => Poll::Ready(Some(item)), - Poll::Ready(None) => self.poll_remote(cx), - } - } - - fn size_hint(&self) -> (usize, Option) { - let (lower, upper) = self.local.size_hint(); - (lower, upper.map(|upper| upper + self.total_ack_cnt)) - } -} - -impl FusedStream for AckStream -where - Err: DeserializeOwned + fmt::Debug, - S: Stream> + FusedStream, -{ - /// The stream is terminated if: - /// * The local stream is terminated. - /// * All the servers have sent the expected ack count. - /// * We have received all the expected acks. - fn is_terminated(&self) -> bool { - // remote stream is terminated if the timeout is reached - let remote_term = (self.ack_cnt == 0 && self.serv_cnt == 0) || self.remote.is_terminated(); - self.local.is_terminated() && remote_term - } -} -impl fmt::Debug for AckStream { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("AckStream") - .field("ack_cnt", &self.ack_cnt) - .field("total_ack_cnt", &self.total_ack_cnt) - .field("serv_cnt", &self.serv_cnt) - .finish() - } -} - -pin_project! { - /// A stream of messages received from a channel. - pub struct ChanStream { - #[pin] - rx: mpsc::Receiver - } -} -impl ChanStream { - pub fn new(rx: mpsc::Receiver) -> Self { - Self { rx } - } -} -impl Stream for ChanStream { - type Item = Item; - - fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - self.project().rx.poll_recv(cx) - } -} -pin_project! { - /// A stream that unsubscribes from its source channel when dropped. - pub struct DropStream { - #[pin] - stream: S, - req_id: Sid, - handlers: Arc> - } - impl PinnedDrop for DropStream { - fn drop(this: Pin<&mut Self>) { - let stream = this.project(); - let chan = stream.req_id; - tracing::debug!(?chan, "dropping stream"); - stream.handlers.lock().unwrap().remove(chan); - } - } -} -impl DropStream { - pub fn new(stream: S, handlers: Arc>, req_id: Sid) -> Self { - Self { - stream, - handlers, - req_id, - } - } -} -impl Stream for DropStream { - type Item = S::Item; - - fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - self.project().stream.poll_next(cx) - } -} -impl FusedStream for DropStream { - fn is_terminated(&self) -> bool { - self.stream.is_terminated() - } +/// Decode a MongoDB `Item`'s msgpack data into a `Response`. +pub fn decode_mongodb_ack( + item: Item, +) -> Result, Box> { + rmp_serde::from_slice::>(&item.data) + .map_err(|e| Box::new(e) as Box) } #[cfg(test)] @@ -243,8 +18,10 @@ mod tests { use futures_core::FusedStream; use futures_util::StreamExt; use socketioxide_core::{Sid, Value}; + use tokio::sync::mpsc; - use super::AckStream; + use crate::stream::decode_mongodb_ack; + use socketioxide_core::adapter::stream::{AckStream, ChanStream}; #[tokio::test] async fn local_ack_stream_should_have_a_closed_remote() { @@ -252,18 +29,15 @@ mod tests { let local = futures_util::stream::once(async move { (sid, Ok::<_, ()>(Value::Str("local".into(), None))) }); - let stream = AckStream::new_local(local); + let (_, rx) = mpsc::channel::(1); + let stream: AckStream<_, _, crate::drivers::Item, ()> = AckStream::new_empty_remote(local, ChanStream::new(rx), decode_mongodb_ack::<()>); futures_util::pin_mut!(stream); - assert_eq!(stream.ack_cnt, 0); - assert_eq!(stream.total_ack_cnt, 0); - assert_eq!(stream.serv_cnt, 0); - assert!(!stream.local.is_terminated()); - assert!(!stream.is_terminated()); + assert!(!FusedStream::is_terminated(&stream)); let data = stream.next().await; assert!( matches!(data, Some((id, Ok(Value::Str(msg, None)))) if id == sid && msg == "local") ); assert_eq!(stream.next().await, None); - assert!(stream.is_terminated()); + assert!(FusedStream::is_terminated(&stream)); } } diff --git a/crates/socketioxide-postgres/src/lib.rs b/crates/socketioxide-postgres/src/lib.rs index b10eb7cd..909e2d81 100644 --- a/crates/socketioxide-postgres/src/lib.rs +++ b/crates/socketioxide-postgres/src/lib.rs @@ -67,6 +67,7 @@ use socketioxide_core::{ RequestIn, RequestOut, RequestTypeIn, RequestTypeOut, Response, ResponseType, ResponseTypeId, }, + stream::{AckStream, ChanStream, ResponseHandlers}, }, packet::Packet, }; @@ -83,7 +84,7 @@ use tokio::{sync::mpsc, task::AbortHandle}; use crate::{ drivers::Notification, - stream::{AckPayload, AckStream, ChanStream}, + stream::AckPayload, }; pub mod drivers; @@ -333,8 +334,6 @@ pub type SqlxAdapter = CustomPostgresAdapter; pub type TokioPostgresAdapter = CustomPostgresAdapter; -type ResponseHandlers = HashMap>; - /// The postgres adapter implementation. /// It is generic over the [`Driver`] used to communicate with the postgres server. /// And over the [`SocketEmitter`] used to communicate with the local server. This allows to @@ -350,7 +349,7 @@ pub struct CustomPostgresAdapter { /// A map of nodes liveness, with the last time remote nodes were seen alive. nodes_liveness: Mutex>, /// A map of response handlers used to await for responses from the remote servers. - responses: Arc>, + responses: Arc>>, ev_stream_task: OnceLock, hb_task: OnceLock, @@ -361,7 +360,7 @@ impl DefinedAdapter for CustomPostgresAdapter {} impl CoreAdapter for CustomPostgresAdapter { type Error = Error; type State = PostgresAdapterCtr; - type AckStream = AckStream; + type AckStream = AckStream, ResponsePayload, E::AckError>; type InitRes = InitRes; fn new(state: &Self::State, local: CoreLocalAdapter) -> Self { @@ -494,7 +493,7 @@ impl CoreAdapter for CustomPostgresAdapter if opts.is_local(self.local.server_id()) { tracing::debug!(?opts, "broadcast with ack is local"); let (local, _) = self.local.broadcast_with_ack(packet, opts, timeout); - let stream = AckStream::new_local(local); + let stream = AckStream::new_empty_remote(local, futures_util::stream::empty::().boxed(), stream::decode_postgres_ack); return Ok(stream); } let req = RequestOut::new( @@ -535,6 +534,7 @@ impl CoreAdapter for CustomPostgresAdapter Ok(AckStream::new( local, remote, + stream::decode_postgres_ack, timeout, remote_serv_cnt, req_id, @@ -1231,7 +1231,7 @@ impl ResponsePacket { } } #[derive(Debug, Deserialize, Serialize)] -pub(crate) enum ResponsePayload { +pub enum ResponsePayload { Data(Box), Attachment { id: i64, is_binary: bool }, } diff --git a/crates/socketioxide-postgres/src/stream.rs b/crates/socketioxide-postgres/src/stream.rs index 0a805a2b..4618585b 100644 --- a/crates/socketioxide-postgres/src/stream.rs +++ b/crates/socketioxide-postgres/src/stream.rs @@ -1,260 +1,34 @@ -use std::{ - fmt, - pin::Pin, - sync::{Arc, Mutex}, - task::{self, Poll}, - time::Duration, -}; +use std::fmt; -use futures_core::{FusedStream, Stream, stream::BoxStream}; -use futures_util::{StreamExt, stream::TakeUntil}; -use pin_project_lite::pin_project; -use serde::{Deserialize, de::DeserializeOwned}; +use serde::Deserialize; use serde_json::value::RawValue; -use socketioxide_core::{ - Sid, - adapter::{ - AckStreamItem, - remote_packet::{Response, ResponseType}, - }, -}; -use tokio::{sync::mpsc, time}; - -use crate::{ResponseHandlers, drivers::Notification}; +use socketioxide_core::adapter::remote_packet::Response; /// A resolved ack-response payload carrying either a JSON `RawValue` (inline data or a /// JSON-encoded attachment) or raw msgpack bytes (a binary attachment). The format /// has to travel with the payload because the eventual `Response` is decoded -/// downstream in [`AckStream::poll_remote`], where the right serde codec is selected. +/// downstream, where the right serde codec is selected. pub enum AckPayload { Json(Box), MsgPack(Vec), } impl AckPayload { - fn deserialize<'de, T: Deserialize<'de>>(&'de self) -> Result> { - let v = match &self { - AckPayload::Json(rv) => serde_json::from_str::(rv.get())?, - AckPayload::MsgPack(bytes) => rmp_serde::from_slice::(bytes)?, - }; - - Ok(v) - } -} - -pin_project! { - /// A stream of acknowledgement messages received from the local and remote servers. - /// It merges the local ack stream with the remote ack stream from all the servers. - // The server_cnt is the number of servers that are expected to send a AckCount message. - // It is decremented each time a AckCount message is received. - // - // The ack_cnt is the number of acks that are expected to be received. It is the sum of all the the ack counts. - // And it is decremented each time an ack is received. - // - // Therefore an exhausted stream correspond to `ack_cnt == 0` and `server_cnt == 0`. - pub struct AckStream { - #[pin] - local: S, - #[pin] - remote: DropStream, time::Sleep>>, - ack_cnt: u32, - total_ack_cnt: usize, - serv_cnt: u16, - } -} - -impl AckStream { - /// Build an ack stream backed by a remote `ResponsePayload` channel. Attachment payloads are - /// resolved lazily, driven by polling — not by a background task — so dropping the stream - /// cancels any still-pending fetch. - pub(crate) fn new( - local: S, - remote: BoxStream<'static, AckPayload>, - timeout: Duration, - serv_cnt: u16, - req_sid: Sid, - handlers: Arc>, - ) -> Self { - let remote = remote.take_until(time::sleep(timeout)); - let remote = DropStream::new(remote, handlers, req_sid); - Self { - local, - ack_cnt: 0, - total_ack_cnt: 0, - serv_cnt, - remote, - } - } - - pub(crate) fn new_local(local: S) -> Self { - let handlers = Arc::new(Mutex::new(ResponseHandlers::new())); - let empty: BoxStream<'static, AckPayload> = futures_util::stream::empty().boxed(); - let remote = empty.take_until(time::sleep(Duration::ZERO)); - let remote = DropStream::new(remote, handlers, Sid::ZERO); - Self { - local, - remote, - ack_cnt: 0, - total_ack_cnt: 0, - serv_cnt: 0, + fn deserialize<'de, T: Deserialize<'de>>(&'de self) -> Result> { + match &self { + AckPayload::Json(rv) => serde_json::from_str::(rv.get()) + .map_err(|e| Box::new(e) as Box), + AckPayload::MsgPack(bytes) => rmp_serde::from_slice::(bytes) + .map_err(|e| Box::new(e) as Box), } } } -impl AckStream -where - Err: DeserializeOwned + fmt::Debug, - S: Stream> + FusedStream, -{ - /// Poll the remote stream. First the count of acks is receivedhen the acks are received. - /// We expect `serv_cnt` of `BroadcastAckCount` messages to be receivedhen we expect - /// `ack_cnt` of `BroadcastAck` messages. - fn poll_remote( - self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - ) -> Poll>> { - // remote stream is not fused, so we need to check if it is terminated - if FusedStream::is_terminated(&self) { - return Poll::Ready(None); - } - let mut projection = self.project(); - loop { - match projection.remote.as_mut().poll_next(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(None) => return Poll::Ready(None), - Poll::Ready(Some(notif)) => match notif.deserialize() { - Ok(Response { - node_id: uid, - r#type: ResponseType::BroadcastAckCount(count), - }) if *projection.serv_cnt > 0 => { - tracing::trace!(?uid, "receiving broadcast ack count {count}"); - *projection.ack_cnt += count; - *projection.total_ack_cnt += count as usize; - *projection.serv_cnt -= 1; - } - Ok(Response { - node_id: uid, - r#type: ResponseType::BroadcastAck((sid, res)), - }) if *projection.ack_cnt > 0 => { - tracing::trace!(?uid, "receiving broadcast ack {sid} {:?}", res); - *projection.ack_cnt -= 1; - return Poll::Ready(Some((sid, res))); - } - Ok(Response { node_id: uid, .. }) => { - tracing::warn!(?uid, "unexpected response type"); - } - Err(e) => { - tracing::warn!("error decoding ack response: {e}"); - } - }, - } - } - } -} -impl Stream for AckStream -where - E: DeserializeOwned + fmt::Debug, - S: Stream> + FusedStream, -{ - type Item = AckStreamItem; - fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - match self.as_mut().project().local.poll_next(cx) { - Poll::Pending => match self.poll_remote(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Some(item)) => Poll::Ready(Some(item)), - Poll::Ready(None) => Poll::Pending, - }, - Poll::Ready(Some(item)) => Poll::Ready(Some(item)), - Poll::Ready(None) => self.poll_remote(cx), - } - } - fn size_hint(&self) -> (usize, Option) { - let (lower, upper) = self.local.size_hint(); - (lower, upper.map(|upper| upper + self.total_ack_cnt)) - } -} - -impl FusedStream for AckStream -where - Err: DeserializeOwned + fmt::Debug, - S: Stream> + FusedStream, -{ - /// The stream is terminated if: - /// * The local stream is terminated. - /// * All the servers have sent the expected ack count. - /// * We have received all the expected acks. - fn is_terminated(&self) -> bool { - // remote stream is terminated if the timeout is reached - let remote_term = (self.ack_cnt == 0 && self.serv_cnt == 0) || self.remote.is_terminated(); - self.local.is_terminated() && remote_term - } -} -impl fmt::Debug for AckStream { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("AckStream") - .field("ack_cnt", &self.ack_cnt) - .field("total_ack_cnt", &self.total_ack_cnt) - .field("serv_cnt", &self.serv_cnt) - .finish() - } -} -pin_project! { - /// A stream of messages received from a channel. - pub struct ChanStream { - #[pin] - rx: mpsc::Receiver - } -} -impl ChanStream { - pub fn new(rx: mpsc::Receiver) -> Self { - Self { rx } - } -} -impl Stream for ChanStream { - type Item = T; - - fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - self.project().rx.poll_recv(cx) - } -} - -pin_project! { - /// A stream that unsubscribes from its source channel when dropped. - pub struct DropStream { - #[pin] - stream: S, - req_id: Sid, - handlers: Arc> - } - impl PinnedDrop for DropStream { - fn drop(this: Pin<&mut Self>) { - let stream = this.project(); - let chan = stream.req_id; - tracing::debug!(?chan, "dropping stream"); - stream.handlers.lock().unwrap().remove(chan); - } - } -} -impl DropStream { - pub fn new(stream: S, handlers: Arc>, req_id: Sid) -> Self { - Self { - stream, - handlers, - req_id, - } - } -} -impl Stream for DropStream { - type Item = S::Item; - - fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - self.project().stream.poll_next(cx) - } -} -impl FusedStream for DropStream { - fn is_terminated(&self) -> bool { - self.stream.is_terminated() - } +/// Decode an `AckPayload` into a `Response` using either JSON or msgpack. +pub fn decode_postgres_ack( + item: AckPayload, +) -> Result, Box> { + item.deserialize() } #[cfg(test)] @@ -263,7 +37,8 @@ mod tests { use futures_util::StreamExt; use socketioxide_core::{Sid, Value}; - use super::AckStream; + use crate::stream::{AckPayload, decode_postgres_ack}; + use socketioxide_core::adapter::stream::AckStream; #[tokio::test] async fn local_ack_stream_should_have_a_closed_remote() { @@ -271,18 +46,16 @@ mod tests { let local = futures_util::stream::once(async move { (sid, Ok::<_, ()>(Value::Str("local".into(), None))) }); - let stream = AckStream::<_>::new_local(local); + let empty: futures_core::stream::BoxStream<'static, AckPayload> = + futures_util::stream::empty().boxed(); + let stream: AckStream<_, _, crate::ResponsePayload, ()> = AckStream::new_empty_remote(local, empty, decode_postgres_ack::<()>); futures_util::pin_mut!(stream); - assert_eq!(stream.ack_cnt, 0); - assert_eq!(stream.total_ack_cnt, 0); - assert_eq!(stream.serv_cnt, 0); - assert!(!stream.local.is_terminated()); - assert!(!stream.is_terminated()); + assert!(!FusedStream::is_terminated(&stream)); let data = stream.next().await; assert!( matches!(data, Some((id, Ok(Value::Str(msg, None)))) if id == sid && msg == "local") ); assert_eq!(stream.next().await, None); - assert!(stream.is_terminated()); + assert!(FusedStream::is_terminated(&stream)); } } diff --git a/crates/socketioxide-redis/src/lib.rs b/crates/socketioxide-redis/src/lib.rs index a1747672..fa86bff3 100644 --- a/crates/socketioxide-redis/src/lib.rs +++ b/crates/socketioxide-redis/src/lib.rs @@ -165,7 +165,7 @@ use socketioxide_core::{ }, packet::Packet, }; -use stream::{AckStream, DropStream}; +use socketioxide_core::adapter::stream::{AckStream, DropStream, ResponseHandlers}; use tokio::{sync::mpsc, time}; /// Drivers are an abstraction over the pub/sub backend used by the adapter. @@ -346,8 +346,6 @@ impl RedisAdapterCtr { } } -pub(crate) type ResponseHandlers = HashMap>>; - /// The redis adapter with the fred driver. #[cfg(feature = "fred")] pub type FredAdapter = CustomRedisAdapter; @@ -378,14 +376,14 @@ pub struct CustomRedisAdapter { /// format: `{prefix}-request#{path}#`. req_chan: String, /// A map of response handlers used to await for responses from the remote servers. - responses: Arc>, + responses: Arc>>>, } impl DefinedAdapter for CustomRedisAdapter {} impl CoreAdapter for CustomRedisAdapter { type Error = Error; type State = RedisAdapterCtr; - type AckStream = AckStream; + type AckStream = AckStream>, Vec, E::AckError>; type InitRes = InitRes; fn new(state: &Self::State, local: CoreLocalAdapter) -> Self { @@ -504,7 +502,7 @@ impl CoreAdapter for CustomRedisAdapter { if opts.is_local(self.uid) { tracing::debug!(?opts, "broadcast with ack is local"); let (local, _) = self.local.broadcast_with_ack(packet, opts, timeout); - let stream = AckStream::new_local(local); + let stream = AckStream::new_empty_remote(local, MessageStream::>::new_empty(), stream::decode_redis_ack); return Ok(stream); } let req = RequestOut::new(self.uid, RequestTypeOut::BroadcastWithAck(&packet), &opts); @@ -528,6 +526,7 @@ impl CoreAdapter for CustomRedisAdapter { Ok(AckStream::new( local, remote, + stream::decode_redis_ack, timeout, remote_serv_cnt, req_id, @@ -959,6 +958,7 @@ pub fn read_req_id(data: &[u8]) -> Option { mod tests { use super::*; use futures_util::stream::{self, FusedStream, StreamExt}; + use crate::stream::decode_redis_ack; use socketioxide_core::{Str, Value, adapter::AckStreamItem}; use std::convert::Infallible; @@ -990,10 +990,11 @@ mod tests { fn new_stub_ack_stream( remote: MessageStream>, timeout: Duration, - ) -> AckStream>> { + ) -> AckStream>, MessageStream>, Vec, ()> { AckStream::new( stream::empty::>(), remote, + decode_redis_ack, timeout, 2, Sid::new(), @@ -1063,9 +1064,10 @@ mod tests { let handlers = Arc::new(Mutex::new(HashMap::new())); let id = Sid::new(); handlers.lock().unwrap().insert(id, tx); - let stream = AckStream::new( + let stream: AckStream<_, _, Vec, ()> = AckStream::new( stream::empty::>(), remote, + decode_redis_ack, Duration::from_secs(10), 2, id, diff --git a/crates/socketioxide-redis/src/stream.rs b/crates/socketioxide-redis/src/stream.rs index b7d24584..e296f70c 100644 --- a/crates/socketioxide-redis/src/stream.rs +++ b/crates/socketioxide-redis/src/stream.rs @@ -1,224 +1,18 @@ -use std::{ - fmt, - pin::Pin, - sync::{Arc, Mutex}, - task::{self, Poll}, - time::Duration, -}; +use std::fmt; -use futures_core::{FusedStream, Stream}; -use futures_util::{StreamExt, stream::TakeUntil}; -use pin_project_lite::pin_project; use serde::de::DeserializeOwned; use socketioxide_core::{ Sid, - adapter::AckStreamItem, - adapter::remote_packet::{Response, ResponseType}, + adapter::remote_packet::Response, }; -use tokio::time; - -use crate::{ResponseHandlers, drivers::MessageStream}; - -pin_project! { - /// A stream of acknowledgement messages received from the local and remote servers. - /// It merges the local ack stream with the remote ack stream from all the servers. - // The server_cnt is the number of servers that are expected to send a AckCount message. - // It is decremented each time a AckCount message is received. - // - // The ack_cnt is the number of acks that are expected to be received. It is the sum of all the the ack counts. - // And it is decremented each time an ack is received. - // - // Therefore an exhausted stream correspond to `ack_cnt == 0` and `server_cnt == 0`. - pub struct AckStream { - #[pin] - local: S, - #[pin] - remote: DropStream>, time::Sleep>>, - ack_cnt: u32, - total_ack_cnt: usize, - serv_cnt: u16, - } -} - -impl AckStream { - pub fn new( - local: S, - remote: MessageStream>, - timeout: Duration, - serv_cnt: u16, - req_id: Sid, - handlers: Arc>, - ) -> Self { - let remote = remote.take_until(time::sleep(timeout)); - let remote = DropStream::new(remote, handlers, req_id); - Self { - local, - remote, - ack_cnt: 0, - total_ack_cnt: 0, - serv_cnt, - } - } - pub fn new_local(local: S) -> Self { - let handlers = Arc::new(Mutex::new(ResponseHandlers::new())); - let remote = MessageStream::new_empty().take_until(time::sleep(Duration::ZERO)); - let remote = DropStream::new(remote, handlers, Sid::ZERO); - Self { - local, - remote, - ack_cnt: 0, - total_ack_cnt: 0, - serv_cnt: 0, - } - } -} -impl AckStream -where - Err: DeserializeOwned + fmt::Debug, - S: Stream> + FusedStream, -{ - /// Poll the remote stream. First the count of acks is received, then the acks are received. - /// We expect `serv_cnt` of `BroadcastAckCount` messages to be received, then we expect - /// `ack_cnt` of `BroadcastAck` messages. - fn poll_remote( - mut self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - ) -> Poll>> { - // remote stream is not fused, so we need to check if it is terminated - if FusedStream::is_terminated(&self) { - return Poll::Ready(None); - } - - let projection = self.as_mut().project(); - match projection.remote.poll_next(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(None) => Poll::Ready(None), - Poll::Ready(Some(item)) => { - let res = rmp_serde::from_slice::<(Sid, Response)>(&item); - match res { - Ok(( - req_id, - Response { - node_id: uid, - r#type: ResponseType::BroadcastAckCount(count), - }, - )) if *projection.serv_cnt > 0 => { - tracing::trace!(?uid, ?req_id, "receiving broadcast ack count {count}"); - *projection.ack_cnt += count; - *projection.total_ack_cnt += count as usize; - *projection.serv_cnt -= 1; - self.poll_remote(cx) - } - Ok(( - req_id, - Response { - node_id: uid, - r#type: ResponseType::BroadcastAck((sid, res)), - }, - )) if *projection.ack_cnt > 0 => { - tracing::trace!(?uid, ?req_id, "receiving broadcast ack {sid} {:?}", res); - *projection.ack_cnt -= 1; - Poll::Ready(Some((sid, res))) - } - Ok((req_id, Response { node_id: uid, .. })) => { - tracing::warn!(?uid, ?req_id, ?self, "unexpected response type"); - self.poll_remote(cx) - } - Err(e) => { - tracing::warn!("error decoding ack response: {e}"); - self.poll_remote(cx) - } - } - } - } - } -} -impl Stream for AckStream -where - E: DeserializeOwned + fmt::Debug, - S: Stream> + FusedStream, -{ - type Item = AckStreamItem; - fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - match self.as_mut().project().local.poll_next(cx) { - Poll::Pending => match self.poll_remote(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Some(item)) => Poll::Ready(Some(item)), - Poll::Ready(None) => Poll::Pending, - }, - Poll::Ready(Some(item)) => Poll::Ready(Some(item)), - Poll::Ready(None) => self.poll_remote(cx), - } - } - - fn size_hint(&self) -> (usize, Option) { - let (lower, upper) = self.local.size_hint(); - (lower, upper.map(|upper| upper + self.total_ack_cnt)) - } -} - -impl FusedStream for AckStream -where - Err: DeserializeOwned + fmt::Debug, - S: Stream> + FusedStream, -{ - /// The stream is terminated if: - /// * The local stream is terminated. - /// * All the servers have sent the expected ack count. - /// * We have received all the expected acks. - fn is_terminated(&self) -> bool { - // remote stream is terminated if the timeout is reached - let remote_term = (self.ack_cnt == 0 && self.serv_cnt == 0) || self.remote.is_terminated(); - self.local.is_terminated() && remote_term - } -} -impl fmt::Debug for AckStream { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("AckStream") - .field("ack_cnt", &self.ack_cnt) - .field("total_ack_cnt", &self.total_ack_cnt) - .field("serv_cnt", &self.serv_cnt) - .finish() - } -} -pin_project! { - /// A stream that unsubscribes from its source channel when dropped. - pub struct DropStream { - #[pin] - stream: S, - req_id: Sid, - handlers: Arc> - } - impl PinnedDrop for DropStream { - fn drop(this: Pin<&mut Self>) { - let stream = this.project(); - let chan = stream.req_id; - tracing::debug!(?chan, "dropping stream"); - stream.handlers.lock().unwrap().remove(chan); - } - } -} -impl DropStream { - pub fn new(stream: S, handlers: Arc>, req_id: Sid) -> Self { - Self { - stream, - handlers, - req_id, - } - } -} -impl Stream for DropStream { - type Item = S::Item; - - fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - self.project().stream.poll_next(cx) - } -} -impl FusedStream for DropStream { - fn is_terminated(&self) -> bool { - self.stream.is_terminated() - } +/// Decode a msgpack-encoded `(Sid, Response)` tuple from raw bytes. +pub fn decode_redis_ack( + item: Vec, +) -> Result, Box> { + rmp_serde::from_slice::<(Sid, Response)>(&item) + .map(|(_, response)| response) + .map_err(|e| Box::new(e) as Box) } #[cfg(test)] @@ -227,7 +21,9 @@ mod tests { use futures_util::StreamExt; use socketioxide_core::{Sid, Value}; - use super::AckStream; + use crate::drivers::MessageStream; + use crate::stream::decode_redis_ack; + use socketioxide_core::adapter::stream::AckStream; #[tokio::test] async fn local_ack_stream_should_have_a_closed_remote() { @@ -235,18 +31,14 @@ mod tests { let local = futures_util::stream::once(async move { (sid, Ok::<_, ()>(Value::Str("local".into(), None))) }); - let stream = AckStream::new_local(local); + let stream: AckStream<_, _, Vec, ()> = AckStream::new_empty_remote(local, MessageStream::>::new_empty(), decode_redis_ack::<()>); futures_util::pin_mut!(stream); - assert_eq!(stream.ack_cnt, 0); - assert_eq!(stream.total_ack_cnt, 0); - assert_eq!(stream.serv_cnt, 0); - assert!(!stream.local.is_terminated()); - assert!(!stream.is_terminated()); + assert!(!FusedStream::is_terminated(&stream)); let data = stream.next().await; assert!( matches!(data, Some((id, Ok(Value::Str(msg, None)))) if id == sid && msg == "local") ); assert_eq!(stream.next().await, None); - assert!(stream.is_terminated()); + assert!(FusedStream::is_terminated(&stream)); } } From 520e7ab469d5ba9939d0f9b01ca2552a92187a83 Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 11 Jul 2026 22:23:00 +0200 Subject: [PATCH 2/2] fix: add module docs and AckDecoder type alias to silence clippy warnings --- crates/socketioxide-core/src/adapter/mod.rs | 1 + crates/socketioxide-core/src/adapter/stream.rs | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/socketioxide-core/src/adapter/mod.rs b/crates/socketioxide-core/src/adapter/mod.rs index 55b3014e..03b4f47d 100644 --- a/crates/socketioxide-core/src/adapter/mod.rs +++ b/crates/socketioxide-core/src/adapter/mod.rs @@ -28,6 +28,7 @@ pub mod errors; #[cfg(feature = "remote-adapter")] pub mod remote_packet; #[cfg(feature = "remote-adapter")] +#[doc = "Shared stream constructs (`AckStream`, `DropStream`, `ResponseHandlers`) for remote adapters."] pub mod stream; /// A room identifier diff --git a/crates/socketioxide-core/src/adapter/stream.rs b/crates/socketioxide-core/src/adapter/stream.rs index ef7e6bd7..99bc884c 100644 --- a/crates/socketioxide-core/src/adapter/stream.rs +++ b/crates/socketioxide-core/src/adapter/stream.rs @@ -20,6 +20,9 @@ use crate::adapter::remote_packet::{Response, ResponseType}; /// A map of request IDs to response senders, used to route responses to the correct awaiting stream. pub type ResponseHandlers = HashMap>; +/// Decoder function that converts a raw remote payload item into a [`Response`]. +pub type AckDecoder = fn(I) -> Result, Box>; + pin_project! { /// A [`Stream`] that wraps an [`mpsc::Receiver`]. pub struct ChanStream { @@ -96,7 +99,7 @@ pin_project! { local: S, #[pin] remote: DropStream, T>, - decode: fn(R::Item) -> Result, Box>, + decode: AckDecoder, ack_cnt: u32, total_ack_cnt: usize, serv_cnt: u16, @@ -108,7 +111,7 @@ impl AckStream { pub fn new( local: S, remote: R, - decode: fn(R::Item) -> Result, Box>, + decode: AckDecoder, timeout: Duration, serv_cnt: u16, req_id: Sid, @@ -127,7 +130,7 @@ impl AckStream { } /// Create a new `AckStream` for local-only use, with an empty remote stream. - pub fn new_empty_remote(local: S, empty_remote: R, decode: fn(R::Item) -> Result, Box>) -> Self { + pub fn new_empty_remote(local: S, empty_remote: R, decode: AckDecoder) -> Self { let handlers = Arc::new(Mutex::new(ResponseHandlers::::new())); let remote = empty_remote.take_until(time::sleep(Duration::ZERO)); let remote = DropStream::new(remote, handlers, Sid::ZERO);