Settle account ties and stop discarding caches on every launch

Two devices editing an account in the same instant — or migrating one,
since every migrated account is stamped at the epoch — each kept its own
label and considered the matter closed. Equal timestamps are now broken
by the label, on the incoming side and in the conflict resolver alike,
the same rule the context already uses.

Cached feeds were thrown away on every ordinary launch: recomputing
statuses posts a notification, and the observer ran before the value it
compares against had been seeded. The seed now happens first.

Cache ownership is checked once at launch and compared against a
recorded owner afterwards. An unresolved identity at launch means the
owner cannot be established at all — a switch to an unseen Apple account
while offline looks exactly like that — so the caches are discarded;
mid-session it is a transient, and with iCloud Drive off it happens on
every activation, so it no longer clears anything.

Whether a Keychain item syncs is fixed when it is written, so a login
stored while sync was off stayed on this device while its account record
travelled, leaving every other device with an account whose credential
never arrives. Turning sync on rewrites them.

Reseeding a deleted zone marks the account records dirty even while the
category is off, so re-enabling it restores them instead of finding an
empty outbox.
This commit is contained in:
Arkadiusz Fal
2026-08-26 23:53:29 +02:00
parent d9244cf0d7
commit b37ca40d37
11 changed files with 158 additions and 43 deletions

View File

@@ -267,6 +267,27 @@ struct AccountSyncTests {
#expect(try mapper.toStoredDataContext(from: serverWins).context == .account(accountID))
}
@Test("Two accounts edited in the same instant converge")
func equalAccountTimestampsBreakTheTieTheSameWayEverywhere() async throws {
let mapper = AccountSyncTests.makeMapper()
let resolver = CloudKitConflictResolver()
let id = UUID()
let instanceID = UUID()
let sameMoment = Date(timeIntervalSince1970: 500)
let lower = Account(id: id, instanceID: instanceID, displayName: "A", updatedAt: sameMoment)
let higher = Account(id: id, instanceID: instanceID, displayName: "B", updatedAt: sameMoment)
#expect(lower.isOutranked(by: higher))
#expect(!higher.isOutranked(by: lower))
let resolved = await resolver.resolveAccountConflict(
local: mapper.toCKRecord(account: higher),
server: mapper.toCKRecord(account: lower)
)
#expect(try mapper.toAccount(from: resolved).displayName == "B")
}
@Test("Two migrated contexts converge instead of diverging")
func equalTimestampsBreakTheTieTheSameWayEverywhere() async throws {
let mapper = AccountSyncTests.makeMapper()