notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 03df939b63a085312a9e966aa36ce28eedfd9369
parent 56987d99d9568d219d9b791a0422c619a64a6f43
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 18 Feb 2026 10:50:23 -0800

fix query_replaceable_filtered not handling arbitrary fold order

ndb.fold iterates notes in storage order, not chronological. If a
newer deleted revision was visited before an older non-deleted one,
the predicate rejection was a no-op (nothing to remove yet), and the
older revision would then be inserted unchallenged — resurrecting
deleted sessions as zombies.

Fix by always tracking the highest created_at per d-tag regardless of
predicate result, storing Option<NoteKey> so rejected revisions still
block older ones from winning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Diffstat:
Mcrates/notedeck_dave/src/session_loader.rs | 25++++++++++++++-----------
1 file changed, 14 insertions(+), 11 deletions(-)

diff --git a/crates/notedeck_dave/src/session_loader.rs b/crates/notedeck_dave/src/session_loader.rs @@ -34,11 +34,14 @@ pub fn query_replaceable_filtered( filters: &[Filter], predicate: impl Fn(&nostrdb::Note) -> bool, ) -> Vec<NoteKey> { - // Fold: for each d-tag value, track (created_at, NoteKey) of the latest + // Fold: for each d-tag value, track the latest created_at and optionally + // a NoteKey (only if the latest revision passes the predicate). + // Notes may arrive in any order from ndb.fold, so we always track the + // highest timestamp and only keep a key when that revision is valid. let best = ndb.fold( txn, filters, - std::collections::HashMap::<String, (u64, NoteKey)>::new(), + std::collections::HashMap::<String, (u64, Option<NoteKey>)>::new(), |mut acc, note| { let Some(d_tag) = get_tag_value(&note, "d") else { return acc; @@ -52,22 +55,22 @@ pub fn query_replaceable_filtered( } } - if predicate(&note) { - acc.insert( - d_tag.to_string(), - (created_at, note.key().expect("note key")), - ); + let key = if predicate(&note) { + Some(note.key().expect("note key")) } else { - // Latest revision rejected — remove any older revision we kept - acc.remove(d_tag); - } + None + }; + acc.insert(d_tag.to_string(), (created_at, key)); acc }, ); match best { - Ok(map) => map.into_values().map(|(_, key)| key).collect(), + Ok(map) => map + .into_values() + .filter_map(|(_, key)| key) + .collect(), Err(_) => vec![], } }