cranpose_services/
host_messages.rs1use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError};
14
15use cranpose_core::{EventStream, ProcessIds, collections::map::HashMap, rememberEventStream};
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct HostMessage {
20 pub channel: String,
22 pub payload: String,
24}
25
26impl HostMessage {
27 pub fn new(channel: impl Into<String>, payload: impl Into<String>) -> Self {
29 Self {
30 channel: channel.into(),
31 payload: payload.into(),
32 }
33 }
34}
35
36type Observer = Arc<dyn Fn(String) + Send + Sync>;
37type Outbox = Arc<dyn Fn(HostMessage) + Send + Sync>;
38
39struct Mailbox {
40 observers: Vec<(u64, String, Observer)>,
41 latest: HashMap<String, String>,
42}
43
44impl Mailbox {
45 fn new() -> Self {
46 Self {
47 observers: Vec::new(),
48 latest: HashMap::default(),
49 }
50 }
51
52 fn observe(&mut self, id: u64, channel: &str, observer: Observer) -> Option<String> {
53 self.observers.push((id, channel.to_owned(), observer));
54 self.latest.get(channel).cloned()
55 }
56
57 fn publish(&mut self, message: &HostMessage) -> Vec<Observer> {
58 self.latest
59 .insert(message.channel.clone(), message.payload.clone());
60 self.observers
61 .iter()
62 .filter(|(_, channel, _)| *channel == message.channel)
63 .map(|(_, _, observer)| Arc::clone(observer))
64 .collect()
65 }
66
67 fn remove_observer(&mut self, id: u64) {
68 self.observers.retain(|(existing, _, _)| *existing != id);
69 }
70
71 fn clear(&mut self) {
72 self.latest.clear();
73 }
74}
75
76fn mailbox() -> MutexGuard<'static, Mailbox> {
77 static MAILBOX: OnceLock<Mutex<Mailbox>> = OnceLock::new();
78 MAILBOX
79 .get_or_init(|| Mutex::new(Mailbox::new()))
80 .lock()
81 .unwrap_or_else(PoisonError::into_inner)
82}
83
84fn outbox() -> MutexGuard<'static, Option<Outbox>> {
85 static OUTBOX: OnceLock<Mutex<Option<Outbox>>> = OnceLock::new();
86 OUTBOX
87 .get_or_init(|| Mutex::new(None))
88 .lock()
89 .unwrap_or_else(PoisonError::into_inner)
90}
91
92static NEXT_ID: ProcessIds = ProcessIds::starting_at(1);
93
94pub struct HostMessageObserver {
96 id: u64,
97}
98
99impl Drop for HostMessageObserver {
100 fn drop(&mut self) {
101 mailbox().remove_observer(self.id);
102 }
103}
104
105pub fn observe_host_messages(
110 channel: &str,
111 observer: impl Fn(String) + Send + Sync + 'static,
112) -> HostMessageObserver {
113 let id = NEXT_ID.next();
114 let observer: Observer = Arc::new(observer);
115 let replay = mailbox().observe(id, channel, Arc::clone(&observer));
116 if let Some(payload) = replay {
117 observer(payload);
118 }
119 HostMessageObserver { id }
120}
121
122pub fn publish_host_message(message: HostMessage) {
125 let observers = mailbox().publish(&message);
126 for observer in observers {
127 observer(message.payload.clone());
128 }
129}
130
131pub fn clear_host_messages() {
134 mailbox().clear();
135}
136
137pub fn install_host_outbox(outbox_fn: impl Fn(HostMessage) + Send + Sync + 'static) {
139 *outbox() = Some(Arc::new(outbox_fn));
140}
141
142pub fn clear_host_outbox() {
145 *outbox() = None;
146}
147
148pub fn send_to_host(channel: &str, payload: &str) -> bool {
153 let Some(outbox_fn) = outbox().clone() else {
154 log::debug!("host message on {channel} dropped: no host is attached");
155 return false;
156 };
157 outbox_fn(HostMessage::new(channel, payload));
158 true
159}
160
161#[expect(non_snake_case, reason = "names follow Compose's casing")]
176#[track_caller]
177pub fn rememberHostMessages(channel: &str) -> EventStream<String> {
178 let channel = channel.to_owned();
179 rememberEventStream(channel.clone(), move |sender| {
180 observe_host_messages(&channel, move |payload| sender.send(payload))
181 })
182}
183
184#[cfg(test)]
185#[path = "tests/host_messages_tests.rs"]
186mod tests;