Files
2026-10-26 22:41:50 +01:00

12 KiB

📡 WebSocket & Echtzeit-Synchronisation

MindSift implementiert eine bidirektionale Echtzeit-Synchronisationsschicht auf Basis von WebSockets (internal/ws/hub.go). Sie sorgt dafür, dass Änderungen an Aufgaben, Triage-Karten, Notizen und geteilten Einkaufslisten ohne manuelles Neuladen sofort auf allen geöffneten Geräten und bei allen Kollaborationspartnern sichtbar werden.


🔌 1. Verbindung & Handshake

Verbindungs-Endpunkte

wss://<domain>/api/v1/ws?token=<jwt-token>
wss://<domain>/api/v1/ws/lists/<listId>?token=<jwt-token>

Authentifizierung & Upgrade

  • Die Authentifizierung erfolgt über das JWT-Token im Query-Parameter ?token=... oder den Authorization: Bearer-Header während des HTTP-Upgrade-Requests.
  • Der Server extrahiert die userID und registriert die Verbindung im internen Hub.
  • Der Upgrader akzeptiert alle Origins (CheckOrigin: return true), um volle Kompatibilität mit Reverse-Proxies (Traefik, Nginx, Caddy, Cloudflare) und Docker-Netzwerken zu gewährleisten.

🏛️ 2. Hub-Architektur & Rooms

Der WebSocket-Hub unterteilt Verbindungen in drei Abstraktionsebenen:

┌─────────────────────────────────────────────────────────────────┐
│                           WebSocket Hub                         │
├─────────────────┬─────────────────────────────┬─────────────────┤
│   allClients    │          userRooms          │      rooms      │
│  (Alle aktiven  │     (Pro Benutzer-ID)       │   (Pro Liste/   │
│   Verbindungen) │  Cross-Device Sync für      │     Space)      │
│                 │  Tasks, Triage & Workspace  │  Geteilte Liste │
└────────┬────────┴──────────────┬──────────────┴────────┬────────┘
         ▼                       ▼                       ▼
   Presence Stats          TASK_CREATED             ITEM_TOGGLED
   DOC_LOCKED              TASK_UPDATED             ITEM_CREATED
   DOC_UNLOCKED            DOC_UPDATED              LIST_UPDATED
  1. allClients (Global): Hält jede aktive Client-Verbindung. Ermöglicht Broadcasts systemweiter Ereignisse (z.B. globale Dokument-Sperrungen).
  2. userRooms[userID] (Benutzer-Räume): Sendet Events an alle Geräte desselben Benutzers (z.B. Desktop und Smartphone). Erzeugt ein Webhook oder Quick Dump auf dem Smartphone einen Eintrag, taucht dieser zeitgleich auf dem Desktop auf.
  3. rooms[listID] (Kollaborations-Räume): Multiplexing für geteilte Listen. Alle Mitglieder einer Liste empfangen Aktionen (wie Abhaken, Löschen, Umsortieren) in Echtzeit.

📨 3. Nachrichten-Format (Envelope)

Alle WebSocket-Nachrichten werden als JSON mit einheitlicher Hülle übertragen:

{
  "event": "ITEM_TOGGLED",
  "listId": "lst_12345",
  "payload": {
    "itemId": "itm_987",
    "isCompleted": true,
    "completedBy": "usr_sandro",
    "completedByName": "Sandro"
  },
  "timestamp": "2026-10-26T13:30:00Z"
}


⚡ 4. Granulare Workspace-Echtzeitsynchronisation (/api/v1/ws/docs/{id})

MindSift nutzt für die Outliner-Synchronisation eine schlanke, hochgradig reaktive In-Memory-Raumarchitektur (DocRoom in internal/ws/docroom.go). Clients verbinden sich mit ihrer eindeutigen clientId:

wss://<domain>/api/v1/ws/docs/<docId>?token=<jwt>&clientId=<cli_...>&userName=<Name>&userColor=<Hex>

4.1 JSON-Echtzeit-Eventprotokoll

Die Kommunikation erfolgt über leichtgewichtige JSON-Nachrichten ohne binären CRDT-Overhead:

Event-Typ Richtung Zweck / Payload
doc_state Server ➔ Client Wird direkt beim Verbinden gesendet: Liefert den aktuellen In-Memory-Zustand (title, blocks[], activeCursors[]).
block_updated Bidirektional Feingranulare Inhalts- oder Statusänderung eines Blocks (blockId, content, level, type, checked, collapsed).
block_created Bidirektional Erzeugt einen neuen Block (block, afterBlockId, newIndex).
block_deleted Bidirektional Löscht einen Block anhand seiner blockId.
block_moved Bidirektional Verschiebt einen Block / Unterbaum (blockId, newIndex, level).
title_updated Bidirektional Aktualisiert den Dokumententitel (title).
cursor_moved Bidirektional Ephemere Live-Präsenz & Cursor: Sendet Benutzer-Avatar, Cursorposition und Textselektion (clientId, userId, userName, userColor, blockId, selectionStart, selectionEnd). Wird nicht in SQLite gespeichert!
client_left Server ➔ Client Wird automatisch ausgelöst, wenn ein Peer die Verbindung trennt, damit Remote-Cursor sofort verschwinden.
sequenceDiagram
    autonumber
    actor UserA as Admin (Fenster A, cli_1)
    participant Room as DocRoom (Go RAM)
    actor UserB as User (Fenster B, cli_2)
    participant DB as SQLite (blocks, block_tags, documents)

    UserA->>Room: Connect (clientId="cli_1", userName="Admin")
    Room-->>UserA: doc_state (title, blocks, activeCursors)
    UserB->>Room: Connect (clientId="cli_2", userName="User")
    Room-->>UserB: doc_state (title, blocks, activeCursors)

    UserA->>Room: block_updated (clientId="cli_1", blockId="b1", content="Neu")
    Note over Room: Mutiert RAM-Slice & resetet 1.5s Timer
    Room-->>UserB: block_updated (clientId="cli_1", blockId="b1", content="Neu")
    Note over UserA: Echo Filter: cli_1 erhält kein Selbstecho!

    UserB->>Room: cursor_moved (clientId="cli_2", blockId="b1", selStart=3)
    Room-->>UserA: cursor_moved (clientId="cli_2", blockId="b1")
    Note over UserA: Farbiger Cursor-Avatar erscheint an b1

    Note over Room: 1.5s Inaktivität ohne neue Tastenanschläge
    Room->>DB: Debounce-Flush (blocks, Regex #tags -> block_tags, markdown_cache)

4.2 Strikter Echo-Schutz (clientId)

  • Jeder Client generiert beim Start einen flüchtigen Schlüssel (clientIdRef, z. B. cli_a8f9_l0z3).
  • Der Go-Hub filtert beim Broadcasten im Raum alle Clients heraus, deren Verbindung oder clientId mit dem Absender übereinstimmt:
    if evt.ClientID != "" && client.clientID == evt.ClientID {
        continue
    }
    
  • Vorteil: Verhindert Cursor-Resets, Fokus-Verluste und Race Conditions durch Selbstechos während des lokalen Tippens.

4.3 1.5-Sekunden Debounce-Flush in SQLite

  • Das Tippen im Editor erfolgt latenzfrei direkt im Arbeitsspeicher des DocRoom.
  • Sobald 1,5 Sekunden lang keine neuen Tastenanschläge oder Mutationen eintreffen (oder wenn der letzte Client den Raum verlässt), stößt der Raum einen synchronisierten Flush an:
    1. Persistiert alle Blöcke in blocks.
    2. Extrahiert alle Hashtags (#tag) per Regex und aktualisiert block_tags.
    3. Kompiliert die Blockliste (anhand der level-Tiefe) in Standard-Markdown und speichert sie in documents.markdown_cache.

🔒 5. Berechtigungen & Bearbeitungsrechte

  • Kollaboration ohne blockierende Locks: Alle Nutzer mit Schreibrechten (permission === 'owner' oder 'write') können zeitgleich im selben Dokument tippen. Pessimistische Locks oder Lease-Heartbeats wurden vollständig entfernt.
  • Schreibschutz: Verfügt ein Nutzer nur über Leserechte (permission === 'read'), ist das Eingabefeld gesperrt (readOnly). Er kann das Dokument jedoch in Echtzeit mitlesen und sieht die Cursors der Bearbeiter.

🔄 6. Fallback-Strategie & Proxy-Resilienz (Dual-Transport Architektur)

In restriktiven Firmennetzwerken, hinter Unternehmens-Proxies (z.B. Zscaler, BlueCoat) oder fehlerhaft konfigurierten Reverse-Proxies wird der Upgrade: websocket-Header häufig verworfen (NS_ERROR_CONNECTION_REFUSED, Code 1006 / 1002 / 1008).

MindSift löst dieses Problem durch eine vollwertige Dual-Transport-Architektur (FLAW-03):

                                  ┌───────────────────────────────┐
                                  │      MindSift Frontend        │
                                  └───────────────┬───────────────┘
                                                  │
                            ┌─────────────────────┴─────────────────────┐
                            ▼                                           ▼
                 Standard: WebSockets                        Fallback: SSE + REST
            (wss://.../api/v1/ws)                       (https://.../api/v1/events)
                            │                                           │
                            │  [Proxy Upgrade Block (Code 1006/Timeout)]│
                            │ ────────────────────────────────────────> │
                            ▼                                           ▼
               ┌────────────────────────┐                  ┌────────────────────────┐
               │    Gorilla WS Conn     │                  │  Server-Sent Events    │
               │   Bidirektionaler      │                  │  (text/event-stream)   │
               │    TCP-WebSocket       │                  │  Standard HTTPS Port   │
               └────────────┬───────────┘                  └────────────┬───────────┘
                            │                                           │
                            └─────────────────────┬─────────────────────┘
                                                  ▼
                                   ┌─────────────────────────────┐
                                   │       Realtime Hub          │
                                   │   (Multiplexing WS & SSE)   │
                                   └─────────────────────────────┘

1. Automatischer SSE-Fallback (GET /api/v1/events)

  • Scheitert der WebSocket-Handshake oder blockiert ein Proxy das Upgrade, wechselt MindSift innerhalb von 3,5 Sekunden automatisch auf einen Server-Sent Events (SSE) Stream.
  • Da SSE ein standardmäßiger HTTP/1.1- bzw. HTTP/2-GET-Request über Standard-Port 443/80 ist (Content-Type: text/event-stream), wird er von jedem Proxy und jeder Firewall ungehindert durchgeleitet.
  • Header X-Accel-Buffering: no und Cache-Control: no-cache verhindern Pufferung im Proxy; periodische Ping-Kommentare (: ping\n\n) alle 15 Sekunden halten Timeouts fern.
  • Der Client bleibt dadurch vollständig echtzeitfähig (Live (SSE)).

2. REST-Endpunkte für Upstream-Aktionen im SSE-Fallback-Modus

Da SSE unidirektional (Server ➔ Client) ist, stellt MindSift für Schreibvorgänge im SSE-Modus REST-Äquivalente bereit:

  • PATCH /api/v1/documents/:id/blocks: Akzeptiert Block-Mutationen (upserted, deletedIds, blockOrder) und synchronisiert den In-Memory-Raum sowie SQLite.
  • PUT /api/v1/documents/:id: Aktualisiert Dokumenttitel und Metadaten.
  • POST /api/v1/events/rooms: Erlaubt SSE-Clients, Räumen dynamisch beizutreten (join) oder sie zu verlassen (leave).

3. Verbindungs-Indikator

  • Grün pulsierend: Live verbunden über WebSockets (Live (WebSocket)).
  • Türkis pulsierend: Live verbunden über Server-Sent Events (Live (Server-Sent Events / Proxy-Stream)).
  • Gelb/Amber: Temporäre Unterbrechung mit automatischem HTTP-Polling-Schutz.

Ausführliche Konfigurationsbeispiele für Nginx, Traefik, Caddy, Cloudflare und Apache finden sich im separaten Handbuch: 👉 Reverse-Proxy-Konfigurationshandbuch