Browse Source

Add Wasm versions of the OT algorithm

Eric Zhang 4 years ago
parent
commit
8e068c2599
4 changed files with 160 additions and 12 deletions
  1. 10 0
      README.md
  2. 114 5
      rustpad-core/src/lib.rs
  3. 5 0
      rustpad-core/src/utils.rs
  4. 31 7
      rustpad-core/tests/web.rs

+ 10 - 0
README.md

@@ -46,6 +46,16 @@ npm start
 This command will open a browser window to `http://localhost:3000`, with hot
 reloading on changes.
 
+## Testing
+
+To run unit tests and integration tests for the server, use the standard
+`cargo test` command. For the WebAssembly component, you can run tests in a
+headless browser with
+
+```
+wasm-pack test rustpad-core --chrome --headless
+```
+
 ## Deployment
 
 Rustpad is distributed as a single ~10 MB Docker image, which is built from the

+ 114 - 5
rustpad-core/src/lib.rs

@@ -2,13 +2,122 @@
 
 #![warn(missing_docs)]
 
+use operational_transform::OperationSeq;
 use wasm_bindgen::prelude::*;
 
-mod utils;
+pub mod utils;
 
-/// Duplicate an input, returning a list of two copies
+/// This is an wrapper around `operational_transform::OperationSeq`, which is
+/// necessary for Wasm compatibility through `wasm-bindgen`.
 #[wasm_bindgen]
-pub fn duplicate(input: String) -> JsValue {
-    utils::set_panic_hook();
-    JsValue::from_serde(&vec![input; 2]).unwrap()
+#[derive(Default, Clone, Debug, PartialEq)]
+pub struct OpSeq(OperationSeq);
+
+/// This is a pair of `OpSeq` structs, which is needed to handle some return
+/// values from `wasm-bindgen`.
+#[wasm_bindgen]
+#[derive(Default, Clone, Debug, PartialEq)]
+pub struct OpSeqPair(OpSeq, OpSeq);
+
+#[wasm_bindgen]
+impl OpSeq {
+    /// Creates a store for operatations which does not need to allocate  until
+    /// `capacity` operations have been stored inside.
+    pub fn with_capacity(capacity: usize) -> Self {
+        Self(OperationSeq::with_capacity(capacity))
+    }
+
+    /// Merges the operation with `other` into one operation while preserving
+    /// the changes of both. Or, in other words, for each input string S and a
+    /// pair of consecutive operations A and B.
+    ///     `apply(apply(S, A), B) = apply(S, compose(A, B))`
+    /// must hold.
+    ///
+    /// # Error
+    ///
+    /// Returns `None` if the operations are not composable due to length
+    /// conflicts.
+    pub fn compose(&self, other: &OpSeq) -> Option<OpSeq> {
+        self.0.compose(&other.0).ok().map(Self)
+    }
+
+    /// Deletes `n` characters at the current cursor position.
+    pub fn delete(&mut self, n: u64) {
+        self.0.delete(n)
+    }
+
+    /// Inserts a `s` at the current cursor position.
+    pub fn insert(&mut self, s: &str) {
+        self.0.insert(s)
+    }
+
+    /// Moves the cursor `n` characters forwards.
+    pub fn retain(&mut self, n: u64) {
+        self.0.retain(n)
+    }
+
+    /// Transforms two operations A and B that happened concurrently and produces
+    /// two operations A' and B' (in an array) such that
+    ///     `apply(apply(S, A), B') = apply(apply(S, B), A')`.
+    /// This function is the heart of OT.
+    ///
+    /// # Error
+    ///
+    /// Returns `None` if the operations cannot be transformed due to
+    /// length conflicts.
+    pub fn transform(&self, other: &OpSeq) -> Option<OpSeqPair> {
+        let (a, b) = self.0.transform(&other.0).ok()?;
+        Some(OpSeqPair(Self(a), Self(b)))
+    }
+
+    /// Applies an operation to a string, returning a new string.
+    ///
+    /// # Error
+    ///
+    /// Returns an error if the operation cannot be applied due to length
+    /// conflicts.
+    pub fn apply(&self, s: &str) -> Option<String> {
+        self.0.apply(s).ok()
+    }
+
+    /// Computes the inverse of an operation. The inverse of an operation is the
+    /// operation that reverts the effects of the operation, e.g. when you have
+    /// an operation 'insert("hello "); skip(6);' then the inverse is
+    /// 'delete("hello "); skip(6);'. The inverse should be used for
+    /// implementing undo.
+    pub fn invert(&self, s: &str) -> Self {
+        Self(self.0.invert(s))
+    }
+
+    /// Checks if this operation has no effect.
+    #[inline]
+    pub fn is_noop(&self) -> bool {
+        self.0.is_noop()
+    }
+
+    /// Returns the length of a string these operations can be applied to
+    #[inline]
+    pub fn base_len(&self) -> usize {
+        self.0.base_len()
+    }
+
+    /// Returns the length of the resulting string after the operations have
+    /// been applied.
+    #[inline]
+    pub fn target_len(&self) -> usize {
+        self.0.target_len()
+    }
+}
+
+#[wasm_bindgen]
+impl OpSeqPair {
+    /// Returns the first element of the pair.
+    pub fn first(&self) -> OpSeq {
+        self.0.clone()
+    }
+
+    /// Returns the second element of the pair.
+    pub fn second(&self) -> OpSeq {
+        self.1.clone()
+    }
 }

+ 5 - 0
rustpad-core/src/utils.rs

@@ -1,4 +1,9 @@
+//! Utility functions
+
+use wasm_bindgen::prelude::*;
+
 /// Set a panic listener to display better error messages.
+#[wasm_bindgen]
 pub fn set_panic_hook() {
     // When the `console_error_panic_hook` feature is enabled, we can call the
     // `set_panic_hook` function at least once during initialization, and then

+ 31 - 7
rustpad-core/tests/web.rs

@@ -2,17 +2,41 @@
 
 #![cfg(target_arch = "wasm32")]
 
-use rustpad_core::duplicate;
+use rustpad_core::OpSeq;
 
-use js_sys::JSON;
 use wasm_bindgen_test::*;
 
 wasm_bindgen_test_configure!(run_in_browser);
 
 #[wasm_bindgen_test]
-fn pass() {
-    let s = String::from("foobar");
-    let value = duplicate(s);
-    let value = JSON::stringify(&value).unwrap();
-    assert_eq!(value.to_string(), String::from("[\"foobar\",\"foobar\"]"));
+fn compose_operations() {
+    let mut a = OpSeq::default();
+    a.insert("abc");
+    let mut b = OpSeq::default();
+    b.retain(3);
+    b.insert("def");
+    let after_a = a.apply("").unwrap();
+    let after_b = b.apply(&after_a).unwrap();
+    let c = a.compose(&b).unwrap();
+    let after_c = c.apply("").unwrap();
+    assert_eq!(after_c, after_b);
+}
+
+#[wasm_bindgen_test]
+fn transform_operations() {
+    let s = "abc";
+    let mut a = OpSeq::default();
+    a.retain(3);
+    a.insert("def");
+    let mut b = OpSeq::default();
+    b.retain(3);
+    b.insert("ghi");
+    let pair = a.transform(&b).unwrap();
+    let (a_prime, b_prime) = (pair.first(), pair.second());
+    let ab_prime = a.compose(&b_prime).unwrap();
+    let ba_prime = b.compose(&a_prime).unwrap();
+    let after_ab_prime = ab_prime.apply(s).unwrap();
+    let after_ba_prime = ba_prime.apply(s).unwrap();
+    assert_eq!(ab_prime, ba_prime);
+    assert_eq!(after_ab_prime, after_ba_prime);
 }