use anyhow::Result;
use yrs::updates::decoder::Decode;
use yrs::updates::encoder::Encode;
use yrs::{Doc, GetString, ReadTxn, StateVector, Text, Transact, Update};
const TEXT_KEY: &str = "content";
pub struct ReplicaState {
doc: Doc,
}
impl ReplicaState {
pub fn new(client_id: u64) -> Self {
let doc = Doc::with_client_id(client_id);
doc.get_or_insert_text(TEXT_KEY);
Self { doc }
}
pub fn from_encoded(client_id: u64, state: &[u8]) -> Result<Self> {
let replica = Self::new(client_id);
replica.apply_update(state)?;
Ok(replica)
}
pub fn text(&self) -> String {
let text = self.doc.get_or_insert_text(TEXT_KEY);
let txn = self.doc.transact();
text.get_string(&txn)
}
pub fn apply_local_edit(&self, offset: u32, delete_len: u32, insert: &str) {
let text = self.doc.get_or_insert_text(TEXT_KEY);
let mut txn = self.doc.transact_mut();
if delete_len > 0 {
text.remove_range(&mut txn, offset, delete_len);
}
if !insert.is_empty() {
text.insert(&mut txn, offset, insert);
}
}
pub fn state_vector(&self) -> Vec<u8> {
let txn = self.doc.transact();
txn.state_vector().encode_v1()
}
pub fn diff(&self, their_sv: &[u8]) -> Result<Vec<u8>> {
let sv = StateVector::decode_v1(their_sv)
.map_err(|e| anyhow::anyhow!("decode state vector: {e}"))?;
let txn = self.doc.transact();
Ok(txn.encode_state_as_update_v1(&sv))
}
pub fn apply_update(&self, update: &[u8]) -> Result<()> {
let update =
Update::decode_v1(update).map_err(|e| anyhow::anyhow!("decode update: {e}"))?;
let mut txn = self.doc.transact_mut();
txn.apply_update(update)
.map_err(|e| anyhow::anyhow!("apply update: {e}"))?;
Ok(())
}
pub fn encode_state(&self) -> Vec<u8> {
let txn = self.doc.transact();
txn.encode_state_as_update_v1(&StateVector::default())
}
}
pub fn sync(a: &ReplicaState, b: &ReplicaState) -> Result<()> {
let a_sv = a.state_vector();
let b_sv = b.state_vector();
let a_to_b = a.diff(&b_sv)?;
let b_to_a = b.diff(&a_sv)?;
b.apply_update(&a_to_b)?;
a.apply_update(&b_to_a)?;
Ok(())
}
fn canonical_covers(canonical: &ReplicaState, peer: &ReplicaState) -> Result<bool> {
let missing = peer.diff(&canonical.state_vector())?;
let probe = ReplicaState::from_encoded(0, &canonical.encode_state())?;
let before = probe.state_vector();
probe.apply_update(&missing)?;
Ok(probe.state_vector() == before)
}
pub fn commit_barrier_ready(canonical: &ReplicaState, editors: &[&ReplicaState]) -> Result<bool> {
for editor in editors {
if !canonical_covers(canonical, editor)? {
return Ok(false);
}
}
Ok(true)
}
pub fn flush_to_commit_barrier(
canonical: &ReplicaState,
editors: &[&ReplicaState],
) -> Result<bool> {
for editor in editors {
let missing = editor.diff(&canonical.state_vector())?;
canonical.apply_update(&missing)?;
}
commit_barrier_ready(canonical, editors)
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_sv() -> Vec<u8> {
ReplicaState::new(999).state_vector()
}
#[test]
fn incremental_sync_converges_divergent_edits() {
let base = ReplicaState::new(1);
base.apply_local_edit(0, 0, "hello world");
let snapshot = base.encode_state();
let a = ReplicaState::from_encoded(1, &snapshot).unwrap();
let b = ReplicaState::from_encoded(2, &snapshot).unwrap();
a.apply_local_edit(0, 0, "A: ");
let b_len = b.text().chars().count() as u32;
b.apply_local_edit(b_len, 0, " [B]");
sync(&a, &b).unwrap();
assert_eq!(a.text(), b.text(), "replicas converge after one sync round");
assert!(a.text().contains("A: ") && a.text().contains("[B]"));
}
#[test]
fn diff_ships_only_missing_delta_not_whole_snapshot() {
let base = ReplicaState::new(1);
base.apply_local_edit(0, 0, "the quick brown fox jumps over the lazy dog");
let snapshot = base.encode_state();
let a = ReplicaState::from_encoded(1, &snapshot).unwrap();
let b = ReplicaState::from_encoded(2, &snapshot).unwrap();
a.apply_local_edit(0, 0, "X");
let delta = a.diff(&b.state_vector()).unwrap();
let full = a.encode_state();
assert!(
delta.len() < full.len(),
"incremental delta ({}) must be smaller than the whole snapshot ({})",
delta.len(),
full.len()
);
b.apply_update(&delta).unwrap();
assert_eq!(a.text(), b.text());
}
#[test]
fn apply_update_is_idempotent() {
let base = ReplicaState::new(1);
base.apply_local_edit(0, 0, "content");
let a = ReplicaState::from_encoded(1, &base.encode_state()).unwrap();
let b = ReplicaState::from_encoded(2, &base.encode_state()).unwrap();
a.apply_local_edit(0, 0, "PREFIX ");
let delta = a.diff(&b.state_vector()).unwrap();
b.apply_update(&delta).unwrap();
let once = b.text();
b.apply_update(&delta).unwrap();
assert_eq!(b.text(), once, "re-applying a known update is idempotent");
}
#[test]
fn out_of_order_updates_self_heal_via_causal_buffering() {
let a = ReplicaState::new(1);
a.apply_local_edit(0, 0, "first");
let u1 = a.diff(&empty_sv()).unwrap();
let sv_after_u1 = a.state_vector();
let a_len = a.text().chars().count() as u32;
a.apply_local_edit(a_len, 0, " second");
let u2 = a.diff(&sv_after_u1).unwrap();
let b = ReplicaState::new(2);
b.apply_update(&u2).unwrap(); assert_ne!(
b.text(),
a.text(),
"u2 alone cannot apply — its deps are missing"
);
b.apply_update(&u1).unwrap(); assert_eq!(
b.text(),
a.text(),
"out-of-order delivery self-heals once causal deps arrive"
);
}
#[test]
fn bidirectional_sync_converges_and_keeps_both_contributions() {
let a = ReplicaState::new(1);
a.apply_local_edit(0, 0, "alpha");
let b = ReplicaState::new(2);
b.apply_local_edit(0, 0, "beta");
sync(&a, &b).unwrap();
assert_eq!(a.text(), b.text());
assert!(a.text().contains("alpha") && a.text().contains("beta"));
}
#[test]
fn repeated_sync_is_stable_no_op_after_convergence() {
let a = ReplicaState::new(1);
a.apply_local_edit(0, 0, "stable");
let b = ReplicaState::new(2);
sync(&a, &b).unwrap();
let converged = a.text();
sync(&a, &b).unwrap();
assert_eq!(a.text(), converged);
assert_eq!(b.text(), converged);
}
#[test]
fn encode_state_roundtrips_text() {
let a = ReplicaState::new(7);
a.apply_local_edit(0, 0, "roundtrip me");
let restored = ReplicaState::from_encoded(7, &a.encode_state()).unwrap();
assert_eq!(restored.text(), "roundtrip me");
}
#[test]
fn commit_barrier_blocks_until_editor_ops_are_flushed() {
let canonical = ReplicaState::new(1);
canonical.apply_local_edit(0, 0, "base");
let editor = ReplicaState::from_encoded(2, &canonical.encode_state()).unwrap();
let len = editor.text().chars().count() as u32;
editor.apply_local_edit(len, 0, " typed");
assert!(
!commit_barrier_ready(&canonical, &[&editor]).unwrap(),
"barrier must block while an editor has un-propagated ops"
);
assert!(flush_to_commit_barrier(&canonical, &[&editor]).unwrap());
assert!(commit_barrier_ready(&canonical, &[&editor]).unwrap());
assert!(canonical.text().contains("typed"));
let restored = ReplicaState::from_encoded(9, &canonical.encode_state()).unwrap();
assert_eq!(restored.text(), canonical.text());
}
#[test]
fn commit_barrier_is_trivially_ready_with_no_live_editors() {
let canonical = ReplicaState::new(1);
canonical.apply_local_edit(0, 0, "headless");
assert!(commit_barrier_ready(&canonical, &[]).unwrap());
}
#[test]
fn commit_barrier_reopens_on_a_new_unsynced_op() {
let canonical = ReplicaState::new(1);
let e1 = ReplicaState::new(2);
let e2 = ReplicaState::new(3);
e1.apply_local_edit(0, 0, "one");
e2.apply_local_edit(0, 0, "two");
assert!(flush_to_commit_barrier(&canonical, &[&e1, &e2]).unwrap());
assert!(canonical.text().contains("one") && canonical.text().contains("two"));
e1.apply_local_edit(0, 0, "Z");
assert!(
!commit_barrier_ready(&canonical, &[&e1, &e2]).unwrap(),
"a new editor op after the cut must re-open the barrier"
);
}
}