Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
441 changes: 207 additions & 234 deletions Cargo.lock

Large diffs are not rendered by default.

113 changes: 111 additions & 2 deletions apps/keck/src/server/sync/collaboration.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
use super::*;
use axum::{
extract,
extract::{ws::WebSocketUpgrade, Path},
response::Response,
Json,
};
use futures::FutureExt;
use jwst_rpc::{axum_socket_connector, handle_connector};
use jwst_rpc::{
axum_socket_connector, handle_connector, webrtc_datachannel_server_connector,
RTCSessionDescription,
};
use serde::Serialize;
use std::sync::Arc;
use tokio::sync::mpsc::channel;

#[derive(Serialize)]
pub struct WebSocketAuthentication {
Expand Down Expand Up @@ -38,12 +43,42 @@ pub async fn upgrade_handler(
})
}

pub async fn webrtc_handler(
Extension(context): Extension<Arc<Context>>,
Path(workspace): Path<String>,
extract::Json(offer): extract::Json<RTCSessionDescription>,
) -> Json<RTCSessionDescription> {
let (answer, tx, rx, _) = webrtc_datachannel_server_connector(offer).await;

let (first_init_tx, mut first_init_rx) = channel::<bool>(10);
let workspace_id = workspace.to_owned();
tokio::spawn(async move {
if let Some(true) = first_init_rx.recv().await {
info!("socket init success: {}", workspace_id);
} else {
error!("socket init failed: {}", workspace_id);
}
});

let identifier = nanoid!();
tokio::spawn(async move {
handle_connector(context.clone(), workspace.clone(), identifier, move || {
(tx, rx, first_init_tx)
})
.await;
});

Json(answer)
}

#[cfg(test)]
mod test {
use jwst::DocStorage;
use jwst::{Block, Workspace};
use jwst_logger::info;
use jwst_rpc::{start_client_sync, BroadcastChannels, RpcContextImpl};
use jwst_rpc::{
start_client_sync, start_webrtc_client_sync, BroadcastChannels, RpcContextImpl,
};
use jwst_storage::JwstStorage;
use libc::{kill, SIGTERM};
use rand::{thread_rng, Rng};
Expand Down Expand Up @@ -77,6 +112,79 @@ mod test {
&self.channel
}
}
#[test]
fn client_collaboration_with_webrtc_server() {
if dotenvy::var("KECK_DEBUG").is_ok() {
jwst_logger::init_logger("keck");
}

// TODO:
// there has a weird question:
// use new terminal run keck is pass
// use `start_collaboration_server` funtion is high probability block

//let server_port = 3000;
let server_port = thread_rng().gen_range(10000..=30000);
let child = start_collaboration_server(server_port);

let rt = Runtime::new().unwrap();
let (workspace_id, workspace) = rt.block_on(async move {
let workspace_id = "1";
let context = Arc::new(TestContext::new(Arc::new(
JwstStorage::new("sqlite::memory:")
.await
.expect("get storage: memory sqlite failed"),
)));
let remote = format!("http://localhost:{server_port}/webrtc-sdp/1");

start_webrtc_client_sync(
Arc::new(Runtime::new().unwrap()),
context.clone(),
Arc::default(),
remote,
workspace_id.to_owned(),
);

(
workspace_id.to_owned(),
context.get_workspace(workspace_id).await.unwrap(),
)
});

for block_id in 0..3 {
let block = create_block(&workspace, block_id.to_string(), "list".to_string());
info!("from client, create a block: {:?}", block);
}

info!("------------------after sync------------------");
std::thread::sleep(std::time::Duration::from_secs(1));

for block_id in 0..3 {
info!(
"get block {block_id} from server: {}",
get_block_from_server(workspace_id.clone(), block_id.to_string(), server_port)
);
assert!(!get_block_from_server(
workspace_id.clone(),
block_id.to_string(),
server_port
)
.is_empty());
}

workspace.with_trx(|mut trx| {
let space = trx.get_space("blocks");
let blocks = space.get_blocks_by_flavour(&trx.trx, "list");
let mut ids: Vec<_> = blocks.iter().map(|block| block.block_id()).collect();
assert_eq!(ids.sort(), vec!["7", "8", "9"].sort());
info!("blocks from local storage:");
for block in blocks {
info!("block: {:?}", block);
}
});

close_collaboration_server(child);
}

#[test]
#[ignore = "not needed in ci"]
Expand Down Expand Up @@ -286,6 +394,7 @@ mod test {
.args(&["run", "-p", "keck"])
.env("KECK_PORT", port.to_string())
.env("USE_MEMORY_SQLITE", "true")
.env("KECK_LOG", "INFO")
.stdout(Stdio::piped())
.spawn()
.expect("Failed to run command");
Expand Down
4 changes: 4 additions & 0 deletions apps/keck/src/server/sync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ pub fn sync_handler(router: Router) -> Router {
"/collaboration/:workspace",
post(collaboration::auth_handler).get(collaboration::upgrade_handler),
)
.nest_service(
"/webrtc-sdp/:workspace",
post(collaboration::webrtc_handler),
)
}
6 changes: 5 additions & 1 deletion libs/jwst-rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ license = "AGPL-3.0-only"
[features]
default = ["websocket", "webrtc"]
websocket = ["axum", "tokio-tungstenite", "url"]
webrtc = ["bytes", "webrtcrs"]
webrtc = ["bytes", "reqwest", "webrtcrs"]

[dependencies]
anyhow = "1.0.70"
Expand Down Expand Up @@ -36,6 +36,10 @@ url = { version = "2.3.1", optional = true }

# ======== webrtc dependencies ========
bytes = { version = "1.4", optional = true }
reqwest = { version = "0.11.18", default-features = false, features = [
"json",
"rustls-tls",
], optional = true }
webrtcrs = { package = "webrtc", version = "0.7.3", optional = true }

# ======= workspace dependencies =======
Expand Down
117 changes: 117 additions & 0 deletions libs/jwst-rpc/src/client_webrtc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use super::*;
use nanoid::nanoid;
use reqwest::Client;
use std::sync::{
atomic::{AtomicBool, Ordering},
RwLock,
};
use tokio::{runtime::Runtime, sync::mpsc::channel};

async fn webrtc_connection(remote: &str) -> (Sender<Message>, Receiver<Vec<u8>>) {
warn!("webrtc_connection start");
let (offer, pc, tx, rx, mut s) = webrtc_datachannel_client_begin().await;
let client = Client::new();

match client.post(remote).json(&offer).send().await {
Ok(res) => {
webrtc_datachannel_client_commit(
res.json::<RTCSessionDescription>().await.unwrap(),
pc,
)
.await;
s.recv().await.ok();
warn!("client already connected");
}
Err(e) => {
error!("failed to http post: {}", e);
}
}

(tx, rx)
}

pub fn start_webrtc_client_sync(
rt: Arc<Runtime>,
context: Arc<impl RpcContextImpl<'static> + Send + Sync + 'static>,
sync_state: Arc<RwLock<SyncState>>,
remote: String,
workspace_id: String,
) {
debug!("spawn sync thread");
let first_sync = Arc::new(AtomicBool::new(false));
let first_sync_cloned = first_sync.clone();
std::thread::spawn(move || {
rt.block_on(async move {
let workspace = match context.get_workspace(&workspace_id).await {
Ok(workspace) => workspace,
Err(e) => {
error!("failed to create workspace: {:?}", e);
return;
}
};
if !workspace.is_empty() {
info!("Workspace not empty, starting async remote connection");
let first_sync = first_sync_cloned.clone();
tokio::spawn(async move {
sleep(Duration::from_secs(2)).await;
first_sync.store(true, Ordering::Release);
});
} else {
info!("Workspace empty, starting sync remote connection");
}

loop {
let identifier = nanoid!();
let workspace_id = workspace_id.clone();
let first_init_tx = {
let (first_init_tx, mut first_init_rx) = channel::<bool>(10);
let first_sync = first_sync_cloned.clone();
let sync_state = sync_state.clone();
tokio::spawn(async move {
if let Some(true) = first_init_rx.recv().await {
first_sync.store(true, Ordering::Release);
let mut state = sync_state.write().unwrap();
match *state {
SyncState::Offline => *state = SyncState::Initialized,
SyncState::Initialized | SyncState::Error(_) => {
*state = SyncState::Syncing
}
_ => {}
}
}
});
first_init_tx
};

let ret = {
let (tx, rx) = webrtc_connection(&remote).await;
handle_connector(context.clone(), workspace_id, identifier, move || {
(tx, rx, first_init_tx)
})
.await
};

{
first_sync_cloned.store(true, Ordering::Release);
let mut state = sync_state.write().unwrap();
if ret {
debug!("sync thread finished");
*state = SyncState::Finished;
} else {
*state =
SyncState::Error("Remote sync connection disconnected".to_string());
}
}

warn!("Remote sync connection disconnected, try again in 2 seconds");
sleep(Duration::from_secs(3)).await;
}
});
});

while let Ok(false) | Err(false) =
first_sync.compare_exchange_weak(true, false, Ordering::Acquire, Ordering::Acquire)
{
std::thread::sleep(Duration::from_millis(100));
}
}
Loading