reovim_kernel/ipc/subscription.rs
1//! Subscription handle for event bus unsubscription.
2//!
3//! `Subscription` is an RAII handle that automatically unsubscribes from the
4//! event bus when dropped. This ensures handlers are properly cleaned up even
5//! in the presence of panics or early returns.
6//!
7//! # Design Philosophy
8//!
9//! Following Rust's RAII pattern:
10//! - Subscription creation is tied to handler registration
11//! - Dropping the subscription removes the handler
12//! - Explicit `cancel()` available for early unsubscription
13//!
14//! # Example
15//!
16//! ```ignore
17//! use reovim_kernel::ipc::{EventBus, Event, Subscription};
18//!
19//! #[derive(Debug)]
20//! struct MyEvent;
21//! impl Event for MyEvent {}
22//!
23//! let bus = EventBus::new();
24//!
25//! // Subscription returned from subscribe
26//! let sub = bus.subscribe::<MyEvent, _>(100, |_event| {
27//! println!("Event received!");
28//! EventResult::Handled
29//! });
30//!
31//! // Handler is active while `sub` is alive
32//! bus.emit(MyEvent);
33//!
34//! // Drop `sub` to unsubscribe
35//! drop(sub);
36//! ```
37
38use std::{
39 any::TypeId,
40 fmt,
41 sync::{
42 Arc,
43 atomic::{AtomicU64, Ordering},
44 },
45};
46
47use reovim_arch::sync::Mutex;
48
49/// Unique identifier for a subscription.
50///
51/// Each subscription gets a unique ID for identification and debugging.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub struct SubscriptionId(u64);
54
55impl SubscriptionId {
56 /// Create a new unique `SubscriptionId`.
57 pub(crate) fn new() -> Self {
58 static COUNTER: AtomicU64 = AtomicU64::new(1);
59 Self(COUNTER.fetch_add(1, Ordering::Relaxed))
60 }
61
62 /// Get the raw ID value.
63 #[inline]
64 #[must_use]
65 pub const fn as_u64(&self) -> u64 {
66 self.0
67 }
68}
69
70impl fmt::Display for SubscriptionId {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 write!(f, "sub-{}", self.0)
73 }
74}
75
76/// Callback type for unsubscription.
77type UnsubscribeFn = Box<dyn FnOnce() + Send + Sync>;
78
79/// RAII handle for event subscriptions.
80///
81/// When a `Subscription` is dropped, the associated handler is automatically
82/// removed from the event bus. This ensures proper cleanup even when code
83/// panics or returns early.
84///
85/// # Thread Safety
86///
87/// `Subscription` is `Send + Sync`, allowing it to be moved between threads
88/// and shared (though sharing is unusual since drop triggers unsubscription).
89///
90/// # Example
91///
92/// ```ignore
93/// // Subscription automatically unsubscribes when dropped
94/// {
95/// let sub = bus.subscribe::<MyEvent, _>(100, handler);
96/// // Handler is active here
97/// } // Handler is removed here
98/// ```
99pub struct Subscription {
100 /// Unique identifier for this subscription.
101 id: SubscriptionId,
102
103 /// `TypeId` of the event type (for debugging).
104 type_id: TypeId,
105
106 /// Type name of the event (for debugging).
107 type_name: &'static str,
108
109 /// Unsubscribe callback, called on drop.
110 /// Wrapped in `Arc<Mutex>` to allow the callback to capture &self references
111 /// from the `EventBus`.
112 unsubscribe_fn: Arc<Mutex<Option<UnsubscribeFn>>>,
113}
114
115impl Subscription {
116 /// Create a new subscription.
117 ///
118 /// This is called internally by `EventBus::subscribe()`.
119 pub(crate) fn new<E: 'static>(
120 id: SubscriptionId,
121 unsubscribe_fn: impl FnOnce() + Send + Sync + 'static,
122 ) -> Self {
123 Self {
124 id,
125 type_id: TypeId::of::<E>(),
126 type_name: std::any::type_name::<E>(),
127 unsubscribe_fn: Arc::new(Mutex::new(Some(Box::new(unsubscribe_fn)))),
128 }
129 }
130
131 /// Get the subscription ID.
132 #[inline]
133 #[must_use]
134 pub const fn id(&self) -> SubscriptionId {
135 self.id
136 }
137
138 /// Get the `TypeId` of the subscribed event type.
139 #[inline]
140 #[must_use]
141 pub const fn type_id(&self) -> TypeId {
142 self.type_id
143 }
144
145 /// Get the type name of the subscribed event type.
146 #[inline]
147 #[must_use]
148 pub const fn type_name(&self) -> &'static str {
149 self.type_name
150 }
151
152 /// Check if the subscription is still active.
153 ///
154 /// Returns `false` after `cancel()` has been called.
155 #[must_use]
156 pub fn is_active(&self) -> bool {
157 self.unsubscribe_fn.lock().is_some()
158 }
159
160 /// Explicitly cancel the subscription.
161 ///
162 /// This is equivalent to dropping the subscription, but allows for
163 /// explicit control over when unsubscription occurs.
164 ///
165 /// Calling `cancel()` multiple times is safe and idempotent.
166 pub fn cancel(&self) {
167 let unsub = self.unsubscribe_fn.lock().take();
168 if let Some(callback) = unsub {
169 callback();
170 }
171 }
172
173 /// Detach the subscription from RAII lifecycle management.
174 ///
175 /// After calling this, dropping the `Subscription` will NOT unsubscribe
176 /// the handler. The handler remains active until the `EventBus` is dropped.
177 ///
178 /// This is necessary when the owner of the `Subscription` has a shorter
179 /// lifetime than the desired handler lifetime (e.g., modules that are
180 /// dropped after initialization while their event handlers should persist
181 /// for the session).
182 ///
183 /// # Example
184 ///
185 /// ```ignore
186 /// let sub = bus.subscribe::<MyEvent, _>(100, handler);
187 /// sub.detach(); // Handler stays active even after `sub` is dropped
188 /// ```
189 pub fn detach(&self) {
190 // Remove the unsubscribe callback without invoking it.
191 // When self is later dropped, cancel() will find None and skip.
192 let _ = self.unsubscribe_fn.lock().take();
193 }
194}
195
196impl Drop for Subscription {
197 fn drop(&mut self) {
198 self.cancel();
199 }
200}
201
202impl fmt::Debug for Subscription {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 f.debug_struct("Subscription")
205 .field("id", &self.id)
206 .field("type_name", &self.type_name)
207 .field("active", &self.is_active())
208 .finish_non_exhaustive()
209 }
210}
211
212// Subscription is automatically Send + Sync because:
213// - SubscriptionId is Copy
214// - TypeId and &'static str are Send + Sync
215// - UnsubscribeFn is Box<dyn FnOnce() + Send + Sync>
216// - Arc<Mutex<...>> is Send + Sync
217//
218// No manual unsafe impl needed - Rust derives these automatically.