Files
MindSift/Doku/api/websocket.md
T
sandrodachandClaude Opus 5.5 1ea0c78b41
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 50s
feat(notes): operation log, block leases and sync API for collaborative editing
Phase 1 of the collaboration plan (server side):
- internal/collab: block tree with parent IDs and fractional order keys,
  operations with conflict rules (version check, block leases, conflict
  copies instead of lost text, restore on late edits, cycle checks),
  in-memory leases/sessions and long-poll notifier.
- Storage: per-note lock + transaction, append-only effect log (doc_ops),
  idempotent results per opId (doc_op_results), snapshot, log paging,
  central DocRole. Old flat block data is converted to the tree.
- The existing whole-document save (PUT/PATCH) runs as an adapter through
  the same engine, so the current editor keeps working and writes to the log.
- API: GET /docs/{id}/snapshot, POST /docs/{id}/sync, GET /docs/{id}/changes
  (long poll), POST /docs/{id}/leave; DOC_SEQ poke for WebSocket/SSE rooms.
- Backend accepts editor (write) shares for notes and folders; the forced
  read-only migration is gone. The share dialog still offers read only.
- Tests: conflict matrix, order keys, leases, storage adapter round trip,
  idempotency, API protocol incl. long poll, and a randomized simulation
  (4 clients, lost/duplicated requests and responses, offline phases,
  lease expiry) checking convergence, no lost text and exactly-once results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:47:56 +01:00

4.4 KiB

Echtzeit: WebSocket und SSE

Der Server verteilt Änderungen als JSON-Ereignisse an verbundene Clients. Die Ereignisse entstehen nach einer REST-Änderung; es gibt keinen Schreibkanal über die Verbindung. Implementierung: hub.go, Client: api.ts.

Keine CRDTs, keine Sperren, keine Cursor-Presence. Frühere Fassungen dieser Doku beschrieben DocRooms, clientId-Echo-Schutz und Yjs; das existiert nicht (mehr).

Transporte

Transport Endpunkt Hinweise
WebSocket GET /api/v1/ws (auch /ws/lists/{id}, /ws/spaces/{id}) Ping alle 30 s, Lesefrist 60 s, Nachrichten bis 4 MB
SSE GET /api/v1/events Kommentar-Ping alle 15 s, Header X-Accel-Buffering: no

Der Client versucht zuerst WebSocket. Gelingt der Aufbau nicht innerhalb von 2,5 s oder bricht die Verbindung unsauber ab, wechselt er auf SSE und bleibt dabei (Firmen-Proxys). Dort liefert der Server SSE_CONNECTED mit einer sseId; Räume werden über POST /api/v1/events/rooms (sseId, room, action: join/leave) abonniert.

Authentifizierung: Token als ?token=<JWT|API-Key> oder Authorization-Header. Ohne gültiges Token antworten /ws und /events mit 401. Das Token wird nur beim Verbinden geprüft (FLAW-22).

Räume: Jeder Beitritt (Raum in der URL, JOIN_ROOM, SSE-Startparameter rooms/docId, POST /events/rooms) wird geprüft: doc:<id> nur mit Lesezugriff auf die Notiz, sonst muss die ID eine lesbare geteilte Liste sein. Nicht erlaubte Räume werden ignoriert, POST /events/rooms antwortet dann mit 403. SSE-Räume kann nur der Besitzer der jeweiligen sseId ändern.

Nachrichtenformat

{ "event": "TASK_UPDATED", "listId": "", "payload": { … }, "timestamp": "2026-10-27T12:00:00Z" }

Client → Server versteht der Server nur: {"event":"JOIN_ROOM","room":"<id>"} und {"event":"LEAVE_ROOM","room":"<id>"}. Räume heißen doc:<docId> für Notizen oder <listId> für geteilte Listen.

Zustellung

Kanal Wer erhält es
Benutzer-Kanal (BroadcastToUser) alle Verbindungen eines Benutzers
Raum (BroadcastRoom, Broadcast) Verbindungen im Raum
Mehrere Benutzer (BroadcastToUsers) alle Verbindungen einer berechneten Benutzergruppe, z. B. Eigentümer und Mitglieder einer Aufgabenliste

Ereignisse

Ereignis Auslöser Kanal
TASK_CREATED POST /inbox, Webhook Eigentümer und Benutzer der Liste
TASK_UPDATED PATCH /thoughts/{id}, Triage Eigentümer und Benutzer der alten und neuen Liste
TASK_DELETED DELETE /thoughts/{id} Eigentümer und Benutzer der Liste
TASK_REORDERED Aufgaben umsortieren Benutzer aller betroffenen Listen
TASK_LIST_UPDATED / TASK_LIST_DELETED Liste ändern, (un)teilen, löschen; Payload nur {id}, Clients laden ihre Listen neu Benutzer vor und nach der Änderung
DOC_CREATED, DOC_UPDATED, DOC_DELETED Notiz anlegen/ändern/löschen, Freigaben berechtigte Benutzer
DOC_SEQ Änderung an Notiz, Leases oder Präsenz (docId, headSeq); Clients holen die Daten per /docs/{id}/sync Raum doc:<id>
DOC_BLOCKS_PATCHED PATCH /documents/{id}/blocks (docId, updatedBy, upserted, deletedIds, blockOrder) Raum doc:<id> und berechtigte Benutzer
WORKSPACE_FOLDERS_UPDATED Ordner/Mounts ändern Benutzer selbst; bei geteilten Ordnern Eigentümer und alle Beteiligten
LIST_CREATED, LIST_UPDATED, LIST_DELETED, LIST_SHARED, LIST_UNSHARED, LIST_CLEARED Listenverwaltung Benutzer bzw. Raum der Liste
ITEM_CREATED, ITEM_UPDATED, ITEM_TOGGLED, ITEM_DELETED, ITEM_REORDERED Listeneinträge Raum der Liste
USER_PRESENCE Verbindung auf-/abgebaut (onlineCount) Raum
SSE_CONNECTED Nur SSE, direkt nach Verbindung Verbindung

Verhalten im Frontend

  • Die globale Verbindung (ein Singleton) leitet jede Nachricht als Browser-Ereignis mindsift:ws weiter; Ansichten (TasksView, WorkspaceView, SharedListsView) hören darauf und laden bzw. patchen ihren Zustand.
  • Nach einem Abbruch verbindet sich der Client mit Verzögerung neu.
  • Weil Notizen nur vom Eigentümer geschrieben werden, dient DOC_BLOCKS_PATCHED vor allem dazu, Lesenden und weiteren Geräten des Eigentümers Änderungen zu zeigen. Es gibt keine Konfliktauflösung (letzter Schreiber gewinnt).