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:
2026-07-08 22:14:26 +02:00
parent 6b97ec3b0f
commit ec3771f2e7
10 changed files with 696 additions and 21 deletions
+25 -2
View File
@@ -8,6 +8,8 @@ use leptos_router::{
use crate::auth::{current_user, User};
use crate::chat::{is_valid_room, room_subject, ChatMessage, SendMessage, DEFAULT_ROOM, ROOMS};
#[cfg(feature = "hydrate")]
use crate::chat::room_history;
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
@@ -144,7 +146,7 @@ fn ChatShell(room: Memo<String>, user: User) -> impl IntoView {
{
let es_handle = StoredValue::new_local(None::<web_sys::EventSource>);
Effect::new(move |_| {
let room = room.get();
let room_name = room.get();
es_handle.update_value(|es| {
if let Some(es) = es.take() {
es.close();
@@ -152,7 +154,28 @@ fn ChatShell(room: Memo<String>, user: User) -> impl IntoView {
});
messages.set(Vec::new());
status.set(BusStatus::Connecting);
es_handle.set_value(open_event_source(&room, messages, status));
es_handle.set_value(open_event_source(&room_name, messages, status));
// Backfill from the Postgres archive; live SSE messages may land
// first, so merge by id (history first, then unseen live ones).
leptos::task::spawn_local(async move {
let Ok(history) = room_history(room_name.clone()).await else {
return;
};
// The user may have switched rooms while we were fetching.
if room.get_untracked() != room_name {
return;
}
messages.update(|live| {
let mut merged = history;
for m in live.drain(..) {
if !merged.iter().any(|h| h.id == m.id) {
merged.push(m);
}
}
*live = merged;
});
});
});
on_cleanup(move || {
es_handle.update_value(|es| {