# Delta Sync & Game Browsing Design **Date:** 2026-01-12 **Status:** Approved **Problem:** Houston Astros only shows 7 games in "By Games" mode ## Problem Analysis ### Root Causes 1. **90-day browsing window** in `TripCreationViewModel.loadGamesForBrowsing()` arbitrarily limits games shown in "By Games" mode 2. **6-month CloudKit sync window** in `CanonicalSyncService.syncGames()` only syncs games within 6 months of today 3. **Wrong query field** - CloudKit queries filter by game `dateTime` (when game is scheduled) instead of `modificationDate` (when record was last modified) ### Impact - Users cannot plan trips around games outside the 90-day window - New schedules added to CloudKit may not sync if games fall outside the window - Delta sync doesn't work correctly - modified historical games never sync ## Solution ### 1. Unified Delta Sync Pattern All CloudKit data types follow the same pattern: ```swift func fetchForSync(since lastSync: Date?) async throws -> [T] { let predicate: NSPredicate if let lastSync = lastSync { // Delta: only modified records predicate = NSPredicate(format: "modificationDate >= %@", lastSync as NSDate) } else { // First sync: ALL records predicate = NSPredicate(value: true) } // ... execute query } ``` **First sync (lastSync == nil):** Fetch all records from CloudKit. This handles the edge case where CloudKit is updated after app build but before user installs. **Delta sync (lastSync has value):** Fetch only records with `modificationDate >= lastSync`. ### 2. CloudKitService Changes | Method | Before | After | |--------|--------|-------| | `fetchStadiumsForSync` | `()` | `(since: Date?)` | | `fetchTeamsForSync` | `(for: Sport)` | `(since: Date?)` | | `fetchGamesForSync` | `(sports:, startDate:, endDate:)` | `(since: Date?)` | | `fetchLeagueStructureChanges` | `(since: Date?)` | No change | | `fetchTeamAliasChanges` | `(since: Date?)` | No change | | `fetchStadiumAliasChanges` | `(since: Date?)` | No change | **Teams optimization:** Fetch all teams in one call instead of 7 separate per-sport calls. ### 3. CanonicalSyncService Changes Remove date range calculations from `syncGames()`: ```swift // BEFORE let startDate = lastSync ?? Date() let endDate = Calendar.current.date(byAdding: .month, value: 6, to: Date()) ?? Date() let syncGames = try await cloudKitService.fetchGamesForSync( sports: Set(Sport.allCases), startDate: startDate, endDate: endDate ) // AFTER let syncGames = try await cloudKitService.fetchGamesForSync(since: lastSync) ``` ### 4. DataProvider Naming Convention Rename methods to clarify local vs network operations: | Before | After | Purpose | |--------|-------|---------| | `fetchGames(sports:, startDate:, endDate:)` | `filterGames(sports:, startDate:, endDate:)` | Local query with date filter | | `fetchRichGames(...)` | `filterRichGames(...)` | Local query with date filter | | New | `allGames(for sports:)` | Local query, no date filter | | New | `allRichGames(for sports:)` | Local query, no date filter | Convention: `filter*` = local query with constraints, `all*` = local query without date filter. ### 5. TripCreationViewModel Changes Remove 90-day browsing limit: ```swift // BEFORE let browseEndDate = Calendar.current.date(byAdding: .day, value: 90, to: Date()) ?? endDate games = try await dataProvider.fetchGames( sports: selectedSports, startDate: Date(), endDate: browseEndDate ) // AFTER games = try await dataProvider.allGames(for: selectedSports) ``` ## Files to Modify | File | Changes | |------|---------| | `SportsTime/Core/Services/CloudKitService.swift` | Update fetch signatures, query by modificationDate | | `SportsTime/Core/Services/CanonicalSyncService.swift` | Remove date range logic, pass lastSync directly | | `SportsTime/Core/Services/DataProvider.swift` | Rename methods, add `allGames`/`allRichGames` | | `SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift` | Use `allGames` instead of date-filtered fetch | | `SportsTimeTests/Mocks/MockCloudKitService.swift` | Update mock signatures | | `SportsTimeTests/Mocks/MockAppDataProvider.swift` | Update mock signatures, add new methods | ## Edge Cases ### App built before CloudKit update | Date | Event | |------|-------| | 1/12 | App published with bundled JSON | | 1/13 | Developer adds NWSL 2026 to CloudKit | | 1/14 | User installs app | **Handled by:** First sync (lastSync == nil) fetches ALL records from CloudKit, including the 1/13 additions. ### New league added When a new league (e.g., NWSL) is added to CloudKit: - New records have `modificationDate` = creation timestamp - Delta sync picks them up if `modificationDate >= lastSync` - Works for games, teams, stadiums, and all other data types ## Testing Existing tests should pass with method renames. Key scenarios to verify: 1. First launch: Full sync fetches all data 2. Subsequent launch: Delta sync only fetches modified records 3. "By Games" mode shows all available games 4. New CloudKit data syncs correctly after app update