Document Stage 3 offline sync and idempotency architecture
This commit is contained in:
@@ -115,11 +115,17 @@ Compare-Node hat zusätzlich Handle-spezifische Farben (`left` → Blau, `right`
|
|||||||
|
|
||||||
**Optimistic Prefix:** Temporäre Nodes/Edges erhalten IDs mit `optimistic_` / `optimistic_edge_`-Prefix. Sie werden durch echte Convex-IDs ersetzt sobald die Mutation abgeschlossen ist.
|
**Optimistic Prefix:** Temporäre Nodes/Edges erhalten IDs mit `optimistic_` / `optimistic_edge_`-Prefix. Sie werden durch echte Convex-IDs ersetzt sobald die Mutation abgeschlossen ist.
|
||||||
|
|
||||||
**localStorage-Cache** (`lib/canvas-local-persistence.ts`): Snapshot + Ops-Queue pro Canvas-ID.
|
**Persistenz-Schichten:**
|
||||||
|
|
||||||
|
- `lib/canvas-op-queue.ts` (IndexedDB, mit localStorage-Fallback): robuste Sync-Queue inkl. Retry/TTL.
|
||||||
|
- `lib/canvas-local-persistence.ts` (localStorage): Snapshot + leichtgewichtiger Op-Mirror für sofortige UI-Pins/Recovery.
|
||||||
|
|
||||||
- Key-Schema: `lemonspace.canvas:snapshot:v1:<canvasId>` und `lemonspace.canvas:ops:v1:<canvasId>`
|
- Key-Schema: `lemonspace.canvas:snapshot:v1:<canvasId>` und `lemonspace.canvas:ops:v1:<canvasId>`
|
||||||
- Snapshot = letzter bekannter State (Nodes + Edges) für schnellen initialen Render
|
- Snapshot = letzter bekannter State (Nodes + Edges) für schnellen initialen Render
|
||||||
- Ops-Queue = ausstehende Mutations (Recovery bei Verbindungsabbruch, Konzept — noch nicht vollständig implementiert)
|
- Ops-Queue ist aktiv für: `createNode*`, `createEdge`, `moveNode`, `resizeNode`, `updateData`, `removeEdge`, `batchRemoveNodes`.
|
||||||
|
- Reconnect synchronisiert als `createEdge + removeEdge` (statt rein lokalem UI-Umbiegen).
|
||||||
|
- ID-Handover `optimistic_* → realId` remappt Folge-Operationen in Queue + localStorage-Mirror, damit Verbindungen während/ nach Replay stabil bleiben.
|
||||||
|
- Unsupported offline (weiterhin online-only): `createWithEdgeSplit`, Datei-Upload/Storage-Mutations, AI-Generierung.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Convex ist das vollständige Backend von LemonSpace: Datenbank, Realtime-Subscri
|
|||||||
| `canvases.ts` | CRUD für Canvases |
|
| `canvases.ts` | CRUD für Canvases |
|
||||||
| `openrouter.ts` | OpenRouter-HTTP-Client + Modell-Config (Backend) |
|
| `openrouter.ts` | OpenRouter-HTTP-Client + Modell-Config (Backend) |
|
||||||
| `auth.ts` | Better Auth Integration |
|
| `auth.ts` | Better Auth Integration |
|
||||||
| `helpers.ts` | `requireAuth()` — von allen Queries/Mutations genutzt |
|
| `helpers.ts` | `requireAuth()` + `optionalAuth()` für Auth-Checks |
|
||||||
| `polar.ts` | Polar.sh Webhook-Handler (Subscriptions) |
|
| `polar.ts` | Polar.sh Webhook-Handler (Subscriptions) |
|
||||||
| `pexels.ts` | Pexels Stock-Bilder API |
|
| `pexels.ts` | Pexels Stock-Bilder API |
|
||||||
| `freepik.ts` | Freepik Asset-Browser API |
|
| `freepik.ts` | Freepik Asset-Browser API |
|
||||||
@@ -40,6 +40,8 @@ Alle Node-Typen sind in zwei Validators definiert: `phase1NodeTypes` (aktiv) und
|
|||||||
|
|
||||||
**`edges`** — Verbindungen zwischen Nodes. Index: `by_canvas`, `by_source`, `by_target`.
|
**`edges`** — Verbindungen zwischen Nodes. Index: `by_canvas`, `by_source`, `by_target`.
|
||||||
|
|
||||||
|
**`mutationRequests`** — Idempotenz-Layer für Client-Replay (`clientRequestId`) bei offline/Retry-Sync. Felder: `userId`, `mutation`, `clientRequestId`, optionale Ziel-IDs (`canvasId`, `nodeId`, `edgeId`). Index: `by_user_mutation_request`.
|
||||||
|
|
||||||
**`creditBalances`** — Pro User: `balance`, `reserved`, `monthlyAllocation`. `available = balance - reserved` (computed, nicht gespeichert).
|
**`creditBalances`** — Pro User: `balance`, `reserved`, `monthlyAllocation`. `available = balance - reserved` (computed, nicht gespeichert).
|
||||||
|
|
||||||
**`creditTransactions`** — Jede Credit-Bewegung. Types: `subscription | topup | usage | reservation | refund`. Status: `committed | reserved | released | failed`.
|
**`creditTransactions`** — Jede Credit-Bewegung. Types: `subscription | topup | usage | reservation | refund`. Status: `committed | reserved | released | failed`.
|
||||||
@@ -109,10 +111,22 @@ processImageGeneration (internalAction)
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
requireAuth(ctx) // → { userId: string }
|
requireAuth(ctx) // → { userId: string }
|
||||||
|
optionalAuth(ctx) // → { userId: string } | null
|
||||||
```
|
```
|
||||||
|
|
||||||
Wirft bei unauthentifiziertem Zugriff. Wird von allen Queries und Mutations genutzt, die User-Daten berühren.
|
Wirft bei unauthentifiziertem Zugriff. Wird von allen Queries und Mutations genutzt, die User-Daten berühren.
|
||||||
|
|
||||||
|
### Auth-Race-Härtung
|
||||||
|
|
||||||
|
- `canvases.get` nutzt optionalen Auth-Check und gibt bei fehlender Session `null` zurück (statt Throw), damit SSR/Client-Hydration bei kurzem Token-Race nicht in `404` kippt.
|
||||||
|
- `credits.getBalance` gibt bei fehlender Session einen Default-Stand (`0`-Werte) zurück (statt Throw), damit UI-Widgets nicht mit `Unauthenticated` fehlschlagen.
|
||||||
|
|
||||||
|
### Idempotente Canvas-Mutations
|
||||||
|
|
||||||
|
- `nodes.create`, `nodes.createWithEdgeFromSource`, `nodes.createWithEdgeToTarget` sind über `clientRequestId` idempotent.
|
||||||
|
- `edges.create` ist über `clientRequestId` idempotent.
|
||||||
|
- `nodes.batchRemove` ist idempotent tolerant: wenn alle angefragten Nodes bereits entfernt sind, wird die Mutation als No-op beendet.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Konventionen
|
## Konventionen
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Geteilte Hilfsfunktionen, Typ-Definitionen und Konfiguration. Keine React-Kompon
|
|||||||
| `auth-server.ts` | Server-Helper: `getAuthUser()`, `getToken()` |
|
| `auth-server.ts` | Server-Helper: `getAuthUser()`, `getToken()` |
|
||||||
| `auth-client.ts` | Client-Helper: `authClient` |
|
| `auth-client.ts` | Client-Helper: `authClient` |
|
||||||
| `canvas-local-persistence.ts` | localStorage-Cache für Canvas-Snapshots und Op-Queue |
|
| `canvas-local-persistence.ts` | localStorage-Cache für Canvas-Snapshots und Op-Queue |
|
||||||
|
| `canvas-op-queue.ts` | IndexedDB-basierte Canvas-Sync-Queue (Retry, TTL, Remap/Pruning) |
|
||||||
| `toast.ts` | Toast-Utility-Wrapper |
|
| `toast.ts` | Toast-Utility-Wrapper |
|
||||||
| `toast-messages.ts` | Typisierte Toast-Message-Definitionen (`msg`, `CanvasNodeDeleteBlockReason`) |
|
| `toast-messages.ts` | Typisierte Toast-Message-Definitionen (`msg`, `CanvasNodeDeleteBlockReason`) |
|
||||||
| `ai-errors.ts` | Error-Kategorisierung und User-facing Fehlermeldungen |
|
| `ai-errors.ts` | Error-Kategorisierung und User-facing Fehlermeldungen |
|
||||||
@@ -81,10 +82,32 @@ writeCanvasSnapshot(canvasId, {nodes, edges}) // Snapshot speichern
|
|||||||
enqueueCanvasOp(canvasId, op) // Op in Queue schreiben
|
enqueueCanvasOp(canvasId, op) // Op in Queue schreiben
|
||||||
resolveCanvasOp(canvasId, opId) // Op aus Queue entfernen
|
resolveCanvasOp(canvasId, opId) // Op aus Queue entfernen
|
||||||
readCanvasOps(canvasId) // Ausstehende Ops lesen
|
readCanvasOps(canvasId) // Ausstehende Ops lesen
|
||||||
|
remapCanvasOpNodeId(canvasId, fromId, toId) // optimistic→real remap
|
||||||
|
dropCanvasOpsByNodeIds(canvasId, ids) // konfliktbedingtes Pruning
|
||||||
|
dropCanvasOpsByClientRequestIds(canvasId, ids) // Create-Cancel
|
||||||
|
dropCanvasOpsByEdgeIds(canvasId, ids) // Remove-Cancel
|
||||||
```
|
```
|
||||||
|
|
||||||
Key-Schema: `lemonspace.canvas:snapshot:v1:<id>` / `lemonspace.canvas:ops:v1:<id>`. Bei Version-Bumps (`SNAPSHOT_VERSION`, `OPS_VERSION`) werden alte Keys automatisch ignoriert.
|
Key-Schema: `lemonspace.canvas:snapshot:v1:<id>` / `lemonspace.canvas:ops:v1:<id>`. Bei Version-Bumps (`SNAPSHOT_VERSION`, `OPS_VERSION`) werden alte Keys automatisch ignoriert.
|
||||||
|
|
||||||
|
## `canvas-op-queue.ts` — Sync-Queue
|
||||||
|
|
||||||
|
Zentrale, persistente Queue für Canvas-Mutations mit IndexedDB (Fallback: localStorage), Retry-Backoff und 24h-TTL.
|
||||||
|
|
||||||
|
Wichtige APIs:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enqueueCanvasSyncOp(...)
|
||||||
|
listCanvasSyncOps(canvasId)
|
||||||
|
ackCanvasSyncOp(opId)
|
||||||
|
markCanvasSyncOpFailed(opId, { nextRetryAt, lastError })
|
||||||
|
dropExpiredCanvasSyncOps(canvasId, now)
|
||||||
|
remapCanvasSyncNodeId(canvasId, fromId, toId)
|
||||||
|
dropCanvasSyncOpsByNodeIds(canvasId, ids)
|
||||||
|
dropCanvasSyncOpsByClientRequestIds(canvasId, ids)
|
||||||
|
dropCanvasSyncOpsByEdgeIds(canvasId, ids)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Auth-Helpers
|
## Auth-Helpers
|
||||||
|
|||||||
Reference in New Issue
Block a user