diff --git a/nostr-sdk/CHANGELOG.md b/nostr-sdk/CHANGELOG.md index dc65dd5e0..09414b8b8 100644 --- a/nostr-sdk/CHANGELOG.md +++ b/nostr-sdk/CHANGELOG.md @@ -27,6 +27,12 @@ --> +## Unreleased + +### Changed + +- local_relay: improve `max_query_results` handling (https://github.com/nostrdevkit/nostr/pull/1457) + ## v0.45.2 - 2026/08/19 ### Fixed diff --git a/nostr-sdk/src/local_relay/local/inner.rs b/nostr-sdk/src/local_relay/local/inner.rs index b2f7544b5..eaf8ddf24 100644 --- a/nostr-sdk/src/local_relay/local/inner.rs +++ b/nostr-sdk/src/local_relay/local/inner.rs @@ -1316,19 +1316,8 @@ impl InnerLocalRelay { } } - // Cap each query and the merged result so multiple filters cannot multiply output. - for filter in filters.iter_mut() { - let requested_limit = filter.limit.unwrap_or_else(|| { - filter - .ids - .as_ref() - .map_or(self.default_filter_limit, |ids| ids.len()) - }); - let per_filter_limit = self - .max_filter_limit - .map_or(requested_limit, |max| requested_limit.min(max)); - filter.limit = Some(per_filter_limit.min(self.max_query_results)); - } + // set the correct limit for the filters + self.set_filters_limit(&mut filters); // Check if subscription has IDs let ids_len: Option = filters @@ -1462,6 +1451,34 @@ impl InnerLocalRelay { GiftWrapQueryAccess::Allowed } + + /// Ensures that each filter's effective limit respects the configured + /// `max_filter_limit` and that the combined limits across all filters do + /// not exceed `max_query_results`. + fn set_filters_limit(&self, filters: &mut [Filter]) { + let mut total_limit: usize = 0; + for filter in filters.iter_mut() { + // Determine the effective limit for this filter: + // - If an ID set exists, its length; otherwise the filter's own limit. + // - Cap that value by `max_filter_limit`. + // - Fall back to `default_filter_limit` if both are absent. + let limit = (filter.ids.as_ref().map(BTreeSet::len).or(filter.limit)) + .min(self.max_filter_limit) + .unwrap_or(self.default_filter_limit); + + filter.limit = Some(limit); + total_limit = total_limit.saturating_add(limit); + } + + // If the sum of all limits exceeds the maximum allowed query results, + // redistribute the budget equally among all filters. + if total_limit > self.max_query_results { + // TODO: Set per-filter limits instead of a global one; filters + // matching a single id should have a limit of 1. + let new_limit = self.max_query_results.div_ceil(filters.len()); + filters.iter_mut().for_each(|f| f.limit = Some(new_limit)); + } + } } #[inline] diff --git a/nostr-sdk/src/local_relay/mod.rs b/nostr-sdk/src/local_relay/mod.rs index 0cc6fb9ad..b816393ed 100644 --- a/nostr-sdk/src/local_relay/mod.rs +++ b/nostr-sdk/src/local_relay/mod.rs @@ -232,4 +232,50 @@ mod tests { output.failed.values().next().unwrap() ); } + + #[tokio::test] + async fn test_max_query_results() { + let relay = LocalRelay::builder() + .database(MemoryDatabase::unbounded()) + .max_query_results(6) + .build(); + relay.run().await.unwrap(); + + let keys = Keys::generate(); + let client = Client::default(); + + client + .add_relay(relay.url().await) + .and_connect() + .await + .unwrap(); + + for i in 0..10 { + client + .send_event( + &EventBuilder::new( + if i % 2 == 1 { + Kind::TextNote + } else { + Kind::Comment + }, + i.to_string(), + ) + .finalize(&keys) + .unwrap(), + ) + .await + .unwrap(); + } + + let result = client + .fetch_events([ + Filter::new().kind(Kind::TextNote), + Filter::new().kind(Kind::Comment), + ]) + .await + .unwrap(); + + assert_eq!(6, result.len(), "max_query_results is 6"); + } }