harn_vm/stdlib/
session_change.rs1use std::path::Path;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, RwLock, RwLockWriteGuard};
15
16use harn_session_store::{SessionMeta, SharedSessionChangeObserver};
17
18use super::session_wal_watch;
19
20static OBSERVERS: RwLock<Vec<(u64, SharedSessionChangeObserver)>> = RwLock::new(Vec::new());
33static NEXT_SUBSCRIPTION: AtomicU64 = AtomicU64::new(1);
34type TitleFingerprint = (Option<String>, bool);
35type RememberedTitles = Vec<(String, TitleFingerprint)>;
36static TITLES: RwLock<RememberedTitles> = RwLock::new(Vec::new());
37
38fn observers() -> RwLockWriteGuard<'static, Vec<(u64, SharedSessionChangeObserver)>> {
39 OBSERVERS
40 .write()
41 .unwrap_or_else(|poisoned| poisoned.into_inner())
42}
43
44fn titles() -> RwLockWriteGuard<'static, RememberedTitles> {
45 TITLES
46 .write()
47 .unwrap_or_else(|poisoned| poisoned.into_inner())
48}
49
50#[must_use = "dropping the subscription immediately unregisters the observer"]
54pub struct SessionChangeSubscription {
55 id: u64,
56}
57
58impl Drop for SessionChangeSubscription {
59 fn drop(&mut self) {
60 observers().retain(|(id, _)| *id != self.id);
61 let live = subscriber_count() > 0;
62 if !live {
63 titles().clear();
64 }
65 session_wal_watch::sync_watchers(live);
66 }
67}
68
69pub fn subscribe(observer: SharedSessionChangeObserver) -> SessionChangeSubscription {
77 let id = NEXT_SUBSCRIPTION.fetch_add(1, Ordering::Relaxed);
78 observers().push((id, observer));
79 session_wal_watch::sync_watchers(true);
80 SessionChangeSubscription { id }
81}
82
83#[must_use = "dropping the registration stops the path being watched"]
89pub(crate) fn watch_store(path: &Path) -> session_wal_watch::StoreWatchRegistration {
90 let registration = session_wal_watch::register_store_path(path);
91 if subscriber_count() > 0 {
92 session_wal_watch::sync_watchers(true);
93 }
94 registration
95}
96
97fn subscriber_count() -> usize {
98 OBSERVERS
99 .read()
100 .unwrap_or_else(|poisoned| poisoned.into_inner())
101 .len()
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub(super) enum TitleMemory {
107 New,
108 Unchanged,
109 Changed,
110}
111
112pub(super) fn remember_title(
113 session_id: &str,
114 title: Option<&str>,
115 title_pinned: bool,
116) -> TitleMemory {
117 let next = (title.map(str::to_string), title_pinned);
118 let mut titles = titles();
119 if let Some((_, previous)) = titles.iter_mut().find(|(id, _)| id == session_id) {
120 if *previous == next {
121 return TitleMemory::Unchanged;
122 }
123 *previous = next;
124 return TitleMemory::Changed;
125 }
126 titles.push((session_id.to_string(), next));
127 TitleMemory::New
128}
129
130pub(super) fn dispatch(meta: &SessionMeta) {
132 let observers: Vec<SharedSessionChangeObserver> = OBSERVERS
133 .read()
134 .unwrap_or_else(|poisoned| poisoned.into_inner())
135 .iter()
136 .map(|(_, observer)| Arc::clone(observer))
137 .collect();
138 for observer in observers {
141 observer.session_updated(meta);
142 }
143}
144
145struct SessionChangeFanout;
146
147impl harn_session_store::SessionChangeObserver for SessionChangeFanout {
148 fn session_updated(&self, meta: &SessionMeta) {
149 remember_title(&meta.id, meta.title.as_deref(), meta.title_pinned);
150 dispatch(meta);
151 }
152}
153
154pub(crate) fn current_observer() -> Option<SharedSessionChangeObserver> {
155 if subscriber_count() == 0 {
156 return None;
157 }
158 Some(Arc::new(SessionChangeFanout))
159}
160
161#[cfg(test)]
162pub(crate) mod test_support {
163 use std::sync::OnceLock;
164
165 use tokio::sync::{Mutex, MutexGuard};
166
167 #[must_use = "the bus is shared for as long as the guard lives"]
182 pub(crate) async fn exclusive_bus() -> MutexGuard<'static, ()> {
183 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
184 LOCK.get_or_init(|| Mutex::new(())).lock().await
185 }
186}