Skip to main content

arora_websocket/
bridge.rs

1//! [`WsBridge`]: the WebSocket server driven as an Arora
2//! [`Bridge`](arora_bridge::Bridge).
3//!
4//! Each incoming message becomes a [`BridgeCommand`] on
5//! [`commands`](Bridge::commands), and the consumer — the Arora runtime —
6//! reacts to the ones it cares about (writes apply to the store, reads reply).
7//! Runtime state flows back out through [`send_data`](Bridge::send_data) as a
8//! `values_changed` push.
9//!
10//! The server's handler API is kept and *built on top of* the command stream:
11//! the handlers registered here simply translate a message into a command. The
12//! value vocabulary is `arora_types::Value`, so the translation is structural,
13//! not a conversion.
14
15use std::collections::HashMap;
16use std::sync::{Arc, Mutex};
17
18use arora_bridge::{
19    Bridge, BridgeCommand, BridgeOp, BridgeResult, CommandStream, DataRequestedStream, DeviceInfo,
20    DeviceInfoStream,
21};
22use arora_types::data::{Key, StateChange};
23use arora_types::value::Value;
24use async_trait::async_trait;
25use futures::channel::{mpsc, oneshot};
26
27use crate::messages::Outgoing;
28use crate::server::AroraWSServer;
29
30/// The WebSocket server as an Arora [`Bridge`].
31///
32/// Note: the server's `validate_paths` (on by default) checks written key
33/// paths against its `Registry`, which this bridge does not populate — either
34/// mirror the runtime's keys into the registry or disable `validate_paths`
35/// when serving purely through the bridge.
36pub struct WsBridge {
37    server: Arc<AroraWSServer>,
38    /// The receiving half of the command stream, handed out once by [`commands`].
39    commands: Mutex<Option<mpsc::UnboundedReceiver<BridgeCommand>>>,
40}
41
42impl WsBridge {
43    /// Wrap a server: register handlers that turn each incoming message into a
44    /// [`BridgeCommand`] on the [`commands`](Bridge::commands) stream.
45    pub async fn new(server: Arc<AroraWSServer>) -> Self {
46        let (cmd_tx, cmd_rx) = mpsc::unbounded::<BridgeCommand>();
47
48        // WriteValues -> Update. The handler is synchronous, so enqueue the
49        // command and acknowledge; the runtime applies it on its next step.
50        let tx = cmd_tx.clone();
51        server
52            .set_write_values_handler(move |values: HashMap<String, Value>| {
53                let mut change = StateChange::new();
54                for (path, value) in values {
55                    change.set.insert(Key::from(path), Some(value));
56                }
57                let (reply_tx, _reply_rx) = oneshot::channel();
58                tx.unbounded_send(BridgeCommand::new(BridgeOp::Update(change), reply_tx))
59                    .map_err(|_| "bridge command channel closed".to_string())
60            })
61            .await;
62
63        // ReadValues -> Get, awaiting the runtime's reply.
64        let tx = cmd_tx.clone();
65        server
66            .set_read_values_handler(Arc::new(move |keys: Vec<String>| {
67                let tx = tx.clone();
68                Box::pin(async move {
69                    let store_keys: Vec<Key> = keys.iter().cloned().map(Key::from).collect();
70                    let (reply_tx, reply_rx) = oneshot::channel();
71                    if tx
72                        .unbounded_send(BridgeCommand::new(BridgeOp::Get(store_keys), reply_tx))
73                        .is_err()
74                    {
75                        return HashMap::new();
76                    }
77                    match reply_rx.await {
78                        Ok(Ok(result)) => values_from_get(&keys, result.ret),
79                        _ => HashMap::new(),
80                    }
81                }) as _
82            }))
83            .await;
84
85        Self {
86            server,
87            commands: Mutex::new(Some(cmd_rx)),
88        }
89    }
90}
91
92/// Decode a `Get` reply — an `ArrayValue` of `Option`s in request order — into a
93/// `path -> value` map.
94fn values_from_get(keys: &[String], ret: Value) -> HashMap<String, Value> {
95    let mut out = HashMap::new();
96    if let Value::ArrayValue(items) = ret {
97        for (key, item) in keys.iter().zip(items) {
98            if let Value::Option(Some(value)) = item {
99                out.insert(key.clone(), *value);
100            }
101        }
102    }
103    out
104}
105
106#[async_trait]
107impl Bridge for WsBridge {
108    async fn get_device_info(&self) -> BridgeResult<Option<DeviceInfo>> {
109        // A local editor connection has no device-registration concept.
110        Ok(None)
111    }
112
113    async fn device_info_updated(&self) -> BridgeResult<DeviceInfoStream> {
114        Ok(Box::pin(futures::stream::empty()))
115    }
116
117    async fn update_device_info(
118        &self,
119        info: Option<DeviceInfo>,
120    ) -> BridgeResult<Option<DeviceInfo>> {
121        Ok(info)
122    }
123
124    async fn data_requested(&self) -> DataRequestedStream {
125        // A connected editor is a data consumer; signal interest once.
126        Box::pin(futures::stream::once(async { true }))
127    }
128
129    async fn send_data(&self, data: StateChange) -> BridgeResult<()> {
130        // Push the changed keys to the connected client(s).
131        let mut values = HashMap::new();
132        for (key, value) in data.set {
133            if let Some(value) = value {
134                values.insert(key.path, value);
135            }
136        }
137        if !values.is_empty() {
138            self.server.push(Outgoing::ValuesChanged { values });
139        }
140        Ok(())
141    }
142
143    /// The command stream is single-use: the channel's receiving half is
144    /// handed out on the first call, and later calls get an empty stream —
145    /// with a loud warning, because a runtime silently losing its command
146    /// plane is the worst failure mode.
147    async fn commands(&self) -> CommandStream {
148        match self.commands.lock().unwrap().take() {
149            Some(rx) => Box::pin(rx),
150            None => {
151                log::warn!(
152                    "WsBridge::commands() called more than once; the command \
153                     stream was already taken, returning an empty stream"
154                );
155                Box::pin(futures::stream::empty())
156            }
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::server::ServerConfig;
165
166    #[tokio::test]
167    async fn send_data_pushes_values_changed() {
168        let server = Arc::new(AroraWSServer::new(ServerConfig::default()));
169        let bridge = WsBridge::new(server.clone()).await;
170        let mut rx = server.subscribe();
171
172        bridge
173            .send_data(StateChange::set("face/mouth", Value::F64(0.5)))
174            .await
175            .expect("send_data");
176
177        match rx.recv().await.expect("a push") {
178            Outgoing::ValuesChanged { values } => {
179                assert_eq!(values.get("face/mouth"), Some(&Value::F64(0.5)));
180            }
181            other => panic!("expected ValuesChanged, got {other:?}"),
182        }
183    }
184}