Skip to main content

clankerdiff_client/
client.rs

1use crate::{
2    ClientError, ClientOptions, ClientState, ConnectionState, DiffReviewEvent, DiffScope,
3    ReconnectPolicy, RemoteError, RepositoryAction, platform,
4    protocol::{
5        client::{ClientCommand, LocalClientTransport},
6        server::ServerEvent,
7        shared::{Event, LIVE_PROTOCOL_VERSION, RemoteErrorCode},
8    },
9    transport::{ClientMessageTransport, Decoded, Transport},
10};
11#[cfg(feature = "websocket")]
12use crate::{ConnectionHeader, transport::Reconnecting};
13use async_channel::{Receiver, Sender, unbounded};
14use futures_util::FutureExt;
15use std::{sync::Arc, time::Duration};
16use tokio::sync::{oneshot, watch};
17
18const IDLE_TIMEOUT: Duration = Duration::from_secs(45);
19
20type Reply = oneshot::Sender<Result<(), ClientError>>;
21type Ready = oneshot::Receiver<Result<(), ClientError>>;
22
23#[derive(Clone)]
24pub struct DiffClient {
25    commands: Sender<Command>,
26    state: watch::Receiver<Arc<ClientState>>,
27}
28
29pub struct ClientSubscription {
30    state: watch::Receiver<Arc<ClientState>>,
31}
32
33impl ClientSubscription {
34    #[must_use]
35    pub fn latest(&self) -> Arc<ClientState> {
36        self.state.borrow().clone()
37    }
38
39    pub async fn changed(&mut self) -> Result<Arc<ClientState>, ClientError> {
40        self.state
41            .changed()
42            .await
43            .map_err(|_| ClientError::Disconnected)?;
44        Ok(self.state.borrow_and_update().clone())
45    }
46
47    /// Waits until the published state satisfies `predicate`, or `timeout` elapses.
48    ///
49    /// # Errors
50    ///
51    /// Returns [`ClientError::Disconnected`] when the client stops publishing or
52    /// the timeout elapses first.
53    pub async fn wait_until(
54        &mut self,
55        timeout: Duration,
56        mut predicate: impl FnMut(&ClientState) -> bool,
57    ) -> Result<Arc<ClientState>, ClientError> {
58        let deadline = platform::sleep(timeout).fuse();
59        futures_util::pin_mut!(deadline);
60        loop {
61            let state = self.latest();
62            if predicate(&state) {
63                return Ok(state);
64            }
65            let changed = self.changed().fuse();
66            futures_util::pin_mut!(changed);
67            futures_util::select! {
68                state = changed => { state?; }
69                () = deadline => return Err(ClientError::Disconnected),
70            }
71        }
72    }
73}
74
75impl DiffClient {
76    #[cfg(feature = "websocket")]
77    pub async fn connect(url: &str, options: ClientOptions) -> Result<Self, ClientError> {
78        Self::connect_with_headers(url, options, Vec::new()).await
79    }
80
81    #[cfg(feature = "websocket")]
82    pub async fn connect_with_headers(
83        url: &str,
84        options: ClientOptions,
85        headers: Vec<ConnectionHeader>,
86    ) -> Result<Self, ClientError> {
87        let transport = Reconnecting::connect(url, headers).await?;
88        Self::start(transport, options).await
89    }
90
91    pub async fn from_transport(
92        transport: LocalClientTransport,
93        options: ClientOptions,
94    ) -> Result<Self, ClientError> {
95        Self::start(transport, options).await
96    }
97
98    pub async fn from_messages(
99        transport: impl ClientMessageTransport,
100        options: ClientOptions,
101    ) -> Result<Self, ClientError> {
102        Self::start(Decoded::new(transport), options).await
103    }
104
105    #[must_use]
106    pub fn spawn(transport: impl ClientMessageTransport, options: ClientOptions) -> Self {
107        Self::start_worker(Decoded::new(transport), options).0
108    }
109
110    async fn start(transport: impl Transport, options: ClientOptions) -> Result<Self, ClientError> {
111        let (client, ready) = Self::start_worker(transport, options);
112        ready.await.map_err(|_| ClientError::Disconnected)??;
113        Ok(client)
114    }
115
116    fn start_worker(transport: impl Transport, options: ClientOptions) -> (Self, Ready) {
117        let (commands, rx) = unbounded();
118        let (state_tx, state) = watch::channel(Arc::new(ClientState::default()));
119        let (ready_tx, ready_rx) = oneshot::channel();
120        platform::spawn(async move {
121            Worker {
122                commands: rx,
123                state_tx,
124                options,
125                client: ClientState::default(),
126                handshake: Handshake::default(),
127                pending: None,
128                closed: false,
129            }
130            .run(transport, ready_tx)
131            .await;
132        });
133        (Self { commands, state }, ready_rx)
134    }
135
136    #[must_use]
137    pub fn state(&self) -> Arc<ClientState> {
138        self.state.borrow().clone()
139    }
140
141    #[must_use]
142    pub fn subscribe(&self) -> ClientSubscription {
143        ClientSubscription {
144            state: self.state.clone(),
145        }
146    }
147
148    pub async fn set_scope(&self, scope: DiffScope) -> Result<(), ClientError> {
149        self.request(ClientCommand::SetScope(scope)).await
150    }
151
152    pub async fn apply(&self, action: RepositoryAction) -> Result<(), ClientError> {
153        self.request(ClientCommand::Apply(action)).await
154    }
155
156    pub async fn refresh(&self) -> Result<(), ClientError> {
157        self.request(ClientCommand::Refresh).await
158    }
159
160    pub async fn handle(&self, event: DiffReviewEvent) -> Result<(), ClientError> {
161        match self.dispatch(event)? {
162            Some(reply) => reply.await.map_err(|_| ClientError::Disconnected)?,
163            None => Ok(()),
164        }
165    }
166
167    pub fn dispatch(
168        &self,
169        event: DiffReviewEvent,
170    ) -> Result<Option<oneshot::Receiver<Result<(), ClientError>>>, ClientError> {
171        let request = match event {
172            DiffReviewEvent::RepositoryAction(action) => ClientCommand::Apply(action),
173            DiffReviewEvent::SetScope(scope) => ClientCommand::SetScope(scope),
174            DiffReviewEvent::Refresh => ClientCommand::Refresh,
175            DiffReviewEvent::SubmitReview(submission) => ClientCommand::Submit(submission),
176            DiffReviewEvent::Cancel => ClientCommand::Cancel,
177            DiffReviewEvent::CopyFormattedReview(_) => return Ok(None),
178        };
179        let (reply, receiver) = oneshot::channel();
180        self.commands
181            .try_send(Command::Request(request, reply))
182            .map_err(|_| ClientError::Disconnected)?;
183        Ok(Some(receiver))
184    }
185
186    pub async fn close(self) -> Result<(), ClientError> {
187        self.command(Command::Close).await
188    }
189
190    async fn request(&self, request: ClientCommand) -> Result<(), ClientError> {
191        self.command(|reply| Command::Request(request, reply)).await
192    }
193
194    async fn command(&self, build: impl FnOnce(Reply) -> Command) -> Result<(), ClientError> {
195        let (reply, rx) = oneshot::channel();
196        self.commands
197            .send(build(reply))
198            .await
199            .map_err(|_| ClientError::Disconnected)?;
200        rx.await.map_err(|_| ClientError::Disconnected)?
201    }
202}
203
204enum Command {
205    Request(ClientCommand, Reply),
206    Close(Reply),
207}
208
209enum ConnectionInput {
210    Command(Option<Command>),
211    Event(Result<ServerEvent, ClientError>),
212    Idle,
213}
214
215struct Pending {
216    reply: Reply,
217    outcome_unknown_on_disconnect: bool,
218}
219
220struct Worker {
221    commands: Receiver<Command>,
222    state_tx: watch::Sender<Arc<ClientState>>,
223    options: ClientOptions,
224    client: ClientState,
225    handshake: Handshake,
226    pending: Option<Pending>,
227    closed: bool,
228}
229
230#[derive(Default)]
231struct Handshake {
232    initialized: bool,
233}
234
235impl Handshake {
236    fn reset(&mut self) {
237        self.initialized = false;
238    }
239
240    fn check(&mut self, event: &ServerEvent) -> Result<(), ClientError> {
241        match event {
242            Event::Initialize {
243                protocol_version, ..
244            } => {
245                if *protocol_version != LIVE_PROTOCOL_VERSION {
246                    return Err(ClientError::Remote(RemoteError::new(
247                        RemoteErrorCode::UnsupportedVersion,
248                        "unsupported live protocol version",
249                    )));
250                }
251                if std::mem::replace(&mut self.initialized, true) {
252                    return Err(ClientError::Protocol("repeated initialization".to_owned()));
253                }
254                Ok(())
255            }
256            Event::Error(_) => Ok(()),
257            _ if !self.initialized => {
258                Err(ClientError::Protocol("expected initialization".to_owned()))
259            }
260            _ => Ok(()),
261        }
262    }
263}
264
265impl Worker {
266    fn publish(&self) {
267        self.state_tx.send_replace(Arc::new(self.client.clone()));
268    }
269
270    async fn run<T: Transport>(mut self, mut transport: T, ready: Reply) {
271        let mut ready = Some(ready);
272        loop {
273            let result = self.connection(&mut transport, &mut ready).await;
274            transport.close().await;
275            self.abandon_request();
276            let retry = !self.closed
277                && ready.is_none()
278                && transport.reconnects()
279                && matches!(self.options.reconnect, ReconnectPolicy::Retry)
280                && !is_terminal(&result);
281            if let Some(ready) = ready.take() {
282                let _ = ready.send(result.clone());
283            }
284            if !retry {
285                self.client.set_connection(ConnectionState::Failed(
286                    result.err().unwrap_or(ClientError::Disconnected),
287                ));
288                self.publish();
289                return;
290            }
291            self.client.set_connection(ConnectionState::Connecting);
292            self.publish();
293            if !self.reconnect(&mut transport).await {
294                break;
295            }
296        }
297        self.client
298            .set_connection(ConnectionState::Failed(ClientError::Disconnected));
299        self.publish();
300    }
301
302    async fn connection<T: Transport>(
303        &mut self,
304        transport: &mut T,
305        ready: &mut Option<Reply>,
306    ) -> Result<(), ClientError> {
307        self.handshake.reset();
308        transport
309            .send(ClientCommand::Initialize {
310                protocol_version: LIVE_PROTOCOL_VERSION,
311                scope: self.options.scope,
312            })
313            .await
314            .map_err(|_| ClientError::Disconnected)?;
315        let idle = platform::sleep(IDLE_TIMEOUT).fuse();
316        futures_util::pin_mut!(idle);
317        loop {
318            let input = {
319                let command = async {
320                    if self.pending.is_none() {
321                        self.commands.recv().await.ok()
322                    } else {
323                        futures_util::future::pending().await
324                    }
325                }
326                .fuse();
327                let event = transport.recv().fuse();
328                futures_util::pin_mut!(command, event);
329                futures_util::select! {
330                    command = command => ConnectionInput::Command(command),
331                    event = event => ConnectionInput::Event(event),
332                    () = idle => ConnectionInput::Idle,
333                }
334            };
335            match input {
336                ConnectionInput::Command(None) => {
337                    self.closed = true;
338                    return Ok(());
339                }
340                ConnectionInput::Command(Some(Command::Close(reply))) => {
341                    self.closed = true;
342                    let _ = reply.send(Ok(()));
343                    return Ok(());
344                }
345                ConnectionInput::Command(Some(Command::Request(request, reply))) => {
346                    self.request(transport, request, reply).await?;
347                }
348                ConnectionInput::Event(event) => {
349                    self.event(&event?)?;
350                    if self.handshake.initialized
351                        && let Some(ready) = ready.take()
352                    {
353                        let _ = ready.send(Ok(()));
354                    }
355                }
356                ConnectionInput::Idle => return Err(ClientError::Disconnected),
357            }
358            idle.set(platform::sleep(IDLE_TIMEOUT).fuse());
359        }
360    }
361
362    fn event(&mut self, event: &ServerEvent) -> Result<(), ClientError> {
363        self.handshake.check(event)?;
364        if let Event::RequestResult(result) = event {
365            if let Some(pending) = self.pending.take() {
366                let _ = pending
367                    .reply
368                    .send(result.clone().map_err(ClientError::Remote));
369            }
370            return Ok(());
371        }
372        let next = self.client.clone().apply(event);
373        if next != self.client {
374            self.client = next;
375            self.publish();
376        }
377        if let Event::Error(error) = event {
378            return Err(ClientError::Remote(error.clone()));
379        }
380        Ok(())
381    }
382
383    async fn request<T: Transport>(
384        &mut self,
385        transport: &mut T,
386        request: ClientCommand,
387        reply: Reply,
388    ) -> Result<(), ClientError> {
389        if !self.handshake.initialized {
390            let _ = reply.send(Err(ClientError::Disconnected));
391            return Ok(());
392        }
393        let outcome_unknown_on_disconnect = matches!(
394            request,
395            ClientCommand::Apply(_) | ClientCommand::Submit(_) | ClientCommand::Cancel
396        );
397        if transport.send(request).await.is_err() {
398            let _ = reply.send(Err(ClientError::Disconnected));
399            return Err(ClientError::Disconnected);
400        }
401        self.pending = Some(Pending {
402            reply,
403            outcome_unknown_on_disconnect,
404        });
405        Ok(())
406    }
407
408    fn abandon_request(&mut self) {
409        if let Some(pending) = self.pending.take() {
410            let _ = pending
411                .reply
412                .send(Err(if pending.outcome_unknown_on_disconnect {
413                    ClientError::OutcomeUnknown
414                } else {
415                    ClientError::Disconnected
416                }));
417        }
418    }
419
420    async fn reconnect<T: Transport>(&mut self, transport: &mut T) -> bool {
421        let mut delay = 250;
422        loop {
423            let wait = delay;
424            let connecting = async {
425                platform::sleep(platform::reconnect_delay(wait)).await;
426                transport.reconnect().await
427            }
428            .fuse();
429            let command = self.commands.recv().fuse();
430            futures_util::pin_mut!(connecting, command);
431            futures_util::select! {
432                next = connecting => match next {
433                    Ok(()) => return true,
434                    Err(_) => delay = (delay * 2).min(5000),
435                },
436                command = command => match command {
437                    Ok(Command::Request(_, reply)) => { let _ = reply.send(Err(ClientError::Disconnected)); }
438                    Ok(Command::Close(reply)) => { self.closed = true; let _ = reply.send(Ok(())); return false; }
439                    Err(_) => { self.closed = true; return false; }
440                },
441            }
442        }
443    }
444}
445
446fn is_terminal(result: &Result<(), ClientError>) -> bool {
447    matches!(
448        result,
449        Err(ClientError::Protocol(_)
450            | ClientError::Remote(RemoteError {
451                code: RemoteErrorCode::UnsupportedVersion | RemoteErrorCode::Protocol,
452                ..
453            }))
454    )
455}