Add Wasm versions of the OT algorithm

This commit is contained in:
Eric Zhang 2021-06-01 16:46:35 -05:00
parent 5ca9a0ff6c
commit 8e068c2599
4 changed files with 160 additions and 12 deletions

View file

@ -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

View file

@ -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()
}
}

View file

@ -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

View file

@ -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);
}