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
|
// ✅ CORRECT - Use AppDataProvider
|
||||||
let stadiums = AppDataProvider.shared.stadiums
|
let stadiums = AppDataProvider.shared.stadiums
|
||||||
let teams = AppDataProvider.shared.teams
|
let teams = AppDataProvider.shared.teams
|
||||||
let games = try await AppDataProvider.shared.fetchGames(sports: sports, startDate: start, endDate: end)
|
let games = try await AppDataProvider.shared.filterGames(sports: sports, startDate: start, endDate: end)
|
||||||
let richGames = try await AppDataProvider.shared.fetchRichGames(...)
|
let richGames = try await AppDataProvider.shared.filterRichGames(...)
|
||||||
|
let allGames = try await AppDataProvider.shared.allGames(for: sports)
|
||||||
|
|
||||||
// ❌ WRONG - Never access CloudKit directly for reads
|
// ❌ WRONG - Never access CloudKit directly for reads
|
||||||
let stadiums = try await CloudKitService.shared.fetchStadiums() // NO!
|
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? {
|
func richGame(from game: Game) -> RichGame? {
|
||||||
guard let homeTeam = teamsById[game.homeTeamId],
|
guard let homeTeam = teamsById[game.homeTeamId],
|
||||||
let awayTeam = teamsById[game.awayTeamId],
|
let awayTeam = teamsById[game.awayTeamId],
|
||||||
|
|||||||
@@ -398,7 +398,7 @@ final class GameMatcher {
|
|||||||
let sports: Set<Sport> = sport != nil ? [sport!] : Set(Sport.allCases)
|
let sports: Set<Sport> = sport != nil ? [sport!] : Set(Sport.allCases)
|
||||||
|
|
||||||
do {
|
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
|
// Filter by stadium
|
||||||
let games = allGames.filter { $0.stadiumId == stadium.id }
|
let games = allGames.filter { $0.stadiumId == stadium.id }
|
||||||
|
|||||||
@@ -104,9 +104,9 @@ final class SuggestedTripsGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
// Fetch all games in the window
|
// Filter all games in the window
|
||||||
let allSports = Set(Sport.supported)
|
let allSports = Set(Sport.supported)
|
||||||
let games = try await dataProvider.fetchGames(
|
let games = try await dataProvider.filterGames(
|
||||||
sports: allSports,
|
sports: allSports,
|
||||||
startDate: startDate,
|
startDate: startDate,
|
||||||
endDate: endDate
|
endDate: endDate
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ final class ScheduleViewModel {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
games = try await dataProvider.fetchRichGames(
|
games = try await dataProvider.filterRichGames(
|
||||||
sports: selectedSports,
|
sports: selectedSports,
|
||||||
startDate: startDate,
|
startDate: startDate,
|
||||||
endDate: endDate
|
endDate: endDate
|
||||||
|
|||||||
@@ -260,8 +260,8 @@ final class TripCreationViewModel {
|
|||||||
stadiums[stadium.id] = stadium
|
stadiums[stadium.id] = stadium
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch games
|
// Filter games within date range
|
||||||
games = try await dataProvider.fetchGames(
|
games = try await dataProvider.filterGames(
|
||||||
sports: selectedSports,
|
sports: selectedSports,
|
||||||
startDate: startDate,
|
startDate: startDate,
|
||||||
endDate: endDate
|
endDate: endDate
|
||||||
@@ -488,13 +488,8 @@ final class TripCreationViewModel {
|
|||||||
stadiums[stadium.id] = stadium
|
stadiums[stadium.id] = stadium
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch games for next 90 days for browsing
|
// Fetch all games for browsing (no date filter)
|
||||||
let browseEndDate = Calendar.current.date(byAdding: .day, value: 90, to: Date()) ?? endDate
|
games = try await dataProvider.allGames(for: selectedSports)
|
||||||
games = try await dataProvider.fetchGames(
|
|
||||||
sports: selectedSports,
|
|
||||||
startDate: Date(),
|
|
||||||
endDate: browseEndDate
|
|
||||||
)
|
|
||||||
|
|
||||||
// Build rich games for display
|
// Build rich games for display
|
||||||
availableGames = games.compactMap { game -> RichGame? in
|
availableGames = games.compactMap { game -> RichGame? in
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ final class MockAppDataProvider: ObservableObject {
|
|||||||
// MARK: - Call Tracking
|
// MARK: - Call Tracking
|
||||||
|
|
||||||
private(set) var loadInitialDataCallCount = 0
|
private(set) var loadInitialDataCallCount = 0
|
||||||
private(set) var fetchGamesCallCount = 0
|
private(set) var filterGamesCallCount = 0
|
||||||
private(set) var fetchRichGamesCallCount = 0
|
private(set) var filterRichGamesCallCount = 0
|
||||||
|
private(set) var allGamesCallCount = 0
|
||||||
|
private(set) var allRichGamesCallCount = 0
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
@@ -89,8 +91,10 @@ final class MockAppDataProvider: ObservableObject {
|
|||||||
error = nil
|
error = nil
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
loadInitialDataCallCount = 0
|
loadInitialDataCallCount = 0
|
||||||
fetchGamesCallCount = 0
|
filterGamesCallCount = 0
|
||||||
fetchRichGamesCallCount = 0
|
filterRichGamesCallCount = 0
|
||||||
|
allGamesCallCount = 0
|
||||||
|
allRichGamesCallCount = 0
|
||||||
config = .default
|
config = .default
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,10 +156,10 @@ final class MockAppDataProvider: ObservableObject {
|
|||||||
teams.filter { $0.sport == sport }
|
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] {
|
func filterGames(sports: Set<Sport>, startDate: Date, endDate: Date) async throws -> [Game] {
|
||||||
fetchGamesCallCount += 1
|
filterGamesCallCount += 1
|
||||||
await simulateLatency()
|
await simulateLatency()
|
||||||
|
|
||||||
if config.shouldFailOnFetch {
|
if config.shouldFailOnFetch {
|
||||||
@@ -169,6 +173,19 @@ final class MockAppDataProvider: ObservableObject {
|
|||||||
}.sorted { $0.dateTime < $1.dateTime }
|
}.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? {
|
func fetchGame(by id: String) async throws -> Game? {
|
||||||
await simulateLatency()
|
await simulateLatency()
|
||||||
|
|
||||||
@@ -179,15 +196,24 @@ final class MockAppDataProvider: ObservableObject {
|
|||||||
return gamesById[id]
|
return gamesById[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchRichGames(sports: Set<Sport>, startDate: Date, endDate: Date) async throws -> [RichGame] {
|
func filterRichGames(sports: Set<Sport>, startDate: Date, endDate: Date) async throws -> [RichGame] {
|
||||||
fetchRichGamesCallCount += 1
|
filterRichGamesCallCount += 1
|
||||||
let filteredGames = try await fetchGames(sports: sports, startDate: startDate, endDate: endDate)
|
let filteredGames = try await filterGames(sports: sports, startDate: startDate, endDate: endDate)
|
||||||
|
|
||||||
return filteredGames.compactMap { game in
|
return filteredGames.compactMap { game in
|
||||||
richGame(from: game)
|
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? {
|
func richGame(from game: Game) -> RichGame? {
|
||||||
guard let homeTeam = teamsById[game.homeTeamId],
|
guard let homeTeam = teamsById[game.homeTeamId],
|
||||||
let awayTeam = teamsById[game.awayTeamId],
|
let awayTeam = teamsById[game.awayTeamId],
|
||||||
@@ -248,8 +274,8 @@ extension MockAppDataProvider {
|
|||||||
stadiumsById[stadium.id] = stadium
|
stadiumsById[stadium.id] = stadium
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all games (for test verification)
|
/// Get all stored games (for test verification)
|
||||||
func allGames() -> [Game] {
|
func getAllStoredGames() -> [Game] {
|
||||||
games
|
games
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -160,10 +160,13 @@ actor MockCloudKitService {
|
|||||||
return games.first { $0.id == id }
|
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()
|
try await simulateNetwork()
|
||||||
|
// Mock doesn't track modification dates, so return all stadiums
|
||||||
|
// (In production, CloudKit filters by modificationDate)
|
||||||
return stadiums.map { stadium in
|
return stadiums.map { stadium in
|
||||||
CloudKitService.SyncStadium(
|
CloudKitService.SyncStadium(
|
||||||
stadium: stadium,
|
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()
|
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(
|
CloudKitService.SyncTeam(
|
||||||
team: team,
|
team: team,
|
||||||
canonicalId: team.id,
|
canonicalId: team.id,
|
||||||
@@ -183,18 +189,12 @@ actor MockCloudKitService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchGamesForSync(
|
/// Fetch games for sync - returns all if lastSync is nil, otherwise filters by modification date
|
||||||
sports: Set<Sport>,
|
func fetchGamesForSync(since lastSync: Date?) async throws -> [CloudKitService.SyncGame] {
|
||||||
startDate: Date,
|
|
||||||
endDate: Date
|
|
||||||
) async throws -> [CloudKitService.SyncGame] {
|
|
||||||
try await simulateNetwork()
|
try await simulateNetwork()
|
||||||
|
// Mock doesn't track modification dates, so return all games
|
||||||
return games.filter { game in
|
// (In production, CloudKit filters by modificationDate)
|
||||||
sports.contains(game.sport) &&
|
return games.map { game in
|
||||||
game.dateTime >= startDate &&
|
|
||||||
game.dateTime <= endDate
|
|
||||||
}.map { game in
|
|
||||||
CloudKitService.SyncGame(
|
CloudKitService.SyncGame(
|
||||||
game: game,
|
game: game,
|
||||||
canonicalId: game.id,
|
canonicalId: game.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user