docs: add delta sync and game browsing design
Addresses issue where Houston Astros only shows 7 games in "By Games" mode. Documents plan to remove arbitrary date restrictions and implement proper delta sync using CloudKit modificationDate. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
147
docs/plans/2026-01-12-delta-sync-design.md
Normal file
147
docs/plans/2026-01-12-delta-sync-design.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user