mirror of
https://github.com/luxfi/multi-party-ecdsa.git
synced 2026-10-30 15:24:24 +00:00
194 lines
5.5 KiB
Rust
194 lines
5.5 KiB
Rust
use std::collections::hash_map::{Entry, HashMap};
|
|
use std::sync::{
|
|
atomic::{AtomicU16, Ordering},
|
|
Arc,
|
|
};
|
|
|
|
use futures::Stream;
|
|
use rocket::data::ToByteUnit;
|
|
use rocket::http::Status;
|
|
use rocket::request::{FromRequest, Outcome, Request};
|
|
use rocket::response::stream::{stream, Event, EventStream};
|
|
use rocket::serde::json::Json;
|
|
use rocket::State;
|
|
use serde::{Deserialize, Serialize};
|
|
use tokio::sync::{Notify, RwLock};
|
|
|
|
#[rocket::get("/rooms/<room_id>/subscribe")]
|
|
async fn subscribe(
|
|
db: &State<Db>,
|
|
mut shutdown: rocket::Shutdown,
|
|
last_seen_msg: LastEventId,
|
|
room_id: &str,
|
|
) -> EventStream<impl Stream<Item = Event>> {
|
|
let room = db.get_room_or_create_empty(room_id).await;
|
|
let mut subscription = room.subscribe(last_seen_msg.0);
|
|
EventStream::from(stream! {
|
|
loop {
|
|
let (id, msg) = tokio::select! {
|
|
message = subscription.next() => message,
|
|
_ = &mut shutdown => return,
|
|
};
|
|
yield Event::data(msg)
|
|
.event("new-message")
|
|
.id(id.to_string())
|
|
}
|
|
})
|
|
}
|
|
|
|
#[rocket::post("/rooms/<room_id>/issue_unique_idx")]
|
|
async fn issue_idx(db: &State<Db>, room_id: &str) -> Json<IssuedUniqueIdx> {
|
|
let room = db.get_room_or_create_empty(room_id).await;
|
|
let idx = room.issue_unique_idx();
|
|
Json::from(IssuedUniqueIdx { unique_idx: idx })
|
|
}
|
|
|
|
#[rocket::post("/rooms/<room_id>/broadcast", data = "<message>")]
|
|
async fn broadcast(db: &State<Db>, room_id: &str, message: String) -> Status {
|
|
let room = db.get_room_or_create_empty(room_id).await;
|
|
room.publish(message).await;
|
|
Status::Ok
|
|
}
|
|
|
|
struct Db {
|
|
rooms: RwLock<HashMap<String, Arc<Room>>>,
|
|
}
|
|
|
|
struct Room {
|
|
messages: RwLock<Vec<String>>,
|
|
message_appeared: Notify,
|
|
subscribers: AtomicU16,
|
|
next_idx: AtomicU16,
|
|
}
|
|
|
|
impl Db {
|
|
pub fn empty() -> Self {
|
|
Self {
|
|
rooms: RwLock::new(HashMap::new()),
|
|
}
|
|
}
|
|
|
|
pub async fn get_room_or_create_empty(&self, room_id: &str) -> Arc<Room> {
|
|
let rooms = self.rooms.read().await;
|
|
if let Some(room) = rooms.get(room_id) {
|
|
// If no one is watching this room - we need to clean it up first
|
|
if !room.is_abandoned() {
|
|
return room.clone();
|
|
}
|
|
}
|
|
drop(rooms);
|
|
|
|
let mut rooms = self.rooms.write().await;
|
|
match rooms.entry(room_id.to_owned()) {
|
|
Entry::Occupied(entry) if !entry.get().is_abandoned() => entry.get().clone(),
|
|
Entry::Occupied(entry) => {
|
|
let room = Arc::new(Room::empty());
|
|
*entry.into_mut() = room.clone();
|
|
room
|
|
}
|
|
Entry::Vacant(entry) => entry.insert(Arc::new(Room::empty())).clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Room {
|
|
pub fn empty() -> Self {
|
|
Self {
|
|
messages: RwLock::new(vec![]),
|
|
message_appeared: Notify::new(),
|
|
subscribers: AtomicU16::new(0),
|
|
next_idx: AtomicU16::new(1),
|
|
}
|
|
}
|
|
|
|
pub async fn publish(self: &Arc<Self>, message: String) {
|
|
let mut messages = self.messages.write().await;
|
|
messages.push(message);
|
|
self.message_appeared.notify_waiters();
|
|
}
|
|
|
|
pub fn subscribe(self: Arc<Self>, last_seen_msg: Option<u16>) -> Subscription {
|
|
self.subscribers.fetch_add(1, Ordering::SeqCst);
|
|
Subscription {
|
|
room: self,
|
|
next_event: last_seen_msg.map(|i| i + 1).unwrap_or(0),
|
|
}
|
|
}
|
|
|
|
pub fn is_abandoned(&self) -> bool {
|
|
self.subscribers.load(Ordering::SeqCst) == 0
|
|
}
|
|
|
|
pub fn issue_unique_idx(&self) -> u16 {
|
|
self.next_idx.fetch_add(1, Ordering::Relaxed)
|
|
}
|
|
}
|
|
|
|
struct Subscription {
|
|
room: Arc<Room>,
|
|
next_event: u16,
|
|
}
|
|
|
|
impl Subscription {
|
|
pub async fn next(&mut self) -> (u16, String) {
|
|
loop {
|
|
let history = self.room.messages.read().await;
|
|
if let Some(msg) = history.get(usize::from(self.next_event)) {
|
|
let event_id = self.next_event;
|
|
self.next_event = event_id + 1;
|
|
return (event_id, msg.clone());
|
|
}
|
|
let notification = self.room.message_appeared.notified();
|
|
drop(history);
|
|
notification.await;
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for Subscription {
|
|
fn drop(&mut self) {
|
|
self.room.subscribers.fetch_sub(1, Ordering::SeqCst);
|
|
}
|
|
}
|
|
|
|
/// Represents a header Last-Event-ID
|
|
struct LastEventId(Option<u16>);
|
|
|
|
#[rocket::async_trait]
|
|
impl<'r> FromRequest<'r> for LastEventId {
|
|
type Error = &'static str;
|
|
|
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
|
let header = request
|
|
.headers()
|
|
.get_one("Last-Event-ID")
|
|
.map(|id| id.parse::<u16>());
|
|
match header {
|
|
Some(Ok(last_seen_msg)) => Outcome::Success(LastEventId(Some(last_seen_msg))),
|
|
Some(Err(_parse_err)) => {
|
|
Outcome::Error((Status::BadRequest, "last seen msg id is not valid"))
|
|
}
|
|
None => Outcome::Success(LastEventId(None)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
struct IssuedUniqueIdx {
|
|
unique_idx: u16,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let figment = rocket::Config::figment().merge((
|
|
"limits",
|
|
rocket::data::Limits::new().limit("string", 100.megabytes()),
|
|
));
|
|
rocket::custom(figment)
|
|
.mount("/", rocket::routes![subscribe, issue_idx, broadcast])
|
|
.manage(Db::empty())
|
|
.launch()
|
|
.await?;
|
|
Ok(())
|
|
}
|