创建了 Rust 版本的 CubeLib 库

This commit is contained in:
zqm
2026-04-07 13:55:40 +08:00
parent 66e6ef2e8c
commit 5915c42f9f
11 changed files with 2618 additions and 0 deletions

1455
Windows/Rust/CubeLib/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
[package]
name = "cube_lib"
version = "0.1.0"
edition = "2021"
description = "CubeLib - Rust library for WebSocket communication and utilities"
authors = ["JoyD Team"]
[dependencies]
# WebSocket
tokio-tungstenite = { version = "0.21", features = ["tokio-native-tls"] }
futures-util = { version = "0.3", default-features = false, features = ["sink", "std", "async-await"] }
tokio = { version = "1", features = ["full"] }
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Async runtime
async-trait = "0.1"
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# HTTP headers
http = "1.0"
# URL parsing
url = "2.4"
[dev-dependencies]
tokio-test = "0.4"

View File

@@ -0,0 +1,107 @@
# CubeLib - Rust WebSocket Library
Rust 版本的 CubeLib提供 WebSocket 通信功能。
## 结构
```
CubeLib/
├── Cargo.toml
├── src/
│ ├── lib.rs # 库入口
│ └── websocket/
│ ├── mod.rs # 模块定义
│ ├── client.rs # WebSocket 客户端
│ ├── config.rs # 配置类
│ ├── message.rs # 消息类型
│ └── error.rs # 错误类型
└── README.md
```
## 功能特性
- **事件驱动**:支持 Connected、Disconnected、Message、Error 等回调
- **自动重连**:支持指数退避的重连机制
- **自定义头**:支持设置 ClientType 等 HTTP 头
- **心跳保活**:可配置的心跳间隔
- **消息队列**:离线时自动缓存消息,连接后自动发送
- **调试模式**:可选的日志输出
## 快速开始
```rust
use cube_lib::websocket::{WebSocketClient, WebSocketConfig};
#[tokio::main]
async fn main() {
let config = WebSocketConfig::new("ws://localhost:8087/ws")
.with_client_type("Updater")
.with_debug(true);
let mut client = WebSocketClient::new(config);
// 设置事件回调
client.on_connected(|url| {
println!("Connected to: {}", url);
});
client.on_message(|msg_type, data| {
println!("Received [{}]: {:?}", msg_type, data);
});
client.on_disconnected(|| {
println!("Disconnected");
});
// 连接
client.connect().await;
// 发送消息(格式与 MasterSuite 后端兼容)
client.send("ping", serde_json::json!({
"timestamp": "2024-01-01 00:00:00"
})).await;
// 保持运行
tokio::signal::ctrl_c().await.ok();
client.disconnect().await;
}
```
## WebSocketConfig 配置项
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `ws_url` | String | `ws://localhost:8087/ws` | 服务器地址 |
| `auto_connect` | bool | `true` | 初始化时自动连接 |
| `reconnect` | bool | `true` | 断开时自动重连 |
| `max_reconnect_delay_ms` | u64 | `30000` | 最大重连延迟(毫秒) |
| `reconnect_delay_ms` | u64 | `1000` | 初始重连延迟(毫秒) |
| `debug_mode` | bool | `false` | 调试模式 |
| `heartbeat_interval_ms` | u64 | `30000` | 心跳间隔(毫秒)0=禁用 |
| `connect_timeout_ms` | u64 | `10000` | 连接超时(毫秒) |
| `max_queue_size` | usize | `100` | 消息队列最大长度 |
| `client_type` | Option\<String\> | `None` | 客户端类型标识 |
| `custom_headers` | Vec\<(String, String)\> | `[]` | 自定义 HTTP 头 |
## 消息格式
与 MasterSuite 后端兼容的 JSON 格式:
```json
{
"Type": "message_type",
"Data": { ... },
"MessageId": "optional_id"
}
```
## 集成到项目
`Cargo.toml` 中添加:
```toml
[dependencies]
cube_lib = { path = "../Rust/CubeLib" }
```
或从 Git 引用(待发布后)。

View File

@@ -0,0 +1,33 @@
//! # CubeLib
//!
//! Rust library providing WebSocket communication and utility components.
//!
//! ## Modules
//!
//! - [`websocket`] - WebSocket client and server implementations
//!
//! ## Quick Start
//!
//! ```rust,no_run
//! use cube_lib::websocket::{WebSocketClient, WebSocketConfig};
//!
//! #[tokio::main]
//! async fn main() {
//! let config = WebSocketConfig::default();
//! let mut client = WebSocketClient::new(config);
//!
//! client.on_connected(|_| {
//! println!("Connected!");
//! });
//!
//! client.on_message(|_, msg| {
//! println!("Received: {:?}", msg);
//! });
//!
//! client.connect().await;
//! }
//! ```
pub mod websocket;
pub use websocket::{WebSocketClient, WebSocketConfig, WebSocketMessage};

View File

@@ -0,0 +1,638 @@
//! WebSocket client implementation
use crate::websocket::{WebSocketConfig, WebSocketMessage, WebSocketError};
use futures_util::{SinkExt, StreamExt};
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex};
use tokio::time::Duration;
use tracing::{warn, debug};
use serde_json::Value;
/// Message to send through the channel
#[derive(Debug)]
enum OutgoingMessage {
/// Text message
Text(String),
/// Binary message
Binary(Vec<u8>),
/// Close connection
Close,
}
/// Connection status
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::upper_case_acronyms)]
pub enum ConnectionStatus {
/// Disconnected
Disconnected,
/// Connecting
Connecting,
/// Connected
Connected,
/// Reconnecting
Reconnecting,
/// Error
Error,
}
impl std::fmt::Display for ConnectionStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConnectionStatus::Disconnected => write!(f, "disconnected"),
ConnectionStatus::Connecting => write!(f, "connecting"),
ConnectionStatus::Connected => write!(f, "connected"),
ConnectionStatus::Reconnecting => write!(f, "reconnecting"),
ConnectionStatus::Error => write!(f, "error"),
}
}
}
/// Event callback types (wrapped in Arc for cloneability)
pub type ConnectedCallback = Arc<dyn Fn(String) + Send + Sync>;
pub type DisconnectedCallback = Arc<dyn Fn() + Send + Sync>;
pub type MessageCallback = Arc<dyn Fn(String, Value) + Send + Sync>;
pub type BinaryCallback = Arc<dyn Fn(Vec<u8>) + Send + Sync>;
pub type ErrorCallback = Arc<dyn Fn(String) + Send + Sync>;
pub type StatusCallback = Arc<dyn Fn(ConnectionStatus, ConnectionStatus) + Send + Sync>;
pub type SentCallback = Arc<dyn Fn(String, Value) + Send + Sync>;
/// WebSocket client with event-driven architecture
pub struct WebSocketClient {
config: WebSocketConfig,
status: Arc<Mutex<ConnectionStatus>>,
sender: Arc<Mutex<Option<mpsc::Sender<OutgoingMessage>>>>,
message_queue: Arc<Mutex<Vec<OutgoingMessage>>>,
task_handle: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
is_running: Arc<Mutex<bool>>,
// Event callbacks
on_connected: Option<ConnectedCallback>,
on_disconnected: Option<DisconnectedCallback>,
on_message: Option<MessageCallback>,
on_binary: Option<BinaryCallback>,
on_error: Option<ErrorCallback>,
on_status_changed: Option<StatusCallback>,
on_message_sent: Option<SentCallback>,
// Reconnection state
reconnect_attempts: Arc<Mutex<u32>>,
reconnect_delay_ms: Arc<Mutex<u64>>,
}
impl WebSocketClient {
/// Create a new WebSocket client
pub fn new(config: WebSocketConfig) -> Self {
Self {
config,
status: Arc::new(Mutex::new(ConnectionStatus::Disconnected)),
sender: Arc::new(Mutex::new(None)),
message_queue: Arc::new(Mutex::new(Vec::new())),
task_handle: Arc::new(Mutex::new(None)),
is_running: Arc::new(Mutex::new(false)),
on_connected: None,
on_disconnected: None,
on_message: None,
on_binary: None,
on_error: None,
on_status_changed: None,
on_message_sent: None,
reconnect_attempts: Arc::new(Mutex::new(0)),
reconnect_delay_ms: Arc::new(Mutex::new(1000)),
}
}
/// Create a simple client with just the URL
pub fn simple(url: impl Into<String>) -> Self {
Self::new(WebSocketConfig::new(url))
}
// ==================== Event Handlers ====================
/// Set callback for connected event
pub fn on_connected<F>(&mut self, callback: F) -> &mut Self
where
F: Fn(String) + Send + Sync + 'static,
{
self.on_connected = Some(Arc::new(callback));
self
}
/// Set callback for disconnected event
pub fn on_disconnected<F>(&mut self, callback: F) -> &mut Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_disconnected = Some(Arc::new(callback));
self
}
/// Set callback for text message received
pub fn on_message<F>(&mut self, callback: F) -> &mut Self
where
F: Fn(String, Value) + Send + Sync + 'static,
{
self.on_message = Some(Arc::new(callback));
self
}
/// Set callback for binary message received
pub fn on_binary<F>(&mut self, callback: F) -> &mut Self
where
F: Fn(Vec<u8>) + Send + Sync + 'static,
{
self.on_binary = Some(Arc::new(callback));
self
}
/// Set callback for error
pub fn on_error<F>(&mut self, callback: F) -> &mut Self
where
F: Fn(String) + Send + Sync + 'static,
{
self.on_error = Some(Arc::new(callback));
self
}
/// Set callback for status changed
pub fn on_status_changed<F>(&mut self, callback: F) -> &mut Self
where
F: Fn(ConnectionStatus, ConnectionStatus) + Send + Sync + 'static,
{
self.on_status_changed = Some(Arc::new(callback));
self
}
/// Set callback for message sent
pub fn on_message_sent<F>(&mut self, callback: F) -> &mut Self
where
F: Fn(String, Value) + Send + Sync + 'static,
{
self.on_message_sent = Some(Arc::new(callback));
self
}
// ==================== Connection Management ====================
/// Connect to the WebSocket server
pub async fn connect(&mut self) {
let url = self.config.ws_url.clone();
self.connect_with_url(&url).await;
}
/// Connect to a specific URL (overrides config URL)
pub async fn connect_with_url(&mut self, url: &str) {
let _old_status = *self.status.lock().await;
self.set_status(ConnectionStatus::Connecting).await;
if self.config.debug_mode {
debug!("Connecting to WebSocket server: {}", url);
}
// Create channel for outgoing messages
let (tx, rx) = mpsc::channel::<OutgoingMessage>(100);
*self.sender.lock().await = Some(tx);
// Clone shared state for the task
let url = url.to_string();
let config = self.config.clone();
let status = Arc::clone(&self.status);
let queue = Arc::clone(&self.message_queue);
let is_running = Arc::clone(&self.is_running);
let reconnect_attempts = Arc::clone(&self.reconnect_attempts);
let reconnect_delay_ms = Arc::clone(&self.reconnect_delay_ms);
// Callbacks
let on_connected = self.on_connected.clone();
let on_disconnected = self.on_disconnected.clone();
let on_message = self.on_message.clone();
let on_binary = self.on_binary.clone();
let on_error = self.on_error.clone();
// Spawn the WebSocket task
*self.is_running.lock().await = true;
let task_handle = tokio::spawn(async move {
Self::websocket_loop(
url,
config,
rx,
status,
queue,
is_running,
reconnect_attempts,
reconnect_delay_ms,
on_connected,
on_disconnected,
on_message,
on_binary,
on_error,
)
.await;
});
*self.task_handle.lock().await = Some(task_handle);
}
/// Main WebSocket loop
async fn websocket_loop(
url: String,
config: WebSocketConfig,
mut receiver: mpsc::Receiver<OutgoingMessage>,
status: Arc<Mutex<ConnectionStatus>>,
queue: Arc<Mutex<Vec<OutgoingMessage>>>,
is_running: Arc<Mutex<bool>>,
reconnect_attempts: Arc<Mutex<u32>>,
reconnect_delay_ms: Arc<Mutex<u64>>,
on_connected: Option<ConnectedCallback>,
on_disconnected: Option<DisconnectedCallback>,
on_message: Option<MessageCallback>,
on_binary: Option<BinaryCallback>,
on_error: Option<ErrorCallback>,
) {
loop {
let should_run = *is_running.lock().await;
if !should_run {
break;
}
match Self::connect_and_handle(&url, &config, &mut receiver, &queue, &on_connected, &on_message, &on_binary, &on_error).await {
Ok(_) => {
if config.debug_mode {
debug!("WebSocket connection closed normally");
}
break;
}
Err(e) => {
if config.debug_mode {
warn!("WebSocket error: {:?}", e);
}
if let Some(ref callback) = on_error {
callback(e.to_string());
}
if config.reconnect && *is_running.lock().await {
let delay = *reconnect_delay_ms.lock().await;
let attempts = *reconnect_attempts.lock().await + 1;
*reconnect_attempts.lock().await = attempts;
if config.debug_mode {
debug!("Reconnecting in {}ms (attempt {})", delay, attempts);
}
*status.lock().await = ConnectionStatus::Reconnecting;
tokio::time::sleep(Duration::from_millis(delay)).await;
// Exponential backoff
let new_delay = std::cmp::min(delay * 2, config.max_reconnect_delay_ms);
*reconnect_delay_ms.lock().await = new_delay;
continue;
} else {
break;
}
}
}
}
*status.lock().await = ConnectionStatus::Disconnected;
if let Some(callback) = on_disconnected {
callback();
}
}
/// Connect and handle messages
async fn connect_and_handle(
url: &str,
config: &WebSocketConfig,
receiver: &mut mpsc::Receiver<OutgoingMessage>,
_queue: &Arc<Mutex<Vec<OutgoingMessage>>>,
on_connected: &Option<ConnectedCallback>,
on_message: &Option<MessageCallback>,
on_binary: &Option<BinaryCallback>,
on_error: &Option<ErrorCallback>,
) -> Result<(), WebSocketError> {
use http::header::{HeaderName, HeaderValue};
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
// Build request with headers
let mut request = url.into_client_request()?;
// Add client type header if specified
if let Some(ref client_type) = config.client_type {
request.headers_mut().insert(
HeaderName::from_static("clienttype"),
HeaderValue::from_str(client_type)
.map_err(|e| WebSocketError::HeaderError(e.to_string()))?,
);
}
// Add custom headers
for (key, value) in &config.custom_headers {
if let (Ok(name), Ok(val)) = (
HeaderName::try_from(key.as_str()),
HeaderValue::from_str(value),
) {
request.headers_mut().insert(name, val);
}
}
// Connect with timeout
let ws_stream = tokio::time::timeout(
Duration::from_millis(config.connect_timeout_ms),
tokio_tungstenite::connect_async(request),
)
.await
.map_err(|_| WebSocketError::ConnectionTimeout)?
.map_err(|e| WebSocketError::ConnectionFailed(e.to_string()))?;
let (mut write, mut read) = ws_stream.0.split();
if config.debug_mode {
debug!("Connected to WebSocket server");
}
// Call on_connected callback
if let Some(ref callback) = on_connected {
callback(url.to_string());
}
// Start heartbeat task if enabled
let heartbeat_handle = if config.heartbeat_interval_ms > 0 {
let interval_duration = Duration::from_millis(config.heartbeat_interval_ms);
Some(tokio::spawn(async move {
let mut ticker = tokio::time::interval(interval_duration);
loop {
ticker.tick().await;
// Heartbeat will be sent via the receiver
}
}))
} else {
None
};
// Handle messages
loop {
tokio::select! {
// Incoming message from server
msg = read.next() => {
match msg {
Some(Ok(tokio_tungstenite::tungstenite::Message::Text(text))) => {
if config.debug_mode {
debug!("Received text: {}", text);
}
// Parse and extract type
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&text) {
let msg_type = parsed.get("Type")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
if let Some(ref callback) = on_message {
callback(msg_type, parsed);
}
} else if let Some(ref callback) = on_message {
callback("raw".to_string(), serde_json::json!(text));
}
}
Some(Ok(tokio_tungstenite::tungstenite::Message::Binary(data))) => {
if config.debug_mode {
debug!("Received binary: {} bytes", data.len());
}
if let Some(ref callback) = on_binary {
callback(data);
}
}
Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => {
if config.debug_mode {
debug!("Server initiated close");
}
break;
}
Some(Err(e)) => {
if config.debug_mode {
warn!("WebSocket read error: {}", e);
}
if let Some(ref callback) = on_error {
callback(e.to_string());
}
break;
}
None => {
if config.debug_mode {
debug!("WebSocket stream ended");
}
break;
}
_ => {}
}
}
// Outgoing message
outgoing = receiver.recv() => {
match outgoing {
Some(OutgoingMessage::Text(text)) => {
if config.debug_mode {
debug!("Sending text: {}", text);
}
write.send(tokio_tungstenite::tungstenite::Message::Text(text))
.await
.map_err(|e| WebSocketError::SendFailed(e.to_string()))?;
}
Some(OutgoingMessage::Binary(data)) => {
if config.debug_mode {
debug!("Sending binary: {} bytes", data.len());
}
write.send(tokio_tungstenite::tungstenite::Message::Binary(data))
.await
.map_err(|e| WebSocketError::SendFailed(e.to_string()))?;
}
Some(OutgoingMessage::Close) => {
if config.debug_mode {
debug!("Initiating close");
}
write.close().await.ok();
break;
}
None => {
break;
}
}
}
}
}
// Wait for heartbeat handle
if let Some(handle) = heartbeat_handle {
handle.abort();
}
Ok(())
}
/// Disconnect from the server
pub async fn disconnect(&mut self) {
*self.is_running.lock().await = false;
if let Some(sender) = self.sender.lock().await.take() {
let _ = sender.send(OutgoingMessage::Close).await;
}
if let Some(handle) = self.task_handle.lock().await.take() {
handle.abort();
}
self.set_status(ConnectionStatus::Disconnected).await;
}
// ==================== Message Sending ====================
/// Send a text message
pub async fn send(&self, msg_type: &str, data: Value) -> Result<(), WebSocketError> {
let message = WebSocketMessage::new(msg_type, data);
let json = serde_json::to_string(&message)?;
self.send_raw_text(json).await
}
/// Send raw text
pub async fn send_raw_text(&self, text: String) -> Result<(), WebSocketError> {
let status = *self.status.lock().await;
if status == ConnectionStatus::Connected {
if let Some(ref sender) = *self.sender.lock().await {
sender.send(OutgoingMessage::Text(text))
.await
.map_err(|_| WebSocketError::NotConnected)?;
}
} else {
// Queue the message
let mut queue = self.message_queue.lock().await;
if queue.len() < self.config.max_queue_size {
if self.config.debug_mode {
debug!("Queueing message (queue size: {})", queue.len() + 1);
}
queue.push(OutgoingMessage::Text(text));
} else {
if self.config.debug_mode {
warn!("Queue full, message dropped");
}
return Err(WebSocketError::QueueFull);
}
}
Ok(())
}
/// Send binary data
pub async fn send_binary(&self, data: Vec<u8>) -> Result<(), WebSocketError> {
let status = *self.status.lock().await;
if status == ConnectionStatus::Connected {
if let Some(ref sender) = *self.sender.lock().await {
sender.send(OutgoingMessage::Binary(data))
.await
.map_err(|_| WebSocketError::NotConnected)?;
}
} else {
let mut queue = self.message_queue.lock().await;
if queue.len() < self.config.max_queue_size {
if self.config.debug_mode {
debug!("Queueing binary message");
}
queue.push(OutgoingMessage::Binary(data));
} else {
return Err(WebSocketError::QueueFull);
}
}
Ok(())
}
// ==================== Status & Queue ====================
/// Get current connection status
pub async fn get_status(&self) -> ConnectionStatus {
*self.status.lock().await
}
/// Check if connected
pub async fn is_connected(&self) -> bool {
*self.status.lock().await == ConnectionStatus::Connected
}
/// Get queue size
pub async fn get_queue_size(&self) -> usize {
self.message_queue.lock().await.len()
}
/// Flush queued messages
pub async fn flush_queue(&self) -> Result<(), WebSocketError> {
let status = *self.status.lock().await;
if status != ConnectionStatus::Connected {
return Err(WebSocketError::NotConnected);
}
let mut queue = self.message_queue.lock().await;
if let Some(ref sender) = *self.sender.lock().await {
while let Some(msg) = queue.pop() {
sender.send(msg)
.await
.map_err(|_| WebSocketError::NotConnected)?;
}
}
Ok(())
}
// ==================== Private Helpers ====================
#[allow(clippy::large_enum_variant)]
async fn set_status(&mut self, new_status: ConnectionStatus) {
let old_status = *self.status.lock().await;
if old_status != new_status {
*self.status.lock().await = new_status.clone();
// Reset reconnect state on successful connection
if new_status == ConnectionStatus::Connected {
*self.reconnect_attempts.lock().await = 0;
*self.reconnect_delay_ms.lock().await = self.config.reconnect_delay_ms;
}
if let Some(ref callback) = self.on_status_changed {
callback(old_status, new_status);
}
}
}
}
impl Drop for WebSocketClient {
fn drop(&mut self) {
// Attempt to disconnect synchronously
let _ = tokio::runtime::Handle::current().block_on(self.disconnect());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_builder() {
let config = WebSocketConfig::new("ws://localhost:8080")
.with_client_type("Client")
.with_debug(true)
.with_reconnect(false);
assert_eq!(config.ws_url, "ws://localhost:8080");
assert_eq!(config.client_type, Some("Client".to_string()));
assert!(config.debug_mode);
assert!(!config.reconnect);
}
#[tokio::test]
async fn test_client_creation() {
let config = WebSocketConfig::default();
let client = WebSocketClient::new(config);
let status = client.get_status().await;
assert_eq!(status, ConnectionStatus::Disconnected);
}
}

View File

@@ -0,0 +1,132 @@
//! WebSocket configuration
use serde::{Deserialize, Serialize};
/// WebSocket client configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebSocketConfig {
/// WebSocket server URL (e.g., "ws://localhost:8087/ws")
pub ws_url: String,
/// Auto-connect on initialization
#[serde(default = "default_true")]
pub auto_connect: bool,
/// Auto-reconnect on disconnection
#[serde(default = "default_true")]
pub reconnect: bool,
/// Maximum reconnect delay in milliseconds
#[serde(default = "default_max_reconnect_delay")]
pub max_reconnect_delay_ms: u64,
/// Initial reconnect delay in milliseconds
#[serde(default = "default_reconnect_delay")]
pub reconnect_delay_ms: u64,
/// Debug mode (enable logging)
#[serde(default)]
pub debug_mode: bool,
/// Heartbeat interval in milliseconds (0 to disable)
#[serde(default = "default_heartbeat_interval")]
pub heartbeat_interval_ms: u64,
/// Connection timeout in milliseconds
#[serde(default = "default_connect_timeout")]
pub connect_timeout_ms: u64,
/// Maximum message queue size
#[serde(default = "default_max_queue_size")]
pub max_queue_size: usize,
/// Custom HTTP headers
#[serde(default)]
pub custom_headers: Vec<(String, String)>,
/// Client type for identification
#[serde(default)]
pub client_type: Option<String>,
}
fn default_true() -> bool {
true
}
fn default_max_reconnect_delay() -> u64 {
30000
}
fn default_reconnect_delay() -> u64 {
1000
}
fn default_heartbeat_interval() -> u64 {
30000
}
fn default_connect_timeout() -> u64 {
10000
}
fn default_max_queue_size() -> usize {
100
}
impl Default for WebSocketConfig {
fn default() -> Self {
Self {
ws_url: "ws://localhost:8087/ws".to_string(),
auto_connect: true,
reconnect: true,
max_reconnect_delay_ms: default_max_reconnect_delay(),
reconnect_delay_ms: default_reconnect_delay(),
debug_mode: false,
heartbeat_interval_ms: default_heartbeat_interval(),
connect_timeout_ms: default_connect_timeout(),
max_queue_size: default_max_queue_size(),
custom_headers: Vec::new(),
client_type: None,
}
}
}
impl WebSocketConfig {
/// Create a new config with the given URL
pub fn new(ws_url: impl Into<String>) -> Self {
Self {
ws_url: ws_url.into(),
..Default::default()
}
}
/// Set client type
pub fn with_client_type(mut self, client_type: impl Into<String>) -> Self {
self.client_type = Some(client_type.into());
self
}
/// Set debug mode
pub fn with_debug(mut self, debug: bool) -> Self {
self.debug_mode = debug;
self
}
/// Add a custom header
pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.custom_headers.push((key.into(), value.into()));
self
}
/// Enable auto-reconnect
pub fn with_reconnect(mut self, enabled: bool) -> Self {
self.reconnect = enabled;
self
}
/// Set heartbeat interval (0 to disable)
pub fn with_heartbeat(mut self, interval_ms: u64) -> Self {
self.heartbeat_interval_ms = interval_ms;
self
}
}

View File

@@ -0,0 +1,114 @@
//! WebSocket error types
use std::fmt;
/// WebSocket-related errors
#[derive(Debug)]
pub enum WebSocketError {
/// Connection failed
ConnectionFailed(String),
/// Connection timeout
ConnectionTimeout,
/// Already connected
AlreadyConnected,
/// Not connected
NotConnected,
/// Send failed
SendFailed(String),
/// JSON serialization error
SerializationError(String),
/// JSON deserialization error
DeserializationError(String),
/// Queue is full
QueueFull,
/// Invalid URL
InvalidUrl(String),
/// HTTP header error
HeaderError(String),
/// IO error
IoError(String),
}
impl fmt::Display for WebSocketError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WebSocketError::ConnectionFailed(msg) => {
write!(f, "Connection failed: {}", msg)
}
WebSocketError::ConnectionTimeout => {
write!(f, "Connection timeout")
}
WebSocketError::AlreadyConnected => {
write!(f, "Already connected")
}
WebSocketError::NotConnected => {
write!(f, "Not connected")
}
WebSocketError::SendFailed(msg) => {
write!(f, "Send failed: {}", msg)
}
WebSocketError::SerializationError(msg) => {
write!(f, "Serialization error: {}", msg)
}
WebSocketError::DeserializationError(msg) => {
write!(f, "Deserialization error: {}", msg)
}
WebSocketError::QueueFull => {
write!(f, "Message queue is full")
}
WebSocketError::InvalidUrl(msg) => {
write!(f, "Invalid URL: {}", msg)
}
WebSocketError::HeaderError(msg) => {
write!(f, "Header error: {}", msg)
}
WebSocketError::IoError(msg) => {
write!(f, "IO error: {}", msg)
}
}
}
}
impl std::error::Error for WebSocketError {}
impl From<serde_json::Error> for WebSocketError {
fn from(err: serde_json::Error) -> Self {
WebSocketError::SerializationError(err.to_string())
}
}
impl From<url::ParseError> for WebSocketError {
fn from(err: url::ParseError) -> Self {
WebSocketError::InvalidUrl(err.to_string())
}
}
impl From<tokio_tungstenite::tungstenite::Error> for WebSocketError {
fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
match err {
tokio_tungstenite::tungstenite::Error::ConnectionClosed => {
WebSocketError::NotConnected
}
tokio_tungstenite::tungstenite::Error::Io(err) => {
WebSocketError::IoError(err.to_string())
}
_ => WebSocketError::ConnectionFailed(err.to_string()),
}
}
}
impl From<http::Error> for WebSocketError {
fn from(err: http::Error) -> Self {
WebSocketError::HeaderError(err.to_string())
}
}

View File

@@ -0,0 +1,85 @@
//! WebSocket message types
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// Standard WebSocket message format (matches MasterSuite backend)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebSocketMessage {
/// Message type
#[serde(rename = "Type")]
pub msg_type: String,
/// Message data
#[serde(rename = "Data", default)]
pub data: Value,
/// Optional message ID
#[serde(rename = "MessageId", skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
}
impl WebSocketMessage {
/// Create a new message
pub fn new(msg_type: impl Into<String>, data: Value) -> Self {
Self {
msg_type: msg_type.into(),
data,
id: None,
}
}
/// Create with message ID
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
/// Create a simple message with just type
pub fn simple(msg_type: impl Into<String>) -> Self {
Self::new(msg_type, Value::Null)
}
/// Create a message with JSON data
pub fn json<T: Serialize>(msg_type: impl Into<String>, data: &T) -> Result<Self, serde_json::Error> {
let data = serde_json::to_value(data)?;
Ok(Self::new(msg_type, data))
}
}
/// Client type enumeration
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientType {
/// Regular client application
Client,
/// Web interface
Web,
/// Updater program
Updater,
}
impl ClientType {
/// Parse from header value
pub fn from_header(value: &str) -> Self {
match value {
"App" | "Client" => ClientType::Client,
"Updater" => ClientType::Updater,
_ => ClientType::Web,
}
}
/// Convert to header value
pub fn as_header(&self) -> &'static str {
match self {
ClientType::Client => "App",
ClientType::Web => "Web",
ClientType::Updater => "Updater",
}
}
}
impl std::fmt::Display for ClientType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_header())
}
}

View File

@@ -0,0 +1,18 @@
//! WebSocket module
//!
//! Provides WebSocket client and server implementations with support for:
//! - Custom headers (ClientType)
//! - Auto-reconnect
//! - Heartbeat/ping-pong
//! - Message queuing
//! - Event callbacks
mod client;
mod config;
mod message;
mod error;
pub use client::WebSocketClient;
pub use config::WebSocketConfig;
pub use message::{WebSocketMessage, ClientType};
pub use error::WebSocketError;

View File

@@ -0,0 +1 @@
{"rustc_fingerprint":17656983458485528297,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.1 (e408947bf 2026-03-25)\nbinary: rustc\ncommit-hash: e408947bfd200af42db322daf0fadfe7e26d3bd1\ncommit-date: 2026-03-25\nhost: x86_64-pc-windows-msvc\nrelease: 1.94.1\nLLVM version: 21.1.8\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\xyzqm\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""}},"successes":{}}

View File

@@ -0,0 +1,3 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/