mirror of
https://github.com/hexedtech/codemp.git
synced 2024-12-23 05:14:54 +01:00
feat: workspace streaming attach and lists
to join a workspace, attach to it. to leave a workspace, close the channel. on such channel you get workspace events (new buffers, user leave, ...). must fetch current buffers and users upon join. if workspace doesn't exist, server should create it on attach also dashmap everywhere to get/put simple
This commit is contained in:
parent
f61836e4ca
commit
948a1b4de5
3 changed files with 81 additions and 80 deletions
|
@ -27,6 +27,7 @@ tokio-stream = { version = "0.1", optional = true }
|
|||
lazy_static = { version = "1.4", optional = true }
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
postcard = "1.0.8"
|
||||
dashmap = "5.5.3"
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.9"
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
//!
|
||||
//! codemp client manager, containing grpc services
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use dashmap::DashMap;
|
||||
use tokio::sync::mpsc;
|
||||
use tonic::service::interceptor::InterceptedService;
|
||||
use tonic::service::Interceptor;
|
||||
use tonic::transport::{Channel, Endpoint};
|
||||
use tonic::IntoRequest;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::proto::auth::auth_client::AuthClient;
|
||||
|
@ -33,7 +34,7 @@ use crate::{
|
|||
pub struct Client {
|
||||
user_id: Uuid,
|
||||
token_tx: Arc<tokio::sync::watch::Sender<Token>>,
|
||||
pub workspaces: BTreeMap<String, Arc<RwLock<Workspace>>>,
|
||||
workspaces: Arc<DashMap<String, Arc<Workspace>>>,
|
||||
services: Arc<Services>
|
||||
}
|
||||
|
||||
|
@ -62,7 +63,7 @@ pub(crate) struct Services {
|
|||
}
|
||||
|
||||
// TODO meno losco
|
||||
fn parse_codemp_connection_string<'a>(string: &'a str) -> (String, String) {
|
||||
fn parse_codemp_connection_string(string: &str) -> (String, String) {
|
||||
let url = string.replace("codemp://", "");
|
||||
let (host, workspace) = url.split_once('/').unwrap();
|
||||
(format!("http://{}", host), workspace.to_string())
|
||||
|
@ -86,14 +87,15 @@ impl Client {
|
|||
let buffer = BufferClient::with_interceptor(channel.clone(), inter.clone());
|
||||
let cursor = CursorClient::with_interceptor(channel.clone(), inter.clone());
|
||||
let workspace = WorkspaceClient::with_interceptor(channel.clone(), inter.clone());
|
||||
let auth = AuthClient::new(channel);
|
||||
|
||||
let user_id = uuid::Uuid::new_v4();
|
||||
|
||||
Ok(Client {
|
||||
user_id,
|
||||
token_tx: Arc::new(token_tx),
|
||||
workspaces: BTreeMap::new(),
|
||||
services: Arc::new(Services { workspace, buffer, cursor })
|
||||
workspaces: Arc::new(DashMap::default()),
|
||||
services: Arc::new(Services { workspace, buffer, cursor, auth })
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -107,52 +109,43 @@ impl Client {
|
|||
}
|
||||
|
||||
/// join a workspace, returns an [tokio::sync::RwLock] to interact with it
|
||||
pub async fn join_workspace(&mut self, workspace_id: &str) -> crate::Result<Arc<RwLock<Workspace>>> {
|
||||
self.token_tx.send(self.services.workspace.clone().join(
|
||||
tonic::Request::new(JoinRequest { username: "".to_string(), password: "".to_string() }) //TODO
|
||||
).await?.into_inner())?;
|
||||
pub async fn join_workspace(&mut self, workspace: &str) -> crate::Result<Arc<Workspace>> {
|
||||
let ws_stream = self.services.workspace.clone().attach(Empty{}.into_request()).await?.into_inner();
|
||||
|
||||
let (tx, rx) = mpsc::channel(10);
|
||||
let stream = self.services.cursor.clone()
|
||||
let cur_stream = self.services.cursor.clone()
|
||||
.attach(tokio_stream::wrappers::ReceiverStream::new(rx))
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
let worker = CursorWorker::new(self.user_id.clone());
|
||||
let worker = CursorWorker::default();
|
||||
let controller = Arc::new(worker.subscribe());
|
||||
tokio::spawn(async move {
|
||||
tracing::debug!("controller worker started");
|
||||
worker.work(tx, stream).await;
|
||||
worker.work(tx, cur_stream).await;
|
||||
tracing::debug!("controller worker stopped");
|
||||
});
|
||||
|
||||
let lock = Arc::new(RwLock::new(
|
||||
Workspace::new(
|
||||
workspace_id.to_string(),
|
||||
self.user_id,
|
||||
self.token_tx.clone(),
|
||||
controller,
|
||||
self.services.clone()
|
||||
).await?
|
||||
let ws = Arc::new(Workspace::new(
|
||||
workspace.to_string(),
|
||||
self.user_id,
|
||||
self.token_tx.clone(),
|
||||
controller,
|
||||
self.services.clone()
|
||||
));
|
||||
|
||||
self.workspaces.insert(workspace_id.to_string(), lock.clone());
|
||||
ws.fetch_users().await?;
|
||||
ws.fetch_buffers().await?;
|
||||
|
||||
Ok(lock)
|
||||
}
|
||||
ws.run_actor(ws_stream);
|
||||
|
||||
/// leave given workspace, disconnecting buffer and cursor controllers
|
||||
pub async fn leave_workspace(&self, workspace_id: &str) -> crate::Result<()> {
|
||||
let mut workspace_client = self.services.workspace.clone();
|
||||
workspace_client.leave_workspace(
|
||||
tonic::Request::new(WorkspaceDetails { id: workspace_id.to_string() })
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
self.workspaces.insert(workspace.to_string(), ws.clone());
|
||||
|
||||
Ok(ws)
|
||||
}
|
||||
|
||||
/// accessor for user id
|
||||
pub fn user_id(&self) -> Uuid {
|
||||
self.user_id.clone()
|
||||
self.user_id
|
||||
}
|
||||
}
|
||||
|
|
101
src/workspace.rs
101
src/workspace.rs
|
@ -1,5 +1,7 @@
|
|||
use std::{collections::{BTreeMap, BTreeSet}, str::FromStr, sync::Arc};
|
||||
use std::{collections::BTreeSet, sync::Arc};
|
||||
use tokio::sync::mpsc;
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use tonic::Streaming;
|
||||
use uuid::Uuid;
|
||||
use crate::{
|
||||
api::controller::ControllerWorker, buffer::{self, worker::BufferWorker}, client::Services, cursor,
|
||||
|
@ -14,43 +16,60 @@ pub struct UserInfo {
|
|||
|
||||
pub struct Workspace {
|
||||
id: String,
|
||||
user_id: Uuid,
|
||||
user_id: Uuid, // reference to global user id
|
||||
token: Arc<tokio::sync::watch::Sender<Token>>,
|
||||
cursor: Arc<cursor::Controller>,
|
||||
buffers: BTreeMap<String, Arc<buffer::Controller>>,
|
||||
filetree: BTreeSet<String>,
|
||||
users: BTreeMap<Uuid, UserInfo>,
|
||||
buffers: Arc<DashMap<String, Arc<buffer::Controller>>>,
|
||||
pub(crate) filetree: Arc<DashSet<String>>,
|
||||
pub(crate) users: Arc<DashMap<Uuid, UserInfo>>,
|
||||
services: Arc<Services>
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
/// create a new buffer and perform initial fetch operations
|
||||
pub(crate) async fn new(
|
||||
pub(crate) fn new(
|
||||
id: String,
|
||||
user_id: Uuid,
|
||||
token: Arc<tokio::sync::watch::Sender<Token>>,
|
||||
cursor: Arc<cursor::Controller>,
|
||||
services: Arc<Services>
|
||||
) -> crate::Result<Self> {
|
||||
let mut ws = Workspace {
|
||||
) -> Self {
|
||||
Workspace {
|
||||
id,
|
||||
user_id,
|
||||
token,
|
||||
cursor,
|
||||
buffers: BTreeMap::new(),
|
||||
filetree: BTreeSet::new(),
|
||||
users: BTreeMap::new(),
|
||||
buffers: Arc::new(DashMap::default()),
|
||||
filetree: Arc::new(DashSet::default()),
|
||||
users: Arc::new(DashMap::default()),
|
||||
services
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ws.fetch_buffers().await?;
|
||||
ws.fetch_users().await?;
|
||||
|
||||
Ok(ws)
|
||||
pub(crate) fn run_actor(&self, mut stream: Streaming<WorkspaceEvent>) {
|
||||
let users = self.users.clone();
|
||||
let filetree = self.filetree.clone();
|
||||
let name = self.id();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match stream.message().await {
|
||||
Err(e) => break tracing::error!("workspace '{}' stream closed: {}", name, e),
|
||||
Ok(None) => break tracing::info!("leaving workspace {}", name),
|
||||
Ok(Some(WorkspaceEvent { event: None })) => tracing::warn!("workspace {} received empty event", name),
|
||||
Ok(Some(WorkspaceEvent { event: Some(ev) })) => match ev {
|
||||
WorkspaceEventInner::Join(UserJoin { user }) => { users.insert(user.clone().into(), UserInfo { uuid: user.into() }); },
|
||||
WorkspaceEventInner::Leave(UserLeave { user }) => { users.remove(&user.into()); },
|
||||
WorkspaceEventInner::Create(FileCreate { path }) => { filetree.insert(path); },
|
||||
WorkspaceEventInner::Rename(FileRename { before, after }) => { filetree.remove(&before); filetree.insert(after); },
|
||||
WorkspaceEventInner::Delete(FileDelete { path }) => { filetree.remove(&path); },
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// create a new buffer in current workspace
|
||||
pub async fn create(&mut self, path: &str) -> crate::Result<()> {
|
||||
pub async fn create(&self, path: &str) -> crate::Result<()> {
|
||||
let mut workspace_client = self.services.workspace.clone();
|
||||
workspace_client.create_buffer(
|
||||
tonic::Request::new(BufferNode { path: path.to_string() })
|
||||
|
@ -69,17 +88,16 @@ impl Workspace {
|
|||
///
|
||||
/// to interact with such buffer use [crate::api::Controller::send] or
|
||||
/// [crate::api::Controller::recv] to exchange [crate::api::TextChange]
|
||||
pub async fn attach(&mut self, path: &str) -> crate::Result<Arc<buffer::Controller>> {
|
||||
pub async fn attach(&self, path: &str) -> crate::Result<Arc<buffer::Controller>> {
|
||||
let mut worskspace_client = self.services.workspace.clone();
|
||||
let mut request = tonic::Request::new(AttachRequest { path: path.to_string() });
|
||||
request.metadata_mut().insert("path", tonic::metadata::MetadataValue::try_from(path).expect("could not represent path as byte sequence"));
|
||||
self.token.send(worskspace_client.attach(request).await?.into_inner())?;
|
||||
let request = tonic::Request::new(BufferNode { path: path.to_string() });
|
||||
let credentials = worskspace_client.access_buffer(request).await?.into_inner();
|
||||
self.token.send(credentials.token)?;
|
||||
|
||||
let (tx, rx) = mpsc::channel(10);
|
||||
let stream = self.services.buffer.clone()
|
||||
.attach(tokio_stream::wrappers::ReceiverStream::new(rx))
|
||||
.await?
|
||||
.into_inner();
|
||||
let mut req = tonic::Request::new(tokio_stream::wrappers::ReceiverStream::new(rx));
|
||||
req.metadata_mut().insert("path", tonic::metadata::MetadataValue::try_from(path).expect("could not represent path as byte sequence"));
|
||||
let stream = self.services.buffer.clone().attach(req).await?.into_inner();
|
||||
|
||||
let worker = BufferWorker::new(self.user_id, path);
|
||||
let controller = Arc::new(worker.subscribe());
|
||||
|
@ -95,7 +113,7 @@ impl Workspace {
|
|||
}
|
||||
|
||||
/// fetch a list of all buffers in a workspace
|
||||
pub async fn fetch_buffers(&mut self) -> crate::Result<()> {
|
||||
pub async fn fetch_buffers(&self) -> crate::Result<()> {
|
||||
let mut workspace_client = self.services.workspace.clone();
|
||||
let buffers = workspace_client.list_buffers(
|
||||
tonic::Request::new(Empty {})
|
||||
|
@ -110,19 +128,16 @@ impl Workspace {
|
|||
}
|
||||
|
||||
/// fetch a list of all users in a workspace
|
||||
pub async fn fetch_users(&mut self) -> crate::Result<()> {
|
||||
pub async fn fetch_users(&self) -> crate::Result<()> {
|
||||
let mut workspace_client = self.services.workspace.clone();
|
||||
let users = BTreeSet::from_iter(workspace_client.list_users(
|
||||
tonic::Request::new(Empty {})
|
||||
).await?.into_inner().users.into_iter().map(Uuid::from));
|
||||
|
||||
// only keep userinfo for users that still exist
|
||||
self.users.retain(|k, _v| users.contains(k));
|
||||
|
||||
let _users = self.users.clone(); // damnnn rust
|
||||
users.iter()
|
||||
.filter(|u| _users.contains_key(u))
|
||||
.for_each(|u| { self.users.insert(*u, UserInfo::from(*u)); });
|
||||
self.users.clear();
|
||||
for u in users {
|
||||
self.users.insert(u, UserInfo { uuid: u });
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -130,7 +145,7 @@ impl Workspace {
|
|||
/// get a list of the users attached to a specific buffer
|
||||
///
|
||||
/// TODO: discuss implementation details
|
||||
pub async fn list_buffer_users(&mut self, path: &str) -> crate::Result<Vec<UserIdentity>> {
|
||||
pub async fn list_buffer_users(&self, path: &str) -> crate::Result<Vec<Identity>> {
|
||||
let mut workspace_client = self.services.workspace.clone();
|
||||
let buffer_users = workspace_client.list_buffer_users(
|
||||
tonic::Request::new(BufferNode { path: path.to_string() })
|
||||
|
@ -139,16 +154,8 @@ impl Workspace {
|
|||
Ok(buffer_users)
|
||||
}
|
||||
|
||||
/// detach from a specific buffer, returns false if there
|
||||
pub fn detach(&mut self, path: &str) -> bool {
|
||||
match &mut self.buffers.remove(path) {
|
||||
None => false,
|
||||
Some(_) => true
|
||||
}
|
||||
}
|
||||
|
||||
/// delete a buffer
|
||||
pub async fn delete(&mut self, path: &str) -> crate::Result<()> {
|
||||
pub async fn delete(&self, path: &str) -> crate::Result<()> {
|
||||
let mut workspace_client = self.services.workspace.clone();
|
||||
workspace_client.delete_buffer(
|
||||
tonic::Request::new(BufferNode { path: path.to_string() })
|
||||
|
@ -167,11 +174,11 @@ impl Workspace {
|
|||
|
||||
/// get a new reference to a buffer controller, if any is active to given path
|
||||
pub fn buffer_by_name(&self, path: &str) -> Option<Arc<buffer::Controller>> {
|
||||
self.buffers.get(path).cloned()
|
||||
self.buffers.get(path).map(|x| x.clone())
|
||||
}
|
||||
|
||||
/// get the currently cached "filetree"
|
||||
pub fn filetree(&self) -> Vec<String> {
|
||||
self.filetree.iter().map(|f| f.clone()).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue