Skip to main content

cranpose_services/
host_messages.rs

1//! Messages between the application and the program embedding it.
2//!
3//! An application launched inside another program — an IDE tool window, an
4//! editor panel — shares a message line with that host. Every message names a
5//! channel and carries a text payload; which channels exist and what their
6//! payloads hold is for the host and the application to agree on (JSON is the
7//! usual choice). The newest message on each channel is kept, so a screen that
8//! starts collecting after the host spoke still learns the current value.
9//!
10//! Outside a host nothing is ever received and [`send_to_host`] reports that
11//! no one listened, so the same application runs standalone unchanged.
12
13use 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/// One message on the line between the application and its host.
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct HostMessage {
24    /// The channel the message belongs to.
25    pub channel: String,
26    /// The message body.
27    pub payload: String,
28}
29
30impl HostMessage {
31    /// A message carrying `payload` on `channel`.
32    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
98/// Keeps a host message observer registered until it is dropped.
99pub 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
109/// Registers `observer` for the messages the host sends on `channel`, handing
110/// it the newest one at once when the host already spoke there.
111///
112/// Applications collect [`rememberHostMessages`] instead of calling this.
113pub 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
126/// Delivers a message the host sent to every observer of its channel.
127/// Callable from any thread; platform hosts call it as messages arrive.
128pub 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
135/// Forgets the newest message of every channel. Used by tests and host
136/// teardown.
137pub fn clear_host_messages() {
138    mailbox().clear();
139}
140
141/// Installs where [`send_to_host`] delivers: the platform host's connection.
142pub fn install_host_outbox(outbox_fn: impl Fn(HostMessage) + Send + Sync + 'static) {
143    *outbox() = Some(Arc::new(outbox_fn));
144}
145
146/// Removes the installed outbox, after which [`send_to_host`] reports that no
147/// host listens.
148pub fn clear_host_outbox() {
149    *outbox() = None;
150}
151
152/// Sends `payload` on `channel` to the embedding host.
153///
154/// Returns `false` when the application runs without a host, so nothing was
155/// sent. Callable from any thread.
156pub 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/// Collects the messages the host sends on `channel` for as long as this call
166/// stays in the composition, starting with the newest one already received.
167///
168/// ```rust,no_run
169/// use cranpose_macros::composable;
170/// use cranpose_services::rememberHostMessages;
171///
172/// #[composable]
173/// fn CurrentFile() {
174///     let file =
175///         cranpose_core::collectAsState(rememberHostMessages("ide.editor"), (), String::new());
176///     log::info!("the host shows {}", file.get());
177/// }
178/// ```
179#[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;