Delete declarations that Periphery reports unused on every platform that
compiles them (iOS, macOS, tvOS), including nine views used only by their
own previews. Platform-gated code still used on another platform, device-
only MPV render paths, migration models, CloudKit record names, App
Intents metadata and unused parameters are kept.
Both were DEBUG-only Developer settings entries used while building
the accounts revamp and are no longer needed. The simulator's
account-sync suspension barrier in AccountSyncCoordinator existed only
to keep invented scopes away from the real zone, so it goes with it.
A repair is a fallback, not a choice: it borrows the timestamp of the
selection it replaced so that a later choice still beats it, and it is
never published. But that borrowed rank also won arguments it had no
business winning. An incoming explicit context stamped at the same instant
that loses the `DataContext.tieBreakKey` tie was neither adopted — the
repair was not outranked — nor pushed back, because a repair pushes
nothing. Neither side moved, and the fallback stood against the server's
explicit value indefinitely. The tie is not theoretical: migrated contexts
are all stamped at the epoch, and a repair keeps the timestamp of whatever
it replaced, so two migrated-era values meet as equals.
A stored repair now never outranks an incoming context — it is replaced by
whatever arrived, older or tied, and nothing is ever enqueued for that.
Adopting is safe whatever the incoming value names: one naming an account a
tombstone dominates has already been turned back into a repair before the
comparison, so it stays unpublished and lands on the same target this
device's repair did.
Two more from the same trace:
- Staged records are applied by category, and contexts already come last so
that a repair reads the accounts and the instance a dominated record left
behind rather than falling to local past a signed-in sibling. That
ordering was load-bearing and unremarked; it is now written down, along
with what it does not cover — a context applied in an earlier batch than
the account record that says where the deleted account lived still
repairs without it, which the next explicit choice anywhere settles.
- An install that repaired its context before the repair bit existed reads
back as having chosen it, so that one repair can still be published once.
Accepted and documented at the load site: no shipped release ever
persisted a data context, the window is interim builds only, and the next
explicit choice heals it.
A repair carries the timestamp of the selection it replaced, which is what
stops it outranking a deliberate choice made elsewhere — but it also makes
every ordering comparison read it as that choice. A fetch holding the
selected account's tombstone and an older server context applied them in
that order: the tombstone repaired the selection, and the older context
then lost to it, so the "the server holds the stale copy" path marked the
repair dirty and uploaded an automatic fallback under the deleted choice's
timestamp, over an explicit choice. The stored context now remembers
whether it is a repair, and a repair that wins an ordering pushes nothing
back: the server's context stands remotely, the fallback stays local, and
the next explicit choice on any device re-converges everyone. The bit is
persisted with the context, or a relaunch would forget and publish it on
the first stale record it read.
Three more from the same trace:
- The repair prefers a signed-in login on the same server, decided off
statuses, and it commits — once the selection no longer names the deleted
account there is nothing left for a later status to move. Foreground
activation refreshes statuses ahead of it, but a deletion delivered by a
silent push arrives outside that sequence, and a credential that reached
this device over iCloud Keychain while it was in the background announces
nothing. The refresh is now asked for by the repair itself, which closes
every entry point rather than each fetch trigger having to remember one.
- A device that meets a tombstone before the account it kills cannot record
which instance that account lived on, and the account record arriving
afterwards was discarded — with the only copy of that fact still in it.
Every later repair of a context naming it then fell to local past a
usable sibling. The association is kept as the record is dropped.
- Removing an orphaned account deleted its credential before recording the
deletion, so a crash in between brought the account back on the next
launch as one nobody can sign into, owing no deletion to any device. The
outbox goes first, here and in the sign-out and instance cascades; the
scoped credential key needs only the account's ID, and the store is
resolved through the instance before anything is removed.
The accounts.unresolved.* UserDefaults namespace was found on a real
device holding a stale, contradictory snapshot: a tombstoned account
listed live, context reset to local, every sync outbox still pending.
With iCloud Drive off the identity scope dips to unresolved on every
foreground activation, so that snapshot was loaded — and rewritten —
in an entirely ordinary configuration.
Close it off on four fronts:
- Gate the unprompted writers. reconcileLegacyAccounts() and
migrateLegacyContextIfNeeded() now no-op while the identity scope is
unresolved; reloadForCurrentIdentity() re-runs both once a scope
resolves, so nothing is lost, and nothing synthesizes accounts or
queues uploads under a namespace that answers to no one.
- Fence the reload gap. Between a scope change and this manager's
observer reloading, namespace already names the new identity while
memory still holds the old snapshot. loadedNamespace records what
memory belongs to; save(), the stored-context flag, and every
coordinator path that builds, enqueues, applies or confirms
account-category records refuses when it mismatches. Staged
downloads stay durable and re-apply after the reload.
- Tie confirmations and conflict retries to the identity that sent
them. Every outbound account-category record carries an
identityNamespace field; didSave honours only confirmations whose
record carries the current identity's stamp, conflict resolutions
inherit the local record's stamp and are dropped when foreign, and
a skipped confirmation re-registers the durable outbox so nothing
strands. The abandoning conflict branches re-register it too.
- Purge the stale blob. Once a scope resolves, the unresolved
namespace keys and the unresolved credential-fingerprint store are
removed; the activation-time validators no longer correct the
context or rewrite the legacy selection from unresolved state.
Hardened through four adversarial review rounds; the last found
nothing.
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.
An additional login could be created but never selected: the legacy
picker lists one entry per instance and always resolved to whichever
account signed in first. Account rows gain a "Use This Account" action,
and the subscriptions view reloads on the data context rather than on
the legacy setting — two logins on the same instance map onto the same
legacy value, so switching between them changed nothing on screen while
writes went to the other account.
Two devices migrating different legacy selections produced contexts with
identical epoch timestamps, and each refused the other's: a permanent,
silent divergence that both sides considered settled. Equal timestamps
are now broken by the selected account's ID, on the incoming side and in
the conflict resolver alike — a record that wins the upload must not be
refused on arrival.
Credential cleanup falls back to both managers whenever the backend
cannot be resolved, not only when the account is unknown here: an
instance deletion can arrive before the tombstone, and the credential
was then left in the Keychain.
Signing in wrote `usesAccountLogin`, which syncs through key-value
storage independently of the account records. A device receiving that
flag before the account synthesized a migrated account for a login that
was never legacy — and the marker arriving later deliberately retracts
nothing, so the credential-less duplicate was permanent and uploaded.
The flag is pre-revamp state that migration only reads; accounts
describe logins now. `needsCredentials` reads the account list for any
instance that has one, so the badge it drives is unaffected.
A tombstone now dominates contexts as well as accounts: an offline
device can select an account another device deleted and publish that
choice with a newer timestamp, which would have left every device
pointing at an account that is never coming back.
Cached private feeds survived an Apple-account switch two ways. A scope
this device has seen before resolves while `CredentialIdentityScope` is
still being constructed, so the change notification fires before any
observer exists — startup now compares the persisted cache owner. And a
purge asked for before the disk cache is read was undone by the load,
which now carries it.
The need to re-bootstrap after dropping records for want of a resolved
identity is persisted. The engine may already have moved its token past
those records, so a launch that lost the in-memory flag would neither
replay nor bootstrap them.
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.