lightyear_link/lib.rs
1//! Transport-agnostic link buffers and link lifecycle markers.
2//!
3//! A [`Link`] is Lightyear's transport-neutral boundary between higher-level networking
4//! systems and concrete IO backends. Protocol, connection, replication, and message systems
5//! read from and write to [`Link`] buffers; transport crates such as `lightyear_udp`,
6//! `lightyear_webtransport`, `lightyear_websocket`, `lightyear_steam`, and
7//! `lightyear_crossbeam` are responsible for moving byte payloads across an actual network or
8//! in-process channel.
9//!
10//! The crate deliberately keeps the IO abstraction narrow:
11//! - [`RecvPayload`] and [`SendPayload`] are opaque byte payloads.
12//! - [`LinkReceiver`] buffers payloads received from a transport until higher-level systems
13//! consume them.
14//! - [`LinkSender`] buffers payloads produced by higher-level systems until a transport flushes
15//! them.
16//! - [`LinkConditioner`] can delay or drop inbound payloads to simulate imperfect networks.
17//! - [`Linking`], [`Linked`], and [`Unlinked`] are mutually exclusive ECS marker components that
18//! keep [`Link::state`] synchronized with the entity lifecycle.
19//!
20//! Server-side fan-out relationships live in [`server`].
21#![no_std]
22
23extern crate alloc;
24#[cfg(feature = "std")]
25extern crate std;
26
27mod conditioner;
28mod mtu;
29pub mod server;
30
31use alloc::{collections::vec_deque::Drain, string::String};
32
33pub use crate::conditioner::LinkConditioner;
34pub use crate::mtu::{DEFAULT_MTU, LinkMtu, MtuTooSmall};
35use alloc::collections::VecDeque;
36use bevy_app::{App, Plugin, PostUpdate, PreUpdate};
37use bevy_ecs::lifecycle::HookContext;
38use bevy_ecs::prelude::*;
39use bevy_ecs::world::DeferredWorld;
40use bevy_reflect::Reflect;
41use bytes::{Bytes, BytesMut};
42use core::time::Duration;
43use lightyear_core::time::Instant;
44use lightyear_utils::adaptive_for_each_mut;
45
46pub mod prelude {
47 pub use crate::conditioner::{LinkConditionerConfig, LinkConditionerState};
48 pub use crate::server::{LinkOf, Server};
49 pub use crate::{
50 DEFAULT_MTU, Link, LinkMtu, LinkStart, LinkStats, LinkSystems, Linked, Linking,
51 MtuTooSmall, RecvLinkConditioner, Unlink, UnlinkReason, Unlinked,
52 };
53
54 pub mod server {
55 pub use crate::server::{LinkOf, Server};
56 }
57}
58
59/// Mutable byte payload received from a transport.
60///
61/// A transport pushes this payload into [`LinkReceiver`] after decoding any transport-specific
62/// envelope. Keeping receive payloads mutable lets connection layers decrypt in place without
63/// first trying to recover mutable ownership from an immutable [`Bytes`] handle. Higher-level
64/// Lightyear systems can freeze the payload once they need cheap immutable subslices.
65pub type RecvPayload = BytesMut;
66
67/// Opaque byte payload queued for a transport to send.
68///
69/// Higher-level Lightyear systems enqueue this payload through [`Link::send`] or [`LinkSender`].
70/// A transport drains [`LinkSender`] in [`LinkSystems::Send`] and writes the bytes to its concrete
71/// IO backend.
72pub type SendPayload = Bytes;
73
74/// Converts an immutable transport payload into Lightyear's mutable receive payload.
75///
76/// Some IO APIs, including Aeronet and Crossbeam, expose received packets as [`Bytes`]. This
77/// conversion reuses the allocation when that handle is uniquely owned and copies only when the
78/// IO backend or sender still holds another reference. IO backends that already receive into a
79/// [`BytesMut`] should push it directly instead of calling this function.
80pub fn recv_payload_from_bytes(payload: Bytes) -> RecvPayload {
81 match payload.try_into_mut() {
82 Ok(payload) => payload,
83 Err(payload) => BytesMut::from(payload),
84 }
85}
86
87/// Current lifecycle state of a [`Link`].
88///
89/// This enum mirrors the mutually exclusive marker components [`Linking`], [`Linked`], and
90/// [`Unlinked`]. User code usually inserts the marker components rather than mutating this value
91/// directly, because the marker hooks also remove the other lifecycle markers.
92#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
93pub enum LinkState {
94 /// The link is established and can exchange payloads.
95 Linked,
96 /// The link is being established by the transport.
97 Linking,
98 /// The link is not connected to a remote peer.
99 #[default]
100 Unlinked,
101}
102
103/// Transport-neutral byte stream between two peers.
104///
105/// `Link` is an ECS component owned by the entity that represents a local transport endpoint or a
106/// remote peer. It only stores buffered payloads and lifecycle/statistics state; concrete IO is
107/// handled by transport-specific components in crates such as `lightyear_udp`,
108/// `lightyear_crossbeam`, `lightyear_webtransport`, or `lightyear_steam`.
109///
110/// Incoming bytes flow: transport -> [`LinkReceiver`] -> higher-level systems.
111/// Outgoing bytes flow: higher-level systems -> [`LinkSender`] -> transport.
112#[derive(Component, Default)]
113pub struct Link {
114 /// Payloads received from the transport and waiting to be consumed by Lightyear systems.
115 pub recv: LinkReceiver,
116 /// Payloads produced by Lightyear systems and waiting to be flushed by the transport.
117 pub send: LinkSender,
118 /// Cached lifecycle state mirrored from [`Linking`], [`Linked`], or [`Unlinked`].
119 pub state: LinkState,
120 /// Transport-observed statistics for this link.
121 pub stats: LinkStats,
122 /// Minimum and current maximum payload sizes exposed by the concrete link.
123 mtu: LinkMtu,
124}
125
126/// Packet conditioner used for inbound [`RecvPayload`] values.
127///
128/// For symmetric simulations, construct two links with matching or split
129/// [`prelude::LinkConditionerConfig`] values.
130pub type RecvLinkConditioner = LinkConditioner<RecvPayload>;
131
132impl Link {
133 /// Configures the receive-side network conditioner.
134 ///
135 /// Accepts either a [`RecvLinkConditioner`] or an `Option<RecvLinkConditioner>`, which makes
136 /// it convenient to forward optional application configuration.
137 pub fn with_conditioner(
138 mut self,
139 recv_conditioner: impl Into<Option<RecvLinkConditioner>>,
140 ) -> Self {
141 self.recv.conditioner = recv_conditioner.into();
142 self
143 }
144
145 /// Configures the link's minimum and current MTU characteristics.
146 ///
147 /// This is intended for constructing a link. Once constructed, only the current MTU can be
148 /// changed through [`set_mtu`](Self::set_mtu); the minimum MTU remains stable.
149 pub fn with_mtu(mut self, mtu: LinkMtu) -> Self {
150 self.mtu = mtu;
151 self
152 }
153
154 /// Returns the link's current maximum payload size.
155 pub const fn mtu(&self) -> usize {
156 self.mtu.mtu()
157 }
158
159 /// Returns the stable minimum MTU configured when this link was constructed.
160 pub const fn min_mtu(&self) -> usize {
161 self.mtu.min_mtu()
162 }
163
164 /// Updates the current MTU without changing the link's stable minimum MTU.
165 pub const fn set_mtu(&mut self, mtu: usize) -> Result<(), MtuTooSmall> {
166 self.mtu.set_mtu(mtu)
167 }
168}
169
170/// Receive-side payload queue for a [`Link`].
171///
172/// Transports push network payloads into this queue, and higher-level Lightyear systems drain or
173/// pop them during [`LinkSystems::Receive`].
174///
175/// If [`conditioner`](Self::conditioner) is present,
176/// [`push`](Self::push) routes packets through [`LinkConditioner`] before they become visible in
177/// the buffer.
178#[derive(Default)]
179pub struct LinkReceiver {
180 buffer: VecDeque<RecvPayload>,
181 /// Optional receive-side link conditioner for latency, jitter, and packet-loss simulation.
182 pub conditioner: Option<LinkConditioner<RecvPayload>>,
183}
184
185impl LinkReceiver {
186 /// Drains every currently available received payload in FIFO order.
187 ///
188 /// Conditioned packets that are not ready yet remain in the [`LinkConditioner`] and are not
189 /// yielded by this iterator.
190 pub fn drain(&mut self) -> Drain<'_, RecvPayload> {
191 self.buffer.drain(..)
192 }
193
194 /// Removes and returns the oldest available received payload.
195 ///
196 /// Returns `None` when the receive buffer is empty.
197 pub fn pop(&mut self) -> Option<RecvPayload> {
198 self.buffer.pop_front()
199 }
200
201 /// Appends a received payload directly to the available buffer.
202 ///
203 /// This bypasses [`conditioner`](Self::conditioner). Transport code should use this when it is
204 /// replaying already-conditioned data, injecting test packets, or implementing an IO backend
205 /// that intentionally does not simulate network conditions.
206 pub fn push_raw(&mut self, value: RecvPayload) {
207 self.buffer.push_back(value);
208 }
209
210 /// Appends a received payload, applying the configured link conditioner if present.
211 ///
212 /// `instant` is the local receive time used as the base timestamp for simulated latency and
213 /// jitter. When no conditioner is configured, this is equivalent to [`push_raw`](Self::push_raw).
214 pub fn push(&mut self, value: RecvPayload, instant: Instant) {
215 if let Some(conditioner) = &mut self.conditioner {
216 conditioner.condition_packet(value, instant);
217 } else {
218 self.push_raw(value);
219 }
220 }
221
222 /// Returns the number of payloads currently available to consumers.
223 ///
224 /// Packets still delayed inside [`conditioner`](Self::conditioner) are not included.
225 pub fn len(&self) -> usize {
226 self.buffer.len()
227 }
228
229 /// Iterates over the currently available received payloads without consuming them.
230 #[cfg(feature = "test_utils")]
231 pub fn iter(&self) -> impl Iterator<Item = &RecvPayload> {
232 self.buffer.iter()
233 }
234}
235
236/// Send-side payload queue for a [`Link`].
237///
238/// Higher-level systems enqueue payloads here. Transport plugins drain the queue during
239/// [`LinkSystems::Send`] and write each [`SendPayload`] to their concrete IO backend.
240#[derive(Default)]
241pub struct LinkSender(VecDeque<SendPayload>);
242
243impl LinkSender {
244 /// Drains every queued outgoing payload in FIFO order.
245 ///
246 /// Transport systems typically call this in [`LinkSystems::Send`] once they are ready to flush
247 /// all pending packets for the frame or tick.
248 pub fn drain(&mut self) -> Drain<'_, SendPayload> {
249 self.0.drain(..)
250 }
251
252 /// Removes and returns the oldest queued outgoing payload.
253 ///
254 /// This is useful for transports that send one packet at a time or need to requeue a packet
255 /// with [`push_front`](Self::push_front) if the backend reports backpressure.
256 pub fn pop(&mut self) -> Option<SendPayload> {
257 self.0.pop_front()
258 }
259
260 /// Appends an outgoing payload to the back of the FIFO queue.
261 pub fn push(&mut self, value: SendPayload) {
262 self.0.push_back(value)
263 }
264
265 /// Prepends an outgoing payload to the front of the queue.
266 pub fn push_front(&mut self, value: SendPayload) {
267 self.0.push_front(value)
268 }
269
270 /// Returns the number of outgoing payloads waiting to be flushed.
271 pub fn len(&self) -> usize {
272 self.0.len()
273 }
274
275 /// Iterates over queued outgoing payloads without consuming them.
276 #[cfg(feature = "test_utils")]
277 pub fn iter(&self) -> impl Iterator<Item = &SendPayload> {
278 self.0.iter()
279 }
280}
281
282impl Link {
283 /// Queues an outgoing payload for the transport layer.
284 ///
285 /// This is the high-level convenience wrapper around [`LinkSender::push`]. It does not perform
286 /// serialization, reliability, fragmentation, encryption, or IO; those responsibilities live
287 /// in higher-level protocol crates and concrete transport crates.
288 pub fn send(&mut self, payload: SendPayload) {
289 self.send.push(payload);
290 }
291}
292
293/// Transport-observed statistics for a [`Link`].
294///
295/// These values are intentionally lightweight and transport-defined. Higher-level diagnostics can
296/// combine them with replication/message metrics from other Lightyear crates.
297#[derive(Default, Debug, Clone, Copy)]
298pub struct LinkStats {
299 /// Estimated round-trip time for this link.
300 pub rtt: Duration,
301 /// Estimated variation in packet delay for this link.
302 pub jitter: Duration,
303}
304
305#[deprecated(note = "Use LinkSystems instead")]
306/// Deprecated alias for [`LinkSystems`].
307pub type LinkSet = LinkSystems;
308
309/// System sets for `Link`-related operations.
310///
311/// These are used to order systems that handle:
312/// - Receiving data from the IO layer into the `Link` buffer.
313/// - Applying link conditioning to received packets.
314/// - Sending data from the `Link` buffer to the IO layer.
315#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone, Copy)]
316pub enum LinkSystems {
317 // PreUpdate
318 /// Receive bytes from IO backends and make them available through [`LinkReceiver`].
319 Receive,
320
321 // PostUpdate
322 /// Flush queued [`SendPayload`] values from [`LinkSender`] to IO backends.
323 Send,
324}
325
326#[deprecated(note = "Use LinkReceiveSystems instead")]
327/// Deprecated alias for [`LinkReceiveSystems`].
328pub type LinkReceiveSet = LinkReceiveSystems;
329
330/// System sets that make up [`LinkSystems::Receive`].
331///
332/// Transport plugins should put their raw receive systems in [`BufferToLink`](Self::BufferToLink).
333/// [`LinkPlugin`] runs [`ApplyConditioner`](Self::ApplyConditioner) afterwards so higher-level
334/// systems see only packets whose simulated delay has elapsed.
335#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone, Copy)]
336pub enum LinkReceiveSystems {
337 /// Receive bytes from IO and push them into [`LinkReceiver`].
338 ///
339 /// If the receiver has a [`LinkConditioner`], transport systems should call
340 /// [`LinkReceiver::push`] so the packet is delayed or dropped before it becomes available.
341 BufferToLink,
342 /// Move ready packets from [`LinkConditioner`] into [`LinkReceiver`].
343 ApplyConditioner,
344}
345
346/// Entity event requesting that a transport start establishing a [`Link`].
347///
348/// `LinkStart` is transport-facing: A transport plugin observes this event for its
349/// own link entities and inserts [`Linking`] or [`Linked`] when the connection progresses.
350#[derive(EntityEvent)]
351pub struct LinkStart {
352 /// Entity that owns the [`Link`] to start.
353 pub entity: Entity,
354}
355
356/// Why a [`Link`] was unlinked via [`Unlink`] or [`Unlinked`].
357#[derive(Default, Debug, Clone, PartialEq, Eq, Reflect)]
358pub enum UnlinkReason {
359 /// The link has not yet been established.
360 #[default]
361 Initial,
362 /// The local user requested the unlink, optionally with additional context.
363 UserRequested(Option<String>),
364 /// The server stopped and closed its links.
365 ServerStopped,
366 /// The remote peer closed the link and supplied a reason.
367 ByPeer(String),
368 /// The transport encountered an error and can no longer communicate with the peer.
369 TransportError(String),
370}
371
372impl core::fmt::Display for UnlinkReason {
373 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
374 match self {
375 Self::Initial => f.write_str("Not connected"),
376 Self::UserRequested(Some(reason)) => write!(f, "User requested: {reason}"),
377 Self::UserRequested(None) => f.write_str("User requested"),
378 Self::ServerStopped => f.write_str("Server stopped"),
379 Self::ByPeer(reason) => write!(f, "Disconnected by peer: {reason}"),
380 Self::TransportError(reason) => write!(f, "Transport error: {reason}"),
381 }
382 }
383}
384
385/// Entity event requesting that a transport terminate a [`Link`].
386///
387/// [`LinkPlugin`] observes this event and inserts [`Unlinked`] with the provided reason. Concrete
388/// transports can also observe it to close sockets, sessions, streams, or in-process channels.
389#[derive(EntityEvent, Clone, Debug)]
390pub struct Unlink {
391 /// Entity that owns the [`Link`] to terminate.
392 #[event_target]
393 pub entity: Entity,
394 /// Structured reason propagated to [`Unlinked::reason`].
395 pub reason: UnlinkReason,
396}
397
398/// Marker component for a link whose transport connection is being established.
399///
400/// Inserting this component updates [`Link::state`] to [`LinkState::Linking`] and removes
401/// [`Linked`] and [`Unlinked`]. If [`Linked`] is inserted in the same frame first, the hook leaves
402/// the link linked to avoid regressing a completed connection back to the in-progress state.
403#[derive(Component, Default, Debug)]
404#[component(on_insert = Linking::on_insert)]
405pub struct Linking;
406
407impl Linking {
408 fn on_insert(mut world: DeferredWorld, context: HookContext) {
409 // If `Linked` got inserted at the same frame right after `Linking`, we don't want to
410 // change the state or remove the `Linked` component.
411 if world.get::<Linked>(context.entity).is_some() {
412 return;
413 }
414 if let Some(mut link) = world.get_mut::<Link>(context.entity) {
415 link.state = LinkState::Linking;
416 }
417 world
418 .commands()
419 .entity(context.entity)
420 .remove::<(Linked, Unlinked)>();
421 }
422}
423
424/// Marker component for an established link.
425///
426/// Inserting this component updates [`Link::state`] to [`LinkState::Linked`] and removes
427/// [`Linking`] and [`Unlinked`].
428#[derive(Component, Default, Debug)]
429#[component(on_insert = Linked::on_insert)]
430pub struct Linked;
431
432impl Linked {
433 fn on_insert(mut world: DeferredWorld, context: HookContext) {
434 if let Some(mut link) = world.get_mut::<Link>(context.entity) {
435 link.state = LinkState::Linked;
436 }
437 world
438 .commands()
439 .entity(context.entity)
440 .remove::<(Linking, Unlinked)>();
441 }
442}
443
444/// Marker component for a link that is not connected.
445///
446/// Inserting this component updates [`Link::state`] to [`LinkState::Unlinked`] and removes
447/// [`Linked`] and [`Linking`]. The [`reason`](Self::reason) is intended for diagnostics and for
448/// transports that need to surface disconnect causes to application code.
449#[derive(Component, Default, Debug)]
450#[component(on_insert = Unlinked::on_insert)]
451pub struct Unlinked {
452 /// Structured disconnect or initial-state reason.
453 pub reason: UnlinkReason,
454}
455
456impl Unlinked {
457 fn on_insert(mut world: DeferredWorld, context: HookContext) {
458 if let Some(mut link) = world.get_mut::<Link>(context.entity) {
459 link.state = LinkState::Unlinked;
460 }
461 world
462 .commands()
463 .entity(context.entity)
464 .remove::<(Linked, Linking)>();
465 }
466}
467
468/// Bevy plugin that installs link lifecycle and receive-buffer systems.
469///
470/// This plugin configures system sets for:
471/// - receiving data into [`Link`] buffers via [`LinkSystems::Receive`];
472/// - applying receive-side link conditioning via [`LinkReceiveSystems::ApplyConditioner`];
473/// - sending data from [`Link`] buffers via [`LinkSystems::Send`].
474///
475/// Concrete transport plugins normally add this plugin, then schedule their IO systems inside
476/// [`LinkReceiveSystems::BufferToLink`] and [`LinkSystems::Send`].
477pub struct LinkPlugin;
478
479impl LinkPlugin {
480 /// Moves ready conditioned packets into each link's receive buffer.
481 ///
482 /// [`LinkReceiver::push`] stores packets in [`LinkConditioner`] when conditioning is enabled.
483 /// This system polls those conditioners against [`Instant::now`] and appends packets whose
484 /// simulated delivery time has elapsed. It is installed in
485 /// [`LinkReceiveSystems::ApplyConditioner`] by [`LinkPlugin`].
486 pub fn apply_link_conditioner(mut query: Query<&mut Link>) {
487 let query = adaptive_for_each_mut!(query);
488 query.for_each(|mut link| {
489 // enable split borrows
490 let recv = &mut link.recv;
491 if let Some(conditioner) = &mut recv.conditioner {
492 while let Some(packet) = conditioner.pop_packet(Instant::now()) {
493 // cannot use push_raw() because of partial borrows issue
494 recv.buffer.push_back(packet);
495 }
496 }
497 });
498 }
499
500 /// Handles [`Unlink`] requests by inserting [`Unlinked`].
501 fn unlink(mut unlink: On<Unlink>, mut commands: Commands) {
502 if let Ok(mut c) = commands.get_entity(unlink.entity) {
503 c.insert(Unlinked {
504 reason: core::mem::take(&mut unlink.reason),
505 });
506 }
507 }
508}
509
510impl Plugin for LinkPlugin {
511 fn build(&self, app: &mut App) {
512 app.add_systems(
513 PreUpdate,
514 Self::apply_link_conditioner.in_set(LinkReceiveSystems::ApplyConditioner),
515 );
516 app.configure_sets(
517 PreUpdate,
518 (
519 LinkReceiveSystems::BufferToLink,
520 LinkReceiveSystems::ApplyConditioner,
521 )
522 .in_set(LinkSystems::Receive)
523 .chain(),
524 );
525 app.configure_sets(PostUpdate, LinkSystems::Send);
526
527 app.add_observer(Self::unlink);
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 #[test]
536 fn immutable_receive_payload_reuses_unique_allocation() {
537 let bytes = Bytes::from(alloc::vec![1, 2, 3]);
538 let allocation = bytes.as_ptr();
539
540 let payload = recv_payload_from_bytes(bytes);
541
542 assert_eq!(payload.as_ptr(), allocation);
543 }
544
545 #[test]
546 fn immutable_receive_payload_copies_shared_allocation() {
547 let bytes = Bytes::from(alloc::vec![1, 2, 3]);
548 let shared = bytes.clone();
549 let allocation = bytes.as_ptr();
550
551 let payload = recv_payload_from_bytes(bytes);
552
553 assert_ne!(payload.as_ptr(), allocation);
554 assert_eq!(payload.as_ref(), shared.as_ref());
555 }
556
557 #[test]
558 fn explicit_link_mtu_does_not_change_link_owned_latency_stats() {
559 let mut link = Link::default().with_mtu(LinkMtu::new(512));
560 link.stats.rtt = Duration::from_millis(20);
561 link.stats.jitter = Duration::from_millis(3);
562
563 assert_eq!(link.mtu(), 512);
564 assert_eq!(link.min_mtu(), 512);
565 assert_eq!(link.stats.rtt, Duration::from_millis(20));
566 assert_eq!(link.stats.jitter, Duration::from_millis(3));
567 }
568
569 #[test]
570 fn link_builder_configures_conditioner_and_mtu() {
571 let conditioner =
572 RecvLinkConditioner::new(crate::conditioner::LinkConditionerConfig::default());
573 let link = Link::default()
574 .with_conditioner(conditioner)
575 .with_mtu(LinkMtu::new(512));
576
577 assert!(link.recv.conditioner.is_some());
578 assert_eq!(link.mtu(), 512);
579 assert_eq!(link.min_mtu(), 512);
580 }
581
582 #[test]
583 fn current_mtu_can_change_but_minimum_mtu_cannot() {
584 let mut link = Link::default().with_mtu(LinkMtu::new(512));
585
586 link.set_mtu(900).unwrap();
587 assert_eq!(link.mtu(), 900);
588 assert_eq!(link.min_mtu(), 512);
589
590 assert_eq!(link.set_mtu(511), Err(MtuTooSmall { mtu: 511, min: 512 }));
591 assert_eq!(link.mtu(), 900);
592 assert_eq!(link.min_mtu(), 512);
593 }
594}