mumble_server_runtime_shard/
shard.rs

1//! The shard: one render, one journal, one task.
2//!
3//! ```text
4//!    business state changes
5//!         |  wake()
6//!         v
7//!   +------------------------------------------------------+
8//!   | the shard's task (woken, one per shard)               |
9//!   |                                                       |
10//!   |  1. logic.render(&mut builder)                        |
11//!   |       -> the SHARED view + the PRIVATE overlays        |
12//!   |          + the AUDIO relation                          |
13//!   |  2. ops = plan(current view -> new view)               |
14//!   |  3. version += 1, ops -> journal                       |
15//!   |  4. publish the audio routing table                    |
16//!   |  5. for each connection:                               |
17//!   |       its observation changed? -> replan for it        |
18//!   |       otherwise -> filter, splice, collapse, encode,   |
19//!   |                    push, advance its committed state   |
20//!   +------------------------------------------------------+
21//! ```
22//!
23//! # The rules this module must never break
24//!
25//! 1. A shard task never awaits IO.
26//! 2. The audio routing table is published **before** the views are pushed. A
27//!    revoked route disappears immediately, which is always safe: cutting too
28//!    early means hearing less than your due, never more. A granted route is
29//!    inert until the receiver's cursor catches up.
30//! 3. The committed state of a connection is a **quadruplet** - cursor,
31//!    observation, overlay, offered actions - and the four only advance together,
32//!    once the queue has accepted everything. This is what lets a lagging
33//!    connection catch up naturally: the shared part replays from the journal,
34//!    the private parts are recomputed from what was last accepted, so neither an
35//!    overlay nor an action ever needs journalling.
36//! 4. An invalid render never replaces the current view: log it, keep the old
37//!    one, and do not close anything.
38//!
39//! REF: docs/design/guide-implementation.md 9
40
41use std::collections::{BTreeMap, BTreeSet};
42use std::sync::Arc;
43use std::sync::atomic::{AtomicU64, Ordering};
44use std::time::Duration;
45
46use mumble_server_runtime_protocol::ControlMessage;
47use tokio::sync::mpsc::error::TryRecvError;
48use tokio::sync::{Notify, mpsc, oneshot, watch};
49use tokio::time::Instant;
50
51use crate::build::{BuildError, ShardBuilder};
52use crate::compose::{collapse, filter};
53use crate::emit::{TextTarget, emit};
54use crate::ids::{
55    ActionKey, ChannelId, ChannelKey, ConnectionId, Occupant, SessionId, ShardId, SharedIds,
56};
57use crate::journal::Journal;
58use crate::plan::{PlanOp, plan, plan_elements};
59use crate::queue::{OutboundQueue, Refused};
60use crate::reply::{Audience, Effects, Reply, Spoken};
61use crate::routing::{AudioRelation, AudioRouting, Silence, compile};
62use crate::scope::ScopeSet;
63use crate::view::{Actions, Channel, On, Overlay, ShardView, User};
64
65/// The floor between two publications.
66///
67/// Commands and flavor events are still absorbed immediately while this
68/// cooldown runs. Only reconciliation is rate-limited, so a burst becomes one
69/// publication instead of one 50 ms delay per command.
70pub const MIN_INTERVAL: Duration = Duration::from_millis(50);
71
72/// A voice-plane fact reported to the flavor.
73///
74/// Always a statement about the runtime, never a command: the flavor alone
75/// decides what its state becomes, and ignoring an event is a valid answer.
76#[derive(Debug, Clone, PartialEq, Eq)]
77#[non_exhaustive]
78pub enum VoiceEvent {
79    /// A connection was attached to this shard.
80    Connected { connection: ConnectionId },
81    /// A connection is gone. No further event will carry it.
82    Disconnected {
83        connection: ConnectionId,
84        reason: String,
85    },
86    /// A connection left for another shard. Its socket is still alive and its
87    /// session is unchanged; only this shard stops describing it.
88    ///
89    /// Distinct from [`VoiceEvent::Disconnected`] because the two mean opposite
90    /// things to a flavor: a disconnect frees a slot, a migration hands it over.
91    Migrated {
92        connection: ConnectionId,
93        to: ShardId,
94    },
95    /// The client asked to enter a channel, by double-clicking it or dragging
96    /// itself into it.
97    ///
98    /// A request, never a fact: the channel is one this connection can actually
99    /// see, and nothing has moved. What it means is entirely the flavor's
100    /// business, up to and including ignoring it.
101    ///
102    /// REF: references/vendored/Mumble.proto : `UserState.channel_id` sent by a
103    ///   client for its own session.
104    RequestedChannel {
105        connection: ConnectionId,
106        channel: ChannelKey,
107    },
108    /// The client asked to mute or deafen **itself**.
109    ///
110    /// A request like the others: the flags a client sees are the ones the
111    /// flavor renders, so refusing is simply rendering nothing new. A flavor
112    /// that grants it stores the pair and hands it back through
113    /// [`crate::build::ShardBuilder::user_flags`].
114    ///
115    /// `None` means the client said nothing about that flag, so whatever the
116    /// flavor currently renders stands. The runtime holds no copy of the pair:
117    /// that state belongs to the flavor, and keeping a second one here is how
118    /// the two start disagreeing.
119    ///
120    /// REF: references/mumble/src/mumble/ServerHandler.cpp :
121    ///   `setSelfMuteDeafState` sends a `UserState` carrying both flags.
122    RequestedSelfState {
123        connection: ConnectionId,
124        self_mute: Option<bool>,
125        self_deaf: Option<bool>,
126    },
127    /// The client invoked a context action this shard had offered it.
128    ///
129    /// Everything is already resolved against what that connection was actually
130    /// granted and can actually see: the key was offered to it, the target is
131    /// visible to it, and the place matches the bits the flavor declared. What
132    /// the action *means* is the flavor's business alone, including doing
133    /// nothing and refusing through [`crate::reply::Reply::refuse`].
134    ///
135    /// REF: references/mumble/src/mumble/MainWindow.cpp : `context_triggered`
136    ///   sends back the identifier the server stored, with the selected user
137    ///   and channel.
138    InvokedAction {
139        connection: ConnectionId,
140        action: ActionKey,
141        on: ActionTarget,
142    },
143    /// The client typed something and aimed it somewhere.
144    ///
145    /// A request like every other: the target is already resolved against what
146    /// this connection can see, and the channel it names is one the flavor
147    /// rendered as writable, but **nothing has been delivered**. A flavor that
148    /// ignores this event delivers nothing, which is the honest default for a
149    /// runtime whose whole subject is who may know what.
150    ///
151    /// [`crate::reply::Reply::relay`] is the one line that carries it out, and
152    /// the flavor is free to rewrite the audience, the text, or both.
153    ///
154    /// REF: references/mumble/src/murmur/Messages.cpp : `msgTextMessage` resolves
155    ///   the targets against the sender's view, then routes.
156    Said {
157        connection: ConnectionId,
158        to: Audience,
159        text: String,
160    },
161}
162
163/// What a context action was invoked on.
164///
165/// In the flavor's own vocabulary rather than the wire's: a key for a channel,
166/// an occupant for a user, exactly like [`VoiceEvent::RequestedChannel`].
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum ActionTarget {
169    /// Nothing was selected: the action was invoked on the server itself.
170    Server,
171    Channel(ChannelKey),
172    User(Occupant),
173}
174
175/// What a flavor writes.
176///
177/// `Send + 'static` and deliberately **not** `Sync`: the runtime never needs to
178/// share the logic, so it imposes no synchronization. A concrete flavor may
179/// happen to be `Sync` if it holds one, which is its own business.
180///
181/// # External business input
182///
183/// [`ShardHandle::send`] is deliberately limited to [`ShardCommand`]: those are
184/// runtime commands, not an extensible business mailbox. A concrete flavor owns
185/// the transport for its own vocabulary instead. For an ordered event stream,
186/// keep an `mpsc::Receiver<Event>` in the logic and give its senders to the
187/// integration. For last-value-wins state, the same boundary can use a
188/// `watch::Receiver` holding an immutable snapshot.
189///
190/// Slow or asynchronous work happens on the producer side. Once it has produced
191/// an owned event or snapshot, the producer publishes it and then calls
192/// [`ShardHandle::wake`]:
193///
194/// ```no_run
195/// use tokio::sync::mpsc;
196/// use mumble_server_runtime_shard::{
197///     ConnectionId, Reply, ScopeSet, ShardBuilder, ShardHandle, ShardLogic, VoiceEvent,
198/// };
199///
200/// struct Notification;
201/// struct GameEvent;
202/// struct GameState;
203///
204/// impl GameState {
205///     fn apply(&mut self, _event: GameEvent) {}
206///
207///     fn render(&self, _out: &mut ShardBuilder<'_>) {}
208/// }
209///
210/// struct GameLogic {
211///     inbox: mpsc::Receiver<GameEvent>,
212///     state: GameState,
213/// }
214///
215/// fn build_logic() -> (mpsc::Sender<GameEvent>, GameLogic) {
216///     let (sender, inbox) = mpsc::channel(64);
217///     (sender, GameLogic { inbox, state: GameState })
218/// }
219///
220/// impl ShardLogic for GameLogic {
221///     fn render(&mut self, out: &mut ShardBuilder<'_>) {
222///         while let Ok(event) = self.inbox.try_recv() {
223///             self.state.apply(event);
224///         }
225///         self.state.render(out);
226///     }
227///
228///     fn observation(&mut self, _connection: ConnectionId) -> ScopeSet {
229///         ScopeSet::NONE
230///     }
231///
232///     fn observe(&mut self, _event: &VoiceEvent, _out: &mut Reply) {}
233/// }
234///
235/// async fn calculate(_notification: Notification) -> GameEvent {
236///     GameEvent
237/// }
238///
239/// async fn publish(
240///     notification: Notification,
241///     sender: &mpsc::Sender<GameEvent>,
242///     handle: &ShardHandle,
243/// ) -> Result<(), mpsc::error::SendError<GameEvent>> {
244///     let event = calculate(notification).await;
245///     sender.send(event).await?;
246///     handle.wake();
247///     Ok(())
248/// }
249/// ```
250///
251/// `wake` carries no data. It only says that the desired state may have
252/// changed, so several wake-ups may be coalesced into one reconciliation. The
253/// channel or snapshot remains the source of truth. Publishing before waking
254/// ensures that the next [`ShardLogic::render`] can observe the change.
255///
256/// Keeping `render` synchronous is intentional: it must not wait for I/O or
257/// perform blocking work. Its `&mut self` receiver lets it drain the
258/// flavor-owned channel and update local state without a lock. Any `.await`
259/// belongs before publication, outside the shard task, as in the example.
260pub trait ShardLogic: Send + 'static {
261    /// Build the shared view, the private overlays and the audio relation.
262    ///
263    /// No viewer parameter: what is SHARED cannot depend on who is looking.
264    fn render(&mut self, out: &mut ShardBuilder<'_>);
265
266    /// What this connection observes of the shared view.
267    ///
268    /// Called once per connection per turn, so it **must** stay a small `Copy`
269    /// value. Returning something bigger is how the cost goes quadratic again.
270    fn observation(&mut self, connection: ConnectionId) -> ScopeSet;
271
272    /// A voice fact happened. The flavor alone decides what to do with it.
273    ///
274    /// `out` is the only way a flavor speaks: everything else it wants to change
275    /// belongs to the next [`ShardLogic::render`]. See [`Reply`] for why the two
276    /// doors are separate.
277    fn observe(&mut self, event: &VoiceEvent, out: &mut Reply);
278}
279
280/// A command for a shard's task.
281#[derive(Debug)]
282pub enum ShardCommand {
283    Attach {
284        connection: ConnectionId,
285        /// Shared with the connection's writer task, which is what lets a
286        /// migration hand the same queue to the next shard.
287        queue: Arc<OutboundQueue>,
288        /// The cell the voice plane gates on. Owned by the runtime rather than
289        /// by the shard, so a migration does not have to republish it.
290        cursor: Arc<AtomicU64>,
291        /// What the client already holds. Empty for a fresh connection, and the
292        /// previous shard's composed view for a migration.
293        held: ShardView,
294        /// Fired once the connection's first transition has been accepted, so
295        /// the handshake knows when it may send `ServerSync`.
296        ready: Option<oneshot::Sender<()>>,
297    },
298    Detach {
299        connection: ConnectionId,
300        reason: String,
301        /// Set when the connection is moving to another shard rather than
302        /// leaving. It receives the view the client still holds, and the
303        /// teardown is **skipped**: the destination plans one transition from
304        /// that view instead.
305        handover: Option<Handover>,
306    },
307    /// A connection's queue drained: retry for **that one alone**, in O(1).
308    Drained(ConnectionId),
309    /// The client asked to enter a channel. Resolved against this connection's
310    /// own view and reported to the flavor, or refused.
311    Requested {
312        connection: ConnectionId,
313        channel: ChannelId,
314    },
315    /// The client asked to mute or deafen itself. Reported to the flavor, which
316    /// alone decides what the view ends up saying.
317    RequestedSelfState {
318        connection: ConnectionId,
319        self_mute: Option<bool>,
320        self_deaf: Option<bool>,
321    },
322    /// The client asked what it may do in a channel. Answered from the render,
323    /// for that connection alone.
324    QueriedPermissions {
325        connection: ConnectionId,
326        channel: ChannelId,
327    },
328    /// The client asked what the server publishes about a user. Answered only
329    /// for a user this connection can actually see.
330    QueriedUserStats {
331        connection: ConnectionId,
332        target: SessionId,
333    },
334    /// The client invoked a context action. Validated against what this
335    /// connection was offered and what it can see, then reported to the flavor.
336    ///
337    /// The wire identifier travels as it arrived: the gateway does not know
338    /// which actions exist, and reading it back is part of the validation.
339    InvokedAction {
340        connection: ConnectionId,
341        action: String,
342        session: Option<SessionId>,
343        channel: Option<ChannelId>,
344    },
345    /// The client sent a text message. Resolved against this connection's own
346    /// view and reported to the flavor, or refused.
347    ///
348    /// The gateway has already checked the shape, the length and the rate; what
349    /// is left is the only question it cannot answer, which is whether this
350    /// connection may name that target at all.
351    Said {
352        connection: ConnectionId,
353        to: TextTarget,
354        message: String,
355    },
356}
357
358impl ShardCommand {
359    /// Attach a connection that holds nothing yet: a fresh arrival.
360    #[must_use]
361    pub fn attach(connection: ConnectionId, queue: Arc<OutboundQueue>) -> ShardCommand {
362        ShardCommand::Attach {
363            connection,
364            queue,
365            cursor: Arc::new(AtomicU64::new(0)),
366            held: ShardView::empty(),
367            ready: None,
368        }
369    }
370
371    /// Detach a connection that is leaving for good.
372    #[must_use]
373    pub fn detach(connection: ConnectionId, reason: impl Into<String>) -> ShardCommand {
374        ShardCommand::Detach {
375            connection,
376            reason: reason.into(),
377            handover: None,
378        }
379    }
380}
381
382/// Where a migrating connection's held view is sent.
383#[derive(Debug)]
384pub struct Handover {
385    /// The shard the connection is moving to, reported to the flavor.
386    pub to: ShardId,
387    /// Receives the composed view the client still holds.
388    pub view: oneshot::Sender<ShardView>,
389}
390
391/// One connection attached to a shard, and its committed state.
392#[derive(Debug)]
393pub struct AttachedConnection {
394    id: ConnectionId,
395    session: SessionId,
396    /// How far through the SHARED journal it has been advanced.
397    cursor: u64,
398    /// What it observes of the shared view.
399    see: ScopeSet,
400    /// What PRIVATE elements it has received.
401    overlay_sent: Overlay,
402    /// What context actions it has been offered. Same discipline as the overlay:
403    /// what the client **holds**, not what the last render wanted it to hold.
404    actions_sent: Actions,
405    /// What the client holds, when this shard cannot derive it from its own
406    /// previous view: right after an attach, and right after a migration.
407    ///
408    /// `Some` only until the first transition is accepted, and then never again.
409    /// Keeping one view per connection permanently is exactly the O(N·W) memory
410    /// this whole design exists to avoid, so the field is a transient rather
411    /// than a cache.
412    held: Option<ShardView>,
413    /// Fired once, when the connection's first transition lands.
414    ready: Option<oneshot::Sender<()>>,
415    queue: Arc<OutboundQueue>,
416    /// Read by the voice plane to gate newly granted routes (guide 9.5).
417    shared_cursor: Arc<AtomicU64>,
418}
419
420impl AttachedConnection {
421    #[must_use]
422    pub fn id(&self) -> ConnectionId {
423        self.id
424    }
425
426    #[must_use]
427    pub fn session(&self) -> SessionId {
428        self.session
429    }
430
431    #[must_use]
432    pub fn cursor(&self) -> u64 {
433        self.cursor
434    }
435
436    #[must_use]
437    pub fn observation(&self) -> ScopeSet {
438        self.see
439    }
440
441    /// The private elements this connection has actually received.
442    #[must_use]
443    pub fn overlay_sent(&self) -> &Overlay {
444        &self.overlay_sent
445    }
446
447    /// The context actions this connection has actually been offered.
448    #[must_use]
449    pub fn actions_sent(&self) -> &Actions {
450        &self.actions_sent
451    }
452
453    /// Whether the queue has refused something unrecoverable.
454    #[must_use]
455    pub fn must_close(&self) -> bool {
456        self.queue.must_close()
457    }
458
459    /// The cursor cell the voice plane gates on.
460    #[must_use]
461    pub fn shared_cursor(&self) -> Arc<AtomicU64> {
462        Arc::clone(&self.shared_cursor)
463    }
464
465    /// Advance the committed quadruplet. The only place the four move, and they
466    /// move together.
467    fn commit(&mut self, cursor: u64, see: ScopeSet, overlay: Overlay, actions: Actions) {
468        self.cursor = cursor;
469        self.see = see;
470        self.overlay_sent = overlay;
471        self.actions_sent = actions;
472        self.shared_cursor.store(cursor, Ordering::Relaxed);
473        // Whatever the client held before this transition is now described by
474        // the triplet, so the transient copy is dropped rather than kept.
475        self.held = None;
476        if let Some(ready) = self.ready.take() {
477            // Nobody waiting is the normal case for every connection but the
478            // one still in its handshake.
479            let _awaited = ready.send(());
480        }
481    }
482
483    /// The view this connection's client actually holds right now.
484    fn composed(&self, shared: &ShardView) -> ShardView {
485        match &self.held {
486            Some(held) => held.clone(),
487            None => shared.restrict(self.see).compose(&self.overlay_sent),
488        }
489    }
490}
491
492/// What one [`Shard::reconcile`] did. Everything an operator or a test needs.
493#[derive(Debug, Clone, Default, PartialEq, Eq)]
494pub struct ReconcileReport {
495    /// The version after this turn.
496    pub version: u64,
497    /// Whether a new version was published.
498    pub published: bool,
499    /// How many operations the shared delta held.
500    pub delta_len: usize,
501    /// Connections that took the slow path because their observation moved.
502    pub replanned: Vec<ConnectionId>,
503    /// Whether the audio routing table was recompiled.
504    pub routing_recompiled: bool,
505    /// Connections that must be torn down.
506    pub closed: Vec<ConnectionId>,
507    /// Set when the render was refused: the previous view was kept.
508    pub refused: Option<BuildError>,
509}
510
511/// A shard: unit of ownership, scheduling, rendering, id allocation and audio
512/// routing.
513pub struct Shard<L: ShardLogic> {
514    id: ShardId,
515    logic: L,
516    ids: SharedIds,
517    view: ShardView,
518    journal: Journal,
519    version: u64,
520    connections: BTreeMap<ConnectionId, AttachedConnection>,
521    /// The version each session first appeared at, threaded through so it
522    /// survives recompilation.
523    since: BTreeMap<SessionId, u64>,
524    /// The inputs the routing table was last compiled from. Comparing against
525    /// them is what makes a channel rename cost nothing on the audio plane.
526    declared_audio: AudioRelation,
527    /// Part of the same comparison: who was muted or deafened last time.
528    declared_silence: Silence,
529    session_of: BTreeMap<ConnectionId, SessionId>,
530    routing: watch::Sender<Arc<AudioRouting>>,
531    /// Where what this shard cannot do itself is sent. `None` for a shard that
532    /// belongs to no runtime, which is a real state - a test, a benchmark - and
533    /// not a missing wire, so it is reported rather than assumed away.
534    effects: Option<Effects>,
535}
536
537impl<L: ShardLogic> Shard<L> {
538    /// A shard with an allocator of its own.
539    ///
540    /// Correct for a runtime that will only ever hold one shard. As soon as a
541    /// connection can move, every shard must share one allocator: see
542    /// [`Shard::with_ids`] and [`SharedIds`].
543    #[must_use]
544    pub fn new(id: ShardId, logic: L) -> Shard<L> {
545        Shard::with_ids(id, logic, SharedIds::new())
546    }
547
548    /// A shard drawing its identifiers from a runtime-wide allocator.
549    #[must_use]
550    pub fn with_ids(id: ShardId, logic: L, ids: SharedIds) -> Shard<L> {
551        let (routing, _) = watch::channel(Arc::new(AudioRouting::default()));
552        Shard {
553            id,
554            logic,
555            ids,
556            view: ShardView::empty(),
557            journal: Journal::new(),
558            version: 0,
559            connections: BTreeMap::new(),
560            since: BTreeMap::new(),
561            declared_audio: AudioRelation::default(),
562            declared_silence: Silence::default(),
563            session_of: BTreeMap::new(),
564            routing,
565            effects: None,
566        }
567    }
568
569    /// Wire this shard's effects to a runtime.
570    ///
571    /// Kept out of the constructors on purpose: a shard is complete without one,
572    /// and only the code that owns several shards can honour a move between two
573    /// of them.
574    pub fn route_effects(&mut self, effects: Effects) {
575        self.effects = Some(effects);
576    }
577
578    #[must_use]
579    pub fn id(&self) -> ShardId {
580        self.id
581    }
582
583    #[must_use]
584    pub fn version(&self) -> u64 {
585        self.version
586    }
587
588    #[must_use]
589    pub fn view(&self) -> &ShardView {
590        &self.view
591    }
592
593    #[must_use]
594    pub fn connection(&self, connection: ConnectionId) -> Option<&AttachedConnection> {
595        self.connections.get(&connection)
596    }
597
598    /// The attached connections, in id order.
599    pub fn connections(&self) -> impl Iterator<Item = &AttachedConnection> {
600        self.connections.values()
601    }
602
603    /// The flavor's own state.
604    ///
605    /// The shard owns the logic outright, so this is how a composition binary
606    /// or a test reaches the business model it handed over. A flavor driven by
607    /// its own channel does not need it.
608    pub fn logic_mut(&mut self) -> &mut L {
609        &mut self.logic
610    }
611
612    /// A reader of the audio routing table, for the voice plane.
613    #[must_use]
614    pub fn routing(&self) -> watch::Receiver<Arc<AudioRouting>> {
615        self.routing.subscribe()
616    }
617
618    /// Apply a command. Never renders: the caller reconciles afterwards.
619    pub fn handle(&mut self, command: ShardCommand) {
620        match command {
621            ShardCommand::Attach {
622                connection,
623                queue,
624                cursor,
625                held,
626                ready,
627            } => self.attach(connection, queue, cursor, held, ready),
628            ShardCommand::Detach {
629                connection,
630                reason,
631                handover,
632            } => self.detach(connection, &reason, handover),
633            ShardCommand::Drained(connection) => self.retry(connection),
634            ShardCommand::Requested {
635                connection,
636                channel,
637            } => self.requested(connection, channel),
638            ShardCommand::RequestedSelfState {
639                connection,
640                self_mute,
641                self_deaf,
642            } => self.requested_self_state(connection, self_mute, self_deaf),
643            ShardCommand::QueriedPermissions {
644                connection,
645                channel,
646            } => self.queried_permissions(connection, channel),
647            ShardCommand::QueriedUserStats { connection, target } => {
648                self.queried_user_stats(connection, target);
649            }
650            ShardCommand::InvokedAction {
651                connection,
652                action,
653                session,
654                channel,
655            } => self.invoked(connection, &action, session, channel),
656            ShardCommand::Said {
657                connection,
658                to,
659                message,
660            } => self.said(connection, to, message),
661        }
662    }
663
664    /// Attach a connection.
665    ///
666    /// It starts observing **nothing** and holding `held`, which is what makes
667    /// attaching an ordinary scope change rather than a mechanism of its own:
668    /// the next reconcile sees a connection that holds a view this shard did not
669    /// produce, takes the slow path, and plans one transition from it. Attach,
670    /// detach and migrate are then all the same code (guide 9.6), and a
671    /// migration inherits the no-flicker property for free.
672    fn attach(
673        &mut self,
674        connection: ConnectionId,
675        queue: Arc<OutboundQueue>,
676        cursor: Arc<AtomicU64>,
677        held: ShardView,
678        ready: Option<oneshot::Sender<()>>,
679    ) {
680        let Ok(session) = self.ids.session(Occupant::Connection(connection)) else {
681            // The session space is exhausted. Refuse the connection rather than
682            // attaching one that can never be rendered.
683            queue.mark_fatal();
684            return;
685        };
686
687        cursor.store(self.version, Ordering::Relaxed);
688        self.connections.insert(
689            connection,
690            AttachedConnection {
691                id: connection,
692                session,
693                cursor: self.version,
694                see: ScopeSet::NONE,
695                overlay_sent: Overlay::default(),
696                actions_sent: Actions::default(),
697                held: Some(held),
698                ready,
699                queue,
700                shared_cursor: cursor,
701            },
702        );
703        self.tell(&VoiceEvent::Connected { connection });
704    }
705
706    /// Detach a connection: it stops hearing and being heard immediately.
707    ///
708    /// A connection that is **leaving** is told to tear its view down. One that
709    /// is **migrating** is not: its held view is handed to the destination,
710    /// which plans a single transition onto its own tree. Tearing down first
711    /// would be worse than wasteful, it would be wrong - the client keeps itself
712    /// in its own model after a `UserRemove` for its own session, so the
713    /// following `ChannelRemove` would look like a removal of an occupied
714    /// channel and the client would disconnect over a protocol violation.
715    ///
716    /// REF: references/mumble/src/mumble/Messages.cpp : `msgUserRemove` skips
717    ///   `removeUser` when the victim is self; `msgChannelRemove` disconnects
718    ///   when `UserModel::removeChannel(c, true)` refuses an occupied channel.
719    fn detach(&mut self, connection: ConnectionId, reason: &str, handover: Option<Handover>) {
720        let Some(mut attached) = self.connections.remove(&connection) else {
721            return;
722        };
723
724        let held = attached.composed(&self.view);
725        if let Some(handover) = handover {
726            // An action belongs to the shard that offered it, and the destination
727            // starts from an empty registry because that is what an attach says.
728            // Without this the client would keep buttons nobody can ever withdraw,
729            // and invoking one would reach a shard that never granted it.
730            let withdrawn = crate::emit::actions(&attached.actions_sent, &Actions::default());
731            if !withdrawn.is_empty()
732                && let Err(refused) = attached.queue.try_send_all(withdrawn)
733            {
734                eprintln!(
735                    "mumble-server-runtime-shard: shard {:?}: {connection:?} leaves with context actions this \
736                     shard could not withdraw: {refused}",
737                    self.id
738                );
739            }
740            // A closed receiver means the migration was abandoned between the
741            // two commands; the connection is then attached nowhere, and its
742            // own task tears it down.
743            let _received = handover.view.send(held);
744            self.tell(&VoiceEvent::Migrated {
745                connection,
746                to: handover.to,
747            });
748            return;
749        }
750
751        let mut ops: Vec<PlanOp> = plan(&held, &ShardView::empty())
752            .into_iter()
753            .map(|planned| planned.op)
754            .collect();
755        collapse(&mut ops);
756        if !ops.is_empty() {
757            // A connection on its way out has nothing to retry with, so a
758            // refusal here is an outcome rather than a fault.
759            match attached.queue.try_send_all(emit(&ops, attached.session)) {
760                Ok(()) => attached.commit(
761                    self.version,
762                    ScopeSet::NONE,
763                    Overlay::default(),
764                    Actions::default(),
765                ),
766                Err(_refused) => attached.queue.mark_fatal(),
767            }
768        }
769
770        self.tell(&VoiceEvent::Disconnected {
771            connection,
772            reason: reason.to_owned(),
773        });
774    }
775
776    /// Resolve a client's channel request against what that client can see.
777    ///
778    /// Refusing an id the connection does not observe is not politeness: it
779    /// keeps a guessed number from working as an existence oracle for channels
780    /// in another team's subtree.
781    fn requested(&mut self, connection: ConnectionId, channel: ChannelId) {
782        let Some(attached) = self.connections.get(&connection) else {
783            return;
784        };
785        let key = self
786            .visible_channel(attached, channel)
787            .map(|rendered| rendered.key);
788
789        match key {
790            Some(key) => self.tell(&VoiceEvent::RequestedChannel {
791                connection,
792                channel: key,
793            }),
794            // Fail closed and stay audible: an operator seeing this repeatedly
795            // is looking at either a stale client or a probe.
796            None => eprintln!(
797                "mumble-server-runtime-shard: shard {:?}: {connection:?} asked for channel {channel:?}, which it \
798                 cannot see",
799                self.id
800            ),
801        }
802    }
803
804    /// A channel as this connection can see it: shared and observed, or held
805    /// from before its first transition, or private to it.
806    ///
807    /// The three places are the whole of what a client may legitimately name.
808    /// Answering about anything else, however harmlessly, would turn a guessed
809    /// identifier into an existence oracle for another team's subtree.
810    fn visible_channel<'a>(
811        &'a self,
812        attached: &'a AttachedConnection,
813        channel: ChannelId,
814    ) -> Option<&'a Channel> {
815        self.view
816            .channels
817            .get(&channel)
818            .filter(|rendered| attached.see.sees(rendered.scope))
819            .or_else(|| {
820                attached
821                    .held
822                    .as_ref()
823                    .and_then(|held| held.channels.get(&channel))
824            })
825            .or_else(|| attached.overlay_sent.channels.get(&channel))
826    }
827
828    /// A user as this connection can see it. The same three places, and the same
829    /// reason.
830    fn seen_user<'a>(
831        &'a self,
832        attached: &'a AttachedConnection,
833        session: SessionId,
834    ) -> Option<&'a User> {
835        self.view
836            .users
837            .get(&session)
838            .filter(|user| attached.see.sees(user.scope))
839            .or_else(|| {
840                attached
841                    .held
842                    .as_ref()
843                    .and_then(|held| held.users.get(&session))
844            })
845            .or_else(|| attached.overlay_sent.users.get(&session))
846    }
847
848    /// Resolve an invocation against what this connection was granted and what
849    /// it can see, then report it.
850    ///
851    /// Three refusals, all silent to the client and audible to an operator, for
852    /// the same reason [`Shard::requested`] refuses: an answer that varied with
853    /// whether the target exists would turn a guessed identifier into an oracle.
854    ///
855    /// The target is chosen by the bits the flavor declared, most specific
856    /// first, rather than by what the message carries. The client reads the
857    /// tree's current selection whichever menu the action came from, so a server
858    /// action routinely arrives with a session and a channel it has nothing to
859    /// do with.
860    ///
861    /// REF: references/mumble/src/mumble/MainWindow.cpp : `context_triggered`
862    ///   fills `session` and `channel_id` from `qtvUsers->currentIndex()`, while
863    ///   server actions live in `qmServer` and are never told about it.
864    fn invoked(
865        &mut self,
866        connection: ConnectionId,
867        action: &str,
868        session: Option<SessionId>,
869        channel: Option<ChannelId>,
870    ) {
871        let Some(attached) = self.connections.get(&connection) else {
872            return;
873        };
874
875        let Some(key) = crate::emit::action_key(action) else {
876            eprintln!(
877                "mumble-server-runtime-shard: shard {:?}: {connection:?} invoked {action:?}, which is not a name \
878                 this server writes",
879                self.id
880            );
881            return;
882        };
883        // What the client holds, not what the last render wanted it to hold: an
884        // action withdrawn in a turn this connection has not received yet is
885        // still legitimately on its screen. The flavor keeps the last word and
886        // can refuse out loud.
887        let Some(offered) = attached.actions_sent.get(&key) else {
888            eprintln!(
889                "mumble-server-runtime-shard: shard {:?}: {connection:?} invoked action {key:?}, which it was \
890                 never offered",
891                self.id
892            );
893            return;
894        };
895        let on = offered.on;
896
897        let target = if let Some(session) = session.filter(|_| on.covers(On::USER)) {
898            // Named a user and the action is about users: it must be a user this
899            // connection has been told about, and no fallback softens that.
900            self.seen_user(attached, session)
901                .map(|user| ActionTarget::User(user.occupant))
902        } else if let Some(channel) = channel.filter(|_| on.covers(On::CHANNEL)) {
903            self.visible_channel(attached, channel)
904                .map(|rendered| ActionTarget::Channel(rendered.key))
905        } else if on.covers(On::SERVER) {
906            Some(ActionTarget::Server)
907        } else {
908            None
909        };
910
911        match target {
912            Some(on) => self.tell(&VoiceEvent::InvokedAction {
913                connection,
914                action: key,
915                on,
916            }),
917            None => eprintln!(
918                "mumble-server-runtime-shard: shard {:?}: {connection:?} invoked action {key:?} on a target it \
919                 cannot see, or that the action was not offered on",
920                self.id
921            ),
922        }
923    }
924
925    /// Resolve where a client aimed a text message, then report it.
926    ///
927    /// Two different refusals, and the difference is the whole point:
928    ///
929    /// - A target this connection cannot see is **silent** to the client and
930    ///   audible to an operator, like every other unseen target here. An answer
931    ///   that varied with whether the id exists would turn a guessed number into
932    ///   an existence oracle.
933    /// - A target it *can* see but that the flavor rendered read-only is refused
934    ///   **out loud**, with the missing permission. Nothing leaks: the client is
935    ///   already holding that channel, and it asked to do something the answer to
936    ///   `PermissionQuery` had already denied.
937    ///
938    /// A private message is checked against the recipient's channel rather than
939    /// the sender's, which is what the reference server does.
940    ///
941    /// REF: references/mumble/src/murmur/Messages.cpp : `msgTextMessage` checks
942    ///   `ChanACL::TextMessage` on each named channel, and on `u->cChannel` for a
943    ///   directly addressed user.
944    fn said(&mut self, connection: ConnectionId, to: TextTarget, message: String) {
945        let Some(attached) = self.connections.get(&connection) else {
946            return;
947        };
948        let session = attached.session;
949
950        // Resolved into the flavor's vocabulary, and only through what this
951        // connection actually holds. `writable_in` is the channel whose
952        // `can_text` decides: the one named, or the recipient's own for a
953        // private message.
954        let resolved = match to {
955            TextTarget::Session(target) => self
956                .seen_user(attached, target)
957                .map(|user| (Audience::User(user.occupant), user.channel)),
958            TextTarget::Channel(channel) => self
959                .visible_channel(attached, channel)
960                .map(|rendered| (Audience::Channel(rendered.key), channel)),
961            TextTarget::Tree(channel) => self
962                .visible_channel(attached, channel)
963                .map(|rendered| (Audience::Tree(rendered.key), channel)),
964        };
965        let Some((audience, writable_in)) = resolved else {
966            eprintln!(
967                "mumble-server-runtime-shard: shard {:?}: {connection:?} wrote to {to:?}, which it cannot see",
968                self.id
969            );
970            return;
971        };
972
973        // For a tree that is its root, deliberately: the flavor chooses the
974        // audience it actually delivers to, so refusing further down would be
975        // refusing on behalf of a decision it has not made yet.
976        let writable = self
977            .visible_channel(attached, writable_in)
978            .is_some_and(|rendered| rendered.can_text);
979        if !writable {
980            self.answer(
981                attached,
982                crate::emit::denied_permission(
983                    session,
984                    writable_in,
985                    crate::emit::perm::TEXT_MESSAGE,
986                ),
987            );
988            return;
989        }
990
991        self.tell(&VoiceEvent::Said {
992            connection,
993            to: audience,
994            text: message,
995        });
996    }
997
998    /// Answer "what may I do in that channel", from the current render.
999    ///
1000    /// The flavor is not consulted and has nothing to decide: it already said
1001    /// everything it had to say by rendering the channel, and a query is not an
1002    /// intent. Keeping it here also keeps it cheap - one lookup, one message -
1003    /// where a round trip through business code would cost a turn.
1004    fn queried_permissions(&self, connection: ConnectionId, channel: ChannelId) {
1005        let Some(attached) = self.connections.get(&connection) else {
1006            return;
1007        };
1008        match self.visible_channel(attached, channel) {
1009            Some(rendered) => self.answer(attached, crate::emit::permission_query(rendered)),
1010            None => eprintln!(
1011                "mumble-server-runtime-shard: shard {:?}: {connection:?} queried permissions on channel \
1012                 {channel:?}, which it cannot see",
1013                self.id
1014            ),
1015        }
1016    }
1017
1018    /// Answer "what do you publish about that user", for a user it can see.
1019    fn queried_user_stats(&self, connection: ConnectionId, target: SessionId) {
1020        let Some(attached) = self.connections.get(&connection) else {
1021            return;
1022        };
1023        if self.seen_user(attached, target).is_some() {
1024            self.answer(attached, crate::emit::user_stats(target));
1025        } else {
1026            eprintln!(
1027                "mumble-server-runtime-shard: shard {:?}: {connection:?} queried stats about session {target:?}, \
1028                 which it cannot see",
1029                self.id
1030            );
1031        }
1032    }
1033
1034    /// Push one message that answers a question, rather than describing a
1035    /// change.
1036    ///
1037    /// Refusing it is an outcome, not a fault: a query the client can simply ask
1038    /// again is worth less than the transitions queued ahead of it, so a
1039    /// congested connection drops the answer instead of being torn down for it.
1040    fn answer(&self, attached: &AttachedConnection, message: ControlMessage) {
1041        if let Err(refused) = attached.queue.try_send_all(vec![message]) {
1042            eprintln!(
1043                "mumble-server-runtime-shard: shard {:?}: dropping an answer for {:?}: {refused}",
1044                self.id, attached.id
1045            );
1046        }
1047    }
1048
1049    /// Report an event to the flavor, and deliver whatever it said.
1050    ///
1051    /// The single door between the runtime and the business model: the flavor
1052    /// takes the event, updates its own state, and writes into a [`Reply`] that
1053    /// borrows nothing, so what it says is delivered here rather than from
1054    /// inside its own call.
1055    ///
1056    /// A word aimed at a connection this shard does not hold is dropped with a
1057    /// log rather than refused loudly. That is the normal shape of a departure:
1058    /// [`Shard::detach`] removes the connection **before** reporting it, so a
1059    /// flavor saying goodbye is speaking to a socket that is already leaving.
1060    fn tell(&mut self, event: &VoiceEvent) {
1061        let mut reply = Reply::default();
1062        self.logic.observe(event, &mut reply);
1063
1064        // Words first: a flavor that says goodbye and switches in the same
1065        // breath must have the farewell on the socket before the connection is
1066        // handed over.
1067        for (connection, words) in reply.drain() {
1068            let Some(attached) = self.connections.get(&connection) else {
1069                eprintln!(
1070                    "mumble-server-runtime-shard: shard {:?}: dropping {} word(s) for {connection:?}, which is \
1071                     not attached here",
1072                    self.id,
1073                    words.len()
1074                );
1075                continue;
1076            };
1077            let messages: Vec<ControlMessage> = words
1078                .iter()
1079                .map(|word| crate::emit::spoken(attached.session, word))
1080                .collect();
1081            // Same bargain as an answer: speech the client can live without is
1082            // worth less than the transitions queued ahead of it.
1083            if let Err(refused) = attached.queue.try_send_all(messages) {
1084                eprintln!(
1085                    "mumble-server-runtime-shard: shard {:?}: dropping what the flavor said to {connection:?}: \
1086                     {refused}",
1087                    self.id
1088                );
1089            }
1090        }
1091
1092        for spoken in reply.drain_spoken() {
1093            self.deliver(&spoken);
1094        }
1095
1096        for effect in reply.drain_effects() {
1097            match &self.effects {
1098                Some(route) => route(effect),
1099                None => eprintln!(
1100                    "mumble-server-runtime-shard: shard {:?}: dropping {effect:?}: this shard belongs to no \
1101                     runtime, so nothing can carry it out",
1102                    self.id
1103                ),
1104            }
1105        }
1106    }
1107
1108    /// Expand an audience and carry one message to each of its members.
1109    ///
1110    /// The expansion happens here rather than in the flavor because it is the
1111    /// only place that holds the view - and it is the composed view, per
1112    /// recipient, so an overlay placement counts as being somewhere just as much
1113    /// as a shared one does.
1114    ///
1115    /// Two members are left out, for reasons that are not politeness:
1116    ///
1117    /// - The speaker, which the reference server drops too. A client already
1118    ///   printed what it typed.
1119    /// - Anyone who cannot see the speaker. That is the audio coupling rule -
1120    ///   a receiver must see the sender - and it is also forced: a `TextMessage`
1121    ///   naming a session the recipient does not hold breaks the view invariants
1122    ///   the conformance model checks. A flavor that wants everyone to read
1123    ///   something whoever said it uses [`crate::reply::Reply::announce`].
1124    ///
1125    /// REF: references/mumble/src/murmur/Messages.cpp : `msgTextMessage` ends on
1126    ///   `users.remove(uSource)` before forwarding.
1127    fn deliver(&self, spoken: &Spoken) {
1128        let speaker = spoken
1129            .from
1130            .and_then(|connection| self.connections.get(&connection))
1131            .map(|attached| attached.session);
1132        if spoken.from.is_some() && speaker.is_none() {
1133            eprintln!(
1134                "mumble-server-runtime-shard: shard {:?}: dropping a relay from {:?}, which is not attached here",
1135                self.id, spoken.from
1136            );
1137            return;
1138        }
1139
1140        // Resolved once rather than per recipient: a key stands for the same id
1141        // whoever is looking, and the alternative is a scan of the render for
1142        // every connection in the shard.
1143        let Some(target) = self.resolve(spoken.to) else {
1144            eprintln!(
1145                "mumble-server-runtime-shard: shard {:?}: dropping a message for {:?}, which names nothing this \
1146                 shard has rendered",
1147                self.id, spoken.to
1148            );
1149            return;
1150        };
1151
1152        for attached in self.connections.values() {
1153            if Some(attached.id) == spoken.from {
1154                continue;
1155            }
1156            if !self.addressed(attached, target) {
1157                continue;
1158            }
1159            if let Some(session) = speaker
1160                && self.seen_user(attached, session).is_none()
1161            {
1162                eprintln!(
1163                    "mumble-server-runtime-shard: shard {:?}: {:?} is in the audience but cannot see session \
1164                     {session:?}, so it is skipped rather than told the message came from nobody",
1165                    self.id, attached.id
1166                );
1167                continue;
1168            }
1169            // Same bargain as an answer: a message the client can live without
1170            // is worth less than the transitions queued ahead of it.
1171            if let Err(refused) = attached.queue.try_send_all(vec![crate::emit::relayed(
1172                speaker,
1173                target,
1174                &spoken.text,
1175            )]) {
1176                eprintln!(
1177                    "mumble-server-runtime-shard: shard {:?}: dropping a relay to {:?}: {refused}",
1178                    self.id, attached.id
1179                );
1180            }
1181        }
1182    }
1183
1184    /// Turn what a flavor named into what a client holds.
1185    ///
1186    /// The same identifier for every recipient, which is why this happens once
1187    /// per message: the wire ids are the shard's, not the observer's. What
1188    /// differs per observer is only whether they are *in* the audience, and that
1189    /// is [`Shard::addressed`].
1190    fn resolve(&self, audience: Audience) -> Option<TextTarget> {
1191        match audience {
1192            Audience::User(occupant) => self
1193                .ids
1194                .allocated_session(occupant)
1195                .map(TextTarget::Session),
1196            Audience::Channel(key) => self.channel_of(key).map(TextTarget::Channel),
1197            Audience::Tree(key) => self.channel_of(key).map(TextTarget::Tree),
1198        }
1199    }
1200
1201    /// Whether `attached` is one of the recipients `target` stands for.
1202    ///
1203    /// Read through [`Shard::seen_user`], so a connection placed by an overlay is
1204    /// where its own client believes it is, which is the only answer that makes
1205    /// sense to the person reading the message.
1206    fn addressed(&self, attached: &AttachedConnection, target: TextTarget) -> bool {
1207        match target {
1208            TextTarget::Session(session) => session == attached.session,
1209            TextTarget::Channel(channel) => self
1210                .seen_user(attached, attached.session)
1211                .is_some_and(|user| user.channel == channel),
1212            TextTarget::Tree(root) => self
1213                .seen_user(attached, attached.session)
1214                .is_some_and(|user| self.descends_from(attached, user.channel, root)),
1215        }
1216    }
1217
1218    /// The id a flavor's channel key currently stands for.
1219    ///
1220    /// The render is consulted first because the root does not go through the
1221    /// allocator at all - it *is* [`ChannelId::ROOT`] in every shard - and asking
1222    /// the allocator for it would answer "no such channel" about the one channel
1223    /// everybody can see. The allocator answers for the rest, including a channel
1224    /// that only lives in somebody's overlay.
1225    fn channel_of(&self, key: ChannelKey) -> Option<ChannelId> {
1226        self.view
1227            .channels
1228            .values()
1229            .find(|channel| channel.key == key)
1230            .map(|channel| channel.id)
1231            .or_else(|| self.ids.allocated_channel(self.id, key))
1232    }
1233
1234    /// Whether `channel` is `root` or sits below it, walking the view this
1235    /// connection composes.
1236    ///
1237    /// Bounded by the tree it walks rather than by a counter: the root is its own
1238    /// parent, so the walk always ends there, and a channel whose parent has
1239    /// dropped out of view ends it early.
1240    fn descends_from(
1241        &self,
1242        attached: &AttachedConnection,
1243        channel: ChannelId,
1244        root: ChannelId,
1245    ) -> bool {
1246        let mut current = channel;
1247        loop {
1248            if current == root {
1249                return true;
1250            }
1251            let Some(rendered) = self.visible_channel(attached, current) else {
1252                return false;
1253            };
1254            if rendered.parent == current {
1255                return false;
1256            }
1257            current = rendered.parent;
1258        }
1259    }
1260
1261    /// Report a client's request about its own audio state to the flavor.
1262    ///
1263    /// There is nothing to resolve against the view: the request names no
1264    /// element, only the connection making it, so the visibility question that
1265    /// guards [`Shard::requested`] does not arise. It is still refused for a
1266    /// connection this shard does not hold, so a command that raced a detach
1267    /// cannot reach the flavor as if the connection were still here.
1268    fn requested_self_state(
1269        &mut self,
1270        connection: ConnectionId,
1271        self_mute: Option<bool>,
1272        self_deaf: Option<bool>,
1273    ) {
1274        if !self.connections.contains_key(&connection) {
1275            eprintln!(
1276                "mumble-server-runtime-shard: shard {:?}: {connection:?} asked to change its own state, but it \
1277                 is not attached here",
1278                self.id
1279            );
1280            return;
1281        }
1282        self.tell(&VoiceEvent::RequestedSelfState {
1283            connection,
1284            self_mute,
1285            self_deaf,
1286        });
1287    }
1288
1289    /// Retry one connection whose queue drained, without touching the others.
1290    ///
1291    /// A retry is the **fast path and nothing else**: it replays the shared
1292    /// journal from the connection's cursor. A connection that still holds a view
1293    /// this shard did not produce - a fresh attach, or one just handed over by a
1294    /// migration - has no cursor into this journal that means anything, and the
1295    /// transition it is owed is a replan against the render. So it is left alone
1296    /// here, and [`Shard::reconcile`] does it: `held` keeps it in `moved` every
1297    /// turn until the transition actually lands.
1298    ///
1299    /// Pushing it anyway is not merely early, it is destructive: the replay from
1300    /// `cursor` to `head` is empty, `push` concludes there is nothing to send and
1301    /// commits, and committing drops `held`. The view the client really holds is
1302    /// then gone, the replan plans from an empty one, and every element of the
1303    /// source shard survives the migration with nothing left to remove it.
1304    fn retry(&mut self, connection: ConnectionId) {
1305        let head = self.journal.head();
1306        let mut retired_channels = BTreeSet::new();
1307        if let Some(attached) = self.connections.get_mut(&connection) {
1308            if attached.held.is_some() {
1309                return;
1310            }
1311            // The private parts are whatever it last received: a retry replays the
1312            // shared journal, and any private change will arrive with the next
1313            // render. Cloning here keeps `push` free of a self-borrow.
1314            let overlay = attached.overlay_sent.clone();
1315            let actions = attached.actions_sent.clone();
1316            let (_outcome, retired) = push(attached, &self.journal, head, &overlay, &actions);
1317            retired_channels = retired;
1318        }
1319        self.ids.retire_channel_ids(self.id, &retired_channels);
1320    }
1321
1322    /// Render, plan, journal, publish routing, and push to every connection.
1323    pub fn reconcile(&mut self) -> ReconcileReport {
1324        let attached: Vec<ConnectionId> = self.connections.keys().copied().collect();
1325        let observations: BTreeMap<ConnectionId, ScopeSet> = attached
1326            .iter()
1327            .map(|connection| (*connection, self.logic.observation(*connection)))
1328            .collect();
1329
1330        let rendered = {
1331            let mut builder = ShardBuilder::new(&self.ids, self.id, &attached);
1332            self.logic.render(&mut builder);
1333            match builder.finish(&observations) {
1334                Ok(rendered) => rendered,
1335                Err(refused) => {
1336                    // Rule 4: keep the current view, close nothing. The
1337                    // committed view is still correct and reconnecting would
1338                    // only reproduce the same broken render.
1339                    return ReconcileReport {
1340                        version: self.version,
1341                        refused: Some(refused),
1342                        ..ReconcileReport::default()
1343                    };
1344                }
1345            }
1346        };
1347
1348        let ops = plan(&self.view, &rendered.view);
1349        let moved: Vec<ConnectionId> = attached
1350            .iter()
1351            .copied()
1352            .filter(|connection| {
1353                let observed = observations
1354                    .get(connection)
1355                    .copied()
1356                    .unwrap_or(ScopeSet::NONE);
1357                self.connections.get(connection).is_some_and(|attached| {
1358                    // A connection holding a view this shard did not produce
1359                    // has to be replanned whatever its observation says, and
1360                    // that includes the case where both are empty.
1361                    attached.held.is_some() || attached.see != observed
1362                })
1363            })
1364            .collect();
1365        let overlays_changed = attached.iter().any(|connection| {
1366            let fresh = rendered.overlays.get(connection);
1367            let sent = self.connections.get(connection).map(|c| &c.overlay_sent);
1368            match (fresh, sent) {
1369                (Some(fresh), Some(sent)) => fresh != sent,
1370                (Some(fresh), None) => !fresh.is_empty(),
1371                (None, Some(sent)) => !sent.is_empty(),
1372                (None, None) => false,
1373            }
1374        });
1375        // Offering or withdrawing a button changes neither the shared view nor
1376        // any observation, so without this term the render would be believed
1377        // unchanged and the menu would never move.
1378        let actions_changed = attached.iter().any(|connection| {
1379            let fresh = rendered.actions.get(connection);
1380            let sent = self.connections.get(connection).map(|c| &c.actions_sent);
1381            match (fresh, sent) {
1382                (Some(fresh), Some(sent)) => fresh != sent,
1383                (Some(fresh), None) => !fresh.is_empty(),
1384                (None, Some(sent)) => !sent.is_empty(),
1385                (None, None) => false,
1386            }
1387        });
1388
1389        if ops.is_empty() && moved.is_empty() && !overlays_changed && !actions_changed {
1390            return ReconcileReport {
1391                version: self.version,
1392                ..ReconcileReport::default()
1393            };
1394        }
1395
1396        // The version indexes the journal, and only the shared delta goes in
1397        // it: an observation change is replanned from the views directly and an
1398        // overlay change is recomputed from `overlay_sent`. So an empty delta
1399        // must not advance the version, or a connection that stays congested
1400        // would churn versions every turn and push the journal's tail past what
1401        // its peers still need.
1402        let delta_len = ops.len();
1403        let published = !ops.is_empty();
1404        if published {
1405            self.version = self.version.saturating_add(1);
1406            self.journal.push(ops);
1407        }
1408        let retained_channels: BTreeSet<ChannelKey> = rendered
1409            .view
1410            .channels
1411            .values()
1412            .chain(
1413                rendered
1414                    .overlays
1415                    .values()
1416                    .flat_map(|overlay| overlay.channels.values()),
1417            )
1418            .filter(|channel| channel.id != ChannelId::ROOT)
1419            .map(|channel| channel.key)
1420            .collect();
1421        let previous = std::mem::replace(&mut self.view, rendered.view);
1422        // The render is valid and has replaced the desired view. Any key absent
1423        // from both its shared and private parts has now been withdrawn from
1424        // every client view; if it returns, the wire id must not.
1425        self.ids.retain_channels(self.id, &retained_channels);
1426
1427        // A session that has just appeared is dated to this version, so the
1428        // voice plane can tell a receiver has not been told about it yet.
1429        let mut since_changed = false;
1430        for session in self.view.users.keys() {
1431            if !self.since.contains_key(session) {
1432                self.since.insert(*session, self.version);
1433                since_changed = true;
1434            }
1435        }
1436
1437        // Rule 2: publish routing BEFORE pushing views.
1438        //
1439        // The flags are read exactly where the sessions are, and from the same
1440        // presence: whichever rendering of a user is the one the audio plane
1441        // will name it by is also the one that decides whether it may speak.
1442        let mut session_of: BTreeMap<ConnectionId, SessionId> = BTreeMap::new();
1443        let mut silence = Silence::default();
1444        for user in self.view.users.values() {
1445            silence.record(user.session, user.flags);
1446            if let Some(connection) = user.occupant.connection() {
1447                session_of.insert(connection, user.session);
1448            }
1449        }
1450        // A connection with no shared presence is not absent from the runtime,
1451        // only from the shared view: that is exactly what a vanish is. Leaving it
1452        // out here would quietly turn "hears everything, heard by nobody" into
1453        // "takes no part in audio at all", and the flavor would have no way to
1454        // tell the two apart. Whether any given receiver may actually hear it is
1455        // a separate question, and the render already refuses a relation where
1456        // the answer is no.
1457        for (connection, overlay) in &rendered.overlays {
1458            if session_of.contains_key(connection) {
1459                continue;
1460            }
1461            let own = overlay
1462                .users
1463                .values()
1464                .find(|user| user.occupant == Occupant::Connection(*connection));
1465            if let Some(user) = own {
1466                session_of.insert(*connection, user.session);
1467                silence.record(user.session, user.flags);
1468            }
1469        }
1470        // Muting somebody changes neither the declared relation nor the session
1471        // map, so without this term the table would keep every route the flavor
1472        // declared and the flag would be pure decoration.
1473        let routing_recompiled = since_changed
1474            || rendered.audio != self.declared_audio
1475            || session_of != self.session_of
1476            || silence != self.declared_silence;
1477        if routing_recompiled {
1478            let table = compile(&rendered.audio, &session_of, &self.since, &silence);
1479            // `send_replace` rather than `send`: the latter reports "no reader"
1480            // as an error and, crucially, does **not** store the value. A shard
1481            // that rendered before its voice plane subscribed would then keep
1482            // publishing tables into a channel that still held the empty one,
1483            // and every route would be silently missing.
1484            let _previous = self.routing.send_replace(Arc::new(table));
1485            self.declared_audio = rendered.audio;
1486            self.session_of = session_of;
1487            self.declared_silence = silence;
1488        }
1489
1490        let head = self.version;
1491        let moved_set: BTreeSet<ConnectionId> = moved.iter().copied().collect();
1492        let mut closed = Vec::new();
1493        let mut retired_channels = BTreeSet::new();
1494        for attached in self.connections.values_mut() {
1495            let overlay = rendered
1496                .overlays
1497                .get(&attached.id)
1498                .cloned()
1499                .unwrap_or_default();
1500            let actions = rendered
1501                .actions
1502                .get(&attached.id)
1503                .cloned()
1504                .unwrap_or_default();
1505            let (outcome, retired) = if moved_set.contains(&attached.id) {
1506                let new_see = observations
1507                    .get(&attached.id)
1508                    .copied()
1509                    .unwrap_or(ScopeSet::NONE);
1510                replan(
1511                    attached, &previous, &self.view, head, new_see, &overlay, &actions,
1512                )
1513            } else {
1514                push(attached, &self.journal, head, &overlay, &actions)
1515            };
1516            retired_channels.extend(retired);
1517            if outcome == Outcome::Close {
1518                attached.queue.mark_fatal();
1519                closed.push(attached.id);
1520            }
1521        }
1522        // Mumble clients remember every ChannelId they have removed. Since ids
1523        // are shard-global, one accepted removal retires that wire id for the
1524        // whole shard; the next render will rotate it for any peers that still
1525        // see the semantic channel.
1526        self.ids.retire_channel_ids(self.id, &retired_channels);
1527
1528        ReconcileReport {
1529            version: self.version,
1530            published,
1531            delta_len,
1532            replanned: moved,
1533            routing_recompiled,
1534            closed,
1535            refused: None,
1536        }
1537    }
1538}
1539
1540/// What a push or replan concluded for one connection.
1541#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1542enum Outcome {
1543    /// Delivered, or nothing to deliver.
1544    Advanced,
1545    /// The queue was full. Nothing moved; `Drained` will retry.
1546    Congested,
1547    /// Unrecoverable: tear the connection down and let it rebuild.
1548    Close,
1549}
1550
1551/// Fast path: this connection's observation has not moved.
1552fn push(
1553    attached: &mut AttachedConnection,
1554    journal: &Journal,
1555    head: u64,
1556    overlay: &Overlay,
1557    actions: &Actions,
1558) -> (Outcome, BTreeSet<ChannelId>) {
1559    let Ok(replayed) = journal.replay(attached.cursor, head) else {
1560        // Below the tail: unrepairable from deltas, and dying anyway.
1561        return (Outcome::Close, BTreeSet::new());
1562    };
1563
1564    let shared = filter(replayed, attached.see);
1565    let private = plan_elements(&attached.overlay_sent, overlay);
1566    let mut ops = crate::compose::splice(shared, private);
1567    collapse(&mut ops);
1568
1569    let session = attached.session;
1570    let mut messages = emit(&ops, session);
1571    // Buttons ride with the view rather than beside it: one refusal, one retry,
1572    // and a menu that can never describe a turn the client did not receive.
1573    messages.extend(crate::emit::actions(&attached.actions_sent, actions));
1574
1575    if messages.is_empty() {
1576        // Nothing visible changed for it. Advance the cursor anyway, otherwise
1577        // a connection that sees nothing change would drift off the tail of the
1578        // journal and be closed for no reason.
1579        attached.commit(head, attached.see, overlay.clone(), actions.clone());
1580        return (Outcome::Advanced, BTreeSet::new());
1581    }
1582
1583    match attached.queue.try_send_all(messages) {
1584        Ok(()) => {
1585            attached.commit(head, attached.see, overlay.clone(), actions.clone());
1586            (Outcome::Advanced, retired_channels(&ops))
1587        }
1588        Err(Refused::Congested { .. }) => (Outcome::Congested, BTreeSet::new()),
1589        Err(Refused::TooLarge { .. } | Refused::Closed) => (Outcome::Close, BTreeSet::new()),
1590    }
1591}
1592
1593/// Slow path: this connection's observation moved.
1594///
1595/// It has to learn its whole new subtree and forget the old one. No delta can
1596/// shorten that: the information it lacks is in **no** change, it was already
1597/// there.
1598///
1599/// This is the ordinary planner over two real views, so every ordering rule
1600/// holds by construction. It is deliberately not a detach followed by an
1601/// attach: whatever the two observations have in common - the public channels -
1602/// is left untouched, so the connection's own tree does not flicker.
1603fn replan(
1604    attached: &mut AttachedConnection,
1605    before: &ShardView,
1606    after: &ShardView,
1607    head: u64,
1608    new_see: ScopeSet,
1609    overlay: &Overlay,
1610    actions: &Actions,
1611) -> (Outcome, BTreeSet<ChannelId>) {
1612    let from = attached.composed(before);
1613    let to = after.restrict(new_see).compose(overlay);
1614
1615    let mut ops: Vec<PlanOp> = plan(&from, &to)
1616        .into_iter()
1617        .map(|planned| planned.op)
1618        .collect();
1619    collapse(&mut ops);
1620
1621    let session = attached.session;
1622    let mut messages = emit(&ops, session);
1623    messages.extend(crate::emit::actions(&attached.actions_sent, actions));
1624
1625    if messages.is_empty() {
1626        attached.commit(head, new_see, overlay.clone(), actions.clone());
1627        return (Outcome::Advanced, BTreeSet::new());
1628    }
1629
1630    match attached.queue.try_send_all(messages) {
1631        // Rule 3: the triplet advances together, and only here. The guide's
1632        // sketch assigns the new observation before sending; doing that would
1633        // let a congested connection keep the new observation with the old
1634        // view, and the next fast-path filter would use a scope set the client
1635        // was never told about.
1636        Ok(()) => {
1637            attached.commit(head, new_see, overlay.clone(), actions.clone());
1638            (Outcome::Advanced, retired_channels(&ops))
1639        }
1640        Err(Refused::Congested { .. }) => (Outcome::Congested, BTreeSet::new()),
1641        Err(Refused::TooLarge { .. } | Refused::Closed) => (Outcome::Close, BTreeSet::new()),
1642    }
1643}
1644
1645fn retired_channels(ops: &[PlanOp]) -> BTreeSet<ChannelId> {
1646    ops.iter()
1647        .filter_map(|op| match op {
1648            PlanOp::RemoveChannel(channel) => Some(*channel),
1649            _ => None,
1650        })
1651        .collect()
1652}
1653
1654/// The handle a flavor uses to say "my state changed".
1655///
1656/// Holds no state: a signal and a command channel. It is the only
1657/// business-to-runtime interface, and it carries no data.
1658#[derive(Debug, Clone)]
1659pub struct ShardHandle {
1660    shard: ShardId,
1661    wake: Arc<Notify>,
1662    commands: mpsc::Sender<ShardCommand>,
1663}
1664
1665impl ShardHandle {
1666    #[must_use]
1667    pub fn shard(&self) -> ShardId {
1668        self.shard
1669    }
1670
1671    /// "My state changed, re-render me when you can."
1672    pub fn wake(&self) {
1673        self.wake.notify_one();
1674    }
1675
1676    /// Queue a command for the shard's task.
1677    ///
1678    /// # Errors
1679    ///
1680    /// The command itself, when the shard's task has ended.
1681    pub fn send(
1682        &self,
1683        command: ShardCommand,
1684    ) -> Result<(), mpsc::error::TrySendError<ShardCommand>> {
1685        self.commands.try_send(command)
1686    }
1687}
1688
1689/// How many commands a shard's mailbox holds before senders are refused.
1690const MAILBOX_DEPTH: usize = 256;
1691
1692/// Drive a shard until every handle to it is dropped.
1693///
1694/// Takes the driver halves produced by [`spawn_parts`] and gives the shard back,
1695/// so a caller that wants to inspect or migrate it can.
1696pub async fn run<L: ShardLogic>(
1697    mut shard: Shard<L>,
1698    wake: Arc<Notify>,
1699    mut mailbox: mpsc::Receiver<ShardCommand>,
1700) -> Shard<L> {
1701    let mut dirty = false;
1702    let mut mailbox_open = true;
1703    let mut next_reconcile = Instant::now();
1704
1705    loop {
1706        if dirty && (!mailbox_open || Instant::now() >= next_reconcile) {
1707            mailbox_open = drain_commands(&mut shard, &mut mailbox);
1708            let _report = shard.reconcile();
1709            dirty = false;
1710            next_reconcile = Instant::now() + MIN_INTERVAL;
1711            if !mailbox_open {
1712                break;
1713            }
1714            continue;
1715        }
1716
1717        if !mailbox_open {
1718            break;
1719        }
1720
1721        if dirty {
1722            tokio::select! {
1723                biased;
1724                // Cancellation-safe: recreating Sleep with the same absolute
1725                // deadline neither loses nor extends the cooldown.
1726                () = tokio::time::sleep_until(next_reconcile) => {}
1727                // Cancellation-safe: `recv` consumes a command only when this
1728                // branch completes.
1729                command = mailbox.recv() => match command {
1730                    Some(command) => shard.handle(command),
1731                    None => mailbox_open = false,
1732                },
1733                // Cancellation-safe: a cancelled `Notified` returns its permit.
1734                // The flavor state is external, so the signal only keeps this
1735                // turn dirty.
1736                () = wake.notified() => {},
1737            }
1738        } else {
1739            tokio::select! {
1740                // Cancellation-safe: `recv` consumes a command only when this
1741                // branch completes.
1742                command = mailbox.recv() => match command {
1743                    Some(command) => {
1744                        shard.handle(command);
1745                        dirty = true;
1746                    }
1747                    // Every handle is gone; nothing can ever wake this shard again.
1748                    None => break,
1749                },
1750                // Cancellation-safe: a cancelled `Notified` returns its permit.
1751                () = wake.notified() => dirty = true,
1752            }
1753        }
1754    }
1755
1756    shard
1757}
1758
1759/// Absorb one bounded burst before publishing.
1760///
1761/// The bound guarantees that a producer which continuously refills the channel
1762/// cannot postpone reconciliation forever. Commands arriving after the batch
1763/// are still consumed during the publication cooldown.
1764fn drain_commands<L: ShardLogic>(
1765    shard: &mut Shard<L>,
1766    mailbox: &mut mpsc::Receiver<ShardCommand>,
1767) -> bool {
1768    for _ in 0..MAILBOX_DEPTH {
1769        match mailbox.try_recv() {
1770            Ok(command) => shard.handle(command),
1771            Err(TryRecvError::Empty) => return true,
1772            Err(TryRecvError::Disconnected) => return false,
1773        }
1774    }
1775    true
1776}
1777
1778/// The handle and the driver halves for one shard.
1779///
1780/// Split out so a test can drive [`Shard::handle`] and [`Shard::reconcile`] by
1781/// hand without a task at all, which is what keeps the whole control plane
1782/// testable without a clock.
1783#[must_use]
1784pub fn spawn_parts(shard: ShardId) -> (ShardHandle, Arc<Notify>, mpsc::Receiver<ShardCommand>) {
1785    let wake = Arc::new(Notify::new());
1786    let (commands, mailbox) = mpsc::channel(MAILBOX_DEPTH);
1787    (
1788        ShardHandle {
1789            shard,
1790            wake: Arc::clone(&wake),
1791            commands,
1792        },
1793        wake,
1794        mailbox,
1795    )
1796}