Skip to content
Draft
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
51 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
417964b
feat: websocket abstractions
Totodore Jul 12, 2026
e0f8cb9
fix: tests
Totodore Jul 12, 2026
ce741ed
fix: are u awake?
Totodore Jul 12, 2026
42cafa9
fix:
Totodore Jul 12, 2026
a1d530c
fix:
Totodore Jul 12, 2026
13f844f
fix:
Totodore Jul 14, 2026
6f55215
fix:
Totodore Jul 14, 2026
dd3362d
fix:
Totodore Jul 14, 2026
af24c5a
test: engine poll
Totodore Jul 18, 2026
e1d6993
test: engine poll
Totodore Jul 18, 2026
d61251a
test: engine poll
Totodore Jul 18, 2026
9995f82
test: engine poll
Totodore Jul 18, 2026
4a2842f
test: engine poll
Totodore Jul 18, 2026
51971e5
test: engine poll
Totodore Jul 18, 2026
7b3b070
Merge branch 'main' into engineioxide-client-impl
Totodore Jul 18, 2026
b34c38b
test: engine poll
Totodore Jul 18, 2026
d4c6618
test: engine poll
Totodore Jul 18, 2026
7d2a979
test: engine poll
Totodore Jul 18, 2026
7b70c1f
test: engine poll
Totodore Jul 18, 2026
da522e9
feat(config): improve config ergonomics
Totodore Jul 18, 2026
b3b7de4
test: test suite
Totodore Jul 18, 2026
b85c480
test: test suite
Totodore Jul 18, 2026
ded8161
test: test suite
Totodore Jul 18, 2026
7c83cdb
config: better config dx
Totodore Jul 19, 2026
39028c3
test: closing
Totodore Jul 19, 2026
c5a156c
test: closing
Totodore Jul 19, 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,467 changes: 755 additions & 712 deletions Cargo.lock

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions crates/engineioxide-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[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"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
engineioxide = { path = "../engineioxide", features = ["tracing", "v3"] }

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

use engineioxide_core::{Packet, PacketParseError, Sid};
use futures_core::Stream;
use futures_util::{
Sink, StreamExt,
stream::{SplitSink, SplitStream},
};

use crate::{
HttpClient,
transport::{Transport, polling::PollingSvc},
};

#[derive(Debug, thiserror::Error)]
pub enum ClientError<S: PollingSvc> {
#[error("packet parse error")]
PacketParse(#[from] PacketParseError),
#[error("transport error")]
Transport(S::Error),
}

pin_project_lite::pin_project! {
pub struct Client<S: PollingSvc> {
#[pin]
pub transport_rx: SplitStream<Transport<S>>,
#[pin]
pub transport_tx: SplitSink<Transport<S>, Packet>,

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

impl<S: PollingSvc> Client<S>
where
S::Error: fmt::Debug,
<S::Body as http_body::Body>::Error: fmt::Debug,
{
pub async fn connect(svc: S) -> Result<Self, PacketParseError> {
let (inner, open) = HttpClient::connect(svc).await?;
let transport = Transport::Polling { inner };
let (transport_tx, transport_rx) = transport.split();
let client = Client {
transport_tx,
transport_rx,
sid: open.sid,

should_flush: false,
should_send_pong: false,
};

Ok(client)
}
}
impl<S: PollingSvc> Client<S>
where
S::Error: fmt::Debug,
<S::Body as http_body::Body>::Error: fmt::Debug,
{
#[tracing::instrument(skip(cx))]
fn heartbeat(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), S::Error>> {
let mut this = self.project();
if let Err(e) = ready!(this.transport_tx.as_mut().poll_ready(cx)) {
return Poll::Ready(Err(e));
}
if let Err(e) = this.transport_tx.as_mut().start_send(Packet::Pong) {
return Poll::Ready(Err(e));
}

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

#[tracing::instrument(skip(cx))]
fn flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), S::Error>> {
let mut this = self.project();
if let Err(e) = ready!(this.transport_tx.as_mut().poll_flush(cx)) {
return Poll::Ready(Err(e));
}
*this.should_flush = false;
Poll::Ready(Ok(()))
}
}

impl<S: PollingSvc> Stream for Client<S>
where
S::Error: fmt::Debug,
<S::Body as http_body::Body>::Error: fmt::Debug,
{
type Item = Result<Packet, ClientError<S>>;

#[tracing::instrument(skip(cx))]
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if self.should_send_pong {
//TODO: ret err
if let Err(e) = ready!(self.as_mut().heartbeat(cx)) {
return Poll::Ready(Some(Err(ClientError::Transport(e))));
}
}

if self.should_flush {
//TODO: ret err
if let Err(e) = ready!(self.as_mut().flush(cx)) {
return Poll::Ready(Some(Err(ClientError::Transport(e))));
}
}

match ready!(self.as_mut().project().transport_rx.poll_next(cx)) {
Some(Ok(Packet::Ping)) => {
if self.as_mut().heartbeat(cx).is_pending() {
self.should_send_pong = true;
}
self.poll_next(cx)
}
Some(Ok(packet)) => Poll::Ready(Some(Ok(packet))),
Some(Err(e)) => Poll::Ready(Some(Err(e.into()))),
None => Poll::Ready(None),
}
}
}

impl<S: PollingSvc> Sink<Packet> for Client<S>
where
S::Error: fmt::Debug,
<S::Body as http_body::Body>::Error: fmt::Debug,
{
type Error = <Transport<S> as Sink<Packet>>::Error;

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

fn start_send(self: Pin<&mut Self>, item: Packet) -> Result<(), Self::Error> {
self.project().transport_tx.start_send(item)
}

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

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

impl<S: PollingSvc> fmt::Debug for Client<S> {
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()
}
}
1 change: 1 addition & 0 deletions crates/engineioxide-client/src/io.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

15 changes: 15 additions & 0 deletions crates/engineioxide-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#![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 io;
mod transport;
pub use crate::client::Client;
pub use crate::transport::polling::HttpClient;
81 changes: 81 additions & 0 deletions crates/engineioxide-client/src/transport/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use std::{
fmt,
pin::Pin,
task::{Context, Poll},
};

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

use crate::{HttpClient, transport::polling::PollingSvc};

pub mod polling;

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

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

impl<S: PollingSvc> Stream for Transport<S>
where
S::Error: fmt::Debug,
<S::Body as http_body::Body>::Error: fmt::Debug,
{
type Item = Result<Packet, PacketParseError>;
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),
TransportProj::Websocket { inner } => inner.poll_next(cx),
}
}
}
impl<S: PollingSvc> Sink<Packet> for Transport<S> {
type Error = <HttpClient<S> as Sink<Packet>>::Error;

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

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

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

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