Skip to main content

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}