feat(workspace): implement Yjs CRDT real-time collaborative editing (resolves FLAW-01)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 29s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 29s
This commit is contained in:
1 parent
0bdeb5f320
commit
ef8cf8f022
13 files changed
+1028
-85
No files matched your search
+35
-25
@@ -64,43 +64,53 @@ Alle WebSocket-Nachrichten werden als JSON mit einheitlicher Hülle übertragen:
|
||||
|
||||
---
|
||||
|
||||
## 🔒 4. Dokument-Sperrsystem (Document Locking)
|
||||
---
|
||||
|
||||
Um Schreibkonflikte bei Notizen im Workspace zu vermeiden, verwendet MindSift ein **serverseitiges Lease-Locking mit Heartbeat**:
|
||||
## ⚡ 4. CRDT-Echtzeitprotokoll (Yjs Collaborative Editing)
|
||||
|
||||
Seit Version 2.1 unterstützt MindSift **echte, konfliktfreie Mehrbenutzer-Kollaboration** für Notizen via Yjs CRDT. Clients tauschen binäre Delta-Updates und Live-Awareness-Informationen über dedizierte WebSocket-Nachrichten aus:
|
||||
|
||||
### CRDT-Nachrichtentypen
|
||||
| Event | Richtung | Zweck / Payload |
|
||||
|---|---|---|
|
||||
| `CRDT_JOIN` | Client ➔ Server | Tritt dem Dokumentenraum (`doc:{docId}`) bei. |
|
||||
| `CRDT_INIT` | Server ➔ Client | Sendet den aktuellen Basiszustand (`stateBlob`) und offene Delta-Updates (`updates[]`) an den Client. |
|
||||
| `CRDT_UPDATE` | Bidirektional | Überträgt inkrementelle binäre Yjs-Updates (Base64-kodiert). Server speichert in `document_crdt_updates` und verteilt an alle anderen Raumnutzer. |
|
||||
| `CRDT_COMPACT` | Client ➔ Server | Komprimiert mehrere Updates zu einem konsolidierten Snapshot in `document_crdt_state`. |
|
||||
| `CRDT_AWARENESS` | Bidirektional | Überträgt Live-Präsenz (Fokus auf `blockId`, Benutzername, Farbe). |
|
||||
| `CRDT_AWARENESS_LEAVE` | Bidirektional | Signalisiert, dass ein Nutzer den Fokus verloren hat oder das Dokument verlassen hat. |
|
||||
| `CRDT_LEAVE` | Client ➔ Server | Verlässt den Dokumentenraum explizit. |
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor UserA as Benutzer A
|
||||
participant WS as WebSocket Hub
|
||||
participant WS as WebSocket Hub & SQLite
|
||||
actor UserB as Benutzer B
|
||||
|
||||
UserA->>WS: LOCK_DOC (docId: "doc_123")
|
||||
WS-->>UserA: Lock bestätigt (30s TTL)
|
||||
WS->>UserB: DOC_LOCKED (docId: "doc_123", lockedBy: "Benutzer A")
|
||||
Note over UserB: Editor gesperrt (Schreibschutz aktiv)
|
||||
UserA->>WS: CRDT_JOIN (docId: "doc_123")
|
||||
WS-->>UserA: CRDT_INIT (stateBlob, updates)
|
||||
UserB->>WS: CRDT_JOIN (docId: "doc_123")
|
||||
WS-->>UserB: CRDT_INIT (stateBlob, updates)
|
||||
|
||||
loop Alle 15 Sekunden
|
||||
UserA->>WS: HEARTBEAT_DOC (docId: "doc_123")
|
||||
WS-->>UserA: TTL um 30s verlängert
|
||||
end
|
||||
UserA->>WS: CRDT_UPDATE (docId, updateBlob)
|
||||
WS->>WS: Append in document_crdt_updates
|
||||
WS->>UserB: CRDT_UPDATE (docId, updateBlob)
|
||||
Note over UserB: Yjs wendet Delta atomar an (0 Konflikte)
|
||||
|
||||
alt Reguläres Schließen
|
||||
UserA->>WS: UNLOCK_DOC (docId: "doc_123")
|
||||
WS->>UserB: DOC_UNLOCKED (docId: "doc_123")
|
||||
Note over UserB: Editor wieder freigegeben
|
||||
else Verbindungsabbruch / Tab geschlossen
|
||||
Note over WS: Heartbeat bleibt aus (TTL läuft ab)
|
||||
WS->>UserB: DOC_UNLOCKED (Automatisch nach Ablauf)
|
||||
end
|
||||
UserA->>WS: CRDT_AWARENESS (docId, focusedBlockId)
|
||||
WS->>UserB: CRDT_AWARENESS (docId, user, focusedBlockId)
|
||||
Note over UserB: Avatar-Badge erscheint am Block
|
||||
```
|
||||
|
||||
### Locking-Events
|
||||
- **`LOCK_DOC` (Client ➔ Server):** Fordert eine 30-Sekunden-Sperre für ein Dokument an.
|
||||
- **`HEARTBEAT_DOC` (Client ➔ Server):** Verlängert die Sperre alle 15 Sekunden, solange der Benutzer das Dokument geöffnet hat.
|
||||
- **`UNLOCK_DOC` (Client ➔ Server):** Gibt die Sperre beim Verlassen des Dokuments frei.
|
||||
- **`DOC_LOCKED` (Server ➔ Clients):** Informiert andere Benutzer, dass das Dokument schreibgeschützt ist.
|
||||
- **`DOC_UNLOCKED` (Server ➔ Clients):** Hebt den Schreibschutz auf.
|
||||
---
|
||||
|
||||
## 🔒 5. Abwärtskompatibles Document-Lease-Locking
|
||||
|
||||
Vor Version 2.1 nutzte MindSift ein serverseitiges Lease-Locking mit 30-Sekunden-Heartbeats. Durch die CRDT-Engine wurde die blockierende Bearbeitungssperre (`isLockedByOther`) abgelöst:
|
||||
- **Aktuelles Verhalten:** Mehrere Benutzer mit Bearbeitungsrechten können zeitgleich dasselbe Dokument bearbeiten.
|
||||
- **Presence & Locks:** Die Events `LOCK_DOC` und `HEARTBEAT_DOC` bleiben abwärtskompatibel erhalten und dienen nun der reinen Präsenzanzeige (z.B. gelber Statusindikator „wird gerade von X angesehen").
|
||||
- **Schreibschutz:** Ein echter Schreibschutz (`isReadOnly`) greift ausschließlich noch dann, wenn der Nutzer nur über Leserechte (`permission === 'read'`) für die Notiz oder den Ordner verfügt.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+11
-8
@@ -8,7 +8,7 @@ Diese Datei dokumentiert eine ehrliche und schonungslose technische Analyse alle
|
||||
|
||||
| ID | Bereich | Problem / Schwachstelle | Schweregrad | Risiko im Produktivbetrieb |
|
||||
|---|---|---|---|---|
|
||||
| **FLAW-01** | Workspace Sync | Kein echtes CRDT/OT – Lock-Heartbeat-Modell mit Last-Write-Wins | **Kritisch** | Zeilenverlust bei gleichzeitigen Bearbeitungen oder Netzschwankungen |
|
||||
| **FLAW-01** | Workspace Sync | Kein echtes CRDT/OT – Lock-Heartbeat-Modell mit Last-Write-Wins | **Gelöst (v2.1)** | Behoben durch Yjs CRDT-Engine & blockbasierte kollaborative Bearbeitung |
|
||||
| **FLAW-02** | Storage | Dual-Storage Drift (SQLite vs. Dateisystem) | **Obsolet / Gelöst** | Kein Risiko: MindSift nutzt Pure-DB (keine `.md`-Dateien auf Platte) |
|
||||
| **FLAW-03** | Netzwerk / WS | Proxy-Inkompatibilität bei WebSocket-Upgrades & Polling-Fallback | **Mittel** | Verzögerte Synchronisierung (1–3s Polling) in Firmennetzwerken |
|
||||
| **FLAW-04** | Datenbank / Suche | Volltextsuche via `LIKE '%query%'` statt SQLite FTS5 | **Mittel** | Performance-Einbrüche bei wachsenden Notizbeständen (>10.000 Blöcke) |
|
||||
@@ -23,13 +23,16 @@ Diese Datei dokumentiert eine ehrliche und schonungslose technische Analyse alle
|
||||
|
||||
## 🔍 Detaillierte Analyse & Maßnahmen
|
||||
|
||||
### FLAW-01: Kein echtes CRDT/OT – Lock-Heartbeat & Last-Write-Wins
|
||||
- **Betroffene Komponenten:** [`api/websocket.md`](./api/websocket.md), [`modules/workspace.md`](./modules/workspace.md), `internal/ws/hub.go`, `frontend/src/features/workspace/WorkspaceView.tsx`
|
||||
- **Ursache:** MindSift nutzt zur Vermeidung von Konflikten eine kooperative Bearbeitungssperre (*Document Lease Lock*, 30 Sekunden TTL mit Heartbeat). Wenn zwei Nutzer dasselbe Dokument geöffnet haben, blockiert der aktive Schreiber das Bearbeiten für andere. Verliert ein Nutzer jedoch kurzzeitig die WebSocket-Verbindung oder tippt während des Übergangs zum Polling-Fallback, werden Änderungen per REST via Last-Write-Wins (`PUT /api/v1/documents/:id`) gespeichert.
|
||||
- **Folgen:** Wie in früheren Versionen beobachtet, können bei parallelem Schreiben Zeilen überschrieben werden oder Strukturhierarchien (Parent-IDs) verloren gehen.
|
||||
- **Empfohlene Lösung:**
|
||||
1. Langfristige Migration auf ein echtes blockweises CRDT-Framework (wie [Yjs](https://yjs.dev/) oder [Automerge](https://automerge.org/)).
|
||||
2. Kurzfristig: Optimistisches Block-Locking (Sperre nur auf Block-Ebene `block_id` statt des gesamten Dokuments) und atomare Versionsprüfung (*Optimistic Concurrency Control* mit `version_seq`).
|
||||
### FLAW-01: [GELÖST / IMPLEMENTIERT] Yjs Outliner CRDT & Multi-User Kollaboration
|
||||
- **Status:** **Gelöst via Yjs CRDT Engine (Lösung i)**
|
||||
- **Betroffene Komponenten:** [`api/websocket.md`](./api/websocket.md), [`modules/workspace.md`](./modules/workspace.md), `internal/ws/hub.go`, `internal/storage/sqlite.go`, `frontend/src/lib/crdt.ts`, `frontend/src/features/workspace/WorkspaceView.tsx`
|
||||
- **Ursache (Historisch):** MindSift nutzte ein kooperatives Dokumenten-Lease-Locking mit Last-Write-Wins. Bei parallelem Zugriff oder Netztrennung konnten Änderungen überschrieben werden.
|
||||
- **Implementierte Lösung (Yjs CRDT):**
|
||||
1. **Block-Level Yjs CRDT:** Implementierung eines vollwertigen CRDT-Modells mit `Y.Doc`. Die Blockreihenfolge wird über ein `Y.Array<string>` (Block-IDs) konfliktfrei mit dem YATA-Algorithmus aufgelöst. Inhalte und Metadaten (Text, Level, Checked, Collapsed) sind in `Y.Map` gekapselt.
|
||||
2. **Wegfall von Ganzdokument-Sperren:** Keine blockierenden Sperren mehr (`isLockedByOther` entfernt). Mehrere Nutzer können zeitgleich am selben Dokument schreiben.
|
||||
3. **Dual-Persistence:** CRDT-Zustände und Delta-Updates werden binär/Base64 in SQLite (`document_crdt_state`, `document_crdt_updates`) persistiert. Zur Aufrechterhaltung relationaler Indizes (Global To-Dos, Tag-Indizierung, Suche) werden Blöcke parallel in `document_blocks` materialisiert.
|
||||
4. **Live-Awareness & Presence:** Block-Fokus und Cursor-Zustand werden via `CRDT_AWARENESS` übertragen. Nutzer sehen ein Live-Badge im Header (`Live (N)`) sowie farbige Kollaborator-Pillen an aktiven Blöcken.
|
||||
5. **REST- & Proxy-Sicherheit:** Ergänzend zu WebSockets stehen Endpunkte `GET /api/v1/documents/:id/crdt` und `PUT /api/v1/documents/:id/crdt` bereit.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+29
-22
@@ -46,33 +46,40 @@ Der Bullet-Dot dient gleichzeitig als Drag-Anfasser. Wird ein Block verschoben,
|
||||
|
||||
---
|
||||
|
||||
## 💾 3. Pure-Database Architecture (Keine MD-Dateien auf Platte)
|
||||
---
|
||||
|
||||
MindSift speichert Notizen und Outliner-Bäume **zu 100% relational in SQLite**. Es werden bewusst **keine Markdown-Dateien auf dem Dateisystem** abgelegt:
|
||||
## ⚡ 3. Yjs CRDT Real-Time Engine & Dual-Persistence
|
||||
|
||||
Ab MindSift v2.1 setzt der Workspace auf ein **konfliktfreies Replicated Data Type (CRDT) Modell** auf Basis von [Yjs](https://yjs.dev/):
|
||||
|
||||
### Funktionsweise
|
||||
1. **CRDT-Struktur im Client:**
|
||||
- Ein `Y.Doc` pro geöffnetem Dokument.
|
||||
- `Y.Array<string>` (`blocks`): Führt die Block-IDs in korrekter Reihenfolge. Der YATA-Algorithmus garantiert, dass simultanes Einfügen, Verschieben oder Löschen von Blöcken durch mehrere Nutzer deterministisch und konfliktfrei konvergiert.
|
||||
- `Y.Map` (`block:<id>`): Speichert Inhalt (`content`), Einrückung (`level`), Erledigungsstatus (`checked`) und Faltungsstatus (`collapsed`).
|
||||
2. **Kollaborative Bearbeitung ohne Sperren:**
|
||||
- Nutzer können simultan dieselbe Notiz bearbeiten.
|
||||
- Fokus- und Cursor-Positionen werden via `CRDT_AWARENESS` übertragen und als farbige Benutzer-Pillen direkt am jeweiligen Block visualisiert.
|
||||
|
||||
### Dual-Persistence in SQLite
|
||||
```
|
||||
Client (Block-Editor)
|
||||
│ PUT /api/v1/documents/:id/blocks (oder WebSocket)
|
||||
▼
|
||||
Backend Engine
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Atomare Transaktion in SQLite: │
|
||||
│ 1. DELETE FROM document_blocks WHERE doc_id │
|
||||
│ 2. Batch INSERT der Blöcke: │
|
||||
│ -> id, doc_id, content, level, checked, │
|
||||
│ sort_order, collapsed │
|
||||
│ 3. WebSocket Broadcast an Raum (doc:id) │
|
||||
└──────────────────────────────────────────────┘
|
||||
Client (Y.Doc Mutation)
|
||||
│
|
||||
├──▶ WebSocket (CRDT_UPDATE Base64 Delta)
|
||||
│ │
|
||||
│ ▼
|
||||
│ SQLite: document_crdt_updates / document_crdt_state
|
||||
│ (Mathematisch exakte CRDT-Konvergenz)
|
||||
│
|
||||
└──▶ Periodischer Snapshot / Flush
|
||||
│
|
||||
▼
|
||||
SQLite: document_blocks
|
||||
(Relationale Indizes für Global To-Dos, Tag-Explorer & FTS)
|
||||
```
|
||||
|
||||
### Vorteile der reinen Datenbank-Architektur
|
||||
1. **Kein Dual-Storage & kein Drift:**
|
||||
- Keine Inkonsistenzen zwischen Festplattendateien und Datenbankzeilen.
|
||||
- Keine Dateisystem-Sperren, Race-Conditions oder Berechtigungskonflikte auf Betriebssystemebene.
|
||||
2. **Atomare Transaktionen (ACID):**
|
||||
- Strukturänderungen (Einrücken, Unterbaum verschieben, Blöcke löschen) sind atomar in einer Transaktion gekapselt.
|
||||
3. **Skalierbare Abfragen & Rollups:**
|
||||
- Aggregationen (wie das globale To-Do Dashboard oder Tag-Suchen) erfolgen mit hochoptimierten SQL-Abfragen über relationale Indizes, anstatt Hunderte `.md`-Dateien von der Festplatte parsen zu müssen.
|
||||
1. **Mathematische CRDT-Garantie:** Binäre Yjs-Updates werden in `document_crdt_updates` und periodisch als Snapshot in `document_crdt_state` gesichert. Auch nach einem Server-Neustart oder Offline-Phasen konvergieren alle Clients verlustfrei.
|
||||
2. **Relationale Query-Fähigkeit:** Um schnelle SQL-Abfragen für das globale To-Do Rollup Dashboard und Hashtags zu ermöglichen, werden die Blöcke parallel in `document_blocks` materialisiert. Es existieren **keine Markdown-Dateien auf der Festplatte**.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user