MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 8s
196 lines
12 KiB
Markdown
196 lines
12 KiB
Markdown
# 📡 WebSocket & Echtzeit-Synchronisation
|
|
|
|
MindSift implementiert eine bidirektionale Echtzeit-Synchronisationsschicht auf Basis von WebSockets ([`internal/ws/hub.go`](../../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:
|
|
|
|
```json
|
|
{
|
|
"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`](../../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. |
|
|
|
|
```mermaid
|
|
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:
|
|
```go
|
|
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](../flaws.md)):
|
|
|
|
```
|
|
┌───────────────────────────────┐
|
|
│ 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](../reverse-proxy.md)**
|
|
|