server/worker/src/warmup.rs
vegard 8b58d434e9 SpacetimeDB som cache foran PG: arkitekturendring
PG er autoritativ, SpacetimeDB er varm cache. Frontend snakker
kun med SpacetimeDB, worker håndterer toveissynk.

Fase 1 — SpacetimeDB-modul:
- delete_message med SyncOutbox-event
- edit_message reducer
- MessageReaction tabell + add/remove_reaction reducers
- load_messages med JSON-parsing (erstatter pipe-format)
- clear_channel reducer for duplikat-fri warmup
- load_reactions reducer

Fase 2 — Worker:
- warmup.rs: PG→ST oppvarming ved oppstart (100 msg/kanal)
- sync.rs: håndter delete/update/reaction actions
- Sync-intervall redusert til 1s

Fase 3 — Frontend:
- spacetime.svelte.ts: ren SpacetimeDB-adapter, ingen PG-hybrid
- ChatConnection interface med edit/delete/react metoder
- ChatBlock bruker chat.edit/delete/react direkte
- PG-adapter som readonly fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 02:09:33 +01:00

168 lines
5.4 KiB
Rust

use reqwest::Client;
use sqlx::PgPool;
use tracing::{info, warn};
/// Oppvarming: les siste N meldinger per aktive kanal fra PG og last inn i SpacetimeDB.
pub async fn run(
pool: &PgPool,
http: &Client,
spacetimedb_url: &str,
module: &str,
limit: i64,
) -> anyhow::Result<()> {
info!(limit, "Starter oppvarming (PG → SpacetimeDB)");
// Finn aktive kanaler (kanaler med meldinger)
let channels: Vec<(String,)> = sqlx::query_as(
"SELECT DISTINCT channel_id::text FROM messages WHERE channel_id IS NOT NULL"
)
.fetch_all(pool)
.await?;
if channels.is_empty() {
info!("Ingen aktive kanaler funnet — oppvarming fullført");
return Ok(());
}
info!(channels = channels.len(), "Aktive kanaler funnet");
let mut total_messages = 0u64;
let mut total_reactions = 0u64;
for (channel_id,) in &channels {
// Rydd kanalen i SpacetimeDB først for å unngå duplikater
if let Err(e) = call_reducer(http, spacetimedb_url, module, "clear_channel", &serde_json::json!({
"channel_id": channel_id
})).await {
warn!(channel_id, error = %e, "Kunne ikke rydde kanal — hopper over");
continue;
}
// Hent meldinger med forfatterinfo
let rows: Vec<(String, String, String, String, String, String, String, Option<String>, String)> = sqlx::query_as(
r#"
SELECT
m.id::text,
m.channel_id::text,
n.workspace_id::text,
COALESCE(m.author_id, ''),
COALESCE(u.name, 'Ukjent'),
COALESCE(m.body, ''),
COALESCE(m.message_type, 'text'),
m.reply_to::text,
m.created_at::text
FROM messages m
JOIN nodes n ON n.id = m.id
LEFT JOIN users u ON u.authentik_id = m.author_id
WHERE m.channel_id = $1::uuid
ORDER BY m.created_at DESC
LIMIT $2
"#
)
.bind(channel_id)
.bind(limit)
.fetch_all(pool)
.await?;
if rows.is_empty() { continue; }
// Bygg JSON-array
let messages: Vec<serde_json::Value> = rows.iter().map(|r| {
serde_json::json!({
"id": r.0,
"channel_id": r.1,
"workspace_id": r.2,
"author_id": r.3,
"author_name": r.4,
"body": r.5,
"message_type": r.6,
"reply_to": r.7.as_deref().unwrap_or(""),
"created_at": r.8
})
}).collect();
let count = messages.len();
let json_str = serde_json::to_string(&messages)?;
if let Err(e) = call_reducer(http, spacetimedb_url, module, "load_messages", &serde_json::json!({
"messages_json": json_str
})).await {
warn!(channel_id, error = %e, "Feil ved lasting av meldinger");
continue;
}
total_messages += count as u64;
// Hent reaksjoner for denne kanalens meldinger
let reaction_rows: Vec<(String, String, String, String)> = sqlx::query_as(
r#"
SELECT
mr.message_id::text,
COALESCE(mr.user_id, ''),
COALESCE(u.name, 'Ukjent'),
mr.reaction
FROM message_reactions mr
JOIN messages m ON m.id = mr.message_id
LEFT JOIN users u ON u.authentik_id = mr.user_id
WHERE m.channel_id = $1::uuid
"#
)
.bind(channel_id)
.fetch_all(pool)
.await?;
if !reaction_rows.is_empty() {
let reactions: Vec<serde_json::Value> = reaction_rows.iter().map(|r| {
serde_json::json!({
"message_id": r.0,
"user_id": r.1,
"user_name": r.2,
"reaction": r.3
})
}).collect();
let reactions_json = serde_json::to_string(&reactions)?;
if let Err(e) = call_reducer(http, spacetimedb_url, module, "load_reactions", &serde_json::json!({
"reactions_json": reactions_json
})).await {
warn!(channel_id, error = %e, "Feil ved lasting av reaksjoner");
} else {
total_reactions += reaction_rows.len() as u64;
}
}
info!(channel_id, messages = count, reactions = reaction_rows.len(), "Kanal oppvarmet");
}
info!(
channels = channels.len(),
messages = total_messages,
reactions = total_reactions,
"Oppvarming fullført"
);
Ok(())
}
async fn call_reducer(
http: &Client,
base_url: &str,
module: &str,
reducer: &str,
args: &serde_json::Value,
) -> anyhow::Result<()> {
let url = format!("{}/v1/database/{}/call/{}", base_url, module, reducer);
let resp = http
.post(&url)
.json(args)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
anyhow::bail!("{} feilet ({}): {}", reducer, status, body);
}
Ok(())
}