use serde_json::{Map, Value};
use crate::sync::protocol::{Change, Op, RemoteRow};
use crate::sync::resolver::{ConflictResolver, LwwResolver, Resolution};
use super::registry::collaborative_columns_for_table;
use super::text::{CollabText, MAX_WIRE_ELEMENTS, MAX_WIRE_PENDING};
#[derive(Debug, Clone)]
pub struct CollabResolver<R = LwwResolver> {
fields: Vec<String>,
collection: Option<String>,
inner: R,
}
impl CollabResolver<LwwResolver> {
#[must_use]
pub fn new<S: Into<String>>(fields: impl IntoIterator<Item = S>) -> Self {
Self::with_fallback(fields, LwwResolver)
}
#[must_use]
pub fn for_table(table: &str) -> Self {
Self::new(collaborative_columns_for_table(table)).in_collection(table)
}
}
impl<R> CollabResolver<R> {
#[must_use]
pub fn with_fallback<S: Into<String>>(fields: impl IntoIterator<Item = S>, inner: R) -> Self {
Self {
fields: fields.into_iter().map(Into::into).collect(),
collection: None,
inner,
}
}
#[must_use]
pub fn in_collection(mut self, collection: impl Into<String>) -> Self {
self.collection = Some(collection.into());
self
}
#[must_use]
pub fn fields(&self) -> &[String] {
&self.fields
}
#[must_use]
pub fn collection(&self) -> Option<&str> {
self.collection.as_deref()
}
}
impl<R: ConflictResolver> ConflictResolver for CollabResolver<R> {
fn resolve(&self, client_device_id: &str, client: &Change, server: &RemoteRow) -> Resolution {
let verdict = self.inner.resolve(client_device_id, client, server);
if self
.collection
.as_ref()
.is_some_and(|only| only != &client.collection)
{
return verdict;
}
if client.op == Op::Delete || server.deleted {
return verdict;
}
let (Some(Value::Object(from_client)), Some(Value::Object(from_server))) =
(client.payload.as_ref(), server.payload.as_ref())
else {
return verdict;
};
let mut merged: Map<String, Value> = match &verdict {
Resolution::TakeClient => from_client.clone(),
Resolution::KeepServer => from_server.clone(),
Resolution::Merge(Value::Object(base)) => base.clone(),
Resolution::Merge(_) => return verdict,
};
let mut changed = false;
for field in &self.fields {
match (from_client.get(field), from_server.get(field)) {
(Some(mine), Some(theirs)) => {
let (Ok(mut mine), Ok(theirs)) = (
serde_json::from_value::<CollabText>(mine.clone()),
serde_json::from_value::<CollabText>(theirs.clone()),
) else {
continue;
};
mine.merge(&theirs);
let total = mine.element_count() + mine.pending_len();
if total > MAX_WIRE_ELEMENTS
|| mine.element_count() > MAX_WIRE_ELEMENTS
|| mine.pending_len() > MAX_WIRE_PENDING
{
tracing::warn!(
field,
elements = mine.element_count(),
pending = mine.pending_len(),
total,
"collab: merged document is past the wire limits; leaving the \
wrapped verdict rather than storing one that cannot be read"
);
continue;
}
let Ok(encoded) = serde_json::to_value(&mine) else {
continue;
};
merged.insert(field.clone(), encoded);
changed = true;
}
(Some(only), None) | (None, Some(only)) => {
if serde_json::from_value::<CollabText>(only.clone()).is_ok() {
merged.insert(field.clone(), only.clone());
changed = true;
}
}
(None, None) => {}
}
}
if changed {
Resolution::Merge(Value::Object(merged))
} else {
verdict
}
}
}