Files
tuwunel/src/api/client/sync/v5/selector.rs
Jason Volk 0397bb8237 Implement specified non-blocking semantic for sliding-sync.
Simplify sliding-sync watch loop and bounds.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-23 11:00:55 +00:00

269 lines
5.8 KiB
Rust

use std::cmp::Ordering;
use futures::{
FutureExt, StreamExt, TryFutureExt,
future::{OptionFuture, join5},
};
use ruma::{OwnedRoomId, UInt, events::room::member::MembershipState, uint};
use tuwunel_core::{
apply, is_true,
matrix::PduCount,
trace,
utils::{
BoolExt,
future::TryExtExt,
math::usize_from_ruma,
stream::{BroadbandExt, IterStream},
},
};
use tuwunel_service::sync::Connection;
use super::{
ListIds, ResponseLists, SyncInfo, Window, WindowRoom, filter_room, filter_room_meta,
};
#[tracing::instrument(level = "debug", skip_all)]
pub(super) async fn selector(
conn: &mut Connection,
sync_info: SyncInfo<'_>,
) -> (Window, ResponseLists) {
use MembershipState::*;
let SyncInfo { services, sender_user, .. } = sync_info;
let mut rooms = services
.state_cache
.user_memberships(sender_user, Some(&[Join, Invite, Knock]))
.map(|(membership, room_id)| (room_id.to_owned(), Some(membership)))
.broad_filter_map(|(room_id, membership)| {
match_lists_for_room(sync_info, conn, room_id, membership)
})
.collect::<Vec<_>>()
.await;
rooms.sort_by(room_sort);
rooms
.iter_mut()
.enumerate()
.for_each(|(i, room)| {
room.ranked = i;
conn.rooms
.entry(room.room_id.clone())
.or_default();
});
trace!(?rooms);
let lists = response_lists(rooms.iter());
trace!(?lists);
let window = select_window(sync_info, conn, rooms.iter(), &lists).await;
trace!(?window);
(window, lists)
}
#[tracing::instrument(
name = "window",
level = "debug",
skip_all,
fields(rooms = rooms.clone().count())
)]
async fn select_window<'a, Rooms>(
sync_info: SyncInfo<'_>,
conn: &Connection,
rooms: Rooms,
lists: &ResponseLists,
) -> Window
where
Rooms: Iterator<Item = &'a WindowRoom> + Clone + Send + Sync,
{
static FULL_RANGE: (UInt, UInt) = (UInt::MIN, UInt::MAX);
let selections = lists
.keys()
.cloned()
.filter_map(|id| conn.lists.get(&id).map(|list| (id, list)))
.flat_map(|(id, list)| {
let full_range = list
.ranges
.is_empty()
.then_some(&FULL_RANGE)
.into_iter();
list.ranges
.iter()
.chain(full_range)
.map(apply!(2, usize_from_ruma))
.map(move |range| (id.clone(), range))
})
.flat_map(|(id, (start, end))| {
rooms
.clone()
.filter(move |&room| room.lists.contains(&id))
.filter(|&room| {
conn.rooms
.get(&room.room_id)
.is_some_and(|conn_room| room.last_count > conn_room.roomsince)
})
.enumerate()
.skip_while(move |&(i, _)| i < start)
.take(end.saturating_add(1).saturating_sub(start))
.map(|(_, room)| (room.room_id.clone(), room.clone()))
})
.stream();
let subscriptions = conn
.subscriptions
.iter()
.stream()
.broad_filter_map(async |(room_id, _)| {
filter_room_meta(sync_info, room_id)
.await
.then(|| WindowRoom {
room_id: room_id.clone(),
membership: None,
lists: Default::default(),
ranked: usize::MAX,
last_count: 0,
})
})
.map(|room| (room.room_id.clone(), room));
subscriptions.chain(selections).collect().await
}
#[tracing::instrument(
name = "matcher",
level = "trace",
skip_all,
fields(?room_id, ?membership)
)]
async fn match_lists_for_room(
sync_info: SyncInfo<'_>,
conn: &Connection,
room_id: OwnedRoomId,
membership: Option<MembershipState>,
) -> Option<WindowRoom> {
let SyncInfo { services, sender_user, .. } = sync_info;
let (matched, lists) = conn
.lists
.iter()
.stream()
.filter_map(async |(id, list)| {
let filter: OptionFuture<_> = list
.filters
.clone()
.map(async |filters| {
filter_room(sync_info, &filters, &room_id, membership.as_ref()).await
})
.into();
filter
.await
.is_none_or(is_true!())
.then(|| id.clone())
})
.collect::<ListIds>()
.map(|lists| (lists.is_empty().is_false(), lists))
.await;
let last_notification: OptionFuture<_> = matched
.then(|| {
services
.user
.last_notification_read(sender_user, &room_id)
})
.into();
let last_privateread: OptionFuture<_> = matched
.then(|| {
services
.read_receipt
.last_privateread_update(sender_user, &room_id)
})
.into();
let last_receipt: OptionFuture<_> = matched
.then(|| {
services
.read_receipt
.last_receipt_count(&room_id, sender_user.into(), None)
.unwrap_or_default()
})
.into();
let last_account: OptionFuture<_> = matched
.then(|| {
services
.account_data
.last_count(Some(room_id.as_ref()), sender_user, Some(conn.next_batch))
.unwrap_or_default()
})
.into();
let last_timeline: OptionFuture<_> = matched
.then(|| {
services
.timeline
.last_timeline_count(None, &room_id, Some(conn.next_batch.into()))
.map_ok(PduCount::into_unsigned)
.unwrap_or_default()
})
.into();
let (last_timeline, last_notification, last_account, last_receipt, last_privateread) =
join5(last_timeline, last_notification, last_account, last_receipt, last_privateread)
.await;
Some(WindowRoom {
room_id: room_id.clone(),
membership,
lists,
ranked: 0,
last_count: [
last_timeline,
last_notification,
last_account,
last_receipt,
last_privateread,
]
.into_iter()
.map(Option::unwrap_or_default)
.filter(|count| conn.next_batch.ge(count))
.max()
.unwrap_or_default(),
})
}
fn response_lists<'a, Rooms>(rooms: Rooms) -> ResponseLists
where
Rooms: Iterator<Item = &'a WindowRoom>,
{
rooms
.flat_map(|room| room.lists.iter())
.fold(ResponseLists::default(), |mut lists, id| {
let list = lists.entry(id.clone()).or_default();
list.count = list
.count
.checked_add(uint!(1))
.expect("list count must not overflow JsInt");
lists
})
}
fn room_sort(a: &WindowRoom, b: &WindowRoom) -> Ordering {
if a.membership != b.membership {
if a.membership == Some(MembershipState::Invite) {
return Ordering::Less;
}
if b.membership == Some(MembershipState::Invite) {
return Ordering::Greater;
}
}
b.last_count.cmp(&a.last_count)
}