瀏覽代碼

Implement multiplexing for editors

Eric Zhang 4 年之前
父節點
當前提交
40a28d4850
共有 7 個文件被更改,包括 56 次插入20 次删除
  1. 11 0
      Cargo.lock
  2. 1 0
      rustpad-server/Cargo.toml
  3. 22 8
      rustpad-server/src/lib.rs
  4. 0 5
      rustpad-server/src/rustpad.rs
  5. 5 2
      rustpad-server/tests/sockets.rs
  6. 6 5
      src/App.tsx
  7. 11 0
      src/index.tsx

+ 11 - 0
Cargo.lock

@@ -118,6 +118,16 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "dashmap"
+version = "4.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c"
+dependencies = [
+ "cfg-if 1.0.0",
+ "num_cpus",
+]
+
 [[package]]
 name = "digest"
 version = "0.9.0"
@@ -843,6 +853,7 @@ name = "rustpad-server"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "dashmap",
  "dotenv",
  "futures",
  "log",

+ 1 - 0
rustpad-server/Cargo.toml

@@ -6,6 +6,7 @@ edition = "2018"
 
 [dependencies]
 anyhow = "1.0.40"
+dashmap = "4.0.2"
 dotenv = "0.15.0"
 futures = "0.3.15"
 log = "0.4.14"

+ 22 - 8
rustpad-server/src/lib.rs

@@ -5,6 +5,7 @@
 
 use std::sync::Arc;
 
+use dashmap::DashMap;
 use rustpad::Rustpad;
 use warp::{filters::BoxedFilter, ws::Ws, Filter, Reply};
 
@@ -24,21 +25,34 @@ fn frontend() -> BoxedFilter<(impl Reply,)> {
 
 /// Construct backend routes, including WebSocket handlers.
 fn backend() -> BoxedFilter<(impl Reply,)> {
-    let rustpad = Arc::new(Rustpad::new());
-    let rustpad = warp::any().map(move || Arc::clone(&rustpad));
+    let rustpad_map: Arc<DashMap<String, Arc<Rustpad>>> = Default::default();
+    let rustpad_map = warp::any().map(move || Arc::clone(&rustpad_map));
 
     let socket = warp::path("socket")
+        .and(warp::path::param())
         .and(warp::path::end())
         .and(warp::ws())
-        .and(rustpad.clone())
-        .map(|ws: Ws, rustpad: Arc<Rustpad>| {
-            ws.on_upgrade(move |socket| async move { rustpad.on_connection(socket).await })
-        });
+        .and(rustpad_map.clone())
+        .map(
+            |id: String, ws: Ws, rustpad_map: Arc<DashMap<String, Arc<Rustpad>>>| {
+                let rustpad = rustpad_map.entry(id).or_default();
+                let rustpad = Arc::clone(rustpad.value());
+                ws.on_upgrade(move |socket| async move { rustpad.on_connection(socket).await })
+            },
+        );
 
     let text = warp::path("text")
+        .and(warp::path::param())
         .and(warp::path::end())
-        .and(rustpad.clone())
-        .map(|rustpad: Arc<Rustpad>| rustpad.text());
+        .and(rustpad_map.clone())
+        .map(
+            |id: String, rustpad_map: Arc<DashMap<String, Arc<Rustpad>>>| {
+                rustpad_map
+                    .get(&id)
+                    .map(|rustpad| rustpad.text())
+                    .unwrap_or_default()
+            },
+        );
 
     socket.or(text).boxed()
 }

+ 0 - 5
rustpad-server/src/rustpad.rs

@@ -63,11 +63,6 @@ impl From<ServerMsg> for Message {
 }
 
 impl Rustpad {
-    /// Construct a new, empty Rustpad object.
-    pub fn new() -> Self {
-        Default::default()
-    }
-
     /// Handle a connection from a WebSocket.
     pub async fn on_connection(&self, socket: WebSocket) {
         let id = self.count.fetch_add(1, Ordering::Relaxed);

+ 5 - 2
rustpad-server/tests/sockets.rs

@@ -30,7 +30,7 @@ impl JsonSocket {
 /// Connect a new test client WebSocket.
 async fn connect(filter: &BoxedFilter<(impl Reply + 'static,)>) -> Result<JsonSocket> {
     let client = warp::test::ws()
-        .path("/api/socket")
+        .path("/api/socket/foobar")
         .handshake(filter.clone())
         .await?;
     Ok(JsonSocket(client))
@@ -38,7 +38,10 @@ async fn connect(filter: &BoxedFilter<(impl Reply + 'static,)>) -> Result<JsonSo
 
 /// Check the text route.
 async fn expect_text(filter: &BoxedFilter<(impl Reply + 'static,)>, text: &str) {
-    let resp = warp::test::request().path("/api/text").reply(filter).await;
+    let resp = warp::test::request()
+        .path("/api/text/foobar")
+        .reply(filter)
+        .await;
     assert_eq!(resp.status(), 200);
     assert_eq!(resp.body(), text);
 }

+ 6 - 5
src/App.tsx

@@ -25,10 +25,11 @@ import languages from "./languages.json";
 
 set_panic_hook();
 
-const WS_URI =
+const id = window.location.hash.slice(1);
+const wsUri =
   (window.location.origin.startsWith("https") ? "wss://" : "ws://") +
   window.location.host +
-  "/api/socket";
+  `/api/socket/${id}`;
 
 function App() {
   const toast = useToast();
@@ -42,7 +43,7 @@ function App() {
       model.setValue("");
       model.setEOL(0); // LF
       const rustpad = new Rustpad({
-        uri: WS_URI,
+        uri: wsUri,
         editor,
         onConnected: () => setConnected(true),
         onDisconnected: () => setConnected(false),
@@ -52,7 +53,7 @@ function App() {
   }, [editor]);
 
   async function handleCopy() {
-    await navigator.clipboard.writeText(`${window.location.origin}/`);
+    await navigator.clipboard.writeText(`${window.location.origin}/#${id}`);
     toast({
       title: "Copied!",
       description: "Link copied to clipboard",
@@ -114,7 +115,7 @@ function App() {
                 pr="3.5rem"
                 variant="outline"
                 bgColor="white"
-                value={`${window.location.origin}/`}
+                value={`${window.location.origin}/#${id}`}
               />
               <InputRightElement width="3.5rem">
                 <Button h="1.4rem" size="xs" onClick={handleCopy}>

+ 11 - 0
src/index.tsx

@@ -3,6 +3,17 @@ import ReactDOM from "react-dom";
 import { ChakraProvider } from "@chakra-ui/react";
 import "./index.css";
 
+const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+const idLen = 6;
+
+if (!window.location.hash) {
+  let id = "";
+  for (let i = 0; i < idLen; i++) {
+    id += chars[Math.floor(Math.random() * chars.length)];
+  }
+  window.location.hash = id;
+}
+
 // An asynchronous entry point is needed to load WebAssembly files.
 import("./App").then(({ default: App }) => {
   ReactDOM.render(