Zum Inhalt springen

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 API
Client: LiveDispatcher | SavableDispatcher ← gleiche API

Konfiguration

// Cockpit (immer online):
createApp({
clientDispatcher: "live",
});
// Fahrer-App (offline-faehig):
createApp({
clientDispatcher: "savable",
});

Verhalten

Live Dispatcher (Cockpit)

write() → HTTP → Server → Response → UI Update
query() → HTTP → Server → Response → UI Update

Direkt. 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: write geht direkt zum Server, query holt vom Server
  • Savable: write speichert lokal + queued, query liest 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-faehig

Daten-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 ✅ → fertig
Order 456: Event 3 ❌ → retry... → Event 3 ✅ → fertig
↑ blockiert nur Order 456, nicht 123

File 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.initialData
3. serverData/ wird komplett ersetzt
4. localChanges/ bleibt (noch nicht gesendete Events)
5. UI merged: serverData + localChanges = angezeigte Daten

Kein 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-FeatureLive DispatcherSavable Dispatcher
useCommand()HTTP direktLokal + Queue
useQuery()HTTP direktLokaler Store
SSELive UpdatesReconnect → Full Refresh
FilesUpload direktSeparater File Queue
Optimistic LockingServer prueft sofortVersion lokal, Server prueft bei Sync
Undo ToastSofort reversibelLokal reversibel, bei Sync evtl. Conflict
Delivery (Toaster)Server-ToastLokaler Toast + Server-Toast bei Sync