Skip to main content

yellowstone_fumarole_client/
stream.rs

1use {
2    crate::{
3        core::runtime::{FumaroleRuntimeCommitEvent, FumaroleRuntimeEvent},
4        error::FumaroleSubscribeError,
5    },
6    crossbeam::queue::SegQueue,
7    futures::{Sink, Stream, ready},
8    std::{
9        collections::{HashMap, VecDeque},
10        sync::Arc,
11        task::Poll,
12    },
13    tokio::sync::mpsc,
14    yellowstone_grpc_proto::geyser,
15};
16
17#[derive(Debug)]
18#[allow(clippy::large_enum_variant)]
19pub enum FumaroleEvent {
20    Data {
21        slot: u64,
22        update: geyser::SubscribeUpdate,
23    },
24    SlotEnded(u64),
25}
26
27/// Sending half of a Fumarole subscription session.
28///
29/// This sink accepts [`geyser::SubscribeRequest`] values and forwards them to the
30/// runtime over an internal bounded Tokio channel.
31///
32/// # Backpressure
33///
34/// The sink is backed by `mpsc::Sender::try_send`, so `start_send` can fail with
35/// `resource_exhausted` when the channel is full.
36///
37/// # Errors
38///
39/// - `unavailable` when the underlying request channel is closed.
40/// - `resource_exhausted` when the channel is full.
41///
42/// # Typical Usage
43///
44/// Use this sink from [`FumaroleSubscription`](crate::FumaroleSubscription) or
45/// split APIs to dynamically update filters/commitment while the data stream is
46/// active.
47pub struct FumaroleSink {
48    inner: mpsc::Sender<geyser::SubscribeRequest>,
49}
50
51impl FumaroleSink {
52    pub(crate) const fn new(inner: mpsc::Sender<geyser::SubscribeRequest>) -> Self {
53        Self { inner }
54    }
55}
56
57impl Sink<geyser::SubscribeRequest> for FumaroleSink {
58    type Error = tonic::Status;
59
60    fn poll_ready(
61        self: std::pin::Pin<&mut Self>,
62        _cx: &mut std::task::Context<'_>,
63    ) -> std::task::Poll<Result<(), Self::Error>> {
64        if self.get_mut().inner.is_closed() {
65            std::task::Poll::Ready(Err(tonic::Status::unavailable(
66                "subscribe request channel is closed",
67            )))
68        } else {
69            std::task::Poll::Ready(Ok(()))
70        }
71    }
72
73    fn start_send(
74        self: std::pin::Pin<&mut Self>,
75        item: geyser::SubscribeRequest,
76    ) -> Result<(), Self::Error> {
77        match self.get_mut().inner.try_send(item) {
78            Ok(()) => Ok(()),
79            Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => Err(
80                tonic::Status::resource_exhausted("subscribe request channel is full"),
81            ),
82            Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => Err(
83                tonic::Status::unavailable("subscribe request channel is closed"),
84            ),
85        }
86    }
87
88    fn poll_flush(
89        self: std::pin::Pin<&mut Self>,
90        _cx: &mut std::task::Context<'_>,
91    ) -> std::task::Poll<Result<(), Self::Error>> {
92        if self.get_mut().inner.is_closed() {
93            std::task::Poll::Ready(Err(tonic::Status::unavailable(
94                "subscribe request channel is closed",
95            )))
96        } else {
97            std::task::Poll::Ready(Ok(()))
98        }
99    }
100
101    fn poll_close(
102        self: std::pin::Pin<&mut Self>,
103        _cx: &mut std::task::Context<'_>,
104    ) -> std::task::Poll<Result<(), Self::Error>> {
105        if self.get_mut().inner.is_closed() {
106            std::task::Poll::Ready(Err(tonic::Status::unavailable(
107                "subscribe request channel is closed",
108            )))
109        } else {
110            std::task::Poll::Ready(Ok(()))
111        }
112    }
113}
114
115///
116/// The main fumarole stream type yielding [`FumaroleEvent`] values from the runtime.
117///
118pub struct FumaroleStream {
119    inner: mpsc::Receiver<Result<FumaroleRuntimeEvent, FumaroleSubscribeError>>,
120    commit_offset_queue: Arc<SegQueue<FumaroleRuntimeCommitEvent>>,
121    pending_commits: VecDeque<FumaroleRuntimeCommitEvent>,
122    auto_commit: bool,
123}
124
125/// Receiving half of a Fumarole subscription session.
126///
127/// This stream yields [`FumaroleEvent`] values wrapped in
128/// [`FumaroleSubscribeError`] for transport/runtime failures.
129///
130/// It acts as the canonical source stream and can be adapted into richer views:
131/// - [`slot_sequential`](Self::slot_sequential): enforces per-slot sequentiality.
132/// - [`block_stream`](Self::block_stream): groups data into slot-scoped blocks.
133/// - [`like_dragonsmouth`](Self::like_dragonsmouth): compatibility adapter that
134///   yields raw `SubscribeUpdate` entries.
135///
136/// # Ordering
137///
138/// Ordering guarantees depend on the selected adapter. The base stream itself
139/// forwards events as produced by the runtime.
140impl FumaroleStream {
141    pub(crate) const fn new(
142        commit_offset_queue: Arc<SegQueue<FumaroleRuntimeCommitEvent>>,
143        inner: mpsc::Receiver<Result<FumaroleRuntimeEvent, FumaroleSubscribeError>>,
144        auto_commit: bool,
145    ) -> Self {
146        Self {
147            inner,
148            pending_commits: VecDeque::new(),
149            auto_commit,
150            commit_offset_queue,
151        }
152    }
153
154    pub fn like_dragonsmouth(self) -> DragonsmouthLike {
155        DragonsmouthLike {
156            inner: self.slot_sequential(),
157        }
158    }
159
160    pub fn slot_sequential(self) -> SlotSequentialStream {
161        SlotSequentialStream {
162            inner: self,
163            state: Default::default(),
164        }
165    }
166
167    pub fn block_stream(self) -> BlockStream {
168        BlockStream {
169            inner: self,
170            state: Default::default(),
171        }
172    }
173}
174
175struct RopeDeque<T> {
176    // this is a deque of deques, where each inner deque contains events from the same slot, and the outer deque is ordered by slot
177    inner: VecDeque<VecDeque<T>>,
178}
179
180impl Default for RopeDeque<FumaroleEvent> {
181    fn default() -> Self {
182        Self {
183            inner: VecDeque::new(),
184        }
185    }
186}
187
188impl<T> RopeDeque<T> {
189    fn push_back(&mut self, item: T) {
190        if let Some(back) = self.inner.back_mut() {
191            back.push_back(item);
192        } else {
193            let mut new_back = VecDeque::new();
194            new_back.push_back(item);
195            self.inner.push_back(new_back);
196        }
197    }
198
199    fn pop_front(&mut self) -> Option<T> {
200        loop {
201            let front = self.inner.front_mut()?;
202            if let Some(item) = front.pop_front() {
203                return Some(item);
204            } else {
205                self.inner.pop_front();
206            }
207        }
208    }
209
210    fn extend_vecdeque(&mut self, items: VecDeque<T>) {
211        self.inner.push_back(items);
212    }
213
214    fn is_empty(&self) -> bool {
215        self.inner.iter().all(VecDeque::is_empty)
216    }
217}
218
219#[derive(Default)]
220struct BufferedSlotState {
221    updates: VecDeque<geyser::SubscribeUpdate>,
222    ended: bool,
223}
224
225#[derive(Default)]
226struct SlotSequentialStreamState {
227    current_slot: Option<u64>,
228    buffered_slot: HashMap<u64, BufferedSlotState>,
229    buffered_slot_order: VecDeque<u64>,
230    poll_ready: RopeDeque<FumaroleEvent>,
231}
232
233impl SlotSequentialStreamState {
234    fn buffer_data(&mut self, slot: u64, update: geyser::SubscribeUpdate) {
235        let state = self.buffered_slot.entry(slot).or_default();
236        if state.updates.is_empty() && !self.buffered_slot_order.contains(&slot) {
237            self.buffered_slot_order.push_back(slot);
238        }
239        state.updates.push_back(update);
240    }
241
242    fn mark_buffered_slot_ended(&mut self, slot: u64) {
243        let state = self.buffered_slot.entry(slot).or_default();
244        if state.updates.is_empty() && !self.buffered_slot_order.contains(&slot) {
245            self.buffered_slot_order.push_back(slot);
246        }
247        state.ended = true;
248    }
249
250    fn flush_next_buffered_slot(&mut self) {
251        while self.current_slot.is_none() {
252            let Some(slot) = self.buffered_slot_order.pop_front() else {
253                return;
254            };
255            let Some(buffered) = self.buffered_slot.remove(&slot) else {
256                continue;
257            };
258
259            if buffered.updates.is_empty() {
260                if buffered.ended {
261                    self.poll_ready.push_back(FumaroleEvent::SlotEnded(slot));
262                }
263                continue;
264            }
265
266            let mut emitted = VecDeque::new();
267            for update in buffered.updates {
268                emitted.push_back(FumaroleEvent::Data { slot, update });
269            }
270
271            let ended = buffered.ended;
272            self.poll_ready.extend_vecdeque(emitted);
273            if ended {
274                self.poll_ready.push_back(FumaroleEvent::SlotEnded(slot));
275            } else {
276                self.current_slot = Some(slot);
277            }
278        }
279    }
280
281    fn handle_fumarole_ev_data(&mut self, slot: u64, update: geyser::SubscribeUpdate) {
282        // Slot status and block metadata updates are guaranteed to be emitted after all data updates from the slot,
283        // so we can let them pass through immediately regardless of the currently active slot.
284        if matches!(
285            update.update_oneof.as_ref(),
286            Some(geyser::subscribe_update::UpdateOneof::Slot(_))
287                | Some(geyser::subscribe_update::UpdateOneof::BlockMeta(_))
288        ) {
289            self.poll_ready
290                .push_back(FumaroleEvent::Data { slot, update });
291            return;
292        }
293
294        match self.current_slot {
295            Some(current) if current == slot => {
296                self.poll_ready
297                    .push_back(FumaroleEvent::Data { slot, update });
298            }
299            Some(_) => {
300                self.buffer_data(slot, update);
301            }
302            None => {
303                self.current_slot = Some(slot);
304                self.poll_ready
305                    .push_back(FumaroleEvent::Data { slot, update });
306            }
307        }
308    }
309
310    fn handle_fumarole_ev_slot_ended(&mut self, slot: u64) {
311        if self.current_slot == Some(slot) {
312            self.poll_ready.push_back(FumaroleEvent::SlotEnded(slot));
313            self.current_slot = None;
314            self.flush_next_buffered_slot();
315            return;
316        }
317
318        if self.current_slot.is_none() {
319            if self.buffered_slot.contains_key(&slot) {
320                self.mark_buffered_slot_ended(slot);
321                self.flush_next_buffered_slot();
322            } else {
323                self.poll_ready.push_back(FumaroleEvent::SlotEnded(slot));
324            }
325            return;
326        }
327
328        self.mark_buffered_slot_ended(slot);
329    }
330
331    fn handle_fumarole_ev(&mut self, event: FumaroleEvent) {
332        match event {
333            FumaroleEvent::Data { slot, update } => {
334                self.handle_fumarole_ev_data(slot, update);
335            }
336            FumaroleEvent::SlotEnded(slot) => {
337                self.handle_fumarole_ev_slot_ended(slot);
338            }
339        }
340    }
341
342    fn poll_next(&mut self) -> Option<Result<FumaroleEvent, FumaroleSubscribeError>> {
343        if let Some(ev) = self.poll_ready.pop_front() {
344            return Some(Ok(ev));
345        }
346        None
347    }
348}
349
350///
351/// A streams that yeild [`FumaroleEvent`] in slot order, meaning that while a slot is active, only events from that
352/// slot will be yielded, and once the slot ends, events from the next slot will be yielded, and so on. So while a slot
353/// has not ended, event's from that slot won't be interleaved with events from other slots.
354///
355/// This is usefulfor consumers that want to process events in slot order and don't care about processing events from
356/// multiple slots concurrently.
357///
358/// If fumarole download two slots in parallel, (when replaying from the past), say the slot 1, 2. Then whatever slot
359/// gives yield the first event, say slot 2, will be the only slot that is yielded until it ends, and then events from
360/// slot 1 will be yielded.
361///
362pub struct SlotSequentialStream {
363    inner: FumaroleStream,
364    state: SlotSequentialStreamState,
365}
366
367impl SlotSequentialStream {
368    ///
369    /// See [`FumaroleStream::commit`] for more details.
370    ///
371    pub fn commit(&mut self) {
372        self.inner.commit();
373    }
374}
375
376/// Stream adapter that enforces slot-local sequential emission.
377///
378/// Given an input stream of [`FumaroleEvent`], this adapter ensures that regular
379/// data events from one active slot are emitted contiguously until that slot
380/// ends. Data from other slots is buffered until the active slot emits
381/// [`FumaroleEvent::SlotEnded`].
382///
383/// `Slot` status and `BlockMeta` updates are intentionally passed through
384/// immediately, because runtime semantics guarantee they are emitted after block
385/// payload data for the slot.
386///
387/// # Generic Parameter
388///
389/// `S` is any stream yielding `Result<FumaroleEvent, FumaroleSubscribeError>`.
390/// This allows composing adapters on top of `FumaroleStream` or custom sources.
391///
392/// # Use Cases
393///
394/// Useful for consumers that want deterministic per-slot processing without
395/// interleaving payload updates from concurrent slot downloads.
396///
397impl Stream for SlotSequentialStream {
398    type Item = Result<FumaroleEvent, FumaroleSubscribeError>;
399
400    fn poll_next(
401        mut self: std::pin::Pin<&mut Self>,
402        cx: &mut std::task::Context<'_>,
403    ) -> std::task::Poll<Option<Self::Item>> {
404        loop {
405            if let Some(item) = self.state.poll_next() {
406                return std::task::Poll::Ready(Some(item));
407            }
408
409            match std::pin::Pin::new(&mut self.inner).poll_next(cx) {
410                std::task::Poll::Ready(Some(Ok(event))) => {
411                    self.state.handle_fumarole_ev(event);
412                    continue;
413                }
414                std::task::Poll::Ready(Some(Err(err))) => {
415                    return std::task::Poll::Ready(Some(Err(err)));
416                }
417                std::task::Poll::Ready(None) => {
418                    if let Some(item) = self.state.poll_next() {
419                        return std::task::Poll::Ready(Some(item));
420                    }
421                    return std::task::Poll::Ready(None);
422                }
423                std::task::Poll::Pending => {
424                    if self.state.poll_ready.is_empty() {
425                        return std::task::Poll::Pending;
426                    }
427                }
428            }
429        }
430    }
431}
432
433impl TryFrom<FumaroleRuntimeEvent> for FumaroleEvent {
434    type Error = FumaroleRuntimeEvent;
435
436    fn try_from(ev: FumaroleRuntimeEvent) -> Result<Self, Self::Error> {
437        match ev {
438            FumaroleRuntimeEvent::Data(data) => Ok(FumaroleEvent::Data {
439                slot: data.slot,
440                update: data.update,
441            }),
442            FumaroleRuntimeEvent::SlotEnded(slot) => Ok(FumaroleEvent::SlotEnded(slot)),
443            other => Err(other),
444        }
445    }
446}
447
448impl FumaroleStream {
449    ///
450    /// Commits all pending progress to the fumarole service.
451    ///
452    /// If `auto_commit` is enabled, this method is called automatically after every slot ends or new commitment level update.
453    ///
454    /// If `auto_commit` is disabled, this method needs to be called manually to commit progress to the fumarole service.
455    /// In this case, the client is responsible for deciding when to commit progress, which can be useful for advanced use cases.
456    ///
457    ///
458    pub fn commit(&mut self) {
459        self.pending_commits.drain(..).for_each(|commit| {
460            self.commit_offset_queue.push(commit);
461        });
462    }
463}
464
465impl Stream for FumaroleStream {
466    type Item = Result<FumaroleEvent, FumaroleSubscribeError>;
467
468    fn poll_next(
469        mut self: std::pin::Pin<&mut Self>,
470        cx: &mut std::task::Context<'_>,
471    ) -> Poll<Option<Self::Item>> {
472        loop {
473            let maybe = ready!(self.inner.poll_recv(cx));
474            let Some(result) = maybe else {
475                return Poll::Ready(None);
476            };
477
478            match result {
479                Ok(ev) => match FumaroleEvent::try_from(ev) {
480                    Ok(ev) => return Poll::Ready(Some(Ok(ev))),
481                    Err(other) => match other {
482                        FumaroleRuntimeEvent::Committable(commit) => {
483                            self.pending_commits.push_back(commit);
484                            if self.auto_commit {
485                                self.commit();
486                            }
487                            continue;
488                        }
489                        _ => unreachable!("try_from should only fail for commit events"),
490                    },
491                },
492                Err(e) => {
493                    return Poll::Ready(Some(Err(e)));
494                }
495            }
496        }
497    }
498}
499
500#[derive(Debug)]
501#[allow(clippy::large_enum_variant)]
502pub enum FumaroleBlockStreamEvent {
503    Block(FumaroleBlockEvent),
504    SlotStatus(FumaroleSlotStatusEvent),
505}
506
507#[derive(Debug)]
508pub struct FumaroleBlockEvent {
509    pub slot: u64,
510    updates: Vec<geyser::SubscribeUpdate>,
511}
512
513///
514/// An iterator over the updates contained in a [`FumaroleBlockStreamEvent`].
515///
516pub struct FumaroleBlockIterator {
517    curr: usize,
518    inner: Vec<geyser::SubscribeUpdate>,
519}
520
521impl Iterator for FumaroleBlockIterator {
522    type Item = geyser::SubscribeUpdate;
523
524    fn next(&mut self) -> Option<Self::Item> {
525        if self.curr >= self.inner.len() {
526            None
527        } else {
528            let item = self.inner[self.curr].clone();
529            self.curr += 1;
530            Some(item)
531        }
532    }
533}
534
535impl IntoIterator for FumaroleBlockEvent {
536    type Item = geyser::SubscribeUpdate;
537    type IntoIter = FumaroleBlockIterator;
538
539    fn into_iter(self) -> Self::IntoIter {
540        FumaroleBlockIterator {
541            curr: 0,
542            inner: self.updates,
543        }
544    }
545}
546
547///
548/// An iterator over the references of updates contained in a [`FumaroleBlockEvent`].
549///
550pub struct FumaroleBlockIter<'a> {
551    curr: usize,
552    inner: &'a [geyser::SubscribeUpdate],
553}
554
555impl<'a> Iterator for FumaroleBlockIter<'a> {
556    type Item = &'a geyser::SubscribeUpdate;
557
558    fn next(&mut self) -> Option<Self::Item> {
559        if self.curr >= self.inner.len() {
560            None
561        } else {
562            let item = &self.inner[self.curr];
563            self.curr += 1;
564            Some(item)
565        }
566    }
567}
568
569impl FumaroleBlockEvent {
570    #[allow(clippy::missing_const_for_fn)]
571    pub fn iter(&self) -> FumaroleBlockIter<'_> {
572        FumaroleBlockIter {
573            curr: 0,
574            inner: &self.updates,
575        }
576    }
577}
578
579#[derive(Debug, Clone)]
580pub struct SlotStatusUpdateLense {
581    inner: geyser::SubscribeUpdate,
582}
583
584impl SlotStatusUpdateLense {
585    const unsafe fn new_unchecked(update: geyser::SubscribeUpdate) -> Self {
586        Self { inner: update }
587    }
588
589    ///
590    /// Focuses the lense on the [`geyser::SubscribeUpdateSlot`].
591    ///
592    pub fn focus(&self) -> &geyser::SubscribeUpdateSlot {
593        match self.inner.update_oneof.as_ref() {
594            Some(geyser::subscribe_update::UpdateOneof::Slot(slot)) => slot,
595            _ => panic!("not a slot update"),
596        }
597    }
598
599    ///
600    /// Returns the root [`geyser::SubscribeUpdate`] the lense can focus on.
601    ///
602    pub const fn inner_ref(&self) -> &geyser::SubscribeUpdate {
603        &self.inner
604    }
605
606    ///
607    /// Consumes the lense and returns the root [`geyser::SubscribeUpdate`] the lense can focus on.
608    ///
609    pub fn into_inner(self) -> geyser::SubscribeUpdate {
610        self.inner
611    }
612
613    ///
614    /// Consumes the lense and returns the focused [`geyser::SubscribeUpdateSlot`].
615    ///
616    pub fn into_focused(self) -> geyser::SubscribeUpdateSlot {
617        match self.inner.update_oneof {
618            Some(geyser::subscribe_update::UpdateOneof::Slot(slot)) => slot,
619            _ => panic!("not a slot update"),
620        }
621    }
622}
623
624#[derive(Debug)]
625pub struct FumaroleSlotStatusEvent {
626    pub slot: u64,
627    pub lense: SlotStatusUpdateLense,
628}
629
630#[derive(Default)]
631struct BlockStreamState {
632    buffered_block_updates: HashMap<u64, VecDeque<geyser::SubscribeUpdate>>,
633    poll_ready: VecDeque<FumaroleBlockStreamEvent>,
634}
635
636impl BlockStreamState {
637    fn handle_fumarole_ev_data(&mut self, slot: u64, update: geyser::SubscribeUpdate) {
638        match update.update_oneof.as_ref() {
639            Some(geyser::subscribe_update::UpdateOneof::Slot(_)) => {
640                self.poll_ready
641                    .push_back(FumaroleBlockStreamEvent::SlotStatus(
642                        FumaroleSlotStatusEvent {
643                            slot,
644                            lense: unsafe { SlotStatusUpdateLense::new_unchecked(update) },
645                        },
646                    ));
647            }
648            _ => {
649                self.buffered_block_updates
650                    .entry(slot)
651                    .or_default()
652                    .push_back(update);
653            }
654        }
655    }
656
657    fn handle_fumarole_ev_slot_ended(&mut self, slot: u64) {
658        let updates = self
659            .buffered_block_updates
660            .remove(&slot)
661            .unwrap_or_default()
662            .into_iter()
663            .collect();
664        self.poll_ready
665            .push_back(FumaroleBlockStreamEvent::Block(FumaroleBlockEvent {
666                slot,
667                updates,
668            }));
669    }
670
671    fn handle_fumarole_ev(&mut self, event: FumaroleEvent) {
672        match event {
673            FumaroleEvent::Data { slot, update } => self.handle_fumarole_ev_data(slot, update),
674            FumaroleEvent::SlotEnded(slot) => self.handle_fumarole_ev_slot_ended(slot),
675        }
676    }
677
678    fn poll_next(&mut self) -> Option<Result<FumaroleBlockStreamEvent, FumaroleSubscribeError>> {
679        self.poll_ready.pop_front().map(Ok)
680    }
681}
682
683pub struct BlockStream {
684    inner: FumaroleStream,
685    state: BlockStreamState,
686}
687
688impl BlockStream {
689    ///
690    /// See [`FumaroleStream::commit`] for more details.
691    ///
692    pub fn commit(&mut self) {
693        self.inner.commit();
694    }
695}
696
697/// Stream adapter that groups payload updates into slot-scoped blocks.
698///
699/// The adapter buffers regular data updates by slot (concurrently across slots).
700/// When [`FumaroleEvent::SlotEnded(slot)`] arrives, the buffered payload updates
701/// for that slot are emitted as [`FumaroleBlockStreamEvent::Block`].
702///
703/// `Slot` status and `BlockMeta` updates are surfaced immediately as
704/// [`FumaroleBlockStreamEvent::SlotStatus`] and
705/// [`FumaroleBlockStreamEvent::BlockMeta`] respectively.
706///
707/// # Generic Parameter
708///
709/// `S` is any stream yielding `Result<FumaroleEvent, FumaroleSubscribeError>`.
710///
711/// # Notes
712///
713/// This adapter is intended for block-oriented consumers that want explicit block
714/// boundaries while still receiving slot-status and block-meta events as soon as
715/// they appear.
716///
717impl Stream for BlockStream {
718    type Item = Result<FumaroleBlockStreamEvent, FumaroleSubscribeError>;
719
720    fn poll_next(
721        mut self: std::pin::Pin<&mut Self>,
722        cx: &mut std::task::Context<'_>,
723    ) -> std::task::Poll<Option<Self::Item>> {
724        loop {
725            if let Some(item) = self.state.poll_next() {
726                return std::task::Poll::Ready(Some(item));
727            }
728
729            match std::pin::Pin::new(&mut self.inner).poll_next(cx) {
730                std::task::Poll::Ready(Some(Ok(event))) => {
731                    self.state.handle_fumarole_ev(event);
732                    continue;
733                }
734                std::task::Poll::Ready(Some(Err(err))) => {
735                    return std::task::Poll::Ready(Some(Err(err)));
736                }
737                std::task::Poll::Ready(None) => {
738                    if let Some(item) = self.state.poll_next() {
739                        return std::task::Poll::Ready(Some(item));
740                    }
741                    return std::task::Poll::Ready(None);
742                }
743                std::task::Poll::Pending => {
744                    if self.state.poll_ready.is_empty() {
745                        return std::task::Poll::Pending;
746                    }
747                }
748            }
749        }
750    }
751}
752
753///
754/// A stream that yields [`geyser::SubscribeUpdate`] one by one, without any guarantee on the order of the updates.
755///
756/// Prefer to use [`SlotSequentialStream`], it yields richer data types that indicate slot boundaries.
757///
758pub struct DragonsmouthLike {
759    inner: SlotSequentialStream,
760}
761
762impl DragonsmouthLike {
763    ///
764    /// See [`FumaroleStream::commit`] for more details.
765    ///
766    pub fn commit(&mut self) {
767        self.inner.commit();
768    }
769}
770
771/// Compatibility adapter exposing a Dragonsmouth-like stream shape.
772///
773/// This adapter consumes a slot-sequential stream and yields only raw
774/// [`geyser::SubscribeUpdate`] values, filtering out explicit slot boundary
775/// markers (`SlotEnded`).
776///
777/// # Generic Parameter
778///
779/// `S` is any stream yielding `Result<FumaroleEvent, FumaroleSubscribeError>`.
780///
781/// # Behavior
782///
783/// - `FumaroleEvent::Data` -> forwarded as `Ok(SubscribeUpdate)`
784/// - `FumaroleEvent::SlotEnded` -> skipped
785/// - errors -> forwarded unchanged
786///
787/// This is useful when migrating existing Dragonsmouth consumers that are not
788/// yet aware of explicit slot boundary events.
789///
790impl Stream for DragonsmouthLike {
791    type Item = Result<geyser::SubscribeUpdate, FumaroleSubscribeError>;
792
793    fn poll_next(
794        mut self: std::pin::Pin<&mut Self>,
795        cx: &mut std::task::Context<'_>,
796    ) -> std::task::Poll<Option<Self::Item>> {
797        loop {
798            match std::pin::Pin::new(&mut self.inner).poll_next(cx) {
799                std::task::Poll::Ready(Some(Ok(FumaroleEvent::Data { slot: _, update }))) => {
800                    return std::task::Poll::Ready(Some(Ok(update)));
801                }
802                std::task::Poll::Ready(Some(Ok(FumaroleEvent::SlotEnded(_)))) => {
803                    continue;
804                }
805                std::task::Poll::Ready(Some(Err(err))) => {
806                    return std::task::Poll::Ready(Some(Err(err)));
807                }
808                std::task::Poll::Ready(None) => return std::task::Poll::Ready(None),
809                std::task::Poll::Pending => return std::task::Poll::Pending,
810            }
811        }
812    }
813}
814
815#[cfg(test)]
816mod tests {
817    use {
818        super::*,
819        crate::core::runtime::FumaroleRuntimeDataEvent,
820        futures::{StreamExt, pin_mut},
821        tokio::sync::mpsc,
822        yellowstone_grpc_proto::geyser::{
823            SubscribeUpdate, SubscribeUpdateEntry, SubscribeUpdateSlot,
824            subscribe_update::UpdateOneof,
825        },
826    };
827
828    fn mk_entry_update(slot: u64, index: u64) -> SubscribeUpdate {
829        SubscribeUpdate {
830            filters: vec![],
831            created_at: None,
832            update_oneof: Some(UpdateOneof::Entry(SubscribeUpdateEntry {
833                slot,
834                index,
835                num_hashes: 0,
836                hash: vec![],
837                executed_transaction_count: 0,
838                starting_transaction_index: 0,
839            })),
840        }
841    }
842
843    fn mk_slot_update(slot: u64) -> SubscribeUpdate {
844        SubscribeUpdate {
845            filters: vec![],
846            created_at: None,
847            update_oneof: Some(UpdateOneof::Slot(SubscribeUpdateSlot {
848                slot,
849                parent: None,
850                status: 0,
851                dead_error: None,
852            })),
853        }
854    }
855
856    #[tokio::test]
857    async fn slot_sequential_keeps_slot_events_grouped_until_slot_end() {
858        let (tx, rx) = mpsc::channel(16);
859        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
860            slot: 2,
861            update: mk_entry_update(2, 1),
862        })))
863        .await
864        .expect("send data slot 2");
865        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
866            slot: 1,
867            update: mk_entry_update(1, 1),
868        })))
869        .await
870        .expect("send data slot 1");
871        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
872            slot: 2,
873            update: mk_entry_update(2, 2),
874        })))
875        .await
876        .expect("send second data slot 2");
877        tx.send(Ok(FumaroleRuntimeEvent::SlotEnded(2)))
878            .await
879            .expect("send slot ended 2");
880        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
881            slot: 1,
882            update: mk_entry_update(1, 2),
883        })))
884        .await
885        .expect("send second data slot 1");
886        tx.send(Ok(FumaroleRuntimeEvent::SlotEnded(1)))
887            .await
888            .expect("send slot ended 1");
889        drop(tx);
890
891        let stream = FumaroleStream::new(Default::default(), rx, true).slot_sequential();
892        pin_mut!(stream);
893
894        let mut got = Vec::new();
895        while let Some(item) = stream.next().await {
896            match item.expect("stream should yield ok") {
897                FumaroleEvent::Data { slot, .. } => got.push(format!("d{slot}")),
898                FumaroleEvent::SlotEnded(slot) => got.push(format!("e{slot}")),
899            }
900        }
901
902        assert_eq!(got, vec!["d2", "d2", "e2", "d1", "d1", "e1"]);
903    }
904
905    #[tokio::test]
906    async fn slot_sequential_buffers_other_slot_end_until_turn() {
907        let (tx, rx) = mpsc::channel(16);
908        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
909            slot: 2,
910            update: mk_entry_update(2, 1),
911        })))
912        .await
913        .expect("send data slot 2");
914        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
915            slot: 1,
916            update: mk_entry_update(1, 1),
917        })))
918        .await
919        .expect("send data slot 1");
920        tx.send(Ok(FumaroleRuntimeEvent::SlotEnded(1)))
921            .await
922            .expect("send slot ended 1 while slot 2 active");
923        tx.send(Ok(FumaroleRuntimeEvent::SlotEnded(2)))
924            .await
925            .expect("send slot ended 2");
926        drop(tx);
927
928        let stream = FumaroleStream::new(Default::default(), rx, true).slot_sequential();
929        pin_mut!(stream);
930
931        let mut got = Vec::new();
932        while let Some(item) = stream.next().await {
933            match item.expect("stream should yield ok") {
934                FumaroleEvent::Data { slot, .. } => got.push(format!("d{slot}")),
935                FumaroleEvent::SlotEnded(slot) => got.push(format!("e{slot}")),
936            }
937        }
938
939        assert_eq!(got, vec!["d2", "e2", "d1", "e1"]);
940    }
941
942    #[tokio::test]
943    async fn slot_sequential_passes_through_slot_and_block_meta_updates() {
944        let (tx, rx) = mpsc::channel(16);
945        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
946            slot: 2,
947            update: mk_entry_update(2, 1),
948        })))
949        .await
950        .expect("send data slot 2");
951        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
952            slot: 1,
953            update: mk_entry_update(1, 1),
954        })))
955        .await
956        .expect("send data slot 1");
957        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
958            slot: 99,
959            update: mk_slot_update(99),
960        })))
961        .await
962        .expect("send slot status update");
963        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
964            slot: 100,
965            update: SubscribeUpdate {
966                filters: vec![],
967                created_at: None,
968                update_oneof: Some(UpdateOneof::BlockMeta(Default::default())),
969            },
970        })))
971        .await
972        .expect("send block meta update");
973        tx.send(Ok(FumaroleRuntimeEvent::SlotEnded(2)))
974            .await
975            .expect("send slot ended 2");
976        tx.send(Ok(FumaroleRuntimeEvent::SlotEnded(1)))
977            .await
978            .expect("send slot ended 1");
979        drop(tx);
980
981        let stream = FumaroleStream::new(Default::default(), rx, true).slot_sequential();
982        pin_mut!(stream);
983
984        let mut got = Vec::new();
985        while let Some(item) = stream.next().await {
986            match item.expect("stream should yield ok") {
987                FumaroleEvent::Data { slot, .. } => got.push(format!("d{slot}")),
988                FumaroleEvent::SlotEnded(slot) => got.push(format!("e{slot}")),
989            }
990        }
991
992        assert_eq!(got, vec!["d2", "d99", "d100", "e2", "d1", "e1"]);
993    }
994
995    #[tokio::test]
996    async fn block_stream_buffers_by_slot_and_emits_block_on_slot_end() {
997        let (tx, rx) = mpsc::channel(16);
998        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
999            slot: 2,
1000            update: mk_entry_update(2, 1),
1001        })))
1002        .await
1003        .expect("send entry slot 2");
1004        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
1005            slot: 1,
1006            update: mk_entry_update(1, 1),
1007        })))
1008        .await
1009        .expect("send entry slot 1");
1010        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
1011            slot: 2,
1012            update: mk_entry_update(2, 2),
1013        })))
1014        .await
1015        .expect("send second entry slot 2");
1016        tx.send(Ok(FumaroleRuntimeEvent::SlotEnded(2)))
1017            .await
1018            .expect("send slot ended 2");
1019        tx.send(Ok(FumaroleRuntimeEvent::SlotEnded(1)))
1020            .await
1021            .expect("send slot ended 1");
1022        drop(tx);
1023
1024        let stream = FumaroleStream::new(Default::default(), rx, true).block_stream();
1025        pin_mut!(stream);
1026
1027        let mut got = Vec::new();
1028        while let Some(item) = stream.next().await {
1029            match item.expect("block stream should yield ok") {
1030                FumaroleBlockStreamEvent::Block(FumaroleBlockEvent { slot, updates }) => {
1031                    got.push(format!("b{slot}:{}", updates.len()))
1032                }
1033                FumaroleBlockStreamEvent::SlotStatus(FumaroleSlotStatusEvent { slot, .. }) => {
1034                    got.push(format!("s{slot}"))
1035                }
1036            }
1037        }
1038
1039        assert_eq!(got, vec!["b2:2", "b1:1"]);
1040    }
1041
1042    #[tokio::test]
1043    async fn block_stream_passes_through_slot_status_and_block_meta() {
1044        let (tx, rx) = mpsc::channel(16);
1045        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
1046            slot: 2,
1047            update: mk_entry_update(2, 1),
1048        })))
1049        .await
1050        .expect("send entry slot 2");
1051        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
1052            slot: 99,
1053            update: mk_slot_update(99),
1054        })))
1055        .await
1056        .expect("send slot status");
1057        tx.send(Ok(FumaroleRuntimeEvent::Data(FumaroleRuntimeDataEvent {
1058            slot: 100,
1059            update: SubscribeUpdate {
1060                filters: vec![],
1061                created_at: None,
1062                update_oneof: Some(UpdateOneof::BlockMeta(Default::default())),
1063            },
1064        })))
1065        .await
1066        .expect("send block meta");
1067        tx.send(Ok(FumaroleRuntimeEvent::SlotEnded(2)))
1068            .await
1069            .expect("send slot ended 2");
1070        drop(tx);
1071
1072        let stream = FumaroleStream::new(Default::default(), rx, true).block_stream();
1073        pin_mut!(stream);
1074
1075        let mut got = Vec::new();
1076        while let Some(item) = stream.next().await {
1077            match item.expect("block stream should yield ok") {
1078                FumaroleBlockStreamEvent::Block(FumaroleBlockEvent { slot, updates }) => {
1079                    got.push(format!("b{slot}:{}", updates.len()))
1080                }
1081                FumaroleBlockStreamEvent::SlotStatus(FumaroleSlotStatusEvent { slot, .. }) => {
1082                    got.push(format!("s{slot}"))
1083                }
1084            }
1085        }
1086
1087        assert_eq!(got, vec!["s99", "b2:1"]);
1088    }
1089}