chore: commit all pending changes
This commit is contained in:
@@ -114,17 +114,33 @@ actor BootstrapService {
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// Bootstrap canonical data from bundled JSON if not already done.
|
||||
/// Bootstrap canonical data from bundled JSON if not already done,
|
||||
/// or re-bootstrap if the bundled data schema version has been bumped.
|
||||
/// This is the main entry point called at app launch.
|
||||
@MainActor
|
||||
func bootstrapIfNeeded(context: ModelContext) async throws {
|
||||
let syncState = SyncState.current(in: context)
|
||||
let hasCoreCanonicalData = hasRequiredCanonicalData(context: context)
|
||||
|
||||
// Skip if already bootstrapped
|
||||
// Re-bootstrap if bundled data version is newer (e.g., updated game schedules)
|
||||
let needsRebootstrap = syncState.bootstrapCompleted && syncState.bundledSchemaVersion < SchemaVersion.current
|
||||
if needsRebootstrap {
|
||||
syncState.bootstrapCompleted = false
|
||||
}
|
||||
|
||||
// Recover from corrupted/partial local stores where bootstrap flag is true but core tables are empty.
|
||||
if syncState.bootstrapCompleted && !hasCoreCanonicalData {
|
||||
syncState.bootstrapCompleted = false
|
||||
}
|
||||
|
||||
// Skip if already bootstrapped with current schema
|
||||
guard !syncState.bootstrapCompleted else {
|
||||
return
|
||||
}
|
||||
|
||||
// Fresh bootstrap should always force a full CloudKit sync baseline.
|
||||
resetSyncProgress(syncState)
|
||||
|
||||
// Clear any partial bootstrap data from a previous failed attempt
|
||||
try clearCanonicalData(context: context)
|
||||
|
||||
@@ -156,6 +172,30 @@ actor BootstrapService {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func resetSyncProgress(_ syncState: SyncState) {
|
||||
syncState.lastSuccessfulSync = nil
|
||||
syncState.lastSyncAttempt = nil
|
||||
syncState.lastSyncError = nil
|
||||
syncState.syncInProgress = false
|
||||
syncState.syncEnabled = true
|
||||
syncState.syncPausedReason = nil
|
||||
syncState.consecutiveFailures = 0
|
||||
|
||||
syncState.stadiumChangeToken = nil
|
||||
syncState.teamChangeToken = nil
|
||||
syncState.gameChangeToken = nil
|
||||
syncState.leagueChangeToken = nil
|
||||
|
||||
syncState.lastStadiumSync = nil
|
||||
syncState.lastTeamSync = nil
|
||||
syncState.lastGameSync = nil
|
||||
syncState.lastLeagueStructureSync = nil
|
||||
syncState.lastTeamAliasSync = nil
|
||||
syncState.lastStadiumAliasSync = nil
|
||||
syncState.lastSportSync = nil
|
||||
}
|
||||
|
||||
// MARK: - Bootstrap Steps
|
||||
|
||||
@MainActor
|
||||
@@ -188,7 +228,7 @@ actor BootstrapService {
|
||||
capacity: jsonStadium.capacity,
|
||||
yearOpened: jsonStadium.year_opened,
|
||||
imageURL: jsonStadium.image_url,
|
||||
sport: jsonStadium.sport,
|
||||
sport: jsonStadium.sport.uppercased(),
|
||||
timezoneIdentifier: jsonStadium.timezone_identifier
|
||||
)
|
||||
context.insert(canonical)
|
||||
@@ -265,7 +305,7 @@ actor BootstrapService {
|
||||
|
||||
let model = LeagueStructureModel(
|
||||
id: structure.id,
|
||||
sport: structure.sport,
|
||||
sport: structure.sport.uppercased(),
|
||||
structureType: structureType,
|
||||
name: structure.name,
|
||||
abbreviation: structure.abbreviation,
|
||||
@@ -302,7 +342,7 @@ actor BootstrapService {
|
||||
source: .bundled,
|
||||
name: jsonTeam.name,
|
||||
abbreviation: jsonTeam.abbreviation,
|
||||
sport: jsonTeam.sport,
|
||||
sport: jsonTeam.sport.uppercased(),
|
||||
city: jsonTeam.city,
|
||||
stadiumCanonicalId: jsonTeam.stadium_canonical_id,
|
||||
conferenceId: jsonTeam.conference_id,
|
||||
@@ -371,6 +411,8 @@ actor BootstrapService {
|
||||
}
|
||||
|
||||
var seenGameIds = Set<String>()
|
||||
let teams = (try? context.fetch(FetchDescriptor<CanonicalTeam>())) ?? []
|
||||
let stadiumByTeamId = Dictionary(uniqueKeysWithValues: teams.map { ($0.canonicalId, $0.stadiumCanonicalId) })
|
||||
|
||||
for jsonGame in games {
|
||||
// Deduplicate
|
||||
@@ -389,6 +431,21 @@ actor BootstrapService {
|
||||
|
||||
guard let dateTime else { continue }
|
||||
|
||||
let explicitStadium = jsonGame.stadium_canonical_id?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let resolvedStadiumCanonicalId: String
|
||||
if let explicitStadium, !explicitStadium.isEmpty {
|
||||
resolvedStadiumCanonicalId = explicitStadium
|
||||
} else if let homeStadium = stadiumByTeamId[jsonGame.home_team_canonical_id],
|
||||
!homeStadium.isEmpty {
|
||||
resolvedStadiumCanonicalId = homeStadium
|
||||
} else if let awayStadium = stadiumByTeamId[jsonGame.away_team_canonical_id],
|
||||
!awayStadium.isEmpty {
|
||||
resolvedStadiumCanonicalId = awayStadium
|
||||
} else {
|
||||
resolvedStadiumCanonicalId = "stadium_placeholder_\(jsonGame.canonical_id)"
|
||||
}
|
||||
|
||||
let game = CanonicalGame(
|
||||
canonicalId: jsonGame.canonical_id,
|
||||
schemaVersion: SchemaVersion.current,
|
||||
@@ -396,9 +453,9 @@ actor BootstrapService {
|
||||
source: .bundled,
|
||||
homeTeamCanonicalId: jsonGame.home_team_canonical_id,
|
||||
awayTeamCanonicalId: jsonGame.away_team_canonical_id,
|
||||
stadiumCanonicalId: jsonGame.stadium_canonical_id ?? "",
|
||||
stadiumCanonicalId: resolvedStadiumCanonicalId,
|
||||
dateTime: dateTime,
|
||||
sport: jsonGame.sport,
|
||||
sport: jsonGame.sport.uppercased(),
|
||||
season: jsonGame.season,
|
||||
isPlayoff: jsonGame.is_playoff,
|
||||
broadcastInfo: jsonGame.broadcast_info
|
||||
@@ -454,6 +511,26 @@ actor BootstrapService {
|
||||
try context.delete(model: CanonicalSport.self)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func hasRequiredCanonicalData(context: ModelContext) -> Bool {
|
||||
let stadiumCount = (try? context.fetchCount(
|
||||
FetchDescriptor<CanonicalStadium>(
|
||||
predicate: #Predicate { $0.deprecatedAt == nil }
|
||||
)
|
||||
)) ?? 0
|
||||
let teamCount = (try? context.fetchCount(
|
||||
FetchDescriptor<CanonicalTeam>(
|
||||
predicate: #Predicate { $0.deprecatedAt == nil }
|
||||
)
|
||||
)) ?? 0
|
||||
let gameCount = (try? context.fetchCount(
|
||||
FetchDescriptor<CanonicalGame>(
|
||||
predicate: #Predicate { $0.deprecatedAt == nil }
|
||||
)
|
||||
)) ?? 0
|
||||
return stadiumCount > 0 && teamCount > 0 && gameCount > 0
|
||||
}
|
||||
|
||||
nonisolated private func parseISO8601(_ string: String) -> Date? {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime]
|
||||
|
||||
Reference in New Issue
Block a user