Establish WebSocket connection to Cloud (#35734)
This PR adds a new WebSocket connection to Cloud. This connection will be used to push down notifications from the server to the client. Release Notes: - N/A --------- Co-authored-by: Marshall Bowers <git@maxdeviant.com>
This commit is contained in:
parent
c595a7576d
commit
1907b16fe6
8 changed files with 214 additions and 17 deletions
|
@ -15,7 +15,10 @@ path = "src/cloud_api_client.rs"
|
|||
anyhow.workspace = true
|
||||
cloud_api_types.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
gpui_tokio.workspace = true
|
||||
http_client.workspace = true
|
||||
parking_lot.workspace = true
|
||||
serde_json.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
yawc.workspace = true
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
mod websocket;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use cloud_api_types::websocket_protocol::{PROTOCOL_VERSION, PROTOCOL_VERSION_HEADER_NAME};
|
||||
pub use cloud_api_types::*;
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{App, Task};
|
||||
use gpui_tokio::Tokio;
|
||||
use http_client::http::request;
|
||||
use http_client::{AsyncBody, HttpClientWithUrl, Method, Request, StatusCode};
|
||||
use parking_lot::RwLock;
|
||||
use yawc::WebSocket;
|
||||
|
||||
use crate::websocket::Connection;
|
||||
|
||||
struct Credentials {
|
||||
user_id: u32,
|
||||
|
@ -78,6 +86,41 @@ impl CloudApiClient {
|
|||
Ok(serde_json::from_str(&body)?)
|
||||
}
|
||||
|
||||
pub fn connect(&self, cx: &App) -> Result<Task<Result<Connection>>> {
|
||||
let mut connect_url = self
|
||||
.http_client
|
||||
.build_zed_cloud_url("/client/users/connect", &[])?;
|
||||
connect_url
|
||||
.set_scheme(match connect_url.scheme() {
|
||||
"https" => "wss",
|
||||
"http" => "ws",
|
||||
scheme => Err(anyhow!("invalid URL scheme: {scheme}"))?,
|
||||
})
|
||||
.map_err(|_| anyhow!("failed to set URL scheme"))?;
|
||||
|
||||
let credentials = self.credentials.read();
|
||||
let credentials = credentials.as_ref().context("no credentials provided")?;
|
||||
let authorization_header = format!("{} {}", credentials.user_id, credentials.access_token);
|
||||
|
||||
Ok(cx.spawn(async move |cx| {
|
||||
let handle = cx
|
||||
.update(|cx| Tokio::handle(cx))
|
||||
.ok()
|
||||
.context("failed to get Tokio handle")?;
|
||||
let _guard = handle.enter();
|
||||
|
||||
let ws = WebSocket::connect(connect_url)
|
||||
.with_request(
|
||||
request::Builder::new()
|
||||
.header("Authorization", authorization_header)
|
||||
.header(PROTOCOL_VERSION_HEADER_NAME, PROTOCOL_VERSION.to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Connection::new(ws))
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn accept_terms_of_service(&self) -> Result<AcceptTermsOfServiceResponse> {
|
||||
let request = self.build_request(
|
||||
Request::builder().method(Method::POST).uri(
|
||||
|
|
73
crates/cloud_api_client/src/websocket.rs
Normal file
73
crates/cloud_api_client/src/websocket.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use cloud_api_types::websocket_protocol::MessageToClient;
|
||||
use futures::channel::mpsc::unbounded;
|
||||
use futures::stream::{SplitSink, SplitStream};
|
||||
use futures::{FutureExt as _, SinkExt as _, Stream, StreamExt as _, TryStreamExt as _, pin_mut};
|
||||
use gpui::{App, BackgroundExecutor, Task};
|
||||
use yawc::WebSocket;
|
||||
use yawc::frame::{FrameView, OpCode};
|
||||
|
||||
const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
pub type MessageStream = Pin<Box<dyn Stream<Item = Result<MessageToClient>>>>;
|
||||
|
||||
pub struct Connection {
|
||||
tx: SplitSink<WebSocket, FrameView>,
|
||||
rx: SplitStream<WebSocket>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
pub fn new(ws: WebSocket) -> Self {
|
||||
let (tx, rx) = ws.split();
|
||||
|
||||
Self { tx, rx }
|
||||
}
|
||||
|
||||
pub fn spawn(self, cx: &App) -> (MessageStream, Task<()>) {
|
||||
let (mut tx, rx) = (self.tx, self.rx);
|
||||
|
||||
let (message_tx, message_rx) = unbounded();
|
||||
|
||||
let handle_io = |executor: BackgroundExecutor| async move {
|
||||
// Send messages on this frequency so the connection isn't closed.
|
||||
let keepalive_timer = executor.timer(KEEPALIVE_INTERVAL).fuse();
|
||||
futures::pin_mut!(keepalive_timer);
|
||||
|
||||
let rx = rx.fuse();
|
||||
pin_mut!(rx);
|
||||
|
||||
loop {
|
||||
futures::select_biased! {
|
||||
_ = keepalive_timer => {
|
||||
let _ = tx.send(FrameView::ping(Vec::new())).await;
|
||||
|
||||
keepalive_timer.set(executor.timer(KEEPALIVE_INTERVAL).fuse());
|
||||
}
|
||||
frame = rx.next() => {
|
||||
let Some(frame) = frame else {
|
||||
break;
|
||||
};
|
||||
|
||||
match frame.opcode {
|
||||
OpCode::Binary => {
|
||||
let message_result = MessageToClient::deserialize(&frame.payload);
|
||||
message_tx.unbounded_send(message_result).ok();
|
||||
}
|
||||
OpCode::Close => {
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let task = cx.spawn(async move |cx| handle_io(cx.background_executor().clone()).await);
|
||||
|
||||
(message_rx.into_stream().boxed(), task)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue