Files
SideStore/Sources/SideStoreCore/Model/MergePolicy.swift
2023-03-10 19:30:09 -05:00

100 lines
4.9 KiB
Swift

//
// MergePolicy.swift
// AltStore
//
// Created by Riley Testut on 7/23/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
import Roxas
open class MergePolicy: RSTRelationshipPreservingMergePolicy {
override open func resolve(constraintConflicts conflicts: [NSConstraintConflict]) throws {
guard conflicts.allSatisfy({ $0.databaseObject != nil }) else {
for conflict in conflicts {
switch conflict.conflictingObjects.first {
case is StoreApp where conflict.conflictingObjects.count == 2:
// Modified cached StoreApp while replacing it with new one, causing context-level conflict.
// Most likely, we set up a relationship between the new StoreApp and a NewsItem,
// causing cached StoreApp to delete it's NewsItem relationship, resulting in (resolvable) conflict.
if let previousApp = conflict.conflictingObjects.first(where: { !$0.isInserted }) as? StoreApp {
// Delete previous permissions (same as below).
for permission in previousApp.permissions {
permission.managedObjectContext?.delete(permission)
}
// Delete previous versions (different than below).
for case let appVersion as AppVersion in previousApp._versions where appVersion.app == nil {
appVersion.managedObjectContext?.delete(appVersion)
}
}
case is AppVersion where conflict.conflictingObjects.count == 2:
// Occurs first time fetching sources after migrating from pre-AppVersion database model.
let conflictingAppVersions = conflict.conflictingObjects.lazy.compactMap { $0 as? AppVersion }
// Primary AppVersion == AppVersion whose latestVersionApp.latestVersion points back to itself.
if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestVersionApp?.latestVersion == $0 }),
let secondaryAppVersion = conflictingAppVersions.first(where: { $0 != primaryAppVersion }) {
secondaryAppVersion.managedObjectContext?.delete(secondaryAppVersion)
print("[ALTLog] Resolving AppVersion context-level conflict. Most likely due to migrating from pre-AppVersion model version.", primaryAppVersion)
}
default:
// Unknown context-level conflict.
assertionFailure("MergePolicy is only intended to work with database-level conflicts.")
}
}
try super.resolve(constraintConflicts: conflicts)
return
}
for conflict in conflicts {
switch conflict.databaseObject {
case let databaseObject as StoreApp:
// Delete previous permissions
for permission in databaseObject.permissions {
permission.managedObjectContext?.delete(permission)
}
if let contextApp = conflict.conflictingObjects.first as? StoreApp {
let contextVersions = Set(contextApp._versions.lazy.compactMap { $0 as? AppVersion }.map { $0.version })
for case let appVersion as AppVersion in databaseObject._versions where !contextVersions.contains(appVersion.version) {
print("[ALTLog] Deleting cached app version: \(appVersion.appBundleID + "_" + appVersion.version), not in:", contextApp.versions.map { $0.appBundleID + "_" + $0.version })
appVersion.managedObjectContext?.delete(appVersion)
}
}
case let databaseObject as Source:
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }
let bundleIdentifiers = Set(conflictedObject.apps.map { $0.bundleIdentifier })
let newsItemIdentifiers = Set(conflictedObject.newsItems.map { $0.identifier })
for app in databaseObject.apps {
if !bundleIdentifiers.contains(app.bundleIdentifier) {
// No longer listed in Source, so remove it from database.
app.managedObjectContext?.delete(app)
}
}
for newsItem in databaseObject.newsItems {
if !newsItemIdentifiers.contains(newsItem.identifier) {
// No longer listed in Source, so remove it from database.
newsItem.managedObjectContext?.delete(newsItem)
}
}
default: break
}
}
try super.resolve(constraintConflicts: conflicts)
}
}