S5: WebSocket real-time — per-channel broadcast, auto-reconnect
Backend: - AppState with per-channel broadcast::Sender map - WS handler: auth via first message, keepalive pings, broadcast forwarding - post_message broadcasts WsEvent::Message to all subscribers Frontend: - useChannelSocket hook: connects, auths, appends messages, auto-reconnects - Removed 3s polling — WebSocket is primary, initial load via REST - Deduplication on WS messages (sender also fetches after post) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ use sqlx::SqlitePool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::db::*;
|
||||
use crate::state::AppState;
|
||||
|
||||
// ── Error handling ──
|
||||
|
||||
@@ -75,30 +76,30 @@ pub async fn health() -> &'static str {
|
||||
|
||||
// ── Channels ──
|
||||
|
||||
pub async fn list_channels(State(db): State<SqlitePool>) -> Result<Json<Vec<Channel>>> {
|
||||
pub async fn list_channels(State(state): State<AppState>) -> Result<Json<Vec<Channel>>> {
|
||||
let rows = sqlx::query_as::<_, ChannelRow>("SELECT * FROM channels ORDER BY created_at")
|
||||
.fetch_all(&db)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let channels: Vec<Channel> = rows.iter().map(|r| r.to_api()).collect();
|
||||
Ok(Json(channels))
|
||||
}
|
||||
|
||||
pub async fn list_users(State(db): State<SqlitePool>) -> Result<Json<Vec<User>>> {
|
||||
pub async fn list_users(State(state): State<AppState>) -> Result<Json<Vec<User>>> {
|
||||
let rows = sqlx::query_as::<_, UserRow>("SELECT * FROM users ORDER BY created_at")
|
||||
.fetch_all(&db)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
Ok(Json(rows.iter().map(|r| r.to_api()).collect()))
|
||||
}
|
||||
|
||||
pub async fn get_me(
|
||||
State(db): State<SqlitePool>,
|
||||
State(state): State<AppState>,
|
||||
Query(user_param): Query<UserParam>,
|
||||
) -> Result<Json<User>> {
|
||||
let username = user_param.user.as_deref().unwrap_or("benji");
|
||||
let row = sqlx::query_as::<_, UserRow>("SELECT * FROM users WHERE username = ?")
|
||||
.bind(username)
|
||||
.fetch_optional(&db)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
match row {
|
||||
Some(r) => Ok(Json(r.to_api())),
|
||||
@@ -107,36 +108,36 @@ pub async fn get_me(
|
||||
}
|
||||
|
||||
pub async fn create_channel(
|
||||
State(db): State<SqlitePool>,
|
||||
State(state): State<AppState>,
|
||||
Query(user_param): Query<UserParam>,
|
||||
Json(body): Json<CreateChannel>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let created_by = resolve_user(&db, &user_param).await?;
|
||||
let created_by = resolve_user(&state.db, &user_param).await?;
|
||||
|
||||
sqlx::query("INSERT INTO channels (id, name, description, created_by) VALUES (?, ?, ?, ?)")
|
||||
.bind(&id)
|
||||
.bind(&body.name)
|
||||
.bind(&body.description)
|
||||
.bind(created_by)
|
||||
.execute(&db)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
let row = sqlx::query_as::<_, ChannelRow>("SELECT * FROM channels WHERE id = ?")
|
||||
.bind(&id)
|
||||
.fetch_one(&db)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok((StatusCode::CREATED, Json(row.to_api())))
|
||||
}
|
||||
|
||||
pub async fn get_channel(
|
||||
State(db): State<SqlitePool>,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<Channel>> {
|
||||
let row = sqlx::query_as::<_, ChannelRow>("SELECT * FROM channels WHERE id = ?")
|
||||
.bind(&id)
|
||||
.fetch_optional(&db)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
match row {
|
||||
@@ -148,7 +149,7 @@ pub async fn get_channel(
|
||||
// ── Messages ──
|
||||
|
||||
pub async fn list_messages(
|
||||
State(db): State<SqlitePool>,
|
||||
State(state): State<AppState>,
|
||||
Path(channel_id): Path<String>,
|
||||
Query(query): Query<MessageQuery>,
|
||||
) -> Result<Json<Vec<Message>>> {
|
||||
@@ -186,13 +187,13 @@ pub async fn list_messages(
|
||||
q = q.bind(b);
|
||||
}
|
||||
|
||||
let rows = q.fetch_all(&db).await?;
|
||||
let rows = q.fetch_all(&state.db).await?;
|
||||
let messages: Vec<Message> = rows.iter().map(|r| r.to_api_message()).collect();
|
||||
Ok(Json(messages))
|
||||
}
|
||||
|
||||
pub async fn post_message(
|
||||
State(db): State<SqlitePool>,
|
||||
State(state): State<AppState>,
|
||||
Path(channel_id): Path<String>,
|
||||
Query(user_param): Query<UserParam>,
|
||||
Json(body): Json<PostMessage>,
|
||||
@@ -202,7 +203,7 @@ pub async fn post_message(
|
||||
"SELECT COUNT(*) FROM channels WHERE id = ?",
|
||||
)
|
||||
.bind(&channel_id)
|
||||
.fetch_one(&db)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
|
||||
if channel_exists == 0 {
|
||||
@@ -215,7 +216,7 @@ pub async fn post_message(
|
||||
"SELECT channel_id FROM messages WHERE id = ?",
|
||||
)
|
||||
.bind(reply_id.to_string())
|
||||
.fetch_optional(&db)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
match reply_channel {
|
||||
@@ -228,7 +229,7 @@ pub async fn post_message(
|
||||
}
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let user_id = resolve_user(&db, &user_param).await?;
|
||||
let user_id = resolve_user(&state.db, &user_param).await?;
|
||||
|
||||
let msg_type = match body.r#type {
|
||||
MessageType::Text => "text",
|
||||
@@ -256,7 +257,7 @@ pub async fn post_message(
|
||||
.bind(&body.content)
|
||||
.bind(&metadata_json)
|
||||
.bind(&reply_to)
|
||||
.execute(&db)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
// Fetch the full message with user
|
||||
@@ -265,10 +266,16 @@ pub async fn post_message(
|
||||
FROM messages m JOIN users u ON m.user_id = u.id WHERE m.id = ?",
|
||||
)
|
||||
.bind(&id)
|
||||
.fetch_one(&db)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok((StatusCode::CREATED, Json(row.to_api_message())))
|
||||
let message = row.to_api_message();
|
||||
|
||||
// Broadcast to WebSocket subscribers
|
||||
let tx = state.get_sender(&channel_id).await;
|
||||
let _ = tx.send(WsEvent::Message(message.clone()));
|
||||
|
||||
Ok((StatusCode::CREATED, Json(message)))
|
||||
}
|
||||
|
||||
// ── Joined row type for message + user ──
|
||||
|
||||
Reference in New Issue
Block a user