Skip to main content

nmp_threading/grouper/
lifecycle.rs

1//! Owns the grouper's mutation entry points — insert, remove, and replace —
2//! along with the orphan-replay and supersession bookkeeping that keeps
3//! those transitions consistent. Placement of an accepted event into the
4//! block layout itself lives in [`super::placement`].
5
6use nmp_core::substrate::{EventId, KernelEvent};
7
8use crate::block::TimelineBlock;
9use crate::resolver::ParentResolver;
10
11use super::{GroupDelta, Grouper};
12
13impl<R: ParentResolver> Grouper<R> {
14    /// Process an inserted event. Returns the strongest single delta
15    /// (wrappers re-snapshot anyway).
16    #[must_use]
17    pub fn on_insert(&mut self, event: &KernelEvent) -> Option<GroupDelta> {
18        if self.by_id.contains_key(&event.id) {
19            return None;
20        }
21        // Suppress events that have been preempted by a superseder. We still
22        // record the payload in `by_id` so reply chains and ancestor walks can
23        // resolve this id as a parent — only the standalone block placement
24        // is skipped.
25        if self
26            .superseded_by
27            .get(&event.id)
28            .is_some_and(|set| !set.is_empty())
29        {
30            self.by_id.insert(event.id.clone(), event.clone());
31            return None;
32        }
33        self.by_id.insert(event.id.clone(), event.clone());
34
35        // Supersession: if this event supersedes a target (e.g., a repost
36        // bumping the original note), evict the target's standalone block so
37        // the superseder takes its place in the layout. Reply chains that
38        // contain the target are left untouched — the target is still useful
39        // as parent context.
40        if let Some(target) = self.resolver.supersedes(event) {
41            self.superseded_by
42                .entry(target.clone())
43                .or_default()
44                .insert(event.id.clone());
45            self.unplace_standalone(&target);
46        }
47
48        // Drain any orphans waiting on this event's id; they will replay
49        // after we've placed this event itself.
50        let waiting = self.orphans.remove(&event.id).unwrap_or_default();
51
52        let mut delta = self.place_event(event);
53
54        // Replay waiting children. Each replay may release further orphans.
55        let mut replay_queue: Vec<EventId> = waiting.into_iter().collect();
56        while let Some(child_id) = replay_queue.pop() {
57            if self.seen.contains(&child_id) {
58                continue;
59            }
60            let Some(child) = self.by_id.get(&child_id).cloned() else {
61                continue;
62            };
63            self.place_event(&child);
64            if let Some(more) = self.orphans.remove(&child_id) {
65                replay_queue.extend(more);
66            }
67        }
68
69        self.sort_blocks_newest_first();
70        self.collapse_adjacent();
71        self.sort_blocks_newest_first();
72        if matches!(
73            delta,
74            Some(GroupDelta::BlockInserted(_) | GroupDelta::BlockReplaced(_))
75        ) {
76            delta = delta.and_then(|d| self.reindex_delta(d, &event.id));
77        }
78        delta
79    }
80
81    /// Process a removed event. Returns at most one delta.
82    #[must_use]
83    pub fn on_remove(&mut self, id: &EventId) -> Option<GroupDelta> {
84        self.by_id.remove(id);
85        self.pending_ancestor_ids.remove(id);
86        self.orphaned.remove(id);
87        self.orphans.retain(|_, set| {
88            set.remove(id);
89            !set.is_empty()
90        });
91
92        // Supersession bookkeeping.
93        //   - if `id` was itself a target, drop its row (the surviving
94        //     superseders no longer have a target to preempt)
95        //   - if `id` was a superseder, scrub it from every set; collect
96        //     targets whose set is now empty so we can restore their blocks
97        self.superseded_by.remove(id);
98        let mut restore_candidates: Vec<EventId> = Vec::new();
99        self.superseded_by.retain(|target, set| {
100            set.remove(id);
101            if set.is_empty() {
102                restore_candidates.push(target.clone());
103                false
104            } else {
105                true
106            }
107        });
108
109        let block_delta = self.remove_id_from_blocks(id);
110
111        // Restore unsuperseded targets that still have payloads on hand.
112        let mut restored_any = false;
113        for target in restore_candidates {
114            if self.seen.contains(&target) {
115                continue;
116            }
117            let Some(event) = self.by_id.get(&target).cloned() else {
118                continue;
119            };
120            let _ = self.place_event(&event);
121            restored_any = true;
122        }
123
124        if block_delta.is_some() || restored_any {
125            self.collapse_adjacent();
126            self.sort_blocks_newest_first();
127        }
128
129        block_delta
130    }
131
132    fn remove_id_from_blocks(&mut self, id: &EventId) -> Option<GroupDelta> {
133        if !self.seen.remove(id) {
134            return None;
135        }
136
137        let mut removed_idx: Option<usize> = None;
138        let mut block_replaced_idx: Option<usize> = None;
139
140        for (idx, block) in self.blocks.iter_mut().enumerate() {
141            match block {
142                TimelineBlock::Standalone { id: eid, .. } if eid == id => {
143                    removed_idx = Some(idx);
144                    break;
145                }
146                TimelineBlock::Module {
147                    events,
148                    has_gap,
149                    root,
150                } => {
151                    if events.iter().any(|e| e == id) {
152                        events.retain(|e| e != id);
153                        // A removed mid-chain element introduces a gap.
154                        *has_gap = true;
155                        if events.is_empty() {
156                            removed_idx = Some(idx);
157                        } else if events.len() == 1 {
158                            let only = events.remove(0);
159                            // Collapsing a module to a single event must keep
160                            // the module's resolved root pointer — otherwise
161                            // the survivor reads as a thread root rather than
162                            // the partial-chain head it actually is.
163                            *block = TimelineBlock::Standalone {
164                                id: only,
165                                root: root.take(),
166                            };
167                            block_replaced_idx = Some(idx);
168                        } else {
169                            block_replaced_idx = Some(idx);
170                        }
171                        break;
172                    }
173                }
174                TimelineBlock::Standalone { .. } => {}
175            }
176        }
177
178        if let Some(idx) = removed_idx {
179            self.blocks.remove(idx);
180            Some(GroupDelta::BlockRemoved(idx))
181        } else {
182            block_replaced_idx.map(GroupDelta::BlockReplaced)
183        }
184    }
185
186    /// Evict `id` from the layout when it appears as a `Standalone` block.
187    /// `Module` membership is left intact — a reposted note that's also
188    /// anchoring a reply chain stays in the chain so the reply still has
189    /// parent context. The event's payload remains in `by_id` so the block
190    /// can be restored if every superseder is later removed.
191    fn unplace_standalone(&mut self, id: &EventId) {
192        let standalone_idx = self.blocks.iter().position(|block| match block {
193            TimelineBlock::Standalone { id: eid, .. } => eid == id,
194            TimelineBlock::Module { .. } => false,
195        });
196        if let Some(idx) = standalone_idx {
197            self.blocks.remove(idx);
198            self.seen.remove(id);
199        }
200    }
201
202    /// Process a replaced event. Modelled as remove + insert; wrappers see a
203    /// single delta — the inserted one.
204    #[must_use]
205    pub fn on_replace(&mut self, old_id: &EventId, new_event: &KernelEvent) -> Option<GroupDelta> {
206        let _ = self.on_remove(old_id); // delta from remove is subsumed by the insert delta below
207        self.on_insert(new_event)
208    }
209}