mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 21:22:53 +00:00
Let the newer channel notification toggle win its conflict
This commit is contained in:
@@ -333,6 +333,37 @@ actor CloudKitConflictResolver {
|
||||
return resolved
|
||||
}
|
||||
|
||||
// MARK: - ChannelNotificationSettings Conflict Resolution
|
||||
|
||||
/// Resolves a conflict between local and server channel notification settings.
|
||||
///
|
||||
/// Strategy:
|
||||
/// - Last write wins based on updatedAt timestamp — the same rule
|
||||
/// `DataManager.upsertChannelNotificationSettings` applies on arrival, so a
|
||||
/// record that wins the upload is never refused by the other device.
|
||||
/// - Preserve server recordChangeTag
|
||||
func resolveChannelNotificationSettingsConflict(
|
||||
local: CKRecord,
|
||||
server: CKRecord
|
||||
) -> CKRecord {
|
||||
let localUpdated = (local["updatedAt"] as? Date) ?? Date.distantPast
|
||||
let serverUpdated = (server["updatedAt"] as? Date) ?? Date.distantPast
|
||||
|
||||
// IMPORTANT: Always start with server record to preserve recordChangeTag
|
||||
let resolved = server
|
||||
|
||||
// If local is newer, copy its fields to server record
|
||||
if localUpdated > serverUpdated {
|
||||
resolved["notificationsEnabled"] = local["notificationsEnabled"]
|
||||
resolved["updatedAt"] = local["updatedAt"]
|
||||
resolved["sourceRawValue"] = local["sourceRawValue"]
|
||||
resolved["instanceURLString"] = local["instanceURLString"]
|
||||
resolved["globalProvider"] = local["globalProvider"]
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
// MARK: - Account Conflict Resolution
|
||||
|
||||
/// Resolves a conflict between local and server account records.
|
||||
|
||||
@@ -2112,6 +2112,8 @@ extension CloudKitSyncEngine: CKSyncEngineDelegate {
|
||||
return await conflictResolver.resolveRecentPlaylistConflict(local: local, server: server)
|
||||
case RecordType.controlsPreset:
|
||||
return await conflictResolver.resolveLayoutPresetConflict(local: local, server: server)
|
||||
case RecordType.channelNotificationSettings:
|
||||
return await conflictResolver.resolveChannelNotificationSettingsConflict(local: local, server: server)
|
||||
case RecordType.account, RecordType.dataContext,
|
||||
RecordType.legacyAccountMigration, RecordType.accountDeletion:
|
||||
guard let accountSync else { return server }
|
||||
|
||||
@@ -40,6 +40,22 @@ struct CloudKitSyncTests {
|
||||
return entry
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func makeNotificationSettings(
|
||||
channelID: String = "channel1",
|
||||
notificationsEnabled: Bool = false,
|
||||
updatedAt: Date = Date(timeIntervalSince1970: 1_000)
|
||||
) -> ChannelNotificationSettings {
|
||||
let settings = ChannelNotificationSettings(
|
||||
channelID: channelID,
|
||||
notificationsEnabled: notificationsEnabled,
|
||||
sourceRawValue: "global",
|
||||
globalProvider: "youtube"
|
||||
)
|
||||
settings.updatedAt = updatedAt
|
||||
return settings
|
||||
}
|
||||
|
||||
// MARK: - Record Mapper
|
||||
|
||||
@Suite("Record Mapper")
|
||||
@@ -138,4 +154,67 @@ struct CloudKitSyncTests {
|
||||
#expect(resolved["watchedSeconds"] as? Double == 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Channel Notification Settings Conflicts")
|
||||
struct ChannelNotificationSettingsConflictTests {
|
||||
@Test("Newer local toggle wins over the older server value")
|
||||
@MainActor
|
||||
func newerLocalToggleWins() async throws {
|
||||
let mapper = CloudKitSyncTests.makeMapper()
|
||||
let local = mapper.toCKRecord(channelNotificationSettings: CloudKitSyncTests.makeNotificationSettings(
|
||||
notificationsEnabled: false,
|
||||
updatedAt: Date(timeIntervalSince1970: 2_000)
|
||||
))
|
||||
let server = mapper.toCKRecord(channelNotificationSettings: CloudKitSyncTests.makeNotificationSettings(
|
||||
notificationsEnabled: true,
|
||||
updatedAt: Date(timeIntervalSince1970: 1_000)
|
||||
))
|
||||
|
||||
let resolver = CloudKitConflictResolver()
|
||||
let resolved = await resolver.resolveChannelNotificationSettingsConflict(local: local, server: server)
|
||||
|
||||
#expect(resolved["notificationsEnabled"] as? Int64 == 0)
|
||||
#expect(resolved["updatedAt"] as? Date == Date(timeIntervalSince1970: 2_000))
|
||||
}
|
||||
|
||||
@Test("Newer server toggle keeps its value")
|
||||
@MainActor
|
||||
func newerServerToggleWins() async throws {
|
||||
let mapper = CloudKitSyncTests.makeMapper()
|
||||
let local = mapper.toCKRecord(channelNotificationSettings: CloudKitSyncTests.makeNotificationSettings(
|
||||
notificationsEnabled: false,
|
||||
updatedAt: Date(timeIntervalSince1970: 1_000)
|
||||
))
|
||||
let server = mapper.toCKRecord(channelNotificationSettings: CloudKitSyncTests.makeNotificationSettings(
|
||||
notificationsEnabled: true,
|
||||
updatedAt: Date(timeIntervalSince1970: 2_000)
|
||||
))
|
||||
|
||||
let resolver = CloudKitConflictResolver()
|
||||
let resolved = await resolver.resolveChannelNotificationSettingsConflict(local: local, server: server)
|
||||
|
||||
#expect(resolved["notificationsEnabled"] as? Int64 == 1)
|
||||
#expect(resolved["updatedAt"] as? Date == Date(timeIntervalSince1970: 2_000))
|
||||
}
|
||||
|
||||
@Test("A local record that only repeats the server value leaves it untouched")
|
||||
@MainActor
|
||||
func equalTimestampsKeepServerValue() async throws {
|
||||
let mapper = CloudKitSyncTests.makeMapper()
|
||||
let stamp = Date(timeIntervalSince1970: 1_000)
|
||||
let local = mapper.toCKRecord(channelNotificationSettings: CloudKitSyncTests.makeNotificationSettings(
|
||||
notificationsEnabled: false,
|
||||
updatedAt: stamp
|
||||
))
|
||||
let server = mapper.toCKRecord(channelNotificationSettings: CloudKitSyncTests.makeNotificationSettings(
|
||||
notificationsEnabled: true,
|
||||
updatedAt: stamp
|
||||
))
|
||||
|
||||
let resolver = CloudKitConflictResolver()
|
||||
let resolved = await resolver.resolveChannelNotificationSettingsConflict(local: local, server: server)
|
||||
|
||||
#expect(resolved["notificationsEnabled"] as? Int64 == 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user