server/docs/erfaringer/svelte5_reaktivitet.md
vegard a5985ef3f8 Dokumentasjon, erfaringslogg, migrasjoner og infra-oppdateringer
- Omorganiser docs/: konsepter, features, infra og proposals i egne mapper
- Ny docs/erfaringer/ med lærdommer fra chat-implementering (Svelte 5, SpacetimeDB, adapter-mønster)
- Oppdater ARCHITECTURE.md: Lag 1 status, ny §10 Erfaringslogg, SpacetimeDB i lokal dev
- Oppdater synkronisering.md med implementeringsstatus og designvalg
- Oppdater lokal.md med SpacetimeDB og AI Gateway
- Utvid PG-skjema med channels, messages, media_files, message_revisions
- Legg til seed_dev.sql, migration_safety.md, .env.example
- Nye feature-specs: chat, kanban, whiteboard, live_ai, lydmeldinger m.fl.
- Nye konsept-specs: studioet, møterommet, redaksjonen, den asynkrone gjesten m.fl.
- SpacetimeDB og AI Gateway i docker-compose.dev.yml
- collect-docs.sh inkluderer erfaringer/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 01:40:14 +01:00

73 lines
2.5 KiB
Markdown

# Erfaring: Svelte 5 Reaktivitet
## 1. `$state` i `.svelte.ts` krever getters
Svelte 5 sin `$state` lager reaktive proxyer. Når en funksjon returnerer et objekt med `$state`-verdier, **mister man reaktiviteten** hvis man returnerer verdien direkte:
```typescript
// FEIL — mister reaktivitet, verdien fryses ved retur
function createThing() {
let count = $state(0);
return { count }; // snapshot, ikke reaktiv
}
// RIKTIG — getter bevarer proxy-tilgang
function createThing() {
let count = $state(0);
return {
get count() { return count; }
};
}
```
**Referanse:** `web/src/lib/chat/pg.svelte.ts` — alle returnerte verdier bruker getters.
## 2. SSR kjører alt utenfor `onMount`
SvelteKit server-renderer kjører komponent-script ved SSR. Alt som bruker browser-APIer (WebSocket, `fetch` til relative URLer, `setInterval`, `sessionStorage`) **krasjer på serveren** hvis det ikke er beskyttet.
```typescript
// FEIL — krasjer ved SSR
let chat = createChat(channelId); // kjøres på server
// RIKTIG — kun i browser
import { onMount } from 'svelte';
let chat = $state<ChatConnection | null>(null);
onMount(() => {
chat = createChat(channelId);
return () => chat?.destroy();
});
```
Alternativt kan factory-funksjonen selv sjekke:
```typescript
import { browser } from '$app/environment';
if (browser) { /* ... */ }
```
**Referanse:** `web/src/lib/chat/create.svelte.ts``browser`-guard i factory, `web/src/lib/blocks/ChatBlock.svelte``onMount` for opprettelse.
## 3. `$derived` og `$effect` med null-initialisert state
Når en `$state`-variabel starter som `null` (fordi den settes i `onMount`), må `$derived` og `$effect` håndtere null-tilfellet:
```typescript
let chat = $state<ChatConnection | null>(null);
let messages = $derived(chat?.messages ?? []); // fallback til tom liste
$effect(() => {
const count = messages.length; // trygt, alltid array
if (count > prevCount) scrollToBottom();
prevCount = count;
});
```
**Referanse:** `web/src/lib/blocks/ChatBlock.svelte``$derived` med optional chaining.
## 4. Polling: full-fetch slår inkrementell akkumulering
Vi prøvde først å bruke en `latestTimestamp`-cursor for å hente kun nye meldinger og appende dem. Dette ga duplikater — telleren vokste mens man tastet (polling i bakgrunnen).
**Løsning:** Enkel `refresh()` som alltid henter full liste og erstatter `messages` i sin helhet. For et lite volum meldinger er dette enklere og tryggere enn inkrementell logikk.
**Referanse:** `web/src/lib/chat/pg.svelte.ts``refresh()` gjør full fetch, ingen akkumulering.