0.3.4-alpha.2
This commit is contained in:
14
crates/common/game-realtime-transport-lib/Cargo.toml
Normal file
14
crates/common/game-realtime-transport-lib/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
# file: crates/common/game-realtime-transport-lib/Cargo.toml
|
||||
# version: 1
|
||||
|
||||
[package]
|
||||
name = "game-realtime-transport-lib"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
42
crates/common/game-realtime-transport-lib/src/connection.rs
Normal file
42
crates/common/game-realtime-transport-lib/src/connection.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
// file: crates/common/game-realtime-transport-lib/src/connection.rs
|
||||
// version: 1
|
||||
|
||||
/// Established realtime connection that owns independent send and receive halves.
|
||||
pub trait RealtimeConnection {
|
||||
/// Send half produced when this connection is split.
|
||||
type Sender: crate::RealtimeSender;
|
||||
/// Receive half produced when this connection is split.
|
||||
type Receiver: crate::RealtimeReceiver;
|
||||
|
||||
/// Consumes the connection and returns independent send and receive halves.
|
||||
fn split(self) -> (Self::Sender, Self::Receiver);
|
||||
}
|
||||
|
||||
/// Send half of an established realtime connection.
|
||||
pub trait RealtimeSender {
|
||||
/// Future returned by [`RealtimeSender::send`].
|
||||
type SendFuture<'a>: core::future::Future<Output = Result<(), crate::TransportError>>
|
||||
where
|
||||
Self: 'a;
|
||||
/// Future returned by [`RealtimeSender::close`].
|
||||
type CloseFuture<'a>: core::future::Future<Output = Result<(), crate::TransportError>>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
/// Sends one owned opaque binary message, respecting backend backpressure.
|
||||
fn send(&mut self, message: crate::TransportMessage) -> Self::SendFuture<'_>;
|
||||
|
||||
/// Initiates a clean local transport-level close.
|
||||
fn close(&mut self) -> Self::CloseFuture<'_>;
|
||||
}
|
||||
|
||||
/// Receive half of an established realtime connection.
|
||||
pub trait RealtimeReceiver {
|
||||
/// Future returned by [`RealtimeReceiver::receive`].
|
||||
type ReceiveFuture<'a>: core::future::Future<Output = Result<crate::TransportReceive, crate::TransportError>>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
/// Receives one binary message or observes a clean remote close.
|
||||
fn receive(&mut self) -> Self::ReceiveFuture<'_>;
|
||||
}
|
||||
82
crates/common/game-realtime-transport-lib/src/error.rs
Normal file
82
crates/common/game-realtime-transport-lib/src/error.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
// file: crates/common/game-realtime-transport-lib/src/error.rs
|
||||
// version: 1
|
||||
|
||||
/// Stable transport-neutral category for a realtime operation failure.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum TransportErrorKind {
|
||||
/// An endpoint or transport configuration is invalid.
|
||||
InvalidConfiguration,
|
||||
/// A client connection could not be established.
|
||||
Connect,
|
||||
/// A server endpoint could not be bound.
|
||||
Bind,
|
||||
/// A server could not accept an incoming connection.
|
||||
Accept,
|
||||
/// An operation exceeded its configured deadline.
|
||||
Timeout,
|
||||
/// A payload exceeds the configured transport bound.
|
||||
MessageTooLarge,
|
||||
/// The transport cannot currently accept more outbound data within its configured bounds.
|
||||
Backpressure,
|
||||
/// The requested operation requires a connection that is already closed.
|
||||
Closed,
|
||||
/// An underlying I/O operation failed.
|
||||
Io,
|
||||
/// The backend reported a protocol-level or transport-specific failure.
|
||||
Protocol,
|
||||
/// The operation was explicitly aborted or cancelled when that distinction is observable.
|
||||
Aborted,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for TransportErrorKind {
|
||||
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
return match self {
|
||||
crate::TransportErrorKind::InvalidConfiguration => formatter.write_str("invalid configuration"),
|
||||
crate::TransportErrorKind::Connect => formatter.write_str("connect failure"),
|
||||
crate::TransportErrorKind::Bind => formatter.write_str("bind failure"),
|
||||
crate::TransportErrorKind::Accept => formatter.write_str("accept failure"),
|
||||
crate::TransportErrorKind::Timeout => formatter.write_str("operation timed out"),
|
||||
crate::TransportErrorKind::MessageTooLarge => formatter.write_str("message too large"),
|
||||
crate::TransportErrorKind::Backpressure => formatter.write_str("transport backpressure"),
|
||||
crate::TransportErrorKind::Closed => formatter.write_str("connection closed"),
|
||||
crate::TransportErrorKind::Io => formatter.write_str("I/O failure"),
|
||||
crate::TransportErrorKind::Protocol => formatter.write_str("protocol failure"),
|
||||
crate::TransportErrorKind::Aborted => formatter.write_str("operation aborted"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Transport-neutral operation failure with a stable category and diagnostic detail.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TransportError {
|
||||
detail: String,
|
||||
kind: crate::TransportErrorKind,
|
||||
}
|
||||
|
||||
impl TransportError {
|
||||
/// Creates a transport failure from a stable category and backend-neutral diagnostic detail.
|
||||
#[must_use]
|
||||
pub fn new(kind: crate::TransportErrorKind, detail: impl Into<String>) -> Self {
|
||||
return Self { detail: detail.into(), kind };
|
||||
}
|
||||
|
||||
/// Borrows the diagnostic detail associated with the failure.
|
||||
#[must_use]
|
||||
pub fn detail(&self) -> &str {
|
||||
return self.detail.as_str();
|
||||
}
|
||||
|
||||
/// Returns the stable transport-neutral failure category.
|
||||
#[must_use]
|
||||
pub fn kind(&self) -> crate::TransportErrorKind {
|
||||
return self.kind;
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for TransportError {
|
||||
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
return write!(formatter, "{}: {}", self.kind, self.detail);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TransportError {}
|
||||
31
crates/common/game-realtime-transport-lib/src/lib.rs
Normal file
31
crates/common/game-realtime-transport-lib/src/lib.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
// file: crates/common/game-realtime-transport-lib/src/lib.rs
|
||||
// version: 1
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Transport-neutral realtime connection contracts for games.sasedev.
|
||||
|
||||
mod connection;
|
||||
mod error;
|
||||
mod message;
|
||||
|
||||
/// Re-export of an established realtime connection that can be split into independent halves.
|
||||
pub use self::connection::RealtimeConnection;
|
||||
/// Re-export of the receive half of an established realtime connection.
|
||||
pub use self::connection::RealtimeReceiver;
|
||||
/// Re-export of the send half of an established realtime connection.
|
||||
pub use self::connection::RealtimeSender;
|
||||
/// Re-export of a transport-neutral operation failure.
|
||||
pub use self::error::TransportError;
|
||||
/// Re-export of transport-neutral error categories.
|
||||
pub use self::error::TransportErrorKind;
|
||||
/// Re-export of an owned opaque binary transport message.
|
||||
pub use self::message::TransportMessage;
|
||||
/// Re-export of the result of one receive operation.
|
||||
pub use self::message::TransportReceive;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/contract.rs"]
|
||||
mod tests;
|
||||
49
crates/common/game-realtime-transport-lib/src/message.rs
Normal file
49
crates/common/game-realtime-transport-lib/src/message.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
// file: crates/common/game-realtime-transport-lib/src/message.rs
|
||||
// version: 1
|
||||
|
||||
/// Owned opaque binary payload carried by a realtime transport.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TransportMessage {
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl TransportMessage {
|
||||
/// Creates a transport message from owned bytes.
|
||||
#[must_use]
|
||||
pub fn new(bytes: Vec<u8>) -> Self {
|
||||
return Self { bytes };
|
||||
}
|
||||
|
||||
/// Borrows the opaque payload bytes.
|
||||
#[must_use]
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
return self.bytes.as_slice();
|
||||
}
|
||||
|
||||
/// Consumes the message and returns its owned payload bytes.
|
||||
#[must_use]
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
return self.bytes;
|
||||
}
|
||||
|
||||
/// Returns whether the payload is empty.
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
return self.bytes.is_empty();
|
||||
}
|
||||
|
||||
/// Returns the payload length in bytes.
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
return self.bytes.len();
|
||||
}
|
||||
}
|
||||
|
||||
/// Outcome of one successful transport receive operation.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum TransportReceive {
|
||||
/// One opaque binary message was received.
|
||||
Message(crate::TransportMessage),
|
||||
/// The remote peer completed a clean transport-level close.
|
||||
Closed,
|
||||
}
|
||||
131
crates/common/game-realtime-transport-lib/unit_tests/contract.rs
Normal file
131
crates/common/game-realtime-transport-lib/unit_tests/contract.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
// file: crates/common/game-realtime-transport-lib/unit_tests/contract.rs
|
||||
// version: 1
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
struct TestSender {
|
||||
closed: bool,
|
||||
sent_bytes: usize,
|
||||
}
|
||||
|
||||
impl crate::RealtimeSender for TestSender {
|
||||
type SendFuture<'a>
|
||||
= std::future::Ready<Result<(), crate::TransportError>>
|
||||
where
|
||||
Self: 'a;
|
||||
type CloseFuture<'a>
|
||||
= std::future::Ready<Result<(), crate::TransportError>>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
fn send(&mut self, message: crate::TransportMessage) -> Self::SendFuture<'_> {
|
||||
self.sent_bytes = self.sent_bytes.saturating_add(message.len());
|
||||
return std::future::ready(Ok(()));
|
||||
}
|
||||
|
||||
fn close(&mut self) -> Self::CloseFuture<'_> {
|
||||
self.closed = true;
|
||||
return std::future::ready(Ok(()));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
struct TestReceiver;
|
||||
|
||||
impl crate::RealtimeReceiver for TestReceiver {
|
||||
type ReceiveFuture<'a>
|
||||
= std::future::Ready<Result<crate::TransportReceive, crate::TransportError>>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
fn receive(&mut self) -> Self::ReceiveFuture<'_> {
|
||||
return std::future::ready(Ok(crate::TransportReceive::Closed));
|
||||
}
|
||||
}
|
||||
|
||||
struct TestConnection;
|
||||
|
||||
impl crate::RealtimeConnection for TestConnection {
|
||||
type Sender = TestSender;
|
||||
type Receiver = TestReceiver;
|
||||
|
||||
fn split(self) -> (Self::Sender, Self::Receiver) {
|
||||
return (TestSender { closed: false, sent_bytes: 0 }, TestReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_ready<T>(future: impl core::future::Future<Output = T>) -> T {
|
||||
let mut future = std::pin::pin!(future);
|
||||
let waker = std::task::Waker::noop();
|
||||
let mut context = std::task::Context::from_waker(waker);
|
||||
return match core::future::Future::poll(future.as_mut(), &mut context) {
|
||||
std::task::Poll::Ready(value) => value,
|
||||
std::task::Poll::Pending => unreachable!("test future unexpectedly remained pending"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_preserves_owned_binary_payload() {
|
||||
let message = crate::TransportMessage::new(vec![0, 1, 2, 255]);
|
||||
assert_eq!(message.as_bytes(), &[0, 1, 2, 255]);
|
||||
assert_eq!(message.len(), 4);
|
||||
assert!(!message.is_empty());
|
||||
assert_eq!(message.into_bytes(), vec![0, 1, 2, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_message_remains_a_valid_transport_payload() {
|
||||
let message = crate::TransportMessage::new(Vec::new());
|
||||
assert!(message.is_empty());
|
||||
assert_eq!(message.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_remote_close_is_distinct_from_transport_error() {
|
||||
let mut receiver = TestReceiver;
|
||||
let received = poll_ready(crate::RealtimeReceiver::receive(&mut receiver));
|
||||
assert_eq!(received, Ok(crate::TransportReceive::Closed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_split_produces_independent_contract_halves() {
|
||||
let (mut sender, mut receiver) = crate::RealtimeConnection::split(TestConnection);
|
||||
assert_eq!(poll_ready(crate::RealtimeSender::send(&mut sender, crate::TransportMessage::new(vec![1, 2, 3]))), Ok(()));
|
||||
assert_eq!(sender.sent_bytes, 3);
|
||||
assert_eq!(poll_ready(crate::RealtimeReceiver::receive(&mut receiver)), Ok(crate::TransportReceive::Closed));
|
||||
assert_eq!(poll_ready(crate::RealtimeSender::close(&mut sender)), Ok(()));
|
||||
assert!(sender.closed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_receive_variant_preserves_payload() {
|
||||
let received = crate::TransportReceive::Message(crate::TransportMessage::new(vec![4, 5, 6]));
|
||||
assert_eq!(received, crate::TransportReceive::Message(crate::TransportMessage::new(vec![4, 5, 6])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_error_preserves_stable_kind_and_diagnostic_detail() {
|
||||
let error = crate::TransportError::new(crate::TransportErrorKind::Timeout, "send deadline exceeded");
|
||||
assert_eq!(error.kind(), crate::TransportErrorKind::Timeout);
|
||||
assert_eq!(error.detail(), "send deadline exceeded");
|
||||
assert_eq!(error.to_string(), "operation timed out: send deadline exceeded");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_error_kinds_have_stable_human_readable_labels() {
|
||||
let cases = [
|
||||
(crate::TransportErrorKind::InvalidConfiguration, "invalid configuration"),
|
||||
(crate::TransportErrorKind::Connect, "connect failure"),
|
||||
(crate::TransportErrorKind::Bind, "bind failure"),
|
||||
(crate::TransportErrorKind::Accept, "accept failure"),
|
||||
(crate::TransportErrorKind::Timeout, "operation timed out"),
|
||||
(crate::TransportErrorKind::MessageTooLarge, "message too large"),
|
||||
(crate::TransportErrorKind::Backpressure, "transport backpressure"),
|
||||
(crate::TransportErrorKind::Closed, "connection closed"),
|
||||
(crate::TransportErrorKind::Io, "I/O failure"),
|
||||
(crate::TransportErrorKind::Protocol, "protocol failure"),
|
||||
(crate::TransportErrorKind::Aborted, "operation aborted"),
|
||||
];
|
||||
for (kind, expected) in cases {
|
||||
assert_eq!(kind.to_string(), expected);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user