cranpose_services/
host_messages.rs1use std::{
14 collections::HashMap,
15 sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError, atomic::Ordering},
16};
17
18use cranpose_core::{EventStream, rememberEventStream};
19use portable_atomic::AtomicU64;
20
21#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct HostMessage {
24 pub channel: String,
26 pub payload: String,
28}
29
30impl HostMessage {
31 pub fn new(channel: impl Into<String>, payload: impl Into<String>) -> Self {
33 Self {
34 channel: channel.into(),
35 payload: payload.into(),
36 }
37 }
38}
39
40type Observer = Arc<dyn Fn(String) + Send + Sync>;
41type Outbox = Arc<dyn Fn(HostMessage) + Send + Sync>;
42
43struct Mailbox {
44 observers: Vec<(u64, String, Observer)>,
45 latest: HashMap<String, String>,
46}
47
48impl Mailbox {
49 fn new() -> Self {
50 Self {
51 observers: Vec::new(),
52 latest: HashMap::new(),
53 }
54 }
55
56 fn observe(&mut self, id: u64, channel: &str, observer: Observer) -> Option<String> {
57 self.observers.push((id, channel.to_owned(), observer));
58 self.latest.get(channel).cloned()
59 }
60
61 fn publish(&mut self, message: &HostMessage) -> Vec<Observer> {
62 self.latest
63 .insert(message.channel.clone(), message.payload.clone());
64 self.observers
65 .iter()
66 .filter(|(_, channel, _)| *channel == message.channel)
67 .map(|(_, _, observer)| Arc::clone(observer))
68 .collect()
69 }
70
71 fn remove_observer(&mut self, id: u64) {
72 self.observers.retain(|(existing, _, _)| *existing != id);
73 }
74
75 fn clear(&mut self) {
76 self.latest.clear();
77 }
78}
79
80fn mailbox() -> MutexGuard<'static, Mailbox> {
81 static MAILBOX: OnceLock<Mutex<Mailbox>> = OnceLock::new();
82 MAILBOX
83 .get_or_init(|| Mutex::new(Mailbox::new()))
84 .lock()
85 .unwrap_or_else(PoisonError::into_inner)
86}
87
88fn outbox() -> MutexGuard<'static, Option<Outbox>> {
89 static OUTBOX: OnceLock<Mutex<Option<Outbox>>> = OnceLock::new();
90 OUTBOX
91 .get_or_init(|| Mutex::new(None))
92 .lock()
93 .unwrap_or_else(PoisonError::into_inner)
94}
95
96static NEXT_ID: AtomicU64 = AtomicU64::new(1);
97
98pub struct HostMessageObserver {
100 id: u64,
101}
102
103impl Drop for HostMessageObserver {
104 fn drop(&mut self) {
105 mailbox().remove_observer(self.id);
106 }
107}
108
109pub fn observe_host_messages(
114 channel: &str,
115 observer: impl Fn(String) + Send + Sync + 'static,
116) -> HostMessageObserver {
117 let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
118 let observer: Observer = Arc::new(observer);
119 let replay = mailbox().observe(id, channel, Arc::clone(&observer));
120 if let Some(payload) = replay {
121 observer(payload);
122 }
123 HostMessageObserver { id }
124}
125
126pub fn publish_host_message(message: HostMessage) {
129 let observers = mailbox().publish(&message);
130 for observer in observers {
131 observer(message.payload.clone());
132 }
133}
134
135pub fn clear_host_messages() {
138 mailbox().clear();
139}
140
141pub fn install_host_outbox(outbox_fn: impl Fn(HostMessage) + Send + Sync + 'static) {
143 *outbox() = Some(Arc::new(outbox_fn));
144}
145
146pub fn clear_host_outbox() {
149 *outbox() = None;
150}
151
152pub fn send_to_host(channel: &str, payload: &str) -> bool {
157 let Some(outbox_fn) = outbox().clone() else {
158 log::debug!("host message on {channel} dropped: no host is attached");
159 return false;
160 };
161 outbox_fn(HostMessage::new(channel, payload));
162 true
163}
164
165#[expect(non_snake_case)]
180#[track_caller]
181pub fn rememberHostMessages(channel: &str) -> EventStream<String> {
182 let channel = channel.to_owned();
183 rememberEventStream(channel.clone(), move |sender| {
184 observe_host_messages(&channel, move |payload| sender.send(payload))
185 })
186}
187
188#[cfg(test)]
189#[path = "tests/host_messages_tests.rs"]
190mod tests;