Fix #1038: cancel in-flight chapter-video downloads on delete/replace#1073
Fix #1038: cancel in-flight chapter-video downloads on delete/replace#1073sampjvv wants to merge 4 commits into
Conversation
…placed A chapter-video "Get/Save video" download fetched the whole LFS object and wrote it to files/ unconditionally, with no AbortController (the progress was even marked cancellable: false). Deleting or replacing the video mid-download didn't stop the fetch, so when it finished it wrote the bytes back — resurrecting the file the user just deleted. - videoDownloadUtils: the in-flight op tracker now holds an AbortController per video; beginVideoOperation returns it and a new abortVideoOperation() cancels it. Both downloaders take a signal, pass it to downloadLFSFile, and guard the write with `signal.aborted` so an aborted op never writes bytes back — even if the downloader ignores the signal. - deleteVideoFile and the metadata replace path abort the op before deleting. - The download progress notification is now cancellable and wired to abort. Scope: the streamed <video> path is left as-is — it self-limits (the browser stops buffering and releases on its own), so it has no equivalent background load to leak. Tests: 6 unit (cancellation contract) + 3 integration driving the real downloadVideoToProject against a temp workspace + LFS pointer with a mocked, abortable downloadLFSFile — asserting a deleted file is not rewritten even when the downloader ignores the signal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LeviXIII
left a comment
There was a problem hiding this comment.
There are a couple of places and scenarios where the cancel button is missing. If we try to save or load the video to the project from pressing the Show Video button, the cancel button is missing in that flow. There is also the scenario where the video is missing. If we try to download it, it will try to retrieve it, but we should be able to cancel from there as well.
Review follow-up: the "Show Video" save/load flow and the missing-video
"Download & play" button both post downloadVideoFile, whose progress toast
was cancellable: false with no abort signal — the one video download left
without a cancel button.
- downloadVideoFile: cancellable progress; the fetch is registered via
beginVideoOperation so a cancel OR a delete/replace mid-download aborts
it (previously the delete-abort only covered the sidebar flow); signal
threaded into downloadLFSFile; cancel resets the player quietly
("Download cancelled." + Retry) instead of an error toast.
- Post-write abort check in all three write sites (handler + both nav
utils): a cancel that lands while the verified bytes are being WRITTEN
(a large video's write takes real time; the pre-write check can't see
it) now reverts files/ to the exact pre-download pointer stub (or
deletes it if none existed) and drops the session-cache entry. The
check is the last statement inside the progress callback, so any
cancel that can be clicked is honored — no window where a cancel is
silently ignored.
- Tests: two deterministic write-race cases (abort fires during the fs
write) — stub restored byte-identical / files/ removed when no stub
pre-existed. 11/11 videoDownloadUtils tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@LeviXIII Both missing-cancel scenarios are fixed in f69fe30. They turned out to share one handler: the Show Video → "Save to project"/"Load for session" buttons and the missing-video "Download & play" button all post
Verified in the app: stream-only → Show Video → Save to project, and stream-and-save → Download & play — both show Cancel; cancelling mid-download leaves |
Closes #1038.
Problem
While a chapter video is downloading ("Get video → Load/Save to project"), deleting or replacing it doesn't stop the download. The fetch runs to completion in the background and writes the bytes to
files/, resurrecting the video the user just deleted.Root cause
downloadVideoToProject/downloadVideoToSessionCache(videoDownloadUtils.ts) fetch the whole LFS object viaauthApi.downloadLFSFilewith noAbortController, thenwriteFileunconditionally. The in-flight tracker was a plainSet(no way to cancel), the delete/replace handlers never signalled it, and the download progress wascancellable: false.Fix
Map<string, AbortController>.beginVideoOperationreturns the controller; newabortVideoOperation()cancels it.signal, pass it todownloadLFSFile(its type already accepts one), and guard the write withsignal.aborted— so even if the downloader ignores the signal, the deleted file is never rewritten.deleteVideoFileand the metadata replace path callabortVideoOperation(...)before removing the file.Scope decision
The streamed
<video>path is intentionally unchanged. It self-limits — the browser buffers a chunk, goes idle, and releases on GC — so there's no equivalent background-load leak to fix there. (Verified by instrumenting the element'snetworkState/bufferedlive.)Verification
downloadVideoToProjectagainst a temp workspace + LFS pointer with a mocked, abortabledownloadLFSFile:genesis-ai-dev/frontier-authentication):downloadLFSFileforwards theAbortSignalthroughdownloadLFSObject(refcounted) into the underlyingfetch, so the network request is genuinely cancelled — a shared object download stops once the last waiter aborts.tsc --noEmit+eslintclean. The targetedvideoDownloadUtilssuite is 9/9.🤖 Generated with Claude Code