Skip to content

fix(files): report real upload progress (XHR with progress events)#333

Open
shukiv wants to merge 2 commits into
bulwarkmail:mainfrom
shukiv:fix/upload-progress
Open

fix(files): report real upload progress (XHR with progress events)#333
shukiv wants to merge 2 commits into
bulwarkmail:mainfrom
shukiv:fix/upload-progress

Conversation

@shukiv
Copy link
Copy Markdown
Contributor

@shukiv shukiv commented May 24, 2026

Problem

On the Files page, uploading a large file shows the progress bar stuck at 0% for the entire upload, then it jumps to 100% on completion. On slow connections or large files the UI looks frozen even though bytes are flowing on the wire.

Root cause: JMAPClient.uploadBlob() uses fetch(), and fetch() does not expose upload progress events (no equivalent to xhr.upload.onprogress). The file-store set loaded: 0 before calling uploadBlob, then loaded: file.size after it resolved, so there were no intermediate updates.

The repo already uses the XHR-with-progress pattern in lib/webdav/client.ts:138, so this PR mirrors that approach for JMAP blob uploads.

Change

  • IJMAPClient.uploadBlob(file)uploadBlob(file, opts?: { onProgress?, signal? }). Backwards-compatible: opts is optional.
  • JMAPClient: when the caller passes onProgress or signal, route through a new private xhrUpload() helper that uses XMLHttpRequest and wires xhr.upload.onprogressopts.onProgress. Otherwise the fetch path is unchanged so we keep the existing 401-retry behaviour in authenticatedFetch() for non-Files callers.
  • DemoJMAPClient.uploadBlob: same signature, synthesises a 0% then 100% callback for parity.
  • stores/file-store.ts (uploadFile + uploadFiles): passes both
    • onProgress: (loaded, total) => set({ uploadProgress: { ..., loaded, total } }), and
    • signal: abortController.signal, so Cancel now actually aborts the network request instead of only short-circuiting the post-upload createFileNode step.

fetch() does have a streaming option (ReadableStream body + duplex: 'half'), but browser support is still inconsistent enough (Safari) that XHR is the safer choice for upload progress today.

Verification

  • npx tsc --noEmit clean
  • npx eslint on the four changed files clean
  • Existing JMAP client tests pass (jmap-client-resilience.test.ts: 16/16)
  • Manual: uploaded a 47 MB file through the Files page — progress now ticks from 0% → 100% smoothly; cancelling mid-upload immediately closes the request.

Files

  • lib/jmap/client-interface.ts — signature
  • lib/jmap/client.ts — XHR path + xhrUpload helper
  • lib/demo/demo-client.ts — signature
  • stores/file-store.ts — wire onProgress + signal

shukiv added 2 commits May 24, 2026 06:04
The Files page UI sat at 0% throughout an upload because uploadBlob()
uses fetch(), which does not surface upload progress events. The store
set loaded=0 before the call and loaded=file.size after it, so users
saw the progress bar jump from 0% straight to 100% on completion --
and on slow connections (or large files) it appeared frozen.

Switch uploadBlob() to XHR when the caller passes onProgress or an
AbortSignal, so progress events from xhr.upload.onprogress can drive
the UI. Callers that don't pass either keep the fetch path so we
preserve the existing 401-retry behaviour in authenticatedFetch().

Wire the file store to pass both onProgress (updates uploadProgress
in real time) and the existing AbortController's signal (so cancel
now actually aborts the network request, not just the post-upload
createFileNode step).

uploadBlob() is part of IJMAPClient so the signature change is also
applied to the demo client (synthesises 0% then 100%).
Resolved conflict in lib/jmap/client.ts around uploadBlob():

* Upstream added a second positional accountId argument to support
  multi-account JMAP setups.
* This branch added an opts bag for progress callback + AbortSignal.

Combined the two by accepting either signature:

    uploadBlob(file, accountId)          // upstream form
    uploadBlob(file, { accountId, onProgress, signal })  // new form

Existing call sites compile unchanged; the new fields are reachable
via the options object. client-interface.ts updated to match.
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