diff options
author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2022-08-20 11:17:24 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-20 11:17:24 -0400 |
commit | 310303ca1a123a77f9bd116af4dc64ad9c3256c5 (patch) | |
tree | af8bad0ec544625970a5f2a4613fff27773b162c /ext/js/background | |
parent | 02483a45b1b7fb0654b3f37571b92400b76734a5 (diff) |
Audio download timeout (#2187)
* Add support for an idle timeout when downloading audio
* Update eslint rules
* Pass idleTimeout to the downloader from DisplayAnki
* Add anki.downloadTimeout setting
* Update tests
* Assign _audioDownloadIdleTimeout using settings
* Show info about cancelled downloads
* Handle Firefox bug
* Improve audio errors
* Refactor
* Move functions to RequestBuilder
Diffstat (limited to 'ext/js/background')
-rw-r--r-- | ext/js/background/backend.js | 32 | ||||
-rw-r--r-- | ext/js/background/request-builder.js | 90 |
2 files changed, 113 insertions, 9 deletions
diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 75ff7bee..f3c76311 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -1809,7 +1809,7 @@ class Backend { return null; } - const {sources, preferredAudioIndex} = details; + const {sources, preferredAudioIndex, idleTimeout} = details; let data; let contentType; try { @@ -1817,7 +1817,8 @@ class Backend { sources, preferredAudioIndex, term, - reading + reading, + idleTimeout )); } catch (e) { const error = this._getAudioDownloadError(e); @@ -1918,6 +1919,9 @@ class Backend { const {errors} = error.data; if (Array.isArray(errors)) { for (const error2 of errors) { + if (error2.name === 'AbortError') { + return this._createAudioDownloadError('Audio download was cancelled due to an idle timeout', 'audio-download-idle-timeout', errors); + } if (!isObject(error2.data)) { continue; } const {details} = error2.data; if (!isObject(details)) { continue; } @@ -1925,12 +1929,7 @@ class Backend { // This is potentially an error due to the extension not having enough URL privileges. // The message logged to the console looks like this: // Access to fetch at '<URL>' from origin 'chrome-extension://<ID>' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. - const result = new Error('Audio download failed due to possible extension permissions error'); - result.data = { - errors, - referenceUrl: '/issues.html#audio-download-failed' - }; - return result; + return this._createAudioDownloadError('Audio download failed due to possible extension permissions error', 'audio-download-failed', errors); } } } @@ -1938,6 +1937,23 @@ class Backend { return null; } + _createAudioDownloadError(message, issueId, errors) { + const error = new Error(message); + const hasErrors = Array.isArray(errors); + const hasIssueId = (typeof issueId === 'string'); + if (hasErrors || hasIssueId) { + error.data = {}; + if (hasErrors) { + // Errors need to be serialized since they are passed to other frames + error.data.errors = errors.map((e) => serializeError(e)); + } + if (hasIssueId) { + error.data.referenceUrl = `/issues.html#${issueId}`; + } + } + return error; + } + _generateAnkiNoteMediaFileName(prefix, extension, timestamp, definitionDetails) { let fileName = prefix; diff --git a/ext/js/background/request-builder.js b/ext/js/background/request-builder.js index ad1536f1..2cdd6f0e 100644 --- a/ext/js/background/request-builder.js +++ b/ext/js/background/request-builder.js @@ -42,6 +42,58 @@ class RequestBuilder { return await this._fetchInternal(url, init, headerModifications); } + static async readFetchResponseArrayBuffer(response, onProgress) { + let reader; + try { + if (typeof onProgress === 'function') { + reader = response.body.getReader(); + } + } catch (e) { + // Not supported + } + + if (typeof reader === 'undefined') { + const result = await response.arrayBuffer(); + if (typeof onProgress === 'function') { + onProgress(true); + } + return result; + } + + const contentLengthString = response.headers.get('Content-Length'); + const contentLength = contentLengthString !== null ? Number.parseInt(contentLengthString, 10) : null; + let target = Number.isFinite(contentLength) ? new Uint8Array(contentLength) : null; + let targetPosition = 0; + let totalLength = 0; + const targets = []; + + while (true) { + const {done, value} = await reader.read(); + if (done) { break; } + onProgress(false); + if (target === null) { + targets.push({array: value, length: value.length}); + } else if (targetPosition + value.length > target.length) { + targets.push({array: target, length: targetPosition}); + target = null; + } else { + target.set(value, targetPosition); + targetPosition += value.length; + } + totalLength += value.length; + } + + if (target === null) { + target = this._joinUint8Arrays(targets, totalLength); + } else if (totalLength < target.length) { + target = target.slice(0, totalLength); + } + + onProgress(true); + + return target; + } + // Private async _fetchInternal(url, init, headerModifications) { @@ -92,7 +144,10 @@ class RequestBuilder { }, 100); } const details = await errorDetailsPromise; - e.data = {details}; + if (details !== null) { + const data = {details}; + this._assignErrorData(e, data); + } throw e; } finally { this._removeWebRequestEventListeners(eventListeners); @@ -295,4 +350,37 @@ class RequestBuilder { } return result; } + + _assignErrorData(error, data) { + try { + error.data = data; + } catch (e) { + // On Firefox, assigning DOMException.data can fail in certain contexts. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1776555 + try { + Object.defineProperty(error, 'data', { + configurable: true, + enumerable: true, + writable: true, + value: data + }); + } catch (e2) { + // NOP + } + } + } + + static _joinUint8Arrays(items, totalLength) { + if (items.length === 1) { + const {array, length} = items[0]; + if (array.length === length) { return array; } + } + const result = new Uint8Array(totalLength); + let position = 0; + for (const {array, length} of items) { + result.set(array, position); + position += length; + } + return result; + } } |