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::{
16        Arc, Mutex, MutexGuard, OnceLock, PoisonError,
17        atomic::{AtomicU64, Ordering},
18    },
19};
20
21use cranpose_core::{EventStream, rememberEventStream};
22
23/// One message on the line between the application and its host.
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct HostMessage {
26    /// The channel the message belongs to.
27    pub channel: String,
28    /// The message body.
29    pub payload: String,
30}
31
32impl HostMessage {
33    /// A message carrying `payload` on `channel`.
34    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
100/// Keeps a host message observer registered until it is dropped.
101pub 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
111/// Registers `observer` for the messages the host sends on `channel`, handing
112/// it the newest one at once when the host already spoke there.
113///
114/// Applications collect [`rememberHostMessages`] instead of calling this.
115pub 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
128/// Delivers a message the host sent to every observer of its channel.
129/// Callable from any thread; platform hosts call it as messages arrive.
130pub 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
137/// Forgets the newest message of every channel. Used by tests and host
138/// teardown.
139pub fn clear_host_messages() {
140    mailbox().clear();
141}
142
143/// Installs where [`send_to_host`] delivers: the platform host's connection.
144pub fn install_host_outbox(outbox_fn: impl Fn(HostMessage) + Send + Sync + 'static) {
145    *outbox() = Some(Arc::new(outbox_fn));
146}
147
148/// Removes the installed outbox, after which [`send_to_host`] reports that no
149/// host listens.
150pub fn clear_host_outbox() {
151    *outbox() = None;
152}
153
154/// Sends `payload` on `channel` to the embedding host.
155///
156/// Returns `false` when the application runs without a host, so nothing was
157/// sent. Callable from any thread.
158pub 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/// Collects the messages the host sends on `channel` for as long as this call
168/// stays in the composition, starting with the newest one already received.
169///
170/// ```rust,no_run
171/// use cranpose_macros::composable;
172/// use cranpose_services::rememberHostMessages;
173///
174/// #[composable]
175/// fn CurrentFile() {
176///     let file =
177///         cranpose_core::collectAsState(rememberHostMessages("ide.editor"), (), String::new());
178///     log::info!("the host shows {}", file.get());
179/// }
180/// ```
181#[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;