#[cfg(all(feature = "ws", target_arch = "wasm32"))]
use std::sync::Arc;
#[cfg(all(feature = "ws", target_arch = "wasm32"))]
use http_kit::error::BoxHttpError;
#[cfg(all(feature = "ws", target_arch = "wasm32"))]
use http_kit::http_error;
use http_kit::ws::WebSocketMessage;
use serde::{de::DeserializeOwned, Serialize};
#[cfg(all(feature = "ws", target_arch = "wasm32"))]
use skyzen_core::Responder;
#[cfg(all(feature = "ws", target_arch = "wasm32"))]
use wasm_bindgen::JsCast;
use super::error::DurableObjectError;
#[derive(Debug)]
pub enum WebSocketEvent {
Message(WebSocketMessage),
Close {
code: u16,
reason: String,
was_clean: bool,
},
Error(String),
}
pub struct WebSocketConnection {
inner: Box<dyn WebSocketConnectionInner>,
}
impl std::fmt::Debug for WebSocketConnection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WebSocketConnection")
.finish_non_exhaustive()
}
}
pub trait WebSocketConnectionInner: Send + Sync {
fn send_text(&self, text: &str) -> Result<(), DurableObjectError>;
fn send_binary(&self, data: &[u8]) -> Result<(), DurableObjectError>;
fn close(&self, code: u16, reason: &str) -> Result<(), DurableObjectError>;
fn tags(&self) -> Result<Vec<String>, DurableObjectError>;
fn get_attachment_raw(&self) -> Result<Option<Vec<u8>>, DurableObjectError>;
fn set_attachment_raw(&self, data: &[u8]) -> Result<(), DurableObjectError>;
}
impl WebSocketConnection {
#[must_use]
pub fn new(inner: Box<dyn WebSocketConnectionInner>) -> Self {
Self { inner }
}
pub fn send_text(&self, text: &str) -> Result<(), DurableObjectError> {
self.inner.send_text(text)
}
pub fn send_binary(&self, data: &[u8]) -> Result<(), DurableObjectError> {
self.inner.send_binary(data)
}
pub fn send_json<T: Serialize>(&self, value: &T) -> Result<(), DurableObjectError> {
let json = serde_json::to_string(value)
.map_err(|e| DurableObjectError::Serialization(e.to_string()))?;
self.send_text(&json)
}
pub fn close(&self, code: u16, reason: &str) -> Result<(), DurableObjectError> {
self.inner.close(code, reason)
}
pub fn tags(&self) -> Result<Vec<String>, DurableObjectError> {
self.inner.tags()
}
pub fn attachment<T: DeserializeOwned>(&self) -> Result<Option<T>, DurableObjectError> {
self.inner
.get_attachment_raw()?
.map(|bytes| {
serde_json::from_slice(&bytes)
.map_err(|e| DurableObjectError::Serialization(e.to_string()))
})
.transpose()
}
pub fn set_attachment<T: Serialize>(&self, value: &T) -> Result<(), DurableObjectError> {
let bytes = serde_json::to_vec(value)
.map_err(|e| DurableObjectError::Serialization(e.to_string()))?;
self.inner.set_attachment_raw(&bytes)
}
}
#[derive(Debug, Default)]
pub struct HibernationWebSocketUpgrade {
tags: Vec<String>,
}
impl HibernationWebSocketUpgrade {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
#[must_use]
pub fn tags(&self) -> &[String] {
&self.tags
}
#[must_use]
pub fn into_tags(self) -> Vec<String> {
self.tags
}
}
#[cfg(all(feature = "ws", target_arch = "wasm32"))]
type DurableObjectWebSocketAcceptFn =
dyn Fn(&web_sys::WebSocket, &[String]) -> Result<(), DurableObjectError> + Send + Sync;
#[cfg(all(feature = "ws", target_arch = "wasm32"))]
#[derive(Clone)]
pub struct DurableClientWebSocket(pub web_sys::WebSocket);
#[cfg(all(feature = "ws", target_arch = "wasm32"))]
impl std::fmt::Debug for DurableClientWebSocket {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DurableClientWebSocket")
.finish_non_exhaustive()
}
}
#[cfg(all(feature = "ws", target_arch = "wasm32"))]
unsafe impl Send for DurableClientWebSocket {}
#[cfg(all(feature = "ws", target_arch = "wasm32"))]
unsafe impl Sync for DurableClientWebSocket {}
#[cfg(all(feature = "ws", target_arch = "wasm32"))]
#[derive(Clone)]
pub struct DurableObjectState {
accept_websocket: Arc<DurableObjectWebSocketAcceptFn>,
}
#[cfg(all(feature = "ws", target_arch = "wasm32"))]
impl std::fmt::Debug for DurableObjectState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DurableObjectState").finish_non_exhaustive()
}
}
#[cfg(all(feature = "ws", target_arch = "wasm32"))]
impl DurableObjectState {
#[must_use]
pub fn new(
accept_websocket: impl Fn(&web_sys::WebSocket, &[String]) -> Result<(), DurableObjectError>
+ Send
+ Sync
+ 'static,
) -> Self {
Self {
accept_websocket: Arc::new(accept_websocket),
}
}
pub fn accept_websocket(
&self,
websocket: &web_sys::WebSocket,
tags: &[String],
) -> Result<(), DurableObjectError> {
(self.accept_websocket)(websocket, tags)
}
}
#[cfg(all(feature = "ws", target_arch = "wasm32"))]
http_error!(
HibernationDurableStateMissing,
http_kit::StatusCode::INTERNAL_SERVER_ERROR,
"DurableObjectState missing in request extensions for hibernation websocket upgrade."
);
#[cfg(all(feature = "ws", target_arch = "wasm32"))]
http_error!(
HibernationWebSocketAcceptFailed,
http_kit::StatusCode::INTERNAL_SERVER_ERROR,
"Failed to accept hibernation websocket connection."
);
#[cfg(all(feature = "ws", target_arch = "wasm32"))]
impl Responder for HibernationWebSocketUpgrade {
type Error = BoxHttpError;
fn respond_to(
self,
request: &crate::Request,
response: &mut crate::Response,
) -> Result<(), Self::Error> {
let durable_state = request
.extensions()
.get::<DurableObjectState>()
.cloned()
.ok_or_else(|| Box::new(HibernationDurableStateMissing::new()) as BoxHttpError)?;
let pair = crate::websocket::ffi::WebSocketPair::new();
let client = pair.client();
let server = pair.server();
let server_socket: web_sys::WebSocket = server.unchecked_into();
durable_state
.accept_websocket(&server_socket, self.tags())
.map_err(|error| {
tracing::error!(%error, "failed to accept hibernation websocket");
Box::new(HibernationWebSocketAcceptFailed::new()) as BoxHttpError
})?;
*response.status_mut() = http_kit::StatusCode::SWITCHING_PROTOCOLS;
let client_socket: web_sys::WebSocket = client.unchecked_into();
response
.extensions_mut()
.insert(DurableClientWebSocket(client_socket));
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use super::{
DurableObjectError, HibernationWebSocketUpgrade, WebSocketConnection,
WebSocketConnectionInner,
};
#[derive(Debug, Default)]
struct MockSocketState {
text_messages: Vec<String>,
binary_messages: Vec<Vec<u8>>,
close_calls: Vec<(u16, String)>,
tags: Vec<String>,
attachment: Option<Vec<u8>>,
fail_text: Option<DurableObjectError>,
fail_binary: Option<DurableObjectError>,
fail_close: Option<DurableObjectError>,
fail_tags: Option<DurableObjectError>,
fail_get_attachment: Option<DurableObjectError>,
fail_set_attachment: Option<DurableObjectError>,
}
#[derive(Debug, Clone)]
struct MockSocketInner {
state: Arc<Mutex<MockSocketState>>,
}
impl MockSocketInner {
fn error_clone(error: &DurableObjectError) -> DurableObjectError {
match error {
DurableObjectError::Runtime(message) => {
DurableObjectError::Runtime(message.clone())
}
DurableObjectError::Serialization(message) => {
DurableObjectError::Serialization(message.clone())
}
DurableObjectError::WebSocket(message) => {
DurableObjectError::WebSocket(message.clone())
}
}
}
}
impl WebSocketConnectionInner for MockSocketInner {
fn send_text(&self, text: &str) -> Result<(), DurableObjectError> {
{
let mut state = self.state.lock().unwrap();
if let Some(error) = &state.fail_text {
return Err(Self::error_clone(error));
}
state.text_messages.push(text.to_owned());
}
Ok(())
}
fn send_binary(&self, data: &[u8]) -> Result<(), DurableObjectError> {
{
let mut state = self.state.lock().unwrap();
if let Some(error) = &state.fail_binary {
return Err(Self::error_clone(error));
}
state.binary_messages.push(data.to_vec());
}
Ok(())
}
fn close(&self, code: u16, reason: &str) -> Result<(), DurableObjectError> {
{
let mut state = self.state.lock().unwrap();
if let Some(error) = &state.fail_close {
return Err(Self::error_clone(error));
}
state.close_calls.push((code, reason.to_owned()));
}
Ok(())
}
fn tags(&self) -> Result<Vec<String>, DurableObjectError> {
let state = self.state.lock().unwrap();
if let Some(error) = &state.fail_tags {
return Err(Self::error_clone(error));
}
Ok(state.tags.clone())
}
fn get_attachment_raw(&self) -> Result<Option<Vec<u8>>, DurableObjectError> {
let state = self.state.lock().unwrap();
if let Some(error) = &state.fail_get_attachment {
return Err(Self::error_clone(error));
}
Ok(state.attachment.clone())
}
fn set_attachment_raw(&self, data: &[u8]) -> Result<(), DurableObjectError> {
{
let mut state = self.state.lock().unwrap();
if let Some(error) = &state.fail_set_attachment {
return Err(Self::error_clone(error));
}
state.attachment = Some(data.to_vec());
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Attachment {
room: String,
revision: u32,
}
#[derive(Debug)]
struct AlwaysFails;
impl Serialize for AlwaysFails {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
Err(serde::ser::Error::custom("expected serialization failure"))
}
}
fn connection_with_state(state: Arc<Mutex<MockSocketState>>) -> WebSocketConnection {
WebSocketConnection::new(Box::new(MockSocketInner { state }))
}
#[test]
fn send_json_serializes_payload_as_text_message() {
let state = Arc::new(Mutex::new(MockSocketState::default()));
let connection = connection_with_state(Arc::clone(&state));
connection
.send_json(&Attachment {
room: "room-1".to_owned(),
revision: 7,
})
.unwrap();
assert_eq!(
state.lock().unwrap().text_messages,
vec![r#"{"room":"room-1","revision":7}"#.to_owned()]
);
}
#[test]
fn send_json_rejects_non_finite_numbers() {
let connection = connection_with_state(Arc::new(Mutex::new(MockSocketState::default())));
let error = connection.send_json(&AlwaysFails).unwrap_err();
assert!(matches!(error, DurableObjectError::Serialization(_)));
}
#[test]
fn attachment_round_trips_through_raw_storage() {
let state = Arc::new(Mutex::new(MockSocketState::default()));
let connection = connection_with_state(Arc::clone(&state));
let attachment = Attachment {
room: "general".to_owned(),
revision: 3,
};
connection.set_attachment(&attachment).unwrap();
let restored = connection.attachment::<Attachment>().unwrap().unwrap();
assert_eq!(restored, attachment);
assert!(state.lock().unwrap().attachment.is_some());
}
#[test]
fn attachment_reports_deserialization_errors_for_invalid_bytes() {
let state = Arc::new(Mutex::new(MockSocketState {
attachment: Some(b"{".to_vec()),
..MockSocketState::default()
}));
let connection = connection_with_state(state);
let error = connection.attachment::<Attachment>().unwrap_err();
assert!(matches!(error, DurableObjectError::Serialization(_)));
}
#[test]
fn close_and_tags_delegate_to_inner_handle() {
let state = Arc::new(Mutex::new(MockSocketState {
tags: vec!["room-1".to_owned(), "admin".to_owned()],
..MockSocketState::default()
}));
let connection = connection_with_state(Arc::clone(&state));
assert_eq!(
connection.tags().unwrap(),
vec!["room-1".to_owned(), "admin".to_owned()]
);
connection.close(1000, "done").unwrap();
assert_eq!(
state.lock().unwrap().close_calls,
vec![(1000, "done".to_owned())]
);
}
#[test]
fn hibernation_upgrade_preserves_tag_order() {
let tags = HibernationWebSocketUpgrade::new()
.tag("room-1")
.tag("presence")
.into_tags();
assert_eq!(tags, vec!["room-1".to_owned(), "presence".to_owned()]);
}
}