use std::collections::BTreeMap;
use super::text::{self, Change};
pub type ClientId = String;
#[derive(Debug, Clone, PartialEq, Eq)]
struct ClientDoc {
version: i64,
text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Doc {
language_id: String,
server_version: i32,
server_text: String,
owner: ClientId,
clients: BTreeMap<ClientId, ClientDoc>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outgoing {
Open { path: String, language_id: String, version: i32, text: String },
Edit { path: String, version: i32, changes: Vec<Change> },
Replace { path: String, version: i32, text: String },
Close { path: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SyncError {
NotOpen { path: String },
Stale { path: String, have: i64, asked: i64 },
Gap { path: String, have: i64, from: i64 },
Invalid { path: String, detail: String },
}
impl std::fmt::Display for SyncError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotOpen { path } => write!(f, "{path} is not open in this page"),
Self::Stale { path, have, asked } => {
write!(f, "{path} is at version {have} here, not {asked}: the request is about text that has changed")
}
Self::Gap { path, have, from } => {
write!(f, "{path} is at version {have} here; an edit from version {from} cannot be applied")
}
Self::Invalid { path, detail } => write!(f, "an edit of {path} does not apply: {detail}"),
}
}
}
#[derive(Debug, Default)]
pub struct Registry {
docs: BTreeMap<String, Doc>,
}
impl Registry {
pub fn server_copy(&self, path: &str) -> Option<(i32, &str)> {
self.docs.get(path).map(|d| (d.server_version, d.server_text.as_str()))
}
pub fn is_empty(&self) -> bool {
self.docs.is_empty()
}
pub fn open(&mut self, client: &str, path: &str, language_id: &str, version: i64, text: &str) -> Vec<Outgoing> {
let page = ClientDoc { version, text: text.to_owned() };
match self.docs.get_mut(path) {
None => {
let mut clients = BTreeMap::new();
clients.insert(client.to_owned(), page);
self.docs.insert(
path.to_owned(),
Doc {
language_id: language_id.to_owned(),
server_version: 1,
server_text: text.to_owned(),
owner: client.to_owned(),
clients,
},
);
vec![Outgoing::Open {
path: path.to_owned(),
language_id: language_id.to_owned(),
version: 1,
text: text.to_owned(),
}]
}
Some(doc) => {
doc.clients.insert(client.to_owned(), page);
take_over(path, doc, client).into_iter().collect()
}
}
}
pub fn change(
&mut self,
client: &str,
path: &str,
from: i64,
to: i64,
changes: &[Change],
) -> Result<Vec<Outgoing>, SyncError> {
let doc = self.docs.get_mut(path).ok_or_else(|| SyncError::NotOpen { path: path.to_owned() })?;
let page = doc.clients.get_mut(client).ok_or_else(|| SyncError::NotOpen { path: path.to_owned() })?;
if page.version != from {
return Err(SyncError::Gap { path: path.to_owned(), have: page.version, from });
}
let edited =
text::apply(&page.text, changes).map_err(|detail| SyncError::Invalid { path: path.to_owned(), detail })?;
page.text = edited;
page.version = to;
if doc.owner == client {
doc.server_text = doc.clients[client].text.clone();
doc.server_version += 1;
return Ok(vec![Outgoing::Edit { path: path.to_owned(), version: doc.server_version, changes: changes.to_vec() }]);
}
Ok(take_over(path, doc, client).into_iter().collect())
}
pub fn ensure(&mut self, client: &str, path: &str, version: i64) -> Result<Vec<Outgoing>, SyncError> {
let doc = self.docs.get_mut(path).ok_or_else(|| SyncError::NotOpen { path: path.to_owned() })?;
let page = doc.clients.get(client).ok_or_else(|| SyncError::NotOpen { path: path.to_owned() })?;
if page.version != version {
return Err(SyncError::Stale { path: path.to_owned(), have: page.version, asked: version });
}
Ok(take_over(path, doc, client).into_iter().collect())
}
pub fn close(&mut self, client: &str, path: &str) -> Vec<Outgoing> {
let Some(doc) = self.docs.get_mut(path) else { return Vec::new() };
if doc.clients.remove(client).is_none() {
return Vec::new();
}
let Some(next) = doc.clients.keys().next().cloned() else {
self.docs.remove(path);
return vec![Outgoing::Close { path: path.to_owned() }];
};
if doc.owner != client {
return Vec::new();
}
take_over(path, doc, &next).into_iter().collect()
}
pub fn close_client(&mut self, client: &str) -> Vec<Outgoing> {
let paths: Vec<String> =
self.docs.iter().filter(|(_, d)| d.clients.contains_key(client)).map(|(p, _)| p.clone()).collect();
paths.iter().flat_map(|p| self.close(client, p)).collect()
}
pub fn replay(&self) -> Vec<Outgoing> {
self.docs
.iter()
.map(|(p, d)| Outgoing::Open {
path: p.clone(),
language_id: d.language_id.clone(),
version: d.server_version,
text: d.server_text.clone(),
})
.collect()
}
}
fn take_over(path: &str, doc: &mut Doc, client: &str) -> Option<Outgoing> {
doc.owner = client.to_owned();
let text = &doc.clients[client].text;
if *text == doc.server_text {
return None;
}
doc.server_text = text.clone();
doc.server_version += 1;
Some(Outgoing::Replace { path: path.to_owned(), version: doc.server_version, text: text.clone() })
}
#[cfg(test)]
#[path = "documents_tests.rs"]
mod tests;