Skip to content

S3 multipart uploads up to 30 GiB#33

Open
bmdavis419 wants to merge 5 commits into
mainfrom
davis/bigger-uploads
Open

S3 multipart uploads up to 30 GiB#33
bmdavis419 wants to merge 5 commits into
mainfrom
davis/bigger-uploads

Conversation

@bmdavis419

@bmdavis419 bmdavis419 commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Raise the per-file upload cap to 30 GiB using S3 multipart uploads for files over 5 GiB, while keeping the existing single-PUT path for smaller files.
  • Add Convex actions to initiate, sign, complete, and abort multipart uploads, plus server-side resume via ListParts.
  • Add a client upload engine with parallel part uploads, IndexedDB resume sessions, and upload progress UI support for resuming.

Test plan

  • Upload a video under 5 GiB and confirm playback still works
  • Upload a video between 5–30 GiB and confirm multipart progress and Mux processing
  • Cancel mid-upload and confirm S3 multipart is aborted
  • Interrupt a large upload, reload, and confirm resume skips completed parts
  • Reject a file over 30 GiB with a clear error
  • Verify storage quota errors still surface at create/reconcile time

Made with Cursor

Note

Add S3 multipart upload support for video files up to 30 GiB

  • Replaces the single PUT upload flow with a multi-strategy engine supporting single PUT (≤5 GiB) and S3 multipart uploads (up to 30 GiB), implemented in videoUpload.ts and coordinated via new server actions in videoActions.ts.
  • Adds resume support: a file fingerprint is computed before upload, and any matching multipart session stored in IndexedDB (via uploadResumeDb.ts) is resumed by listing already-uploaded parts from S3.
  • Uploads parts concurrently using XHR, with progress metrics (bytes/sec, estimated time remaining) streamed back to the UI via callbacks.
  • Cancelling an upload aborts the S3 multipart session server-side and clears the local resume state from IndexedDB.
  • Risk: The legacy getUploadUrl action now rejects files requiring multipart upload, which is a breaking change for any client still using that path for large files.

Macroscope summarized 6352c41.

Summary by CodeRabbit

  • New Features

    • Resumable multipart video uploads with persistent resume sessions so interrupted uploads continue without re-uploading completed parts.
    • UI shows “Resuming” on resumed uploads plus live progress, upload speed and ETA.
  • Improvements

    • Immediate feedback for oversized files and enforced upload size limits.
    • Safer cancellation and cleanup that removes stale resume state and better distinguishes cancelled vs failed uploads.

Review Change Stack

Large files use chunked S3 uploads while smaller files keep the single-PUT path, with IndexedDB-backed resume for interrupted transfers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented May 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lawn Ready Ready Preview, Comment May 21, 2026 12:58am

Request Review

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown

Walkthrough

Adds resumable S3 multipart video uploads: server Convex actions and S3 helpers, client upload orchestrator with resume and progress tracking, IndexedDB resume persistence, and UI wiring to show a "Resuming" state.

Changes

Resumable Multipart Video Uploads

Layer / File(s) Summary
Upload limits and configuration
convex/uploadLimits.ts, src/lib/uploadLimits.ts
Size constants (GiB/MiB, max file, single-put threshold, part size, batch limits) and validators (computePartCount, usesMultipartUpload) are centralized; client adds formatMaxUploadSize(), isFileTooLarge(), and buildFileFingerprint().
S3 multipart API layer
convex/s3Multipart.ts
Low-level S3 multipart helpers export UploadedPartInfo and functions to initiate sessions, presign part URLs, list uploaded parts (paginated), complete uploads, abort sessions, and compute part plans.
Server schema and upload actions
convex/schema.ts, convex/videoActions.ts, convex/videos.ts
videos schema adds optional s3MultipartUploadId; videoActions.ts implements initiateVideoUpload, signUploadParts, completeMultipartUpload, and abortVideoUpload, updates getUploadUrl/markUploadComplete for single-vs-multipart, and adds/clears multipart metadata via mutations.
Client-side resume session storage
src/lib/uploadResumeDb.ts
IndexedDB (lawn-upload-resume) persists MultipartUploadResumeSession records keyed by videoId with by_fingerprint index; exports saveUploadResumeSession, loadUploadResumeSession, deleteUploadResumeSession, findUploadResumeSessionByFingerprint.
Client-side upload orchestration
src/lib/videoUpload.ts
Exports uploadVideoFile and types; implements single PUT, multipart uploads with concurrent signed-part uploads and resume support, ETag normalization, progress/throughput/ETA reporting, per-part persistence, completion, and cleanup.
UI components and hook integration
src/components/upload/UploadProgress.tsx, app/routes/dashboard/-useVideoUploadManager.ts, app/routes/dashboard/-layout.tsx
UploadProgress gains optional resuming prop and appends "· Resuming" when true. useVideoUploadManager computes file fingerprints, detects/loads resume sessions, initializes uploads with resuming, delegates to uploadVideoFile with action bundle and abort signal, updates progress/throughput/ETA, and calls abortVideoUpload + deletes resume session on cancel. -layout.tsx passes resuming into UploadProgress.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Dashboard as Dashboard UI
  participant Hook as useVideoUploadManager
  participant ResumeDB as IndexedDB
  participant Upload as uploadVideoFile
  participant Server as Convex Server
  participant S3 as S3
  
  User->>Dashboard: Select file to upload
  Dashboard->>Hook: addFilesToProject(files)
  Hook->>Hook: Compute fingerprint
  Hook->>ResumeDB: findUploadResumeSessionByFingerprint
  alt Resume session found
    ResumeDB-->>Hook: Resume session {uploadId, completedParts, ...}
    Hook->>Hook: Set resuming: true
  else No resume session
    Hook->>Hook: Set resuming: false
  end
  Hook->>Upload: uploadVideoFile({file, resumeSession})
  Upload->>Server: initiateVideoUpload
  Server->>S3: Decide: single | multipart | already_uploaded
  alt strategy = multipart
    Server-->>Upload: {uploadId, parts: [], plan}
    Upload->>Upload: Merge completed parts from resume
    Upload->>Upload: Compute pending parts
    Upload->>ResumeDB: saveUploadResumeSession
    loop For each batch of pending parts
      Upload->>Server: signUploadParts({partNumbers})
      Server->>S3: PresignUploadPartCommand
      S3-->>Upload: Signed URLs
      Upload->>S3: PUT part blobs (concurrent)
      S3-->>Upload: ETags
      Upload->>Upload: Update completedParts
      Upload->>ResumeDB: saveUploadResumeSession (progress)
      Upload->>Hook: onProgress(update)
      Hook->>Dashboard: Display progress %, throughput, ETA
    end
    Upload->>Server: completeMultipartUpload({parts})
    Server->>S3: CompleteMultipartUploadCommand
    Upload->>ResumeDB: deleteUploadResumeSession
  else strategy = single
    Server-->>Upload: {url, expiration}
    Upload->>S3: PUT entire file
    S3-->>Upload: 200 OK
  else strategy = already_uploaded
    Upload->>ResumeDB: deleteUploadResumeSession
  end
  Upload->>Server: markUploadComplete
  Server-->>Upload: {success: true}
  Upload-->>Hook: Complete
  Hook->>Dashboard: Update upload to complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary change: implementing S3 multipart uploads to support files up to 30 GiB, which is the core feature added across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch davis/bigger-uploads

Comment @coderabbitai help to get the list of available commands and usage tips.

@bmdavis419 bmdavis419 marked this pull request as ready for review May 21, 2026 00:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
app/routes/dashboard/-useVideoUploadManager.ts (1)

152-171: 💤 Low value

Fragile cancellation detection relies on exact error message string.

Checking errorMessage === "Upload cancelled" is brittle. If uploadVideoFile changes its error message, cancellation handling breaks silently. Consider checking abortController.signal.aborted instead.

♻️ Suggested improvement
         } catch (error) {
           const errorMessage =
             error instanceof Error ? error.message : "Upload failed";
-          const cancelled = errorMessage === "Upload cancelled";
+          const cancelled = abortController.signal.aborted;

           setUploads((prev) =>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/routes/dashboard/-useVideoUploadManager.ts` around lines 152 - 171, The
catch block in the upload handler should not treat cancellation by comparing
error.message to "Upload cancelled"; instead detect cancellation via the
AbortSignal or standard AbortError: replace the fragile check `errorMessage ===
"Upload cancelled"` with a robust check like `abortController.signal.aborted`
(or `error.name === "AbortError"`) inside the same catch in
-useVideoUploadManager.ts, then keep the existing setUploads behavior (set
status to "pending" and remove the upload when cancelled) using the uploadId and
the existing setUploads calls; ensure you reference the same abortController
used when calling uploadVideoFile so the signal is available in this catch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@convex/s3Multipart.ts`:
- Around line 139-149: The current empty catch around s3.send(new
AbortMultipartUploadCommand(...)) swallows real failures and can leave orphaned
uploads; change the catch to capture the error (e.g. catch (err)), log the error
with context including BUCKET_NAME, args.key and args.uploadId (use the module's
logger or console.error), and rethrow or return a failure so callers know the
abort did not succeed (i.e. don't silently ignore errors from
AbortMultipartUploadCommand / s3.send).

In `@convex/videoActions.ts`:
- Around line 544-578: The code currently calls
ctx.runMutation(internal.videos.clearMultipartUploadId, { videoId: args.videoId
}) before validating the uploaded S3 object; move that call so it only runs
after successful metadata validation and reconciliation (i.e., after the
HeadObjectCommand checks, content length/type validation, and after
ctx.runMutation(internal.videos.reconcileUploadedObjectMetadata, ...)). Keep the
S3 metadata fetching (getS3Client + HeadObjectCommand), content length/type
checks (normalizeContentType, isAllowedUploadContentType) and the
reconcileUploadedObjectMetadata call as-is, and ensure clearMultipartUploadId is
invoked only on the successful path to preserve the multipart upload ID for
retries on transient failures.

In `@src/lib/uploadResumeDb.ts`:
- Around line 55-59: The saveUploadResumeSession currently lets IndexedDB write
failures propagate and block uploads; change it to be best-effort by wrapping
the runTransaction call inside a try/catch inside saveUploadResumeSession
(function name) so any error from runTransaction("readwrite", ...) is caught,
not rethrown, and the function returns normally; in the catch block log the
failure (e.g., console.warn or a logger) including the session id and the error
so we preserve observability while ensuring resume persistence failures do not
abort the multipart upload flow.

In `@src/lib/videoUpload.ts`:
- Around line 262-265: The code blindly merges resumeSession.completedParts into
the active upload (via mergeUploadedParts and initiate.uploadedParts), risking
stale ETags/part numbers corrupting the multipart state; add a compatibility
check (e.g., isResumeSessionCompatible or inline checks) that verifies the
resumeSession matches the current initiate context (compare uploadId/sessionId,
target bucket/key, file size or checksum, and any other identifying metadata)
and only pass resumeSession.completedParts into mergeUploadedParts when that
check passes; if it fails, ignore the resumeSession.completedParts (and
optionally log/warn) to avoid poisoning completedParts.

---

Nitpick comments:
In `@app/routes/dashboard/-useVideoUploadManager.ts`:
- Around line 152-171: The catch block in the upload handler should not treat
cancellation by comparing error.message to "Upload cancelled"; instead detect
cancellation via the AbortSignal or standard AbortError: replace the fragile
check `errorMessage === "Upload cancelled"` with a robust check like
`abortController.signal.aborted` (or `error.name === "AbortError"`) inside the
same catch in -useVideoUploadManager.ts, then keep the existing setUploads
behavior (set status to "pending" and remove the upload when cancelled) using
the uploadId and the existing setUploads calls; ensure you reference the same
abortController used when calling uploadVideoFile so the signal is available in
this catch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4a3939c0-1688-4cca-9bad-d395f5f06472

📥 Commits

Reviewing files that changed from the base of the PR and between 45792ad and 24ed03f.

📒 Files selected for processing (11)
  • app/routes/dashboard/-layout.tsx
  • app/routes/dashboard/-useVideoUploadManager.ts
  • convex/s3Multipart.ts
  • convex/schema.ts
  • convex/uploadLimits.ts
  • convex/videoActions.ts
  • convex/videos.ts
  • src/components/upload/UploadProgress.tsx
  • src/lib/uploadLimits.ts
  • src/lib/uploadResumeDb.ts
  • src/lib/videoUpload.ts

Comment thread convex/s3Multipart.ts
Comment thread convex/videoActions.ts Outdated
Comment thread src/lib/uploadResumeDb.ts
Comment thread src/lib/videoUpload.ts
bmdavis419 and others added 2 commits May 20, 2026 17:26
Presigned UploadPart URLs do not include Content-Type in the signature, so sending that header caused S3 to reject part PUTs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Propagate abort failures, clear multipart state after reconcile, make resume persistence best-effort, and validate resume session compatibility before merging parts.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread convex/videoActions.ts
Comment thread src/lib/uploadResumeDb.ts
Only apply the 20-part batch cap when signing URLs, not when completing uploads with hundreds of parts, and close IndexedDB on transaction errors.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
convex/videoActions.ts (2)

400-414: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle stale S3 multipart uploads gracefully.

If S3 has auto-aborted the multipart upload (e.g., after lifecycle expiry), listMultipartUploadedParts throws NoSuchUpload. Since canResumeMultipartUpload passed based on DB state, users get stuck with no recovery path—initiateVideoUpload keeps failing.

Consider catching NoSuchUpload here and falling through to create a fresh multipart session, clearing the stale s3MultipartUploadId in the process.

Proposed fix
     if (canResumeMultipartUpload(video, args.fileSize)) {
-      const uploadedParts = await listMultipartUploadedParts({
-        key: video.s3Key!,
-        uploadId: video.s3MultipartUploadId!,
-      });
-      const { partSizeBytes, partCount } = getMultipartPlan(args.fileSize);
-      return {
-        strategy: "multipart" as const,
-        key: video.s3Key!,
-        uploadId: video.s3MultipartUploadId!,
-        partSizeBytes,
-        partCount,
-        uploadedParts,
-      };
+      try {
+        const uploadedParts = await listMultipartUploadedParts({
+          key: video.s3Key!,
+          uploadId: video.s3MultipartUploadId!,
+        });
+        const { partSizeBytes, partCount } = getMultipartPlan(args.fileSize);
+        return {
+          strategy: "multipart" as const,
+          key: video.s3Key!,
+          uploadId: video.s3MultipartUploadId!,
+          partSizeBytes,
+          partCount,
+          uploadedParts,
+        };
+      } catch (error) {
+        // S3 multipart upload may have expired; clear stale state and create fresh session below.
+        await ctx.runMutation(internal.videos.clearMultipartUploadId, {
+          videoId: args.videoId,
+        });
+      }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@convex/videoActions.ts` around lines 400 - 414, When resuming multipart
uploads in the block that uses canResumeMultipartUpload(video, args.fileSize),
wrap the listMultipartUploadedParts call in a try/catch that specifically
handles the S3 NoSuchUpload error: on that error clear the stale
video.s3MultipartUploadId (persist the change back to the DB so the record no
longer points to a dead upload) and allow execution to fall through to the
normal multipart-initiation path (so initiateVideoUpload can create a fresh
multipart session); keep existing behavior for other errors and still return
uploadedParts when listing succeeds.

605-617: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear multipart state even when S3 abort fails with NoSuchUpload.

If the S3 multipart upload was already aborted or expired, abortMultipartUploadSession throws and clearMultipartUploadId never runs. This leaves the video record in a broken state that can't be recovered via the client.

Consider wrapping the S3 abort call and proceeding with DB cleanup regardless.

Proposed fix
   handler: async (ctx, args) => {
     await requireVideoMemberAccess(ctx, args.videoId);
     const video = await getVideoForUpload(ctx, args.videoId);

     if (video.s3Key && video.s3MultipartUploadId) {
-      await abortMultipartUploadSession({
-        key: video.s3Key,
-        uploadId: video.s3MultipartUploadId,
-      });
+      try {
+        await abortMultipartUploadSession({
+          key: video.s3Key,
+          uploadId: video.s3MultipartUploadId,
+        });
+      } catch {
+        // S3 upload may already be aborted/expired; proceed to clear DB state.
+      }
     }

     await ctx.runMutation(internal.videos.clearMultipartUploadId, {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@convex/videoActions.ts` around lines 605 - 617, Currently
abortMultipartUploadSession may throw (e.g., NoSuchUpload) and prevent running
internal.videos.clearMultipartUploadId; wrap the call to
abortMultipartUploadSession in a try/catch around the if (video.s3Key &&
video.s3MultipartUploadId) block, log or swallow the error when appropriate
(ignore/continue on NoSuchUpload or other non-fatal S3 errors) and ensure
ctx.runMutation(internal.videos.clearMultipartUploadId, { videoId: args.videoId
}) always runs in the finally path so the DB multipart state is cleared even if
the S3 abort fails.
src/lib/uploadResumeDb.ts (1)

104-104: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Close DB on request error to prevent connection leak.

The request.onerror handler rejects but doesn't close the database connection. While the outer try-catch handles the rejection gracefully, the DB connection remains open.

Proposed fix
-      request.onerror = () => reject(request.error ?? new Error("Failed to query resume sessions"));
+      request.onerror = () => {
+        db.close();
+        reject(request.error ?? new Error("Failed to query resume sessions"));
+      };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/uploadResumeDb.ts` at line 104, The request.onerror handler currently
rejects but leaves the IndexedDB connection open; update the request.onerror to
close the database before rejecting (e.g., call db?.close() or the db variable
used to open the connection) so the connection is released on errors; modify the
handler attached to request (request.onerror) to call db.close() (guarded if
necessary) and then reject(request.error ?? new Error("Failed to query resume
sessions")).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@convex/videoActions.ts`:
- Around line 400-414: When resuming multipart uploads in the block that uses
canResumeMultipartUpload(video, args.fileSize), wrap the
listMultipartUploadedParts call in a try/catch that specifically handles the S3
NoSuchUpload error: on that error clear the stale video.s3MultipartUploadId
(persist the change back to the DB so the record no longer points to a dead
upload) and allow execution to fall through to the normal multipart-initiation
path (so initiateVideoUpload can create a fresh multipart session); keep
existing behavior for other errors and still return uploadedParts when listing
succeeds.
- Around line 605-617: Currently abortMultipartUploadSession may throw (e.g.,
NoSuchUpload) and prevent running internal.videos.clearMultipartUploadId; wrap
the call to abortMultipartUploadSession in a try/catch around the if
(video.s3Key && video.s3MultipartUploadId) block, log or swallow the error when
appropriate (ignore/continue on NoSuchUpload or other non-fatal S3 errors) and
ensure ctx.runMutation(internal.videos.clearMultipartUploadId, { videoId:
args.videoId }) always runs in the finally path so the DB multipart state is
cleared even if the S3 abort fails.

In `@src/lib/uploadResumeDb.ts`:
- Line 104: The request.onerror handler currently rejects but leaves the
IndexedDB connection open; update the request.onerror to close the database
before rejecting (e.g., call db?.close() or the db variable used to open the
connection) so the connection is released on errors; modify the handler attached
to request (request.onerror) to call db.close() (guarded if necessary) and then
reject(request.error ?? new Error("Failed to query resume sessions")).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cb8cf763-296a-43a2-8bc8-32802ea490af

📥 Commits

Reviewing files that changed from the base of the PR and between 52b59d6 and 6d03062.

📒 Files selected for processing (2)
  • convex/videoActions.ts
  • src/lib/uploadResumeDb.ts

Comment thread convex/videoActions.ts
Comment on lines +600 to +605
} catch (error) {
try {
await deleteUploadedObject(video.s3Key);
} catch {
// Preserve the original validation or reconciliation failure.
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High convex/videoActions.ts:600

In completeMultipartUpload, the catch block unconditionally deletes the uploaded S3 object on any error from lines 591-599. When reconcileUploadedObjectMetadata or clearMultipartUploadId fail due to transient database issues, the valid uploaded file is incorrectly deleted. Use shouldDeleteUploadedObjectOnFailure(error) to gate deletion, matching the pattern at line 756 in markUploadComplete.

-      try {
-        await deleteUploadedObject(video.s3Key);
-      } catch {
-        // Preserve the original validation or reconciliation failure.
-      }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file convex/videoActions.ts around lines 600-605:

In `completeMultipartUpload`, the catch block unconditionally deletes the uploaded S3 object on any error from lines 591-599. When `reconcileUploadedObjectMetadata` or `clearMultipartUploadId` fail due to transient database issues, the valid uploaded file is incorrectly deleted. Use `shouldDeleteUploadedObjectOnFailure(error)` to gate deletion, matching the pattern at line 756 in `markUploadComplete`.

Evidence trail:
convex/videoActions.ts lines 564-614 (completeMultipartUpload try/catch block at REVIEWED_COMMIT): catch at line 600 unconditionally calls deleteUploadedObject at line 602. Lines 591-599 include database mutations that can fail transiently. convex/videoActions.ts lines 263-275 (shouldDeleteUploadedObjectOnFailure): gates deletion only for validation errors. convex/videoActions.ts lines 755-763 (markUploadComplete): uses shouldDeleteUploadedObjectOnFailure to conditionally delete, demonstrating the intended pattern.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant