Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b293bd1
wip
Totodore Jun 9, 2025
b1c6445
Merge branch 'main' into engineioxide-client
Totodore Jul 20, 2025
a387107
Merge remote-tracking branch 'origin/main' into engineioxide-client
Totodore Jul 20, 2025
cb13f2f
feat(engineio): refactor shared types to core
Totodore Jul 20, 2025
7266fc8
feat(engineio): refactor shared types to core
Totodore Jul 20, 2025
a7f23e0
feat(client): wip
Totodore Jul 26, 2025
63011e0
feat(client): wip
Totodore Jul 27, 2025
60d2788
feat(client): wip
Totodore Aug 3, 2025
765d055
Merge remote-tracking branch 'origin/main' into engineioxide-client
Totodore Aug 3, 2025
9dd5988
fix(engineio): add content type to decoder
Totodore Aug 3, 2025
24ea9c5
feat(client): wip
Totodore Aug 3, 2025
c27e3b1
Merge remote-tracking branch 'origin/main' into engineioxide-client
Totodore Aug 9, 2025
d111347
Merge branch 'main' into engineioxide-client
Totodore Aug 9, 2025
edc3354
Merge branch 'main' into engineioxide-client
Totodore Aug 20, 2025
1fac3aa
wip
Totodore Oct 10, 2025
4a4439f
wip
Totodore Oct 11, 2025
47ad1b6
Merge remote-tracking branch 'origin/main' into engineioxide-client
Totodore Jun 29, 2026
3a53753
Merge remote-tracking branch 'origin/main' into engineioxide-client-impl
Totodore Jul 9, 2026
5da9f12
wip
Totodore Jul 9, 2026
98ab73f
feat: polling transport
Totodore Jul 11, 2026
fc95b11
feat: polling transport
Totodore Jul 11, 2026
3b252ed
feat: polling transport
Totodore Jul 11, 2026
3e7ca00
feat: websocket
Totodore Jul 11, 2026
efb4d77
feat: eioevent
Totodore Jul 11, 2026
dec543b
feat: eioevent
Totodore Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,469 changes: 757 additions & 712 deletions Cargo.lock

Large diffs are not rendered by default.

50 changes: 50 additions & 0 deletions crates/engineioxide-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[package]
name = "engineioxide-client"
description = "Engine IO client implementation in rust"
version = "0.17.0"
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
keywords.workspace = true
categories.workspace = true
license.workspace = true
readme = "README.md"

[dependencies]
engineioxide-core = { path = "../engineioxide-core", version = "0.2" }
bytes.workspace = true
futures-core.workspace = true
futures-util.workspace = true
http.workspace = true
http-body.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["rt", "time"] }
hyper = { workspace = true, features = ["client", "http1"] }
tokio-tungstenite.workspace = true
http-body-util.workspace = true
pin-project-lite.workspace = true
smallvec.workspace = true
hyper-util = { workspace = true, features = ["tokio"] }

# Tracing
tracing = { workspace = true } # TODO: make optional

[dev-dependencies]
tokio = { workspace = true, features = ["macros", "parking_lot"] }
tokio-stream.workspace = true
tokio-util.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }
engineioxide = { path = "../engineioxide", features = [
"tracing",
"v3",
"__test_harness",
] }

[features]
v3 = ["engineioxide-core/v3"]
# tracing = ["dep:tracing", "engineioxide-core/tracing"]
__test_harness = []
Empty file.
159 changes: 159 additions & 0 deletions crates/engineioxide-client/src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use std::{
fmt,
pin::Pin,
task::{Context, Poll, ready},
};

use engineioxide_core::{Packet, Sid};
use futures_core::Stream;
use futures_util::Sink;

use crate::{
EioEvent,
transport::{
PollingSvc, PollingTransport, PollingTransportError, Transport, TransportError, WebSocket,
WsTransport, WsTransportError, noop_impl::NoopWebSocket,
},
};

pin_project_lite::pin_project! {
pub struct Client<S: PollingSvc, WS > {
#[pin]
pub transport: Transport<S, WS>,

should_send_pong: bool,
should_flush: bool,
closing: bool,
pub sid: Sid,
}
}

impl<S: PollingSvc, WS: WebSocket> Client<S, WS> {
pub async fn connect_ws(ws: impl Into<WS>) -> Result<Self, WsTransportError<WS>> {
let (inner, open) = WsTransport::connect(ws).await?;
let transport = Transport::Websocket { inner };
let client = Client {
transport,
sid: open.sid,

should_flush: false,
should_send_pong: false,
closing: false,
};

Ok(client)
}
}
impl<S: PollingSvc> Client<S, NoopWebSocket> {
pub async fn connect(svc: S) -> Result<Self, PollingTransportError<S>> {
let (inner, open) = PollingTransport::connect(svc).await?;
let transport = Transport::Polling { inner };
let client = Client {
transport,
sid: open.sid,

should_flush: false,
should_send_pong: false,
closing: false,
};

Ok(client)
}
}

impl<S: PollingSvc, WS: WebSocket> Client<S, WS> {
#[tracing::instrument(skip(cx))]
fn heartbeat(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), TransportError<S, WS>>> {
let mut proj = self.project();
ready!(proj.transport.as_mut().poll_ready(cx))?;
proj.transport.as_mut().start_send(Packet::Pong)?;

*proj.should_send_pong = false;
*proj.should_flush = true;
Poll::Ready(Ok(()))
}

#[tracing::instrument(skip(cx))]
fn flush(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), TransportError<S, WS>>> {
let mut proj = self.project();
ready!(proj.transport.as_mut().poll_flush(cx))?;
*proj.should_flush = false;
Poll::Ready(Ok(()))
}
}

impl<S: PollingSvc, WS: WebSocket> Stream for Client<S, WS> {
type Item = Result<EioEvent, TransportError<S, WS>>;

#[tracing::instrument(skip(cx))]
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if self.closing {
ready!(self.project().transport.poll_close(cx))?;
return Poll::Ready(None);
}

if self.should_send_pong {
ready!(self.as_mut().heartbeat(cx))?;
}

if self.should_flush {
ready!(self.as_mut().flush(cx))?;
}

match ready!(self.as_mut().project().transport.poll_next(cx)) {
Some(Ok(Packet::Ping)) => {
*self.as_mut().project().should_send_pong = true;
self.poll_next(cx)
}
Some(Ok(Packet::Close)) => {
*self.as_mut().project().closing = true;
cx.waker().wake_by_ref(); // wake up to close the transport
Poll::Ready(Some(Ok(EioEvent::Disconnect)))
}
Some(Ok(Packet::Message(v))) => Poll::Ready(Some(Ok(EioEvent::Message(v)))),
Some(Ok(Packet::Binary(v))) => Poll::Ready(Some(Ok(EioEvent::Binary(v)))),
Some(Ok(v)) => unreachable!("unexpected msg {v:?}"),
Some(Err(e)) => Poll::Ready(Some(Err(e))),
None => Poll::Ready(None),
}
}
}

impl<S: PollingSvc, WS: WebSocket> Sink<EioEvent> for Client<S, WS> {
type Error = <Transport<S, WS> as Sink<Packet>>::Error;

fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.project().transport.poll_ready(cx)
}

fn start_send(self: Pin<&mut Self>, event: EioEvent) -> Result<(), Self::Error> {
if let Some(packet) = event.into() {
self.project().transport.start_send(packet)?;
}
Ok(())
}

fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.project().transport.poll_flush(cx)
}

fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.project().transport.poll_close(cx)
}
}

impl<S: PollingSvc, WS: WebSocket> fmt::Debug for Client<S, WS> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Client")
.field("should_send_pong", &self.should_send_pong)
.field("should_flush", &self.should_flush)
.field("sid", &self.sid)
.finish()
}
}
20 changes: 20 additions & 0 deletions crates/engineioxide-client/src/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use bytes::Bytes;
use engineioxide_core::{Packet, Str};

#[derive(Debug, PartialEq, Eq)]
pub enum EioEvent {
Connect,
Disconnect,
Message(Str),
Binary(Bytes),
}

impl From<EioEvent> for Option<Packet> {
fn from(value: EioEvent) -> Option<Packet> {
match value {
EioEvent::Message(msg) => Some(Packet::Message(msg)),
EioEvent::Binary(bin) => Some(Packet::Binary(bin)),
_ => None,
}
}
}
18 changes: 18 additions & 0 deletions crates/engineioxide-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#![allow(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/Totodore/socketioxide/refs/heads/main/.github/logo_dark.svg"
)]
#![doc(
html_favicon_url = "https://raw.githubusercontent.com/Totodore/socketioxide/refs/heads/main/.github/logo_dark.ico"
)]
//! Engine.IO client library for Rust.

mod client;
mod event;
mod transport;

pub use crate::client::Client;
pub use crate::event::EioEvent;
pub use crate::transport::PollingTransport;
pub use transport::{WsTransport, noop_impl, tungstenite_impl};
121 changes: 121 additions & 0 deletions crates/engineioxide-client/src/transport/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use std::{
fmt,
pin::Pin,
task::{Context, Poll},
};

use engineioxide_core::{Packet, TransportType};
use futures_core::Stream;
use futures_util::Sink;

pub use crate::transport::polling::{PollingSvc, PollingTransport, PollingTransportError};
pub use crate::transport::ws::{
WebSocket, WsTransport, WsTransportError, noop_impl, tungstenite_impl,
};

mod polling;
mod ws;

pub enum TransportError<S: PollingSvc, WS: WebSocket> {
Polling(PollingTransportError<S>),
Websocket(WsTransportError<WS>),
}

impl<S: PollingSvc, WS: WebSocket> fmt::Debug for TransportError<S, WS> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}
impl<S: PollingSvc, WS: WebSocket> fmt::Display for TransportError<S, WS> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TransportError::Polling(e) => write!(f, "polling error: {}", e),
TransportError::Websocket(e) => write!(f, "ws error: {}", e),
}
}
}
impl<S: PollingSvc, WS: WebSocket> std::error::Error for TransportError<S, WS> {}

pin_project_lite::pin_project! {
#[project = TransportProj]
pub enum Transport<S: PollingSvc, WS> {
Polling {
#[pin]
inner: PollingTransport<S>
},
Websocket {
#[pin]
inner: WsTransport<WS>
}
}
}

impl<S: PollingSvc, WS> Transport<S, WS> {
pub fn transport_type(&self) -> TransportType {
match self {
Transport::Polling { .. } => TransportType::Polling,
Transport::Websocket { .. } => TransportType::Websocket,
}
}
}

impl<S: PollingSvc, WS: WebSocket> Stream for Transport<S, WS> {
type Item = Result<Packet, TransportError<S, WS>>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match self.as_mut().project() {
TransportProj::Polling { inner } => {
inner.poll_next(cx).map_err(TransportError::Polling)
}
TransportProj::Websocket { inner } => {
inner.poll_next(cx).map_err(TransportError::Websocket)
}
}
}
}
impl<S: PollingSvc, WS: WebSocket> Sink<Packet> for Transport<S, WS> {
type Error = TransportError<S, WS>;

fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match self.project() {
TransportProj::Polling { inner } => {
inner.poll_ready(cx).map_err(TransportError::Polling)
}
TransportProj::Websocket { inner } => {
inner.poll_ready(cx).map_err(TransportError::Websocket)
}
}
}

fn start_send(self: Pin<&mut Self>, item: Packet) -> Result<(), Self::Error> {
match self.project() {
TransportProj::Polling { inner } => {
inner.start_send(item).map_err(TransportError::Polling)
}
TransportProj::Websocket { inner } => {
inner.start_send(item).map_err(TransportError::Websocket)
}
}
}

fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match self.project() {
TransportProj::Polling { inner } => {
inner.poll_flush(cx).map_err(TransportError::Polling)
}
TransportProj::Websocket { inner } => {
inner.poll_flush(cx).map_err(TransportError::Websocket)
}
}
}

fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match self.project() {
TransportProj::Polling { inner } => {
inner.poll_close(cx).map_err(TransportError::Polling)
}
TransportProj::Websocket { inner } => {
inner.poll_close(cx).map_err(TransportError::Websocket)
}
}
}
}
Loading
Loading