matrix-sdk-common 0.17.0

Collection of common types and imports used in the matrix-sdk
Documentation
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
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::sync::{Arc, RwLock};

use eyeball_im::VectorDiff;

use super::{
    Position,
    updates::{ReaderToken, Update, UpdatesInner},
};
use crate::linked_chunk::{ChunkMetadata, UpdateToVectorDiff};

/// A tracker for the order of items in a linked chunk.
///
/// This can be used to determine the absolute ordering of an item, and thus the
/// relative ordering of two items in a linked chunk, in an
/// efficient manner, thanks to [`OrderTracker::ordering`]. Internally, it
/// keeps track of the relative ordering of the chunks themselves; given a
/// [`Position`] in a linked chunk, the item ordering is the lexicographic
/// ordering of the chunk in the linked chunk, and the internal position within
/// the chunk. For the sake of ease, we return the absolute vector index of the
/// item in the linked chunk.
///
/// It requires the full links' metadata to be provided at creation time, so
/// that it can also give an order for an item that's not loaded yet, in the
/// context of lazy-loading.
#[derive(Debug)]
pub struct OrderTracker<Item, Gap> {
    /// Strong reference to [`UpdatesInner`].
    updates: Arc<RwLock<UpdatesInner<Item, Gap>>>,

    /// The token to read the updates.
    token: ReaderToken,

    /// Mapper from `Update` to `VectorDiff`.
    mapper: UpdateToVectorDiff<Item, NullAccumulator<Item>>,
}

struct NullAccumulator<Item> {
    _phantom: std::marker::PhantomData<Item>,
}

#[cfg(not(tarpaulin_include))]
impl<Item> std::fmt::Debug for NullAccumulator<Item> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("NullAccumulator")
    }
}

impl<Item> super::UpdatesAccumulator<Item> for NullAccumulator<Item> {
    fn new(_num_updates_hint: usize) -> Self {
        Self { _phantom: std::marker::PhantomData }
    }
}

impl<Item> Extend<VectorDiff<Item>> for NullAccumulator<Item> {
    fn extend<T: IntoIterator<Item = VectorDiff<Item>>>(&mut self, _iter: T) {
        // This is a no-op, as we don't want to accumulate anything.
    }
}

impl<Item, Gap> OrderTracker<Item, Gap>
where
    Item: Clone,
{
    /// Create a new [`OrderTracker`].
    ///
    /// The `all_chunks_metadata` parameter must include the metadata for *all*
    /// chunks (the full collection, even if the linked chunk is
    /// lazy-loaded).
    ///
    /// They must be ordered by their links in the linked chunk, i.e. the first
    /// chunk in the vector is the first chunk in the linked chunk, the
    /// second in the vector is the first's next chunk, and so on. If that
    /// precondition doesn't hold, then the ordering of items will be undefined.
    pub(super) fn new(
        updates: Arc<RwLock<UpdatesInner<Item, Gap>>>,
        token: ReaderToken,
        all_chunks_metadata: Vec<ChunkMetadata>,
    ) -> Self {
        // Drain previous updates so that this type is synced with `Updates`.
        {
            let mut updates = updates.write().unwrap();
            let _ = updates.take_with_token(token);
        }

        Self { updates, token, mapper: UpdateToVectorDiff::from_metadata(all_chunks_metadata) }
    }

    /// Force flushing of the updates manually.
    ///
    /// If `inhibit` is `true` (which is useful in the case of lazy-loading
    /// related updates, which shouldn't affect the canonical, persisted
    /// linked chunk), the updates are ignored; otherwise, they are consumed
    /// normally.
    pub fn flush_updates(&mut self, inhibit: bool) {
        if inhibit {
            // Ignore the updates.
            let _ = self.updates.write().unwrap().take_with_token(self.token);
        } else {
            // Consume the updates.
            let mut updater = self.updates.write().unwrap();
            let updates = updater.take_with_token(self.token);
            let _ = self.mapper.map(updates);
        }
    }

    /// Apply some out-of-band updates to the ordering tracker.
    ///
    /// This must only be used when the updates do not affect the observed
    /// linked chunk, but would affect the fully-loaded collection.
    pub fn map_updates(&mut self, updates: &[Update<Item, Gap>]) {
        let _ = self.mapper.map(updates);
    }

    /// Given an event's position, returns its final ordering in the current
    /// state of the linked chunk as a vector.
    ///
    /// Useful to compare the ordering of multiple events.
    ///
    /// Precondition: the reader must be up to date, i.e.
    /// [`Self::flush_updates`] must have been called before this method.
    ///
    /// Will return `None` if the position doesn't match a known chunk in the
    /// linked chunk, or if the chunk is a gap.
    pub fn ordering(&self, event_pos: Position) -> Option<usize> {
        // Check the precondition: there must not be any pending updates for this
        // reader.
        debug_assert!(self.updates.read().unwrap().is_reader_up_to_date(self.token));

        // Find the chunk that contained the event.
        let mut ordering = 0;
        for (chunk_id, chunk_length) in &self.mapper.chunks {
            if *chunk_id == event_pos.chunk_identifier() {
                let offset_within_chunk = event_pos.index();
                if offset_within_chunk >= *chunk_length {
                    // The event is out of bounds for this chunk, return None.
                    return None;
                }
                // The final ordering is the number of items before the event, plus its own
                // index within the chunk.
                return Some(ordering + offset_within_chunk);
            }
            // This is not the target chunk yet, so add the size of the current chunk to the
            // number of seen items, and continue.
            ordering += *chunk_length;
        }

        None
    }
}

#[cfg(test)]
mod tests {
    use assert_matches::assert_matches;
    use matrix_sdk_test_macros::async_test;

    use crate::linked_chunk::{
        ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunk,
        OrderTracker, Position, RawChunk, Update, lazy_loader::from_last_chunk,
    };

    #[async_test]
    async fn test_linked_chunk_without_update_history_no_tracking() {
        let mut linked_chunk = LinkedChunk::<10, char, ()>::new();
        assert_matches!(linked_chunk.order_tracker(None), None);
    }

    /// Given a fully-loaded linked chunk, checks that the ordering of an item
    /// is effectively the same as its index in an iteration of items.
    fn assert_order_fully_loaded(
        linked_chunk: &LinkedChunk<3, char, ()>,
        tracker: &OrderTracker<char, ()>,
    ) {
        assert_order(linked_chunk, tracker, 0);
    }

    /// Given a linked chunk with an offset representing the number of items not
    /// loaded yet, checks that the ordering of an item is effectively the
    /// same as its index+offset in an iteration of items.
    fn assert_order(
        linked_chunk: &LinkedChunk<3, char, ()>,
        tracker: &OrderTracker<char, ()>,
        offset: usize,
    ) {
        for (i, (item_pos, _value)) in linked_chunk.items().enumerate() {
            assert_eq!(tracker.ordering(item_pos), Some(i + offset));
        }
    }

    #[async_test]
    async fn test_non_lazy_updates() {
        // Assume the linked chunk is fully loaded, so we have all the chunks at
        // our disposal.
        let mut linked_chunk = LinkedChunk::<3, _, _>::new_with_update_history();

        let mut tracker = linked_chunk.order_tracker(None).unwrap();

        // Let's apply some updates to the live linked chunk.

        // Pushing new items.
        {
            linked_chunk.push_items_back(['a', 'b', 'c']);
            tracker.flush_updates(false);
            assert_order_fully_loaded(&linked_chunk, &tracker);
        }

        // Pushing a gap.
        {
            linked_chunk.push_gap_back(());
            tracker.flush_updates(false);
            assert_order_fully_loaded(&linked_chunk, &tracker);
        }

        // Inserting items in the middle.
        {
            let pos_b = linked_chunk.item_position(|c| *c == 'b').unwrap();
            linked_chunk.insert_items_at(pos_b, ['d', 'e']).unwrap();
            tracker.flush_updates(false);
            assert_order_fully_loaded(&linked_chunk, &tracker);
        }

        // Inserting a gap in the middle.
        {
            let c_pos = linked_chunk.item_position(|c| *c == 'c').unwrap();
            linked_chunk.insert_gap_at((), c_pos).unwrap();
            tracker.flush_updates(false);
            assert_order_fully_loaded(&linked_chunk, &tracker);
        }

        // Replacing a gap with items.
        {
            let last_gap =
                linked_chunk.rchunks().filter(|c| c.is_gap()).last().unwrap().identifier();
            linked_chunk.replace_gap_at(['f', 'g'], last_gap).unwrap();
            tracker.flush_updates(false);
            assert_order_fully_loaded(&linked_chunk, &tracker);
        }

        // Removing an item.
        {
            let a_pos = linked_chunk.item_position(|c| *c == 'd').unwrap();
            linked_chunk.remove_item_at(a_pos).unwrap();
            tracker.flush_updates(false);
            assert_order_fully_loaded(&linked_chunk, &tracker);
        }

        // Replacing an item.
        {
            let b_pos = linked_chunk.item_position(|c| *c == 'e').unwrap();
            linked_chunk.replace_item_at(b_pos, 'E').unwrap();
            tracker.flush_updates(false);
            assert_order_fully_loaded(&linked_chunk, &tracker);
        }

        // Clearing all items.
        {
            linked_chunk.clear();
            tracker.flush_updates(false);
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 0)), None);
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), None);
        }
    }

    #[async_test]
    async fn test_lazy_loading() {
        // Assume that all the chunks haven't been loaded yet, so we have a few of them
        // in some memory, and some of them are still in an hypothetical
        // database.
        let db_metadata = vec![
            // Hypothetical non-empty items chunk with items 'a', 'b', 'c'.
            ChunkMetadata {
                previous: None,
                identifier: ChunkIdentifier(0),
                next: Some(ChunkIdentifier(1)),
                num_items: 3,
            },
            // Hypothetical gap chunk.
            ChunkMetadata {
                previous: Some(ChunkIdentifier(0)),
                identifier: ChunkIdentifier(1),
                next: Some(ChunkIdentifier(2)),
                num_items: 0,
            },
            // Hypothetical non-empty items chunk with items 'd', 'e', 'f'.
            ChunkMetadata {
                previous: Some(ChunkIdentifier(1)),
                identifier: ChunkIdentifier(2),
                next: Some(ChunkIdentifier(3)),
                num_items: 3,
            },
            // Hypothetical non-empty items chunk with items 'g'.
            ChunkMetadata {
                previous: Some(ChunkIdentifier(2)),
                identifier: ChunkIdentifier(3),
                next: None,
                num_items: 1,
            },
        ];

        // The in-memory linked chunk contains the latest chunk only.
        let mut linked_chunk = from_last_chunk::<3, _, ()>(
            Some(RawChunk {
                content: ChunkContent::Items(vec!['g']),
                previous: Some(ChunkIdentifier(2)),
                identifier: ChunkIdentifier(3),
                next: None,
            }),
            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier(3)),
        )
        .expect("could recreate the linked chunk")
        .expect("the linked chunk isn't empty");

        let tracker = linked_chunk.order_tracker(Some(db_metadata)).unwrap();

        // At first, even if the main linked chunk is empty, the order tracker can
        // compute the position for unloaded items.

        // Order of 'a':
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 0)), Some(0));
        // Order of 'b':
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
        // Order of 'c':
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 2)), Some(2));
        // An invalid position in a known chunk returns no ordering.
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 42)), None);

        // A gap chunk doesn't have an ordering.
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(1), 0)), None);
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(1), 42)), None);

        // Order of 'd':
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 0)), Some(3));
        // Order of 'e':
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 1)), Some(4));
        // Order of 'f':
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 2)), Some(5));
        // No subsequent entry in the same chunk, it's been split when inserting g.
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 3)), None);

        // Order of 'g':
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(6));
        // This was the final entry so far.
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 1)), None);
    }

    #[async_test]
    async fn test_lazy_updates() {
        // Assume that all the chunks haven't been loaded yet, so we have a few of them
        // in some memory, and some of them are still in an hypothetical
        // database.
        let db_metadata = vec![
            // Hypothetical non-empty items chunk with items 'a', 'b'.
            ChunkMetadata {
                previous: None,
                identifier: ChunkIdentifier(0),
                next: Some(ChunkIdentifier(1)),
                num_items: 2,
            },
            // Hypothetical gap chunk.
            ChunkMetadata {
                previous: Some(ChunkIdentifier(0)),
                identifier: ChunkIdentifier(1),
                next: Some(ChunkIdentifier(2)),
                num_items: 0,
            },
            // Hypothetical non-empty items chunk with items 'd', 'e', 'f'.
            ChunkMetadata {
                previous: Some(ChunkIdentifier(1)),
                identifier: ChunkIdentifier(2),
                next: Some(ChunkIdentifier(3)),
                num_items: 3,
            },
            // Hypothetical non-empty items chunk with items 'g'.
            ChunkMetadata {
                previous: Some(ChunkIdentifier(2)),
                identifier: ChunkIdentifier(3),
                next: None,
                num_items: 1,
            },
        ];

        // The in-memory linked chunk contains the latest chunk only.
        let mut linked_chunk = from_last_chunk(
            Some(RawChunk {
                content: ChunkContent::Items(vec!['g']),
                previous: Some(ChunkIdentifier(2)),
                identifier: ChunkIdentifier(3),
                next: None,
            }),
            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier(3)),
        )
        .expect("could recreate the linked chunk")
        .expect("the linked chunk isn't empty");

        let mut tracker = linked_chunk.order_tracker(Some(db_metadata)).unwrap();

        // Sanity checks on the initial state.
        {
            // Order of 'b':
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
            // Order of 'g':
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
        }

        // Let's apply some updates to the live linked chunk.

        // Pushing new items.
        {
            linked_chunk.push_items_back(['h', 'i']);
            tracker.flush_updates(false);

            // Order of items not loaded:
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
            // The loaded items are off by 5 (the absolute order of g).
            assert_order(&linked_chunk, &tracker, 5);
        }

        // Pushing a gap.
        let gap_id = {
            linked_chunk.push_gap_back(());
            tracker.flush_updates(false);

            // The gap doesn't have an ordering.
            let last_chunk = linked_chunk.rchunks().next().unwrap();
            assert!(last_chunk.is_gap());
            assert_eq!(tracker.ordering(Position::new(last_chunk.identifier(), 0)), None);
            assert_eq!(tracker.ordering(Position::new(last_chunk.identifier(), 42)), None);

            // The previous items are still ordered.
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
            // The loaded items are off by 5 (the absolute order of g).
            assert_order(&linked_chunk, &tracker, 5);

            last_chunk.identifier()
        };

        // Inserting items in the middle.
        {
            let pos_h = linked_chunk.item_position(|c| *c == 'h').unwrap();
            linked_chunk.insert_items_at(pos_h, ['j', 'k']).unwrap();
            tracker.flush_updates(false);

            // The previous items are still ordered.
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
            // The loaded items are off by 5 (the absolute order of g).
            assert_order(&linked_chunk, &tracker, 5);
        }

        // Replacing a gap with items.
        {
            linked_chunk.replace_gap_at(['l', 'm'], gap_id).unwrap();
            tracker.flush_updates(false);

            // The previous items are still ordered.
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
            // The loaded items are off by 5 (the absolute order of g).
            assert_order(&linked_chunk, &tracker, 5);
        }

        // Removing an item.
        {
            let j_pos = linked_chunk.item_position(|c| *c == 'j').unwrap();
            linked_chunk.remove_item_at(j_pos).unwrap();
            tracker.flush_updates(false);

            // The previous items are still ordered.
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
            // The loaded items are off by 5 (the absolute order of g).
            assert_order(&linked_chunk, &tracker, 5);
        }

        // Replacing an item.
        {
            let k_pos = linked_chunk.item_position(|c| *c == 'k').unwrap();
            linked_chunk.replace_item_at(k_pos, 'K').unwrap();
            tracker.flush_updates(false);

            // The previous items are still ordered.
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
            // The loaded items are off by 5 (the absolute order of g).
            assert_order(&linked_chunk, &tracker, 5);
        }

        // Clearing all items.
        {
            linked_chunk.clear();
            tracker.flush_updates(false);
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 0)), None);
            assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), None);
        }
    }

    #[async_test]
    async fn test_out_of_band_updates() {
        // Assume that all the chunks haven't been loaded yet, so we have a few of them
        // in some memory, and some of them are still in an hypothetical
        // database.
        let db_metadata = vec![
            // Hypothetical non-empty items chunk with items 'a', 'b'.
            ChunkMetadata {
                previous: None,
                identifier: ChunkIdentifier(0),
                next: Some(ChunkIdentifier(1)),
                num_items: 2,
            },
            // Hypothetical gap chunk.
            ChunkMetadata {
                previous: Some(ChunkIdentifier(0)),
                identifier: ChunkIdentifier(1),
                next: Some(ChunkIdentifier(2)),
                num_items: 0,
            },
            // Hypothetical non-empty items chunk with items 'd', 'e', 'f'.
            ChunkMetadata {
                previous: Some(ChunkIdentifier(1)),
                identifier: ChunkIdentifier(2),
                next: Some(ChunkIdentifier(3)),
                num_items: 3,
            },
            // Hypothetical non-empty items chunk with items 'g'.
            ChunkMetadata {
                previous: Some(ChunkIdentifier(2)),
                identifier: ChunkIdentifier(3),
                next: None,
                num_items: 1,
            },
        ];

        let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();

        let mut tracker = linked_chunk.order_tracker(Some(db_metadata)).unwrap();

        // Sanity checks.
        // Order of 'b':
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
        // Order of 'e':
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 1)), Some(3));

        // It's possible to apply updates out of band, i.e. without affecting the
        // observed linked chunk. This can be useful when an update only applies
        // to a database, but not to the in-memory linked chunk.
        tracker.map_updates(&[Update::RemoveChunk(ChunkIdentifier::new(0))]);

        // 'b' doesn't exist anymore, so its ordering is now undefined.
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), None);
        // 'e' has been shifted back by 2 places, aka the number of items in the first
        // chunk.
        assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 1)), Some(1));
    }
}