Add StadiumAlias CloudKit sync and offline-first data architecture
- Add CKStadiumAlias model for CloudKit record mapping - Add fetchStadiumAliases/fetchStadiumAliasChanges to CloudKitService - Add syncStadiumAliases to CanonicalSyncService for delta sync - Add subscribeToStadiumAliasUpdates for push notifications - Update cloudkit_import.py with --stadium-aliases-only option Data Architecture Updates: - Remove obsolete provider files (CanonicalDataProvider, CloudKitDataProvider, StubDataProvider) - AppDataProvider now reads exclusively from SwiftData - Add background CloudKit sync on app startup (non-blocking) - Document data architecture in CLAUDE.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -42,12 +42,13 @@ actor CanonicalSyncService {
|
||||
let gamesUpdated: Int
|
||||
let leagueStructuresUpdated: Int
|
||||
let teamAliasesUpdated: Int
|
||||
let stadiumAliasesUpdated: Int
|
||||
let skippedIncompatible: Int
|
||||
let skippedOlder: Int
|
||||
let duration: TimeInterval
|
||||
|
||||
var totalUpdated: Int {
|
||||
stadiumsUpdated + teamsUpdated + gamesUpdated + leagueStructuresUpdated + teamAliasesUpdated
|
||||
stadiumsUpdated + teamsUpdated + gamesUpdated + leagueStructuresUpdated + teamAliasesUpdated + stadiumAliasesUpdated
|
||||
}
|
||||
|
||||
var isEmpty: Bool { totalUpdated == 0 }
|
||||
@@ -81,7 +82,7 @@ actor CanonicalSyncService {
|
||||
guard syncState.syncEnabled else {
|
||||
return SyncResult(
|
||||
stadiumsUpdated: 0, teamsUpdated: 0, gamesUpdated: 0,
|
||||
leagueStructuresUpdated: 0, teamAliasesUpdated: 0,
|
||||
leagueStructuresUpdated: 0, teamAliasesUpdated: 0, stadiumAliasesUpdated: 0,
|
||||
skippedIncompatible: 0, skippedOlder: 0,
|
||||
duration: 0
|
||||
)
|
||||
@@ -101,6 +102,7 @@ actor CanonicalSyncService {
|
||||
var totalGames = 0
|
||||
var totalLeagueStructures = 0
|
||||
var totalTeamAliases = 0
|
||||
var totalStadiumAliases = 0
|
||||
var totalSkippedIncompatible = 0
|
||||
var totalSkippedOlder = 0
|
||||
|
||||
@@ -138,13 +140,21 @@ actor CanonicalSyncService {
|
||||
totalSkippedIncompatible += skipIncompat4
|
||||
totalSkippedOlder += skipOlder4
|
||||
|
||||
let (games, skipIncompat5, skipOlder5) = try await syncGames(
|
||||
let (stadiumAliases, skipIncompat5, skipOlder5) = try await syncStadiumAliases(
|
||||
context: context,
|
||||
since: syncState.lastSuccessfulSync
|
||||
)
|
||||
totalStadiumAliases = stadiumAliases
|
||||
totalSkippedIncompatible += skipIncompat5
|
||||
totalSkippedOlder += skipOlder5
|
||||
|
||||
let (games, skipIncompat6, skipOlder6) = try await syncGames(
|
||||
context: context,
|
||||
since: syncState.lastSuccessfulSync
|
||||
)
|
||||
totalGames = games
|
||||
totalSkippedIncompatible += skipIncompat5
|
||||
totalSkippedOlder += skipOlder5
|
||||
totalSkippedIncompatible += skipIncompat6
|
||||
totalSkippedOlder += skipOlder6
|
||||
|
||||
// Mark sync successful
|
||||
syncState.syncInProgress = false
|
||||
@@ -176,6 +186,7 @@ actor CanonicalSyncService {
|
||||
gamesUpdated: totalGames,
|
||||
leagueStructuresUpdated: totalLeagueStructures,
|
||||
teamAliasesUpdated: totalTeamAliases,
|
||||
stadiumAliasesUpdated: totalStadiumAliases,
|
||||
skippedIncompatible: totalSkippedIncompatible,
|
||||
skippedOlder: totalSkippedOlder,
|
||||
duration: Date().timeIntervalSince(startTime)
|
||||
@@ -346,6 +357,30 @@ actor CanonicalSyncService {
|
||||
return (updated, skippedIncompatible, skippedOlder)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func syncStadiumAliases(
|
||||
context: ModelContext,
|
||||
since lastSync: Date?
|
||||
) async throws -> (updated: Int, skippedIncompatible: Int, skippedOlder: Int) {
|
||||
let remoteAliases = try await cloudKitService.fetchStadiumAliasChanges(since: lastSync)
|
||||
|
||||
var updated = 0
|
||||
var skippedIncompatible = 0
|
||||
var skippedOlder = 0
|
||||
|
||||
for remoteAlias in remoteAliases {
|
||||
let result = try mergeStadiumAlias(remoteAlias, context: context)
|
||||
|
||||
switch result {
|
||||
case .applied: updated += 1
|
||||
case .skippedIncompatible: skippedIncompatible += 1
|
||||
case .skippedOlder: skippedOlder += 1
|
||||
}
|
||||
}
|
||||
|
||||
return (updated, skippedIncompatible, skippedOlder)
|
||||
}
|
||||
|
||||
// MARK: - Merge Logic
|
||||
|
||||
private enum MergeResult {
|
||||
@@ -631,4 +666,41 @@ actor CanonicalSyncService {
|
||||
return .applied
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func mergeStadiumAlias(
|
||||
_ remote: StadiumAlias,
|
||||
context: ModelContext
|
||||
) throws -> MergeResult {
|
||||
// Schema version check
|
||||
guard remote.schemaVersion <= SchemaVersion.current else {
|
||||
return .skippedIncompatible
|
||||
}
|
||||
|
||||
let remoteAliasName = remote.aliasName
|
||||
let descriptor = FetchDescriptor<StadiumAlias>(
|
||||
predicate: #Predicate { $0.aliasName == remoteAliasName }
|
||||
)
|
||||
let existing = try context.fetch(descriptor).first
|
||||
|
||||
if let existing = existing {
|
||||
// lastModified check
|
||||
guard remote.lastModified > existing.lastModified else {
|
||||
return .skippedOlder
|
||||
}
|
||||
|
||||
// Update all fields (no user fields on StadiumAlias)
|
||||
existing.stadiumCanonicalId = remote.stadiumCanonicalId
|
||||
existing.validFrom = remote.validFrom
|
||||
existing.validUntil = remote.validUntil
|
||||
existing.schemaVersion = remote.schemaVersion
|
||||
existing.lastModified = remote.lastModified
|
||||
|
||||
return .applied
|
||||
} else {
|
||||
// Insert new
|
||||
context.insert(remote)
|
||||
return .applied
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user