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,
});
const fetchPromise = this.fetch(request);
if (opts.onUploadProgress) {
MastodonClient.fakeProgress(fetchPromise, opts.onUploadProgress);
}
const response = MastodonResponse.fromResponse(await fetchPromise);
const response = opts.onUploadProgress
? await this.xhr(request, opts)
: MastodonResponse.fromResponse(await this.fetch(request));
if (!response.ok) {
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.
* TODO: Replace this with: https://stackoverflow.com/a/69400632
* Perform an XHR request from the native `Request` object and get back a `MastodonResponse`.
* This is needed because unfortunately `fetch` does not support upload progress.
*/
private static async fakeProgress(promise: Promise<unknown>, cb: (e: ProgressEvent) => void) {
const controller = new AbortController();
private async xhr(request: Request, opts: Opts = {}): Promise<MastodonResponse> {
const xhr = new XMLHttpRequest();
const { resolve, reject, promise } = Promise.withResolvers<MastodonResponse>();
let loaded = 0;
const total = 100;
xhr.responseType = 'arraybuffer';
cb(new ProgressEvent('loadstart', { lengthComputable: true, loaded, total }));
xhr.onreadystatechange = () => {
if (xhr.readyState !== XMLHttpRequest.DONE) {
return;
}
promise.then(() => {
loaded = 100;
controller.abort();
cb(new ProgressEvent('loadend', { lengthComputable: true, loaded, total }));
cb(new ProgressEvent('load', { lengthComputable: true, loaded, total }));
}).catch(() => {
loaded = 0;
controller.abort();
cb(new ProgressEvent('loadend', { lengthComputable: true, loaded, total }));
cb(new ProgressEvent('error', { lengthComputable: true, loaded, total }));
});
const headers = new Headers(
xhr.getAllResponseHeaders()
.trim()
.split(/[\r\n]+/)
.map((line): [string, string] => {
const [name, ...rest] = line.split(': ');
const value = rest.join(': ');
return [name, value];
}),
);
while (!controller.signal.aborted && loaded < 90) {
await new Promise(resolve => setTimeout(resolve, 10));
loaded += 10;
cb(new ProgressEvent('progress', { lengthComputable: true, loaded, total }));
const response = new MastodonResponse(xhr.response, {
status: xhr.status,
statusText: xhr.statusText,
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;
}
}