From 16fae5b23c00435e554638ae6d9e41be0712fa52 Mon Sep 17 00:00:00 2001 From: reverse Date: Wed, 19 Aug 2026 02:34:22 +0000 Subject: [PATCH] =?UTF-8?q?watch:=20notify=20WATCH/MONITOR=20via=20nick->w?= =?UTF-8?q?atchers=20reverse=20indexes=20(watch=5Fby/monitor=5Fby)=20inste?= =?UTF-8?q?ad=20of=20scanning=20every=20user=20on=20each=20online/offline/?= =?UTF-8?q?nick-change=20flip=20=E2=80=94=20O(watchers)=20not=20O(users);?= =?UTF-8?q?=20maintained=20through=20centralized=20watch=5Findex=5F*/monit?= =?UTF-8?q?or=5Findex=5F*=20helpers=20+=20quit=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/coremods/core_watch.rs | 28 ++------ src/server.rs | 24 +++++++ src/watch.rs | 137 ++++++++++++++++++++++++++++++++++--- 3 files changed, 158 insertions(+), 31 deletions(-) diff --git a/src/coremods/core_watch.rs b/src/coremods/core_watch.rs index 21d0c22..a849570 100644 --- a/src/coremods/core_watch.rs +++ b/src/coremods/core_watch.rs @@ -58,11 +58,7 @@ fn watch_add(s: &mut Server, uid: Uid, nick: &str) { ); return; } - if let Some(u) = s.users.get_mut(&uid) { - if !u.watch.contains(&low) { - u.watch.push(low); - } - } + s.watch_index_add(uid, low); watch_status(s, uid, nick); } @@ -94,9 +90,7 @@ impl Command for Watch { for tok in params.iter().flat_map(|p| p.split_whitespace()) { match tok { "C" | "c" => { - if let Some(u) = s.users.get_mut(&uid) { - u.watch.clear(); - } + s.watch_index_clear(uid); s.numeric(uid, RPL_ENDOFWATCHLIST, ":End of WATCH list"); } "S" | "s" => { @@ -126,9 +120,7 @@ impl Command for Watch { _ if tok.starts_with('+') => watch_add(s, uid, &tok[1..]), _ if tok.starts_with('-') => { let low = tok[1..].to_ascii_lowercase(); - if let Some(u) = s.users.get_mut(&uid) { - u.watch.retain(|n| n != &low); - } + s.watch_index_remove(uid, &low); s.numeric( uid, RPL_WATCHOFF, @@ -199,11 +191,7 @@ impl Command for Monitor { ); continue; } - if let Some(u) = s.users.get_mut(&uid) { - if !u.monitor.contains(&low) { - u.monitor.push(low); - } - } + s.monitor_index_add(uid, low); added.push(t); } monitor_report(s, uid, &added); @@ -218,14 +206,12 @@ impl Command for Monitor { .collect() }) .unwrap_or_default(); - if let Some(u) = s.users.get_mut(&uid) { - u.monitor.retain(|n| !targets.contains(n)); + for t in &targets { + s.monitor_index_remove(uid, t); } } "C" => { - if let Some(u) = s.users.get_mut(&uid) { - u.monitor.clear(); - } + s.monitor_index_clear(uid); } "L" => { let list = s diff --git a/src/server.rs b/src/server.rs index 24ce3f2..e501895 100644 --- a/src/server.rs +++ b/src/server.rs @@ -171,6 +171,11 @@ pub struct Server { /// the common case (count 0) skips the O(users) scan. Maintained solely through /// `accept_add`/`accept_remove` and the quit path. pub accepted_nicks: HashMap, + /// Reverse index (lowercased) nick -> uids WATCHing / MONITORing it, so an + /// online/offline flip notifies only the watchers instead of scanning every user. + /// Maintained solely through the `watch_index_*` / `monitor_index_*` helpers and quit. + pub watch_by: HashMap>, + pub monitor_by: HashMap>, // labeled-response: while Some((uid, buf)), that client's own responses are // diverted into `buf` instead of the socket, so `on_line` can wrap them with // the command's `label` (single tag, BATCH, or ACK). RefCell because the @@ -228,6 +233,8 @@ impl Server { raw_config: cfg.raw, config_gen: 0, accepted_nicks: HashMap::default(), + watch_by: HashMap::default(), + monitor_by: HashMap::default(), label_capture: RefCell::new(None), log: RefCell::new(LogState::default()), event_tx, @@ -726,6 +733,23 @@ impl Server { } } } + // drop this user from the WATCH/MONITOR reverse indexes + for n in &user.watch { + if let Some(set) = self.watch_by.get_mut(n) { + set.remove(&uid); + if set.is_empty() { + self.watch_by.remove(n); + } + } + } + for n in &user.monitor { + if let Some(set) = self.monitor_by.get_mut(n) { + set.remove(&uid); + if set.is_empty() { + self.monitor_by.remove(n); + } + } + } self.uuid_local.remove(&user.uuid); if user.registered { self.push_whowas( diff --git a/src/watch.rs b/src/watch.rs index 21bb461..935fdd1 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -37,15 +37,17 @@ impl Server { }) else { return; }; - for (&uid, u) in &self.users { - if u.watch.contains(&low) { + if let Some(set) = self.watch_by.get(&low) { + for &uid in set { self.numeric( uid, RPL_LOGON, &format!("{dnick} {ident} {host} {ts} :is now online"), ); } - if u.monitor.contains(&low) { + } + if let Some(set) = self.monitor_by.get(&low) { + for &uid in set { self.numeric(uid, RPL_MONONLINE, &format!(":{dnick}!{ident}@{host}")); } } @@ -56,11 +58,13 @@ impl Server { pub fn watch_notify_offline(&self, nick: &str) { let low = nick.to_ascii_lowercase(); let ts = now(); - for (&uid, u) in &self.users { - if u.watch.contains(&low) { + if let Some(set) = self.watch_by.get(&low) { + for &uid in set { self.numeric(uid, RPL_LOGOFF, &format!("{nick} * * {ts} :is now offline")); } - if u.monitor.contains(&low) { + } + if let Some(set) = self.monitor_by.get(&low) { + for &uid in set { self.numeric(uid, RPL_MONOFFLINE, &format!(":{nick}")); } } @@ -69,10 +73,123 @@ impl Server { /// How many users currently WATCH `nick` (for `WATCH S` stats). pub fn watchers_of(&self, nick: &str) -> usize { let low = nick.to_ascii_lowercase(); - self.users - .values() - .filter(|u| u.watch.contains(&low)) - .count() + self.watch_by.get(&low).map(|s| s.len()).unwrap_or(0) + } + + // ── WATCH/MONITOR list mutators — keep the reverse index in sync ────────── + + /// Add `nick_low` to `uid`'s WATCH list (if absent) and index it. + pub fn watch_index_add(&mut self, uid: Uid, nick_low: String) { + let added = self + .users + .get_mut(&uid) + .map(|u| { + if u.watch.contains(&nick_low) { + false + } else { + u.watch.push(nick_low.clone()); + true + } + }) + .unwrap_or(false); + if added { + self.watch_by.entry(nick_low).or_default().insert(uid); + } + } + + /// Remove `nick_low` from `uid`'s WATCH list and de-index it. + pub fn watch_index_remove(&mut self, uid: Uid, nick_low: &str) { + let removed = self + .users + .get_mut(&uid) + .map(|u| { + let before = u.watch.len(); + u.watch.retain(|n| n != nick_low); + before != u.watch.len() + }) + .unwrap_or(false); + if removed { + if let Some(set) = self.watch_by.get_mut(nick_low) { + set.remove(&uid); + if set.is_empty() { + self.watch_by.remove(nick_low); + } + } + } + } + + /// Clear `uid`'s whole WATCH list (WATCH C) and de-index every entry. + pub fn watch_index_clear(&mut self, uid: Uid) { + let nicks = self + .users + .get_mut(&uid) + .map(|u| std::mem::take(&mut u.watch)) + .unwrap_or_default(); + for n in nicks { + if let Some(set) = self.watch_by.get_mut(&n) { + set.remove(&uid); + if set.is_empty() { + self.watch_by.remove(&n); + } + } + } + } + + /// Add `nick_low` to `uid`'s MONITOR list (if absent) and index it. + pub fn monitor_index_add(&mut self, uid: Uid, nick_low: String) { + let added = self + .users + .get_mut(&uid) + .map(|u| { + if u.monitor.contains(&nick_low) { + false + } else { + u.monitor.push(nick_low.clone()); + true + } + }) + .unwrap_or(false); + if added { + self.monitor_by.entry(nick_low).or_default().insert(uid); + } + } + + /// Remove `nick_low` from `uid`'s MONITOR list and de-index it. + pub fn monitor_index_remove(&mut self, uid: Uid, nick_low: &str) { + let removed = self + .users + .get_mut(&uid) + .map(|u| { + let before = u.monitor.len(); + u.monitor.retain(|n| n != nick_low); + before != u.monitor.len() + }) + .unwrap_or(false); + if removed { + if let Some(set) = self.monitor_by.get_mut(nick_low) { + set.remove(&uid); + if set.is_empty() { + self.monitor_by.remove(nick_low); + } + } + } + } + + /// Clear `uid`'s whole MONITOR list (MONITOR C) and de-index every entry. + pub fn monitor_index_clear(&mut self, uid: Uid) { + let nicks = self + .users + .get_mut(&uid) + .map(|u| std::mem::take(&mut u.monitor)) + .unwrap_or_default(); + for n in nicks { + if let Some(set) = self.monitor_by.get_mut(&n) { + set.remove(&uid); + if set.is_empty() { + self.monitor_by.remove(&n); + } + } + } } /// True if `sender_nick` is on `target`'s ACCEPT list (callerid +g).