Skip to content

Commit 346d390

Browse files
maxholmanclaude
andcommitted
refactor(transport): deduplicate QUIC/WS client task setup
Extract spawn_client_tasks() in client.rs — shared by both QUIC and WS connect paths. Handles the oneshot handshake channel, control stream task, data-in task, and ConnectResult construction that was previously copy-pasted across both transports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d83adaa commit 346d390

3 files changed

Lines changed: 95 additions & 143 deletions

File tree

crates/core/src/client/client.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,87 @@ where
133133
}
134134
}
135135

136+
/// Spawn the control and data-in tasks shared by all client transports.
137+
///
138+
/// Called after the transport is established and the handshake has been
139+
/// queued on `control_tx`. Creates the handshake oneshot, control loop
140+
/// task, incoming data task, and returns a fully wired `ConnectResult`.
141+
pub fn spawn_client_tasks<T: wallhack_transport::Transport + 'static>(
142+
transport: Arc<T>,
143+
control_tx: mpsc::Sender<ControlMessage>,
144+
control_rx: mpsc::Receiver<ControlMessage>,
145+
peer_registry: Option<std::sync::Arc<crate::control::peers::Registry>>,
146+
remote_addr: String,
147+
) -> ConnectResult<T>
148+
where
149+
T::SendStream: 'static,
150+
T::RecvStream: 'static,
151+
T::BiStream: Send + 'static,
152+
{
153+
use crate::transport::protocol;
154+
155+
let (handshake_tx, handshake_rx) = oneshot::channel::<Handshake>();
156+
157+
let control_handle = {
158+
let transport = Arc::clone(&transport);
159+
tokio::spawn(async move {
160+
let mut channels = protocol::ControlChannels {
161+
outgoing_rx: control_rx,
162+
handshake_tx: Some(handshake_tx),
163+
control_response_tx: None,
164+
peer_registry,
165+
peer_name: None,
166+
};
167+
match protocol::run_control_stream_initiator(
168+
&*transport,
169+
&mut channels,
170+
None,
171+
std::time::Duration::from_secs(30),
172+
)
173+
.await
174+
{
175+
Ok(exit) => tracing::debug!("Control stream finished: {exit:?}"),
176+
Err(e) => tracing::debug!("Control stream error: {e}"),
177+
}
178+
})
179+
};
180+
181+
let channels = DataChannels::new();
182+
183+
let incoming_handle = {
184+
let transport = Arc::clone(&transport);
185+
let instructions_tx = channels.instructions_tx.clone();
186+
let responses_tx = channels.responses_tx.clone();
187+
tokio::spawn(async move {
188+
match transport.accept_uni().await {
189+
Ok(Some(mut recv)) => {
190+
if let Err(e) =
191+
protocol::run_data_in(&mut recv, &instructions_tx, &responses_tx).await
192+
{
193+
tracing::debug!("Data-in handler finished: {e}");
194+
}
195+
}
196+
Ok(None) => tracing::debug!("Transport closed before data-in stream accepted"),
197+
Err(e) => tracing::debug!("Failed to accept data-in stream: {e}"),
198+
}
199+
})
200+
};
201+
202+
let tasks = ConnectionTasks {
203+
incoming: incoming_handle,
204+
control: control_handle,
205+
};
206+
207+
ConnectResult::new(
208+
transport,
209+
channels,
210+
remote_addr,
211+
tasks,
212+
control_tx,
213+
Some(handshake_rx),
214+
)
215+
}
216+
136217
pub trait Client {
137218
type Error: std::error::Error + std::fmt::Debug + Send + Sync + 'static;
138219
type Transport: wallhack_transport::Transport;

crates/core/src/client/quic/mod.rs

Lines changed: 7 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,16 @@ use std::sync::Arc;
22

33
use quinn::{IdleTimeout, VarInt, crypto::rustls::QuicClientConfig};
44
use tokio::time::Instant;
5-
use wallhack_transport::Transport;
65

76
use crate::{
8-
ClientConfig, NodeRole,
9-
client::tls_config,
10-
psk::HandshakeExt,
11-
server::server::DataChannels,
12-
transport::{protocol, quic::QuicTransport},
7+
ClientConfig, NodeRole, client::tls_config, psk::HandshakeExt, transport::quic::QuicTransport,
138
};
149
use wallhack_wire::{
1510
control::{ControlMessage, control_message},
1611
data::Handshake,
1712
};
1813

19-
use super::client::{Client, ConnectResult, ConnectionTasks};
14+
use super::client::{Client, ConnectResult};
2015

2116
#[derive(thiserror::Error, Debug)]
2217
pub enum Error {
@@ -172,72 +167,12 @@ impl Client for QuicClient {
172167
})?;
173168
}
174169

175-
// Create oneshot for receiving server's Handshake via the control loop.
176-
let (handshake_tx, handshake_rx) = tokio::sync::oneshot::channel::<Handshake>();
177-
// Spawn control stream task
178-
let control_handle = {
179-
let transport = Arc::clone(&transport);
180-
let peer_registry = self.peer_registry.clone();
181-
tokio::spawn(async move {
182-
let mut channels = protocol::ControlChannels {
183-
outgoing_rx: control_rx,
184-
handshake_tx: Some(handshake_tx),
185-
control_response_tx: None,
186-
peer_registry,
187-
peer_name: None,
188-
};
189-
match protocol::run_control_stream_initiator(
190-
&*transport,
191-
&mut channels,
192-
None, // client doesn't handle ControlRequests
193-
std::time::Duration::from_secs(30),
194-
)
195-
.await
196-
{
197-
Ok(exit) => tracing::debug!("Control stream finished: {exit:?}"),
198-
Err(e) => tracing::debug!("Control stream error: {e}"),
199-
}
200-
})
201-
};
202-
203-
let channels = DataChannels::new();
204-
205-
// Incoming data task: accept uni stream from peer, dispatch messages.
206-
let incoming_handle = {
207-
let transport = Arc::clone(&transport);
208-
let instructions_tx = channels.instructions_tx.clone();
209-
let responses_tx = channels.responses_tx.clone();
210-
tokio::spawn(async move {
211-
match transport.accept_uni().await {
212-
Ok(Some(mut recv)) => {
213-
if let Err(e) =
214-
protocol::run_data_in(&mut recv, &instructions_tx, &responses_tx).await
215-
{
216-
tracing::debug!("Data-in handler finished: {e}");
217-
}
218-
}
219-
Ok(None) => tracing::debug!("Transport closed before data-in stream accepted"),
220-
Err(e) => tracing::debug!("Failed to accept data-in stream: {e}"),
221-
}
222-
})
223-
};
224-
225-
// Outgoing data task is NOT spawned here; the caller opens the uni stream
226-
// and drives run_send_instructions / run_send_responses as appropriate for
227-
// its role, consuming the receiver from DataChannels.
228-
229-
let tasks = ConnectionTasks {
230-
incoming: incoming_handle,
231-
control: control_handle,
232-
};
233-
234-
Ok(ConnectResult::new(
235-
Arc::clone(&transport),
236-
channels,
237-
remote_addr,
238-
tasks,
170+
Ok(super::client::spawn_client_tasks(
171+
transport,
239172
control_tx,
240-
Some(handshake_rx),
173+
control_rx,
174+
self.peer_registry.clone(),
175+
remote_addr,
241176
))
242177
}
243178

crates/core/src/client/ws/mod.rs

Lines changed: 7 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,10 @@ use crate::{
2727
NodeRole,
2828
client::config::ClientConfig,
2929
psk::HandshakeExt,
30-
server::server::DataChannels,
31-
transport::{
32-
Transport, protocol,
33-
websocket::{WebSocketByteStream, WebSocketTransport, WebSocketTransportConfig},
34-
},
30+
transport::websocket::{WebSocketByteStream, WebSocketTransport, WebSocketTransportConfig},
3531
};
3632

37-
use super::client::{ConnectResult, ConnectionTasks};
33+
use super::client::ConnectResult;
3834

3935
/// Errors that can occur in the WebSocket client.
4036
#[derive(Debug, thiserror::Error)]
@@ -389,72 +385,12 @@ impl WsClient {
389385
let _ = control_tx.send(msg).await;
390386
}
391387

392-
// Create oneshot for receiving server's Handshake via the control loop.
393-
let (handshake_tx, handshake_rx) = tokio::sync::oneshot::channel::<Handshake>();
394-
// Spawn control stream task
395-
let control_handle = {
396-
let transport = Arc::clone(&transport);
397-
let peer_registry = self.peer_registry.clone();
398-
tokio::spawn(async move {
399-
let mut channels = protocol::ControlChannels {
400-
outgoing_rx: control_rx,
401-
handshake_tx: Some(handshake_tx), // receive server's Handshake
402-
control_response_tx: None,
403-
peer_registry,
404-
peer_name: None,
405-
};
406-
match protocol::run_control_stream_initiator(
407-
&*transport,
408-
&mut channels,
409-
None, // client doesn't handle ControlRequests
410-
std::time::Duration::from_secs(30),
411-
)
412-
.await
413-
{
414-
Ok(exit) => tracing::debug!("Control stream finished: {exit:?}"),
415-
Err(e) => tracing::debug!("Control stream error: {e}"),
416-
}
417-
})
418-
};
419-
420-
let channels = DataChannels::new();
421-
422-
// Incoming data task: accept uni stream from peer, dispatch messages.
423-
let incoming_handle = {
424-
let transport = Arc::clone(&transport);
425-
let instructions_tx = channels.instructions_tx.clone();
426-
let responses_tx = channels.responses_tx.clone();
427-
tokio::spawn(async move {
428-
match transport.accept_uni().await {
429-
Ok(Some(mut recv)) => {
430-
if let Err(e) =
431-
protocol::run_data_in(&mut recv, &instructions_tx, &responses_tx).await
432-
{
433-
tracing::debug!("Data-in handler finished: {e}");
434-
}
435-
}
436-
Ok(None) => tracing::debug!("Transport closed before data-in stream accepted"),
437-
Err(e) => tracing::debug!("Failed to accept data-in stream: {e}"),
438-
}
439-
})
440-
};
441-
442-
// Outgoing data task is NOT spawned here; the caller opens the uni stream
443-
// and drives run_send_instructions / run_send_responses as appropriate for
444-
// its role, consuming the receiver from DataChannels.
445-
446-
let tasks = ConnectionTasks {
447-
incoming: incoming_handle,
448-
control: control_handle,
449-
};
450-
451-
Ok(ConnectResult::new(
452-
Arc::clone(&transport),
453-
channels,
454-
remote_addr_str,
455-
tasks,
388+
Ok(super::client::spawn_client_tasks(
389+
transport,
456390
control_tx,
457-
Some(handshake_rx),
391+
control_rx,
392+
self.peer_registry.clone(),
393+
remote_addr_str,
458394
))
459395
}
460396
}

0 commit comments

Comments
 (0)