Phase 5 of the collaboration plan:
- Remove PATCH /documents/{id}/blocks, PatchDocumentBlocks, the
DOC_BLOCKS_PATCHED event and its types, FlushDocumentState and
LoadDocumentState. PUT /documents/{id} rejects blocks (400) and only
changes title, icon, folder and pin; editors may now change title/icon.
- The to-do overview ticks via a single operation on /docs/{id}/sync
(new skipLog flag). Task links set the block tick with a targeted
operation and notify open editors; creating a linked task needs edit
rights.
- Keep the three share tables: DocRole already decides centrally, and
rewriting the visibility queries would only add risk (documented in plan).
- Tests adapted to the new paths; share e2e test now also ticks a to-do in
the overview.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.9 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 und wechselt in folgenden Fällen für die Sitzung des Tabs auf SSE:
- Aufbau gelingt nicht innerhalb von 2,5 s, oder die Verbindung bricht unsauber ab (z. B. Code 1006/1008).
- Herzschlag: Alle 15 s schickt der Client
{"event":"PING"}, der Server antwortetPONG. Kommt 35 s lang keine Nachricht, gilt der WebSocket als stumm (Proxy nimmt das Upgrade an, leitet aber nichts weiter).
Über SSE liefert der Server zuerst SSE_CONNECTED mit einer sseId; erst dann gilt SSE als verbunden. Bleibt dieses erste Ereignis 5 s nach dem Öffnen aus, puffert ein Proxy den Stream: SSE wird für den Tab abgeschaltet. Räume werden über POST /api/v1/events/rooms (sseId, room, action: join/leave) abonniert.
Ohne WebSocket und SSE nutzt die Notiz-Synchronisation Long-Polling (GET /docs/{id}/changes, bis 25 s), und wenn auch das dreimal scheitert, eine Abfrage alle 3 s (siehe modules/workspace.md).
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>"}, {"event":"LEAVE_ROOM","room":"<id>"} und {"event":"PING"} (Antwort PONG). 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> |
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:wsweiter; Ansichten (TasksView,WorkspaceView,SharedListsView) hören darauf und laden bzw. patchen ihren Zustand. - Nach einem Abbruch verbindet sich der Client mit Verzögerung neu.
- Notiz-Inhalte kommen nie per Push, sondern immer über
POST /docs/{id}/sync;DOC_SEQist nur der Weckruf. Konflikte entscheidet der Server (Versionen und Sperren, siehe plans/kollaboration-notizen.md).