From ab0ccae71fdc5e50083194dac7fdeadf1aa2c6ea Mon Sep 17 00:00:00 2001 From: reverse Date: Wed, 19 Aug 2026 02:09:36 +0000 Subject: [PATCH] =?UTF-8?q?server:=20scrub=20a=20departed=20user's=20pendi?= =?UTF-8?q?ng=20invites=20via=20a=20User.invited=20reverse=20index=20inste?= =?UTF-8?q?ad=20of=20scanning=20every=20channel=20on=20the=20network=20per?= =?UTF-8?q?=20quit=20=E2=80=94=20the=20old=20O(channels)-per-quit=20path?= =?UTF-8?q?=20was=20O(channels*quits)=20on=20a=20netsplit;=20the=20index?= =?UTF-8?q?=20is=20maintained=20at=20the=204=20invite=20add/remove=20sites?= =?UTF-8?q?=20(INVITE=20cmd,=20S2S=20INVITE,=20join-consume,=20UNINVITE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/channels.rs | 1 + src/coremods/core_channel.rs | 6 ++++++ src/link.rs | 5 +++++ src/modules/reputation.rs | 1 + src/s2s_sim.rs | 1 + src/server.rs | 10 ++++++++-- src/users.rs | 1 + 7 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/channels.rs b/src/channels.rs index 4002886..df0b96d 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -907,6 +907,7 @@ impl Server { ch.recent_kicks.remove(&uid); // they got back in; clear any +J rejoin timer if let Some(u) = self.users.get_mut(&uid) { u.channels.insert(key.clone()); + u.invited.remove(&key); // invite consumed — drop it from the reverse index } // chancreate: snotice when a brand-new channel comes into being if is_new diff --git a/src/coremods/core_channel.rs b/src/coremods/core_channel.rs index 7700b11..65891ba 100644 --- a/src/coremods/core_channel.rs +++ b/src/coremods/core_channel.rs @@ -323,6 +323,9 @@ impl Command for Invite { if let Some(ch) = s.channels.get_mut(&key) { ch.invites.insert(tuid); } + if let Some(u) = s.users.get_mut(&tuid) { + u.invited.insert(key.clone()); // reverse index for O(1) quit scrub + } let who = s.users[&tuid].nick.clone(); s.numeric(uid, RPL_INVITING, &format!("{who} {chan}")); let prefix = s.users[&uid].prefix(); @@ -390,6 +393,9 @@ impl Command for Uninvite { .get_mut(&key) .map(|ch| ch.invites.remove(&tuid)) .unwrap_or(false); + if let Some(u) = s.users.get_mut(&tuid) { + u.invited.remove(&key); // keep the reverse index in sync + } let who = s.users[&tuid].nick.clone(); let word = if removed { "is no longer invited to" diff --git a/src/link.rs b/src/link.rs index 1abdb9f..66570bd 100644 --- a/src/link.rs +++ b/src/link.rs @@ -1344,6 +1344,9 @@ impl Server { if let Some(ch) = self.channels.get_mut(&key) { ch.invites.insert(luid); } + if let Some(u) = self.users.get_mut(&luid) { + u.invited.insert(key.clone()); // reverse index for O(1) quit scrub + } let prefix = self.uuid_prefix(&src).unwrap_or_else(|| src.clone()); let nick = self.users.get(&luid).map(|u| u.nick.clone()).unwrap_or_default(); self.send(luid, format!(":{prefix} INVITE {nick} :{chan}")); @@ -2768,6 +2771,7 @@ mod tests { caps: Caps::default(), sasl_mech: None, channels: HashSet::default(), + invited: HashSet::default(), watch: Vec::new(), monitor: Vec::new(), silence: Vec::new(), @@ -2855,6 +2859,7 @@ mod tests { caps, sasl_mech: None, channels: HashSet::default(), + invited: HashSet::default(), watch: Vec::new(), monitor: Vec::new(), silence: Vec::new(), diff --git a/src/modules/reputation.rs b/src/modules/reputation.rs index 63d3ada..fb98f72 100644 --- a/src/modules/reputation.rs +++ b/src/modules/reputation.rs @@ -392,6 +392,7 @@ mod tests { caps: Caps::default(), sasl_mech: None, channels: chans, + invited: crate::map::HashSet::default(), watch: Vec::new(), monitor: Vec::new(), silence: Vec::new(), diff --git a/src/s2s_sim.rs b/src/s2s_sim.rs index 7ebc9d7..7ad52e4 100644 --- a/src/s2s_sim.rs +++ b/src/s2s_sim.rs @@ -92,6 +92,7 @@ impl Node { caps: Caps::default(), sasl_mech: None, channels: HashSet::default(), + invited: HashSet::default(), watch: Vec::new(), monitor: Vec::new(), silence: Vec::new(), diff --git a/src/server.rs b/src/server.rs index 220edcc..6d15f86 100644 --- a/src/server.rs +++ b/src/server.rs @@ -373,6 +373,7 @@ impl Server { caps: Caps::default(), sasl_mech: None, channels: HashSet::default(), + invited: HashSet::default(), watch: Vec::new(), monitor: Vec::new(), silence: Vec::new(), @@ -724,8 +725,12 @@ impl Server { // Scrub any pending +i invite for this user from channels they never joined // (a member consumes their invite on join; a never-joined invite for a now- // departed uid would otherwise linger forever on a persistent channel). - for ch in self.channels.values_mut() { - ch.invites.remove(&uid); + // The `invited` reverse index gives the exact channels, so this is O(pending + // invites) rather than a scan of every channel on the network. + for key in &user.invited { + if let Some(ch) = self.channels.get_mut(key) { + ch.invites.remove(&uid); + } } self.channels.retain(|_, c| c.keep_alive()); self.watch_notify_offline(&user.nick); // tell WATCH/MONITOR watchers @@ -1424,6 +1429,7 @@ mod tests { caps: Caps::default(), sasl_mech: None, channels: HashSet::default(), + invited: HashSet::default(), watch: Vec::new(), monitor: Vec::new(), silence: Vec::new(), diff --git a/src/users.rs b/src/users.rs index 09ffb82..7c1e20d 100644 --- a/src/users.rs +++ b/src/users.rs @@ -248,6 +248,7 @@ pub struct User { pub caps: Caps, // enabled IRCv3 capabilities pub sasl_mech: Option, // SASL mechanism chosen, mid-handshake pub channels: HashSet, // lowercased channel keys + pub invited: HashSet, // channels this user has a pending +i invite to (reverse index) pub watch: Vec, // WATCH list — lowercased nicks pub monitor: Vec, // MONITOR list — lowercased nicks pub silence: Vec, // SILENCE masks — nick!user@host globs