wowlab_centrifuge/client/
commands.rs1use std::{sync::atomic::Ordering, time::Duration};
2
3use tokio::sync::{mpsc, oneshot};
4
5use super::{Client, CommandRequest, EVENT_CHANNEL_BUFFER, check_reply};
6use crate::{
7 Subscription,
8 error::Error,
9 proto,
10 subscription::{SubscriptionConfig, SubscriptionInner, SubscriptionState},
11 types::{
12 HistoryResult, PresenceResult, PresenceStats, RpcResult, StreamPosition, SubRefreshResult,
13 SubscribeResult, SubscriptionEvent,
14 },
15};
16
17const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
18
19impl Client {
20 pub async fn subscribe(&self, config: SubscriptionConfig) -> Result<Subscription, Error> {
24 let channel = config.channel.clone();
25
26 {
27 let inner = self.inner.read().await;
28
29 if inner.subscriptions.contains_key(&channel) {
30 return Err(Error::already_subscribed(channel));
31 }
32 }
33
34 let (event_tx, event_rx) = mpsc::channel(EVENT_CHANNEL_BUFFER);
35 let sub_inner = SubscriptionInner::new(config, event_tx);
36
37 {
38 let mut inner = self.inner.write().await;
39
40 inner.subscriptions.insert(channel.clone(), sub_inner)
41 };
42
43 if self.is_connected().await {
44 self.subscribe_internal(&channel).await?;
45 }
46
47 Ok(Subscription::new(channel, event_rx))
48 }
49
50 pub async fn unsubscribe(&self, channel: &str) -> Result<(), Error> {
54 let cmd = proto::Command {
55 unsubscribe: Some(proto::UnsubscribeRequest {
56 channel: channel.to_string(),
57 }),
58 ..Default::default()
59 };
60
61 self.send_void(cmd).await?;
62
63 {
64 let mut inner = self.inner.write().await;
65
66 inner.subscriptions.remove(channel)
67 };
68
69 Ok(())
70 }
71
72 pub async fn presence(&self, channel: &str) -> Result<PresenceResult, Error> {
76 let cmd = proto::Command {
77 presence: Some(proto::PresenceRequest {
78 channel: channel.to_string(),
79 }),
80 ..Default::default()
81 };
82
83 self.send_extract(cmd, "presence", |r| r.presence).await
84 }
85
86 pub async fn presence_stats(&self, channel: &str) -> Result<PresenceStats, Error> {
90 let cmd = proto::Command {
91 presence_stats: Some(proto::PresenceStatsRequest {
92 channel: channel.to_string(),
93 }),
94 ..Default::default()
95 };
96
97 self.send_extract(cmd, "presence stats", |r| r.presence_stats)
98 .await
99 }
100
101 pub async fn history(
105 &self,
106 channel: &str,
107 limit: i32,
108 since: Option<StreamPosition>,
109 reverse: bool,
110 ) -> Result<HistoryResult, Error> {
111 let cmd = proto::Command {
112 history: Some(proto::HistoryRequest {
113 channel: channel.to_string(),
114 limit,
115 since: since.map(|p| proto::StreamPosition {
116 offset: p.offset,
117 epoch: p.epoch,
118 }),
119 reverse,
120 }),
121 ..Default::default()
122 };
123
124 self.send_extract(cmd, "history", |r| r.history).await
125 }
126
127 pub async fn publish(&self, channel: &str, data: Vec<u8>) -> Result<(), Error> {
131 let cmd = proto::Command {
132 publish: Some(proto::PublishRequest {
133 channel: channel.to_string(),
134 data,
135 }),
136 ..Default::default()
137 };
138
139 self.send_void(cmd).await
140 }
141
142 pub async fn rpc(&self, method: &str, data: Vec<u8>) -> Result<RpcResult, Error> {
146 let cmd = proto::Command {
147 rpc: Some(proto::RpcRequest {
148 method: method.to_string(),
149 data,
150 }),
151 ..Default::default()
152 };
153
154 self.send_extract(cmd, "RPC", |r| r.rpc).await
155 }
156
157 pub async fn send(&self, data: Vec<u8>) -> Result<(), Error> {
161 let cmd = proto::Command {
162 send: Some(proto::SendRequest { data }),
163 ..Default::default()
164 };
165
166 self.send_void(cmd).await
167 }
168
169 pub async fn sub_refresh(&self, channel: &str, token: &str) -> Result<SubRefreshResult, Error> {
173 let cmd = proto::Command {
174 sub_refresh: Some(proto::SubRefreshRequest {
175 channel: channel.to_string(),
176 token: token.to_string(),
177 }),
178 ..Default::default()
179 };
180
181 self.send_extract(cmd, "sub refresh", |r| r.sub_refresh)
182 .await
183 }
184
185 pub fn disconnect(&self) {
187 self.shutdown.cancel();
188 }
189
190 pub(super) async fn subscribe_internal(&self, channel: &str) -> Result<SubscribeResult, Error> {
191 let req = {
192 let mut inner = self.inner.write().await;
193 let sub = inner
194 .subscriptions
195 .get_mut(channel)
196 .ok_or_else(|| Error::subscription_not_found(channel))?;
197
198 sub.state = SubscriptionState::Subscribing;
199
200 sub.build_subscribe_request(false)
201 };
202
203 self.emit_subscription_event(channel, SubscriptionEvent::Subscribing)
204 .await;
205
206 let cmd = proto::Command {
207 subscribe: Some(req),
208 ..Default::default()
209 };
210
211 let reply = check_reply(self.send_command(cmd).await?)?;
212
213 let result = reply
214 .subscribe
215 .ok_or_else(|| Error::protocol("No subscribe result"))?;
216
217 let subscribe_result = SubscribeResult::from(result);
218
219 let subscribed = {
220 let mut inner = self.inner.write().await;
221
222 if let Some(sub) = inner.subscriptions.get_mut(channel) {
223 sub.on_subscribed(&subscribe_result);
224
225 true
226 } else {
227 false
228 }
229 };
230
231 if subscribed {
232 self.emit_subscription_event(
233 channel,
234 SubscriptionEvent::Subscribed(subscribe_result.clone()),
235 )
236 .await;
237 }
238
239 Ok(subscribe_result)
240 }
241
242 async fn send_extract<P, T>(
243 &self,
244 cmd: proto::Command,
245 label: &'static str,
246 extract: impl FnOnce(proto::Reply) -> Option<P>,
247 ) -> Result<T, Error>
248 where
249 P: Into<T>,
250 {
251 let reply = check_reply(self.send_command(cmd).await?)?;
252
253 extract(reply)
254 .ok_or_else(|| Error::protocol(format!("No {label} result")))
255 .map(Into::into)
256 }
257
258 async fn send_void(&self, cmd: proto::Command) -> Result<(), Error> {
259 check_reply(self.send_command(cmd).await?)?;
260
261 Ok(())
262 }
263
264 async fn send_command(&self, cmd: proto::Command) -> Result<proto::Reply, Error> {
265 let id = self
266 .inner
267 .read()
268 .await
269 .next_id
270 .fetch_add(1, Ordering::Relaxed);
271 let (reply_tx, reply_rx) = oneshot::channel();
272 let cmd_tx = {
273 let mut inner = self.inner.write().await;
274 let cmd_tx = inner.cmd_tx.clone().ok_or_else(Error::not_connected)?;
275
276 inner.pending.insert(id, reply_tx);
277
278 cmd_tx
279 };
280
281 if let Err(error) = cmd_tx.send(CommandRequest { id, cmd }).await {
282 self.inner.write().await.pending.remove(&id);
283 tracing::debug!(%error, "Command channel closed");
284
285 return Err(Error::not_connected());
286 }
287
288 match tokio::time::timeout(DEFAULT_TIMEOUT, reply_rx).await {
289 Ok(Ok(result)) => result,
290 Ok(Err(error)) => {
291 self.inner.write().await.pending.remove(&id);
292 tracing::debug!(%error, "Command channel closed");
293
294 Err(Error::connection_closed())
295 }
296 Err(error) => {
297 self.inner.write().await.pending.remove(&id);
298 tracing::debug!(?DEFAULT_TIMEOUT, %error, "Command timed out");
299
300 Err(Error::timeout())
301 }
302 }
303 }
304}