feat(sync): add pagination, cancellation, and network restoration

- CloudKit pagination: fetchAllRecords() handles >400 record batches
  with cursor-based pagination (400 records per page)
- Cancellation support: SyncCancellationToken protocol enables graceful
  sync termination when background tasks expire
- Per-entity progress: SyncState now tracks timestamps per entity type
  so interrupted syncs resume where they left off
- NetworkMonitor: NWPathMonitor integration triggers sync on network
  restoration with 2.5s debounce to handle WiFi↔cellular flapping
- wasCancelled flag in SyncResult distinguishes partial from full syncs

This addresses critical data sync issues:
- CloudKit queries were limited to ~400 records but bundled data has ~5000 games
- Background tasks could be killed mid-sync without saving progress
- App had no awareness of network state changes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-13 19:18:55 -06:00
parent 00b33202f5
commit 5686af262f
8 changed files with 1607 additions and 94 deletions

View File

@@ -152,13 +152,19 @@ struct BootstrappedContentView: View {
await StoreManager.shared.loadProducts()
await StoreManager.shared.updateEntitlements()
// 6. App is now usable
// 6. Start network monitoring and wire up sync callback
NetworkMonitor.shared.onSyncNeeded = {
await BackgroundSyncManager.shared.triggerSyncFromNetworkRestoration()
}
NetworkMonitor.shared.startMonitoring()
// 7. App is now usable
isBootstrapping = false
// 7. Schedule background tasks for future syncs
// 8. Schedule background tasks for future syncs
BackgroundSyncManager.shared.scheduleAllTasks()
// 8. Background: Try to refresh from CloudKit (non-blocking)
// 9. Background: Try to refresh from CloudKit (non-blocking)
Task.detached(priority: .background) {
await self.performBackgroundSync(context: context)
await MainActor.run {