256 lines
6.9 KiB
Rust
256 lines
6.9 KiB
Rust
//! # QUIC Multi-Stream
|
|
//!
|
|
//! Cross-platform QUIC multi-stream support for native and WASM targets.
|
|
//!
|
|
//! ## Features
|
|
//! - Unified API for native (quinn) and WASM (WebTransport)
|
|
//! - Multiplexed bidirectional and unidirectional streams
|
|
//! - Stream prioritization for QoS
|
|
//! - 0-RTT connection establishment (native)
|
|
//! - Built-in encryption and security
|
|
//!
|
|
//! ## Examples
|
|
//!
|
|
//! ### Native Example
|
|
//! ```no_run
|
|
//! # #[cfg(not(target_arch = "wasm32"))]
|
|
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
|
//! use midstreamer_quic::{QuicConnection, StreamPriority};
|
|
//!
|
|
//! // Connect to server
|
|
//! let connection = QuicConnection::connect("localhost:4433").await?;
|
|
//!
|
|
//! // Open bidirectional stream
|
|
//! let mut stream = connection.open_bi_stream().await?;
|
|
//!
|
|
//! // Send data
|
|
//! stream.send(b"Hello QUIC!").await?;
|
|
//!
|
|
//! // Receive response
|
|
//! let mut buffer = vec![0u8; 1024];
|
|
//! let n = stream.recv(&mut buffer).await?;
|
|
//!
|
|
//! println!("Received: {:?}", &buffer[..n]);
|
|
//! # Ok(())
|
|
//! # }
|
|
//! ```
|
|
//!
|
|
//! ### WASM Example
|
|
//! ```no_run
|
|
//! # #[cfg(target_arch = "wasm32")]
|
|
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
|
//! use midstreamer_quic::QuicConnection;
|
|
//!
|
|
//! // Connect via WebTransport
|
|
//! let connection = QuicConnection::connect("https://server.example.com").await?;
|
|
//!
|
|
//! // Open stream and communicate
|
|
//! let mut stream = connection.open_bi_stream().await?;
|
|
//! stream.send(b"Hello from WASM!").await?;
|
|
//! # Ok(())
|
|
//! # }
|
|
//! ```
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use std::fmt;
|
|
use thiserror::Error;
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
mod native;
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub use native::*;
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
mod wasm;
|
|
#[cfg(target_arch = "wasm32")]
|
|
pub use wasm::*;
|
|
|
|
/// Errors that can occur during QUIC operations
|
|
#[derive(Debug, Error)]
|
|
pub enum QuicError {
|
|
#[error("Connection failed: {0}")]
|
|
ConnectionFailed(String),
|
|
|
|
#[error("Stream error: {0}")]
|
|
StreamError(String),
|
|
|
|
#[error("Send error: {0}")]
|
|
SendError(String),
|
|
|
|
#[error("Receive error: {0}")]
|
|
RecvError(String),
|
|
|
|
#[error("Invalid configuration: {0}")]
|
|
InvalidConfig(String),
|
|
|
|
#[error("TLS error: {0}")]
|
|
TlsError(String),
|
|
|
|
#[error("Timeout: {0}")]
|
|
Timeout(String),
|
|
|
|
#[error("Connection closed: {0}")]
|
|
ConnectionClosed(String),
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
#[error("Quinn error: {0}")]
|
|
QuinnError(#[from] quinn::ConnectionError),
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
#[error("Quinn connect error: {0}")]
|
|
QuinnConnectError(#[from] quinn::ConnectError),
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
#[error("Quinn read error: {0}")]
|
|
QuinnReadError(#[from] quinn::ReadError),
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
#[error("Quinn write error: {0}")]
|
|
QuinnWriteError(#[from] quinn::WriteError),
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
#[error("WASM error: {0}")]
|
|
WasmError(String),
|
|
|
|
#[error("IO error: {0}")]
|
|
IoError(String),
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
impl From<wasm_bindgen::JsValue> for QuicError {
|
|
fn from(err: wasm_bindgen::JsValue) -> Self {
|
|
QuicError::WasmError(format!("{:?}", err))
|
|
}
|
|
}
|
|
|
|
/// Stream priority for quality-of-service control
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub enum StreamPriority {
|
|
/// Critical priority (highest)
|
|
Critical = 0,
|
|
/// High priority
|
|
High = 1,
|
|
/// Normal priority (default)
|
|
Normal = 2,
|
|
/// Low priority
|
|
Low = 3,
|
|
}
|
|
|
|
impl Default for StreamPriority {
|
|
fn default() -> Self {
|
|
StreamPriority::Normal
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for StreamPriority {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
StreamPriority::Critical => write!(f, "Critical"),
|
|
StreamPriority::High => write!(f, "High"),
|
|
StreamPriority::Normal => write!(f, "Normal"),
|
|
StreamPriority::Low => write!(f, "Low"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Connection statistics
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ConnectionStats {
|
|
/// Number of active bidirectional streams
|
|
pub active_bi_streams: usize,
|
|
/// Number of active unidirectional streams
|
|
pub active_uni_streams: usize,
|
|
/// Total bytes sent
|
|
pub bytes_sent: u64,
|
|
/// Total bytes received
|
|
pub bytes_received: u64,
|
|
/// Round-trip time in milliseconds
|
|
pub rtt_ms: f64,
|
|
}
|
|
|
|
impl Default for ConnectionStats {
|
|
fn default() -> Self {
|
|
Self {
|
|
active_bi_streams: 0,
|
|
active_uni_streams: 0,
|
|
bytes_sent: 0,
|
|
bytes_received: 0,
|
|
rtt_ms: 0.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_priority_ordering() {
|
|
assert!(StreamPriority::Critical < StreamPriority::High);
|
|
assert!(StreamPriority::High < StreamPriority::Normal);
|
|
assert!(StreamPriority::Normal < StreamPriority::Low);
|
|
}
|
|
|
|
#[test]
|
|
fn test_priority_default() {
|
|
assert_eq!(StreamPriority::default(), StreamPriority::Normal);
|
|
}
|
|
|
|
#[test]
|
|
fn test_priority_display() {
|
|
assert_eq!(StreamPriority::Critical.to_string(), "Critical");
|
|
assert_eq!(StreamPriority::High.to_string(), "High");
|
|
assert_eq!(StreamPriority::Normal.to_string(), "Normal");
|
|
assert_eq!(StreamPriority::Low.to_string(), "Low");
|
|
}
|
|
|
|
#[test]
|
|
fn test_connection_stats_default() {
|
|
let stats = ConnectionStats::default();
|
|
assert_eq!(stats.active_bi_streams, 0);
|
|
assert_eq!(stats.active_uni_streams, 0);
|
|
assert_eq!(stats.bytes_sent, 0);
|
|
assert_eq!(stats.bytes_received, 0);
|
|
assert_eq!(stats.rtt_ms, 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_display() {
|
|
let err = QuicError::ConnectionFailed("test".to_string());
|
|
assert!(err.to_string().contains("Connection failed"));
|
|
|
|
let err = QuicError::StreamError("stream".to_string());
|
|
assert!(err.to_string().contains("Stream error"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_priority_serialization() {
|
|
let priority = StreamPriority::High;
|
|
let json = serde_json::to_string(&priority).unwrap();
|
|
let deserialized: StreamPriority = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(priority, deserialized);
|
|
}
|
|
|
|
#[test]
|
|
fn test_stats_serialization() {
|
|
let stats = ConnectionStats {
|
|
active_bi_streams: 5,
|
|
active_uni_streams: 3,
|
|
bytes_sent: 1024,
|
|
bytes_received: 2048,
|
|
rtt_ms: 15.5,
|
|
};
|
|
let json = serde_json::to_string(&stats).unwrap();
|
|
let deserialized: ConnectionStats = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(stats.active_bi_streams, deserialized.active_bi_streams);
|
|
assert_eq!(stats.rtt_ms, deserialized.rtt_ms);
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_conversion() {
|
|
let err = QuicError::IoError("io test".to_string());
|
|
assert!(matches!(err, QuicError::IoError(_)));
|
|
}
|
|
}
|