State as struct property vs as function parameter

So I have a struct called Api:

pub struct Api {
    rest_api_url: String,
    ws_api_url: String,
    ws_stream_url: String,
    api_key: String,
    api_secret: String,
}

It contains properties for the different endpoints to a API and the API-credentials. The struct Api contains methods to make REST-API calls but also endpoints to make calls to a websocket API. The purpose of the websocket API is for usecases where low latency and fast response time is important.

The struct contains the following function:

async fn ws_order(&self, params: &mut std::collections::HashMap<String, String>, ws_send: Arc<Mutex<SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>>) {
    //...
    // Create JSON from params...
    //...
     
    // Send call over websocket to API.
    ws_send.lock().await.send(Message::Text(json.to_string())).await;

}

The Api also contains another function. I will include it in this question to explain my issue:

async fn order(&self, params: &mut std::collections::HashMap<String, String>) -> Result<String, String> {
    // Make HTTP client.
    let http_client = reqwest::Client::new();
    // ...
    // Make HTTP request from params etc...
   
    // Send request to REST API
    request.send().await
}

Both ws_order and order do the same action: Making an order. The difference between the two is that order makes a HTTP request. Each time order is called the function creates a HTTP client, forges the request, and send it. ws_order is meant to be faster and with a lower latency. That is why the function ws_order does not create a websocket client each time it is called. Instead the websocket stream is created only once.

        // Make websocket stream to API endpoint for making orders.
        let url = url::Url::parse(api.get_urls("ws_api_url").unwrap()).unwrap();
        let (ws_stream, _) = connect_async(url).await.expect("Failed to connect");
        let (mut send, mut read) = ws_stream.split();
       
        let send = Arc::new(Mutex::new(send));

Then the variable send is passed as parameter to a lot of different functions from another struct. E.g:

pub fn start(self, psql: Psql, datastream: Arc<Mutex<mpsc::Receiver<CandleStick>>>, api: Api, ws_send: Arc<Mutex<SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>>) -> tokio::task::JoinHandle<()>;
//
async fn execute(&self, psql: Psql, data: &std::vec::Vec<CandleStick>, api: Api, ws_send: Arc<Mutex<SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>>);
//
async fn process(&self, psql: Psql, r: f64, api: Api, ws_send: Arc<Mutex<SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>>) -> Result<(), String>;
//
async fn buy(&self, psql: Psql, r: f64, api: Api, ws_send: Arc<Mutex<SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>>) -> Result<(), String>;

In the last function (buy) the following code is called:

api.ws_order(&mut params, ws_send).await;

Note that api is an instance of Api created only once in the main() function and passed as parameter to all the functions who requires it. ws_send is the SendStream from the websocket split.

This way:

  • I only have to generate a websocket connection once.
  • The connection is split into a Send/Receive stream. The Send-stream is passed as parameter to all the functions.
  • The final function (buy) can simply send a message through the Send-stream to the API endpoint.
  • This is more performant then a REST-API or a function who needs to initiate a new websocket connection on each call.

This code works, but I am wondering if there is a more elegant solution. What bothers me a bit is that I currently have to pass both Api and ws_send to the functions requiring it. Api is needed so the functions can call api.ws_order(...) and ws_send is needed because ws_order takes the sendstream as parameter to send the request to the API.

Wouldn't the following be better?

pub struct Api {
    rest_api_url: String,
    ws_api_url: String,
    ws_stream_url: String,
    api_key: String,
    api_secret: String,
    ws_send: Option<Arc<Mutex<SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>>>,
}
// This is a function in the struct Api
async fn ws_order(&self, params: &mut std::collections::HashMap<String, String>) {

    // Check if self.ws_send is set already in a previous call. If not, make it so this and future calls can use it.
    let mut ws_send = match self.ws_send {
         Some(s) => self.ws_send,
         None => {
             let url = url::Url::parse(self.get_urls("ws_api_url").unwrap()).unwrap();
             let (ws_stream, _) = connect_async(url).await.expect("Failed to connect");
             let (mut send, mut read) = ws_stream.split();
       
             Arc::new(Mutex::new(send))
         }
    }

    //...
    // Create JSON from params...
    //...
     
    // Send call over websocket to API.
    ws_send.lock().await.send(Message::Text(json.to_string())).await;

}
// Now ws_order can be called from other functions like:
async fn buy(&self, psql: Psql, r: f64, api: Api) -> Result<(), String>;

In this implementation I'd add a property to my struct Api holding ws_send. If ws_order is called for the first time, I initiate the websocket connection. If it is called when the websocket connection already exists I just retrieve it from the property.

Because all the functions require api: Api as parameter anyway, ws_send would be accessible that way instead of being a separate parameter.

What would be the best solution? Note that I am using a lot of threads and async.