1use crate::types::{MessageId, RunConfig, ThreadMessage};
10use std::cell::RefCell;
11use std::collections::{HashMap, HashSet, VecDeque};
12use std::fmt;
13use std::sync::Arc;
14
15#[derive(Clone, Debug, PartialEq)]
17pub struct ExportedMessage {
18 pub message: Arc<ThreadMessage>,
20 pub parent_id: Option<MessageId>,
22 pub run_config: Option<RunConfig>,
24}
25
26#[derive(Clone, Debug, Default, PartialEq)]
28pub struct ExportedRepository {
29 pub head_id: Option<Option<MessageId>>,
31 pub messages: Vec<ExportedMessage>,
33}
34
35impl ExportedRepository {
36 pub fn from_linear(messages: impl IntoIterator<Item = ThreadMessage>) -> Self {
38 let mut parent: Option<MessageId> = None;
39 let messages = messages
40 .into_iter()
41 .map(|message| {
42 let id = message.id().clone();
43 let item = ExportedMessage {
44 message: Arc::new(message),
45 parent_id: parent.take(),
46 run_config: None,
47 };
48 parent = Some(id);
49 item
50 })
51 .collect();
52 Self {
53 head_id: None,
54 messages,
55 }
56 }
57}
58
59#[derive(Clone, Debug, PartialEq, Eq)]
62#[non_exhaustive]
63pub enum RepositoryError {
64 MessageNotFound(MessageId),
66 ParentNotFound(MessageId),
68 Cycle(MessageId),
70 InvalidReplacement(MessageId),
72}
73
74impl fmt::Display for RepositoryError {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 match self {
77 Self::MessageNotFound(id) => write!(f, "message {id} not found"),
78 Self::ParentNotFound(id) => write!(f, "parent message {id} not found"),
79 Self::Cycle(id) => write!(f, "message {id} would become its own ancestor"),
80 Self::InvalidReplacement(id) => {
81 write!(f, "{id} cannot replace a message it descends from")
82 }
83 }
84 }
85}
86impl std::error::Error for RepositoryError {}
87
88#[derive(Clone, Debug)]
90pub struct MessageEntry {
91 pub message: Arc<ThreadMessage>,
93 pub parent_id: Option<MessageId>,
95 pub index: usize,
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct MessageChanges {
105 pub version: u64,
107 pub indices: Option<Vec<usize>>,
111}
112
113const CHANGE_HISTORY: usize = 64;
116
117#[derive(Default)]
118struct Children {
119 ids: Vec<MessageId>,
120 next: Option<MessageId>,
122}
123
124struct Node {
125 message: Arc<ThreadMessage>,
126 parent: Option<MessageId>,
127 children: Children,
128 level: usize,
129}
130
131#[derive(Default)]
133pub struct MessageRepository {
134 nodes: HashMap<MessageId, Node>,
135 root: Children,
136 head: Option<MessageId>,
137 path: RefCell<Option<Arc<[Arc<ThreadMessage>]>>>,
138 shape_version: u64,
140 change_version: u64,
141 change_base: u64,
142 changes: VecDeque<(u64, usize)>,
143}
144
145impl MessageRepository {
146 pub fn new() -> Self {
148 Self::default()
149 }
150
151 pub fn len(&self) -> usize {
153 self.nodes.len()
154 }
155 pub fn is_empty(&self) -> bool {
157 self.nodes.is_empty()
158 }
159 pub fn contains(&self, id: &MessageId) -> bool {
161 self.nodes.contains_key(id)
162 }
163 pub fn head_id(&self) -> Option<&MessageId> {
165 self.head.as_ref()
166 }
167
168 pub fn canonical_head_id(&self) -> Option<&MessageId> {
170 let mut head = self.head.as_ref();
171 while let Some(node) = head.and_then(|id| self.nodes.get(id)) {
172 if !node.message.is_optimistic() {
173 break;
174 }
175 head = node.parent.as_ref();
176 }
177 head
178 }
179
180 pub fn messages(&self) -> Arc<[Arc<ThreadMessage>]> {
182 if let Some(path) = &*self.path.borrow() {
183 return path.clone();
184 }
185 let path: Arc<[Arc<ThreadMessage>]> = self.path_to(self.head.as_ref()).into();
186 *self.path.borrow_mut() = Some(path.clone());
187 path
188 }
189
190 pub fn messages_to(
192 &self,
193 head_id: &MessageId,
194 ) -> Result<Vec<Arc<ThreadMessage>>, RepositoryError> {
195 if !self.nodes.contains_key(head_id) {
196 return Err(RepositoryError::MessageNotFound(head_id.clone()));
197 }
198 Ok(self.path_to(Some(head_id)))
199 }
200
201 fn path_to(&self, head: Option<&MessageId>) -> Vec<Arc<ThreadMessage>> {
202 let mut path = Vec::with_capacity(
203 head.and_then(|id| self.nodes.get(id))
204 .map_or(0, |n| n.level + 1),
205 );
206 let mut current = head;
207 while let Some(node) = current.and_then(|id| self.nodes.get(id)) {
208 path.push(node.message.clone());
209 current = node.parent.as_ref();
210 }
211 path.reverse();
212 path
213 }
214
215 pub fn get(&self, id: &MessageId) -> Result<MessageEntry, RepositoryError> {
217 let node = self.node(id)?;
218 Ok(MessageEntry {
219 message: node.message.clone(),
220 parent_id: node.parent.clone(),
221 index: node.level,
222 })
223 }
224
225 pub fn has_children(&self, id: &MessageId) -> bool {
227 self.nodes
228 .get(id)
229 .is_some_and(|node| !node.children.ids.is_empty())
230 }
231
232 pub fn branches(&self, id: &MessageId) -> Result<&[MessageId], RepositoryError> {
234 let parent = self.node(id)?.parent.clone();
235 Ok(&self.children_of(parent.as_ref()).ids)
236 }
237
238 pub fn branch_position(&self, id: &MessageId) -> Result<(usize, usize), RepositoryError> {
241 let node = self.node(id)?;
242 let ids = &self.children_of(node.parent.as_ref()).ids;
243 let position = ids.iter().position(|b| b == id).map_or(1, |p| p + 1);
244 Ok((position, ids.len().max(1)))
245 }
246
247 fn node(&self, id: &MessageId) -> Result<&Node, RepositoryError> {
248 self.nodes
249 .get(id)
250 .ok_or_else(|| RepositoryError::MessageNotFound(id.clone()))
251 }
252 fn children_of(&self, parent: Option<&MessageId>) -> &Children {
253 match parent {
254 Some(id) => &self.nodes[id].children,
255 None => &self.root,
256 }
257 }
258 fn children_of_mut(&mut self, parent: Option<&MessageId>) -> &mut Children {
259 match parent {
260 Some(id) => &mut self.nodes.get_mut(id).expect("parent is stored").children,
261 None => &mut self.root,
262 }
263 }
264 fn dirty(&mut self) {
265 *self.path.get_mut() = None;
266 self.shape_version = self.shape_version.wrapping_add(1);
267 self.reset_changes();
268 }
269
270 fn reset_changes(&mut self) {
271 self.change_version = self.change_version.wrapping_add(1);
272 self.change_base = self.change_version;
273 self.changes.clear();
274 }
275
276 fn record_change(&mut self, index: usize) {
277 self.change_version = self.change_version.wrapping_add(1);
278 if self.changes.len() == CHANGE_HISTORY {
279 self.change_base = self.changes.pop_front().expect("journal is full").0;
280 }
281 self.changes.push_back((self.change_version, index));
282 }
283
284 pub fn message_changes_since(&self, version: Option<u64>) -> MessageChanges {
289 let indices = version.and_then(|version| {
290 let behind = self.change_version.wrapping_sub(version);
292 if behind > self.change_version.wrapping_sub(self.change_base) {
293 return None;
294 }
295 let mut indices: Vec<_> = self
296 .changes
297 .iter()
298 .rev()
299 .take_while(|(changed, _)| self.change_version.wrapping_sub(*changed) < behind)
300 .map(|(_, index)| *index)
301 .collect();
302 indices.sort_unstable();
303 indices.dedup();
304 Some(indices)
305 });
306 MessageChanges {
307 version: self.change_version,
308 indices,
309 }
310 }
311
312 pub fn shape_version(&self) -> u64 {
316 self.shape_version
317 }
318
319 fn replace_in_path(&mut self, level: usize, id: &MessageId, message: Arc<ThreadMessage>) {
324 let Some(path) = self.path.get_mut().as_mut() else {
325 self.reset_changes();
328 return;
329 };
330 if path.get(level).map(|m| m.id()) == Some(id) {
331 if Arc::ptr_eq(&path[level], &message) {
332 return;
333 }
334 if let Some(slots) = Arc::get_mut(path) {
335 slots[level] = message;
337 } else {
338 let mut patched: Vec<Arc<ThreadMessage>> = path.to_vec();
339 patched[level] = message;
340 *path = patched.into();
341 }
342 self.record_change(level);
343 }
344 }
346
347 fn find_head(&self, start: Option<&MessageId>) -> Option<MessageId> {
349 let mut head = start.cloned();
350 while let Some(next) = self.children_of(head.as_ref()).next.clone() {
351 head = Some(next);
352 }
353 head
354 }
355
356 fn head_path_selected(&self) -> bool {
359 let mut current = self.head.clone();
360 while let Some(id) = current {
361 let parent = self.nodes[&id].parent.clone();
362 if self.children_of(parent.as_ref()).next.as_ref() != Some(&id) {
363 return false;
364 }
365 current = parent;
366 }
367 true
368 }
369
370 fn select_path_to(&mut self, id: &MessageId) {
372 let mut current = Some(id.clone());
373 while let Some(id) = current {
374 let parent = self.nodes[&id].parent.clone();
375 self.children_of_mut(parent.as_ref()).next = Some(id);
376 current = parent;
377 }
378 }
379
380 fn update_levels(&mut self, id: &MessageId, level: usize) {
381 let mut pending = vec![(id.clone(), level)];
382 while let Some((id, level)) = pending.pop() {
383 let node = self.nodes.get_mut(&id).expect("descendant is stored");
384 node.level = level;
385 pending.extend(
386 node.children
387 .ids
388 .iter()
389 .map(|child| (child.clone(), level + 1)),
390 );
391 }
392 }
393
394 fn is_ancestor_or_self(&self, candidate: &MessageId, of: Option<&MessageId>) -> bool {
395 let mut current = of;
396 while let Some(id) = current {
397 if id == candidate {
398 return true;
399 }
400 current = self.nodes.get(id).and_then(|n| n.parent.as_ref());
401 }
402 false
403 }
404
405 fn cut(&mut self, child: &MessageId) {
407 let parent = self.nodes[child].parent.clone();
408 let siblings = self.children_of_mut(parent.as_ref());
409 siblings.ids.retain(|id| id != child);
410 if siblings.next.as_ref() == Some(child) {
411 siblings.next = siblings.ids.last().cloned();
412 }
413 }
414
415 fn link(&mut self, parent: Option<&MessageId>, child: &MessageId) {
417 let head_was_parent = self.head.as_ref() == parent && parent.is_some();
418 self.children_of_mut(parent).ids.push(child.clone());
419 self.nodes.get_mut(child).expect("child is stored").parent = parent.cloned();
420 if self.find_head(Some(child)) == self.head && self.head.is_some() {
421 self.select_path_to(child);
422 } else if self.children_of(parent).next.is_none() {
423 self.children_of_mut(parent).next = Some(child.clone());
424 if head_was_parent {
425 self.head = self.find_head(Some(child));
426 }
427 }
428 let level = parent.map_or(0, |p| self.nodes[p].level + 1);
429 self.update_levels(child, level);
430 }
431
432 pub fn add_or_update(
435 &mut self,
436 parent_id: Option<&MessageId>,
437 message: impl Into<Arc<ThreadMessage>>,
438 ) -> Result<(), RepositoryError> {
439 let message = message.into();
440 let id = message.id().clone();
441 if let Some(parent) = parent_id {
442 if !self.nodes.contains_key(parent) {
443 return Err(RepositoryError::ParentNotFound(parent.clone()));
444 }
445 }
446 if let Some(existing) = self.nodes.get(&id) {
447 if existing.parent.as_ref() != parent_id {
448 if self.is_ancestor_or_self(&id, parent_id) {
449 return Err(RepositoryError::Cycle(id));
450 }
451 self.cut(&id);
452 self.link(parent_id, &id);
453 self.dirty();
455 }
456 let node = self.nodes.get_mut(&id).expect("checked above");
457 let level = node.level;
458 node.message = message.clone();
459 self.replace_in_path(level, &id, message);
460 return Ok(());
461 }
462 let level = parent_id.map_or(0, |p| self.nodes[p].level + 1);
463 self.nodes.insert(
464 id.clone(),
465 Node {
466 message,
467 parent: parent_id.cloned(),
468 children: Children::default(),
469 level,
470 },
471 );
472 let head_was_parent = self.head.as_ref() == parent_id;
473 self.link(parent_id, &id);
474 if head_was_parent {
475 self.head = Some(id.clone());
479 self.children_of_mut(parent_id).next = Some(id.clone());
480 }
481 debug_assert!(self.head_path_selected());
482 self.dirty();
483 Ok(())
484 }
485
486 pub fn delete(
489 &mut self,
490 id: &MessageId,
491 replacement: Option<Option<&MessageId>>,
492 ) -> Result<(), RepositoryError> {
493 let parent = self.node(id)?.parent.clone();
494 let replacement: Option<MessageId> = match replacement {
495 None => parent,
496 Some(None) => None,
497 Some(Some(other)) => {
498 self.node(other)?;
499 Some(other.clone())
500 }
501 };
502 if self.is_ancestor_or_self(id, replacement.as_ref()) {
503 return Err(RepositoryError::InvalidReplacement(id.clone()));
504 }
505 for child in self.nodes[id].children.ids.clone() {
506 self.cut(&child);
507 self.link(replacement.as_ref(), &child);
508 }
509 self.cut(id);
510 self.nodes.remove(id);
511 if self.head.as_ref() == Some(id) {
512 self.head = self.find_head(replacement.as_ref());
513 if let Some(head) = self.head.clone() {
514 self.select_path_to(&head);
516 }
517 }
518 self.dirty();
519 Ok(())
520 }
521
522 pub fn switch_to_branch(&mut self, id: &MessageId) -> Result<(), RepositoryError> {
524 self.node(id)?;
525 let previous = self.head.clone();
526 self.select_path_to(id);
527 self.head = self.find_head(Some(id));
528 self.evict_off_branch_optimistic(previous);
529 self.dirty();
530 Ok(())
531 }
532
533 pub fn reset_head(&mut self, id: Option<&MessageId>) -> Result<(), RepositoryError> {
535 self.set_head(id, true)
536 }
537
538 fn set_head(&mut self, id: Option<&MessageId>, prune: bool) -> Result<(), RepositoryError> {
539 let Some(id) = id else {
540 self.clear();
541 return Ok(());
542 };
543 self.node(id)?;
544 let previous = self.head.clone();
545 if prune {
546 let mut pending =
547 std::mem::take(&mut self.nodes.get_mut(id).expect("checked").children).ids;
548 while let Some(child) = pending.pop() {
549 if let Some(node) = self.nodes.remove(&child) {
550 pending.extend(node.children.ids);
551 }
552 }
553 }
554 self.head = Some(id.clone());
555 self.select_path_to(id);
556 self.evict_off_branch_optimistic(previous);
557 self.dirty();
558 Ok(())
559 }
560
561 fn evict_off_branch_optimistic(&mut self, previous: Option<MessageId>) {
563 let on_head: HashSet<MessageId> = self
564 .path_to(self.head.as_ref())
565 .iter()
566 .map(|m| m.id().clone())
567 .collect();
568 let mut stale = Vec::new();
569 let mut current = previous;
570 while let Some(id) = current {
571 if on_head.contains(&id) {
572 break;
573 }
574 let Some(node) = self.nodes.get(&id) else {
575 break;
576 };
577 if node.message.is_optimistic() {
578 stale.push(id.clone());
579 }
580 current = node.parent.clone();
581 }
582 for id in stale {
583 if self.nodes.contains_key(&id) {
584 let _ = self.delete(&id, None);
585 }
586 }
587 }
588
589 pub fn clear(&mut self) {
591 self.nodes.clear();
592 self.root = Children::default();
593 self.head = None;
594 self.dirty();
595 }
596
597 pub fn export(&self) -> ExportedRepository {
600 let mut messages = Vec::with_capacity(self.nodes.len());
601 let mut pending: Vec<MessageId> = self.root.ids.iter().rev().cloned().collect();
602 while let Some(id) = pending.pop() {
603 let Some(node) = self.nodes.get(&id) else {
604 continue;
605 };
606 pending.extend(node.children.ids.iter().rev().cloned());
607 if node.message.is_optimistic() {
608 continue;
609 }
610 let mut parent = node.parent.as_ref();
611 while let Some(p) = parent
612 .and_then(|p| self.nodes.get(p))
613 .filter(|n| n.message.is_optimistic())
614 {
615 parent = p.parent.as_ref();
616 }
617 messages.push(ExportedMessage {
618 message: node.message.clone(),
619 parent_id: parent.cloned(),
620 run_config: None,
621 });
622 }
623 ExportedRepository {
624 head_id: Some(self.exported_head_id()),
625 messages,
626 }
627 }
628
629 fn exported_head_id(&self) -> Option<MessageId> {
630 let mut head = self.head.clone();
631 while let Some(node) = head
632 .as_ref()
633 .and_then(|id| self.nodes.get(id))
634 .filter(|n| n.message.is_optimistic())
635 {
636 head = node.parent.clone();
637 }
638 if head == self.head {
639 return head;
640 }
641 while let Some(next) = self.exported_child(head.as_ref()) {
642 head = Some(next);
643 }
644 head
645 }
646
647 fn exported_child(&self, parent: Option<&MessageId>) -> Option<MessageId> {
650 let children = self.children_of(parent);
651 if let Some(selected) = &children.next {
652 if !self.nodes[selected].message.is_optimistic() {
653 return Some(selected.clone());
654 }
655 if let Some(found) = self.exported_child(Some(selected)) {
656 return Some(found);
657 }
658 }
659 let selected_index = children
660 .next
661 .as_ref()
662 .and_then(|s| children.ids.iter().position(|id| id == s))
663 .unwrap_or(children.ids.len());
664 let before = children.ids[..selected_index].iter().rev();
665 let after = children
666 .ids
667 .get(selected_index + 1..)
668 .unwrap_or_default()
669 .iter()
670 .rev();
671 for id in before.chain(after) {
672 let Some(child) = self.nodes.get(id) else {
673 continue;
674 };
675 if !child.message.is_optimistic() {
676 return Some(id.clone());
677 }
678 if let Some(found) = self.exported_child(Some(id)) {
679 return Some(found);
680 }
681 }
682 None
683 }
684
685 pub fn import(&mut self, exported: ExportedRepository) -> Result<(), RepositoryError> {
688 let ordered = with_parents_first(exported.messages, |id| self.nodes.contains_key(id));
689 let last = ordered.last().map(|item| item.message.id().clone());
690 for item in ordered {
691 self.add_or_update(item.parent_id.as_ref(), item.message)?;
692 }
693 let head = match exported.head_id {
694 Some(head) => head,
695 None => last,
696 };
697 self.set_head(head.as_ref(), false)
698 }
699}
700
701pub fn with_parents_first(
703 messages: Vec<ExportedMessage>,
704 is_stored: impl Fn(&MessageId) -> bool,
705) -> Vec<ExportedMessage> {
706 let listed: HashSet<MessageId> = messages.iter().map(|m| m.message.id().clone()).collect();
707 let mut added: HashSet<MessageId> = HashSet::new();
708 let mut waiting: HashMap<MessageId, Vec<ExportedMessage>> = HashMap::new();
709 let mut waiting_order: Vec<MessageId> = Vec::new();
710 let mut ordered: Vec<ExportedMessage> = Vec::with_capacity(messages.len());
711 for item in messages {
712 if let Some(parent) = &item.parent_id {
713 if listed.contains(parent) && !added.contains(parent) && !is_stored(parent) {
714 if !waiting.contains_key(parent) {
715 waiting_order.push(parent.clone());
716 }
717 waiting.entry(parent.clone()).or_default().push(item);
718 continue;
719 }
720 }
721 let mut index = ordered.len();
722 ordered.push(item);
723 while index < ordered.len() {
724 let id = ordered[index].message.id().clone();
725 index += 1;
726 if !added.insert(id.clone()) {
727 continue;
728 }
729 if let Some(children) = waiting.remove(&id) {
730 ordered.extend(children);
731 }
732 }
733 }
734 for parent in waiting_order {
735 if let Some(children) = waiting.remove(&parent) {
736 ordered.extend(children);
737 }
738 }
739 ordered
740}
741
742#[cfg(test)]
743mod tests {
744 use super::*;
745 use crate::types::{MessageMetadata, TextPart, Timestamp, UserMessage, UserMessagePart};
746
747 fn msg(id: &str) -> ThreadMessage {
748 ThreadMessage::User(UserMessage {
749 id: id.into(),
750 created_at: Timestamp(0),
751 content: vec![UserMessagePart::Text(TextPart::new(id))],
752 attachments: vec![],
753 metadata: MessageMetadata::default(),
754 })
755 }
756 fn optimistic(id: &str) -> ThreadMessage {
757 let mut message = msg(id);
758 if let ThreadMessage::User(m) = &mut message {
759 m.metadata.is_optimistic = true;
760 }
761 message
762 }
763 fn id(s: &str) -> MessageId {
764 s.into()
765 }
766 fn ids(repo: &MessageRepository) -> Vec<String> {
767 repo.messages().iter().map(|m| m.id().0.clone()).collect()
768 }
769 fn chain(repo: &mut MessageRepository, items: &[(&str, Option<&str>)]) {
770 for (child, parent) in items {
771 repo.add_or_update(parent.map(id).as_ref(), msg(child))
772 .unwrap();
773 }
774 }
775
776 #[test]
777 fn change_cursors_cover_multiple_updates_without_consuming_other_readers() {
778 let mut repo = MessageRepository::new();
779 chain(
780 &mut repo,
781 &[("a", None), ("b", Some("a")), ("c", Some("b"))],
782 );
783 let before = repo.messages();
784 let first = repo.message_changes_since(None);
785 assert_eq!(first.indices, None);
786 assert_eq!(
787 repo.message_changes_since(Some(first.version)).indices,
788 Some(vec![])
789 );
790
791 repo.add_or_update(Some(&id("b")), msg("c")).unwrap();
792 let fast = repo.message_changes_since(Some(first.version));
793 assert_eq!(fast.indices, Some(vec![2]));
794 repo.add_or_update(None, msg("a")).unwrap();
795 repo.add_or_update(Some(&id("b")), msg("c")).unwrap();
796 assert_eq!(
797 repo.message_changes_since(Some(first.version)).indices,
798 Some(vec![0, 2])
799 );
800 assert_eq!(
801 repo.message_changes_since(Some(fast.version)).indices,
802 Some(vec![0, 2])
803 );
804 assert!(!Arc::ptr_eq(&before[0], &repo.messages()[0]));
805 assert!(Arc::ptr_eq(&before[1], &repo.messages()[1]));
806 }
807
808 #[test]
809 fn streaming_updates_keep_retained_snapshots_immutable() {
810 let mut repo = MessageRepository::new();
811 chain(
812 &mut repo,
813 &[("a", None), ("b", Some("a")), ("c", Some("b"))],
814 );
815 let _ = repo.messages();
816 for _ in 0..5 {
817 repo.add_or_update(Some(&id("b")), msg("c")).unwrap();
818 }
819 let snap = repo.messages();
821 let before = snap[2].clone();
822 repo.add_or_update(Some(&id("b")), msg("c")).unwrap();
823 assert!(Arc::ptr_eq(&snap[2], &before));
824 assert!(!Arc::ptr_eq(&snap[2], &repo.messages()[2]));
825 }
826
827 #[test]
828 fn change_cursors_ignore_unselected_content_but_reset_for_structural_changes() {
829 let mut repo = MessageRepository::new();
830 chain(
831 &mut repo,
832 &[("a", None), ("b", Some("a")), ("c", Some("a"))],
833 );
834 let _ = repo.messages();
835 let before = repo.message_changes_since(None).version;
836 repo.add_or_update(Some(&id("a")), msg("c")).unwrap();
837 assert_eq!(
838 repo.message_changes_since(Some(before)).indices,
839 Some(vec![])
840 );
841
842 repo.switch_to_branch(&id("c")).unwrap();
843 assert_eq!(repo.message_changes_since(Some(before)).indices, None);
844 let _ = repo.messages();
845 let switched = repo.message_changes_since(None).version;
846 repo.add_or_update(Some(&id("a")), msg("c")).unwrap();
847 assert_eq!(
848 repo.message_changes_since(Some(switched)).indices,
849 Some(vec![1])
850 );
851 repo.delete(&id("c"), None).unwrap();
852 assert_eq!(repo.message_changes_since(Some(switched)).indices, None);
853 let deleted = repo.message_changes_since(None).version;
854 repo.clear();
855 assert_eq!(repo.message_changes_since(Some(deleted)).indices, None);
856 }
857
858 #[test]
859 fn change_cursors_fall_back_when_the_bounded_history_is_exceeded() {
860 let mut repo = MessageRepository::new();
861 chain(&mut repo, &[("a", None)]);
862 let _ = repo.messages();
863 let before = repo.message_changes_since(None).version;
864 for _ in 0..CHANGE_HISTORY {
865 repo.add_or_update(None, msg("a")).unwrap();
866 }
867 assert_eq!(
868 repo.message_changes_since(Some(before)).indices,
869 Some(vec![0])
870 );
871 let recent = repo.message_changes_since(None).version;
872 repo.add_or_update(None, msg("a")).unwrap();
873 assert_eq!(repo.message_changes_since(Some(before)).indices, None);
874 assert_eq!(
875 repo.message_changes_since(Some(recent)).indices,
876 Some(vec![0])
877 );
878 assert_eq!(repo.changes.len(), CHANGE_HISTORY);
879 assert_eq!(repo.message_changes_since(Some(recent + 10)).indices, None);
880 }
881
882 #[test]
883 fn change_cursors_cover_rollover_and_updates_before_a_path_is_materialized() {
884 let mut repo = MessageRepository::new();
885 chain(&mut repo, &[("a", None)]);
886 let before_read = repo.message_changes_since(None).version;
887 repo.add_or_update(None, msg("a")).unwrap();
888 assert_eq!(repo.message_changes_since(Some(before_read)).indices, None);
889 let _ = repo.messages();
890 repo.change_version = u64::MAX - 1;
891 repo.change_base = u64::MAX - 1;
892 repo.changes.clear();
893 repo.add_or_update(None, msg("a")).unwrap();
894 repo.add_or_update(None, msg("a")).unwrap();
895 let rolled = repo.message_changes_since(Some(u64::MAX - 1));
896 assert_eq!(rolled.version, 0);
897 assert_eq!(rolled.indices, Some(vec![0]));
898 assert_eq!(repo.message_changes_since(Some(0)).indices, Some(vec![]));
899 repo.clear();
900 assert_eq!(repo.message_changes_since(Some(0)).indices, None);
901 }
902
903 #[test]
904 fn appending_moves_the_head_along_the_selected_path() {
905 let mut repo = MessageRepository::new();
906 chain(
907 &mut repo,
908 &[("a", None), ("b", Some("a")), ("c", Some("b"))],
909 );
910 assert_eq!(ids(&repo), ["a", "b", "c"]);
911 assert_eq!(repo.get(&id("c")).unwrap().index, 2);
912 }
913
914 #[test]
915 fn a_sibling_is_a_new_branch_and_switching_restores_descendants() {
916 let mut repo = MessageRepository::new();
917 chain(
918 &mut repo,
919 &[("a", None), ("b", Some("a")), ("c", Some("b"))],
920 );
921 repo.add_or_update(Some(&id("a")), msg("b2")).unwrap();
922 assert_eq!(ids(&repo), ["a", "b", "c"]);
924 assert_eq!(repo.branches(&id("b")).unwrap(), [id("b"), id("b2")]);
925 repo.switch_to_branch(&id("b2")).unwrap();
926 assert_eq!(ids(&repo), ["a", "b2"]);
927 repo.switch_to_branch(&id("b")).unwrap();
928 assert_eq!(
929 ids(&repo),
930 ["a", "b", "c"],
931 "returning restores the last selected descendant"
932 );
933 }
934
935 fn assert_path_is_fresh(repo: &MessageRepository) {
937 let cached = ids(repo);
938 let fresh: Vec<String> = match repo.head_id() {
939 Some(head) => repo
940 .messages_to(head)
941 .unwrap()
942 .iter()
943 .map(|m| m.id().0.clone())
944 .collect(),
945 None => Vec::new(),
946 };
947 assert_eq!(cached, fresh);
948 }
949
950 #[test]
951 fn the_cached_path_stays_correct_through_updates_and_moves() {
952 let mut repo = MessageRepository::new();
953 chain(
954 &mut repo,
955 &[
956 ("a", None),
957 ("b", Some("a")),
958 ("c", Some("b")),
959 ("d", Some("a")),
960 ],
961 );
962 assert_eq!(ids(&repo), ["a", "b", "c"], "primes the cache");
963 let mut edited = msg("b");
965 if let ThreadMessage::User(m) = &mut edited {
966 m.content = vec![UserMessagePart::Text(TextPart::new("edited"))];
967 }
968 repo.add_or_update(Some(&id("a")), edited).unwrap();
969 assert_eq!(repo.messages()[1].text(), "edited");
970 assert_path_is_fresh(&repo);
971 repo.add_or_update(Some(&id("a")), msg("d")).unwrap();
973 assert_eq!(ids(&repo), ["a", "b", "c"]);
974 assert_path_is_fresh(&repo);
975 repo.add_or_update(Some(&id("d")), msg("c")).unwrap();
978 assert_path_is_fresh(&repo);
979 repo.add_or_update(Some(&id("a")), msg("c")).unwrap();
980 assert_path_is_fresh(&repo);
981 }
982
983 #[test]
984 fn update_keeps_position_and_replaces_content() {
985 let mut repo = MessageRepository::new();
986 chain(&mut repo, &[("a", None), ("b", Some("a"))]);
987 let mut updated = msg("b");
988 if let ThreadMessage::User(m) = &mut updated {
989 m.content = vec![UserMessagePart::Text(TextPart::new("edited"))];
990 }
991 repo.add_or_update(Some(&id("a")), updated).unwrap();
992 assert_eq!(repo.messages()[1].text(), "edited");
993 }
994
995 #[test]
996 fn keeps_the_old_content_when_an_update_is_rejected_as_a_cycle() {
997 let mut repo = MessageRepository::new();
998 chain(&mut repo, &[("a", None), ("b", Some("a"))]);
999 let mut moved = msg("a");
1000 if let ThreadMessage::User(m) = &mut moved {
1001 m.content = vec![UserMessagePart::Text(TextPart::new("new"))];
1002 }
1003 assert_eq!(
1004 repo.add_or_update(Some(&id("b")), moved),
1005 Err(RepositoryError::Cycle(id("a")))
1006 );
1007 assert_eq!(repo.messages()[0].text(), "a");
1008 assert_eq!(ids(&repo), ["a", "b"]);
1009 }
1010
1011 #[test]
1012 fn moves_no_child_when_the_replacement_is_a_descendant_of_the_deleted_message() {
1013 let mut repo = MessageRepository::new();
1014 chain(
1015 &mut repo,
1016 &[("a", None), ("b", Some("a")), ("c", Some("b"))],
1017 );
1018 assert_eq!(
1019 repo.delete(&id("a"), Some(Some(&id("c")))),
1020 Err(RepositoryError::InvalidReplacement(id("a")))
1021 );
1022 assert_eq!(ids(&repo), ["a", "b", "c"]);
1023 assert_eq!(
1024 repo.delete(&id("b"), Some(Some(&id("b")))),
1025 Err(RepositoryError::InvalidReplacement(id("b")))
1026 );
1027 }
1028
1029 #[test]
1030 fn deleting_reattaches_children_to_the_parent() {
1031 let mut repo = MessageRepository::new();
1032 chain(
1033 &mut repo,
1034 &[("a", None), ("b", Some("a")), ("c", Some("b"))],
1035 );
1036 repo.delete(&id("b"), None).unwrap();
1037 assert_eq!(ids(&repo), ["a", "c"]);
1038 assert_eq!(repo.get(&id("c")).unwrap().index, 1);
1039 repo.delete(&id("c"), None).unwrap();
1040 assert_eq!(
1041 ids(&repo),
1042 ["a"],
1043 "the head falls back to the replacement's selected leaf"
1044 );
1045 }
1046
1047 #[test]
1048 fn deleting_the_selected_sibling_selects_the_last_remaining_one() {
1049 let mut repo = MessageRepository::new();
1050 chain(
1051 &mut repo,
1052 &[
1053 ("a", None),
1054 ("b1", Some("a")),
1055 ("b2", Some("a")),
1056 ("b3", Some("a")),
1057 ],
1058 );
1059 repo.switch_to_branch(&id("b2")).unwrap();
1060 repo.delete(&id("b2"), None).unwrap();
1061 assert_eq!(ids(&repo), ["a", "b3"]);
1062 }
1063
1064 #[test]
1065 fn reset_head_prunes_descendants() {
1066 let mut repo = MessageRepository::new();
1067 chain(
1068 &mut repo,
1069 &[("a", None), ("b", Some("a")), ("c", Some("b"))],
1070 );
1071 repo.reset_head(Some(&id("a"))).unwrap();
1072 assert_eq!(ids(&repo), ["a"]);
1073 assert!(!repo.contains(&id("b")) && !repo.contains(&id("c")));
1074 repo.reset_head(None).unwrap();
1075 assert!(repo.is_empty() && repo.messages().is_empty());
1076 }
1077
1078 #[test]
1079 fn switching_away_evicts_optimistic_messages_on_the_left_branch() {
1080 let mut repo = MessageRepository::new();
1081 chain(&mut repo, &[("a", None)]);
1082 repo.add_or_update(Some(&id("a")), optimistic("tmp"))
1083 .unwrap();
1084 repo.add_or_update(Some(&id("a")), msg("server")).unwrap();
1085 assert_eq!(ids(&repo), ["a", "tmp"]);
1086 repo.switch_to_branch(&id("server")).unwrap();
1087 assert!(!repo.contains(&id("tmp")));
1088 assert_eq!(repo.branches(&id("server")).unwrap(), [id("server")]);
1089 }
1090
1091 #[test]
1092 fn imports_a_message_listed_before_its_parent() {
1093 let mut repo = MessageRepository::new();
1094 let exported = ExportedRepository {
1095 head_id: None,
1096 messages: vec![
1097 ExportedMessage {
1098 message: Arc::new(msg("b")),
1099 parent_id: Some(id("a")),
1100 run_config: None,
1101 },
1102 ExportedMessage {
1103 message: Arc::new(msg("a")),
1104 parent_id: None,
1105 run_config: None,
1106 },
1107 ],
1108 };
1109 repo.import(exported).unwrap();
1110 assert_eq!(
1111 ids(&repo),
1112 ["a", "b"],
1113 "head is the last message in parent-first order"
1114 );
1115 }
1116
1117 fn branchable(items: &[(&str, Option<&str>)], head: &str) -> ExportedRepository {
1118 ExportedRepository {
1119 head_id: Some(Some(id(head))),
1120 messages: items
1121 .iter()
1122 .map(|(m, p)| ExportedMessage {
1123 message: Arc::new(msg(m)),
1124 parent_id: p.map(id),
1125 run_config: None,
1126 })
1127 .collect(),
1128 }
1129 }
1130 fn exported_ids(repo: &MessageRepository) -> Vec<String> {
1131 repo.export()
1132 .messages
1133 .iter()
1134 .map(|m| m.message.id().0.clone())
1135 .collect()
1136 }
1137
1138 #[test]
1139 fn keeps_descendants_when_importing_with_an_inner_head() {
1140 let items = [
1141 ("u1", None),
1142 ("a1", Some("u1")),
1143 ("u2", Some("a1")),
1144 ("a2", Some("u2")),
1145 ("a1b", Some("u1")),
1146 ];
1147 for (head, visible) in [("a1", vec!["u1", "a1"]), ("u1", vec!["u1"])] {
1148 let mut repo = MessageRepository::new();
1149 repo.import(branchable(&items, head)).unwrap();
1150 assert_eq!(ids(&repo), visible);
1151 assert_eq!(repo.head_id(), Some(&id(head)));
1152 assert_eq!(repo.export().head_id, Some(Some(id(head))));
1153 assert_eq!(exported_ids(&repo), ["u1", "a1", "u2", "a2", "a1b"]);
1154 }
1155 }
1156
1157 #[test]
1158 fn appends_a_sibling_from_an_imported_head_with_children() {
1159 let mut repo = MessageRepository::new();
1160 repo.import(branchable(&[("u2", None), ("a2", Some("u2"))], "u2"))
1161 .unwrap();
1162 repo.add_or_update(Some(&id("u2")), msg("a2b")).unwrap();
1163 assert_eq!(repo.head_id(), Some(&id("a2b")));
1164 assert_eq!(ids(&repo), ["u2", "a2b"]);
1165 assert_eq!(repo.branches(&id("a2b")).unwrap(), [id("a2"), id("a2b")]);
1166 assert_eq!(exported_ids(&repo), ["u2", "a2", "a2b"]);
1167 repo.switch_to_branch(&id("u2")).unwrap();
1168 assert_eq!(repo.head_id(), Some(&id("a2b")));
1169 }
1170
1171 #[test]
1172 fn export_skips_optimistic_messages_and_round_trips() {
1173 let mut repo = MessageRepository::new();
1174 chain(&mut repo, &[("a", None), ("b", Some("a"))]);
1175 repo.add_or_update(Some(&id("b")), optimistic("o")).unwrap();
1176 assert_eq!(ids(&repo), ["a", "b", "o"]);
1177 let exported = repo.export();
1178 assert_eq!(exported.head_id, Some(Some(id("b"))));
1179 assert_eq!(
1180 exported
1181 .messages
1182 .iter()
1183 .map(|m| m.message.id().0.as_str())
1184 .collect::<Vec<_>>(),
1185 ["a", "b"]
1186 );
1187 let mut restored = MessageRepository::new();
1188 restored.import(exported).unwrap();
1189 assert_eq!(ids(&restored), ["a", "b"]);
1190 }
1191
1192 #[test]
1193 fn export_follows_a_persisted_sibling_when_the_selected_branch_is_optimistic() {
1194 let mut repo = MessageRepository::new();
1195 chain(&mut repo, &[("a", None), ("b1", Some("a"))]);
1196 repo.add_or_update(Some(&id("a")), optimistic("b2"))
1197 .unwrap();
1198 repo.switch_to_branch(&id("b2")).unwrap();
1199 assert_eq!(repo.export().head_id, Some(Some(id("b1"))));
1200 }
1201
1202 #[test]
1203 fn the_path_is_cached_until_the_next_change() {
1204 let mut repo = MessageRepository::new();
1205 chain(&mut repo, &[("a", None)]);
1206 let first = repo.messages();
1207 assert!(Arc::ptr_eq(&first, &repo.messages()));
1208 chain(&mut repo, &[("b", Some("a"))]);
1209 assert!(!Arc::ptr_eq(&first, &repo.messages()));
1210 }
1211
1212 #[test]
1213 fn errors_name_the_missing_message() {
1214 let mut repo = MessageRepository::new();
1215 assert_eq!(
1216 repo.add_or_update(Some(&id("x")), msg("a")),
1217 Err(RepositoryError::ParentNotFound(id("x")))
1218 );
1219 assert_eq!(
1220 repo.switch_to_branch(&id("x")),
1221 Err(RepositoryError::MessageNotFound(id("x")))
1222 );
1223 assert!(repo.is_empty());
1224 }
1225}