feat: complete delta sync implementation - add allGames, update callers

- Add allRichGames method to DataProvider
- Update TripCreationViewModel.loadGamesForBrowsing to use allGames (removes 90-day limit)
- Update MockCloudKitService sync methods to use new delta sync signatures
- Update MockAppDataProvider with renamed methods and new allGames/allRichGames
- Fix all callers: ScheduleViewModel, TripCreationViewModel, SuggestedTripsGenerator, GameMatcher
- Update CLAUDE.md documentation with new method names

This completes the delta sync implementation:
- CloudKit sync now uses modificationDate for proper delta sync
- First sync fetches ALL data, subsequent syncs only fetch modified records
- "By Games" mode now shows all available games (not just 90 days)
- All data types (stadiums, teams, games) use consistent delta sync pattern

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-12 11:04:52 -06:00
parent b3ad386d2b
commit 3978429716
8 changed files with 78 additions and 42 deletions

View File

@@ -102,8 +102,9 @@ All code that reads stadiums, teams, games, or league structure MUST use `AppDat
// CORRECT - Use AppDataProvider
let stadiums = AppDataProvider.shared.stadiums
let teams = AppDataProvider.shared.teams
let games = try await AppDataProvider.shared.fetchGames(sports: sports, startDate: start, endDate: end)
let richGames = try await AppDataProvider.shared.fetchRichGames(...)
let games = try await AppDataProvider.shared.filterGames(sports: sports, startDate: start, endDate: end)
let richGames = try await AppDataProvider.shared.filterRichGames(...)
let allGames = try await AppDataProvider.shared.allGames(for: sports)
// WRONG - Never access CloudKit directly for reads
let stadiums = try await CloudKitService.shared.fetchStadiums() // NO!

View File

@@ -197,6 +197,20 @@ final class AppDataProvider: ObservableObject {
}
}
/// Get all games with full team and stadium data (no date filtering)
func allRichGames(for sports: Set<Sport>) async throws -> [RichGame] {
let games = try await allGames(for: sports)
return games.compactMap { game in
guard let homeTeam = teamsById[game.homeTeamId],
let awayTeam = teamsById[game.awayTeamId],
let stadium = stadiumsById[game.stadiumId] else {
return nil
}
return RichGame(game: game, homeTeam: homeTeam, awayTeam: awayTeam, stadium: stadium)
}
}
func richGame(from game: Game) -> RichGame? {
guard let homeTeam = teamsById[game.homeTeamId],
let awayTeam = teamsById[game.awayTeamId],

View File

@@ -398,7 +398,7 @@ final class GameMatcher {
let sports: Set<Sport> = sport != nil ? [sport!] : Set(Sport.allCases)
do {
let allGames = try await dataProvider.fetchGames(sports: sports, startDate: startDate, endDate: endDate)
let allGames = try await dataProvider.filterGames(sports: sports, startDate: startDate, endDate: endDate)
// Filter by stadium
let games = allGames.filter { $0.stadiumId == stadium.id }

View File

@@ -104,9 +104,9 @@ final class SuggestedTripsGenerator {
}
do {
// Fetch all games in the window
// Filter all games in the window
let allSports = Set(Sport.supported)
let games = try await dataProvider.fetchGames(
let games = try await dataProvider.filterGames(
sports: allSports,
startDate: startDate,
endDate: endDate

View File

@@ -95,7 +95,7 @@ final class ScheduleViewModel {
return
}
games = try await dataProvider.fetchRichGames(
games = try await dataProvider.filterRichGames(
sports: selectedSports,
startDate: startDate,
endDate: endDate

View File

@@ -260,8 +260,8 @@ final class TripCreationViewModel {
stadiums[stadium.id] = stadium
}
// Fetch games
games = try await dataProvider.fetchGames(
// Filter games within date range
games = try await dataProvider.filterGames(
sports: selectedSports,
startDate: startDate,
endDate: endDate
@@ -488,13 +488,8 @@ final class TripCreationViewModel {
stadiums[stadium.id] = stadium
}
// Fetch games for next 90 days for browsing
let browseEndDate = Calendar.current.date(byAdding: .day, value: 90, to: Date()) ?? endDate
games = try await dataProvider.fetchGames(
sports: selectedSports,
startDate: Date(),
endDate: browseEndDate
)
// Fetch all games for browsing (no date filter)
games = try await dataProvider.allGames(for: selectedSports)
// Build rich games for display
availableGames = games.compactMap { game -> RichGame? in

View File

@@ -48,8 +48,10 @@ final class MockAppDataProvider: ObservableObject {
// MARK: - Call Tracking
private(set) var loadInitialDataCallCount = 0
private(set) var fetchGamesCallCount = 0
private(set) var fetchRichGamesCallCount = 0
private(set) var filterGamesCallCount = 0
private(set) var filterRichGamesCallCount = 0
private(set) var allGamesCallCount = 0
private(set) var allRichGamesCallCount = 0
// MARK: - Initialization
@@ -89,8 +91,10 @@ final class MockAppDataProvider: ObservableObject {
error = nil
errorMessage = nil
loadInitialDataCallCount = 0
fetchGamesCallCount = 0
fetchRichGamesCallCount = 0
filterGamesCallCount = 0
filterRichGamesCallCount = 0
allGamesCallCount = 0
allRichGamesCallCount = 0
config = .default
}
@@ -152,10 +156,10 @@ final class MockAppDataProvider: ObservableObject {
teams.filter { $0.sport == sport }
}
// MARK: - Game Fetching
// MARK: - Game Filtering (Local Queries)
func fetchGames(sports: Set<Sport>, startDate: Date, endDate: Date) async throws -> [Game] {
fetchGamesCallCount += 1
func filterGames(sports: Set<Sport>, startDate: Date, endDate: Date) async throws -> [Game] {
filterGamesCallCount += 1
await simulateLatency()
if config.shouldFailOnFetch {
@@ -169,6 +173,19 @@ final class MockAppDataProvider: ObservableObject {
}.sorted { $0.dateTime < $1.dateTime }
}
func allGames(for sports: Set<Sport>) async throws -> [Game] {
allGamesCallCount += 1
await simulateLatency()
if config.shouldFailOnFetch {
throw DataProviderError.contextNotConfigured
}
return games.filter { game in
sports.contains(game.sport)
}.sorted { $0.dateTime < $1.dateTime }
}
func fetchGame(by id: String) async throws -> Game? {
await simulateLatency()
@@ -179,15 +196,24 @@ final class MockAppDataProvider: ObservableObject {
return gamesById[id]
}
func fetchRichGames(sports: Set<Sport>, startDate: Date, endDate: Date) async throws -> [RichGame] {
fetchRichGamesCallCount += 1
let filteredGames = try await fetchGames(sports: sports, startDate: startDate, endDate: endDate)
func filterRichGames(sports: Set<Sport>, startDate: Date, endDate: Date) async throws -> [RichGame] {
filterRichGamesCallCount += 1
let filteredGames = try await filterGames(sports: sports, startDate: startDate, endDate: endDate)
return filteredGames.compactMap { game in
richGame(from: game)
}
}
func allRichGames(for sports: Set<Sport>) async throws -> [RichGame] {
allRichGamesCallCount += 1
let allFilteredGames = try await allGames(for: sports)
return allFilteredGames.compactMap { game in
richGame(from: game)
}
}
func richGame(from game: Game) -> RichGame? {
guard let homeTeam = teamsById[game.homeTeamId],
let awayTeam = teamsById[game.awayTeamId],
@@ -248,8 +274,8 @@ extension MockAppDataProvider {
stadiumsById[stadium.id] = stadium
}
/// Get all games (for test verification)
func allGames() -> [Game] {
/// Get all stored games (for test verification)
func getAllStoredGames() -> [Game] {
games
}

View File

@@ -160,10 +160,13 @@ actor MockCloudKitService {
return games.first { $0.id == id }
}
// MARK: - Sync Fetch Methods
// MARK: - Sync Fetch Methods (Delta Sync Pattern)
func fetchStadiumsForSync() async throws -> [CloudKitService.SyncStadium] {
/// Fetch stadiums for sync - returns all if lastSync is nil, otherwise filters by modification date
func fetchStadiumsForSync(since lastSync: Date?) async throws -> [CloudKitService.SyncStadium] {
try await simulateNetwork()
// Mock doesn't track modification dates, so return all stadiums
// (In production, CloudKit filters by modificationDate)
return stadiums.map { stadium in
CloudKitService.SyncStadium(
stadium: stadium,
@@ -172,9 +175,12 @@ actor MockCloudKitService {
}
}
func fetchTeamsForSync(for sport: Sport) async throws -> [CloudKitService.SyncTeam] {
/// Fetch teams for sync - returns all if lastSync is nil, otherwise filters by modification date
func fetchTeamsForSync(since lastSync: Date?) async throws -> [CloudKitService.SyncTeam] {
try await simulateNetwork()
return teams.filter { $0.sport == sport }.map { team in
// Mock doesn't track modification dates, so return all teams
// (In production, CloudKit filters by modificationDate)
return teams.map { team in
CloudKitService.SyncTeam(
team: team,
canonicalId: team.id,
@@ -183,18 +189,12 @@ actor MockCloudKitService {
}
}
func fetchGamesForSync(
sports: Set<Sport>,
startDate: Date,
endDate: Date
) async throws -> [CloudKitService.SyncGame] {
/// Fetch games for sync - returns all if lastSync is nil, otherwise filters by modification date
func fetchGamesForSync(since lastSync: Date?) async throws -> [CloudKitService.SyncGame] {
try await simulateNetwork()
return games.filter { game in
sports.contains(game.sport) &&
game.dateTime >= startDate &&
game.dateTime <= endDate
}.map { game in
// Mock doesn't track modification dates, so return all games
// (In production, CloudKit filters by modificationDate)
return games.map { game in
CloudKitService.SyncGame(
game: game,
canonicalId: game.id,