server/docs/erfaringer/spacetimedb_integrasjon.md
vegard 88a22e131b SpacetimeDB: subscription-erfaringer, refresh med enrichFromPg, whitespace-fiks
- Dokumentert at subscriptions ikke støtter JOINs (feiler stille)
- refresh() kaller enrichFromPg() for å hente fersk metadata fra PG
- Whitespace-normalisering i autogenererte module_bindings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 09:18:45 +01:00

132 lines
5.4 KiB
Markdown

# 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)
```typescript
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. BigInt-tidsstempler
SpacetimeDB `Timestamp`-type bruker `microsSinceEpoch` som er `BigInt`. JavaScript kan ikke blande BigInt med Number:
```typescript
// FEIL — TypeError: Cannot mix BigInt and other types
const ms = micros / 1000;
// RIKTIG — sjekk type, bruk BigInt-divisjon
const ms = typeof micros === 'bigint'
? Number(micros / 1000n)
: Number(micros) / 1000;
```
**Referanse:** `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:
```rust
// 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
```bash
# 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`, `message_reactions/insert`, `message_reactions/delete`
## 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:
```typescript
// 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`.