Persist chat history to Postgres via durable JetStream consumer
DATABASE_URL required at startup; schema self-initializes. Compose gains a postgres service for standalone dev. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+35
-1
@@ -30,8 +30,14 @@ pub struct ChatMessage {
|
||||
pub text: String,
|
||||
/// Pre-formatted UTC wall-clock time, e.g. "14:03:27".
|
||||
pub time: String,
|
||||
/// Publish instant as UTC epoch milliseconds; orders history.
|
||||
#[serde(default)]
|
||||
pub ts: i64,
|
||||
}
|
||||
|
||||
/// How many messages `room_history` backfills when joining a room.
|
||||
pub const HISTORY_LIMIT: i64 = 100;
|
||||
|
||||
/// Publishes a message to the room's NATS subject. Requires a signed-in
|
||||
/// session; the sender identity always comes from the session, never from
|
||||
/// the client.
|
||||
@@ -58,13 +64,15 @@ pub async fn send_message(room: String, text: String) -> Result<(), ServerFnErro
|
||||
};
|
||||
|
||||
let state = expect_context::<AppState>();
|
||||
let now = chrono::Utc::now();
|
||||
let msg = ChatMessage {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
room: room.clone(),
|
||||
username: user.username,
|
||||
display_name: user.display_name,
|
||||
text,
|
||||
time: chrono::Utc::now().format("%H:%M:%S").to_string(),
|
||||
time: now.format("%H:%M:%S").to_string(),
|
||||
ts: now.timestamp_millis(),
|
||||
};
|
||||
let payload = serde_json::to_vec(&msg).map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
state
|
||||
@@ -74,3 +82,29 @@ pub async fn send_message(room: String, text: String) -> Result<(), ServerFnErro
|
||||
.map_err(|e| ServerFnError::new(format!("nats publish failed: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the most recent messages for a room (oldest first), read from the
|
||||
/// Postgres archive maintained by the JetStream consumer.
|
||||
#[server]
|
||||
pub async fn room_history(room: String) -> Result<Vec<ChatMessage>, ServerFnError> {
|
||||
use crate::auth::{User, SESSION_USER_KEY};
|
||||
use crate::server::AppState;
|
||||
|
||||
if !is_valid_room(&room) {
|
||||
return Err(ServerFnError::new("unknown room"));
|
||||
}
|
||||
let session: tower_sessions::Session = leptos_axum::extract().await?;
|
||||
if session
|
||||
.get::<User>(SESSION_USER_KEY)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?
|
||||
.is_none()
|
||||
{
|
||||
return Err(ServerFnError::new("not signed in"));
|
||||
}
|
||||
|
||||
let state = expect_context::<AppState>();
|
||||
crate::server::store::recent(&state.pool, &room, HISTORY_LIMIT)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("history unavailable: {e}")))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user