From 760c44ccb5cd133c752864b40d77b4aca5b0d39b Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Wed, 20 May 2026 10:42:35 +0100 Subject: [PATCH] ML-169.10: rescope to list pages and use async handling --- ...ing-optimistic-UI-for-delete-operations.md | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/backlog/tasks/ml-169.10 - Fix-Missing-optimistic-UI-for-delete-operations.md b/backlog/tasks/ml-169.10 - Fix-Missing-optimistic-UI-for-delete-operations.md index d0384df7..9223b9d9 100644 --- a/backlog/tasks/ml-169.10 - Fix-Missing-optimistic-UI-for-delete-operations.md +++ b/backlog/tasks/ml-169.10 - Fix-Missing-optimistic-UI-for-delete-operations.md @@ -1,9 +1,10 @@ --- id: ML-169.10 -title: "Fix: Missing optimistic UI for delete operations" +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 @@ -15,7 +16,8 @@ references: 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/show.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 @@ -26,34 +28,39 @@ ordinal: 31000 -Add optimistic UI for delete operations so the UI removes items immediately without waiting for the database transaction. +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 -All delete operations wait for the database transaction to complete before updating the UI. `stream_delete` is already used in some places, but the pattern is inconsistent. CollectionLive.Show deletes don't use `stream_delete` at all (they navigate away). +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:94-98` — `handle_delete` -- `lib/music_library_web/live/collection_live/show.ex:381-385` — `handle_event("delete")` -- `lib/music_library_web/live/scrobbled_tracks_live/index.ex:305-310` — `handle_event("delete")` - -## Impact - -Adds unnecessary latency to delete operations that could feel instant. While delete failures are rare, the benefit of snappy UI is worth the minimal risk. +- `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 `stream_delete` immediately, then perform the DB operation async. If DB fails, re-insert the item and show error toast. +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. -```elixir -def handle_delete(socket, id) do - record = Records.get_record!(id) - socket = stream_delete(socket, :records, record) - Task.start(fn -> Records.delete_record(record) end) - {:noreply, socket} -end -``` +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). @@ -61,9 +68,10 @@ end -- [ ] #1 Delete operations remove items from UI immediately (no DB wait) -- [ ] #2 Database delete still executes successfully in background -- [ ] #3 If DB delete fails, item is re-inserted into stream and error toast shown -- [ ] #4 Existing delete confirmation modal still works correctly -- [ ] #5 No orphaned UI state after failed delete +- [ ] #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