use std::{cell::RefCell, collections::HashMap, rc::Rc};
use vertigo_macro::bind;
use crate::{
Computed, DropResource, JsJsonDeserialize, Value, WebsocketConnection, WebsocketMessage,
dev::ValueMut, get_driver, spawn, websocket_collection::ws_message_from::WsMessageData,
};
mod types;
mod ws_message_from;
mod ws_message_to;
pub use types::{CollectionWhereValue, IntoCollectionWhereValue, WsQuery};
pub use ws_message_from::{WsMessageKind, WsServerMessageFrom};
use self::{types::WebsocketQueryId, ws_message_to::WsClientMessageTo};
pub type Callback = Rc<dyn Fn(WsServerMessageFrom)>;
pub type AuthTokenProvider = Rc<dyn Fn() -> Option<String>>;
#[derive(Clone)]
struct Subscription {
query: WsQuery,
callback: Callback,
}
pub struct WsSocket {
_drop: DropResource,
connection_box: Rc<ValueMut<Option<WebsocketConnection>>>,
collections: Rc<ValueMut<HashMap<WebsocketQueryId, Subscription>>>,
auth: AuthTokenProvider,
}
impl WsSocket {
fn run_unsubscribe(
collections: &Rc<ValueMut<HashMap<WebsocketQueryId, Subscription>>>,
connection_box: &Rc<ValueMut<Option<WebsocketConnection>>>,
query_id: WebsocketQueryId,
) {
collections.change(|inner| {
inner.remove(&query_id);
});
let message = WsClientMessageTo::unsubscribe(query_id);
connection_box.change(|opt| {
if let Some(conn) = opt {
conn.send(message);
} else {
log::warn!("ws-collection - no connection, cannot unsubscribe");
}
});
}
pub fn new(url: impl Into<String>, auth: AuthTokenProvider) -> Self {
Self::new_internal(url.into(), auth, None)
}
pub fn new_with_error_cb(
url: impl Into<String>,
auth: AuthTokenProvider,
error_cb: impl Fn(WsMessageData) + 'static,
) -> Self {
Self::new_internal(url.into(), auth, Some(Rc::new(error_cb)))
}
fn new_internal(
url: String,
auth: AuthTokenProvider,
error_cb: Option<Rc<dyn Fn(WsMessageData)>>,
) -> Self {
let collections = Rc::new(ValueMut::new(
HashMap::<WebsocketQueryId, Subscription>::new(),
));
let connection_box = Rc::new(ValueMut::new(None));
let drop = get_driver().websocket(
url.as_str(),
bind!(connection_box, collections, auth, error_cb, |message| {
match message {
WebsocketMessage::Connection(connection) => {
log::info!("ws-collection - connection ...");
collections.change(|inner| {
for (query_id, subscription) in inner.iter() {
send_subscribe(
&connection,
query_id.clone(),
subscription.query.clone(),
&auth,
);
}
});
connection_box.set(Some(connection));
}
WebsocketMessage::Message(message) => {
let parsed = match WsServerMessageFrom::from_js_json(message) {
Ok(p) => p,
Err(err) => {
log::error!("ws-collection - skipping undecodable frame: {err}");
return;
}
};
dispatch(&collections, parsed, &error_cb);
}
WebsocketMessage::Close => {
log::info!("ws-collection - close ...");
}
}
}),
);
Self {
_drop: drop,
connection_box,
collections,
auth,
}
}
pub fn subscribe(&self, query: WsQuery, callback: Callback) -> DropResource {
let query_id = WebsocketQueryId::next();
self.collections.change(|inner| {
inner.insert(
query_id.clone(),
Subscription {
query: query.clone(),
callback,
},
);
});
let query_id_for_drop = query_id.clone();
self.send_subscribe(query_id, query);
let collections = self.collections.clone();
let connection_box = self.connection_box.clone();
DropResource::new(move || {
Self::run_unsubscribe(&collections, &connection_box, query_id_for_drop);
})
}
fn send_subscribe(&self, query_id: WebsocketQueryId, query: WsQuery) {
let auth = self.auth.clone();
self.connection_box.change(|opt| {
if let Some(conn) = opt {
send_subscribe(conn, query_id, query, &auth);
} else {
log::info!("ws-collection - no connection, cannot subscribe");
}
});
}
}
fn dispatch(
collections: &Rc<ValueMut<HashMap<WebsocketQueryId, Subscription>>>,
parsed: WsServerMessageFrom,
error_cb: &Option<Rc<dyn Fn(WsMessageData)>>,
) {
match parsed {
WsServerMessageFrom::Batch(items) => {
for item in items {
dispatch(collections, item, error_cb);
}
}
WsServerMessageFrom::Message(data) => match data.kind {
WsMessageKind::Error => {
if let Some(cb) = error_cb {
cb(data)
} else {
log::error!(
"ws-collection - server reported error (query_id={:?}): {}",
data.query_id,
data.message,
);
}
}
WsMessageKind::Log => log::info!(
"ws-collection - server log (query_id={:?}): {}",
data.query_id,
data.message,
),
},
other => {
let Some(query_id) = other.query_id() else {
return;
};
collections.change(move |inner| {
let Some(sub) = inner.get(&query_id) else {
log::warn!("ws-collection - no subscription for query_id={query_id:?}");
return;
};
(sub.callback)(other);
});
}
}
}
fn send_subscribe(
connection: &WebsocketConnection,
query_id: WebsocketQueryId,
query: WsQuery,
auth: &AuthTokenProvider,
) {
let connection = connection.clone();
let auth = auth.clone();
spawn(async move {
let Some(token) = auth() else {
log::error!("ws-collection - no auth token, deferring subscribe");
return;
};
let message = WsClientMessageTo::subscribe(query_id, token, query);
connection.send(message);
});
}
fn map_to_sorted_vec<T: Clone>(opt: Option<HashMap<String, T>>) -> Option<Vec<T>> {
opt.map(|map| {
let mut entries: Vec<(String, T)> = map.into_iter().collect();
entries.sort_by(|a, b| a.0.cmp(&b.0));
entries.into_iter().map(|(_, v)| v).collect()
})
}
pub type MergeFn<T> = Rc<dyn Fn(T, &T) -> T>;
fn default_merge<T>() -> MergeFn<T> {
Rc::new(|new, _prev| new)
}
pub struct WsCollection<T: Clone + PartialEq + 'static> {
pub items_sorted: Computed<Option<Vec<T>>>,
socket: Rc<WsSocket>,
state: Value<Option<HashMap<String, T>>>,
active_drop: Rc<RefCell<Option<DropResource>>>,
merge: MergeFn<T>,
}
impl<T: Clone + PartialEq + JsJsonDeserialize + 'static> WsCollection<T> {
pub fn new(socket: Rc<WsSocket>, query: WsQuery) -> Self {
Self::new_with_merge(socket, query, default_merge())
}
pub fn new_with_merge(socket: Rc<WsSocket>, query: WsQuery, merge: MergeFn<T>) -> Self {
let state: Value<Option<HashMap<String, T>>> = Value::new(None);
let keepalive = socket.subscribe(
query,
Self::make_message_callback(state.clone(), merge.clone()),
);
let items_sorted = state.clone().map(move |opt| {
let _ = &keepalive;
map_to_sorted_vec(opt)
});
Self {
items_sorted,
socket,
state,
active_drop: Rc::new(RefCell::new(None)),
merge,
}
}
pub fn empty(socket: Rc<WsSocket>) -> Self {
let state: Value<Option<HashMap<String, T>>> = Value::new(None);
let items_sorted = state.clone().map(map_to_sorted_vec);
Self {
items_sorted,
socket,
state,
active_drop: Rc::new(RefCell::new(None)),
merge: default_merge(),
}
}
pub fn set_query(&self, query: WsQuery) {
let had_previous = self.active_drop.replace(None).is_some();
if had_previous {
self.state.set(None);
}
let drop = self.socket.subscribe(
query,
Self::make_message_callback(self.state.clone(), self.merge.clone()),
);
self.active_drop.replace(Some(drop));
}
pub fn extend_query(&self, query: WsQuery) {
self.active_drop.replace(None);
let drop = self.socket.subscribe(
query,
Self::make_message_callback(self.state.clone(), self.merge.clone()),
);
self.active_drop.replace(Some(drop));
}
fn make_message_callback(
state: Value<Option<HashMap<String, T>>>,
merge: MergeFn<T>,
) -> Callback {
Rc::new(move |message| match message {
WsServerMessageFrom::Init(data) => {
let mut map = HashMap::new();
for item in data.list {
match crate::from_json::<T>(item.model) {
Ok(model) => {
map.insert(item.id, model);
}
Err(err) => {
log::error!("ws-collection - init: skip row id={:?}: {err}", item.id);
}
}
}
state.set(Some(map));
}
WsServerMessageFrom::Set(data) => match crate::from_json::<T>(data.model) {
Ok(model) => {
let merge = merge.clone();
state.change(move |opt| {
let map = opt.get_or_insert_with(HashMap::new);
let merged = match map.get(&data.model_id) {
Some(prev) => (merge)(model, prev),
None => model,
};
map.insert(data.model_id, merged);
});
}
Err(err) => {
log::error!("ws-collection - set: could not decode model: {err}");
}
},
WsServerMessageFrom::Delete(data) => {
state.change(|opt| {
if let Some(map) = opt {
map.remove(&data.model_id);
}
});
}
WsServerMessageFrom::Batch(_) | WsServerMessageFrom::Message(_) => {}
})
}
}