Found while reviewing #386, but the hazard predates it.
Mechanism
modalContext.existingAnnotation on LocalizeAlertPage is derived from the react-query cache, and saveDetection.onSuccess only invalidates — the value stays null until the refetch lands. saveDetectionReview branches create-vs-patch on it, so two commits in quick succession on a frame with no annotation yet both take the POST branch. The second POST hits uq_detection_annotation_detection_id and the annotator sees a "Failed to save frame — try again" toast for a save that actually succeeded.
Nothing debounces or disables between the two writes: useMutation.mutate has no in-flight guard and isSaving only feeds the "Saving…" label.
Triggers
Any two fast commits on an undecided frame: double-press of Delete/Backspace (auto-repeat is already dropped, a deliberate double press is not), two quick candidate clicks on the rail, and since #386 a double-press of P.
Possible directions
- An in-flight guard on the editor's commit path (drop commits while one is pending), or
- upsert semantics on the single-annotation POST (the bulk endpoint already guards this case), or
- an optimistic cache write so the second commit sees the annotation and PATCHes.
Found while reviewing #386, but the hazard predates it.
Mechanism
modalContext.existingAnnotationonLocalizeAlertPageis derived from the react-query cache, andsaveDetection.onSuccessonly invalidates — the value staysnulluntil the refetch lands.saveDetectionReviewbranches create-vs-patch on it, so two commits in quick succession on a frame with no annotation yet both take the POST branch. The second POST hitsuq_detection_annotation_detection_idand the annotator sees a "Failed to save frame — try again" toast for a save that actually succeeded.Nothing debounces or disables between the two writes:
useMutation.mutatehas no in-flight guard andisSavingonly feeds the "Saving…" label.Triggers
Any two fast commits on an undecided frame: double-press of
Delete/Backspace(auto-repeat is already dropped, a deliberate double press is not), two quick candidate clicks on the rail, and since #386 a double-press ofP.Possible directions