Offline Support: Live vs. Savable Dispatcher
Konzept
Gleiche Idee wie Server-seitig (InMemoryDispatcher vs. RedisDispatcher) — auf Client-Seite gibt es zwei Dispatcher-Modi. Der Feature-Code aendert sich nie. Kein r.offline(), kein r.requires("offline"). Der Dispatcher wird pro App/Workspace konfiguriert.
Server: InMemoryDispatcher | RedisDispatcher ← gleiche APIClient: LiveDispatcher | SavableDispatcher ← gleiche APIKonfiguration
// Cockpit (immer online):createApp({ clientDispatcher: "live",});
// Fahrer-App (offline-faehig):createApp({ clientDispatcher: "savable",});Verhalten
Live Dispatcher (Cockpit)
write() → HTTP → Server → Response → UI Updatequery() → HTTP → Server → Response → UI UpdateDirekt. Kein lokaler Store noetig (ausser fuer UI State).
Savable Dispatcher (Fahrer-App)
write() → lokaler Store + Event Queue → Optimistic UI Update ↓ (bei Netz) Queue abarbeiten → HTTP → Server → Bestaetigung
query() → lokaler Store (gefuellt durch Initial Sync + SSE)Alles geht gegen den lokalen Store. Der Server wird asynchron synchronisiert.
Der Entwickler merkt keinen Unterschied
// Dieser Code funktioniert mit beiden Dispatchern identisch:const { write } = useCommand();const result = await write("order.accept", { orderId: 123 });
const { data } = useQuery("order.list", { limit: 50 });- Live:
writegeht direkt zum Server,queryholt vom Server - Savable:
writespeichert lokal + queued,queryliest lokal
Initial Sync (nur Savable)
Beim App-Start laedt der Savable Dispatcher alle relevanten Daten:
// Automatisch beim Start / Login:const data = await query("sync.initialData", {});// → Alle Daten die der User braucht// → In lokalen Store schreiben// → Ab jetzt offline-faehigDaten-Trennung im Store (nur Savable)
Lokaler Store: serverData/ ← Vom Server (read-only, wird bei Sync ersetzt) orders: [...] clients: [...]
localChanges/ ← Eigene Aenderungen (pending Events) order_123: [event1, event2] order_456: [event3]
fileQueue/ ← Bilder zum Upload [file1, file2]Server-Daten koennen jederzeit komplett ersetzt werden ohne lokale Aenderungen zu verlieren.
Event Queue
Lokale Aenderungen werden als Events gespeichert und an den Server gesendet:
Sync-Regeln:
- FIFO pro Entity (Order 123: Event 1 vor Event 2)
- Parallel ueber Entities (Order 123 und Order 456 gleichzeitig)
- Event failed → nur diese Entity pausieren, andere laufen weiter
- Retry mit Backoff pro Entity
Order 123: Event 1 ✅ → Event 2 ✅ → fertigOrder 456: Event 3 ❌ → retry... → Event 3 ✅ → fertig ↑ blockiert nur Order 456, nicht 123File Queue (separat)
Bilder werden unabhaengig von Events hochgeladen:
Reihenfolge Event vs. File ist egal:
- Event sagt “Bild existiert” (ohne Binaerdaten) → DB Insert ohne Size/Binary
- File Upload liefert Binaerdaten nach → DB Update mit Size/Binary
- Oder umgekehrt — beide Wege beruecksichtigen den anderen
Incremental Sync
Bei SSE-Reconnect:
1. SSE Event kommt rein (z.B. "orders changed")2. App macht Full Refresh: GET sync.initialData3. serverData/ wird komplett ersetzt4. localChanges/ bleibt (noch nicht gesendete Events)5. UI merged: serverData + localChanges = angezeigte DatenKein Delta-Merge. Server-Daten ersetzen, lokale Changes bleiben bis gesendet.
UI Indicator
Online: [keine Anzeige]Offline: ⚡ Offline (3 ausstehend)Syncing: ↻ Synchronisiere...Error (einzelne Entity): ⚠ Order 123: Sync fehlgeschlagen [Erneut versuchen] [Verwerfen]Zusammenspiel
| Framework-Feature | Live Dispatcher | Savable Dispatcher |
|---|---|---|
| useCommand() | HTTP direkt | Lokal + Queue |
| useQuery() | HTTP direkt | Lokaler Store |
| SSE | Live Updates | Reconnect → Full Refresh |
| Files | Upload direkt | Separater File Queue |
| Optimistic Locking | Server prueft sofort | Version lokal, Server prueft bei Sync |
| Undo Toast | Sofort reversibel | Lokal reversibel, bei Sync evtl. Conflict |
| Delivery (Toaster) | Server-Toast | Lokaler Toast + Server-Toast bei Sync |