Add Wasm versions of the OT algorithm
This commit is contained in:
parent
5ca9a0ff6c
commit
8e068c2599
4 changed files with 160 additions and 12 deletions
10
README.md
10
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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue