Skip to content

Commit fd6602c

Browse files
committed
refactor(rpc): use Vec<Topic> for the events filter instead of a bitmask
The TopicSet newtype (a Copy u16 bitmask plus a FromIterator impl) was more machinery than a handful of topics warrant. Parse `?topics=` straight into a Vec<Topic> and membership-test with Vec::contains in the stream loop. Drops the type and its unit test; filtering stays covered end-to-end by events_topics_filter_skips_unmatched.
1 parent c20b72c commit fd6602c

1 file changed

Lines changed: 11 additions & 59 deletions

File tree

crates/net/rpc/src/events.rs

Lines changed: 11 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -34,50 +34,27 @@ struct EventsParams {
3434
topics: Option<String>,
3535
}
3636

37-
/// Topics an SSE client asked for: a `Copy` bitmask over [`Topic`]
38-
/// discriminants, tested once per received event.
39-
///
40-
/// The filter lives here, in the sole consumer that needs it, rather than in
41-
/// the [`EventBus`]: the bus stays a plain fan-out, and any other subscriber
42-
/// pays nothing for a feature only this endpoint uses.
43-
#[derive(Clone, Copy)]
44-
struct TopicSet(u16);
45-
46-
impl TopicSet {
47-
fn contains(self, topic: Topic) -> bool {
48-
self.0 & (1 << topic as u16) != 0
49-
}
50-
}
51-
52-
impl FromIterator<Topic> for TopicSet {
53-
fn from_iter<I: IntoIterator<Item = Topic>>(iter: I) -> Self {
54-
iter.into_iter().fold(TopicSet(0), |set, topic| {
55-
TopicSet(set.0 | (1 << topic as u16))
56-
})
57-
}
58-
}
59-
6037
async fn get_events(
6138
Extension(events): Extension<EventBus>,
6239
Query(params): Query<EventsParams>,
6340
) -> Response {
6441
// `topics` is required, matching the Beacon API: a missing or empty value
6542
// is a 400, as is any unknown topic name. See docs/rpc.md.
66-
let topics = match params.topics.as_deref() {
43+
//
44+
// The parsed selection lives here, in the sole consumer that filters, not
45+
// in the bus: a plain `Vec` since there are only a handful of topics.
46+
let topics: Vec<Topic> = match params.topics.as_deref() {
6747
None | Some("") => {
6848
return (
6949
StatusCode::BAD_REQUEST,
7050
"missing required query parameter: topics",
7151
)
7252
.into_response();
7353
}
74-
Some(list) => {
75-
let parsed: Result<TopicSet, _> = list.split(',').map(Topic::from_str).collect();
76-
match parsed {
77-
Ok(topics) => topics,
78-
Err(err) => return (StatusCode::BAD_REQUEST, err.to_string()).into_response(),
79-
}
80-
}
54+
Some(list) => match list.split(',').map(Topic::from_str).collect() {
55+
Ok(topics) => topics,
56+
Err(err) => return (StatusCode::BAD_REQUEST, err.to_string()).into_response(),
57+
},
8158
};
8259

8360
Sse::new(event_stream(events.subscribe(), topics))
@@ -89,13 +66,13 @@ async fn get_events(
8966
/// events the client's `topics` filter excludes.
9067
fn event_stream(
9168
rx: broadcast::Receiver<ChainEvent>,
92-
topics: TopicSet,
69+
topics: Vec<Topic>,
9370
) -> impl Stream<Item = Result<Event, Infallible>> {
9471
unfold((rx, topics), |(mut rx, topics)| async move {
9572
loop {
9673
match rx.recv().await {
9774
// Not in the client's `?topics=` set: skip without a frame.
98-
Ok(ev) if !topics.contains(ev.topic()) => continue,
75+
Ok(ev) if !topics.contains(&ev.topic()) => continue,
9976
Ok(ev) => {
10077
let frame = Event::default()
10178
.event(ev.topic().as_str())
@@ -143,40 +120,15 @@ mod tests {
143120
body::Body,
144121
http::{Request, StatusCode},
145122
};
146-
use ethlambda_blockchain::{ChainEvent, EventBus, Topic};
123+
use ethlambda_blockchain::{ChainEvent, EventBus};
147124
use ethlambda_storage::{Store, backend::InMemoryBackend};
148125
use futures_util::StreamExt;
149126
use http_body_util::BodyExt;
150127
use std::sync::Arc;
151128
use tower::ServiceExt;
152129

153-
use super::TopicSet;
154130
use crate::test_utils::create_test_state;
155131

156-
#[test]
157-
fn topic_set_collects_and_contains() {
158-
let set: TopicSet = [Topic::Head, Topic::FinalizedCheckpoint]
159-
.into_iter()
160-
.collect();
161-
assert!(set.contains(Topic::Head));
162-
assert!(set.contains(Topic::FinalizedCheckpoint));
163-
assert!(!set.contains(Topic::Block));
164-
assert!(!set.contains(Topic::JustifiedCheckpoint));
165-
166-
// The empty set matches nothing; a fully-populated set matches all.
167-
let all = [
168-
Topic::Head,
169-
Topic::Block,
170-
Topic::JustifiedCheckpoint,
171-
Topic::FinalizedCheckpoint,
172-
];
173-
let full: TopicSet = all.into_iter().collect();
174-
for topic in all {
175-
assert!(!TopicSet(0).contains(topic));
176-
assert!(full.contains(topic));
177-
}
178-
}
179-
180132
async fn events_response(events: &EventBus, uri: &str) -> axum::response::Response {
181133
let store = Store::from_anchor_state(Arc::new(InMemoryBackend::new()), create_test_state());
182134
let app = crate::test_utils::test_api_router(store).layer(Extension(events.clone()));

0 commit comments

Comments
 (0)