brokapi_sansio/core.rs
1//! Sans-io broker core trait.
2
3use std::time::Instant;
4
5use crate::event::EventSink;
6use crate::outbound::Outbound;
7use crate::subscription::SubscriptionSet;
8
9/// Protocol logic of one broker stream, written without sockets, timers or an async runtime.
10///
11/// This is the "sans-io" pattern. A core is a plain state machine. The code that owns the socket
12/// feeds it inbound frames and the current time, and collects what it wants to say: events for the
13/// consumer ([`EventSink`]), messages to put on the wire ([`poll_transmit`](Self::poll_transmit))
14/// and the next time it wants to be woken ([`poll_timeout`](Self::poll_timeout)). Because time is
15/// an argument and there is no I/O, a core can be tested with recorded frames and virtual time, see
16/// [`Harness`](crate::Harness).
17///
18/// How [`Harness`](crate::Harness) drives a core, and what a real driver should do the same way:
19///
20/// - call [`on_frame`](Self::on_frame) for each inbound frame, with the time it arrived;
21/// - after every call into the core, drain [`poll_transmit`](Self::poll_transmit) until it returns
22/// `None` and send the messages;
23/// - sleep until [`poll_timeout`](Self::poll_timeout), then call [`on_timeout`](Self::on_timeout);
24/// - call [`apply`](Self::apply) when the set of wanted subscriptions changes.
25///
26/// A core must never block, sleep or read the clock itself. Use the `now` it is given.
27///
28/// # Examples
29///
30/// A core that answers every frame and reports a resync after 10 seconds of silence, driven with
31/// virtual time:
32///
33/// ```
34/// use std::time::{Duration, Instant};
35/// use brokapi_core::{Event, InstrumentId, InstrumentKey, NativeId, ResyncReason, SubscriptionId};
36/// use brokapi_sansio::{BrokerCore, EventSink, Harness, Outbound, SubscriptionSet};
37///
38/// /// Answers every frame with a pong. If the line is silent for 10 seconds it reports a resync.
39/// struct WatchdogCore {
40/// silent_since: Instant,
41/// armed: bool,
42/// pongs: Vec<String>,
43/// }
44///
45/// impl BrokerCore for WatchdogCore {
46/// type Frame = String;
47///
48/// fn on_frame(&mut self, frame: String, now: Instant, _out: &mut EventSink) {
49/// self.silent_since = now;
50/// self.armed = true;
51/// self.pongs.push(format!("pong {frame}"));
52/// }
53///
54/// fn poll_transmit(&mut self) -> Option<Outbound> {
55/// if self.pongs.is_empty() { None } else { Some(Outbound::WsText(self.pongs.remove(0))) }
56/// }
57///
58/// fn poll_timeout(&self) -> Option<Instant> {
59/// self.armed.then(|| self.silent_since + Duration::from_secs(10))
60/// }
61///
62/// fn on_timeout(&mut self, _now: Instant, out: &mut EventSink) {
63/// // Disarm first: a core that keeps the same deadline would be called again for ever.
64/// self.armed = false;
65/// let instrument = InstrumentId::new(
66/// InstrumentKey::new("SBER", "TQBR"),
67/// NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() },
68/// );
69/// out.push_payload(
70/// Some(SubscriptionId::new(1)),
71/// Event::Resync { sub: SubscriptionId::new(1), instrument, reason: ResyncReason::SubscriptionReset },
72/// );
73/// }
74///
75/// fn apply(&mut self, _desired: &SubscriptionSet) {}
76/// }
77///
78/// let t0 = Instant::now();
79/// let core = WatchdogCore { silent_since: t0, armed: false, pongs: Vec::new() };
80/// let mut harness = Harness::new(core, t0);
81///
82/// harness.feed_frame("ping".to_string());
83/// assert_eq!(harness.transmitted(), [Outbound::WsText("pong ping".to_string())]);
84///
85/// harness.advance_by(Duration::from_secs(9));
86/// assert!(harness.events().is_empty());
87/// harness.advance_by(Duration::from_secs(2));
88/// assert_eq!(harness.events().len(), 1);
89/// ```
90pub trait BrokerCore {
91 /// The inbound frame type: for example `String` for WebSocket text, or a decoded message.
92 type Frame;
93
94 /// Handles one inbound transport frame received at `now`.
95 ///
96 /// Events for the consumer are pushed into `out`. Messages for the broker are queued inside the
97 /// core and picked up by [`poll_transmit`](Self::poll_transmit). The method does not return an
98 /// error: a frame the core cannot use is its own business (skip it, or emit an event).
99 fn on_frame(&mut self, frame: Self::Frame, now: Instant, out: &mut EventSink);
100
101 /// Takes the next outbound wire message, if any. Call it until it returns `None`.
102 fn poll_transmit(&mut self) -> Option<Outbound>;
103
104 /// When the core wants [`on_timeout`](Self::on_timeout) to be called next, or `None` if it has
105 /// no timer pending.
106 ///
107 /// After `on_timeout` the core must move this deadline forward or clear it. A core that
108 /// returns the same past deadline again makes a driver loop for ever.
109 fn poll_timeout(&self) -> Option<Instant>;
110
111 /// Handles a timer that fired. `now` is the time the timer was due.
112 fn on_timeout(&mut self, now: Instant, out: &mut EventSink);
113
114 /// Tells the core the full set of subscriptions the consumer wants now.
115 ///
116 /// The argument is the whole target state, not a change. The core works out what to subscribe
117 /// and unsubscribe and queues the messages for [`poll_transmit`](Self::poll_transmit).
118 fn apply(&mut self, desired: &SubscriptionSet);
119}