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:
@@ -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!
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -95,7 +95,7 @@ final class ScheduleViewModel {
|
||||
return
|
||||
}
|
||||
|
||||
games = try await dataProvider.fetchRichGames(
|
||||
games = try await dataProvider.filterRichGames(
|
||||
sports: selectedSports,
|
||||
startDate: startDate,
|
||||
endDate: endDate
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user