commit 441a7a1b60871d2c11f02470c09a73b617e5fbd2
parent 3a55578e5d1996fe552776f48994ffda640dc547
Author: triesap <tyson@radroots.org>
Date: Fri, 26 Jun 2026 23:14:38 +0000
runtime: preserve matched filters after projection limit
Pass every request filter that matches a post-limit query candidate into projection hooks, matching the normal query projection path. This keeps mixed-filter projection decisions deterministic when a candidate was found through one filter but also matches another filter in the same request.\n\nValidation:\n- cargo extbuild run -- cargo fmt --all -- --check\n- cargo extbuild run -- cargo check --workspace --all-targets\n- cargo extbuild run -- cargo test --workspace\n- cargo extbuild run -- cargo clippy --workspace --all-targets -- -D warnings
Diffstat:
1 file changed, 81 insertions(+), 2 deletions(-)
diff --git a/crates/tangle_runtime/src/runtime.rs b/crates/tangle_runtime/src/runtime.rs
@@ -1249,7 +1249,7 @@ impl RelayRuntimeShared {
let mut output = Vec::new();
let mut group_read_denied = false;
let mut query_metrics = BaseRelayQueryMetrics::default();
- for (filter_index, filter) in filters.iter().enumerate() {
+ for filter in &filters {
let report = BaseRelay::query_filter_events_report_with_services(
&self.store,
self.groups.as_ref(),
@@ -1264,13 +1264,14 @@ impl RelayRuntimeShared {
let events = BaseRelay::sort_and_dedupe_query_events(report.into_events());
let mut projected = Vec::new();
for event in events {
+ let matched_filters = self.matched_filters_for_event(&filters, &event)?;
if let Some(event) = self.project_event_output(RelayEventProjectionRequest {
subscription_id: &subscription_id,
projection,
source: RelayEventProjectionSource::HistoricalQuery,
event: &event,
auth,
- matched_filters: vec![(matched_filter_context(filter_index, filter), filter)],
+ matched_filters,
})? {
projected.push(event);
}
@@ -3608,6 +3609,84 @@ mod tests {
}
#[tokio::test]
+ async fn runtime_projection_post_limit_reports_all_matching_query_filters() {
+ let root = temp_root("runtime-projection-post-limit-all-matched-filters");
+ let _ = std::fs::remove_dir_all(&root);
+ let hooks = Arc::new(ProjectingHooks::new(
+ "post-limit-multi-filter",
+ ProjectionHookScope::Historical,
+ None,
+ RelayEventProjectionDecision::Emit,
+ ));
+ hooks.set_query_plan(RelayProjectionQueryPlan::limit_after_projection(
+ NonZeroU32::new(10).expect("candidate limit"),
+ ));
+ let handle = RelayRuntimeHandle::new(
+ RelayRuntime::open_with_hooks(runtime_config(&root, 8), hooks.clone())
+ .expect("runtime"),
+ );
+ let mut auth = handle.auth_state().await.expect("auth");
+ let event = tangle_v2_event(
+ FixtureKey::Member,
+ 1_714_124_433,
+ 30078,
+ Vec::new(),
+ "post limit multi filter query",
+ )
+ .expect("event");
+ assert_accepted_reply(
+ runtime_event_reply(&handle, event.clone(), &mut auth, 1_714_124_433).await,
+ &event,
+ );
+ let subscription_id =
+ SubscriptionId::new("post-limit-query-multi-filter").expect("subscription");
+
+ let report = handle
+ .query_req_with_auth_report_with_projection_context(
+ subscription_id.clone(),
+ vec![
+ pocket_filter(json!({"kinds": [30078]})),
+ pocket_filter(json!({"ids": [event.id().as_str()]})),
+ ],
+ false,
+ &auth,
+ &RelayProjectionContext::named("post-limit-multi-filter").expect("projection"),
+ )
+ .await
+ .expect("query");
+ assert!(matches!(
+ report.into_messages().as_slice(),
+ [
+ RuntimeRelayMessage::Event {
+ subscription_id: delivered,
+ event: delivered_event
+ },
+ RuntimeRelayMessage::Protocol(RelayMessage::Eose(eose))
+ ] if delivered == &subscription_id
+ && delivered_event.id().as_hex_string() == event.id().as_str()
+ && eose == &subscription_id
+ ));
+ let contexts = hooks.contexts();
+ assert_eq!(contexts.len(), 2);
+ for context in contexts {
+ let matched_filters = context.matched_filters();
+ assert_eq!(matched_filters.len(), 2);
+ assert_eq!(matched_filters[0].filter_index(), 0);
+ assert_eq!(
+ matched_filters[0].requested_kinds(),
+ &RelayRequestedKinds::Explicit(BTreeSet::from([30078]))
+ );
+ assert_eq!(matched_filters[1].filter_index(), 1);
+ assert_eq!(
+ matched_filters[1].requested_kinds(),
+ &RelayRequestedKinds::Absent
+ );
+ }
+
+ let _ = std::fs::remove_dir_all(root);
+ }
+
+ #[tokio::test]
async fn runtime_projection_can_suppress_live_fanout_events() {
let root = temp_root("runtime-projection-live-suppress");
let _ = std::fs::remove_dir_all(&root);