Commit da8896938cf0
tarball12 files changed+438-109
@@ -1,43 +1,190 @@
1
-use std::{collections::HashMap, sync::Arc};1
+use std::{2
+ collections::{HashMap, VecDeque},3
+ sync::Arc,4
+};25
36
use futures_util::future::{join_all, try_join};47
use matrix_sdk::{5
- Room, deserialized_responses::TimelineEvent as SdkTimelineEvent, room::MessagesOptions,8
+ Client, Room, RoomState, deserialized_responses::TimelineEvent as SdkTimelineEvent,9
+ room::MessagesOptions,610
};7
-use ruma::{OwnedEventId, OwnedUserId, UInt, push::Action};11
+use ruma::{OwnedEventId, OwnedRoomId, OwnedUserId, UInt, push::Action};812
use serde_json::Value;13
+use tokio::sync::{Mutex, broadcast::error::RecvError};914
1015
use crate::{11
- Error, ErrorKind, EventContext, EventContextCommand, EventContextPage, MediaId, Sender,12
- TimelineEvent, TimelineItem,16
+ Error, ErrorKind, EventContext, EventContextCommand, EventContextPage, EventContextSession,17
+ MediaId, Sender, TimelineEvent, TimelineItem,1318
engine::MediaService,1419
normalise::{15
- MediaRegistration, apply_profiles, normalise_context, normalise_observed, resolve_replies,20
+ MediaRegistration, MediaScope, apply_profiles, normalise_context, normalise_observed,21
+ resolve_replies,1622
},1723
};1824
1925
const MAX_CONTEXT_LIMIT: u16 = 50;26
+/// a consumer that never closes its jump views still holds at most this many27
+const MAX_SESSIONS: usize = 32;28
+29
+/// open event contexts, oldest first, each owning the media its pages registered30
+#[derive(Default)]31
+pub(crate) struct Sessions {32
+ open: Mutex<VecDeque<Session>>,33
+}34
+35
+struct Session {36
+ id: String,37
+ room_id: OwnedRoomId,38
+ scope: MediaScope,39
+}40
+41
+impl Sessions {42
+ async fn open(43
+ &self,44
+ media: &MediaService,45
+ room_id: OwnedRoomId,46
+ registrations: Vec<MediaRegistration>,47
+ ) -> String {48
+ let scope = media.context_scope();49
+ let id = format!("context-{}", scope.nonce());50
+ let mut open = self.open.lock().await;51
+ media.register(&scope, registrations).await;52
+ open.push_back(Session {53
+ id: id.clone(),54
+ room_id,55
+ scope,56
+ });57
+ while open.len() > MAX_SESSIONS {58
+ if let Some(oldest) = open.pop_front() {59
+ media.release(&oldest.scope).await;60
+ }61
+ }62
+ id63
+ }64
+65
+ async fn room(&self, id: &str) -> Result<OwnedRoomId, Error> {66
+ let open = self.open.lock().await;67
+ let session = open.iter().find(|session| session.id == id);68
+ session69
+ .map(|session| session.room_id.clone())70
+ .ok_or_else(not_open)71
+ }72
+73
+ /// under the lock, so a page that lands after its close registers nothing74
+ async fn register(75
+ &self,76
+ media: &MediaService,77
+ id: &str,78
+ registrations: Vec<MediaRegistration>,79
+ ) -> Result<(), Error> {80
+ let open = self.open.lock().await;81
+ let session = open82
+ .iter()83
+ .find(|session| session.id == id)84
+ .ok_or_else(not_open)?;85
+ media.register(&session.scope, registrations).await;86
+ Ok(())87
+ }88
+89
+ async fn close(&self, media: &MediaService, id: &str) -> Result<(), Error> {90
+ let mut open = self.open.lock().await;91
+ let index = open92
+ .iter()93
+ .position(|session| session.id == id)94
+ .ok_or_else(not_open)?;95
+ if let Some(session) = open.remove(index) {96
+ media.release(&session.scope).await;97
+ }98
+ Ok(())99
+ }100
+101
+ async fn close_left(&self, media: &MediaService, client: &Client) {102
+ let mut open = self.open.lock().await;103
+ let mut kept = VecDeque::with_capacity(open.len());104
+ for session in open.drain(..) {105
+ let left = client106
+ .get_room(&session.room_id)107
+ .is_none_or(|room| matches!(room.state(), RoomState::Left | RoomState::Banned));108
+ if left {109
+ media.release(&session.scope).await;110
+ } else {111
+ kept.push_back(session);112
+ }113
+ }114
+ *open = kept;115
+ }116
+117
+ /// the media service is cleared beside this, so nothing is released one by one118
+ pub(crate) async fn clear(&self) {119
+ self.open.lock().await.clear();120
+ }121
+}122
+123
+fn not_open() -> Error {124
+ Error::new(125
+ ErrorKind::EventContextNotOpen,126
+ "event context is closed or was never opened",127
+ )128
+}129
+130
+/// leaving a room closes its event contexts131
+pub(super) async fn close_on_leave(132
+ client: Client,133
+ media: Arc<MediaService>,134
+ sessions: Arc<Sessions>,135
+) {136
+ let mut info = client.room_info_notable_update_receiver();137
+ loop {138
+ match info.recv().await {139
+ Ok(_) | Err(RecvError::Lagged(_)) => sessions.close_left(&media, &client).await,140
+ Err(RecvError::Closed) => return,141
+ }142
+ }143
+}20144
21145
pub(crate) async fn execute(22
- room: Room,23
- current_user_id: Option<OwnedUserId>,146
+ client: Client,24147
media: Arc<MediaService>,148
+ sessions: Arc<Sessions>,25149
command: EventContextCommand,26150
) -> Result<crate::CommandOutput, Error> {27
- let (token, limit, backwards) = match command {151
+ let current_user_id = client.user_id().map(ToOwned::to_owned);152
+ let room = |room_id: &OwnedRoomId| client.get_room(room_id).ok_or_else(Error::room_unknown);153
+ let (context_id, token, limit, backwards) = match command {28154
EventContextCommand::Fetch {155
+ room_id,29156
event_id,30157
before_limit,31158
after_limit,32
- ..33159
} => {34
- let (context, registrations, _) =35
- capture(room, current_user_id, event_id, before_limit, after_limit).await?;36
- media.register(registrations).await;37
- return Ok(crate::CommandOutput::EventContext(context));160
+ let (context, registrations, _) = capture(161
+ room(&room_id)?,162
+ current_user_id,163
+ event_id,164
+ before_limit,165
+ after_limit,166
+ )167
+ .await?;168
+ let context_id = sessions.open(&media, room_id, registrations).await;169
+ return Ok(crate::CommandOutput::EventContext(EventContextSession {170
+ context_id,171
+ context,172
+ }));173
+ }174
+ EventContextCommand::Close { context_id } => {175
+ sessions.close(&media, &context_id).await?;176
+ return Ok(crate::CommandOutput::Done);38177
}39
- EventContextCommand::Before { token, limit, .. } => (token, limit, true),40
- EventContextCommand::After { token, limit, .. } => (token, limit, false),178
+ EventContextCommand::Before {179
+ context_id,180
+ token,181
+ limit,182
+ } => (context_id, token, limit, true),183
+ EventContextCommand::After {184
+ context_id,185
+ token,186
+ limit,187
+ } => (context_id, token, limit, false),41188
};42189
if limit == 0 {43190
return Err(Error::new(@@ -52,6 +199,7 @@ pub(crate) async fn execute(
52199
"context pagination token is empty",53200
));54201
}202
+ let room = room(&sessions.room(&context_id).await?)?;55203
let side = fetch_side(&room, Some(&token), limit, backwards).await?;56204
let mut page = Assembly::new(57205
&room,@@ -62,7 +210,7 @@ pub(crate) async fn execute(
62210
)63211
.await;64212
let events = page.take(0..side.values.len());65
- media.register(page.media).await;213
+ sessions.register(&media, &context_id, page.media).await?;66214
Ok(crate::CommandOutput::EventContextPage(EventContextPage {67215
events,68216
next_token: side.token,@@ -56,27 +56,34 @@ pub(crate) struct MediaService {
5656
registry: Mutex<MediaRegistry>,5757
flights: Mutex<HashMap<String, WeakShared<FlightFuture>>>,5858
max_bytes: usize,59
+ context_limit: usize,5960
url_previews: UrlPreviews,6061
}6162
6263
impl MediaService {63
- pub(crate) fn new(client: Client, max_bytes: usize, max_sources: usize) -> Self {64
+ pub(crate) fn new(client: Client, max_bytes: usize, context_limit: usize) -> Self {6465
Self {6566
url_previews: UrlPreviews::new(client.clone()),6667
client,67
- registry: Mutex::new(MediaRegistry::new(max_sources)),68
+ registry: Mutex::new(MediaRegistry::default()),6869
flights: Mutex::new(HashMap::new()),6970
max_bytes,71
+ context_limit,7072
}7173
}7274
75
+ /// one event context session's media, bounded by `EngineConfig::max_context_media`76
+ pub(crate) fn context_scope(&self) -> MediaScope {77
+ MediaScope::bounded(self.context_limit)78
+ }79
+7380
pub(crate) async fn url_preview(&self, url: &str) -> Result<Option<LinkPreview>, Error> {7481
self.url_previews.preview(url).await7582
}7683
77
- pub(crate) async fn register(&self, media: Vec<MediaRegistration>) {84
+ pub(crate) async fn register(&self, scope: &MediaScope, media: Vec<MediaRegistration>) {7885
if !media.is_empty() {79
- self.registry.lock().await.insert_all(media);86
+ self.registry.lock().await.insert_all(scope, media);8087
}8188
}8289
@@ -747,64 +754,178 @@ async fn check_reply_thread(
747754
}748755
}749756
750
-/// least recently seen goes first. stale order slots are skipped by stamp rather than hunted down757
+/// every entry belongs to the scopes that registered it and lives until the last of them758
+/// lets go. there is no global cap, a bounded scope only ever trims itself759
+#[derive(Default)]751760
struct MediaRegistry {752
- entries: HashMap<MediaId, (MediaRegistration, u64)>,761
+ entries: HashMap<MediaId, Entry>,762
+ scopes: HashMap<u64, ScopeEntries>,763
+}764
+765
+struct Entry {766
+ media: MediaRegistration,767
+ owners: Vec<u64>,768
+}769
+770
+/// least recently used first. stale order slots are skipped by stamp rather than hunted down771
+#[derive(Default)]772
+struct ScopeEntries {773
+ stamps: HashMap<MediaId, u64>,753774
order: VecDeque<(MediaId, u64)>,754775
next_stamp: u64,755
- limit: usize,776
+ limit: Option<usize>,756777
}757778
758
-impl MediaRegistry {759
- fn new(limit: usize) -> Self {760
- Self {761
- entries: HashMap::new(),762
- order: VecDeque::new(),763
- next_stamp: 0,764
- limit,779
+impl ScopeEntries {780
+ fn touch(&mut self, id: &MediaId) {781
+ if self.limit.is_none() {782
+ self.stamps.insert(id.clone(), 0);783
+ return;765784
}785
+ let stamp = self.next_stamp;786
+ self.next_stamp += 1;787
+ self.stamps.insert(id.clone(), stamp);788
+ self.order.push_back((id.clone(), stamp));766789
}767790
768
- fn get(&self, id: &MediaId) -> Option<&MediaRegistration> {769
- self.entries.get(id).map(|(media, _)| media)770
- }771
-772
- fn insert_all(&mut self, media: Vec<MediaRegistration>) {773
- for media in media {774
- let stamp = self.next_stamp;775
- self.next_stamp += 1;776
- self.order.push_back((media.id.clone(), stamp));777
- self.entries.insert(media.id.clone(), (media, stamp));778
- }779
- while self.entries.len() > self.limit {791
+ /// what fell out past the limit792
+ fn trim(&mut self) -> Vec<MediaId> {793
+ let Some(limit) = self.limit else {794
+ return Vec::new();795
+ };796
+ let mut dropped = Vec::new();797
+ while self.stamps.len() > limit {780798
let Some((id, stamp)) = self.order.pop_front() else {781799
break;782800
};783
- if self.is_current(&id, stamp) {784
- self.entries.remove(&id);801
+ if self.stamps.get(&id) == Some(&stamp) {802
+ self.stamps.remove(&id);803
+ dropped.push(id);785804
}786805
}787
- if self.order.len() > self.limit.saturating_mul(4) {788
- let entries = &self.entries;806
+ if self.order.len() > limit.saturating_mul(4) {807
+ let stamps = &self.stamps;789808
self.order790
- .retain(|(id, stamp)| entries.get(id).is_some_and(|(_, current)| current == stamp));809
+ .retain(|(id, stamp)| stamps.get(id) == Some(stamp));791810
}811
+ dropped792812
}813
+}793814
794
- fn is_current(&self, id: &MediaId, stamp: u64) -> bool {795
- self.entries796
- .get(id)797
- .is_some_and(|(_, current)| *current == stamp)815
+impl MediaRegistry {816
+ fn get(&mut self, id: &MediaId) -> Option<&MediaRegistration> {817
+ let entry = self.entries.get(id)?;818
+ for owner in &entry.owners {819
+ if let Some(scope) = self.scopes.get_mut(owner) {820
+ scope.touch(id);821
+ }822
+ }823
+ Some(&entry.media)824
+ }825
+826
+ fn insert_all(&mut self, scope: &MediaScope, media: Vec<MediaRegistration>) {827
+ let owner = scope.nonce();828
+ let entries = self.scopes.entry(owner).or_insert_with(|| ScopeEntries {829
+ limit: scope.limit(),830
+ ..ScopeEntries::default()831
+ });832
+ for media in media {833
+ let id = media.id.clone();834
+ entries.touch(&id);835
+ let entry = self.entries.entry(id).or_insert_with(|| Entry {836
+ media: media.clone(),837
+ owners: Vec::new(),838
+ });839
+ entry.media = media;840
+ if !entry.owners.contains(&owner) {841
+ entry.owners.push(owner);842
+ }843
+ }844
+ for id in entries.trim() {845
+ Self::disown(&mut self.entries, &id, owner);846
+ }847
+ }848
+849
+ fn disown(entries: &mut HashMap<MediaId, Entry>, id: &MediaId, owner: u64) {850
+ let Some(entry) = entries.get_mut(id) else {851
+ return;852
+ };853
+ entry.owners.retain(|current| *current != owner);854
+ if entry.owners.is_empty() {855
+ entries.remove(id);856
+ }798857
}799858
800859
fn release(&mut self, scope: &MediaScope) {801
- self.entries.retain(|id, _| !scope.owns(id));802
- let entries = &self.entries;803
- self.order.retain(|(id, _)| entries.contains_key(id));860
+ let Some(released) = self.scopes.remove(&scope.nonce()) else {861
+ return;862
+ };863
+ for id in released.stamps.keys() {864
+ Self::disown(&mut self.entries, id, scope.nonce());865
+ }804866
}805867
806868
fn clear(&mut self) {807869
self.entries.clear();808
- self.order.clear();870
+ self.scopes.clear();871
+ }872
+}873
+874
+#[cfg(test)]875
+mod tests {876
+ use super::*;877
+ use crate::MediaKind;878
+879
+ fn media(name: &str) -> MediaRegistration {880
+ let uri = ruma::OwnedMxcUri::from(format!("mxc://example.org/{name}"));881
+ MediaRegistration {882
+ id: MediaId::new(format!("test#{name}")),883
+ kind: MediaKind::Image,884
+ full: MediaSourceInfo {885
+ source: MediaSource::Plain(uri),886
+ mimetype: None,887
+ declared_size: None,888
+ },889
+ thumbnail: None,890
+ }891
+ }892
+893
+ fn has(registry: &mut MediaRegistry, name: &str) -> bool {894
+ registry.get(&media(name).id).is_some()895
+ }896
+897
+ #[test]898
+ fn shared_media_lives_until_every_owner_lets_go() {899
+ let mut registry = MediaRegistry::default();900
+ let (timeline, context) = (MediaScope::new(), MediaScope::bounded(8));901
+ registry.insert_all(&timeline, vec![media("shared"), media("row")]);902
+ registry.insert_all(&context, vec![media("shared"), media("page")]);903
+ registry.release(&context);904
+ assert!(has(&mut registry, "shared"));905
+ assert!(has(&mut registry, "row"));906
+ assert!(!has(&mut registry, "page"));907
+ registry.release(&timeline);908
+ assert!(!has(&mut registry, "shared"));909
+ assert!(registry.entries.is_empty());910
+ }911
+912
+ #[test]913
+ fn heavy_context_paging_trims_only_its_own_least_recent_entries() {914
+ let mut registry = MediaRegistry::default();915
+ let (timeline, context) = (MediaScope::new(), MediaScope::bounded(4));916
+ registry.insert_all(&timeline, vec![media("on-screen"), media("both")]);917
+ registry.insert_all(&context, vec![media("both"), media("kept")]);918
+ for page in 0..50 {919
+ assert!(has(&mut registry, "kept"));920
+ registry.insert_all(&context, vec![media(&format!("page-{page}"))]);921
+ }922
+ assert!(has(&mut registry, "on-screen"));923
+ assert!(has(&mut registry, "both"));924
+ assert!(has(&mut registry, "kept"));925
+ assert!(!has(&mut registry, "page-0"));926
+ assert!(registry.scopes[&context.nonce()].stamps.len() <= 4);927
+ registry.release(&context);928
+ assert!(has(&mut registry, "both"));929
+ assert!(!has(&mut registry, "kept"));809930
}810931
}@@ -64,7 +64,8 @@ pub struct EngineConfig {
6464
/// what a thread paginates as it opens, and a room if its recent load fails. 0 for neither6565
pub initial_history_limit: u16,6666
pub max_media_bytes: usize,67
- pub max_media_sources: usize,67
+ /// media one event context session keeps registered, least recently used dropped first68
+ pub max_context_media: usize,6869
pub max_mentions_bytes: usize,6970
pub max_mentions_entries: usize,7071
pub max_mention_context_events: usize,@@ -123,8 +124,8 @@ impl EngineConfig {
123124
}124125
125126
#[must_use]126
- pub const fn with_max_media_sources(mut self, sources: usize) -> Self {127
- self.max_media_sources = sources;127
+ pub const fn with_max_context_media(mut self, media: usize) -> Self {128
+ self.max_context_media = media;128129
self129130
}130131
@@ -142,7 +143,7 @@ impl Default for EngineConfig {
142143
event_capacity: 128,143144
initial_history_limit: 30,144145
max_media_bytes: 20 * 1024 * 1024,145
- max_media_sources: 2_048,146
+ max_context_media: 1_024,146147
max_mentions_bytes: 64 * 1024 * 1024,147148
max_mentions_entries: 5_000,148149
max_mention_context_events: 25_000,@@ -302,6 +303,7 @@ pub struct Engine {
302303
port: DriverPort,303304
config: EngineConfig,304305
media: Arc<MediaService>,306
+ contexts: Arc<context::Sessions>,305307
tasks: TaskRegistry,306308
rooms: HashMap<OwnedRoomId, RoomSlot>,307309
next_generation: u64,@@ -351,7 +353,7 @@ impl Engine {
351353
}352354
if config.command_capacity == 0353355
|| config.event_capacity == 0354
- || config.max_media_sources == 0356
+ || config.max_context_media == 0355357
|| config.max_mentions_entries == 0356358
|| config.max_mention_context_events == 0357359
|| config.max_mention_candidates == 0@@ -362,7 +364,7 @@ impl Engine {
362364
let media = Arc::new(MediaService::new(363365
client.clone(),364366
config.max_media_bytes,365
- config.max_media_sources,367
+ config.max_context_media,366368
));367369
let (mentions, mentions_service, mentions_updates) =368370
crate::mentions::service::Service::new(@@ -407,6 +409,7 @@ impl Engine {
407409
port,408410
config,409411
media,412
+ contexts: Arc::default(),410413
tasks: TaskRegistry {411414
tasks: TaskSet::new(),412415
},@@ -535,7 +538,7 @@ impl Engine {
535538
let client = &self.client;536539
let events = || self.port.event_sender();537540
let (notices, notice_queue) = mpsc::unbounded_channel();538
- let watchers: [(&'static str, EngineBoxFuture<'static, ()>); 13] = [541
+ let watchers: [(&'static str, EngineBoxFuture<'static, ()>); 14] = [539542
(540543
"upload tracker",541544
Box::pin(watchers::uploads(@@ -600,6 +603,14 @@ impl Engine {
600603
Box::pin(watchers::rooms(client.clone(), events())),601604
),602605
(606
+ "event context watcher",607
+ Box::pin(context::close_on_leave(608
+ client.clone(),609
+ self.media.clone(),610
+ self.contexts.clone(),611
+ )),612
+ ),613
+ (603614
"image pack watcher",604615
Box::pin(watchers::image_packs(client.clone(), events())),605616
),@@ -815,20 +826,11 @@ impl Engine {
815826
),816827
Command::Timeline(command) => self.route_timeline(command, reply),817828
Command::EventContext(command) => {818
- let room_id = match &command {819
- crate::EventContextCommand::Fetch { room_id, .. }820
- | crate::EventContextCommand::Before { room_id, .. }821
- | crate::EventContextCommand::After { room_id, .. } => room_id,822
- };823
- let Some(room) = client.get_room(room_id) else {824
- reply.done(Err(Error::room_unknown()));825
- return None;826
- };827
- let current_user_id = client.user_id().map(ToOwned::to_owned);828
- let media = self.media.clone();829
+ let (client, media, contexts) =830
+ (client.clone(), self.media.clone(), self.contexts.clone());829831
self.tasks830832
.spawn(TaskContext::Command("event context"), async move {831
- reply.send(context::execute(room, current_user_id, media, command).await);833
+ reply.send(context::execute(client, media, contexts, command).await);832834
});833835
}834836
Command::Mentions(command) => {@@ -1146,6 +1148,7 @@ impl Engine {
11461148
}11471149
self.tasks.shutdown().await;11481150
self.rooms.clear();1151
+ self.contexts.clear().await;11491152
self.media.clear().await;11501153
self.port11511154
.connection_sender()@@ -417,7 +417,7 @@ async fn start_watcher(
417417
let current_user_id = context.current_user_id.clone();418418
let (normalised, mut hidden) =419419
normalise_timeline(items.iter(), current_user_id.as_deref(), scope);420
- context.media.register(normalised.media).await;420
+ context.media.register(scope, normalised.media).await;421421
context422422
.events423423
.send(Event::TimelineReset {@@ -462,7 +462,7 @@ async fn start_watcher(
462462
&scope,463463
&mut hidden,464464
);465
- media.register(normalised.media).await;465
+ media.register(&scope, normalised.media).await;466466
replies.fetch(normalised.missing_replies);467467
if normalised.diffs.is_empty() {468468
continue;@@ -53,6 +53,7 @@ pub enum ErrorKind {
5353
RoomUnknown,5454
RoomNotOpen,5555
TimelineNotOpen,56
+ EventContextNotOpen,5657
EventNotFound,5758
RoomBusy,5859
QueueFull,@@ -57,8 +57,8 @@ pub use mirror::{
5757
pub use model::{5858
AccountCommand, AttachmentEncryption, AttachmentUpload, Change, Command, CommandOutput,5959
ConnectionState, CreateRoomOptions, EncryptionCommand, EncryptionState, EngineErrorKind,60
- EngineFailure, Event, EventContext, EventContextCommand, EventContextPage, EventRef,61
- HistoryState, ImagePack, ImagePackSource, ImagePacks, KeyBackupState, LatestEvent,60
+ EngineFailure, Event, EventContext, EventContextCommand, EventContextPage, EventContextSession,61
+ EventRef, HistoryState, ImagePack, ImagePackSource, ImagePacks, KeyBackupState, LatestEvent,6262
LifecycleCommand, LinkPreview, LinkPreviewImage, Media, MediaCommand, MediaDescriptor,6363
MediaFile, MediaId, MediaKind, MediaLocation, MediaSize, MemberFilter, MemberMembership,6464
MemberSummary, MembershipChange, Message, MessageContent, MessageEditContent, MessageKind,@@ -30,7 +30,7 @@ use crate::{
3030
MentionGapReason, MentionKey, MentionReason, MentionsCommand, MentionsPage, MentionsUpdate,3131
TimelineContent, TimelineItem,3232
engine::{MediaService, MentionsStore, context, executor::EngineBoxFuture},33
- normalise::{MediaRegistration, normalise_observed},33
+ normalise::{MediaRegistration, MediaScope, normalise_observed},3434
transport::EventSender,3535
};3636
@@ -60,6 +60,8 @@ pub(crate) struct Service {
6060
client: Client,6161
account: OwnedUserId,6262
media: Arc<MediaService>,63
+ /// as many media as the inbox keeps events64
+ media_scope: MediaScope,6365
store: Option<MentionsStore>,6466
data: Data,6567
sync: SyncState,@@ -231,8 +233,10 @@ impl Service {
231233
data.gap(None, *reason, None);232234
}233235
let page = data.page(&account, None, 50)?;236
+ let media_scope = MediaScope::bounded(config.max_mention_context_events);234237
media235238
.register(239
+ &media_scope,236240
page.entries237241
.iter()238242
.filter_map(|entry| data.events.get(&id(&entry.key)))@@ -247,6 +251,7 @@ impl Service {
247251
client,248252
account,249253
media,254
+ media_scope,250255
store,251256
data,252257
sync,@@ -291,6 +296,7 @@ impl Service {
291296
};292297
self.media293298
.register(299
+ &self.media_scope,294300
page.entries295301
.iter()296302
.filter_map(|entry| self.data.events.get(&id(&entry.key)))@@ -387,6 +393,7 @@ impl Service {
387393
let page = self.data.page(&self.account, cursor.as_deref(), limit)?;388394
self.media389395
.register(396
+ &self.media_scope,390397
page.entries391398
.iter()392399
.filter_map(|entry| self.data.events.get(&id(&entry.key)))@@ -398,7 +405,7 @@ impl Service {
398405
}399406
MentionsCommand::Context(key) => {400407
let (context, media) = self.data.context(&key)?;401
- self.media.register(media).await;408
+ self.media.register(&self.media_scope, media).await;402409
Ok(CommandOutput::MentionContext(context))403410
}404411
MentionsCommand::RetryContext(key) => {@@ -429,7 +436,7 @@ impl Service {
429436
});430437
self.apply(next).await?;431438
let (context, media) = self.data.context(&key)?;432
- self.media.register(media).await;439
+ self.media.register(&self.media_scope, media).await;433440
Ok(CommandOutput::MentionContext(context))434441
}435442
MentionsCommand::SetRead { keys, read } => {@@ -1007,6 +1014,7 @@ impl Service {
10071014
if self.dirty && let Err(error) = self.flush().await {10081015
tracing::error!(%error, "saving the mentions inbox at shutdown failed");10091016
}1017
+ self.media.release(&self.media_scope).await;10101018
let _ = reply.send(());10111019
return;10121020
}@@ -113,7 +113,7 @@ async fn service(client: &Client, store: Option<Result<MentionsStore, Error>>) -
113113
let media = Arc::new(MediaService::new(114114
client.clone(),115115
EngineConfig::default().max_media_bytes,116
- EngineConfig::default().max_media_sources,116
+ EngineConfig::default().max_context_media,117117
));118118
let (_, mut service, _) = Service::new(client.clone(), media, store, EngineConfig::default())119119
.await@@ -412,7 +412,7 @@ async fn service_recovers_notification_history_into_durable_unread_inbox() {
412412
let media = Arc::new(MediaService::new(413413
client.clone(),414414
EngineConfig::default().max_media_bytes,415
- EngineConfig::default().max_media_sources,415
+ EngineConfig::default().max_context_media,416416
));417417
let (handle, service, mut updates) = Service::new(418418
client.clone(),@@ -504,7 +504,7 @@ async fn live_context_survives_capture_then_edit_and_redaction() {
504504
let media = Arc::new(MediaService::new(505505
client.clone(),506506
EngineConfig::default().max_media_bytes,507
- EngineConfig::default().max_media_sources,507
+ EngineConfig::default().max_context_media,508508
));509509
let (_handle, mut service, _updates) =510510
Service::new(client, media, None, EngineConfig::default())@@ -622,7 +622,7 @@ async fn explicit_recovery_rechecks_server_support() {
622622
let media = Arc::new(MediaService::new(623623
client.clone(),624624
config.max_media_bytes,625
- config.max_media_sources,625
+ config.max_context_media,626626
));627627
let (_, mut service, _) = Service::new(client, media, None, config).await.unwrap();628628
service.data.coverage.historical_supported = Some(false);@@ -37,6 +37,8 @@ pub enum Command {
3737
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]3838
#[non_exhaustive]3939
pub enum EventContextCommand {40
+ /// opens a session that owns the media of everything it hands out. at most 32 stay41
+ /// open, opening another closes the oldest, and leaving the room closes its sessions4042
Fetch {4143
room_id: OwnedRoomId,4244
event_id: OwnedEventId,@@ -44,15 +46,17 @@ pub enum EventContextCommand {
4446
after_limit: u16,4547
},4648
Before {47
- room_id: OwnedRoomId,49
+ context_id: String,4850
limit: u16,4951
token: String,5052
},5153
After {52
- room_id: OwnedRoomId,54
+ context_id: String,5355
limit: u16,5456
token: String,5557
},58
+ /// frees the session's media. its rows' media ids stop resolving59
+ Close { context_id: String },5660
}5761
5862
#[derive(Debug)]@@ -668,7 +672,7 @@ pub enum CommandOutput {
668672
shortcodes: Vec<String>,669673
skipped: Vec<String>,670674
},671
- EventContext(EventContext),675
+ EventContext(EventContextSession),672676
EventContextPage(EventContextPage),673677
MentionsPage(crate::MentionsPage),674678
MentionContext(crate::SavedMentionContext),@@ -685,6 +689,16 @@ pub struct EventContext {
685689
pub end_token: Option<String>,686690
}687691
692
+/// a session's media stay resolvable until it closes, and past `EngineConfig::max_context_media`693
+/// its least recently used are dropped694
+#[derive(Clone, Debug, Eq, PartialEq)]695
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]696
+#[non_exhaustive]697
+pub struct EventContextSession {698
+ pub context_id: String,699
+ pub context: EventContext,700
+}701
+688702
#[derive(Clone, Debug, Eq, PartialEq)]689703
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]690704
#[non_exhaustive]@@ -24,22 +24,38 @@ static NEXT_MEDIA_SCOPE: AtomicU64 = AtomicU64::new(1);
2424
/// echo replacement, while transaction IDs disappear. Scoping the recycled ID2525
/// by a process-wide nonce keeps media IDs stable across replacement without2626
/// colliding across rooms, threads, or reopened sessions.27
+///28
+/// It is also the owner its registrations belong to. Media stays registered while any29
+/// scope that registered it is open, and a bounded scope drops its least recently used30
+/// entries past its limit without touching anyone else's.2731
#[derive(Clone, Debug)]2832
pub(crate) struct MediaScope {2933
nonce: u64,34
+ limit: Option<usize>,3035
}3136
3237
impl MediaScope {38
+ /// for a timeline, which holds what is on screen and so is never trimmed3339
pub(crate) fn new() -> Self {3440
Self {3541
nonce: NEXT_MEDIA_SCOPE.fetch_add(1, Ordering::Relaxed),42
+ limit: None,3643
}3744
}3845
39
- pub(crate) fn owns(&self, id: &MediaId) -> bool {40
- id.as_str()41
- .split_once('#')42
- .is_some_and(|(nonce, _)| nonce.parse() == Ok(self.nonce))46
+ pub(crate) fn bounded(limit: usize) -> Self {47
+ Self {48
+ limit: Some(limit.max(1)),49
+ ..Self::new()50
+ }51
+ }52
+53
+ pub(crate) const fn nonce(&self) -> u64 {54
+ self.nonce55
+ }56
+57
+ pub(crate) const fn limit(&self) -> Option<usize> {58
+ self.limit4359
}4460
}4561
@@ -28,15 +28,15 @@ use tokio::sync::{OnceCell, mpsc, oneshot, watch};
2828
2929
use crate::{3030
AccountCommand, AttachmentUpload, Command, CommandOutput, ConnectionState, CreateRoomOptions,31
- EncryptionCommand, EncryptionState, Error, ErrorKind, Event, EventContext, EventContextCommand,32
- EventContextPage, EventRef, HistoryRepairCommand, ImagePackSource, ImagePacks, InteractiveAuth,33
- InteractiveAuthOutcome, LifecycleCommand, LinkPreview, Media, MediaCommand, MediaId,34
- MediaLocation, MediaSize, MemberFilter, MemberSummary, MentionKey, MentionsBoundary,35
- MentionsCommand, MentionsPage, MessageContent, MessageEditContent, NotificationCommand,36
- OwnDevice, PackCommand, PackImageInfo, PackUsage, Patch, PowerLevelChanges, Profile,37
- PushNotification, PushNotificationEvent, PushNotificationStatus, RoomCommand, RoomListFilter,38
- RoomNotificationMode, RoomNotificationSettings, RoomPowerLevels, RoomSummary,39
- SavedMentionContext, SpaceChild, TimelineCommand, TimelineId, VerificationAction,31
+ EncryptionCommand, EncryptionState, Error, ErrorKind, Event, EventContextCommand,32
+ EventContextPage, EventContextSession, EventRef, HistoryRepairCommand, ImagePackSource,33
+ ImagePacks, InteractiveAuth, InteractiveAuthOutcome, LifecycleCommand, LinkPreview, Media,34
+ MediaCommand, MediaId, MediaLocation, MediaSize, MemberFilter, MemberSummary, MentionKey,35
+ MentionsBoundary, MentionsCommand, MentionsPage, MessageContent, MessageEditContent,36
+ NotificationCommand, OwnDevice, PackCommand, PackImageInfo, PackUsage, Patch,37
+ PowerLevelChanges, Profile, PushNotification, PushNotificationEvent, PushNotificationStatus,38
+ RoomCommand, RoomListFilter, RoomNotificationMode, RoomNotificationSettings, RoomPowerLevels,39
+ RoomSummary, SavedMentionContext, SpaceChild, TimelineCommand, TimelineId, VerificationAction,4040
VerificationTarget,4141
encryption::encryption_state,4242
engine::{MediaService, StoreLock},@@ -358,13 +358,14 @@ impl ClientHandle {
358358
.await359359
}360360
361
+ /// close the session when the view goes, or its media stay registered361362
pub async fn event_context(362363
&self,363364
room_id: &RoomId,364365
event_id: &EventId,365366
before_limit: u16,366367
after_limit: u16,367
- ) -> Result<EventContext, Error> {368
+ ) -> Result<EventContextSession, Error> {368369
match self369370
.execute(Command::EventContext(EventContextCommand::Fetch {370371
room_id: room_id.to_owned(),@@ -374,19 +375,19 @@ impl ClientHandle {
374375
}))375376
.await?376377
{377
- CommandOutput::EventContext(context) => Ok(context),378
+ CommandOutput::EventContext(session) => Ok(session),378379
output => Err(unexpected("event context", &output)),379380
}380381
}381382
382383
pub async fn event_context_before(383384
&self,384
- room_id: &RoomId,385
+ context_id: &str,385386
token: &str,386387
limit: u16,387388
) -> Result<EventContextPage, Error> {388389
self.event_context_page(EventContextCommand::Before {389
- room_id: room_id.to_owned(),390
+ context_id: context_id.to_owned(),390391
limit,391392
token: token.to_owned(),392393
})@@ -395,18 +396,25 @@ impl ClientHandle {
395396
396397
pub async fn event_context_after(397398
&self,398
- room_id: &RoomId,399
+ context_id: &str,399400
token: &str,400401
limit: u16,401402
) -> Result<EventContextPage, Error> {402403
self.event_context_page(EventContextCommand::After {403
- room_id: room_id.to_owned(),404
+ context_id: context_id.to_owned(),404405
limit,405406
token: token.to_owned(),406407
})407408
.await408409
}409410
411
+ pub async fn close_event_context(&self, context_id: &str) -> Result<(), Error> {412
+ self.run(Command::EventContext(EventContextCommand::Close {413
+ context_id: context_id.to_owned(),414
+ }))415
+ .await416
+ }417
+410418
async fn event_context_page(411419
&self,412420
command: EventContextCommand,@@ -717,12 +717,22 @@ async fn event_context_uses_independent_direction_limits_and_saved_mention_conte
717717
.count();718718
assert_eq!(after_saved_request_count, before_saved_request_count);719719
720
- let context = handle720
+ let session = handle721721
.event_context(room_id, context_id, 2, 3)722722
.await723723
.unwrap();724
+ let context = &session.context;724725
assert_eq!(context.start_token.as_deref(), Some("manual-before-next"));725726
assert_eq!(context.end_token.as_deref(), Some("manual-after-next"));727
+ handle728
+ .close_event_context(&session.context_id)729
+ .await730
+ .unwrap();731
+ let closed = handle732
+ .event_context_before(&session.context_id, "manual-before-next", 5)733
+ .await734
+ .unwrap_err();735
+ assert_eq!(closed.kind, macltok::ErrorKind::EventContextNotOpen);726736
727737
let requests = server.server().received_requests().await.unwrap();728738
let context_limits: Vec<_> = requests