Skip to main content

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.