- Fiks NULL-håndtering i warmup (COALESCE for title/content/created_by) - Renere health check (delete nonexistent node i stedet for create+delete) - Dokumenter HTTP API-format og warmup-flyt i erfaringer - Lagre STDB-token i server .env - Republiser STDB-modul etter containerrestart Verifisert: warmup laster 2 noder + 1 edge, /health viser stdb=connected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.9 KiB
Erfaring: SpacetimeDB-integrasjon
1. Genererte bindings — navnekonvensjoner
SpacetimeDB sin spacetime generate --lang typescript produserer bindings med inkonsistente konvensjoner. Sjekk alltid de genererte filene i stedet for å gjette.
| Hva | Konvensjon | Eksempel |
|---|---|---|
Tabell-accessor på conn.db |
snake_case | conn.db.chat_message |
Reducer-accessor på conn.reducers |
camelCase | conn.reducers.sendMessage() |
| Felt-navn i tabellrader | camelCase | row.channelId, row.authorName |
| Reducer-parametere | Enkelt objekt | sendMessage({ id, channelId, body, ... }) |
Felle: Man forventer at tabell-accessorer er camelCase (chatMessage), men de er snake_case.
2. DbConnection.builder() — API-detaljer (SDK v2)
DbConnection.builder()
.withUri(spacetimeUrl)
.withDatabaseName(moduleName) // IKKE withModuleName
.withToken(token)
.onConnect((connection) => { // første param er DbConnection, ikke EventContext
connection.subscriptionBuilder()
.subscribe([`SELECT * FROM chat_message WHERE channel_id = '${id}'`]);
})
.build();
Feller:
withModuleName()finnes ikke — brukwithDatabaseName()onConnect-callback mottarDbConnection, ikkeEventContextonConnectError-callback har signatur(ctx, errMessage)dererrMessageer en string
3. Timestamps — bruk SDK-metoder, ikke interne felter
SpacetimeDB Timestamp-objektet har en intern property __timestamp_micros_since_unix_epoch__ (BigInt). Ikke bruk den direkte — bruk SDK-metodene:
// FEIL — microsSinceEpoch finnes ikke, __timestamp_micros_since_unix_epoch__ er internt
const micros = row.createdAt?.microsSinceEpoch;
// RIKTIG — bruk SDK-metoder
const iso = row.createdAt?.toISOString(); // "2026-03-15T23:57:11.677139Z"
const date = row.createdAt?.toDate(); // Date-objekt
const ms = row.createdAt?.toDate()?.getTime(); // millisekunder for sortering
Timestamp-parsing i Rust-modul (warmup)
PG returnerer timestamps som "2026-03-15 23:57:11.677139+00". chrono parser IKKE +00 — krever +00:00:
// FEIL — chrono gir ParseError(TooShort) på "+00"
let dt = s.parse::<chrono::DateTime<chrono::FixedOffset>>();
// RIKTIG — normaliser PG-offset først
let normalized = if s.ends_with("+00") { format!("{}:00", s) } else { s.to_string() };
let dt = chrono::DateTime::parse_from_str(&normalized, "%Y-%m-%d %H:%M:%S%.f%:z");
Uten korrekt parsing faller load_messages tilbake til ctx.timestamp (nåtidspunkt), og alle meldinger får samme klokkeslett.
Referanse: spacetimedb/src/lib.rs — parse_timestamp(), web/src/lib/chat/spacetime.svelte.ts — spacetimeRowToMessage().
4. Rust-modul — borrow checker med SpacetimeDB-makroer
SpacetimeDB-makroer genererer kode som tar eierskap over struct-felter. Bruk verdier før du flytter dem inn i structs:
// FEIL — channel_id er moved inn i ChatMessage, kan ikke bruke i log!() etterpå
let msg = ChatMessage { channel_id, body, ... };
log::info!("Melding i kanal {}", channel_id); // borrow after move
// RIKTIG — bruk verdien før struct-opprettelse
let log_msg = format!("Melding i kanal {}", channel_id);
let msg = ChatMessage { channel_id, body, ... };
log::info!("{}", log_msg);
5. Publisering og lokal testing
# Publiser modul mot lokal SpacetimeDB (må kjøre i Docker først)
cd spacetimedb
spacetime publish sidelinja-realtime --server local
# Generer TypeScript-bindings
spacetime generate --lang typescript --out-dir ../web/src/lib/chat/module_bindings \
--module-path .
Merk: SpacetimeDB-modulen publiseres manuelt med spacetime publish mot server-instansen.
6. Arkitekturendring: SpacetimeDB som cache foran PG (mars 2026)
Tidligere: Hybrid-adapter der frontend merget data fra PG (historikk) og SpacetimeDB (sanntid) med dedup, deletedIds og BigInt-workarounds.
Ny modell:
- PG autoritativ — all persistent data i PostgreSQL
- SpacetimeDB = varm cache — worker gjør warmup (PG → ST) ved oppstart
- Frontend snakker KUN med ST — ingen PG API-kall fra chat-adapteren
- Worker håndterer toveissynk — ST → PG for nye/redigerte/slettede meldinger og reaksjoner
Warmup-flyt
- Worker starter →
warmup::run()leser kanaler med config fra PG - Per kanal: sjekker
channels.config.warmup_mode(all/messages/days/none) - Kaller
clear_channelreducer (unngår duplikater ved restart) - Trådbasert henting: Finner kvalifiserende tråder, henter alle meldinger i disse (komplett med svar)
messages-modus: de N nyeste trådene (sortert etter siste aktivitet)days-modus: alle tråder med minst én melding i tidsvinduet- Et svar som kvalifiserer tar med hele tråden (inkludert eldre trådstarter)
- Kaller
load_messagesreducer med JSON-array - Laster også reaksjoner via
load_reactionsreducer
Per-kanal konfigurasjon
- Lagres i
channels.configJSONB:warmup_mode+warmup_value - Admin-UI:
/admin/channels— tabell med inline-redigering - Default:
"all"(last alt). Andre:"messages"(siste N tråder),"days"(siste N dager),"none"(inaktiv)
Sync-flyt (ST → PG)
- SyncOutbox-events prosesseres hver 1. sekund
- Støtter:
messages/insert,messages/delete,messages/update,messages/ai_update,message_reactions/insert,message_reactions/delete ai_update-action: oppdaterer body + metadata + edited_at i PG, inserter revisjon
7. Subscription-begrensninger
SpacetimeDB-subscriptions støtter IKKE JOINs. En subscription-query som SELECT mr.* FROM message_reaction mr JOIN chat_message cm ON cm.id = mr.message_id WHERE ... feiler stille — onApplied kalles aldri, og ingen data vises.
Bruk kun enkle SELECT * FROM tabell WHERE ...-queries i .subscribe([...]). Filtrer heller klient-side etter at data er lastet.
Eksempel:
// FEIL — feiler stille, ingen data
.subscribe([
`SELECT * FROM chat_message WHERE channel_id = '${id}'`,
`SELECT mr.* FROM message_reaction mr JOIN chat_message cm ON cm.id = mr.message_id WHERE cm.channel_id = '${id}'`
]);
// RIKTIG — last alle reaksjoner, filtrer i koden
.subscribe([
`SELECT * FROM chat_message WHERE channel_id = '${id}'`,
`SELECT * FROM message_reaction`
]);
Fallback
PG-polling adapter (pg.svelte.ts) brukes kun når SpacetimeDB ikke er konfigurert. Markeres som readonly: true.
8. Reducer-parameternavn — unngå underscore-prefix
Kodeeksemplene i denne seksjonen er fra v1 og bruker
workspace_id-parametere. Workspace-modellen er erstattet av noder og edges (sedocs/retninger/bruker_ikke_workspace.md), men lærdommen om underscore-prefix gjelder generelt for alle SpacetimeDB-reducere.
SpacetimeDB eksponerer Rust-parameternavn direkte i HTTP JSON API-et. Underscore-prefix (_workspace_id) blir til _workspace_id i JSON, ikke workspace_id:
// FEIL — HTTP-kall med {"workspace_id": "..."} feiler med 400
pub fn set_ai_processing(ctx: &ReducerContext, id: String, _workspace_id: String) { ... }
// RIKTIG — bruk vanlig navn, suppress warning med let _ = &var;
pub fn set_ai_processing(ctx: &ReducerContext, id: String, workspace_id: String) -> Result<(), String> {
let _ = &workspace_id;
// ...
}
9. Schema-migrering ved nye kolonner
Å legge til kolonner på eksisterende SpacetimeDB-tabeller krever --delete-data ved publish. Dette sletter all data og krever warmup på nytt:
# Feiler uten --delete-data:
# "Adding a column metadata to table chat_message requires a default value annotation"
echo "y" | spacetime publish sidelinja-realtime --server local --delete-data
10. AI-worker-flyt via SpacetimeDB
Worker som gjør AI-behandling av meldinger:
- Leser meldingens body fra PG (OK — PG er persistent lager)
- Kaller
set_ai_processingreducer → frontend ser pulsering umiddelbart - Kaller AI Gateway med prompt
- Kaller
ai_update_messagereducer → SpacetimeDB oppdaterer body/metadata/edited_at atomisk, lagrer revisjon, legger outbox-entry - Sync-worker persisterer til PG via
ai_updateaction - Ved feil:
clear_ai_processingreducer rydder flagget
11. HTTP API for å kalle reducere fra Rust (maskinrommet)
SpacetimeDB eksponerer et HTTP JSON API for å kalle reducere server-side.
Maskinrommet bruker reqwest (allerede en avhengighet) — ingen STDB SDK nødvendig.
Endepunkt
POST /v1/database/{database_name}/call/{reducer_name}
Authorization: Bearer {stdb_token}
Content-Type: application/json
{
"param1": "verdi1",
"param2": "verdi2"
}
Viktige detaljer
- Navngitte parametre: Body er et JSON-objekt med nøkler som matcher
reducer-parameternavn, IKKE en
{"args": [...]}array. - Token: Opprettes via
POST /v1/identity→{"identity": "...", "token": "..."}. Token er en JWT signert med serverens ES256-nøkkel. Ny server = ny nøkkel = nye tokens. - Suksess: HTTP 200 med tom body.
- Reducer-feil: HTTP 200 med
Err(String)— feilmeldingen er i body. - Valideringsfeil: HTTP 400 med beskrivende feilmelding.
- Auth-feil: HTTP 401/403.
Token-håndtering ved containerrestart
Hvis SpacetimeDB-containeren gjenskapes (data slettet), må:
- Ny identitet opprettes via
POST /v1/identity - Modulen republiseres med
spacetime publish - STDB-tokenet i
.envoppdateres
Maskinrommet kan automatisk opprette en ny identitet ved oppstart hvis
SPACETIMEDB_TOKEN ikke er satt, men denne identiteten vil ikke ha
tilgang til en eksisterende database. Bruk derfor alltid et stabilt token.
Warmup via HTTP API
Maskinrommet gjør warmup (PG → STDB) ved oppstart:
clear_all— tøm STDB (unngå duplikater ved restart)- Last alle noder fra PG, kall
create_nodefor hver - Last alle edges fra PG, kall
create_edgefor hver
NULLable PG-kolonner (title, content, created_by) må håndteres med
COALESCE i SQL-spørringen — STDB-modulen bruker String, ikke Option.