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>
168 lines
5.4 KiB
Rust
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(())
|
|
}
|