Skip to main content

rivetkit_core/
websocket.rs

1use std::fmt;
2use std::sync::Arc;
3
4use anyhow::Result;
5use futures::future::BoxFuture;
6use parking_lot::RwLock;
7use rivet_envoy_client::config::WebSocketSender;
8
9use crate::actor::context::WebSocketCallbackRegion;
10use crate::error::ActorRuntime;
11use crate::types::WsMessage;
12
13// Rivet supports a non-standard async close-listener extension for actor
14// WebSockets. Core tracks close-event delivery with the websocket callback
15// region instead of reusing disconnect callbacks because close listeners are
16// WebSocket event work, while `onDisconnect` is connection lifecycle work.
17pub(crate) type WebSocketSendCallback = Arc<dyn Fn(WsMessage) -> Result<()> + Send + Sync>;
18pub(crate) type WebSocketCloseCallback =
19	Arc<dyn Fn(Option<u16>, Option<String>) -> BoxFuture<'static, Result<()>> + Send + Sync>;
20pub(crate) type WebSocketMessageEventCallback =
21	Arc<dyn Fn(WsMessage, Option<u16>) -> Result<()> + Send + Sync>;
22pub(crate) type WebSocketCloseEventCallback =
23	Arc<dyn Fn(u16, String, bool) -> BoxFuture<'static, Result<()>> + Send + Sync>;
24pub(crate) type WebSocketCallbackRegionFactory =
25	Arc<dyn Fn() -> WebSocketCallbackRegion + Send + Sync>;
26
27#[derive(Clone)]
28pub struct WebSocket(Arc<WebSocketInner>);
29
30struct WebSocketInner {
31	// Forced-sync: WebSocket configuration and event dispatch are synchronous
32	// public APIs, so callbacks are cloned out before any async close work.
33	send_callback: RwLock<Option<WebSocketSendCallback>>,
34	close_callback: RwLock<Option<WebSocketCloseCallback>>,
35	message_event_callback: RwLock<Option<WebSocketMessageEventCallback>>,
36	close_event_callback: RwLock<Option<WebSocketCloseEventCallback>>,
37	close_event_callback_region: RwLock<Option<WebSocketCallbackRegionFactory>>,
38}
39
40impl WebSocket {
41	pub fn new() -> Self {
42		Self(Arc::new(WebSocketInner {
43			send_callback: RwLock::new(None),
44			close_callback: RwLock::new(None),
45			message_event_callback: RwLock::new(None),
46			close_event_callback: RwLock::new(None),
47			close_event_callback_region: RwLock::new(None),
48		}))
49	}
50
51	pub fn from_sender(sender: WebSocketSender) -> Self {
52		let websocket = Self::new();
53		websocket.configure_sender(sender);
54		websocket
55	}
56
57	pub fn send(&self, msg: WsMessage) {
58		if let Err(error) = self.try_send(msg) {
59			tracing::error!(?error, "failed to send websocket message");
60		}
61	}
62
63	pub async fn close(&self, code: Option<u16>, reason: Option<String>) {
64		if let Err(error) = self.try_close(code, reason).await {
65			tracing::error!(?error, "failed to close websocket");
66		}
67	}
68
69	pub fn dispatch_message_event(&self, msg: WsMessage, message_index: Option<u16>) {
70		if let Err(error) = self.try_dispatch_message_event(msg, message_index) {
71			tracing::error!(?error, "failed to dispatch websocket message event");
72		}
73	}
74
75	pub async fn dispatch_close_event(&self, code: u16, reason: String, was_clean: bool) {
76		if let Err(error) = self.try_dispatch_close_event(code, reason, was_clean).await {
77			tracing::error!(?error, "failed to dispatch websocket close event");
78		}
79		self.configure_message_event_callback(None);
80		self.configure_close_event_callback(None);
81		self.configure_close_event_callback_region(None);
82	}
83
84	pub fn configure_sender(&self, sender: WebSocketSender) {
85		let send_sender = sender.clone();
86		let close_sender = sender;
87		self.configure_send_callback(Some(Arc::new(move |message| {
88			match message {
89				WsMessage::Text(text) => send_sender.send_text(&text),
90				WsMessage::Binary(bytes) => send_sender.send(bytes, true),
91			}
92			Ok(())
93		})));
94		self.configure_close_callback(Some(Arc::new(move |code, reason| {
95			let close_sender = close_sender.clone();
96			Box::pin(async move {
97				close_sender.close(code, reason);
98				Ok(())
99			})
100		})));
101	}
102
103	pub(crate) fn configure_send_callback(&self, send_callback: Option<WebSocketSendCallback>) {
104		*self.0.send_callback.write() = send_callback;
105	}
106
107	pub(crate) fn configure_close_callback(&self, close_callback: Option<WebSocketCloseCallback>) {
108		*self.0.close_callback.write() = close_callback;
109	}
110
111	pub fn configure_message_event_callback(
112		&self,
113		message_event_callback: Option<WebSocketMessageEventCallback>,
114	) {
115		*self.0.message_event_callback.write() = message_event_callback;
116	}
117
118	pub fn configure_close_event_callback(
119		&self,
120		close_event_callback: Option<WebSocketCloseEventCallback>,
121	) {
122		*self.0.close_event_callback.write() = close_event_callback;
123	}
124
125	pub(crate) fn configure_close_event_callback_region(
126		&self,
127		close_event_callback_region: Option<WebSocketCallbackRegionFactory>,
128	) {
129		*self.0.close_event_callback_region.write() = close_event_callback_region;
130	}
131
132	pub(crate) fn try_send(&self, msg: WsMessage) -> Result<()> {
133		let callback = self.send_callback()?;
134		callback(msg)
135	}
136
137	pub(crate) async fn try_close(&self, code: Option<u16>, reason: Option<String>) -> Result<()> {
138		let callback = self.close_callback()?;
139		callback(code, reason).await
140	}
141
142	pub(crate) fn try_dispatch_message_event(
143		&self,
144		msg: WsMessage,
145		message_index: Option<u16>,
146	) -> Result<()> {
147		let callback = self.message_event_callback()?;
148		callback(msg, message_index)
149	}
150
151	pub(crate) async fn try_dispatch_close_event(
152		&self,
153		code: u16,
154		reason: String,
155		was_clean: bool,
156	) -> Result<()> {
157		let callback = self.close_event_callback()?;
158		let _region = self.close_event_callback_region().map(|create| create());
159		callback(code, reason, was_clean).await
160	}
161
162	fn send_callback(&self) -> Result<WebSocketSendCallback> {
163		self.0
164			.send_callback
165			.read()
166			.clone()
167			.ok_or_else(|| websocket_not_configured("send callback"))
168	}
169
170	fn close_callback(&self) -> Result<WebSocketCloseCallback> {
171		self.0
172			.close_callback
173			.read()
174			.clone()
175			.ok_or_else(|| websocket_not_configured("close callback"))
176	}
177
178	fn message_event_callback(&self) -> Result<WebSocketMessageEventCallback> {
179		self.0
180			.message_event_callback
181			.read()
182			.clone()
183			.ok_or_else(|| websocket_not_configured("message event callback"))
184	}
185
186	fn close_event_callback(&self) -> Result<WebSocketCloseEventCallback> {
187		self.0
188			.close_event_callback
189			.read()
190			.clone()
191			.ok_or_else(|| websocket_not_configured("close event callback"))
192	}
193
194	fn close_event_callback_region(&self) -> Option<WebSocketCallbackRegionFactory> {
195		self.0.close_event_callback_region.read().clone()
196	}
197}
198
199fn websocket_not_configured(component: &str) -> anyhow::Error {
200	ActorRuntime::NotConfigured {
201		component: format!("websocket {component}"),
202	}
203	.build()
204}
205
206impl Default for WebSocket {
207	fn default() -> Self {
208		Self::new()
209	}
210}
211
212impl fmt::Debug for WebSocket {
213	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214		f.debug_struct("WebSocket")
215			.field("send_configured", &self.0.send_callback.read().is_some())
216			.field("close_configured", &self.0.close_callback.read().is_some())
217			.field(
218				"message_event_configured",
219				&self.0.message_event_callback.read().is_some(),
220			)
221			.field(
222				"close_event_configured",
223				&self.0.close_event_callback.read().is_some(),
224			)
225			.field(
226				"close_event_region_configured",
227				&self.0.close_event_callback_region.read().is_some(),
228			)
229			.finish()
230	}
231}
232
233// Test shim keeps moved tests in crate-root tests/ with private-module access.
234