mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
The composer chip only knew a boolean: `uploading` flipped on, an indeterminate bar animated at 40% width, and nothing anywhere carried a percentage, so a large attachment on a slow link looked frozen for the whole transfer. The JMAP client has been able to report real progress since #333 gave uploadBlob an XHR path with onProgress; the composer just never passed it. Stock path: pass onProgress and the existing AbortController's signal to client.uploadBlob. The chip's bar becomes determinate as soon as bytes are reported (falling back to the indeterminate animation otherwise), and cancel now aborts the transfer itself instead of being checked only once the upload has finished. Plugin offload path (#751): a plugin that offloads an attachment moves the bytes itself via api.http.post, which the composer cannot observe. It also cannot be handed a callback: the sandbox bridge marshals functions host-to-plugin only, and the File a plugin holds is a structured clone, so object identity cannot correlate its request back to an attachment. The staged file id can: the composer generated it and handed it to onBeforeBlobUpload. A plugin echoes it back as the new optional `progressFileId` on the post options; the host sends the Blob via XMLHttpRequest (fetch exposes no upload progress events) and reports bytes into a host-side registry keyed by that id, which the composer subscribes to before running the hook. Progress never crosses into the sandbox, and reports for an id the composer is not tracking are dropped, so the plugin API grows exactly one optional field. Calls without progressFileId keep the fetch path untouched. Closes #754
43 lines
1.8 KiB
TypeScript
43 lines
1.8 KiB
TypeScript
/**
|
|
* Byte-level progress for staged-attachment uploads, keyed by the staged file
|
|
* id the composer hands to `onBeforeBlobUpload`.
|
|
*
|
|
* The composer cannot observe a plugin's own transfer directly: a plugin that
|
|
* offloads an attachment (see `isExternalAttachmentResult`) uploads the bytes
|
|
* itself via `api.http.post`, and the File it holds is a structured clone from
|
|
* the sandbox boundary, so object identity can't correlate that request back
|
|
* to an attachment. The staged file id can: the composer generated it, handed
|
|
* it to the hook, and the plugin passes it back via `progressFileId` on the
|
|
* post options. The host counts the bytes (see `doHttpPost`) and reports them
|
|
* here; the composer subscribes before running the hook.
|
|
*
|
|
* Progress never crosses into the sandbox. Reports for an id nobody listens to
|
|
* are dropped, so a plugin sending noise for ids it was never handed reaches
|
|
* nothing.
|
|
*/
|
|
|
|
export type UploadProgressListener = (loaded: number, total: number) => void;
|
|
|
|
const listeners = new Map<string, UploadProgressListener>();
|
|
|
|
/**
|
|
* Subscribe to progress reports for one staged file id. Returns the
|
|
* unsubscribe function. One listener per id: the composer owns the staged id,
|
|
* nothing else has a reason to listen.
|
|
*/
|
|
export function onUploadProgress(fileId: string, listener: UploadProgressListener): () => void {
|
|
listeners.set(fileId, listener);
|
|
return () => {
|
|
// Only clear our own registration, in case the id was re-registered.
|
|
if (listeners.get(fileId) === listener) listeners.delete(fileId);
|
|
};
|
|
}
|
|
|
|
/** Report transferred bytes for a staged file id. No listener, no effect. */
|
|
export function reportUploadProgress(fileId: string, loaded: number, total: number): void {
|
|
const listener = listeners.get(fileId);
|
|
if (!listener) return;
|
|
if (!Number.isFinite(loaded) || !Number.isFinite(total)) return;
|
|
listener(loaded, total);
|
|
}
|