-
Notifications
You must be signed in to change notification settings - Fork 27
feat(rpc): add GET /lean/v0/events SSE stream #517
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
60d9c2e
feat(blockchain): add chain-event bus (head/block/justified/finalized)
MegaRedHand 03118fc
feat(rpc): add GET /lean/v0/events SSE stream
MegaRedHand d9b4c37
refactor(blockchain): emit head before justified_checkpoint in diff_a…
MegaRedHand 4b7a2ae
refactor(blockchain): move ChainEventSnapshot into events module
MegaRedHand 9cbe5b7
test(blockchain): drop chain_event_diff_head_recency_boundary
MegaRedHand 8464e6a
refactor(blockchain): drop unused EventBus::disabled
MegaRedHand cc1d413
Merge branch 'main' into feat/chain-events-bus
MegaRedHand 55d1310
fix(blockchain): beacon-align chain events; emit proposer checkpoints
MegaRedHand 22b296f
Merge remote-tracking branch 'origin/main' into feat/chain-events-bus
MegaRedHand 0072f27
Merge branch 'feat/chain-events-bus' into feat/events-sse-endpoint
MegaRedHand d83ea93
fix(rpc): adapt SSE events to beacon-aligned ChainEvent shape
MegaRedHand acf7d5a
refactor(rpc): derive Serialize on ChainEvent instead of a DTO
MegaRedHand cb0c8af
Merge remote-tracking branch 'origin/main' into feat/events-sse-endpoint
MegaRedHand d66e2e9
Merge branch 'main' into feat/events-sse-endpoint
MegaRedHand File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| //! `GET /lean/v0/events` — Server-Sent Events stream of chain events. | ||
| //! | ||
| //! The [`ethlambda_blockchain::BlockChainServer`] actor publishes | ||
| //! [`ethlambda_blockchain::ChainEvent`]s on the [`EventBus`]; this read-only | ||
| //! handler subscribes a new receiver per connection and forwards each event as | ||
| //! one SSE frame. The flow is strictly one-directional (actor → bus → SSE), so | ||
| //! RPC never writes into the actor. | ||
| //! | ||
| //! Framing: the topic name goes on the SSE `event:` line | ||
| //! ([`ethlambda_blockchain::ChainEvent::topic`]) and the `data:` line carries | ||
| //! the event's flat JSON payload; the topic is never repeated inside the body. | ||
|
|
||
| use std::convert::Infallible; | ||
|
|
||
| use axum::{ | ||
| Extension, Router, | ||
| response::{Sse, sse::Event}, | ||
| routing::get, | ||
| }; | ||
| use ethlambda_blockchain::EventBus; | ||
| use ethlambda_storage::Store; | ||
| use futures_core::Stream; | ||
| use tokio_stream::{ | ||
| StreamExt, | ||
| wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}, | ||
| }; | ||
|
|
||
| async fn get_events( | ||
| Extension(events): Extension<EventBus>, | ||
| ) -> Sse<impl Stream<Item = Result<Event, Infallible>>> { | ||
| let stream = BroadcastStream::new(events.subscribe()).filter_map(|res| { | ||
| // A slow client falls behind and the bounded broadcast channel | ||
| // overwrites events it never read; skip past the gap rather than | ||
| // ending the stream. The stream is best-effort by contract: clients | ||
| // re-sync via the blocks endpoints after a gap. | ||
| let ev = match res { | ||
| Ok(ev) => ev, | ||
| Err(BroadcastStreamRecvError::Lagged(skipped)) => { | ||
| tracing::debug!(skipped, "SSE client lagged; dropped chain events"); | ||
| return None; | ||
| } | ||
| }; | ||
| Some(Ok(Event::default() | ||
| .event(ev.topic().as_str()) | ||
| .json_data(&ev) | ||
| .inspect_err(|err| tracing::warn!(%err, "Failed to serialize SSE chain event")) | ||
| .ok()?)) | ||
| }); | ||
| Sse::new(stream).keep_alive(axum::response::sse::KeepAlive::default()) | ||
| } | ||
|
|
||
| pub(crate) fn routes() -> Router<Store> { | ||
| Router::new().route("/lean/v0/events", get(get_events)) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use axum::{Extension, body::Body, http::Request}; | ||
| use ethlambda_blockchain::{ChainEvent, EventBus}; | ||
| use ethlambda_storage::{Store, backend::InMemoryBackend}; | ||
| use std::sync::Arc; | ||
| use tower::ServiceExt; | ||
|
|
||
| use crate::test_utils::create_test_state; | ||
|
|
||
| #[tokio::test] | ||
| async fn events_streams_head_with_flat_payload() { | ||
| let events = EventBus::new(16); | ||
| let store = Store::from_anchor_state(Arc::new(InMemoryBackend::new()), create_test_state()); | ||
| let app = crate::test_utils::test_api_router(store).layer(Extension(events.clone())); | ||
|
|
||
| // Issue the request first so the handler subscribes its receiver | ||
| // before we publish — `emit` drops events with no live receivers. | ||
| let resp = app | ||
| .oneshot( | ||
| Request::builder() | ||
| .uri("/lean/v0/events") | ||
| .body(Body::empty()) | ||
| .unwrap(), | ||
| ) | ||
| .await | ||
| .unwrap(); | ||
| assert_eq!(resp.status(), axum::http::StatusCode::OK); | ||
|
|
||
| events.emit(ChainEvent::Head { | ||
| slot: 3, | ||
| block: Default::default(), | ||
| state: Default::default(), | ||
| }); | ||
|
|
||
| let mut body = resp.into_body().into_data_stream(); | ||
| let chunk = tokio_stream::StreamExt::next(&mut body) | ||
| .await | ||
| .unwrap() | ||
| .unwrap(); | ||
| let text = String::from_utf8_lossy(&chunk); | ||
|
|
||
| // Topic on the `event:` line... | ||
| assert!( | ||
| text.contains("event:head") || text.contains("event: head"), | ||
| "missing head event name in frame: {text}" | ||
| ); | ||
| // ...and a flat payload: the variant's own fields (`slot`, `block`, | ||
| // `state`) at the top level, `slot` as a plain number, with no | ||
| // `event`/`data` wrapper keys inside the JSON body (the #460 | ||
| // double-tag bug). | ||
| assert!( | ||
| text.contains("\"slot\":3"), | ||
| "missing top-level slot in frame: {text}" | ||
| ); | ||
| assert!( | ||
| text.contains("\"block\":") && text.contains("\"state\":"), | ||
| "missing beacon-aligned block/state fields in frame: {text}" | ||
| ); | ||
| assert!( | ||
| !text.contains("\"data\":") && !text.contains("\"event\":"), | ||
| "payload is not flat: {text}" | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.