Repository navigation
Conversation
prepareTurnAttachments re-verified every already uploaded attachment with its own assets.createUrl round trip, one after another, so a message with N saved uploads waited N round trips before the send command could go out. Start the verifications up front, at most ASSET_URL_MAX_IN_FLIGHT (6) at a time, and let the existing loop read the results in attachment order. Result order, which failing attachment is reported, upload and cancellation handling stay as they were. Verifications that have not started are dropped once the loop ends or the send is cancelled.
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — Saved attachment checks now run up to six at a time while preserving attachment order and existing upload handling. The change is limited to request scheduling, with concurrency bounded by existing asset limits. You can add or adjust custom eligibility rules. Learn more. |
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
Problem
Photos and files start uploading as soon as they are attached, so by the time the user taps Send they
are usually already saved on the server. Delivery then re-checks each saved upload before it sends the
turn:
prepareTurnAttachments(apps/mobile/src/lib/attachmentUpload.ts) callsverifyPersistedAttachmentUpload(oneassets.createUrlWebSocket request) inside its per-attachmentloop and awaits it before looking at the next attachment. A message with N saved attachments therefore
waits N round trips before the send command goes out. The same wait applies when a queued message is
edited and saved. Web runs these checks inside its upload jobs, up to 3 per environment at a time; mobile runs
them one after another.
On a 100 ms request (a typical remote or cellular round trip) that is about 0.2 s for 3 attachments,
0.4 s for 5, 0.85 s for 10, and 9 s at the cap of 100. With 1 or 2 attachments the saving is at most one
round trip, so this matters from about 4 attachments up.
Change
The checks for all saved attachments now start up front, at most
ASSET_URL_MAX_IN_FLIGHT(6) at a time,and the existing loop reads the results in attachment order. Everything else in the loop is unchanged:
the access checks, uploads of attachments that need one,
runAttachmentUploadCycle, the abort signal,and the error that is thrown.
assets.createUrlalready has per environment (the transport semaphore inpackages/client-runtime/src/state/assets.ts), so the server sees no more concurrent requests than itdoes today when a thread loads its images. The server handler is a path lookup with no rate limit of
its own. Using the same number here avoids queueing dozens of requests in front of thread image URLs.
attachmentUpload.ts(a 14-line limiter helper and the up-front start); tests are the rest.Remaining differences from main (I checked each; none changes a result):
the upload time of earlier attachments. The server keeps pending uploads for 24 hours, and the loop
already had this kind of gap between a check and the send command. One case this makes reachable in a
new order: the server's 24-hour stale-upload sweep runs inside
attachments.createUploadUrl. With[A needs an upload, B saved more than 24 h ago and not yet swept], main checks B after A's uploadrequest has swept it and re-uploads B; this branch can check B first, then A's upload sweeps B, and the
send fails when the server claims B. Main already has the same failure for the order
[B, A], and aretry recovers in both cases.
requireUploadAccessstill runs before each attachment is used, but a verification request nowstarts without a per-item check. The server enforces the scope on
assets.createUrlanyway.already in flight and finish unread (main would have sent none for later attachments).
assets.createUrlslots with thread image URLs; those can wait about oneround trip while a batch of verifications runs.
Unchanged: result order,
pendingAttachmentIdsorder, which attachment's error is reported when severalfail (the first in attachment order), a rejected verification being thrown where the loop reaches it,
cancel returning
abandoned, and the pre-existing gap where a verified upload can still disappear beforethe server claims it.
Scope and approval
No prior issue or discussion. I am not claiming this is an obvious bug: it is a behaviour-preserving
performance fix of one underlying problem (serial verification) in one function, with no setting, UI,
protocol or default changed. If you'd rather agree on it in an Ideas discussion first, since this is a
latency improvement rather than a defect, say so and I'll move it there.
Open #12610 (upload timeout) edits
uploadFileBytesand a different part of the test file. It is alreadymarked conflicting with main. The two do not touch the same lines of
prepareTurnAttachments; the helperhere is placed away from the spot where #12610 adds its constant. With #12610 a single upload can take up
to 3 minutes, which makes difference 1 above up to 3 minutes per earlier upload, still inside the 24 hour
server keep time. Open #18631 changes one line of the same file, away from these lines.
Verification
Tests (Node 26.12.1,
vp test run --project unit src/lib/attachmentUpload.test.tsinapps/mobile): 35pass. Eight of them are new, in
describe("verifying saved uploads"), using deferred fakes for the checks:pendingAttachmentIdsstay in orderwhen they resolve in reverse;
of this passes on main and on the branch, so it pins behaviour that must not change);
abandonedwithout minting or uploading.On main (the new tests against the unchanged source), 6 of the 8 fail (overlap, bound, stop, ordering under
reverse completion, uploading while verifying, cancel) and the other 2 pass. Removing the stop flag or the
bound from the change fails a test; removing the
.catchleaves every test passing but produces 3unhandled rejections, which fail the run. Also green: the other mobile tests that import this module (6 files, 167 tests),
tsc --noEmitin
apps/mobile,vp lintandvp fmt --checkon the two files.Timing, real
prepareTurnAttachmentsand realverifyPersistedAttachmentUploadbundled with esbuild; only theWebSocket request is a timer (100 ms each, at most 6 in flight, as the transport allows). n = 5 per row,
main and branch alternating, median (min), ms:
These come from a timer model, not a phone or a real tunnel. Real savings follow the real round-trip
time plus server time. Not checked: a device, a real remote connection, Android, iOS, and the
reacttest project. Raw rows:raw/verify-m302.jsonlin the scratch folder, available on request.Claude Sonnet 5.5 and Claude Opus 5.5 in T3 Code (Claude Code harness).