Skip to main content

ai_ui_core/
repository.rs

1//! Branching message store, ported from `message-repository.ts`.
2//!
3//! Messages form a forest: each has a parent (or none at the root) and an
4//! ordered list of children. Each parent remembers its selected child, and the
5//! head is the last message of the selected path. `messages()` returns that path
6//! from the root, cached until the next change. Messages are `Arc`s, so handing
7//! the path to a model adapter does not copy content.
8
9use crate::types::{MessageId, RunConfig, ThreadMessage};
10use std::cell::RefCell;
11use std::collections::{HashMap, HashSet, VecDeque};
12use std::fmt;
13use std::sync::Arc;
14
15/// A stored message and where it attaches, for export and import.
16#[derive(Clone, Debug, PartialEq)]
17pub struct ExportedMessage {
18    /// The message.
19    pub message: Arc<ThreadMessage>,
20    /// Its parent, or `None` for a root message.
21    pub parent_id: Option<MessageId>,
22    /// The run configuration it was sent with, if recorded.
23    pub run_config: Option<RunConfig>,
24}
25
26/// Serializable snapshot of a repository (`ExportedMessageRepository`).
27#[derive(Clone, Debug, Default, PartialEq)]
28pub struct ExportedRepository {
29    /// `None` imports with the last listed message as head.
30    pub head_id: Option<Option<MessageId>>,
31    /// Every message, parents before children.
32    pub messages: Vec<ExportedMessage>,
33}
34
35impl ExportedRepository {
36    /// A linear history: each message follows the previous one.
37    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/// An operation that would corrupt the tree. Upstream reports these as
60/// internal-bug exceptions; here the repository is left unchanged.
61#[derive(Clone, Debug, PartialEq, Eq)]
62#[non_exhaustive]
63pub enum RepositoryError {
64    /// No message has this id.
65    MessageNotFound(MessageId),
66    /// No message has the given parent id.
67    ParentNotFound(MessageId),
68    /// The update would make a message its own ancestor.
69    Cycle(MessageId),
70    /// The replacement is the deleted message or one of its descendants.
71    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/// A message with its position on its own branch.
89#[derive(Clone, Debug)]
90pub struct MessageEntry {
91    /// The message.
92    pub message: Arc<ThreadMessage>,
93    /// Its parent, or `None` for a root message.
94    pub parent_id: Option<MessageId>,
95    /// Depth from the root, which is also its index on any path through it.
96    pub index: usize,
97}
98
99/// Changes to the selected messages since a consumer's last read.
100///
101/// Each consumer keeps its own version; reading does not consume changes.
102/// Versions belong to one repository/runtime and must be discarded on rebind.
103#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct MessageChanges {
105    /// Pass this back on the next read, including after a full rebuild.
106    pub version: u64,
107    /// Sorted, unique indices whose contents changed. `None` requires a full
108    /// read: the shape changed, the consumer is new, or the bounded journal
109    /// no longer covers its version. `Some([])` means no message changed.
110    pub indices: Option<Vec<usize>>,
111}
112
113// A slow or temporarily hidden view falls back to a full read instead of
114// retaining an unbounded stream of updates (or old message contents).
115const CHANGE_HISTORY: usize = 64;
116
117#[derive(Default)]
118struct Children {
119    ids: Vec<MessageId>,
120    /// The selected child.
121    next: Option<MessageId>,
122}
123
124struct Node {
125    message: Arc<ThreadMessage>,
126    parent: Option<MessageId>,
127    children: Children,
128    level: usize,
129}
130
131/// A tree of messages with one selected path, root to head.
132#[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    /// Bumped whenever the tree's shape or the selected path changes.
139    shape_version: u64,
140    change_version: u64,
141    change_base: u64,
142    changes: VecDeque<(u64, usize)>,
143}
144
145impl MessageRepository {
146    /// An empty repository.
147    pub fn new() -> Self {
148        Self::default()
149    }
150
151    /// The number of messages in the tree, on every branch.
152    pub fn len(&self) -> usize {
153        self.nodes.len()
154    }
155    /// Whether the tree has no messages.
156    pub fn is_empty(&self) -> bool {
157        self.nodes.is_empty()
158    }
159    /// Whether a message with this id is in the tree.
160    pub fn contains(&self, id: &MessageId) -> bool {
161        self.nodes.contains_key(id)
162    }
163    /// The last message of the selected path.
164    pub fn head_id(&self) -> Option<&MessageId> {
165        self.head.as_ref()
166    }
167
168    /// The head after skipping optimistic messages.
169    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    /// The selected path from the root to the head.
181    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    /// The path from the root to `head_id`.
191    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    /// A message with its parent and depth.
216    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    /// Whether the message has replies.
226    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    /// The ids of the message and its siblings, in creation order.
233    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    /// `(position, count)` of the message among its siblings, 1-based, without
239    /// allocating (what a branch picker shows).
240    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    /// Selected-message changes since `version`, with bounded bookkeeping.
285    /// Call [`messages`](Self::messages) for a full read when `indices` is
286    /// `None`. A consumer must also refresh global runtime state (running,
287    /// capabilities), which is not part of this repository's journal.
288    pub fn message_changes_since(&self, version: Option<u64>) -> MessageChanges {
289        let indices = version.and_then(|version| {
290            // Wrapping distances preserve the cursor across u64 rollover.
291            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    /// Changes whenever messages are added, moved or removed, or the selected
313    /// path changes; not when a message is updated in place (streaming). While
314    /// it is unchanged, every message keeps its branch position.
315    pub fn shape_version(&self) -> u64 {
316        self.shape_version
317    }
318
319    /// A message changed in place: patch the cached path (copying pointers)
320    /// instead of rebuilding it with a lookup per ancestor. Streaming replaces
321    /// the head message on every update, so this keeps an update cheap on a
322    /// long thread.
323    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            // No materialized path to establish membership against. A full
326            // read remains necessary, even for a cursor acquired beforehand.
327            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                // No reader holds this path: patch it in place, O(1).
336                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        // Otherwise the message is on another branch: the selected path is unchanged.
345    }
346
347    /// Follow selected children from `start` (`None` for the root) to a leaf.
348    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    /// The invariant appends rely on: every link from the root to the head is
357    /// selected, so following selected children from the root reaches the head.
358    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    /// Select every ancestor link down to `id`.
371    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    /// Detach `child` from its parent, selecting the last remaining sibling.
406    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    /// Attach `child` (already stored) as the last child of `parent`.
416    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    /// Insert a new message after `parent_id`, or replace the stored message
433    /// with the same id, moving it under `parent_id` if that changed.
434    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                // The tree changed shape, so the selected path may too.
454                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            // Every link above the head is selected (see `head_path_selected`),
476            // so appending to the head selects one more link. Walking to the
477            // root here made importing or growing a long thread quadratic.
478            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    /// Remove a message. Its children move to `replacement`: by default its
487    /// parent, `Some(None)` for the root, or `Some(Some(id))` for another message.
488    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                // The replacement may sit on an unselected branch.
515                self.select_path_to(&head);
516            }
517        }
518        self.dirty();
519        Ok(())
520    }
521
522    /// Make the branch through `id` current, selecting its last-used descendants.
523    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    /// Make `id` the head and delete everything below it; `None` clears all.
534    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    /// Drop optimistic messages on the branch the head just left.
562    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    /// Remove every message.
590    pub fn clear(&mut self) {
591        self.nodes.clear();
592        self.root = Children::default();
593        self.head = None;
594        self.dirty();
595    }
596
597    /// Persisted messages in parent-first order; optimistic ones are skipped
598    /// and their children reattached to the nearest persisted ancestor.
599    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    /// The persisted child to follow below `parent`: the selected one (or its
648    /// first persisted descendant), else the nearest persisted sibling.
649    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    /// Add or update every listed message (parents first), then select the
686    /// listed head, or the last listed message.
687    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
701/// Reorder so each listed parent precedes its children (`withParentsFirst`).
702pub 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        // A retained snapshot must not be mutated by later updates.
820        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        // Adding off the head does not move it.
923        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    /// The cached selected path must always equal a fresh walk from the head.
936    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        // Updating a message on the path patches it in place.
964        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        // Updating a message on another branch leaves the path alone.
972        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        // Moving a message under a different parent changes the tree: the path
976        // must be rebuilt, not patched.
977        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}