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>
183 lines
7.9 KiB
Markdown
183 lines
7.9 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. 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:
|
|
|
|
```typescript
|
|
// 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`:
|
|
|
|
```rust
|
|
// 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:
|
|
|
|
```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`, `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:
|
|
```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`.
|
|
|
|
## 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`:
|
|
|
|
```rust
|
|
// 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:
|
|
|
|
```bash
|
|
# 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
|