commit 364b84de718e4fe30c33276a9afb09cec7d2c0ec
parent f76da4773e072bb7e14cb6f6a077512352d7753c
Author: William Casarin <jb55@jb55.com>
Date: Fri, 27 Feb 2026 13:56:28 -0800
nostrverse: clean up scene objects when expiring stale users
maybe_expire now returns the removed RoomUsers so callers can
remove their avatar scene objects, preventing lingering 3D models.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
2 files changed, 31 insertions(+), 13 deletions(-)
diff --git a/crates/notedeck_nostrverse/src/lib.rs b/crates/notedeck_nostrverse/src/lib.rs
@@ -563,11 +563,20 @@ impl NostrverseApp {
}
// Expire stale remote users (throttled to every ~10s)
- let removed = self
+ let expired = self
.presence_expiry
.maybe_expire(&mut self.state.users, now);
- if removed > 0 {
- tracing::info!("Expired {} stale users", removed);
+ if !expired.is_empty() {
+ tracing::info!("Expired {} stale users", expired.len());
+ // Clean up scene objects so avatars don't linger in the 3D scene
+ if let Some(renderer) = &self.renderer {
+ let mut r = renderer.renderer.lock().unwrap();
+ for user in &expired {
+ if let Some(scene_id) = user.scene_object_id {
+ r.remove_object(scene_id);
+ }
+ }
+ }
}
}
diff --git a/crates/notedeck_nostrverse/src/presence.rs b/crates/notedeck_nostrverse/src/presence.rs
@@ -222,15 +222,23 @@ impl PresenceExpiry {
Self { last_check: 0.0 }
}
- /// Maybe expire stale users. Returns the number removed (0 if check was skipped).
- pub fn maybe_expire(&mut self, users: &mut Vec<RoomUser>, now: f64) -> usize {
+ /// Maybe expire stale users. Returns removed users so callers can clean up
+ /// their scene objects. Empty if the check was throttled.
+ pub fn maybe_expire(&mut self, users: &mut Vec<RoomUser>, now: f64) -> Vec<RoomUser> {
if now - self.last_check < EXPIRY_CHECK_INTERVAL {
- return 0;
+ return Vec::new();
}
self.last_check = now;
- let before = users.len();
- users.retain(|u| u.is_self || (now - u.last_seen) < STALE_TIMEOUT);
- before - users.len()
+ let mut expired = Vec::new();
+ let mut i = 0;
+ while i < users.len() {
+ if !users[i].is_self && (now - users[i].last_seen) >= STALE_TIMEOUT {
+ expired.push(users.swap_remove(i));
+ } else {
+ i += 1;
+ }
+ }
+ expired
}
}
@@ -266,18 +274,19 @@ mod tests {
let mut expiry = PresenceExpiry::new();
// First call at t=5 — too soon (< 10s from init at 0.0), skipped
- assert_eq!(expiry.maybe_expire(&mut users, 5.0), 0);
+ assert!(expiry.maybe_expire(&mut users, 5.0).is_empty());
assert_eq!(users.len(), 3); // no one removed
// At t=100 — enough time, bob is stale
- let removed = expiry.maybe_expire(&mut users, 100.0);
- assert_eq!(removed, 1);
+ let expired = expiry.maybe_expire(&mut users, 100.0);
+ assert_eq!(expired.len(), 1);
+ assert_eq!(expired[0].display_name, "bob");
assert_eq!(users.len(), 2);
assert!(users.iter().any(|u| u.is_self));
assert!(users.iter().any(|u| u.display_name == "alice"));
// Immediately again at t=101 — throttled, skipped
- assert_eq!(expiry.maybe_expire(&mut users, 101.0), 0);
+ assert!(expiry.maybe_expire(&mut users, 101.0).is_empty());
}
#[test]