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