Files
wotra/internal/handler/sync_handler.go

87 lines
2.3 KiB
Go

package handler
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/wotra/wotra/internal/store"
)
// SyncHandler serves /api/sync routes.
type SyncHandler struct {
syncStore *store.SyncStore
}
func NewSyncHandler(syncStore *store.SyncStore) *SyncHandler {
return &SyncHandler{syncStore: syncStore}
}
func (h *SyncHandler) Routes(r chi.Router) {
r.Post("/sync/pull", h.Pull)
r.Post("/sync/push", h.Push)
}
type pullRequest struct {
SinceVersion int64 `json:"since_version"`
}
type pullResponse struct {
Changes []store.SyncChange `json:"changes"`
ServerVersion int64 `json:"server_version"`
}
// Pull POST /api/sync/pull
func (h *SyncHandler) Pull(w http.ResponseWriter, r *http.Request) {
var req pullRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
changes, serverVersion, err := h.syncStore.Pull(r.Context(), req.SinceVersion)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if changes == nil {
changes = []store.SyncChange{}
}
writeJSON(w, http.StatusOK, pullResponse{Changes: changes, ServerVersion: serverVersion})
}
type pushChange struct {
Entity string `json:"_entity"`
Op string `json:"_op"`
EntityID string `json:"id"` // most entities use "id" or entity-specific key
Raw json.RawMessage `json:"-"`
}
type pushRequest struct {
Changes []json.RawMessage `json:"changes"`
}
type pushResponse struct {
Applied []string `json:"applied"`
Conflicts []string `json:"conflicts"`
}
// Push POST /api/sync/push — simple: log each item and return all as applied.
// Full conflict resolution is out of scope for v1; server is authoritative.
// Clients should pull after push to get the canonical state.
func (h *SyncHandler) Push(w http.ResponseWriter, r *http.Request) {
var req pushRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
applied := make([]string, 0, len(req.Changes))
// For v1, we acknowledge all pushes. The sync log is server-authoritative;
// direct API mutations are the canonical path. Client pushes are advisory.
for range req.Changes {
applied = append(applied, "ok")
}
writeJSON(w, http.StatusOK, pushResponse{Applied: applied, Conflicts: []string{}})
}