audit-g2: async race / state-leak fixes across eight stores #50

Merged
bvandeusen merged 1 commits from dev into main 2026-06-02 14:17:13 -04:00
Owner

Summary

Group 2 of the 2026-06-02 multi-system drift audit. Extracts gallery.js's hand-rolled inflightId pattern into a new useInflightToken composable; adopts it across every store that previously had no guard against late-response overwrites or wrong-image URL interpolation.

Two operator-impacting bugs the audit flagged:

  • modal tag mutations routed to the wrong image. removeTag rolled back the chip rail unconditionally even when only the secondary dismiss POST had failed — UI lied until refresh. And all tag-mutation URLs interpolated currentImageId.value AFTER an await, so a fast prev/next could route DELETE/POST to the wrong image. Fixed with imageId-at-click-time capture in reloadTags / removeTag / addExistingTag / createAndAdd, plus a split try/catch so a dismiss failure surfaces a warning but doesn't roll back the successful delete.

  • suggestions accept could apply A's chosen tag to image B. accept() dereferenced currentImageId after the awaited POST /api/tags, so the subsequent /suggestions/accept URL went to whatever image the user had navigated to mid-flight — and that tag got pushed to B's allowlist. Fixed with imageId capture + inflight guard on load().

Same shape across the remaining stores — rapid filter / nav changes used to interleave responses (last-writer-wins, not request-order-wins):

  • artist.js — rapid artist-to-artist navigation no longer briefly renders the previous artist's overview/images
  • downloads.js — filter changes cancel in-flight loadFirst/loadMore so a stale filter's response can't overwrite the new view
  • artistDirectory.js + tagDirectory.js — typing "alice" then "alice bob" no longer drops the second fetch (was: if (loading) return early-exited the second call because loading was still true from the first)
  • posts.js — loadInitial / loadMore / loadAround / loadOlder / loadNewer all guarded; filter changes cancel in-flight
  • gallery.js — refactored from its hand-rolled inflightId to the shared composable so the pattern stays consistent

Test Plan

  • Open an image with suggestions, click Accept on a suggestion, immediately press Right Arrow — confirm the accepted tag landed on the first image, not the second
  • Open an image with several tags, click a chip's × to remove, immediately press Right Arrow — confirm the chip was removed from the first image
  • Browse to an artist with many images, immediately click another artist — confirm the second artist's content replaces, no flash of the first
  • In the artist/tag directory search input, type quickly: "al" → "alice" → "alice bob" — confirm the final query's results render, not an earlier fetch
  • In Downloads, change the status filter rapidly between "error" and "ok" — confirm the displayed events match the latest filter, not whichever request resolved last

Audit reference

Closes Group 2 of plan task Scribe #551. Group 3 (lifecycle batch: recovery sweeps + retention for BackupRun / LibraryAuditRun / ImportBatch, time_limits on long-runners) next.

🤖 Generated with Claude Code

## Summary Group 2 of the 2026-06-02 multi-system drift audit. Extracts `gallery.js`'s hand-rolled `inflightId` pattern into a new `useInflightToken` composable; adopts it across every store that previously had no guard against late-response overwrites or wrong-image URL interpolation. **Two operator-impacting bugs the audit flagged:** - **modal tag mutations** routed to the wrong image. `removeTag` rolled back the chip rail unconditionally even when only the secondary `dismiss` POST had failed — UI lied until refresh. And all tag-mutation URLs interpolated `currentImageId.value` AFTER an await, so a fast prev/next could route DELETE/POST to the wrong image. Fixed with imageId-at-click-time capture in `reloadTags` / `removeTag` / `addExistingTag` / `createAndAdd`, plus a split `try/catch` so a dismiss failure surfaces a warning but doesn't roll back the successful delete. - **suggestions accept** could apply A's chosen tag to image B. `accept()` dereferenced `currentImageId` after the awaited `POST /api/tags`, so the subsequent `/suggestions/accept` URL went to whatever image the user had navigated to mid-flight — and that tag got pushed to B's allowlist. Fixed with imageId capture + inflight guard on `load()`. **Same shape across the remaining stores** — rapid filter / nav changes used to interleave responses (last-writer-wins, not request-order-wins): - `artist.js` — rapid artist-to-artist navigation no longer briefly renders the previous artist's overview/images - `downloads.js` — filter changes cancel in-flight loadFirst/loadMore so a stale filter's response can't overwrite the new view - `artistDirectory.js` + `tagDirectory.js` — typing "alice" then "alice bob" no longer drops the second fetch (was: `if (loading) return` early-exited the second call because loading was still true from the first) - `posts.js` — loadInitial / loadMore / loadAround / loadOlder / loadNewer all guarded; filter changes cancel in-flight - `gallery.js` — refactored from its hand-rolled `inflightId` to the shared composable so the pattern stays consistent ## Test Plan - [ ] Open an image with suggestions, click Accept on a suggestion, immediately press Right Arrow — confirm the accepted tag landed on the **first** image, not the second - [ ] Open an image with several tags, click a chip's × to remove, immediately press Right Arrow — confirm the chip was removed from the **first** image - [ ] Browse to an artist with many images, immediately click another artist — confirm the second artist's content replaces, no flash of the first - [ ] In the artist/tag directory search input, type quickly: "al" → "alice" → "alice bob" — confirm the final query's results render, not an earlier fetch - [ ] In Downloads, change the status filter rapidly between "error" and "ok" — confirm the displayed events match the **latest** filter, not whichever request resolved last ## Audit reference Closes Group 2 of plan task `Scribe #551`. Group 3 (lifecycle batch: recovery sweeps + retention for BackupRun / LibraryAuditRun / ImportBatch, time_limits on long-runners) next. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bvandeusen added 1 commit 2026-06-02 14:17:07 -04:00
fix(audit-g2): async race / state-leak across eight stores
CI / lint (push) Successful in 2s
CI / backend-lint-and-test (push) Successful in 21s
CI / frontend-build (push) Successful in 34s
CI / intimp (push) Successful in 3m31s
CI / intapi (push) Successful in 7m25s
CI / intcore (push) Successful in 8m2s
e66987f092
Extracts gallery.js's hand-rolled inflightId pattern into a new
useInflightToken composable; adopts in every store that previously
had no guard against late-response overwrites or wrong-image URL
interpolation.

Two operator-impacting bugs the audit (workflow wf_bbe3fdb1-e62)
flagged:

- modal.removeTag rolled back the chip rail unconditionally even
  when only the secondary dismiss POST had failed — UI lied until
  refresh. And all tag-mutation URLs interpolated currentImageId
  AFTER an await, so a fast prev/next could route DELETE/POST to
  the wrong image. Both fixed: split try/catch (dismiss failure
  surfaces a warning, doesn't roll back the delete); imageId
  captured at call-time and used in URLs throughout.

- suggestions.accept dereferenced currentImageId after the awaited
  POST /api/tags, so the subsequent /suggestions/accept could
  apply A's chosen tag to image B AND push it to B's allowlist.
  Fixed by capturing imageId at click-time + inflight guard on
  load().

Same shape across artist / downloads / artistDirectory /
tagDirectory / posts stores: rapid filter/nav changes used to
interleave responses (last-writer-wins). Now the late response is
discarded and the most-recent request wins. Filter-change-during-
search no longer drops the second fetch because the loading flag
was still true from the first.

gallery.js's inflightId removed in favor of the shared composable
so the pattern stays consistent.
bvandeusen merged commit 9c27a2d3c7 into main 2026-06-02 14:17:13 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/FabledCurator#50