Skip to content

feat: implement support for volatile events with new VolatileSocket w…#744

Open
LiamCarPer wants to merge 10 commits into
Totodore:mainfrom
LiamCarPer:feat/volatile-flag
Open

feat: implement support for volatile events with new VolatileSocket w…#744
LiamCarPer wants to merge 10 commits into
Totodore:mainfrom
LiamCarPer:feat/volatile-flag

Conversation

@LiamCarPer

Copy link
Copy Markdown

Motivation

Socket.io's Node.js implementation supports a volatile flag on emits that drops events when the underlying connection is not ready. This is highly useful for high-frequency, non-critical data like game position updates or telemetry, as it prevents buffer buildup on unstable connections.

This feature was requested in #602 and is documented in the [Socket.io volatile events spec](https://www.google.com/search?q=https://socket.io/docs/v4/emitting-events/%23volatile-events). Currently, socketioxide has no equivalent, forcing users to either accept buffer pressure or implement their own messaging layer.


Solution

Added a Volatile variant to BroadcastFlags (0x04) that flows through the existing adapter broadcast pipeline. On the local adapter, when volatile is set, errors from send_many are silently discarded rather than propagated, perfectly matching fire-and-forget semantics.

User-Facing API

Three entry points are provided to mirror the Node.js patterns:

  • Socket::volatile() Returns a VolatileSocket<'a, A> wrapper with a direct emit() that silently drops events when the socket is disconnected, the internal buffer is full, or encoding fails. The wrapper also exposes chain methods (to, within, except, local, broadcast, timeout) that delegate to BroadcastOperators with the volatile flag pre-set.
  • **BroadcastOperators::volatile() and ConfOperators::volatile()** Sets the flag on the operator chain, enabling room-based broadcasting patterns like:
io.to("room").volatile().emit(...);
  • SocketIo::volatile() A convenience alias on the default namespace for global emits:
io.volatile().emit(...);

Adapter Propagation

The flag propagates directly through the BroadcastOptions struct. This ensures remote adapters (Redis, Postgres, MongoDB) receive it and can handle volatile semantics on their own nodes out of the box, without requiring any adapter-specific changes.

@codspeed-hq

codspeed-hq Bot commented Jun 20, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 87 untouched benchmarks


Comparing LiamCarPer:feat/volatile-flag (804212c) with main (6f0774a)

Open in CodSpeed

@Totodore

Copy link
Copy Markdown
Owner

Please check this comment regarding the implementation:
#602 (comment)

you are missing a way to bypass the mpsc channel in engineio. Volatile packets will still be buffered, it is not what we want.

I might be wrong but, before Claude-ing something please try to understand the issue details before implementing something. Feel free to ask more details here or on the issue.

@LiamCarPer

Copy link
Copy Markdown
Author

Thanks for the Review. You are right, my current approach still goes through the mpcs channel so volatile packets can buffer.
i am planning to add a separate volatile send path in engineio, a second mpsc channel on Socket with capacity 1, paired with a send_volatile() method that does try _send iton it (drops if full). Then update both transporters to drain it with priority. Then thread the volatile flag BroadcastOptions through send_many so individual socket sends in broadcast flows also use the volatile engine.io path.

Does that sound good? Want to make sure i am in the right track before spending more time on it

@LiamCarPer

Copy link
Copy Markdown
Author

Is this what you were looking for?

@Totodore Totodore added A-socketioxide Area related to socketioxide A-engineioxide Area related to engineioxide C-Feature-request Request for a feature labels Jun 21, 2026
@Totodore

Copy link
Copy Markdown
Owner

Yes! One thing that we are missing with this solution is that volatile packets are out of order with classic packets. It might be ok though. We can simply document this specific case.
It would be nice to check what are the ordering constraints in the js implementation though and explore if there is any solution for this. I'll review this whenever I can.

@LiamCarPer

Copy link
Copy Markdown
Author

Perfect, let me look into it

@LiamCarPer

Copy link
Copy Markdown
Author

I checked js docs and they dont specify ordering constraints for volatile vs regular event.
I have added doc comments on both VolatileSocket and the engine.io emit.volatile() methods.

@Totodore Totodore left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the good direction!
We need to rethink how the polling payload encoding is done though. You can pick some changes from #555 regarding the use of Stream in the encoder.

Comment thread crates/engineioxide/src/transport/polling/mod.rs Outdated
Comment thread crates/engineioxide/Cargo.toml Outdated
Comment thread crates/engineioxide/src/socket.rs
Comment thread crates/socketioxide/src/operators.rs Outdated
Comment thread crates/socketioxide/src/socket.rs Outdated
Comment thread crates/socketioxide/src/socket.rs
@LiamCarPer LiamCarPer force-pushed the feat/volatile-flag branch from 90575ac to 8861111 Compare June 26, 2026 09:53
@LiamCarPer

Copy link
Copy Markdown
Author

Hey man, sorry for being this late ran out of claude usage, jajjajaj just joking, was busy with work.

I removed VolatileSocket, used ConfOperators with volatile flag, removed tokio macros feature, documented return values, moved volatile docs to external file. Test results, 79/79 unit tests, 106/109 doc tests, 0 clippy warnings, clean.

You had a concern about polling encoder, there are no .await points between the volatile snapshot and the encoder call, so the concern doesnt apply to the specific code,

Check and tell me is everything as you want it!

… polling in encoders to capture packets arriving during encoding

@Totodore Totodore left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, it is good, however we are missing integration tests for engineioxide and socketioxide and unit tests for the encoder functions in engineioxide.
We need to test every combination possible of volatile vs non-volatile packets to ensure payloads are still correct.

@LiamCarPer

Copy link
Copy Markdown
Author

I went ahead and added a bunch of comprehensive test coverage.

For the encoder unit tests in encoder.rs, I added 10 new tests checking the core logic for all three versions (v4, v3 string, and v3 binary). That covers volatile-only payloads, mixed normal and volatile ordering both ways, making sure the latest volatile data overwrites the old stuff, and testing what happens if volatile data drops in right while a buffer is draining.

Then for engineioxide integration tests in tests/volatile.rs, I added 3 tests to check the transport layer. It confirms volatile messages make it all the way through the full polling transport, double-checks the ordering when mixing volatile and normal messages, and makes sure those overwrite semantics hold up at the engine level too.

Lastly for socketioxide integration tests in tests/volatile.rs, i added 4 tests for the user-facing API and broadcasting. It ensures socket.volatile().emit() returns Ok(()), makes sure broadcasting with the volatile flag on doesn't panic, and checks that io.volatile() plays nice with the rest of our tools.

I have left two edge cases uncovered, Parked encoder + volatile arrives and Max payload boundary with volatile. What do you want me to do with it?

@Totodore Totodore left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tests, yes it would be nice to add parked encoder and max payload checks (especially because these are edge cases that may easily results in bugs).

Comment thread crates/engineioxide/src/transport/ws.rs Outdated
@LiamCarPer

Copy link
Copy Markdown
Author

I handled the WS flush consolidation in crates/engineioxide/src/transport/ws.rs. I removed that double-flush pattern where it was flushing once after the main channel drain and then again after volatile. Now there is just a single unconditional tx.flush().await.ok() at the end of each loop iteration, so it processes both the main and volatile packets before doing a single flush.

I also added 3 new parked encoder tests. These cover scenarios where volatile arrives while the encoder is blocked on recv_packet() and confirms that volatile isn't captured in that specific payload, only on the next poll. I explicitly tested this across v4, v3 string, and v3 binary encoders.

I added 3 new max payload volatile tests. These make sure that if volatile pushes data.len() over the limit, it correctly leaves the normal packets in the channel for the next poll. Like the others, this is fully tested for v4, v3 string, and v3 binary.

@Totodore

Copy link
Copy Markdown
Owner

I cant make it work on the whiteboard example, no events is being emitted. Try on your side to emit the drawing event with volatile and check if you get something. Same if I force polling transport, it doesn't work. Once it works with the whiteboard example check also with remote adapters (redis/postgres to find the issue).

Once you find the bug in the whiteboard example, please add regression tests.

Tip:
Maybe add more logs to track engineioxide behaviors, you can check with RUST_LOG=socketioxide=trace,engineioxide=trace.

@LiamCarPer

Copy link
Copy Markdown
Author

Found it, volatile packets arriving while the encoder is parked on recv_packet() were not captured. The encoder checks volatile in the initial check and in the drain loop, but after parking, volatile arriving during that .await is missed, it only gets captured on the next poll.

I have added new regression tests "volatile_broadcast_arrives_via_polling_transport".

Thank you for the tip, it was useful

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-engineioxide Area related to engineioxide A-socketioxide Area related to socketioxide C-Feature-request Request for a feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants