pub trait ShardLogic: Send + 'static {
// Required methods
fn render(&mut self, out: &mut ShardBuilder<'_>);
fn observation(&mut self, connection: ConnectionId) -> ScopeSet;
fn observe(&mut self, event: &VoiceEvent, out: &mut Reply);
}Expand description
What a flavor writes.
Send + 'static and deliberately not Sync: the runtime never needs to
share the logic, so it imposes no synchronization. A concrete flavor may
happen to be Sync if it holds one, which is its own business.
§External business input
ShardHandle::send is deliberately limited to ShardCommand: those are
runtime commands, not an extensible business mailbox. A concrete flavor owns
the transport for its own vocabulary instead. For an ordered event stream,
keep an mpsc::Receiver<Event> in the logic and give its senders to the
integration. For last-value-wins state, the same boundary can use a
watch::Receiver holding an immutable snapshot.
Slow or asynchronous work happens on the producer side. Once it has produced
an owned event or snapshot, the producer publishes it and then calls
ShardHandle::wake:
use tokio::sync::mpsc;
use mumble_server_runtime_shard::{
ConnectionId, Reply, ScopeSet, ShardBuilder, ShardHandle, ShardLogic, VoiceEvent,
};
struct Notification;
struct GameEvent;
struct GameState;
impl GameState {
fn apply(&mut self, _event: GameEvent) {}
fn render(&self, _out: &mut ShardBuilder<'_>) {}
}
struct GameLogic {
inbox: mpsc::Receiver<GameEvent>,
state: GameState,
}
fn build_logic() -> (mpsc::Sender<GameEvent>, GameLogic) {
let (sender, inbox) = mpsc::channel(64);
(sender, GameLogic { inbox, state: GameState })
}
impl ShardLogic for GameLogic {
fn render(&mut self, out: &mut ShardBuilder<'_>) {
while let Ok(event) = self.inbox.try_recv() {
self.state.apply(event);
}
self.state.render(out);
}
fn observation(&mut self, _connection: ConnectionId) -> ScopeSet {
ScopeSet::NONE
}
fn observe(&mut self, _event: &VoiceEvent, _out: &mut Reply) {}
}
async fn calculate(_notification: Notification) -> GameEvent {
GameEvent
}
async fn publish(
notification: Notification,
sender: &mpsc::Sender<GameEvent>,
handle: &ShardHandle,
) -> Result<(), mpsc::error::SendError<GameEvent>> {
let event = calculate(notification).await;
sender.send(event).await?;
handle.wake();
Ok(())
}wake carries no data. It only says that the desired state may have
changed, so several wake-ups may be coalesced into one reconciliation. The
channel or snapshot remains the source of truth. Publishing before waking
ensures that the next ShardLogic::render can observe the change.
Keeping render synchronous is intentional: it must not wait for I/O or
perform blocking work. Its &mut self receiver lets it drain the
flavor-owned channel and update local state without a lock. Any .await
belongs before publication, outside the shard task, as in the example.
Required Methods§
Sourcefn render(&mut self, out: &mut ShardBuilder<'_>)
fn render(&mut self, out: &mut ShardBuilder<'_>)
Build the shared view, the private overlays and the audio relation.
No viewer parameter: what is SHARED cannot depend on who is looking.
Sourcefn observation(&mut self, connection: ConnectionId) -> ScopeSet
fn observation(&mut self, connection: ConnectionId) -> ScopeSet
What this connection observes of the shared view.
Called once per connection per turn, so it must stay a small Copy
value. Returning something bigger is how the cost goes quadratic again.
Sourcefn observe(&mut self, event: &VoiceEvent, out: &mut Reply)
fn observe(&mut self, event: &VoiceEvent, out: &mut Reply)
A voice fact happened. The flavor alone decides what to do with it.
out is the only way a flavor speaks: everything else it wants to change
belongs to the next ShardLogic::render. See Reply for why the two
doors are separate.