Skip to main content

lattice_mode/
event.rs

1//! Typed internal mode-dispatch signals.
2//!
3//! MA.1 split the mode-event surface: the four **observable**
4//! lifecycle transitions (`MajorEntered` / `MajorExiting` /
5//! `MinorActivated` / `MinorDeactivated`, design.md §5.10.1) moved to
6//! the `lattice_protocol::Event` enum so hooks and the EF.1
7//! `EventFilter` apply to them uniformly (mode-architecture.md §7.4).
8//! What stays here is the dispatcher's **internal** signalling that
9//! isn't part of the public lifecycle catalog:
10//!
11//! - `ModeActivationFailed` — `on_activate` returned `Err`; the
12//!   App-side subscriber rolls back `active_modes` / `mode_guards`.
13//! - `OptionConflict` — two active minors disagree on an option
14//!   (M.2 emits this; reserved).
15//!
16//! Both ride the typed bus (`EventBus::publish_typed` /
17//! `subscribe_typed`) because they carry richer payloads (a reason
18//! string, a conflicting-mode set) and have a single internal
19//! consumer rather than open subscription.
20
21use lattice_protocol::ids::BufferId;
22use smallvec::SmallVec;
23
24use crate::error::ModeActivationError;
25use crate::mode::ModeId;
26
27/// Internal mode-dispatch signals on the typed event bus
28/// (`ModeActivationFailed` / `OptionConflict`). The observable
29/// lifecycle quartet lives on the [`lattice_protocol::Event`] enum
30/// (MA.1) — see the module docs.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum ModeEvent {
33    /// `on_activate` returned `Err`. Published from
34    /// the spawned lifecycle task instead of the success lifecycle
35    /// event. `active_modes` was mutated
36    /// synchronously by the dispatcher's sync prefix; M-async.3
37    /// adds an App-side subscriber that rolls back on this
38    /// event. `reason` is the boundary string of the original
39    /// `ModeActivationError` (the error type itself isn't `Eq` /
40    /// `Clone`-friendly across crate boundaries; the string is
41    /// what subscribers actually use) (M-async.2).
42    ModeActivationFailed {
43        /// The buffer the activation targeted.
44        buffer: BufferId,
45        /// The mode whose hook failed, or a later cascade step that never ran.
46        mode: ModeId,
47        /// The error's display string; for a skipped cascade step,
48        /// `"cascade aborted by <mode>"`.
49        reason: String,
50    },
51
52    /// Two active minor modes contributed conflicting values
53    /// for the same option. Reserved: declared for M.2's resolver, but
54    /// nothing publishes it today.
55    OptionConflict {
56        /// The buffer whose resolution saw the conflict.
57        buffer: BufferId,
58        /// The option's canonical name.
59        option: &'static str,
60        /// The minors that disagree.
61        modes: SmallVec<[ModeId; 2]>,
62    },
63}
64
65impl ModeEvent {
66    /// Build a `ModeActivationFailed` from a
67    /// [`ModeActivationError`]. Stringifies the error so the
68    /// event payload is `Clone + Eq` (the error variants
69    /// contain registry-internal types that aren't worth
70    /// threading through every subscriber).
71    pub fn activation_failed(buffer: BufferId, mode: ModeId, err: &ModeActivationError) -> Self {
72        Self::ModeActivationFailed {
73            buffer,
74            mode,
75            reason: err.to_string(),
76        }
77    }
78}
79
80// M-async.2: register `ModeEvent` as a typed event so the bus's
81// `publish_typed` / `subscribe_typed` API can carry it. One
82// type, one event name -- subscribers filter on payload variant.
83lattice_protocol::register_event!(
84    ModeEvent,
85    "mode.lifecycle",
86    "Mode lifecycle transitions (MajorEntered, MajorExiting, \
87     MinorActivated, MinorDeactivated, ModeActivationFailed).",
88    "lattice-mode",
89);
90
91#[cfg(test)]
92mod tests {
93    #![allow(clippy::unwrap_used)]
94    use super::*;
95
96    #[test]
97    fn variants_are_orderable_for_test_assertions() {
98        let buf = BufferId::new(1);
99        let mode = ModeId::new("test-mode");
100        let a = ModeEvent::ModeActivationFailed {
101            buffer: buf,
102            mode,
103            reason: "boom".into(),
104        };
105        let b = ModeEvent::ModeActivationFailed {
106            buffer: buf,
107            mode,
108            reason: "boom".into(),
109        };
110        assert_eq!(a, b);
111        let c = a.clone();
112        assert_eq!(a, c);
113    }
114
115    #[test]
116    fn activation_failed_builder_carries_reason() {
117        let buf = BufferId::new(1);
118        let mode = ModeId::new("x-mode");
119        let err = ModeActivationError::NotRegistered(mode);
120        let evt = ModeEvent::activation_failed(buf, mode, &err);
121        match evt {
122            ModeEvent::ModeActivationFailed {
123                buffer,
124                mode: m,
125                reason,
126            } => {
127                assert_eq!(buffer, buf);
128                assert_eq!(m, mode);
129                assert!(reason.contains("not registered"));
130            }
131            other => panic!("expected ModeActivationFailed, got {other:?}"),
132        }
133    }
134}