use yrs::updates::decoder::Decode;
use yrs::{Doc, GetString, ReadTxn, StateVector, Text, Transact, Update};
fn load_doc(bytes: &[u8]) -> Result<Doc, String> {
let doc = Doc::new();
if !bytes.is_empty() {
let update = Update::decode_v1(bytes)
.map_err(|e| format!("crdt column is not a Yjs update: {e}"))?;
doc.transact_mut()
.apply_update(update)
.map_err(|e| format!("applying crdt update failed: {e}"))?;
}
Ok(doc)
}
fn column_bytes(doc: &Doc) -> Vec<u8> {
doc.transact()
.encode_state_as_update_v1(&StateVector::default())
}
pub fn text(bytes: &[u8], name: &str) -> Result<String, String> {
let doc = load_doc(bytes)?;
let text = doc.get_or_insert_text(name);
let value = text.get_string(&doc.transact());
Ok(value)
}
pub fn insert_text(bytes: &[u8], name: &str, index: u32, value: &str) -> Result<Vec<u8>, String> {
let doc = load_doc(bytes)?;
let text = doc.get_or_insert_text(name);
{
let mut txn = doc.transact_mut();
let len = text.len(&txn);
if index > len {
return Err(format!("crdt insert index {index} past text length {len}"));
}
text.insert(&mut txn, index, value);
}
Ok(column_bytes(&doc))
}
pub fn delete_text(bytes: &[u8], name: &str, index: u32, len: u32) -> Result<Vec<u8>, String> {
let doc = load_doc(bytes)?;
let text = doc.get_or_insert_text(name);
{
let mut txn = doc.transact_mut();
let text_len = text.len(&txn);
if index.saturating_add(len) > text_len {
return Err(format!(
"crdt delete range {index}+{len} past text length {text_len}"
));
}
text.remove_range(&mut txn, index, len);
}
Ok(column_bytes(&doc))
}
pub fn apply_update(bytes: &[u8], update: &[u8]) -> Result<Vec<u8>, String> {
let doc = load_doc(bytes)?;
if !update.is_empty() {
let update = Update::decode_v1(update)
.map_err(|e| format!("crdt update is not a Yjs update: {e}"))?;
doc.transact_mut()
.apply_update(update)
.map_err(|e| format!("applying crdt update failed: {e}"))?;
}
Ok(column_bytes(&doc))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn insert_then_materialize_roundtrips() {
let update = insert_text(&[], "text", 0, "hello").unwrap();
assert_eq!(text(&update, "text").unwrap(), "hello");
let update2 = insert_text(&update, "text", 5, " world").unwrap();
assert_eq!(text(&update2, "text").unwrap(), "hello world");
}
#[test]
fn delete_removes_range() {
let update = insert_text(&[], "text", 0, "hello world").unwrap();
let update = delete_text(&update, "text", 5, 6).unwrap();
assert_eq!(text(&update, "text").unwrap(), "hello");
}
#[test]
fn empty_bytes_is_empty_document() {
assert_eq!(text(&[], "text").unwrap(), "");
}
#[test]
fn apply_update_merges_and_is_idempotent() {
let a = insert_text(&[], "text", 0, "A").unwrap();
let merged = apply_update(&[], &a).unwrap();
assert_eq!(text(&merged, "text").unwrap(), "A");
let merged2 = apply_update(&merged, &a).unwrap();
assert_eq!(text(&merged2, "text").unwrap(), "A");
}
#[test]
fn malformed_bytes_error_loudly() {
assert!(text(&[1, 2, 3, 4], "text").is_err());
assert!(insert_text(&[9, 9, 9], "text", 0, "x").is_err());
}
}