feat(realtime): implement SSE fallback, dual-transport hub & proxy resilience (resolves FLAW-03)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
This commit is contained in:
1 parent
80bb74b5b2
commit
6343d62bab
10 files changed
+1402
-240
No files matched your search
+115
-1
@@ -69,6 +69,8 @@ func (s *Server) Router() http.Handler {
|
||||
r.Get("/api/v1/ws", s.handleWebSocket)
|
||||
r.Get("/api/v1/ws/spaces/{spaceId}", s.handleWebSocket)
|
||||
r.Get("/api/v1/ws/lists/{listId}", s.handleWebSocket)
|
||||
r.Get("/api/v1/events", s.handleSSE)
|
||||
r.Post("/api/v1/events/rooms", s.handleSSERooms)
|
||||
|
||||
// Authenticated API Routes
|
||||
r.Group(func(r chi.Router) {
|
||||
@@ -148,6 +150,8 @@ func (s *Server) Router() http.Handler {
|
||||
// Document CRDT Realtime Sync
|
||||
r.Get("/api/v1/documents/{id}/crdt", s.handleGetDocCRDT)
|
||||
r.Put("/api/v1/documents/{id}/crdt", s.handleSaveDocCRDT)
|
||||
r.Post("/api/v1/documents/{id}/awareness", s.handlePostDocAwareness)
|
||||
r.Delete("/api/v1/documents/{id}/awareness", s.handleDeleteDocAwareness)
|
||||
|
||||
// Shared Lists & Realtime Collaboration
|
||||
r.Get("/api/v1/lists", s.handleGetAllLists)
|
||||
@@ -1423,12 +1427,29 @@ func (s *Server) handleSaveDocCRDT(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
State string `json:"state"`
|
||||
State string `json:"state"`
|
||||
Update string `json:"update"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiger Request-Body")
|
||||
return
|
||||
}
|
||||
if req.Update != "" {
|
||||
raw, err := base64.StdEncoding.DecodeString(req.Update)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges Base64 Format für Update")
|
||||
return
|
||||
}
|
||||
if err := s.store.AppendDocumentCRDTUpdate(docID, raw, userID); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
s.wsHub.BroadcastRoom("doc:"+docID, "CRDT_UPDATE", map[string]interface{}{
|
||||
"docId": docID,
|
||||
"update": req.Update,
|
||||
"fromUserId": userID,
|
||||
}, nil)
|
||||
}
|
||||
if req.State != "" {
|
||||
raw, err := base64.StdEncoding.DecodeString(req.State)
|
||||
if err != nil {
|
||||
@@ -1443,6 +1464,37 @@ func (s *Server) handleSaveDocCRDT(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func (s *Server) handlePostDocAwareness(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
docID := chi.URLParam(r, "id")
|
||||
doc, err := s.store.GetDocumentByID(userID, docID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "Dokument nicht gefunden")
|
||||
return
|
||||
}
|
||||
_ = doc
|
||||
|
||||
var payload json.RawMessage
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
|
||||
s.wsHub.BroadcastRoom("doc:"+docID, "CRDT_AWARENESS", payload, nil)
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteDocAwareness(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
docID := chi.URLParam(r, "id")
|
||||
|
||||
s.wsHub.BroadcastRoom("doc:"+docID, "CRDT_AWARENESS_LEAVE", map[string]interface{}{
|
||||
"docId": docID,
|
||||
"userId": userID,
|
||||
}, nil)
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
// --- REALTIME LISTS & SHARING ---
|
||||
|
||||
func (s *Server) handleGetAllLists(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1712,6 +1764,68 @@ func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
s.wsHub.ServeWS(w, r, roomID, userID)
|
||||
}
|
||||
|
||||
func (s *Server) handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||
roomID := r.URL.Query().Get("listId")
|
||||
if roomID == "" {
|
||||
roomID = r.URL.Query().Get("spaceId")
|
||||
}
|
||||
|
||||
token := r.URL.Query().Get("token")
|
||||
if token == "" {
|
||||
if authHdr := r.Header.Get("Authorization"); strings.HasPrefix(authHdr, "Bearer ") {
|
||||
token = strings.TrimPrefix(authHdr, "Bearer ")
|
||||
}
|
||||
}
|
||||
userID := "anonymous"
|
||||
if token != "" {
|
||||
if claims, err := auth.ValidateToken(s.cfg.JWTSecret, token); err == nil {
|
||||
userID = claims.UserID
|
||||
}
|
||||
}
|
||||
|
||||
var initialRooms []string
|
||||
if roomsParam := r.URL.Query().Get("rooms"); roomsParam != "" {
|
||||
for _, rm := range strings.Split(roomsParam, ",") {
|
||||
rm = strings.TrimSpace(rm)
|
||||
if rm != "" {
|
||||
initialRooms = append(initialRooms, rm)
|
||||
}
|
||||
}
|
||||
}
|
||||
if docID := r.URL.Query().Get("docId"); docID != "" {
|
||||
initialRooms = append(initialRooms, "doc:"+strings.TrimSpace(docID))
|
||||
}
|
||||
|
||||
s.wsHub.ServeSSE(w, r, roomID, userID, initialRooms)
|
||||
}
|
||||
|
||||
func (s *Server) handleSSERooms(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
SSEID string `json:"sseId"`
|
||||
Room string `json:"room"`
|
||||
DocID string `json:"docId"`
|
||||
Action string `json:"action"` // "join" or "leave"
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
if req.Room == "" && req.DocID != "" {
|
||||
req.Room = "doc:" + req.DocID
|
||||
}
|
||||
if req.SSEID == "" || req.Room == "" {
|
||||
writeError(w, http.StatusBadRequest, "sseId und room erforderlich")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Action == "leave" {
|
||||
s.wsHub.LeaveSSERoomByID(req.SSEID, req.Room)
|
||||
} else {
|
||||
s.wsHub.JoinSSERoomByID(req.SSEID, req.Room)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
// --- API KEYS & WEBHOOK INGESTION ---
|
||||
|
||||
func (s *Server) handleGetAPIKeys(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in new issue
Block a user