commit b9cb95abfc26d965edd7785ecf9be69a1ba387a9
parent 4f8ba91c3bdf29e5fcc3f6b8621ef5085623bef6
Author: triesap <tyson@radroots.org>
Date: Sun, 14 Jun 2026 03:21:29 -0700
runtime: count visible query results exactly
- count COUNT filters through the central visible query screen instead of REQ result length
- add an unbounded filter clone so COUNT ignores initial-query limit caps
- dedupe overlapping filter matches by event id before returning exact counts
- cover visible COUNT dedupe, unbounded limit behavior, and filter helper semantics
Diffstat:
2 files changed, 78 insertions(+), 1 deletion(-)
diff --git a/crates/tangle_protocol/src/lib.rs b/crates/tangle_protocol/src/lib.rs
@@ -686,6 +686,12 @@ impl Filter {
self.limit
}
+ pub fn without_limit(&self) -> Self {
+ let mut filter = self.clone();
+ filter.limit = None;
+ filter
+ }
+
pub fn search(&self) -> Option<&str> {
self.search.as_deref()
}
@@ -1889,6 +1895,10 @@ mod tests {
assert!(Filter::empty().matches(&event));
assert_eq!(Filter::empty().limit(), None);
assert_eq!(Filter::empty().search(), None);
+ let without_limit = filter.without_limit();
+ assert_eq!(without_limit.limit(), None);
+ assert_eq!(without_limit.search(), filter.search());
+ assert!(without_limit.matches(&event));
assert_eq!(
filter_from_value(&filter_to_value(&filter)).expect("encoded"),
filter
diff --git a/crates/tangle_runtime/src/relay/core.rs b/crates/tangle_runtime/src/relay/core.rs
@@ -347,7 +347,7 @@ impl BaseRelay {
) -> Result<RelayMessage, BaseRelayError> {
Ok(RelayMessage::Count {
subscription_id,
- count: self.query_events(&filters, auth)?.len() as u64,
+ count: self.count_events(&filters, auth)?,
})
}
@@ -389,6 +389,21 @@ impl BaseRelay {
Ok(Self::sort_and_dedupe_query_events(output))
}
+ fn count_events(
+ &self,
+ filters: &[Filter],
+ auth: &GroupAuthContext,
+ ) -> Result<u64, BaseRelayError> {
+ let mut seen = BTreeSet::new();
+ for filter in filters {
+ let filter = filter.without_limit();
+ for event in self.query_filter_events(&filter, auth)? {
+ seen.insert(event.id().clone());
+ }
+ }
+ u64::try_from(seen.len()).map_err(|_| BaseRelayError::error("visible event count overflow"))
+ }
+
fn query_filter_events(
&self,
filter: &Filter,
@@ -607,6 +622,58 @@ mod tests {
}
#[test]
+ fn base_relay_count_dedupes_overlapping_visible_filters() {
+ let mut relay = test_relay("base-relay-count-dedupe", 8);
+ let market_tag = Tag::from_parts("t", &["market"]).expect("tag");
+ let first = signed_event_at(7, 1, vec![market_tag.clone()], "first", 1_714_124_433);
+ let second = signed_event_at(8, 1, vec![market_tag], "second", 1_714_124_434);
+ let third = signed_event_at(7, 2, Vec::new(), "third", 1_714_124_435);
+
+ for event in [&first, &second, &third] {
+ assert_accepted(relay.handle_event(event.clone()).expect("event"), event);
+ }
+
+ let market_notes =
+ filter_from_value(&serde_json::json!({"kinds":[1],"#t":["market"],"limit":2}))
+ .expect("market filter");
+ let author_events = filter_from_value(&serde_json::json!({
+ "authors":[first.unsigned().pubkey().as_str()],
+ "kinds":[1,2],
+ "limit":10
+ }))
+ .expect("author filter");
+ let limited_market =
+ filter_from_value(&serde_json::json!({"kinds":[1],"#t":["market"],"limit":1}))
+ .expect("limited filter");
+
+ assert_eq!(
+ relay
+ .handle_count(
+ SubscriptionId::new("count-limit").expect("sub"),
+ vec![limited_market]
+ )
+ .expect("count"),
+ RelayMessage::Count {
+ subscription_id: SubscriptionId::new("count-limit").expect("sub"),
+ count: 2
+ }
+ );
+
+ assert_eq!(
+ relay
+ .handle_count(
+ SubscriptionId::new("count-dedupe").expect("sub"),
+ vec![market_notes, author_events]
+ )
+ .expect("count"),
+ RelayMessage::Count {
+ subscription_id: SubscriptionId::new("count-dedupe").expect("sub"),
+ count: 3
+ }
+ );
+ }
+
+ #[test]
fn base_relay_rejects_group_marked_events_before_group_service() {
let mut relay = test_relay("base-relay-group-reject", 4);
let event = signed_public_event(