mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-23 11:03:41 +00:00
7ef4506cfa
When the browser tab lost focus and regained it, 5 independent onTabResume callbacks all fired simultaneously, flooding the server with redundant requests. This replaces that pattern with a 4-layer sync architecture: 1. Replay buffer — per-workspace ring buffer stores recent events with monotonic IDs. On SSE reconnect, missed events are replayed via Last-Event-ID so the client is already caught up. 2. Last-Event-ID support — SSE handler reads the header, replays from buffer, or sends sync_required if the gap is too large. 3. Incremental sync — new /changes?since=<ms> endpoint returns only modified/deleted items since a timestamp, including archived items for view consistency. 4. Centralized sync coordinator — single decision tree replaces 5 scattered callbacks. Short absences skip sync entirely, SSE-covered gaps need no API calls, and full refresh is a last resort. Key robustness details: - Global event IDs via Redis INCR for multi-instance safety - Server-time cursors to avoid client clock skew - Safe cursor management (only advances on confirmed sync) - 9 new tests for replay buffer and event ID behavior Fixes BUG-26.
78 lines
2.3 KiB
Go
78 lines
2.3 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// ChangesResponse is the response for GET /workspaces/{ws}/changes?since=<unix_ms>.
|
|
type ChangesResponse struct {
|
|
// Updated items (with full item data).
|
|
Updated []interface{} `json:"updated"`
|
|
// IDs of items that were deleted since the requested timestamp.
|
|
Deleted []string `json:"deleted"`
|
|
// Server timestamp at the time of this response (unix ms).
|
|
// Clients should use this as the `since` value for the next sync.
|
|
ServerTime int64 `json:"server_time"`
|
|
// Whether the collection metadata (counts, schemas) may have changed.
|
|
// True if any items were updated/deleted, signaling the client should
|
|
// also refresh collection metadata.
|
|
CollectionsChanged bool `json:"collections_changed"`
|
|
}
|
|
|
|
// handleGetChanges returns items modified since a given timestamp.
|
|
// GET /api/v1/workspaces/{ws}/changes?since=<unix_milliseconds>
|
|
//
|
|
// This is the incremental sync endpoint used by the frontend when the
|
|
// tab regains focus. Instead of re-fetching everything, the client sends
|
|
// the timestamp of its last successful sync and gets back only the delta.
|
|
func (s *Server) handleGetChanges(w http.ResponseWriter, r *http.Request) {
|
|
workspaceID, ok := s.getWorkspaceID(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
sinceStr := r.URL.Query().Get("since")
|
|
if sinceStr == "" {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "since query parameter is required (unix milliseconds)")
|
|
return
|
|
}
|
|
|
|
sinceMs, err := strconv.ParseInt(sinceStr, 10, 64)
|
|
if err != nil || sinceMs < 0 {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "since must be a valid unix timestamp in milliseconds")
|
|
return
|
|
}
|
|
|
|
since := time.UnixMilli(sinceMs)
|
|
serverTime := time.Now().UnixMilli()
|
|
|
|
updated, deletedIDs, err := s.store.ItemsModifiedSince(workspaceID, since)
|
|
if err != nil {
|
|
writeInternalError(w, err)
|
|
return
|
|
}
|
|
|
|
// Convert to interface slice for JSON marshaling.
|
|
updatedItems := make([]interface{}, len(updated))
|
|
for i, item := range updated {
|
|
updatedItems[i] = item
|
|
}
|
|
if updatedItems == nil {
|
|
updatedItems = []interface{}{}
|
|
}
|
|
if deletedIDs == nil {
|
|
deletedIDs = []string{}
|
|
}
|
|
|
|
resp := ChangesResponse{
|
|
Updated: updatedItems,
|
|
Deleted: deletedIDs,
|
|
ServerTime: serverTime,
|
|
CollectionsChanged: len(updated) > 0 || len(deletedIDs) > 0,
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|