MastodonClient: fallback to XHR for onUploadProgress

merge-requests/3305/head
Alex Gleason 2024-12-13 19:52:31 -06:00
rodzic 2ffff220e4
commit 878c3ac54d
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
1 zmienionych plików z 56 dodań i 29 usunięć

Wyświetl plik

@ -93,13 +93,9 @@ export class MastodonClient {
body, body,
}); });
const fetchPromise = this.fetch(request); const response = opts.onUploadProgress
? await this.xhr(request, opts)
if (opts.onUploadProgress) { : MastodonResponse.fromResponse(await this.fetch(request));
MastodonClient.fakeProgress(fetchPromise, opts.onUploadProgress);
}
const response = MastodonResponse.fromResponse(await fetchPromise);
if (!response.ok) { if (!response.ok) {
throw new HTTPError(response, request); throw new HTTPError(response, request);
@ -109,34 +105,65 @@ export class MastodonClient {
} }
/** /**
* `fetch` does not natively support upload progress. Implement a fake progress callback instead. * Perform an XHR request from the native `Request` object and get back a `MastodonResponse`.
* TODO: Replace this with: https://stackoverflow.com/a/69400632 * This is needed because unfortunately `fetch` does not support upload progress.
*/ */
private static async fakeProgress(promise: Promise<unknown>, cb: (e: ProgressEvent) => void) { private async xhr(request: Request, opts: Opts = {}): Promise<MastodonResponse> {
const controller = new AbortController(); const xhr = new XMLHttpRequest();
const { resolve, reject, promise } = Promise.withResolvers<MastodonResponse>();
let loaded = 0; xhr.responseType = 'arraybuffer';
const total = 100;
cb(new ProgressEvent('loadstart', { lengthComputable: true, loaded, total })); xhr.onreadystatechange = () => {
if (xhr.readyState !== XMLHttpRequest.DONE) {
return;
}
promise.then(() => { const headers = new Headers(
loaded = 100; xhr.getAllResponseHeaders()
controller.abort(); .trim()
cb(new ProgressEvent('loadend', { lengthComputable: true, loaded, total })); .split(/[\r\n]+/)
cb(new ProgressEvent('load', { lengthComputable: true, loaded, total })); .map((line): [string, string] => {
}).catch(() => { const [name, ...rest] = line.split(': ');
loaded = 0; const value = rest.join(': ');
controller.abort(); return [name, value];
cb(new ProgressEvent('loadend', { lengthComputable: true, loaded, total })); }),
cb(new ProgressEvent('error', { lengthComputable: true, loaded, total })); );
});
while (!controller.signal.aborted && loaded < 90) { const response = new MastodonResponse(xhr.response, {
await new Promise(resolve => setTimeout(resolve, 10)); status: xhr.status,
loaded += 10; statusText: xhr.statusText,
cb(new ProgressEvent('progress', { lengthComputable: true, loaded, total })); headers,
});
resolve(response);
};
xhr.onerror = () => {
reject(new TypeError('Network request failed'));
};
xhr.onabort = () => {
reject(new DOMException('The request was aborted', 'AbortError'));
};
if (opts.onUploadProgress) {
xhr.upload.onprogress = opts.onUploadProgress;
} }
if (opts.signal) {
opts.signal.addEventListener('abort', () => xhr.abort(), { once: true });
}
xhr.open(request.method, request.url, true);
for (const [name, value] of request.headers) {
xhr.setRequestHeader(name, value);
}
xhr.send(await request.arrayBuffer());
return promise;
} }
} }