tangled
alpha
login
or
join now
diffdown.com
/
diffdown-app
0
fork
atom
Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol
diffdown.com
0
fork
atom
overview
issues
10
pulls
pipelines
feat(handler): add SubmitSteps and GetSteps HTTP endpoints
John Luther
2 weeks ago
09ebc4b0
bd78f965
+109
2 changed files
expand all
collapse all
unified
split
cmd
server
main.go
internal
handler
handler.go
+2
cmd/server/main.go
reviewed
···
114
114
mux.HandleFunc("GET /docs/{rkey}/accept", h.AcceptInvite)
115
115
mux.HandleFunc("POST /api/docs/{rkey}/comments", h.CommentCreate)
116
116
mux.HandleFunc("GET /api/docs/{rkey}/comments", h.CommentList)
117
117
+
mux.HandleFunc("POST /api/docs/{rkey}/steps", h.SubmitSteps)
118
118
+
mux.HandleFunc("GET /api/docs/{rkey}/steps", h.GetSteps)
117
119
118
120
// WebSocket
119
121
mux.HandleFunc("GET /ws/docs/{rkey}", h.CollaboratorWebSocket)
+107
internal/handler/handler.go
reviewed
···
941
941
}
942
942
return colors[hash%len(colors)]
943
943
}
944
944
+
945
945
+
// SubmitSteps receives ProseMirror steps from a collaborator, appends them
946
946
+
// to the step log, and broadcasts confirmed steps to the room.
947
947
+
//
948
948
+
// POST /api/docs/{rkey}/steps
949
949
+
// Body: {"clientVersion": N, "steps": ["...json..."], "clientID": "did:..."}
950
950
+
// Response 200: {"version": N}
951
951
+
// Response 409: {"version": N, "steps": ["...json..."]} — client must rebase
952
952
+
func (h *Handler) SubmitSteps(w http.ResponseWriter, r *http.Request) {
953
953
+
user := h.currentUser(r)
954
954
+
if user == nil {
955
955
+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
956
956
+
return
957
957
+
}
958
958
+
rkey := r.PathValue("rkey")
959
959
+
960
960
+
var body struct {
961
961
+
ClientVersion int `json:"clientVersion"`
962
962
+
Steps []string `json:"steps"`
963
963
+
ClientID string `json:"clientID"`
964
964
+
}
965
965
+
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
966
966
+
http.Error(w, "Bad request", http.StatusBadRequest)
967
967
+
return
968
968
+
}
969
969
+
if len(body.Steps) == 0 {
970
970
+
http.Error(w, "No steps", http.StatusBadRequest)
971
971
+
return
972
972
+
}
973
973
+
974
974
+
newVersion, err := h.DB.AppendSteps(rkey, body.ClientVersion, body.Steps, body.ClientID)
975
975
+
if err != nil {
976
976
+
// Version conflict — return steps the client missed.
977
977
+
missed, dbErr := h.DB.GetStepsSince(rkey, body.ClientVersion)
978
978
+
if dbErr != nil {
979
979
+
log.Printf("SubmitSteps: GetStepsSince: %v", dbErr)
980
980
+
http.Error(w, "Internal error", http.StatusInternalServerError)
981
981
+
return
982
982
+
}
983
983
+
currentVersion, _ := h.DB.GetDocVersion(rkey)
984
984
+
stepJSONs := make([]string, len(missed))
985
985
+
for i, s := range missed {
986
986
+
stepJSONs[i] = s.JSON
987
987
+
}
988
988
+
w.Header().Set("Content-Type", "application/json")
989
989
+
w.WriteHeader(http.StatusConflict)
990
990
+
json.NewEncoder(w).Encode(map[string]interface{}{
991
991
+
"version": currentVersion,
992
992
+
"steps": stepJSONs,
993
993
+
})
994
994
+
return
995
995
+
}
996
996
+
997
997
+
// Broadcast to other room members via WebSocket.
998
998
+
if room := h.CollaborationHub.GetRoom(rkey); room != nil {
999
999
+
type stepsMsg struct {
1000
1000
+
Type string `json:"type"`
1001
1001
+
Steps []string `json:"steps"`
1002
1002
+
Version int `json:"version"`
1003
1003
+
ClientID string `json:"clientID"`
1004
1004
+
}
1005
1005
+
data, _ := json.Marshal(stepsMsg{
1006
1006
+
Type: "steps",
1007
1007
+
Steps: body.Steps,
1008
1008
+
Version: newVersion,
1009
1009
+
ClientID: body.ClientID,
1010
1010
+
})
1011
1011
+
room.Broadcast(data)
1012
1012
+
}
1013
1013
+
1014
1014
+
h.jsonResponse(w, map[string]int{"version": newVersion}, http.StatusOK)
1015
1015
+
}
1016
1016
+
1017
1017
+
// GetSteps returns all steps since the given version.
1018
1018
+
//
1019
1019
+
// GET /api/docs/{rkey}/steps?since={v}
1020
1020
+
// Response 200: {"version": N, "steps": ["...json..."]}
1021
1021
+
func (h *Handler) GetSteps(w http.ResponseWriter, r *http.Request) {
1022
1022
+
user := h.currentUser(r)
1023
1023
+
if user == nil {
1024
1024
+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
1025
1025
+
return
1026
1026
+
}
1027
1027
+
rkey := r.PathValue("rkey")
1028
1028
+
1029
1029
+
sinceStr := r.URL.Query().Get("since")
1030
1030
+
var since int
1031
1031
+
if sinceStr != "" {
1032
1032
+
fmt.Sscanf(sinceStr, "%d", &since)
1033
1033
+
}
1034
1034
+
1035
1035
+
rows, err := h.DB.GetStepsSince(rkey, since)
1036
1036
+
if err != nil {
1037
1037
+
log.Printf("GetSteps: %v", err)
1038
1038
+
http.Error(w, "Internal error", http.StatusInternalServerError)
1039
1039
+
return
1040
1040
+
}
1041
1041
+
version, _ := h.DB.GetDocVersion(rkey)
1042
1042
+
stepJSONs := make([]string, len(rows))
1043
1043
+
for i, s := range rows {
1044
1044
+
stepJSONs[i] = s.JSON
1045
1045
+
}
1046
1046
+
h.jsonResponse(w, map[string]interface{}{
1047
1047
+
"version": version,
1048
1048
+
"steps": stepJSONs,
1049
1049
+
}, http.StatusOK)
1050
1050
+
}