Say what the save exemption does not cover, rather than implying it is total

The note claimed `save` is left unguarded because nothing can overwrite what
it writes. That half is true -- the only observation that could belongs to a
job already in a terminal state. The other half was missing: a save whose
copy is still in flight when the user taps Start over lands `Saved` on a
screen they have just cleared.

Guarding it would drop that write instead, which reports nothing for a file
that may genuinely have reached the destination. That is a question about
what the screen should offer during a save, and answering it in a race fix
would be deciding it by accident.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-25 22:36:23 -05:00
co-authored by Claude Opus 5
parent cc424dd08f
commit 3599307040
2 changed files with 15 additions and 8 deletions
@@ -198,11 +198,17 @@ class ConversionViewModel @JvmOverloads constructor(
* cancelling the superseded coroutine is not one.
*
* Every write below that lands after a suspension point is guarded by it: the two in
* [onInputPicked] and the one in [observe]. [save] is the one deferred write that is not, and
* deliberately: it is only reachable from [ConversionState.Converted] or a
* [ConversionState.Failed] carrying its file, so the only observation that could overwrite
* what it writes belongs to a job that has already reached a terminal state and will not emit
* again.
* [onInputPicked] and the one in [observe].
*
* [save] is the one left out, deliberately — and not because it is safe in both directions.
* Nothing can overwrite what it writes: it is reachable only from [ConversionState.Converted]
* or a [ConversionState.Failed] carrying its file, so the only observation that could belongs
* to a job already in a terminal state, which will not emit again. What it can still do is
* land on top of a [reset] taken while its copy was in flight, putting `Saved` on a screen the
* user has just cleared. Guarding it would drop that write instead, reporting nothing for a
* file that may genuinely have reached the user's destination. Which of those two is right is
* a question about what the screen should offer during a save, not about this race, and it is
* left open rather than answered in passing.
*/
private val ownership = ScreenOwnership()
@@ -107,9 +107,10 @@ class JoinViewModel @JvmOverloads constructor(
* The convert side had issue #49 reported against it four times in two days; this side has the
* identical shape and was never reported, because nothing was watching. Every write below that
* lands after a suspension point is guarded: the one in [onInputsPicked] and the one in
* [observe]. [save] is the deferred write that is not, deliberately and for the reason
* `ConversionViewModel` writes out -- it is reachable only from a job that has already reached
* a terminal state and will not emit again.
* [observe]. [save] is the one left out, deliberately and with the same limit its counterpart
* in `ConversionViewModel` spells out: nothing can overwrite what it writes, but it can still
* land on top of a [reset] taken while its copy was in flight, and which way that should go is
* a question about the save screen rather than about this race.
*/
private val ownership = ScreenOwnership()