add ws client and server packages
This commit is contained in:
1
client_ws/.gitignore
vendored
Normal file
1
client_ws/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
31
client_ws/Cargo.toml
Normal file
31
client_ws/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "circle_client_ws"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
uuid = { version = "1.6", features = ["v4", "serde", "js"] }
|
||||
log = "0.4"
|
||||
futures-channel = { version = "0.3", features = ["sink"] } # For mpsc
|
||||
futures-util = { version = "0.3", features = ["sink"] } # For StreamExt, SinkExt
|
||||
thiserror = "1.0"
|
||||
async-trait = "0.1" # May be needed for abstracting WS connection
|
||||
|
||||
# WASM-specific dependencies
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
gloo-net = { version = "0.4.0", features = ["websocket"] }
|
||||
wasm-bindgen-futures = "0.4"
|
||||
gloo-console = "0.3.0" # For wasm logging if needed, or use `log` with wasm_logger
|
||||
|
||||
# Native-specific dependencies
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio-tungstenite = { version = "0.23.0", features = ["native-tls"] }
|
||||
tokio = { version = "1", features = ["rt", "macros"] } # For tokio::spawn on native
|
||||
url = "2.5.0" # For native WebSocket connection
|
||||
|
||||
[dev-dependencies]
|
||||
# For examples within this crate, if any, or for testing
|
||||
env_logger = "0.10"
|
||||
# tokio = { version = "1", features = ["full"] } # If examples need full tokio runtime
|
86
client_ws/README.md
Normal file
86
client_ws/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Circle WebSocket Client (`circle_client_ws`)
|
||||
|
||||
This crate provides a WebSocket client (`CircleWsClient`) designed to interact with a server that expects JSON-RPC messages, specifically for executing Rhai scripts.
|
||||
|
||||
It is designed to be compatible with both WebAssembly (WASM) environments (e.g., web browsers) and native Rust applications.
|
||||
|
||||
## Features
|
||||
|
||||
- **Cross-Platform:** Works in WASM and native environments.
|
||||
- Uses `gloo-net` for WebSockets in WASM.
|
||||
- Uses `tokio-tungstenite` for WebSockets in native applications.
|
||||
- **JSON-RPC Communication:** Implements client-side JSON-RPC 2.0 request and response handling.
|
||||
- **Rhai Script Execution:** Provides a `play(script: String)` method to send Rhai scripts to the server for execution and receive their output.
|
||||
- **Asynchronous Operations:** Leverages `async/await` and `futures` for non-blocking communication.
|
||||
- **Connection Management:** Supports connecting to and disconnecting from a WebSocket server.
|
||||
- **Error Handling:** Defines a comprehensive `CircleWsClientError` enum for various client-side errors.
|
||||
|
||||
## Core Component
|
||||
|
||||
- **`CircleWsClient`**: The main client struct.
|
||||
- `new(ws_url: String)`: Creates a new client instance targeting the given WebSocket URL.
|
||||
- `connect()`: Establishes the WebSocket connection.
|
||||
- `play(script: String)`: Sends a Rhai script to the server for execution and returns the result.
|
||||
- `disconnect()`: Closes the WebSocket connection.
|
||||
|
||||
## Usage Example (Conceptual)
|
||||
|
||||
```rust
|
||||
use circle_client_ws::CircleWsClient;
|
||||
|
||||
async fn run_client() {
|
||||
let mut client = CircleWsClient::new("ws://localhost:8080/ws".to_string());
|
||||
|
||||
if let Err(e) = client.connect().await {
|
||||
eprintln!("Failed to connect: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
let script = "print(\"Hello from Rhai via WebSocket!\"); 40 + 2".to_string();
|
||||
|
||||
match client.play(script).await {
|
||||
Ok(result) => {
|
||||
println!("Script output: {}", result.output);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error during play: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
client.disconnect().await;
|
||||
}
|
||||
|
||||
// To run this example, you'd need an async runtime like tokio for native
|
||||
// or wasm-bindgen-test for WASM.
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### Native
|
||||
```bash
|
||||
cargo build
|
||||
```
|
||||
|
||||
### WASM
|
||||
```bash
|
||||
cargo build --target wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
Key dependencies include:
|
||||
|
||||
- `serde`, `serde_json`: For JSON serialization/deserialization.
|
||||
- `futures-channel`, `futures-util`: For asynchronous stream and sink handling.
|
||||
- `uuid`: For generating unique request IDs.
|
||||
- `log`: For logging.
|
||||
- `thiserror`: For error type definitions.
|
||||
|
||||
**WASM-specific:**
|
||||
- `gloo-net`: For WebSocket communication in WASM.
|
||||
- `wasm-bindgen-futures`: To bridge Rust futures with JavaScript promises.
|
||||
|
||||
**Native-specific:**
|
||||
- `tokio-tungstenite`: For WebSocket communication in native environments.
|
||||
- `tokio`: Asynchronous runtime for native applications.
|
||||
- `url`: For URL parsing.
|
419
client_ws/src/lib.rs
Normal file
419
client_ws/src/lib.rs
Normal file
@@ -0,0 +1,419 @@
|
||||
use futures_channel::{mpsc, oneshot};
|
||||
use futures_util::{StreamExt, SinkExt, FutureExt};
|
||||
use log::{debug, error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
// Platform-specific WebSocket imports and spawn function
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use {
|
||||
gloo_net::websocket::{futures::WebSocket, Message as GlooWsMessage, WebSocketError as GlooWebSocketError},
|
||||
wasm_bindgen_futures::spawn_local,
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use {
|
||||
tokio_tungstenite::{
|
||||
connect_async,
|
||||
tungstenite::protocol::Message as TungsteniteWsMessage,
|
||||
// tungstenite::error::Error as TungsteniteError, // Unused
|
||||
},
|
||||
tokio::spawn as spawn_local, // Use tokio::spawn for native
|
||||
// url::Url, // Url::parse is not used in the final connect_async call path
|
||||
};
|
||||
|
||||
|
||||
// JSON-RPC Structures (client-side perspective)
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct JsonRpcRequestClient {
|
||||
jsonrpc: String,
|
||||
method: String,
|
||||
params: Value,
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct JsonRpcResponseClient {
|
||||
jsonrpc: String,
|
||||
pub result: Option<Value>,
|
||||
pub error: Option<JsonRpcErrorClient>,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct JsonRpcErrorClient {
|
||||
pub code: i32,
|
||||
pub message: String,
|
||||
pub data: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct PlayParamsClient {
|
||||
pub script: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct PlayResultClient {
|
||||
pub output: String,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CircleWsClientError {
|
||||
#[error("WebSocket connection error: {0}")]
|
||||
ConnectionError(String),
|
||||
#[error("WebSocket send error: {0}")]
|
||||
SendError(String),
|
||||
#[error("WebSocket receive error: {0}")]
|
||||
ReceiveError(String),
|
||||
#[error("JSON serialization/deserialization error: {0}")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
#[error("Request timed out for request ID: {0}")]
|
||||
Timeout(String),
|
||||
#[error("JSON-RPC error response: {code} - {message}")]
|
||||
JsonRpcError { code: i32, message: String, data: Option<Value> },
|
||||
#[error("No response received for request ID: {0}")]
|
||||
NoResponse(String),
|
||||
#[error("Client is not connected")]
|
||||
NotConnected,
|
||||
#[error("Internal channel error: {0}")]
|
||||
ChannelError(String),
|
||||
}
|
||||
|
||||
// Wrapper for messages sent to the WebSocket task
|
||||
enum InternalWsMessage {
|
||||
SendJsonRpc(JsonRpcRequestClient, oneshot::Sender<Result<JsonRpcResponseClient, CircleWsClientError>>),
|
||||
Close,
|
||||
}
|
||||
|
||||
pub struct CircleWsClient {
|
||||
ws_url: String,
|
||||
// Sender to the internal WebSocket task
|
||||
internal_tx: Option<mpsc::Sender<InternalWsMessage>>,
|
||||
// Handle to the spawned WebSocket task (for native, to join on drop if desired)
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
task_handle: Option<tokio::task::JoinHandle<()>>,
|
||||
// Callback for unsolicited messages (e.g. notifications from server)
|
||||
// For Yew, this would typically be a yew::Callback<JsonRpcResponseClient> or similar
|
||||
// For simplicity in this generic client, we'll use a Box<dyn Fn(JsonRpcResponseClient) + Send + Sync>
|
||||
// This part is more complex to make fully generic and easy for Yew, so keeping it simple for now.
|
||||
// unsolicited_message_callback: Option<Box<dyn Fn(JsonRpcResponseClient) + Send + Sync + 'static>>,
|
||||
}
|
||||
|
||||
impl CircleWsClient {
|
||||
pub fn new(ws_url: String) -> Self {
|
||||
Self {
|
||||
ws_url,
|
||||
internal_tx: None,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
task_handle: None,
|
||||
// unsolicited_message_callback: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect(&mut self) -> Result<(), CircleWsClientError> {
|
||||
if self.internal_tx.is_some() {
|
||||
info!("Client already connected or connecting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (internal_tx, internal_rx) = mpsc::channel::<InternalWsMessage>(32);
|
||||
self.internal_tx = Some(internal_tx);
|
||||
|
||||
let url = self.ws_url.clone();
|
||||
|
||||
// Pending requests: map request_id to a oneshot sender for the response
|
||||
let pending_requests: Arc<Mutex<HashMap<String, oneshot::Sender<Result<JsonRpcResponseClient, CircleWsClientError>>>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
let task_pending_requests = pending_requests.clone();
|
||||
|
||||
let task = async move {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let ws_result = WebSocket::open(&url);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let connect_attempt = async {
|
||||
// Validate URL parsing separately if needed, but connect_async takes &str
|
||||
// let parsed_url = Url::parse(&url).map_err(|e| CircleWsClientError::ConnectionError(format!("Invalid URL: {}", e)))?;
|
||||
connect_async(&url).await.map_err(|e| CircleWsClientError::ConnectionError(e.to_string()))
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let ws_result = connect_attempt.await;
|
||||
|
||||
match ws_result {
|
||||
Ok(ws_conn_maybe_response) => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let ws_conn = ws_conn_maybe_response;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let (ws_conn, _) = ws_conn_maybe_response;
|
||||
|
||||
info!("Successfully connected to WebSocket: {}", url);
|
||||
let (mut ws_tx, mut ws_rx) = ws_conn.split();
|
||||
let mut internal_rx_fused = internal_rx.fuse();
|
||||
|
||||
loop {
|
||||
futures_util::select! {
|
||||
// Handle messages from the client's public methods (e.g., play)
|
||||
internal_msg = internal_rx_fused.next().fuse() => {
|
||||
match internal_msg {
|
||||
Some(InternalWsMessage::SendJsonRpc(req, response_sender)) => {
|
||||
let req_id = req.id.clone();
|
||||
match serde_json::to_string(&req) {
|
||||
Ok(req_str) => {
|
||||
debug!("Sending JSON-RPC request (ID: {}): {}", req_id, req_str);
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let send_res = ws_tx.send(GlooWsMessage::Text(req_str)).await;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let send_res = ws_tx.send(TungsteniteWsMessage::Text(req_str)).await;
|
||||
|
||||
if let Err(e) = send_res {
|
||||
error!("WebSocket send error for request ID {}: {:?}", req_id, e);
|
||||
let _ = response_sender.send(Err(CircleWsClientError::SendError(e.to_string())));
|
||||
} else {
|
||||
// Store the sender to await the response
|
||||
task_pending_requests.lock().unwrap().insert(req_id, response_sender);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to serialize request ID {}: {}", req_id, e);
|
||||
let _ = response_sender.send(Err(CircleWsClientError::JsonError(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(InternalWsMessage::Close) => {
|
||||
info!("Close message received internally, closing WebSocket.");
|
||||
let _ = ws_tx.close().await;
|
||||
break;
|
||||
}
|
||||
None => { // internal_rx closed, meaning client was dropped
|
||||
info!("Internal MPSC channel closed, WebSocket task shutting down.");
|
||||
let _ = ws_tx.close().await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Handle messages received from the WebSocket server
|
||||
ws_msg_res = ws_rx.next().fuse() => {
|
||||
match ws_msg_res {
|
||||
Some(Ok(msg)) => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
match msg {
|
||||
GlooWsMessage::Text(text) => {
|
||||
debug!("Received WebSocket message: {}", text);
|
||||
// ... (parse logic as before)
|
||||
match serde_json::from_str::<JsonRpcResponseClient>(&text) {
|
||||
Ok(response) => {
|
||||
if let Some(sender) = task_pending_requests.lock().unwrap().remove(&response.id) {
|
||||
if let Err(failed_send_val) = sender.send(Ok(response)) {
|
||||
if let Ok(resp_for_log) = failed_send_val { warn!("Failed to send response to waiting task for ID: {}", resp_for_log.id); }
|
||||
else { warn!("Failed to send response to waiting task, and also failed to get original response for logging.");}
|
||||
}
|
||||
} else { warn!("Received response for unknown request ID or unsolicited message: {:?}", response); }
|
||||
}
|
||||
Err(e) => { error!("Failed to parse JSON-RPC response: {}. Raw: {}", e, text); }
|
||||
}
|
||||
}
|
||||
GlooWsMessage::Bytes(_) => {
|
||||
debug!("Received binary WebSocket message (WASM).");
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
match msg {
|
||||
TungsteniteWsMessage::Text(text) => {
|
||||
debug!("Received WebSocket message: {}", text);
|
||||
// ... (parse logic as before)
|
||||
match serde_json::from_str::<JsonRpcResponseClient>(&text) {
|
||||
Ok(response) => {
|
||||
if let Some(sender) = task_pending_requests.lock().unwrap().remove(&response.id) {
|
||||
if let Err(failed_send_val) = sender.send(Ok(response)) {
|
||||
if let Ok(resp_for_log) = failed_send_val { warn!("Failed to send response to waiting task for ID: {}", resp_for_log.id); }
|
||||
else { warn!("Failed to send response to waiting task, and also failed to get original response for logging.");}
|
||||
}
|
||||
} else { warn!("Received response for unknown request ID or unsolicited message: {:?}", response); }
|
||||
}
|
||||
Err(e) => { error!("Failed to parse JSON-RPC response: {}. Raw: {}", e, text); }
|
||||
}
|
||||
}
|
||||
TungsteniteWsMessage::Binary(_) => {
|
||||
debug!("Received binary WebSocket message (Native).");
|
||||
}
|
||||
TungsteniteWsMessage::Ping(_) | TungsteniteWsMessage::Pong(_) => {
|
||||
debug!("Received Ping/Pong (Native).");
|
||||
}
|
||||
TungsteniteWsMessage::Close(_) => {
|
||||
info!("WebSocket connection closed by server (Native).");
|
||||
break;
|
||||
}
|
||||
TungsteniteWsMessage::Frame(_) => {
|
||||
debug!("Received Frame (Native) - not typically handled directly.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
error!("WebSocket receive error: {:?}", e);
|
||||
break; // Exit loop on receive error
|
||||
}
|
||||
None => { // WebSocket stream closed
|
||||
info!("WebSocket connection closed by server (stream ended).");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cleanup pending requests on exit
|
||||
task_pending_requests.lock().unwrap().drain().for_each(|(_, sender)| {
|
||||
let _ = sender.send(Err(CircleWsClientError::ConnectionError("WebSocket task terminated".to_string())));
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to connect to WebSocket: {:?}", e);
|
||||
// Notify any waiting senders about the connection failure
|
||||
internal_rx.for_each(|msg| async {
|
||||
if let InternalWsMessage::SendJsonRpc(_, response_sender) = msg {
|
||||
let _ = response_sender.send(Err(CircleWsClientError::ConnectionError(e.to_string())));
|
||||
}
|
||||
}).await;
|
||||
}
|
||||
}
|
||||
info!("WebSocket task finished.");
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
spawn_local(task);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{ self.task_handle = Some(spawn_local(task)); }
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn play(&self, script: String) -> impl std::future::Future<Output = Result<PlayResultClient, CircleWsClientError>> + Send + 'static {
|
||||
let req_id_outer = Uuid::new_v4().to_string();
|
||||
|
||||
// Clone the sender option. The sender itself (mpsc::Sender) is also Clone.
|
||||
let internal_tx_clone_opt = self.internal_tx.clone();
|
||||
|
||||
async move {
|
||||
let req_id = req_id_outer; // Move req_id into the async block
|
||||
let params = PlayParamsClient { script }; // script is moved in
|
||||
|
||||
let request = match serde_json::to_value(params) {
|
||||
Ok(p_val) => JsonRpcRequestClient {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
method: "play".to_string(),
|
||||
params: p_val,
|
||||
id: req_id.clone(),
|
||||
},
|
||||
Err(e) => return Err(CircleWsClientError::JsonError(e)),
|
||||
};
|
||||
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
|
||||
if let Some(mut internal_tx) = internal_tx_clone_opt {
|
||||
internal_tx.send(InternalWsMessage::SendJsonRpc(request, response_tx)).await
|
||||
.map_err(|e| CircleWsClientError::ChannelError(format!("Failed to send request to internal task: {}", e)))?;
|
||||
} else {
|
||||
return Err(CircleWsClientError::NotConnected);
|
||||
}
|
||||
|
||||
// Add a timeout for waiting for the response
|
||||
// For simplicity, using a fixed timeout here. Could be configurable.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
match response_rx.await {
|
||||
Ok(Ok(rpc_response)) => {
|
||||
if let Some(json_rpc_error) = rpc_response.error {
|
||||
Err(CircleWsClientError::JsonRpcError {
|
||||
code: json_rpc_error.code,
|
||||
message: json_rpc_error.message,
|
||||
data: json_rpc_error.data,
|
||||
})
|
||||
} else if let Some(result_value) = rpc_response.result {
|
||||
serde_json::from_value(result_value).map_err(CircleWsClientError::JsonError)
|
||||
} else {
|
||||
Err(CircleWsClientError::NoResponse(req_id.clone()))
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => Err(e), // Error propagated from the ws task
|
||||
Err(_) => Err(CircleWsClientError::Timeout(req_id.clone())), // oneshot channel cancelled
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
use tokio::time::timeout as tokio_timeout;
|
||||
match tokio_timeout(std::time::Duration::from_secs(10), response_rx).await {
|
||||
Ok(Ok(Ok(rpc_response))) => { // Timeout -> Result<ChannelRecvResult, Error>
|
||||
if let Some(json_rpc_error) = rpc_response.error {
|
||||
Err(CircleWsClientError::JsonRpcError {
|
||||
code: json_rpc_error.code,
|
||||
message: json_rpc_error.message,
|
||||
data: json_rpc_error.data,
|
||||
})
|
||||
} else if let Some(result_value) = rpc_response.result {
|
||||
serde_json::from_value(result_value).map_err(CircleWsClientError::JsonError)
|
||||
} else {
|
||||
Err(CircleWsClientError::NoResponse(req_id.clone()))
|
||||
}
|
||||
}
|
||||
Ok(Ok(Err(e))) => Err(e), // Error propagated from the ws task
|
||||
Ok(Err(_)) => Err(CircleWsClientError::ChannelError("Response channel cancelled".to_string())), // oneshot cancelled
|
||||
Err(_) => Err(CircleWsClientError::Timeout(req_id.clone())), // tokio_timeout expired
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn disconnect(&mut self) {
|
||||
if let Some(mut tx) = self.internal_tx.take() {
|
||||
info!("Sending close signal to internal WebSocket task.");
|
||||
let _ = tx.send(InternalWsMessage::Close).await;
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(handle) = self.task_handle.take() {
|
||||
let _ = handle.await; // Wait for the task to finish
|
||||
}
|
||||
info!("Client disconnected.");
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure client cleans up on drop for native targets
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl Drop for CircleWsClient {
|
||||
fn drop(&mut self) {
|
||||
if self.internal_tx.is_some() || self.task_handle.is_some() {
|
||||
warn!("CircleWsClient dropped without explicit disconnect. Spawning task to send close signal.");
|
||||
// We can't call async disconnect directly in drop.
|
||||
// Spawn a new task to send the close message if on native.
|
||||
if let Some(mut tx) = self.internal_tx.take() {
|
||||
spawn_local(async move {
|
||||
info!("Drop: Sending close signal to internal WebSocket task.");
|
||||
let _ = tx.send(InternalWsMessage::Close).await;
|
||||
});
|
||||
}
|
||||
if let Some(handle) = self.task_handle.take() {
|
||||
spawn_local(async move {
|
||||
info!("Drop: Waiting for WebSocket task to finish.");
|
||||
let _ = handle.await;
|
||||
info!("Drop: WebSocket task finished.");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// use super::*;
|
||||
#[test]
|
||||
fn it_compiles() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user