Reference solution
The findings below are ordered by priority.
Findings
1. Removing an item does not cancel its upload
Location: src/progressive-image-uploader.tsx:28-44
The AbortController exists only inside upload, so remove cannot reach it. A removed item continues consuming bandwidth and can still call complete, publishing an upload the user explicitly removed.
Store each active controller by stable item ID, abort it before removal, and guard progress and completion commits against the item no longer existing.
2. Every selected file starts a PUT immediately
Location: src/progressive-image-uploader.tsx:13-23
forEach launches the entire batch without a shared scheduler. Large or repeated selections can create unbounded concurrent storage requests instead of the global limit of three.
Feed item IDs into a queue or semaphore shared by all batches and release a slot in finally.
3. Progress and completion mutate stale state in place
Location: src/progressive-image-uploader.tsx:13-23,30-40
The code mutates item and passes the closed-over items array back to its setter. React can bail out on the same reference, later additions can be overwritten, and concurrent item callbacks update a snapshot that no longer represents the list.
Use functional state updates that map the current list and immutably replace only the matching ID.
4. Object preview URLs are never revoked
Location: src/progressive-image-uploader.tsx:13-19,41-45
Each selection allocates a browser object URL, but removal does not revoke it and there is no unmount cleanup. Repeated batches retain the underlying blobs for the page lifetime.
Revoke a removed item's URL exactly once and add cleanup that revokes every still-owned URL on unmount without revoking previews merely because state rerendered.
5. Upload failures escape and leave misleading item state
Location: src/progressive-image-uploader.tsx:26-39
Failures from credential creation, PUT, or completion reject the fire-and-forget promise with no handler. The item remains marked uploading and other lifecycle cleanup has no reliable finally path.
Catch expected non-abort failures inside the task, immutably mark an existing item failed, and always release controller and concurrency resources in finally.
Reasonable non-findings
- Decorative preview images may use empty alternative text because the adjacent upload item is the operative UI.
- Stable random item IDs are suitable React keys.
- Creating a fresh credential for each newly queued upload is appropriate.