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}