Commit 1f875b103b27
tarball7 files changed+148-51
@@ -13,13 +13,14 @@ use serde_json::Value;
1313
use tokio::sync::{Mutex, broadcast::error::RecvError};1414
1515
use crate::{16
- Error, ErrorKind, EventContext, EventContextCommand, EventContextPage, EventContextSession,17
- MediaId, Sender, TimelineEvent, TimelineItem,16
+ Error, ErrorKind, Event, EventContext, EventContextCloseReason, EventContextCommand,17
+ EventContextPage, EventContextSession, MediaId, Sender, TimelineEvent, TimelineItem,1818
engine::MediaService,1919
normalise::{2020
MediaRegistration, MediaScope, apply_profiles, normalise_context, normalise_observed,2121
resolve_replies,2222
},23
+ transport::EventSender,2324
};2425
2526
const MAX_CONTEXT_LIMIT: u16 = 50;@@ -27,9 +28,9 @@ const MAX_CONTEXT_LIMIT: u16 = 50;
2728
const MAX_SESSIONS: usize = 32;2829
2930
/// open event contexts, oldest first, each owning the media its pages registered30
-#[derive(Default)]3131
pub(crate) struct Sessions {3232
open: Mutex<VecDeque<Session>>,33
+ events: EventSender,3334
}3435
3536
struct Session {@@ -38,36 +39,61 @@ struct Session {
3839
scope: MediaScope,3940
}4041
42
+impl Session {43
+ /// media ids carry the session, so two owners of one event never share an entry44
+ fn media_prefix(scope: &MediaScope) -> String {45
+ format!("{}#", scope.nonce())46
+ }47
+}48
+4149
impl Sessions {50
+ pub(crate) fn new(events: EventSender) -> Self {51
+ Self {52
+ open: Mutex::default(),53
+ events,54
+ }55
+ }56
+4257
async fn open(4358
&self,4459
media: &MediaService,4560
room_id: OwnedRoomId,61
+ scope: MediaScope,4662
registrations: Vec<MediaRegistration>,4763
) -> String {48
- let scope = media.context_scope();4964
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;65
+ let mut evicted = Vec::new();66
+ {67
+ let mut open = self.open.lock().await;68
+ media.register(&scope, registrations).await;69
+ open.push_back(Session {70
+ id: id.clone(),71
+ room_id,72
+ scope,73
+ });74
+ while open.len() > MAX_SESSIONS {75
+ if let Some(oldest) = open.pop_front() {76
+ media.release(&oldest.scope).await;77
+ evicted.push(oldest.id);78
+ }6079
}6180
}81
+ self.announce(evicted, EventContextCloseReason::Evicted)82
+ .await;6283
id6384
}6485
65
- async fn room(&self, id: &str) -> Result<OwnedRoomId, Error> {86
+ /// the room and the media prefix the session's pages use87
+ async fn find(&self, id: &str) -> Result<(OwnedRoomId, String), Error> {6688
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)89
+ let session = open90
+ .iter()91
+ .find(|session| session.id == id)92
+ .ok_or_else(not_open)?;93
+ Ok((94
+ session.room_id.clone(),95
+ Session::media_prefix(&session.scope),96
+ ))7197
}7298
7399
/// under the lock, so a page that lands after its close registers nothing@@ -99,19 +125,37 @@ impl Sessions {
99125
}100126
101127
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);128
+ let mut closed = Vec::new();129
+ {130
+ let mut open = self.open.lock().await;131
+ let mut kept = VecDeque::with_capacity(open.len());132
+ for session in open.drain(..) {133
+ let left = client.get_room(&session.room_id).is_none_or(|room| {134
+ matches!(room.state(), RoomState::Left | RoomState::Banned)135
+ });136
+ if left {137
+ media.release(&session.scope).await;138
+ closed.push(session.id);139
+ } else {140
+ kept.push_back(session);141
+ }142
+ }143
+ *open = kept;144
+ }145
+ self.announce(closed, EventContextCloseReason::RoomLeft)146
+ .await;147
+ }148
+149
+ async fn announce(&self, ids: Vec<String>, reason: EventContextCloseReason) {150
+ for context_id in ids {151
+ let event = Event::EventContextClosed {152
+ context_id,153
+ reason: reason.clone(),154
+ };155
+ if self.events.send(event).await.is_err() {156
+ return;112157
}113158
}114
- *open = kept;115159
}116160
117161
/// the media service is cleared beside this, so nothing is released one by one@@ -157,15 +201,17 @@ pub(crate) async fn execute(
157201
before_limit,158202
after_limit,159203
} => {160
- let (context, registrations, _) = capture(204
+ let scope = media.context_scope();205
+ let (context, registrations, _) = capture_in(161206
room(&room_id)?,162207
current_user_id,163208
event_id,164209
before_limit,165210
after_limit,211
+ &Session::media_prefix(&scope),166212
)167213
.await?;168
- let context_id = sessions.open(&media, room_id, registrations).await;214
+ let context_id = sessions.open(&media, room_id, scope, registrations).await;169215
return Ok(crate::CommandOutput::EventContext(EventContextSession {170216
context_id,171217
context,@@ -199,7 +245,8 @@ pub(crate) async fn execute(
199245
"context pagination token is empty",200246
));201247
}202
- let room = room(&sessions.room(&context_id).await?)?;248
+ let (room_id, media_prefix) = sessions.find(&context_id).await?;249
+ let room = room(&room_id)?;203250
let side = fetch_side(&room, Some(&token), limit, backwards).await?;204251
let mut page = Assembly::new(205252
&room,@@ -207,6 +254,7 @@ pub(crate) async fn execute(
207254
&side.values,208255
&side.highlights,209256
None,257
+ &media_prefix,210258
)211259
.await;212260
let events = page.take(0..side.values.len());@@ -224,6 +272,17 @@ pub(crate) async fn capture(
224272
before_limit: u16,225273
after_limit: u16,226274
) -> Result<(EventContext, Vec<MediaRegistration>, Vec<Value>), Error> {275
+ capture_in(room, current_user_id, event_id, before_limit, after_limit, "").await276
+}277
+278
+async fn capture_in(279
+ room: Room,280
+ current_user_id: Option<OwnedUserId>,281
+ event_id: OwnedEventId,282
+ before_limit: u16,283
+ after_limit: u16,284
+ media_prefix: &str,285
+) -> Result<(EventContext, Vec<MediaRegistration>, Vec<Value>), Error> {227286
validate_limit(before_limit)?;228287
validate_limit(after_limit)?;229288
let raw = room@@ -264,6 +323,7 @@ pub(crate) async fn capture(
264323
&values,265324
&highlights,266325
Some(target_index),326
+ media_prefix,267327
)268328
.await;269329
let events_before = page.take(0..target_index);@@ -357,8 +417,15 @@ impl Assembly {
357417
values: &[Value],358418
highlights: &[bool],359419
keep: Option<usize>,420
+ media_prefix: &str,360421
) -> Self {361
- let page = normalise_context(values, keep, current_user_id, room.room_id());422
+ let page = normalise_context(423
+ values,424
+ keep,425
+ current_user_id,426
+ room.room_id(),427
+ media_prefix,428
+ );362429
let mut items = page.items;363430
for (index, item) in &mut items {364431
if let TimelineItem::Event(event) = item {@@ -386,7 +453,7 @@ impl Assembly {
386453
let fetched = join_all(387454
missing388455
.into_iter()389
- .map(|event_id| fetch_reply(room, current_user_id, event_id)),456
+ .map(|event_id| fetch_reply(room, current_user_id, event_id, media_prefix)),390457
)391458
.await;392459
for (event, registrations) in fetched.into_iter().flatten() {@@ -433,6 +500,7 @@ async fn fetch_reply(
433500
room: &Room,434501
current_user_id: Option<&ruma::UserId>,435502
event_id: OwnedEventId,503
+ media_prefix: &str,436504
) -> Option<(TimelineEvent, Vec<MediaRegistration>)> {437505
let event = room438506
.load_or_fetch_event(&event_id, None)@@ -440,7 +508,7 @@ async fn fetch_reply(
440508
.inspect_err(|error| tracing::debug!(%event_id, %error, "fetching a reply target failed"))441509
.ok()?;442510
let value: Value = serde_json::from_str(event.raw().json().get()).ok()?;443
- let identity = format!("{}\n{event_id}", room.room_id());511
+ let identity = format!("{media_prefix}{}\n{event_id}", room.room_id());444512
match normalise_observed(&value, current_user_id, &identity)? {445513
(TimelineItem::Event(event), media) => Some((*event, media)),446514
_ => None,@@ -778,14 +778,19 @@ struct ScopeEntries {
778778
779779
impl ScopeEntries {780780
fn touch(&mut self, id: &MediaId) {781
- if self.limit.is_none() {781
+ let Some(limit) = self.limit else {782782
self.stamps.insert(id.clone(), 0);783783
return;784
- }784
+ };785785
let stamp = self.next_stamp;786786
self.next_stamp += 1;787787
self.stamps.insert(id.clone(), stamp);788788
self.order.push_back((id.clone(), stamp));789
+ if self.order.len() > limit.saturating_mul(4) {790
+ let stamps = &self.stamps;791
+ self.order792
+ .retain(|(id, stamp)| stamps.get(id) == Some(stamp));793
+ }789794
}790795
791796
/// what fell out past the limit@@ -803,11 +808,6 @@ impl ScopeEntries {
803808
dropped.push(id);804809
}805810
}806
- if self.order.len() > limit.saturating_mul(4) {807
- let stamps = &self.stamps;808
- self.order809
- .retain(|(id, stamp)| stamps.get(id) == Some(stamp));810
- }811811
dropped812812
}813813
}@@ -404,12 +404,12 @@ impl Engine {
404404
let (notifications, notification_queue) = mpsc::channel(config.command_capacity);405405
let (encryption, encryption_queue) = mpsc::channel(config.command_capacity);406406
let engine = Self {407
+ contexts: Arc::new(context::Sessions::new(port.event_sender())),407408
client,408409
sync,409410
port,410411
config,411412
media,412
- contexts: Arc::default(),413413
tasks: TaskRegistry {414414
tasks: TaskSet::new(),415415
},@@ -57,7 +57,7 @@ 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, EventContextSession,60
+ EngineFailure, Event, EventContext, EventContextCloseReason, EventContextCommand, EventContextPage, EventContextSession,6161
EventRef, HistoryState, ImagePack, ImagePackSource, ImagePacks, KeyBackupState, LatestEvent,6262
LifecycleCommand, LinkPreview, LinkPreviewImage, Media, MediaCommand, MediaDescriptor,6363
MediaFile, MediaId, MediaKind, MediaLocation, MediaSize, MemberFilter, MemberMembership,@@ -20,7 +20,7 @@ use tokio::sync::broadcast;
2020
2121
use crate::{2222
ClientHandle, Command, ConnectionState, EncryptionState, EngineFailure, ErrorKind, Event,23
- HistoryRepairReport, HistoryState, ImagePacks, RecentHistoryState, RoomSummary,23
+ EventContextCloseReason, HistoryRepairReport, HistoryState, ImagePacks, RecentHistoryState, RoomSummary,2424
TimelineCommand, TimelineDiff, TimelineId, TimelineItem, TypingState, UploadOutcome,2525
VerificationState, VerificationTarget, engine::executor::EngineBoxFuture,2626
transport::PublicationRegistration,@@ -145,6 +145,10 @@ pub enum StateChange {
145145
},146146
HistoryRepair(HistoryRepairReport),147147
Typing(TypingState),148
+ EventContextClosed {149
+ context_id: String,150
+ reason: EventContextCloseReason,151
+ },148152
Upload {149153
transaction_id: OwnedTransactionId,150154
upload: UploadState,@@ -407,6 +411,9 @@ impl Mirror {
407411
})408412
});409413
}414
+ Event::EventContextClosed { context_id, reason } => {415
+ self.publish(|_| Some(StateChange::EventContextClosed { context_id, reason }));416
+ }410417
Event::TypingChanged(typing) => self.publish(|state| {411418
if typing.users.is_empty() {412419
state.typing.remove(&typing.room_id);@@ -38,7 +38,8 @@ pub enum Command {
3838
#[non_exhaustive]3939
pub enum EventContextCommand {4040
/// 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 sessions41
+ /// open, opening another closes the oldest, and leaving the room closes its sessions,42
+ /// both told with `Event::EventContextClosed`4243
Fetch {4344
room_id: OwnedRoomId,4445
event_id: OwnedEventId,@@ -690,6 +691,14 @@ pub struct EventContext {
690691
pub end_token: Option<String>,691692
}692693
694
+#[derive(Clone, Debug, Eq, PartialEq)]695
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]696
+pub enum EventContextCloseReason {697
+ /// opening a 33rd session closed the oldest698
+ Evicted,699
+ RoomLeft,700
+}701
+693702
/// a session's media stay resolvable until it closes, and past `EngineConfig::max_context_media`694703
/// its least recently used are dropped695704
#[derive(Clone, Debug, Eq, PartialEq)]@@ -930,6 +939,12 @@ pub enum Event {
930939
TimelineClosed {931940
timeline_id: TimelineId,932941
},942
+ /// an event context closed by something other than its own `Close`. its id answers943
+ /// `EventContextNotOpen` from now on944
+ EventContextClosed {945
+ context_id: String,946
+ reason: EventContextCloseReason,947
+ },933948
/// a room that stops being watched says so with an empty `users`934949
TypingChanged(TypingState),935950
MediaUploadProgress {@@ -20,12 +20,14 @@ struct Annotation<'a> {
2020
2121
/// raw events in timeline order, shown the way the live room timeline shows them. edits and2222
/// reactions fold into their targets within the page, and redactions, unmatched relations23
-/// and thread replies go. `keep` is never dropped23
+/// and thread replies go. `keep` is never dropped. `media_prefix` keeps one owner's media ids24
+/// apart from another's for the same event2425
pub(crate) fn normalise_context(2526
values: &[Value],2627
keep: Option<usize>,2728
current_user_id: Option<&UserId>,2829
room_id: &RoomId,30
+ media_prefix: &str,2931
) -> ContextPage {3032
let mut edits: HashMap<&str, Vec<&Value>> = HashMap::new();3133
let mut annotations: HashMap<&str, Vec<Annotation<'_>>> = HashMap::new();@@ -56,7 +58,12 @@ pub(crate) fn normalise_context(
5658
}5759
}5860
(Some("m.reaction"), ..) => {}59
- (_, Some("m.replace"), Some(target)) => edits.entry(target).or_default().push(value),61
+ // an edit still encrypted keeps its relation in the clear but has no new content62
+ (_, Some("m.replace"), Some(target)) => {63
+ if value.pointer("/content/m.new_content").is_some() {64
+ edits.entry(target).or_default().push(value);65
+ }66
+ }6067
(_, Some("m.thread"), _) => {}6168
_ => shown.push(index),6269
}@@ -89,7 +96,7 @@ pub(crate) fn normalise_context(
8996
.as_object_mut()?9097
.insert("m.replace".to_owned(), edit.clone());9198
}92
- let identity = format!("{room_id}\n{event_id}");99
+ let identity = format!("{media_prefix}{room_id}\n{event_id}");93100
let (mut item, registrations) = normalise_observed(&value, current_user_id, &identity)?;94101
media.extend(registrations);95102
if let (TimelineItem::Event(event), Some(annotations)) =@@ -232,7 +239,7 @@ mod tests {
232239
"content": {},233240
}),234241
];235
- let page = normalise_context(&values, None, Some(user_id!("@a:x")), room_id!("!room:x"));242
+ let page = normalise_context(&values, None, Some(user_id!("@a:x")), room_id!("!room:x"), "");236243
assert_eq!(page.items.len(), 1);237244
let TimelineItem::Event(event) = &page.items[0].1 else {238245
panic!("expected an event");