synops/docs/erfaringer/spacetimedb_integrasjon.md
vegard 0a467066ba Synops v2: arkitektur, retninger og dokumentasjon
Nystart basert på arkitektonisk innsikt fra Sidelinja v1.
Koden er ny, visjon og primitiver er validert gjennom tidligere arbeid.

Inneholder:
- Komplett arkitekturdokumentasjon (docs/arkitektur.md)
- 6 vedtatte retninger (docs/retninger/)
- Alle concepts, features, proposals og erfaringer fra v1
- Server-oppsett og drift (docs/setup/)
- LiteLLM-konfigurasjon (API-nøkler via env)
- Editor.svelte referanse fra v1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 06:43:08 +01:00

7.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 — bruk withDatabaseName()
  • onConnect-callback mottar DbConnection, ikke EventContext
  • onConnectError-callback har signatur (ctx, errMessage) der errMessage er 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.rsparse_timestamp(), web/src/lib/chat/spacetime.svelte.tsspacetimeRowToMessage().

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: Bruk ./dev.sh for å starte hele stacken automatisk (inkl. SpacetimeDB publish + binding-generering). ./dev.sh --clean starter blankt.

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

  1. Worker starter → warmup::run() leser kanaler med config fra PG
  2. Per kanal: sjekker channels.config.warmup_mode (all/messages/days/none)
  3. Kaller clear_channel reducer (unngår duplikater ved restart)
  4. 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)
  5. Kaller load_messages reducer med JSON-array
  6. Laster også reaksjoner via load_reactions reducer

Per-kanal konfigurasjon

  • Lagres i channels.config JSONB: 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

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:

  1. Leser meldingens body fra PG (OK — PG er persistent lager)
  2. Kaller set_ai_processing reducer → frontend ser pulsering umiddelbart
  3. Kaller AI Gateway med prompt
  4. Kaller ai_update_message reducer → SpacetimeDB oppdaterer body/metadata/edited_at atomisk, lagrer revisjon, legger outbox-entry
  5. Sync-worker persisterer til PG via ai_update action
  6. Ved feil: clear_ai_processing reducer rydder flagget