Files
yattee/Yattee/Services/CloudKit
Arkadiusz Fal b67b3472f6 Let a purge be answered once and a re-seed be answered until it sticks
The two answers to a deleted zone were sharing one lifecycle, and only one of
them deserved it. A re-seed only adds, so it is held open until the engine has
actually serialized the work and redone for as long as it has not. A purge
removes, and replaying it removes more than it was asked to — the account added
while the app kept running, the category switched back on, the reset that runs
its own upload afterwards. It is done the moment its durable half is, and the
comment that called both idempotent now says which is which.

The note itself was never scoped to an identity. A marker written under one
Apple account replayed against the next one's namespace, answering a wipe that
never happened there, and a purge would have taken that account's staged records
and its outbox with it. The account change forgets it now, the way it forgets
everything else it holds.

Retiring the marker trusted whichever state update arrived next, which is not
the same as one that carries the work: an update handed over before the recovery
registered anything can still reach the main actor after it, describing a state
in which none of it exists. Deliveries are counted where the engine hands them
over, and only a later one can retire what was armed.

A refused re-seed waited for the next launch. The accounts it could not speak
for announce themselves when they load, and the recovery is answered then —
outside the callback tree that announcement comes from.

A purge left two ways back for what it dropped: a pending zone save would have
recreated the zone on the next send, and the pre-engine pending-change keys
would have been rediscovered on a later launch. It clears both, and it now runs
after everything that puts work into the engine rather than before.

The account half fences before it destroys, like its upload twin, so a purge
arriving during the identity reload gap answers with nothing rather than with
somebody else's records.

And the reset that promises a re-upload is refused while sync is off, where
nothing would have performed one.

Re-entering the engine from inside its own callback is what started all of this,
and nothing in the code said so. A task-local marks the callback tree and the
two entry points assert on it in debug builds — a plain Task inherits it, which
is the mistake; Task.detached escapes it, which is the fix.
2026-09-09 23:59:59 +02:00
..
2026-04-18 20:37:24 +02:00