--- id: ML-169.10 title: "Fix: Async optimistic UI for index/list delete streams" status: To Do assignee: [] created_date: "2026-05-19 11:48" updated_date: "2026-05-20 09:41" labels: - perf - fix - ux dependencies: [] references: - >- backlog/documents/doc-27 - Phase-4-Performance-Audit-Low-Hanging-Fruit-Findings.md modified_files: - lib/music_library_web/live_helpers/index_actions.ex - lib/music_library_web/live/collection_live/index.ex - lib/music_library_web/live/wishlist_live/index.ex - lib/music_library_web/live/scrobbled_tracks_live/index.ex parent_task_id: ML-169 priority: medium ordinal: 31000 --- ## Description Add optimistic UI for delete operations only where the item is rendered from a LiveView stream on an index/list page. The item should be removed from the stream immediately, while the database delete runs through LiveView-owned async handling so failures can be reported and rolled back. ## Problem Index/list delete handlers currently wait for the database delete before updating the UI. `stream_delete` is used after the delete succeeds, so the interaction can feel slower than necessary. This task is intentionally scoped to stream-backed list views only: - Collection index and wishlist index record streams via `IndexActions.handle_delete/2` - Scrobbled tracks index stream Show-page deletes are out of scope. `CollectionLive.Show` and `WishlistLive.Show` navigate away after delete and do not have a list stream to optimistically update. Their failure and rollback behavior should remain synchronous unless a separate navigation-specific design is created. ## Files - `lib/music_library_web/live_helpers/index_actions.ex` — remove record from `:records` immediately and start a LiveView async delete operation - `lib/music_library_web/live/collection_live/index.ex` — handle async record delete success/failure for collection index - `lib/music_library_web/live/wishlist_live/index.ex` — handle async record delete success/failure for wishlist index - `lib/music_library_web/live/scrobbled_tracks_live/index.ex` — remove track from `:tracks` immediately and handle async track delete success/failure ## Fix Use LiveView-owned async handling (`start_async/3` plus `handle_async/3`, or an equivalent pattern that sends results back to the LiveView process). Do not use bare `Task.start/1`, because it cannot update the socket, reinsert the stream item, or show an error toast. Expected behavior: 1. Fetch the item needed for rollback. 2. Immediately call `stream_delete/3` on the relevant stream. 3. Start an async delete operation owned by the LiveView. 4. On success, leave the stream as-is and optionally broadcast/refresh any cross-view state the existing design requires. 5. On failure or async exit, reinsert the original item into the stream and show a user-facing error toast. For error text, use the project’s existing user-facing error conventions (`ErrorMessages.friendly_message/1` where an error reason is shown). ## Acceptance Criteria - [ ] #1 Index/list delete operations remove the item from the relevant stream before waiting for the database delete - [ ] #2 Database delete still executes through LiveView-owned async handling, not bare `Task.start/1` - [ ] #3 Async delete failure or exit reinserts the original item into the stream and shows an error toast - [ ] #4 Existing delete confirmation prompts still work correctly - [ ] #5 Show-page delete handlers are not changed as part of this task - [ ] #6 Collection and wishlist index delete behavior remains shared through `IndexActions` where practical