fix(relations): Fix duplicate messages in thread pagination

The /relations endpoint incorrectly handled pagination tokens, causing
duplicate messages when scrolling through threads. This was due to:

1. Incorrectly modifying the 'from' parameter with saturating_inc()
2. Not excluding the 'from' event from results

Changed pagination to use ShortEventId directly as the next_batch token
instead of complex PduCount handling. This provides more reliable
pagination that handles federation edge cases.

The fix ensures proper event ordering and prevents duplicates by:
- Using ShortEventId (u64) directly in pagination tokens
- Properly filtering events based on direction
- Excluding the starting event from results
This commit is contained in:
Tom Foster 2025-08-10 12:27:44 +01:00
commit 39671718ac
2 changed files with 19 additions and 9 deletions

View file

@ -109,13 +109,13 @@ async fn paginate_relations_with_filter(
recurse: bool,
dir: Direction,
) -> Result<get_relating_events::v1::Response> {
let start: PduCount = from
.map(str::parse)
.transpose()?
.unwrap_or_else(|| match dir {
let start: PduCount = from.and_then(|s| s.parse::<u64>().ok()).map_or_else(
|| match dir {
| Direction::Forward => PduCount::min(),
| Direction::Backward => PduCount::max(),
});
},
PduCount::Normal,
);
let to: Option<PduCount> = to.map(str::parse).flat_ok();
@ -156,9 +156,7 @@ async fn paginate_relations_with_filter(
| Direction::Forward => events.last(),
| Direction::Backward => events.first(),
}
.map(at!(0))
.as_ref()
.map(ToString::to_string);
.map(|(count, _)| count.into_unsigned().to_string());
Ok(get_relating_events::v1::Response {
next_batch,

View file

@ -61,9 +61,10 @@ impl Data {
from: PduCount,
dir: Direction,
) -> impl Stream<Item = (PduCount, impl Event)> + Send + '_ {
let from_unsigned = from.into_unsigned();
let mut current = ArrayVec::<u8, 16>::new();
current.extend(target.to_be_bytes());
current.extend(from.saturating_inc(dir).into_unsigned().to_be_bytes());
current.extend(from_unsigned.to_be_bytes());
let current = current.as_slice();
match dir {
| Direction::Forward => self.tofrom_relation.raw_keys_from(current).boxed(),
@ -73,6 +74,17 @@ impl Data {
.ready_take_while(move |key| key.starts_with(&target.to_be_bytes()))
.map(|to_from| u64_from_u8(&to_from[8..16]))
.map(PduCount::from_unsigned)
.ready_filter(move |count| {
if from == PduCount::min() || from == PduCount::max() {
true
} else {
let count_unsigned = count.into_unsigned();
match dir {
| Direction::Forward => count_unsigned > from_unsigned,
| Direction::Backward => count_unsigned < from_unsigned,
}
}
})
.wide_filter_map(move |shorteventid| async move {
let pdu_id: RawPduId = PduId { shortroomid, shorteventid }.into();