Преглед изворни кода

Start writing client code, reconnecting websocket

Eric Zhang пре 4 година
родитељ
комит
a886e17cf4

+ 1 - 1
package-lock.json

@@ -21214,7 +21214,7 @@
       }
       }
     },
     },
     "rustpad-wasm/pkg": {
     "rustpad-wasm/pkg": {
-      "name": "rustpad-core",
+      "name": "rustpad-wasm",
       "version": "0.1.0"
       "version": "0.1.0"
     }
     }
   },
   },

+ 4 - 4
rustpad-server/src/lib.rs

@@ -1,4 +1,4 @@
-//! Server backend for the Rustpad collaborative text editor
+//! Server backend for the Rustpad collaborative text editor.
 
 
 #![forbid(unsafe_code)]
 #![forbid(unsafe_code)]
 #![warn(missing_docs)]
 #![warn(missing_docs)]
@@ -10,19 +10,19 @@ use warp::{filters::BoxedFilter, ws::Ws, Filter, Reply};
 
 
 mod rustpad;
 mod rustpad;
 
 
-/// A combined filter handling all server routes
+/// A combined filter handling all server routes.
 pub fn server() -> BoxedFilter<(impl Reply,)> {
 pub fn server() -> BoxedFilter<(impl Reply,)> {
     warp::path("api").and(backend()).or(frontend()).boxed()
     warp::path("api").and(backend()).or(frontend()).boxed()
 }
 }
 
 
-/// Construct routes for static files from React
+/// Construct routes for static files from React.
 fn frontend() -> BoxedFilter<(impl Reply,)> {
 fn frontend() -> BoxedFilter<(impl Reply,)> {
     warp::fs::dir("build")
     warp::fs::dir("build")
         .or(warp::get().and(warp::fs::file("build/index.html")))
         .or(warp::get().and(warp::fs::file("build/index.html")))
         .boxed()
         .boxed()
 }
 }
 
 
-/// Construct backend routes, including WebSocket handlers
+/// Construct backend routes, including WebSocket handlers.
 fn backend() -> BoxedFilter<(impl Reply,)> {
 fn backend() -> BoxedFilter<(impl Reply,)> {
     let rustpad = Arc::new(Rustpad::new());
     let rustpad = Arc::new(Rustpad::new());
     let rustpad = warp::any().map(move || Arc::clone(&rustpad));
     let rustpad = warp::any().map(move || Arc::clone(&rustpad));

+ 12 - 12
rustpad-server/src/rustpad.rs

@@ -1,4 +1,4 @@
-//! Asynchronous systems logic for Rustpad
+//! Asynchronous systems logic for Rustpad.
 
 
 use std::sync::atomic::{AtomicU64, Ordering};
 use std::sync::atomic::{AtomicU64, Ordering};
 use std::time::Duration;
 use std::time::Duration;
@@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
 use tokio::{sync::Notify, time};
 use tokio::{sync::Notify, time};
 use warp::ws::{Message, WebSocket};
 use warp::ws::{Message, WebSocket};
 
 
-/// The main object for a collaborative session
+/// The main object for a collaborative session.
 #[derive(Default)]
 #[derive(Default)]
 pub struct Rustpad {
 pub struct Rustpad {
     state: RwLock<State>,
     state: RwLock<State>,
@@ -20,7 +20,7 @@ pub struct Rustpad {
     notify: Notify,
     notify: Notify,
 }
 }
 
 
-/// Shared state involving multiple users, protected by a lock
+/// Shared state involving multiple users, protected by a lock.
 #[derive(Default)]
 #[derive(Default)]
 struct State {
 struct State {
     operations: Vec<UserOperation>,
     operations: Vec<UserOperation>,
@@ -33,22 +33,22 @@ struct UserOperation {
     operation: OperationSeq,
     operation: OperationSeq,
 }
 }
 
 
-/// A message received from the client over WebSocket
+/// A message received from the client over WebSocket.
 #[derive(Clone, Debug, Serialize, Deserialize)]
 #[derive(Clone, Debug, Serialize, Deserialize)]
 enum ClientMsg {
 enum ClientMsg {
-    /// Represents a sequence of local edits from the user
+    /// Represents a sequence of local edits from the user.
     Edit {
     Edit {
         revision: usize,
         revision: usize,
         operation: OperationSeq,
         operation: OperationSeq,
     },
     },
 }
 }
 
 
-/// A message sent to the client over WebSocket
+/// A message sent to the client over WebSocket.
 #[derive(Clone, Debug, Serialize, Deserialize)]
 #[derive(Clone, Debug, Serialize, Deserialize)]
 enum ServerMsg {
 enum ServerMsg {
-    /// Informs the client of their unique socket ID
+    /// Informs the client of their unique socket ID.
     Identity(u64),
     Identity(u64),
-    /// Broadcasts text operations to all clients
+    /// Broadcasts text operations to all clients.
     History {
     History {
         start: usize,
         start: usize,
         operations: Vec<UserOperation>,
         operations: Vec<UserOperation>,
@@ -63,12 +63,12 @@ impl From<ServerMsg> for Message {
 }
 }
 
 
 impl Rustpad {
 impl Rustpad {
-    /// Construct a new, empty Rustpad object
+    /// Construct a new, empty Rustpad object.
     pub fn new() -> Self {
     pub fn new() -> Self {
         Default::default()
         Default::default()
     }
     }
 
 
-    /// Handle a connection from a WebSocket
+    /// Handle a connection from a WebSocket.
     pub async fn on_connection(&self, socket: WebSocket) {
     pub async fn on_connection(&self, socket: WebSocket) {
         let id = self.count.fetch_add(1, Ordering::Relaxed);
         let id = self.count.fetch_add(1, Ordering::Relaxed);
         info!("connection! id = {}", id);
         info!("connection! id = {}", id);
@@ -78,13 +78,13 @@ impl Rustpad {
         info!("disconnection, id = {}", id);
         info!("disconnection, id = {}", id);
     }
     }
 
 
-    /// Returns a snapshot of the latest text
+    /// Returns a snapshot of the latest text.
     pub fn text(&self) -> String {
     pub fn text(&self) -> String {
         let state = self.state.read();
         let state = self.state.read();
         state.text.clone()
         state.text.clone()
     }
     }
 
 
-    /// Returns the current revision
+    /// Returns the current revision.
     pub fn revision(&self) -> usize {
     pub fn revision(&self) -> usize {
         let state = self.state.read();
         let state = self.state.read();
         state.operations.len()
         state.operations.len()

+ 3 - 3
rustpad-server/tests/sockets.rs

@@ -8,7 +8,7 @@ use serde_json::{json, Value};
 use tokio::time;
 use tokio::time;
 use warp::{filters::BoxedFilter, test::WsClient, Reply};
 use warp::{filters::BoxedFilter, test::WsClient, Reply};
 
 
-/// A test WebSocket client that sends and receives JSON messages
+/// A test WebSocket client that sends and receives JSON messages.
 struct JsonSocket(WsClient);
 struct JsonSocket(WsClient);
 
 
 impl JsonSocket {
 impl JsonSocket {
@@ -27,7 +27,7 @@ impl JsonSocket {
     }
     }
 }
 }
 
 
-/// Connect a new test client WebSocket
+/// Connect a new test client WebSocket.
 async fn connect(filter: &BoxedFilter<(impl Reply + 'static,)>) -> Result<JsonSocket> {
 async fn connect(filter: &BoxedFilter<(impl Reply + 'static,)>) -> Result<JsonSocket> {
     let client = warp::test::ws()
     let client = warp::test::ws()
         .path("/api/socket")
         .path("/api/socket")
@@ -36,7 +36,7 @@ async fn connect(filter: &BoxedFilter<(impl Reply + 'static,)>) -> Result<JsonSo
     Ok(JsonSocket(client))
     Ok(JsonSocket(client))
 }
 }
 
 
-/// Check the text route
+/// Check the text route.
 async fn expect_text(filter: &BoxedFilter<(impl Reply + 'static,)>, text: &str) {
 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").reply(filter).await;
     assert_eq!(resp.status(), 200);
     assert_eq!(resp.status(), 200);

+ 1 - 1
rustpad-wasm/src/lib.rs

@@ -1,4 +1,4 @@
-//! Core logic for Rustpad, shared with the client through WebAssembly
+//! Core logic for Rustpad, shared with the client through WebAssembly.
 
 
 #![warn(missing_docs)]
 #![warn(missing_docs)]
 
 

+ 1 - 1
rustpad-wasm/src/utils.rs

@@ -1,4 +1,4 @@
-//! Utility functions
+//! Utility functions.
 
 
 use wasm_bindgen::prelude::*;
 use wasm_bindgen::prelude::*;
 
 

+ 15 - 49
src/App.tsx

@@ -1,33 +1,22 @@
-import { useEffect, useState } from "react";
+import { useEffect } from "react";
 import { set_panic_hook } from "rustpad-wasm";
 import { set_panic_hook } from "rustpad-wasm";
+import Rustpad from "./rustpad";
 
 
-function App() {
-  const [input, setInput] = useState("");
-  const [socket, setSocket] = useState<WebSocket>();
-  const [messages, setMessages] = useState<[number, string][]>([]);
+set_panic_hook();
 
 
-  useEffect(() => {
-    set_panic_hook();
+const WS_URI =
+  (window.location.origin.startsWith("https") ? "wss://" : "ws://") +
+  window.location.host +
+  "/api/socket";
 
 
-    const uri =
-      (window.location.origin.startsWith("https") ? "wss://" : "ws://") +
-      window.location.host +
-      "/api/socket";
-    const ws = new WebSocket(uri);
-    console.log("connecting...");
-    ws.onopen = () => {
-      console.log("connected!");
-      setSocket(ws);
-    };
-    ws.onmessage = ({ data }) => {
-      console.log("message:", data);
-      setMessages((messages) => [...messages, ...JSON.parse(data)]);
-    };
-    ws.onclose = () => {
-      console.log("disconnected!");
-      setSocket(undefined);
-    };
-    return () => ws.close();
+function App() {
+  useEffect(() => {
+    const rustpad = new Rustpad({
+      uri: WS_URI,
+      onConnected: () => console.log("connected!"),
+      onDisconnected: () => console.log("disconnected!"),
+    });
+    return () => rustpad.dispose();
   }, []);
   }, []);
 
 
   return (
   return (
@@ -36,29 +25,6 @@ function App() {
         <div className="one-half column" style={{ marginTop: "25%" }}>
         <div className="one-half column" style={{ marginTop: "25%" }}>
           <h4>Chat Application</h4>
           <h4>Chat Application</h4>
           <p>Let's send some messages!</p>
           <p>Let's send some messages!</p>
-          <ul>
-            {messages.map(([sender, message], key) => (
-              <li key={key}>
-                <strong>User #{sender}:</strong> {message}
-              </li>
-            ))}
-          </ul>
-          <form
-            onSubmit={(event) => {
-              event.preventDefault();
-              socket?.send(input);
-              setInput("");
-            }}
-          >
-            <input
-              className="u-full-width"
-              required
-              placeholder="Hello!"
-              value={input}
-              onChange={(event) => setInput(event.target.value)}
-            />
-            <input className="button-primary" type="submit" />
-          </form>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>

+ 60 - 0
src/rustpad.ts

@@ -0,0 +1,60 @@
+/** Options passed in to the Rustpad constructor. */
+type RustpadOptions = {
+  readonly uri: string;
+  readonly onConnected?: () => unknown;
+  readonly onDisconnected?: () => unknown;
+  readonly reconnectInterval?: number;
+};
+
+/** Browser client for Rustpad. */
+class Rustpad {
+  private ws?: WebSocket;
+  private connecting?: boolean;
+  private readonly intervalId: number;
+
+  constructor(readonly options: RustpadOptions) {
+    this.tryConnect();
+    this.intervalId = window.setInterval(
+      () => this.tryConnect(),
+      options.reconnectInterval ?? 1000
+    );
+  }
+
+  /** Destroy this Rustpad instance and close any sockets. */
+  dispose() {
+    window.clearInterval(this.intervalId);
+    if (this.ws) this.ws.close();
+  }
+
+  /**
+   * Attempts a WebSocket connection.
+   *
+   * Safety Invariant: Until this WebSocket connection is closed, no other
+   * connections will be attempted because either `this.ws` or
+   * `this.connecting` will be set to a truthy value.
+   *
+   * Liveness Invariant: After this WebSocket connection closes, either through
+   * error or successful end, both `this.connecting` and `this.ws` will be set
+   * to falsy values.
+   */
+  private tryConnect() {
+    if (this.connecting || this.ws) return;
+    this.connecting = true;
+    const ws = new WebSocket(this.options.uri);
+    ws.onopen = () => {
+      this.connecting = false;
+      this.ws = ws;
+      this.options.onConnected?.();
+    };
+    ws.onclose = () => {
+      if (this.ws) {
+        this.ws = undefined;
+        this.options.onDisconnected?.();
+      } else {
+        this.connecting = false;
+      }
+    };
+  }
+}
+
+export default Rustpad;