server/docs/infra/synkronisering.md
vegard 17b2c44bdd Oppdater docs etter SpacetimeDB-loven-implementering
synkronisering.md: oppdater implementeringsstatus, marker PG-lekkasjer som fikset
spacetimedb_integrasjon.md: nye lærdommer om timestamps, reducer-params,
schema-migrering og AI-worker-flyt

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

176 lines
12 KiB
Markdown

# Infrastruktur: PostgreSQL ↔ SpacetimeDB Synkronisering
**Filsti:** `docs/infra/synkronisering.md`
## 1. Konsept
SpacetimeDB gir sanntidsopplevelsen, PostgreSQL er langtidsminnet. Denne spec-en definerer hvordan data flyter mellom dem, hvem som eier sannheten, og hva som skjer ved feil.
### 1.1 Grunnregel: SpacetimeDB er en ren sanntidsbuffer
SpacetimeDB skal **aldri** være eneste lagringssted for data med varig verdi. All data som SpacetimeDB holder skal enten:
1. Allerede eksistere i PostgreSQL (read-only cache, f.eks. aktør-navn for autocomplete), eller
2. Synkes til PostgreSQL innen ~1 sekund (chat, kanban, markører).
**Konsekvens for design:** Ethvert SpacetimeDB-modul-skjema skal ha en tilsvarende PostgreSQL-tabell som kan fungere som drop-in erstatning dersom SpacetimeDB fjernes. Frontend-kode som leser fra SpacetimeDB skal kunne peke om til et SvelteKit SSE-endepunkt + REST uten arkitekturendring.
### 1.2 PostgreSQL-fallback-prinsippet
Dersom SpacetimeDB fjernes fra stacken, skal systemet fungere med følgende erstatning:
- **Sanntidsoppdateringer:** PostgreSQL `LISTEN/NOTIFY` → SvelteKit Server-Sent Events (SSE)
- **Skriving:** Direkte til PostgreSQL via SvelteKit server-side
- **Autocomplete/cache:** PostgreSQL-spørringer med cursor-basert paginering
Denne fallbacken trenger ikke implementeres på forhånd, men SpacetimeDB-moduler skal designes slik at fallbacken forblir triviell.
## 2. Strategi: Event-drevet med kort forsinkelse
SpacetimeDB-modulene (Rust) produserer persisterings-events ved dataendringer. En Rust-worker konsumerer disse og skriver til PostgreSQL med ~1 sekunds intervall.
**Akseptabelt datatap:** Maks 1 sekund ved hard krasj av SpacetimeDB. Dette er akseptabelt for chat, kanban og show notes.
**Unntak — kritiske events:** Aha-markører fra studioet (live-innspilling) er tidssensitive og vanskelige å gjenskape. Disse bør flushes til PG umiddelbart (ikke batched) via en dedikert `sync_critical()`-funksjon som skriver direkte til PG i stedet for via `sync_outbox`. Alternativt kan SpacetimeDB-modulen skrive kritiske events til sin egen WAL/disk umiddelbart. Hvilke event-typer som er "kritiske" defineres per workspace i `workspaces.settings`.
## 3. Dataflyt
```
┌──────────────┐ events ┌──────────────┐ batch write ┌──────────────┐
│ SpacetimeDB │ ──────────────► │ Rust Worker │ ────────────────► │ PostgreSQL │
│ (sanntid) │ │ (sync_to_pg) │ │ (persistent)│
└──────────────┘ └──────────────┘ └──────────────┘
┌──────────────┐ oppvarming (oppstart / reconnect) ┌──────────────┐
│ PostgreSQL │ ──────────────────────────────────────► │ SpacetimeDB │
└──────────────┘ └──────────────┘
```
## 4. Eierskapsmodell
| Data | Autoritativ kilde | Synkretning | Merknad |
|---|---|---|---|
| Chatmeldinger | SpacetimeDB | → PG (event, batched) | |
| Kanban-posisjon | SpacetimeDB | → PG (event) | |
| Show notes | SpacetimeDB | → PG (event) | |
| Live studio-markører | SpacetimeDB | → PG (event) | |
| Kunnskapsgraf | PostgreSQL | → SpacetimeDB (oppvarming) | Read-only i SpacetimeDB |
| Episodemetadata | PostgreSQL | Ingen synk | |
| Brukerkontoer | PostgreSQL (Authentik) | Ingen synk | |
| Statistikk | PostgreSQL | Ingen synk | |
| Valgomat | TBD | TBD | Konseptet må modnes. Mulig PG-autoritativ med SpacetimeDB som serveringslag |
## 5. Mekanisme
### 5.1 SpacetimeDB → PostgreSQL (persistering)
- SpacetimeDB-reducerne legger sync-events i `sync_outbox`-tabellen med tidsstempel og payload
- Rust-workeren poller `sync_outbox` hvert sekund, leser alle usynkede events, skriver til PostgreSQL, og markerer dem som synket via `mark_synced`-reducer
- Ved PG-nedetid: events akkumuleres i `sync_outbox`. Workeren prøver igjen ved neste poll. Ingen data tapes så lenge SpacetimeDB kjører
### 5.2 PostgreSQL → SpacetimeDB (oppvarming)
- Ved oppstart laster Rust-workeren data fra PG per kanal, basert på `channels.config`:
- `warmup_mode: "all"` — alle meldinger i kanalen
- `warmup_mode: "messages"` — de N nyeste *trådene* (komplett med svar)
- `warmup_mode: "days"` — alle tråder med aktivitet siste N dager (komplett)
- `warmup_mode: "none"` — kanalen hoppes over (arkivert/inaktiv)
- Trådbasert henting: et svar som kvalifiserer tar med hele tråden (inkludert eldre trådstarter)
- Konfigureres per kanal via admin-UI (`/admin/channels`) eller `channels.config` JSONB
- Kanalen ryddes (`clear_channel`) før lasting for å unngå duplikater ved restart
- Reaksjoner lastes også inn via `load_reactions`-reducer
## 6. Feilhåndtering
- **SpacetimeDB krasjer:** Data siden siste synk (~1 sek) tapes. Ved restart oppvarmes fra PG
- **PostgreSQL nede:** Sanntidsfunksjoner fortsetter å fungere. `sync_outbox` vokser. Workeren logger advarsler. Ved PG-recovery synkes backloggen automatisk
- **Rust-worker krasjer:** `sync_outbox` akkumuleres. Ved restart plukker workeren opp der den slapp (usynkede events har ingen markering)
## 7. Workspace-isolasjon
SpacetimeDB-synkronisering er fullstendig workspace-scopet:
* SpacetimeDB-tilkoblinger bærer `workspace_id` som context-token. Modulen partisjonerer minnet og kringkaster kun til klienter i samme workspace.
* `sync_outbox`-events inkluderer `workspace_id` i payloaden, slik at Rust-workeren skriver til riktig silo i PostgreSQL.
* Ved oppvarming (PG → SpacetimeDB) laster workeren kun data for den aktuelle workspace-en.
## 8. Konflikthåndtering
### 8.1 Strategi: Last-Write-Wins (LWW) med reservasjoner
SpacetimeDB er autoritativ for sanntidsdata. Konflikter kan oppstå i to scenarioer:
**Scenario A — Samtidig redigering i SpacetimeDB:**
SpacetimeDB er single-threaded per modul. Reducer-funksjoner serialiseres automatisk. Ingen klassiske race conditions.
**Scenario B — PG-oppvarming kolliderer med fersk SpacetimeDB-state:**
Ved restart av SpacetimeDB lastes data fra PG (oppvarming). Hvis en klient skriver til SpacetimeDB *under* oppvarming, kan PG-dataen overskrive ferske endringer.
**Løsning:** Oppvarming setter et `warming_up`-flagg i SpacetimeDB-modulen. Klienttilkoblinger aksepteres, men skrivinger bufres til oppvarmingen er fullført. Buffrede skrivinger appliseres deretter i rekkefølge.
### 8.2 Kanban: Posisjonskonflikter
Kanban-kort har en `position`-kolonne (float). To brukere som drar kort samtidig kan skape overlappende posisjoner. Løsning: SpacetimeDB-modulen re-beregner posisjoner (midtpunkts-strategi) og kringkaster oppdatert rekkefølge til alle klienter.
### 8.3 Chat: Ingen konflikter
Meldinger er append-only. Redigering av egne meldinger er last-write-wins — akseptabelt fordi kun én bruker eier meldingen.
## 9. Workers som endrer data frontend ser
**Kritisk regel:** En worker (Rust) som transformerer data som frontend viser, MÅ skrive resultatet til SpacetimeDB — ikke direkte til PG. PG-oppdatering skjer via sync-worker i bakgrunnen.
### Eksempel: AI-vask av chatmelding
**Riktig flyt:**
```
Frontend viser melding fra SpacetimeDB
→ Bruker trykker ✨
→ SvelteKit oppretter jobb i PG job_queue
→ Worker plukker opp jobb
→ Worker leser prompt + routing fra PG (det er infrastrukturdata, ikke frontend-data)
→ Worker kaller AI Gateway
→ Worker skriver resultat til SpacetimeDB via edit_message reducer
→ SpacetimeDB pusher oppdatering til frontend via onUpdate
→ Sync-worker persisterer til PG i bakgrunnen
```
**Feil flyt (anti-pattern som har blitt implementert gjentatte ganger):**
```
Worker skriver til PG direkte
→ Frontend henter metadata fra PG via enrichFromPg() ← BRYTER ABSTRAKSJONEN
→ SpacetimeDB-oppdatering er "best-effort" ← FEIL PRIORITERING
```
### Hva betyr dette for nye felter?
Når frontend trenger å vise noe nytt (metadata, revisjoner, edited_at), er prosedyren:
1. **Utvid SpacetimeDB-modulen** — legg til feltet i Rust-strukturen
2. **Utvid warmup** — last feltet fra PG ved oppstart
3. **Utvid sync** — persist feltet til PG i bakgrunnen
4. **Worker skriver til SpacetimeDB** — via reducer, aldri direkte PG for synlig data
5. **Frontend leser fra SpacetimeDB** — ingen enrichFromPg, ingen PG API-kall
### Hva kan workers lese fra PG?
Workers kan og bør lese infrastrukturdata direkte fra PG:
- Jobbkø (`job_queue`) — det er jobbens opphavssted
- AI-prompts, modellkonfigurasjon, routing — det er infrastruktur, ikke brukerdata
- Workspace-konfigurasjon
Skillet er: **data frontend viser** → SpacetimeDB. **Data kun worker trenger** → PG direkte.
## 10. Implementeringsstatus (mars 2026)
### Ferdig
- **SpacetimeDB som cache foran PG:** PG er autoritativ, SpacetimeDB er varm cache. Frontend snakker kun med SpacetimeDB.
- **SpacetimeDB Rust-modul** (`spacetimedb/src/lib.rs`): `ChatMessage` (med `metadata`, `edited_at`), `MessageReaction`, `MessageRevision` og `SyncOutbox`-tabeller. Reducers: `send_message`, `delete_message`, `edit_message`, `add_reaction`, `remove_reaction`, `load_messages`, `load_reactions`, `load_revisions`, `clear_channel`, `mark_synced`, `set_ai_processing`, `clear_ai_processing`, `ai_update_message`.
- **Worker warmup** (`worker/src/warmup.rs`): Ved oppstart lastes meldinger (med metadata/edited_at), reaksjoner og revisjoner per kanal fra PG → SpacetimeDB. Originale timestamps parses korrekt. Kanaler ryddes først med `clear_channel` for å unngå duplikater.
- **Worker sync** (`worker/src/sync.rs`): Poller `sync_outbox` hvert 1. sekund. Håndterer insert/delete/update/ai_update for meldinger og insert/delete for reaksjoner.
- **SpacetimeDB-adapter** (`web/src/lib/chat/spacetime.svelte.ts`): Ren SpacetimeDB-adapter. Null PG API-kall (`fetch()`). Bruker `onInsert`/`onUpdate`/`onDelete` callbacks for sanntid. Reaksjoner bygges fra `message_reaction`-tabellen. Revisjoner leses fra `message_revision`-tabellen.
- **PG-fallback** (`web/src/lib/chat/pg.svelte.ts`): Brukes kun når SpacetimeDB ikke er konfigurert. Markert som `readonly: true`.
- **Adapter-mønster:** `ChatConnection`-interface med `send`, `edit`, `delete`, `react` metoder. Factory velger basert på env-variabel.
### Fikset (mars 2026)
- **`enrichFromPg` fjernet** — SpacetimeDB-modulen har nå `metadata` og `edited_at``ChatMessage`. Frontend leser alt fra SpacetimeDB, null `fetch()`-kall i adapteren.
- **Worker AI-vask via reducers** — `set_ai_processing`, `ai_update_message` og `clear_ai_processing` reducers erstatter direkte PG-skriving. Sync-worker persisterer til PG via `ai_update` outbox-action.
- **Revisjoner i SpacetimeDB** — `MessageRevision`-tabell med `load_revisions` warmup-reducer. Frontend leser revisjoner via `getRevisions()` fra `conn.db.message_revision`.
- **Warmup med metadata** — `load_messages` inkluderer `metadata`, `edited_at` og parser originale timestamps korrekt.
### Gjenstår
- **Workspace-partisjonering (§7):** SpacetimeDB-modulen har `workspace_id`-felt men bruker ikke workspace-token på tilkobling ennå.
- **Pin/konvertering via SpacetimeDB:** Pin og kanban/kalender-konvertering går fortsatt direkte til PG API.
- **Lazy warmup per kanal:** Alle aktive kanaler oppvarmes ved oppstart. Kan optimaliseres til per-kanal ved tilkobling.
## 11. Instruks for Claude Code
- `sync_outbox`-tabellen i SpacetimeDB bør ha et `synced`-flagg og `created_at`-tidsstempel
- Workeren skal bruke jobbkø-infrastrukturen (se `docs/infra/jobbkø.md`) for sin egen helse/observabilitet, men selve pollingen er en egen loop — ikke en vanlig jobb i køen
- Hold sync-payloaden enkel: `{ "table": "chat_messages", "action": "insert", "data": {...} }` — workeren mapper dette til riktig PG-tabell
- Ikke optimaliser for store datamengder ennå. Enkle INSERTs er bra nok til volumet stabiliserer seg
- Alle sync-payloads må inkludere `workspace_id`. Workeren bruker dette til å sette RLS-kontekst (`SET app.current_workspace_id`) før PG-skriving, eller kjører som superuser og filtrerer eksplisitt