feat(storage): support pause and resume for downloads#990
Open
grdsdev wants to merge 6 commits into
Open
Conversation
Contributor
Author
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
grdsdev
commented
May 7, 2026
ffd5ec5 to
a6f3159
Compare
Adds `DownloadEngine` — an actor-based state machine that manages URLSessionDownloadTask lifecycle with pause/resume/cancel support. When the server returns `Accept-Ranges: bytes`, pausing captures resume data via `cancel(byProducingResumeData:)` so the next `resume()` call restarts from the last received byte. If the server does not support range requests the download restarts from byte 0. Changes: - New `DownloadEngine` actor with states: idle, starting, downloading, paused, completed, failed, cancelled - `DownloadSessionDelegate` refactored to callback-based routing (`DownloadTaskCallbacks`) instead of holding continuations directly; temp-file move is now done synchronously inside the delegate method so URLSession cannot reclaim the file before the move completes - `StorageFileAPI.download()` now wires through `DownloadEngine.makeTask` and documents pause/resume behavior - `StorageTransferTask.pause()` / `resume()` docs updated to cover downloads alongside TUS uploads - New `DownloadEngineTests` (Darwin-only) covering basic download, pause-then-resume, cancel-yields-cancelled-error, and network-error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lude downloads Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers happy path, pause/resume cycles, cancel semantics, error handling, and concurrent download scenarios using a live Supabase instance via the existing withUploadedFile helper pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Task-identity race in DownloadEngine: after pause() cancels the active URLSessionDownloadTask (ID=X) and resume() starts a new one (ID=Y), the stale URLError.cancelled from X could arrive while state is .downloading(Y) and incorrectly call cancel(). Fix: capture the task identifier at callback-registration time and guard every engine callback against a mismatch with the current active task. 2. HTTP error responses delivered as completed downloads: URLSession calls didFinishDownloadingTo for 4xx/5xx responses too, saving the error JSON as the "downloaded" file. Fix: check the HTTP status code in the delegate and parse the Supabase error envelope into a StorageError, delivering .failed instead of .success. Also adds StorageError.from(httpResponse:data:) as an internal factory shared by the delegate, and removes LockIsolated (unavailable in IntegrationTests target) from the multi-pause-cycle test in favour of a local actor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
a6f3159 to
6b62a19
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
DownloadEngine— an actor-based state machine (idle → starting → downloading → paused → completed/failed/cancelled) that owns theURLSessionDownloadTasklifecyclepause()callscancel(byProducingResumeData:)and awaits the callback so resume data is captured synchronously before returning;resume()usesdownloadTask(withResumeData:)when available, otherwise restarts from byte 0DownloadSessionDelegaterefactored from continuation-holding to callback-based routing (DownloadTaskCallbacks); temp-file move now happens synchronously inside the delegate method before URLSession reclaims the fileStorageTransferTask.pause()/resume()docs updated to describe download behaviour alongside TUS uploadsDesign notes
Why
pause()isasync:cancel(byProducingResumeData:)delivers resume data via a callback. Makingpause()async lets usawait withCheckedContinuationso the data is stored on the actor before the method returns — no extraTask {}or intermediate state needed.Temp-file safety: URLSession deletes the temp file passed to
didFinishDownloadingTowhen that delegate method returns. The old design moved the file inside a continuation yield (safe). The new callback design moves it synchronously inside the delegate method before invokingonFinished(Result<URL, StorageError>), so the engine only ever sees the already-moved URL.didCompleteWithErrorfiltering: AURLError.cancelledarriving while state is.pausedor.startingis intentional (fromcancel(byProducingResumeData:)or a stale cancel afterresume()) and is silently dropped. Only.downloadingstate treats an unexpected.cancelledas a real failure.Test plan
DownloadEngineTests.downloadCompletesSuccessfully— basic happy pathDownloadEngineTests.pauseAndResumeCompletesDownload— pause a hanging download, resume, verify completionDownloadEngineTests.cancelYieldsCancelledError— cancel mid-download, verify.failed(.cancelled)eventDownloadEngineTests.cancelThrowsFromValue— cancel mid-download, verifytask.valuethrowsStorageError.cancelledDownloadEngineTests.networkErrorYieldsFailedEvent— network error surfaces as.failedDownloadSessionDelegateTests— delegate routing still correct after refactorStorageDownloadTaskTests— end-to-end download API unchanged🤖 Generated with Claude Code