fractal/session/model/room/timeline/timeline_item_diff_minimizer/
mod.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
use std::{
    collections::{HashMap, VecDeque},
    sync::Arc,
};

use gtk::prelude::*;
use matrix_sdk_ui::{eyeball_im::VectorDiff, timeline::TimelineItem as SdkTimelineItem};

mod tests;

use super::TimelineItem;
use crate::prelude::*;

/// Trait to access data from a type that store `TimelineItem`s.
pub(super) trait TimelineItemStore: Sized {
    type Item: IsA<TimelineItem>;
    type Data: TimelineItemData;

    /// The current list of items.
    fn items(&self) -> Vec<Self::Item>;

    /// Create a `TimelineItem` with the given `TimelineItemData`.
    fn create_item(&self, data: &Self::Data) -> Self::Item;

    /// Update the given item with the given timeline ID.
    fn update_item(&self, item: &Self::Item, data: &Self::Data);

    /// Apply the given list of item diffs to this store.
    fn apply_item_diff_list(&self, item_diff_list: Vec<TimelineItemDiff<Self::Item>>);

    /// Whether the given diff list can be minimized by calling
    /// `minimize_diff_list`.
    ///
    /// It can be minimized if there is more than 1 item in the list and if the
    /// list only includes supported `VectorDiff` variants.
    fn can_minimize_diff_list(&self, diff_list: &[VectorDiff<Self::Data>]) -> bool {
        diff_list.len() > 1
            && !diff_list.iter().any(|diff| {
                matches!(
                    diff,
                    VectorDiff::Clear | VectorDiff::Truncate { .. } | VectorDiff::Reset { .. }
                )
            })
    }

    /// Minimize the given diff list and apply it to this store.
    ///
    /// Panics if the diff list contains unsupported `VectorDiff` variants. This
    /// will never panic if `can_minimize_diff_list` returns `true`.
    fn minimize_diff_list(&self, diff_list: Vec<VectorDiff<Self::Data>>) {
        TimelineItemDiffMinimizer::new(self).apply(diff_list);
    }
}

/// Trait implemented by types that provide data for `TimelineItem`s.
pub(super) trait TimelineItemData {
    /// The unique timeline ID of the data.
    fn timeline_id(&self) -> &str;
}

impl TimelineItemData for SdkTimelineItem {
    fn timeline_id(&self) -> &str {
        &self.unique_id().0
    }
}

impl<T> TimelineItemData for Arc<T>
where
    T: TimelineItemData,
{
    fn timeline_id(&self) -> &str {
        (**self).timeline_id()
    }
}

/// A helper struct to minimize a list of `VectorDiff`.
///
/// This does not support `VectorDiff::Clear`, `VectorDiff::Truncate` and
/// `VectorDiff::Reset` as we assume that lists including those cannot be
/// minimized in an optimal way.
struct TimelineItemDiffMinimizer<'a, S, I> {
    store: &'a S,
    item_map: HashMap<String, I>,
    updated_item_ids: Vec<String>,
}

impl<'a, S, I> TimelineItemDiffMinimizer<'a, S, I> {
    /// Construct a `TimelineItemDiffMinimizer` with the given store.
    fn new(store: &'a S) -> Self {
        Self {
            store,
            item_map: HashMap::new(),
            updated_item_ids: Vec::new(),
        }
    }
}

impl<S, I> TimelineItemDiffMinimizer<'_, S, I>
where
    S: TimelineItemStore<Item = I>,
    I: IsA<TimelineItem>,
{
    /// Load the items from the store.
    ///
    /// Returns the list of timeline IDs of the items.
    fn load_items(&mut self) -> Vec<String> {
        let items = self.store.items();
        let item_ids = items.iter().map(S::Item::timeline_id).collect();

        self.item_map
            .extend(items.into_iter().map(|item| (item.timeline_id(), item)));

        item_ids
    }

    /// Update or create an item in the store using the given data.
    ///
    /// Returns the timeline ID of the item.
    fn update_or_create_item(&mut self, data: &S::Data) -> String {
        let timeline_id = data.timeline_id().to_owned();
        self.item_map
            .entry(timeline_id)
            .and_modify(|item| {
                self.store.update_item(item, data);
                self.updated_item_ids.push(item.timeline_id());
            })
            .or_insert_with(|| self.store.create_item(data))
            .timeline_id()
    }

    /// Apply the given diff to the given items.
    fn apply_diff_to_items(
        &mut self,
        item_ids: &[String],
        diff_list: Vec<VectorDiff<S::Data>>,
    ) -> Vec<String> {
        let mut new_item_ids = VecDeque::from(item_ids.to_owned());

        // Get the new state by applying the diffs.
        for diff in diff_list {
            match diff {
                VectorDiff::Append { values } => {
                    let items = values
                        .into_iter()
                        .map(|data| self.update_or_create_item(data));
                    new_item_ids.extend(items);
                }
                VectorDiff::PushFront { value } => {
                    let item = self.update_or_create_item(&value);
                    new_item_ids.push_front(item);
                }
                VectorDiff::PushBack { value } => {
                    let item = self.update_or_create_item(&value);
                    new_item_ids.push_back(item);
                }
                VectorDiff::PopFront => {
                    new_item_ids.pop_front();
                }
                VectorDiff::PopBack => {
                    new_item_ids.pop_back();
                }
                VectorDiff::Insert { index, value } => {
                    let item = self.update_or_create_item(&value);
                    new_item_ids.insert(index, item);
                }
                VectorDiff::Set { index, value } => {
                    let item_id = self.update_or_create_item(&value);
                    *new_item_ids
                        .get_mut(index)
                        .expect("an item should already exist at the given index") = item_id;
                }
                VectorDiff::Remove { index } => {
                    new_item_ids.remove(index);
                }
                VectorDiff::Clear | VectorDiff::Truncate { .. } | VectorDiff::Reset { .. } => {
                    unreachable!()
                }
            }
        }

        new_item_ids.into()
    }

    /// Compute the list of item diffs between the two given lists.
    ///
    /// Uses a diff algorithm to minimize the removals and additions.
    fn item_diff_list(
        &self,
        old_item_ids: &[String],
        new_item_ids: &[String],
    ) -> Vec<TimelineItemDiff<S::Item>> {
        let mut item_diff_list = Vec::new();
        let mut pos = 0;
        // Group diffs in batch.
        let mut n_removals = 0;
        let mut additions = None;
        let mut n_updates = 0;

        for result in diff::slice(old_item_ids, new_item_ids) {
            match result {
                diff::Result::Left(_) => {
                    if let Some(additions) = additions.take() {
                        let item_diff = SpliceDiff {
                            pos,
                            n_removals: 0,
                            additions,
                        };
                        pos += item_diff.additions.len() as u32;
                        item_diff_list.push(item_diff.into());
                    } else if n_updates > 0 {
                        let item_diff = UpdateDiff {
                            pos,
                            n_items: n_updates,
                        };
                        item_diff_list.push(item_diff.into());

                        pos += n_updates;
                        n_updates = 0;
                    }

                    n_removals += 1;
                }
                diff::Result::Both(timeline_id, _) => {
                    if additions.is_some() || n_removals > 0 {
                        let item_diff = SpliceDiff {
                            pos,
                            n_removals,
                            additions: additions.take().unwrap_or_default(),
                        };
                        pos += item_diff.additions.len() as u32;
                        item_diff_list.push(item_diff.into());

                        n_removals = 0;
                    }

                    if self.updated_item_ids.contains(timeline_id) {
                        n_updates += 1;
                    } else {
                        if n_updates > 0 {
                            let item_diff = UpdateDiff {
                                pos,
                                n_items: n_updates,
                            };
                            item_diff_list.push(item_diff.into());

                            pos += n_updates;
                            n_updates = 0;
                        }

                        pos += 1;
                    }
                }
                diff::Result::Right(timeline_id) => {
                    if n_updates > 0 {
                        let item_diff = UpdateDiff {
                            pos,
                            n_items: n_updates,
                        };
                        item_diff_list.push(item_diff.into());

                        pos += n_updates;
                        n_updates = 0;
                    }

                    let item = self
                        .item_map
                        .get(timeline_id)
                        .expect("item should exist in map")
                        .clone();
                    additions.get_or_insert_with(Vec::new).push(item);
                }
            }
        }

        // Process the remaining batches.
        if additions.is_some() || n_removals > 0 {
            let item_diff = SpliceDiff {
                pos,
                n_removals,
                additions: additions.take().unwrap_or_default(),
            };
            item_diff_list.push(item_diff.into());
        } else if n_updates > 0 {
            let item_diff = UpdateDiff {
                pos,
                n_items: n_updates,
            };
            item_diff_list.push(item_diff.into());
        }

        item_diff_list
    }

    /// Minimize the given diff and apply it to the store.
    fn apply(mut self, diff_list: Vec<VectorDiff<S::Data>>) {
        let old_item_ids = self.load_items();
        let new_item_ids = self.apply_diff_to_items(&old_item_ids, diff_list);
        let item_diff_list = self.item_diff_list(&old_item_ids, &new_item_ids);
        self.store.apply_item_diff_list(item_diff_list);
    }
}

/// A minimized diff for timeline items.
#[derive(Debug, Clone)]
pub(super) enum TimelineItemDiff<T> {
    /// Remove then add items.
    Splice(SpliceDiff<T>),

    /// Update items.
    Update(UpdateDiff),
}

impl<T> From<SpliceDiff<T>> for TimelineItemDiff<T> {
    fn from(value: SpliceDiff<T>) -> Self {
        Self::Splice(value)
    }
}

impl<T> From<UpdateDiff> for TimelineItemDiff<T> {
    fn from(value: UpdateDiff) -> Self {
        Self::Update(value)
    }
}

/// A diff to remove then add items.
#[derive(Debug, Clone)]
pub(super) struct SpliceDiff<T> {
    /// The position where the change happens
    pub(super) pos: u32,
    /// The number of items to remove.
    pub(super) n_removals: u32,
    /// The items to add.
    pub(super) additions: Vec<T>,
}

/// A diff to update items.
#[derive(Debug, Clone)]
pub(super) struct UpdateDiff {
    /// The position from where to start updating items.
    pub(super) pos: u32,
    /// The number of items to update.
    pub(super) n_items: u32,
}