diff --git a/internal/api/sync.go b/internal/api/sync.go index 6a36f1f..ee70ebd 100644 --- a/internal/api/sync.go +++ b/internal/api/sync.go @@ -1,47 +1,53 @@ package api -import "encoding/json" +import ( + "encoding/json" + "fmt" + "io" + "time" +) type SyncResponse struct { // The new token to use for the next incremental sync. - SyncToken string `json:"sync_token"` - // If true, this response is a full sync and the client should not merge but instead replace it's state. - FullSync bool `json:"full_sync"` + SyncToken string `json:"sync_token,omitempty"` + // If true, this response is a full sync and the client should not merge but instead replace its state. + FullSync bool `json:"full_sync,omitempty"` // Used for commands where local IDs are temporarily chosen by the client and need to be mapped to the server's IDs. - TempIDMapping map[string]interface{} `json:"temp_id_mapping"` - Items []interface{} `json:"items"` - // CompletedInfo []interface{} `json:"completed_info"` - // Collaborators []interface{} `json:"collaborators"` - // CollaboratorStates []interface{} `json:"collaborator_states"` - // DayOrders map[string]interface{} `json:"day_orders"` - // Filters []interface{} `json:"filters"` - // Labels []interface{} `json:"labels"` - // LiveNotifications []interface{} `json:"live_notifications"` - // LiveNotificationsLastReadID string `json:"live_notifications_last_read_id"` - // Locations []interface{} `json:"locations"` - // Notes []interface{} `json:"notes"` - // ProjectNotes []interface{} `json:"project_notes"` - // Projects []interface{} `json:"projects"` - // Reminders []interface{} `json:"reminders"` - // Sections []interface{} `json:"sections"` - // Stats map[string]interface{} `json:"stats"` - // SettingsNotifications map[string]interface{} `json:"settings_notifications"` - // User map[string]interface{} `json:"user"` - // UserPlanLimits map[string]interface{} `json:"user_plan_limits"` - // UserSettings map[string]interface{} `json:"user_settings"` + TempIDMapping map[string]interface{} `json:"temp_id_mapping,omitempty"` + Items []Item `json:"items,omitempty"` + // CompletedInfo []interface{} `json:"completed_info,omitempty"` + // Collaborators []interface{} `json:"collaborators,omitempty"` + // CollaboratorStates []interface{} `json:"collaborator_states,omitempty"` + // DayOrders map[string]interface{} `json:"day_orders,omitempty"` + // Filters []interface{} `json:"filters,omitempty"` + // Labels []interface{} `json:"labels,omitempty"` + // LiveNotifications []interface{} `json:"live_notifications,omitempty"` + // LiveNotificationsLastReadID string `json:"live_notifications_last_read_id,omitempty"` + // Locations []interface{} `json:"locations,omitempty"` + // Notes []interface{} `json:"notes,omitempty"` + // ProjectNotes []interface{} `json:"project_notes,omitempty"` + // Projects []interface{} `json:"projects,omitempty"` + // Reminders []interface{} `json:"reminders,omitempty"` + // Sections []interface{} `json:"sections,omitempty"` + // Stats map[string]interface{} `json:"stats,omitempty"` + // SettingsNotifications map[string]interface{} `json:"settings_notifications,omitempty"` + // User map[string]interface{} `json:"user,omitempty"` + // UserPlanLimits map[string]interface{} `json:"user_plan_limits,omitempty"` + // UserSettings map[string]interface{} `json:"user_settings,omitempty"` } type State struct { Items map[string]Item } -type Changes struct { - Added []string - Updated []string - Deleted []string +// NewState creates a new blank state with initialized fields. +func NewState() *State { + return &State{ + Items: make(map[string]Item), + } } -// sync synchronizes the client's state with the server. If the full parameter is set to true, +// Synchronize the client's state with the server. If the full parameter is set to true, // a full synchronization is performed, otherwise, a partial synchronization is done. // This strongly mutates the client's state. // @@ -54,54 +60,72 @@ type Changes struct { // int - the number of changes synchronized. // *Changes - a pointer to a Changes struct containing the details of the changes. // error - an error object if an error occurred during synchronization, otherwise nil. -func (sc *SyncClient) Synchronize(full bool) (*Changes, error) { - if sc.RequireFullSync { +func (sc *SyncClient) Synchronize(full bool) (*[]byte, error) { + if sc.requireFullSync || sc.lastFullSync.IsZero() { + fmt.Printf("Performing full sync\n") sc.syncToken = "*" } + resourceTypes := make([]string, 4) + for resourceType := range sc.resourceTypes { + resourceTypes = append(resourceTypes, string(resourceType)) + } + body := map[string]interface{}{ - "sync_token": sc.syncToken, + "sync_token": sc.syncToken, + "resource_types": resourceTypes, } jsonBody, err := json.Marshal(body) if err != nil { - return 0, nil, err + return nil, fmt.Errorf("failed to marshal request body: %w", err) } res, err := sc.post("/sync", nil, jsonBody) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to post sync request: %w", err) } + now := time.Now() defer res.Body.Close() - var syncResponse SyncResponse - if err := json.NewDecoder(res.Body).Decode(&syncResponse); err != nil { - return nil, err + bodyBytes, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) } + var syncResponse SyncResponse + if err := json.Unmarshal(bodyBytes, &syncResponse); err != nil { + return nil, fmt.Errorf("failed to decode sync response: %w", err) + } + + // Always set in every request sc.syncToken = syncResponse.SyncToken + sc.lastSync = now - // changes := &Changes{ - // Added: []string{}, - // Updated: []string{}, - // Deleted: []string{}, - // } + // Simple, replace the state with the new items + if syncResponse.FullSync { + sc.lastFullSync = now + sc.requireFullSync = false - // // Process the items in syncResponse.Items to populate changes - // for _, item := range syncResponse.Items { - // // Assuming item is a map[string]interface{} and has a "status" field - // itemMap := item.(map[string]interface{}) - // if status, ok := itemMap["status"].(string); ok { - // switch status { - // case "added": - // changes.Added = append(changes.Added, itemMap["id"].(string)) - // case "updated": - // changes.Updated = append(changes.Updated, itemMap["id"].(string)) - // case "deleted": - // changes.Deleted = append(changes.Deleted, itemMap["id"].(string)) - // } - // } - // } + if syncResponse.Items != nil { + sc.State.Items = make(map[string]Item) + for _, item := range syncResponse.Items { + sc.State.Items[item.ID] = item + } + } + } else { + // Partial sync + if syncResponse.Items != nil { + for _, item := range syncResponse.Items { + sc.State.Items[item.ID] = item - return changes, nil + if item.IsDeleted { + delete(sc.State.Items, item.ID) + } + } + } + } + + return nil, nil + // return changes, nil }