mirror of
https://github.com/yattee/yattee.git
synced 2026-11-01 01:17:20 +00:00
Sync accounts through CloudKit records
A login created after the revamp got a random account UUID that never left the device, so its credential was keyed to an ID no other device had and the login was unusable there. Migrated accounts were fine only because their ID is deterministically the instance ID. Accounts, the permanent legacy-migration markers, the permanent deletion tombstones and the selected data context now ride the existing CKSyncEngine as four record types, with the rules the category needs: - Absence proves nothing. A fetch resumes from a change token and never enumerates the zone, so deletions are records of their own and they dominate the account wherever they are seen — including the engine's retry-as-insert path, which is how a queued offline edit would otherwise resurrect a deleted account. - Fetched records are staged durably before each delegate event returns and applied as a set, tombstones first, gated on the zone's own result rather than didFetchChanges, which fires after failed fetches too. - Nothing activates for an identity that has not bootstrapped: a device left on the previous release consumed these record types as unknown and advanced its token past them, so each identity fetches the zone once from a nil token, outside the engine's own token. - A disabled category defers records rather than discarding them; every persisted artifact is namespaced by identity, and async work is generation-checked so a fetch begun under one Apple account can never land in another's state. - Uploads are ordered: a marker never outruns the account it describes, and a context naming an account is never published before that account is server-confirmed.
This commit is contained in:
@@ -52,6 +52,7 @@ final class AppEnvironment {
|
||||
/// resolves the scope for all of them.
|
||||
let credentialIdentityScope: CredentialIdentityScope
|
||||
let accountsManager: AccountsManager
|
||||
let accountSyncCoordinator: AccountSyncCoordinator
|
||||
let homeInstanceCache: HomeInstanceCache
|
||||
let invidiousAPI: InvidiousAPI
|
||||
let pipedAPI: PipedAPI
|
||||
@@ -186,6 +187,17 @@ final class AppEnvironment {
|
||||
self.cloudKitSync = cloudKit
|
||||
dm.cloudKitSync = cloudKit
|
||||
|
||||
// Accounts sync as CloudKit records rather than through KVS: they need
|
||||
// per-record conflict resolution and durable deletions, neither of
|
||||
// which a single overwritten key-value document can provide.
|
||||
let accountSync = AccountSyncCoordinator(
|
||||
accountsManager: accounts,
|
||||
identityScope: identityScope,
|
||||
isCategoryEnabled: { [weak cloudKit] in cloudKit?.canSyncAccounts ?? false }
|
||||
)
|
||||
self.accountSyncCoordinator = accountSync
|
||||
cloudKit.accountSync = accountSync
|
||||
|
||||
// Initialize DeArrow with low-priority networking
|
||||
let lowPrioritySession = URLSessionFactory.shared.lowPrioritySession()
|
||||
let deArrowHTTPClient = HTTPClient(session: lowPrioritySession)
|
||||
@@ -367,6 +379,16 @@ final class AppEnvironment {
|
||||
subscriptionService.setContextValidator(dataContextValidator)
|
||||
subscriptionAccountValidator.setAccountsManager(accounts)
|
||||
|
||||
// A deletion that arrives from another device takes the credential with
|
||||
// it. Only a deletion does this — an account merely missing from a
|
||||
// fetch keeps everything, because a fetch is not an enumeration.
|
||||
accountSync.onAccountDeleted = { [weak self] account in
|
||||
guard let self else { return }
|
||||
let credentials = self.accountCredentialsManager(for: account)
|
||||
credentials?.deleteCredential(for: account)
|
||||
credentials?.deleteLegacyCredentialIfOwned(for: account)
|
||||
}
|
||||
|
||||
// Statuses are cleared whenever the identity namespace reloads, so they
|
||||
// have to be recomputed once the new scope's Keychain is readable —
|
||||
// otherwise a synced account shows "sign in required" until relaunch.
|
||||
|
||||
Reference in New Issue
Block a user