fix in db and collaborate
This commit is contained in:
+909
@@ -0,0 +1,909 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl
|
||||
* Source-Available Code / Dual-Licensed.
|
||||
*
|
||||
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
|
||||
* Commercial or production use requires a valid paid license.
|
||||
* See LICENSE file in repository root for details.
|
||||
*/
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
error::Error,
|
||||
fmt,
|
||||
};
|
||||
|
||||
pub const MAX_OPERATION_COMPONENTS: usize = 4096;
|
||||
const MAX_OPERATION_OWNER_SPANS: usize = 8192;
|
||||
const MAX_OPERATION_INSERT_BYTES: usize = 2_000_000;
|
||||
const MAX_OWNER_LENGTH: usize = 120;
|
||||
const MAX_OPERATION_HISTORY: usize = 512;
|
||||
const MAX_OPERATION_HISTORY_BYTES: usize = 8 * 1024 * 1024;
|
||||
const AUTHORSHIP_VERSION: u8 = 2;
|
||||
const OWNER_COLOR_SEPARATOR: char = '\u{001f}';
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct OwnerSpan {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub owner: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum OperationComponent {
|
||||
Retain {
|
||||
count: usize,
|
||||
},
|
||||
Delete {
|
||||
count: usize,
|
||||
},
|
||||
Insert {
|
||||
text: String,
|
||||
#[serde(default)]
|
||||
owners: Vec<OwnerSpan>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TextOperation {
|
||||
#[serde(default)]
|
||||
pub components: Vec<OperationComponent>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct OwnerReplacement {
|
||||
pub owner: String,
|
||||
pub replacement: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AppliedOperation {
|
||||
pub base_revision_id: i64,
|
||||
pub revision_id: i64,
|
||||
pub client_id: String,
|
||||
pub update_id: u64,
|
||||
pub operation: TextOperation,
|
||||
pub owner_replacements: Vec<OwnerReplacement>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CollaborativeDocument {
|
||||
pub content: String,
|
||||
pub owner_map: String,
|
||||
pub revision_id: i64,
|
||||
history: VecDeque<AppliedOperation>,
|
||||
history_bytes: usize,
|
||||
acknowledged_updates: HashMap<String, u64>,
|
||||
}
|
||||
|
||||
impl CollaborativeDocument {
|
||||
pub fn new(content: String, owner_map: String, revision_id: i64) -> Self {
|
||||
Self {
|
||||
content,
|
||||
owner_map,
|
||||
revision_id,
|
||||
history: VecDeque::new(),
|
||||
history_bytes: 0,
|
||||
acknowledged_updates: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transform_from(
|
||||
&self,
|
||||
base_revision_id: i64,
|
||||
operation: &TextOperation,
|
||||
client_id: &str,
|
||||
update_id: u64,
|
||||
) -> Result<TextOperation, OperationError> {
|
||||
let mut transformed = normalize_operation(operation)?;
|
||||
if base_revision_id == self.revision_id {
|
||||
return Ok(transformed);
|
||||
}
|
||||
|
||||
let Some(start) = self
|
||||
.history
|
||||
.iter()
|
||||
.position(|entry| entry.base_revision_id == base_revision_id)
|
||||
else {
|
||||
return Err(OperationError::RevisionUnavailable);
|
||||
};
|
||||
|
||||
let mut expected_revision = base_revision_id;
|
||||
for applied in self.history.iter().skip(start) {
|
||||
if applied.base_revision_id != expected_revision {
|
||||
return Err(OperationError::RevisionUnavailable);
|
||||
}
|
||||
let incoming_has_priority =
|
||||
operation_key_before(client_id, update_id, &applied.client_id, applied.update_id);
|
||||
transformed =
|
||||
transform_operation(&transformed, &applied.operation, incoming_has_priority)?;
|
||||
expected_revision = applied.revision_id;
|
||||
if expected_revision == self.revision_id {
|
||||
return Ok(transformed);
|
||||
}
|
||||
}
|
||||
Err(OperationError::RevisionUnavailable)
|
||||
}
|
||||
|
||||
pub fn acknowledge(&mut self, client_id: &str, update_id: u64) {
|
||||
self.acknowledged_updates
|
||||
.entry(client_id.to_owned())
|
||||
.and_modify(|acknowledged| *acknowledged = (*acknowledged).max(update_id))
|
||||
.or_insert(update_id);
|
||||
}
|
||||
|
||||
pub fn has_applied_update(&self, client_id: &str, update_id: u64) -> bool {
|
||||
self.acknowledged_updates
|
||||
.get(client_id)
|
||||
.is_some_and(|acknowledged| update_id <= *acknowledged)
|
||||
}
|
||||
|
||||
pub fn acknowledged_updates(&self, client_id: &str) -> Vec<u64> {
|
||||
self.acknowledged_updates
|
||||
.get(client_id)
|
||||
.copied()
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn operations_after(&self, revision_id: i64) -> Option<Vec<AppliedOperation>> {
|
||||
if revision_id == self.revision_id {
|
||||
return Some(Vec::new());
|
||||
}
|
||||
let start = self
|
||||
.history
|
||||
.iter()
|
||||
.position(|entry| entry.base_revision_id == revision_id)?;
|
||||
let mut expected_revision = revision_id;
|
||||
let mut operations = Vec::new();
|
||||
for applied in self.history.iter().skip(start) {
|
||||
if applied.base_revision_id != expected_revision {
|
||||
return None;
|
||||
}
|
||||
operations.push(applied.clone());
|
||||
expected_revision = applied.revision_id;
|
||||
if expected_revision == self.revision_id {
|
||||
return Some(operations);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn record(&mut self, operation: AppliedOperation) {
|
||||
self.acknowledge(&operation.client_id, operation.update_id);
|
||||
self.history_bytes = self
|
||||
.history_bytes
|
||||
.saturating_add(applied_operation_size(&operation));
|
||||
self.history.push_back(operation);
|
||||
while self.history.len() > MAX_OPERATION_HISTORY
|
||||
|| self.history_bytes > MAX_OPERATION_HISTORY_BYTES
|
||||
{
|
||||
let Some(removed) = self.history.pop_front() else {
|
||||
break;
|
||||
};
|
||||
self.history_bytes = self
|
||||
.history_bytes
|
||||
.saturating_sub(applied_operation_size(&removed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ComponentKind {
|
||||
Retain,
|
||||
Delete,
|
||||
Insert,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum OperationError {
|
||||
InvalidComponent,
|
||||
InvalidUtf16Boundary,
|
||||
LengthMismatch,
|
||||
TooManyComponents,
|
||||
RevisionUnavailable,
|
||||
Serialization,
|
||||
}
|
||||
|
||||
impl fmt::Display for OperationError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let message = match self {
|
||||
Self::InvalidComponent => "invalid text operation component",
|
||||
Self::InvalidUtf16Boundary => "text operation splits a UTF-16 character",
|
||||
Self::LengthMismatch => "text operation length does not match the document",
|
||||
Self::TooManyComponents => "text operation contains too many components",
|
||||
Self::RevisionUnavailable => "the base revision is no longer available",
|
||||
Self::Serialization => "invalid authorship metadata",
|
||||
};
|
||||
formatter.write_str(message)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for OperationError {}
|
||||
|
||||
fn applied_operation_size(operation: &AppliedOperation) -> usize {
|
||||
let components = operation
|
||||
.operation
|
||||
.components
|
||||
.iter()
|
||||
.map(|component| match component {
|
||||
OperationComponent::Retain { .. } | OperationComponent::Delete { .. } => 24,
|
||||
OperationComponent::Insert { text, owners } => {
|
||||
32usize.saturating_add(text.len()).saturating_add(
|
||||
owners
|
||||
.iter()
|
||||
.map(|span| 24usize.saturating_add(span.owner.len()))
|
||||
.sum::<usize>(),
|
||||
)
|
||||
}
|
||||
})
|
||||
.sum::<usize>();
|
||||
components
|
||||
.saturating_add(operation.client_id.len())
|
||||
.saturating_add(
|
||||
operation
|
||||
.owner_replacements
|
||||
.iter()
|
||||
.map(|replacement| replacement.owner.len() + replacement.replacement.len() + 16)
|
||||
.sum::<usize>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn operation_key_before(
|
||||
left_client_id: &str,
|
||||
left_update_id: u64,
|
||||
right_client_id: &str,
|
||||
right_update_id: u64,
|
||||
) -> bool {
|
||||
left_client_id < right_client_id
|
||||
|| (left_client_id == right_client_id && left_update_id < right_update_id)
|
||||
}
|
||||
|
||||
fn component_kind(component: &OperationComponent) -> ComponentKind {
|
||||
match component {
|
||||
OperationComponent::Retain { .. } => ComponentKind::Retain,
|
||||
OperationComponent::Delete { .. } => ComponentKind::Delete,
|
||||
OperationComponent::Insert { .. } => ComponentKind::Insert,
|
||||
}
|
||||
}
|
||||
|
||||
fn component_length(component: &OperationComponent) -> usize {
|
||||
match component {
|
||||
OperationComponent::Retain { count } | OperationComponent::Delete { count } => *count,
|
||||
OperationComponent::Insert { text, .. } => text.encode_utf16().count(),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_owner_spans(spans: &[OwnerSpan], length: usize) -> Vec<OwnerSpan> {
|
||||
let mut sorted = spans
|
||||
.iter()
|
||||
.filter_map(|span| {
|
||||
let start = span.start.min(length);
|
||||
let end = span.end.min(length).max(start);
|
||||
if span.owner.is_empty() || end <= start {
|
||||
None
|
||||
} else {
|
||||
Some(OwnerSpan {
|
||||
start,
|
||||
end,
|
||||
owner: span.owner.clone(),
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
sorted.sort_by_key(|span| (span.start, span.end));
|
||||
let mut result: Vec<OwnerSpan> = Vec::new();
|
||||
for mut span in sorted {
|
||||
if let Some(previous) = result.last_mut() {
|
||||
if previous.owner == span.owner && span.start <= previous.end {
|
||||
previous.end = previous.end.max(span.end);
|
||||
continue;
|
||||
}
|
||||
if span.start < previous.end {
|
||||
span.start = previous.end;
|
||||
}
|
||||
}
|
||||
if span.end > span.start {
|
||||
result.push(span);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn slice_owner_spans(spans: &[OwnerSpan], start: usize, length: usize) -> Vec<OwnerSpan> {
|
||||
let end = start.saturating_add(length);
|
||||
let sliced = spans
|
||||
.iter()
|
||||
.filter_map(|span| {
|
||||
let overlap_start = start.max(span.start);
|
||||
let overlap_end = end.min(span.end);
|
||||
(overlap_end > overlap_start).then(|| OwnerSpan {
|
||||
start: overlap_start - start,
|
||||
end: overlap_end - start,
|
||||
owner: span.owner.clone(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
normalize_owner_spans(&sliced, length)
|
||||
}
|
||||
|
||||
fn shift_owner_spans(spans: &[OwnerSpan], offset: usize) -> Vec<OwnerSpan> {
|
||||
spans
|
||||
.iter()
|
||||
.map(|span| OwnerSpan {
|
||||
start: span.start + offset,
|
||||
end: span.end + offset,
|
||||
owner: span.owner.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn append_component(
|
||||
components: &mut Vec<OperationComponent>,
|
||||
component: OperationComponent,
|
||||
) -> Result<(), OperationError> {
|
||||
match component {
|
||||
OperationComponent::Retain { count } => {
|
||||
if count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(OperationComponent::Retain { count: previous }) = components.last_mut() {
|
||||
*previous = previous
|
||||
.checked_add(count)
|
||||
.ok_or(OperationError::InvalidComponent)?;
|
||||
} else {
|
||||
components.push(OperationComponent::Retain { count });
|
||||
}
|
||||
}
|
||||
OperationComponent::Delete { count } => {
|
||||
if count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(OperationComponent::Delete { count: previous }) = components.last_mut() {
|
||||
*previous = previous
|
||||
.checked_add(count)
|
||||
.ok_or(OperationError::InvalidComponent)?;
|
||||
} else {
|
||||
components.push(OperationComponent::Delete { count });
|
||||
}
|
||||
}
|
||||
OperationComponent::Insert { text, owners } => {
|
||||
let length = text.encode_utf16().count();
|
||||
if length == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let owners = normalize_owner_spans(&owners, length);
|
||||
if let Some(OperationComponent::Insert {
|
||||
text: previous_text,
|
||||
owners: previous_owners,
|
||||
}) = components.last_mut()
|
||||
{
|
||||
let offset = previous_text.encode_utf16().count();
|
||||
previous_text.push_str(&text);
|
||||
previous_owners.extend(shift_owner_spans(&owners, offset));
|
||||
*previous_owners =
|
||||
normalize_owner_spans(previous_owners, previous_text.encode_utf16().count());
|
||||
} else {
|
||||
components.push(OperationComponent::Insert { text, owners });
|
||||
}
|
||||
}
|
||||
}
|
||||
if components.len() > MAX_OPERATION_COMPONENTS {
|
||||
return Err(OperationError::TooManyComponents);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn normalize_operation(operation: &TextOperation) -> Result<TextOperation, OperationError> {
|
||||
if operation.components.len() > MAX_OPERATION_COMPONENTS {
|
||||
return Err(OperationError::TooManyComponents);
|
||||
}
|
||||
let mut inserted_bytes = 0usize;
|
||||
let mut owner_span_count = 0usize;
|
||||
let mut components = Vec::with_capacity(operation.components.len());
|
||||
for component in &operation.components {
|
||||
if let OperationComponent::Insert { text, owners } = component {
|
||||
inserted_bytes = inserted_bytes
|
||||
.checked_add(text.len())
|
||||
.ok_or(OperationError::InvalidComponent)?;
|
||||
owner_span_count = owner_span_count
|
||||
.checked_add(owners.len())
|
||||
.ok_or(OperationError::InvalidComponent)?;
|
||||
let text_length = text.encode_utf16().count();
|
||||
if inserted_bytes > MAX_OPERATION_INSERT_BYTES
|
||||
|| owner_span_count > MAX_OPERATION_OWNER_SPANS
|
||||
|| owners.iter().any(|span| {
|
||||
span.start > span.end
|
||||
|| span.end > text_length
|
||||
|| utf16_byte_index(text, span.start).is_err()
|
||||
|| utf16_byte_index(text, span.end).is_err()
|
||||
|| span.owner.chars().count() > MAX_OWNER_LENGTH
|
||||
|| span.owner.chars().any(|character| {
|
||||
character.is_control() && character != OWNER_COLOR_SEPARATOR
|
||||
})
|
||||
})
|
||||
{
|
||||
return Err(OperationError::InvalidComponent);
|
||||
}
|
||||
}
|
||||
append_component(&mut components, component.clone())?;
|
||||
}
|
||||
Ok(TextOperation { components })
|
||||
}
|
||||
|
||||
pub fn operation_base_length(operation: &TextOperation) -> Result<usize, OperationError> {
|
||||
normalize_operation(operation)?
|
||||
.components
|
||||
.iter()
|
||||
.try_fold(0usize, |length, component| {
|
||||
let component_length = match component {
|
||||
OperationComponent::Retain { count } | OperationComponent::Delete { count } => {
|
||||
*count
|
||||
}
|
||||
OperationComponent::Insert { .. } => 0,
|
||||
};
|
||||
length
|
||||
.checked_add(component_length)
|
||||
.ok_or(OperationError::InvalidComponent)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn operation_from_edit(
|
||||
previous_content: &str,
|
||||
next_content: &str,
|
||||
next_owner_map: &str,
|
||||
) -> TextOperation {
|
||||
let mut previous_prefix_bytes = 0usize;
|
||||
let mut next_prefix_bytes = 0usize;
|
||||
for (previous, next) in previous_content.chars().zip(next_content.chars()) {
|
||||
if previous != next {
|
||||
break;
|
||||
}
|
||||
previous_prefix_bytes += previous.len_utf8();
|
||||
next_prefix_bytes += next.len_utf8();
|
||||
}
|
||||
|
||||
let previous_remainder = &previous_content[previous_prefix_bytes..];
|
||||
let next_remainder = &next_content[next_prefix_bytes..];
|
||||
let mut previous_suffix_bytes = 0usize;
|
||||
let mut next_suffix_bytes = 0usize;
|
||||
for (previous, next) in previous_remainder
|
||||
.chars()
|
||||
.rev()
|
||||
.zip(next_remainder.chars().rev())
|
||||
{
|
||||
if previous != next {
|
||||
break;
|
||||
}
|
||||
previous_suffix_bytes += previous.len_utf8();
|
||||
next_suffix_bytes += next.len_utf8();
|
||||
}
|
||||
|
||||
let previous_middle_end = previous_content.len() - previous_suffix_bytes;
|
||||
let next_middle_end = next_content.len() - next_suffix_bytes;
|
||||
let previous_prefix = &previous_content[..previous_prefix_bytes];
|
||||
let previous_middle = &previous_content[previous_prefix_bytes..previous_middle_end];
|
||||
let next_middle = &next_content[next_prefix_bytes..next_middle_end];
|
||||
let suffix = &previous_content[previous_middle_end..];
|
||||
|
||||
let prefix_length = previous_prefix.encode_utf16().count();
|
||||
let deleted_length = previous_middle.encode_utf16().count();
|
||||
let inserted_length = next_middle.encode_utf16().count();
|
||||
let suffix_length = suffix.encode_utf16().count();
|
||||
let next_authorship = parse_authorship(next_content, next_owner_map);
|
||||
let inserted_owners = slice_owner_spans(&next_authorship.spans, prefix_length, inserted_length);
|
||||
|
||||
let mut components = Vec::new();
|
||||
if prefix_length > 0 {
|
||||
components.push(OperationComponent::Retain {
|
||||
count: prefix_length,
|
||||
});
|
||||
}
|
||||
if deleted_length > 0 {
|
||||
components.push(OperationComponent::Delete {
|
||||
count: deleted_length,
|
||||
});
|
||||
}
|
||||
if !next_middle.is_empty() {
|
||||
components.push(OperationComponent::Insert {
|
||||
text: next_middle.to_owned(),
|
||||
owners: inserted_owners,
|
||||
});
|
||||
}
|
||||
if suffix_length > 0 {
|
||||
components.push(OperationComponent::Retain {
|
||||
count: suffix_length,
|
||||
});
|
||||
}
|
||||
TextOperation { components }
|
||||
}
|
||||
|
||||
pub fn replace_operation(
|
||||
base_length: usize,
|
||||
content: String,
|
||||
owners: Vec<OwnerSpan>,
|
||||
) -> TextOperation {
|
||||
let mut components = Vec::new();
|
||||
if base_length > 0 {
|
||||
components.push(OperationComponent::Delete { count: base_length });
|
||||
}
|
||||
if !content.is_empty() {
|
||||
components.push(OperationComponent::Insert {
|
||||
text: content,
|
||||
owners,
|
||||
});
|
||||
}
|
||||
TextOperation { components }
|
||||
}
|
||||
|
||||
struct OperationCursor {
|
||||
components: Vec<OperationComponent>,
|
||||
index: usize,
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
impl OperationCursor {
|
||||
fn new(operation: &TextOperation) -> Result<Self, OperationError> {
|
||||
Ok(Self {
|
||||
components: normalize_operation(operation)?.components,
|
||||
index: 0,
|
||||
offset: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn current(&self) -> Option<&OperationComponent> {
|
||||
self.components.get(self.index)
|
||||
}
|
||||
|
||||
fn kind(&self) -> Option<ComponentKind> {
|
||||
self.current().map(component_kind)
|
||||
}
|
||||
|
||||
fn remaining(&self) -> usize {
|
||||
self.current()
|
||||
.map(|component| component_length(component).saturating_sub(self.offset))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn take(&mut self, count: usize) -> Result<OperationComponent, OperationError> {
|
||||
let component = self
|
||||
.current()
|
||||
.cloned()
|
||||
.ok_or(OperationError::InvalidComponent)?;
|
||||
if count == 0 || count > self.remaining() {
|
||||
return Err(OperationError::InvalidComponent);
|
||||
}
|
||||
let component_length = component_length(&component);
|
||||
let part = match component {
|
||||
OperationComponent::Retain { .. } => OperationComponent::Retain { count },
|
||||
OperationComponent::Delete { .. } => OperationComponent::Delete { count },
|
||||
OperationComponent::Insert { text, owners } => OperationComponent::Insert {
|
||||
text: slice_utf16(&text, self.offset, count)?.to_owned(),
|
||||
owners: slice_owner_spans(&owners, self.offset, count),
|
||||
},
|
||||
};
|
||||
self.offset += count;
|
||||
if self.offset == component_length {
|
||||
self.index += 1;
|
||||
self.offset = 0;
|
||||
}
|
||||
Ok(part)
|
||||
}
|
||||
|
||||
fn take_remaining(&mut self) -> Result<OperationComponent, OperationError> {
|
||||
let count = self.remaining();
|
||||
self.take(count)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transform_operation(
|
||||
left_operation: &TextOperation,
|
||||
right_operation: &TextOperation,
|
||||
left_before_right: bool,
|
||||
) -> Result<TextOperation, OperationError> {
|
||||
if operation_base_length(left_operation)? != operation_base_length(right_operation)? {
|
||||
return Err(OperationError::LengthMismatch);
|
||||
}
|
||||
let mut left = OperationCursor::new(left_operation)?;
|
||||
let mut right = OperationCursor::new(right_operation)?;
|
||||
let mut left_prime = Vec::new();
|
||||
|
||||
while left.current().is_some() || right.current().is_some() {
|
||||
if left.kind() == Some(ComponentKind::Insert)
|
||||
&& (right.kind() != Some(ComponentKind::Insert) || left_before_right)
|
||||
{
|
||||
append_component(&mut left_prime, left.take_remaining()?)?;
|
||||
continue;
|
||||
}
|
||||
if right.kind() == Some(ComponentKind::Insert) {
|
||||
let count = right.remaining();
|
||||
right.take_remaining()?;
|
||||
append_component(&mut left_prime, OperationComponent::Retain { count })?;
|
||||
continue;
|
||||
}
|
||||
let (Some(left_kind), Some(right_kind)) = (left.kind(), right.kind()) else {
|
||||
return Err(OperationError::InvalidComponent);
|
||||
};
|
||||
let count = left.remaining().min(right.remaining());
|
||||
match (left_kind, right_kind) {
|
||||
(ComponentKind::Retain, ComponentKind::Retain) => {
|
||||
append_component(&mut left_prime, OperationComponent::Retain { count })?;
|
||||
}
|
||||
(ComponentKind::Delete, ComponentKind::Retain) => {
|
||||
append_component(&mut left_prime, OperationComponent::Delete { count })?;
|
||||
}
|
||||
(ComponentKind::Retain, ComponentKind::Delete)
|
||||
| (ComponentKind::Delete, ComponentKind::Delete) => {}
|
||||
_ => return Err(OperationError::InvalidComponent),
|
||||
}
|
||||
left.take(count)?;
|
||||
right.take(count)?;
|
||||
}
|
||||
Ok(TextOperation {
|
||||
components: left_prime,
|
||||
})
|
||||
}
|
||||
|
||||
fn utf16_byte_index(value: &str, offset: usize) -> Result<usize, OperationError> {
|
||||
if offset == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let mut current = 0usize;
|
||||
for (byte_index, character) in value.char_indices() {
|
||||
if current == offset {
|
||||
return Ok(byte_index);
|
||||
}
|
||||
current += character.len_utf16();
|
||||
if current > offset {
|
||||
return Err(OperationError::InvalidUtf16Boundary);
|
||||
}
|
||||
}
|
||||
if current == offset {
|
||||
Ok(value.len())
|
||||
} else {
|
||||
Err(OperationError::LengthMismatch)
|
||||
}
|
||||
}
|
||||
|
||||
fn slice_utf16(value: &str, start: usize, length: usize) -> Result<&str, OperationError> {
|
||||
let start_byte = utf16_byte_index(value, start)?;
|
||||
let end_byte = utf16_byte_index(value, start.saturating_add(length))?;
|
||||
value
|
||||
.get(start_byte..end_byte)
|
||||
.ok_or(OperationError::InvalidUtf16Boundary)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct AuthorshipModel {
|
||||
#[serde(default = "authorship_version")]
|
||||
version: u8,
|
||||
#[serde(default)]
|
||||
spans: Vec<OwnerSpan>,
|
||||
}
|
||||
|
||||
fn authorship_version() -> u8 {
|
||||
AUTHORSHIP_VERSION
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RawAuthorship {
|
||||
Model(AuthorshipModel),
|
||||
LineOwners(Vec<String>),
|
||||
}
|
||||
|
||||
fn parse_authorship(content: &str, raw: &str) -> AuthorshipModel {
|
||||
let length = content.encode_utf16().count();
|
||||
match serde_json::from_str::<RawAuthorship>(raw) {
|
||||
Ok(RawAuthorship::Model(model)) if model.version == AUTHORSHIP_VERSION => AuthorshipModel {
|
||||
version: AUTHORSHIP_VERSION,
|
||||
spans: normalize_owner_spans(&model.spans, length),
|
||||
},
|
||||
Ok(RawAuthorship::LineOwners(owners)) => {
|
||||
let lines = content.split('\n').collect::<Vec<_>>();
|
||||
let mut offset = 0usize;
|
||||
let mut spans = Vec::new();
|
||||
for (index, line) in lines.iter().enumerate() {
|
||||
let line_length =
|
||||
line.encode_utf16().count() + usize::from(index + 1 < lines.len());
|
||||
let owner = owners.get(index).cloned().unwrap_or_default();
|
||||
if !owner.is_empty() && line_length > 0 {
|
||||
spans.push(OwnerSpan {
|
||||
start: offset,
|
||||
end: offset + line_length,
|
||||
owner,
|
||||
});
|
||||
}
|
||||
offset += line_length;
|
||||
}
|
||||
AuthorshipModel {
|
||||
version: AUTHORSHIP_VERSION,
|
||||
spans: normalize_owner_spans(&spans, length),
|
||||
}
|
||||
}
|
||||
_ => AuthorshipModel {
|
||||
version: AUTHORSHIP_VERSION,
|
||||
spans: Vec::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_retained_spans(
|
||||
target: &mut Vec<OwnerSpan>,
|
||||
spans: &[OwnerSpan],
|
||||
source_start: usize,
|
||||
length: usize,
|
||||
output_start: usize,
|
||||
) {
|
||||
let source_end = source_start + length;
|
||||
for span in spans {
|
||||
let start = source_start.max(span.start);
|
||||
let end = source_end.min(span.end);
|
||||
if end > start {
|
||||
target.push(OwnerSpan {
|
||||
start: output_start + start - source_start,
|
||||
end: output_start + end - source_start,
|
||||
owner: span.owner.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_operation_to_document(
|
||||
content: &str,
|
||||
owner_map: &str,
|
||||
operation: &TextOperation,
|
||||
owner_replacements: &[OwnerReplacement],
|
||||
) -> Result<(String, String), OperationError> {
|
||||
let operation = normalize_operation(operation)?;
|
||||
let content_length = content.encode_utf16().count();
|
||||
if operation_base_length(&operation)? != content_length {
|
||||
return Err(OperationError::LengthMismatch);
|
||||
}
|
||||
|
||||
let source_model = parse_authorship(content, owner_map);
|
||||
let mut output_spans = Vec::new();
|
||||
let mut source_offset = 0usize;
|
||||
let mut output_offset = 0usize;
|
||||
let mut output_content = String::new();
|
||||
|
||||
for component in &operation.components {
|
||||
match component {
|
||||
OperationComponent::Retain { count } => {
|
||||
output_content.push_str(slice_utf16(content, source_offset, *count)?);
|
||||
copy_retained_spans(
|
||||
&mut output_spans,
|
||||
&source_model.spans,
|
||||
source_offset,
|
||||
*count,
|
||||
output_offset,
|
||||
);
|
||||
source_offset += *count;
|
||||
output_offset += *count;
|
||||
}
|
||||
OperationComponent::Delete { count } => {
|
||||
source_offset += *count;
|
||||
}
|
||||
OperationComponent::Insert { text, owners } => {
|
||||
output_content.push_str(text);
|
||||
output_spans.extend(shift_owner_spans(owners, output_offset));
|
||||
output_offset += text.encode_utf16().count();
|
||||
}
|
||||
}
|
||||
}
|
||||
if source_offset != content_length {
|
||||
return Err(OperationError::LengthMismatch);
|
||||
}
|
||||
|
||||
for span in &mut output_spans {
|
||||
let identity = span
|
||||
.owner
|
||||
.split(OWNER_COLOR_SEPARATOR)
|
||||
.next()
|
||||
.unwrap_or_default();
|
||||
if let Some(replacement) = owner_replacements.iter().find(|replacement| {
|
||||
replacement.owner == identity && !replacement.replacement.is_empty()
|
||||
}) {
|
||||
span.owner.clone_from(&replacement.replacement);
|
||||
}
|
||||
}
|
||||
let model = AuthorshipModel {
|
||||
version: AUTHORSHIP_VERSION,
|
||||
spans: normalize_owner_spans(&output_spans, output_offset),
|
||||
};
|
||||
let owner_map = serde_json::to_string(&model).map_err(|_| OperationError::Serialization)?;
|
||||
Ok((output_content, owner_map))
|
||||
}
|
||||
|
||||
pub fn owner_spans_from_map(content: &str, owner_map: &str) -> Vec<OwnerSpan> {
|
||||
parse_authorship(content, owner_map).spans
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn operation(components: Vec<OperationComponent>) -> TextOperation {
|
||||
TextOperation { components }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_insertions_have_stable_order() {
|
||||
let left = operation(vec![
|
||||
OperationComponent::Retain { count: 1 },
|
||||
OperationComponent::Insert {
|
||||
text: "X".into(),
|
||||
owners: Vec::new(),
|
||||
},
|
||||
OperationComponent::Retain { count: 1 },
|
||||
]);
|
||||
let right = operation(vec![
|
||||
OperationComponent::Retain { count: 1 },
|
||||
OperationComponent::Insert {
|
||||
text: "Y".into(),
|
||||
owners: Vec::new(),
|
||||
},
|
||||
OperationComponent::Retain { count: 1 },
|
||||
]);
|
||||
let left_prime = transform_operation(&left, &right, true).unwrap();
|
||||
let right_prime = transform_operation(&right, &left, false).unwrap();
|
||||
let after_right = apply_operation_to_document("aYb", "[]", &left_prime, &[])
|
||||
.unwrap()
|
||||
.0;
|
||||
let after_left = apply_operation_to_document("aXb", "[]", &right_prime, &[])
|
||||
.unwrap()
|
||||
.0;
|
||||
assert_eq!(after_right, "aXYb");
|
||||
assert_eq!(after_left, "aXYb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf16_offsets_support_emoji() {
|
||||
let operation = operation(vec![
|
||||
OperationComponent::Retain { count: 3 },
|
||||
OperationComponent::Insert {
|
||||
text: "x".into(),
|
||||
owners: Vec::new(),
|
||||
},
|
||||
OperationComponent::Retain { count: 1 },
|
||||
]);
|
||||
let result = apply_operation_to_document("A😀B", "[]", &operation, &[])
|
||||
.unwrap()
|
||||
.0;
|
||||
assert_eq!(result, "A😀xB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_from_edit_preserves_utf16_boundaries() {
|
||||
let operation = operation_from_edit("A😀B", "A😀xB", "[]");
|
||||
let result = apply_operation_to_document("A😀B", "[]", &operation, &[])
|
||||
.unwrap()
|
||||
.0;
|
||||
assert_eq!(result, "A😀xB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acknowledgements_survive_history_compaction() {
|
||||
let mut document = CollaborativeDocument::new(String::new(), "[]".into(), 0);
|
||||
for update_id in 1..=MAX_OPERATION_HISTORY as u64 + 8 {
|
||||
let base_revision_id = document.revision_id;
|
||||
let revision_id = base_revision_id + 1;
|
||||
document.revision_id = revision_id;
|
||||
document.record(AppliedOperation {
|
||||
base_revision_id,
|
||||
revision_id,
|
||||
client_id: "client-123".into(),
|
||||
update_id,
|
||||
operation: TextOperation::default(),
|
||||
owner_replacements: Vec::new(),
|
||||
});
|
||||
}
|
||||
assert!(document.has_applied_update("client-123", 1));
|
||||
assert_eq!(
|
||||
document.acknowledged_updates("client-123"),
|
||||
vec![MAX_OPERATION_HISTORY as u64 + 8]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user