fix(upload): self-aborting watchdog + lower concurrency to end stalls

A folder upload with several large files could appear frozen for ~2 min: a few
concurrent uploads stalled and the old 120s per-file timeout neither aborted the
request (leaving zombie XHRs that exhaust the browser's per-host connection pool)
nor recovered quickly.

- uploadFileWithProgress now self-aborts on a stalled connection: the deadline
  resets on every upload-progress tick (a slow but *moving* transfer is fine),
  and once the body is sent the server gets a fixed window to respond; on a stall
  xhr.abort() frees the connection immediately — no zombie, no cascade.
- Lower upload concurrency 4 -> 3 to reduce server contention from large
  concurrent uploads.
- The outer per-file timeout is now just a generous backstop for a wedged delta
  worker / by-hash request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DioCrafts
2026-06-20 18:33:53 +02:00
parent edfbd68e6c
commit b58b2d8f95
2 changed files with 35 additions and 9 deletions
@@ -291,14 +291,14 @@
// Upload at most this many files concurrently. Bounded so one stuck file
// blocks only its own lane (the others keep going) without overwhelming the
// browser's per-host connection cap or spawning too many delta workers.
const UPLOAD_CONCURRENCY = 4;
// browser's per-host connection cap, spawning too many delta workers, or
// over-contending the server with many large concurrent uploads.
const UPLOAD_CONCURRENCY = 3;
/** Per-file deadline (ms): a tiny file that stops responding fails after 2
* min; larger files get proportionally longer (20 KB/s floor) so a slow but
* progressing transfer is never killed. Stops a stuck file pinning its lane
* forever. */
const uploadDeadlineMs = (file: File) => Math.max(120_000, (file.size / (20 * 1024)) * 1000);
/** Outer backstop deadline (ms). The plain-upload path already self-aborts on
* a stalled connection (see `uploadFileWithProgress`); this only catches a
* wedged delta worker or by-hash request so a lane can never hang forever. */
const FILE_BACKSTOP_MS = 4 * 60_000;
/** Reject after `ms` if `p` hasn't settled. */
function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
@@ -388,7 +388,7 @@
},
owned.get(file) ?? null
),
uploadDeadlineMs(file)
FILE_BACKSTOP_MS
);
} catch (e) {
failures++;