Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions nostr-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 30 additions & 13 deletions nostr-sdk/src/local_relay/local/inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize> = filters
Expand Down Expand Up @@ -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]
Expand Down
46 changes: 46 additions & 0 deletions nostr-sdk/src/local_relay/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Loading