Move timeout to fetch, as XHR has native timeout support

This commit is contained in:
Bruno Windels 2020-08-05 15:36:44 +00:00 committed by Bruno Windels
parent 3a5e3a69f2
commit e8e9740521
4 changed files with 83 additions and 84 deletions

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay.js"; // import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay.js";
import {fetchRequest} from "./matrix/net/request/fetch.js"; import {createFetchRequest} from "./matrix/net/request/fetch.js";
import {SessionContainer} from "./matrix/SessionContainer.js"; import {SessionContainer} from "./matrix/SessionContainer.js";
import {StorageFactory} from "./matrix/storage/idb/StorageFactory.js"; import {StorageFactory} from "./matrix/storage/idb/StorageFactory.js";
import {SessionInfoStorage} from "./matrix/sessioninfo/localstorage/SessionInfoStorage.js"; import {SessionInfoStorage} from "./matrix/sessioninfo/localstorage/SessionInfoStorage.js";
@ -32,13 +32,13 @@ export default async function main(container) {
// const request = replay.request; // const request = replay.request;
// to record: // to record:
// const recorder = new RecordRequester(fetchRequest); // const recorder = new RecordRequester(createFetchRequest(clock.createTimeout));
// const request = recorder.request; // const request = recorder.request;
// window.getBrawlFetchLog = () => recorder.log(); // window.getBrawlFetchLog = () => recorder.log();
// normal network: // normal network:
const request = fetchRequest;
const sessionInfoStorage = new SessionInfoStorage("brawl_sessions_v1");
const clock = new Clock(); const clock = new Clock();
const request = createFetchRequest(clock.createTimeout);
const sessionInfoStorage = new SessionInfoStorage("brawl_sessions_v1");
const storageFactory = new StorageFactory(); const storageFactory = new StorageFactory();
const vm = new BrawlViewModel({ const vm = new BrawlViewModel({

View file

@ -21,9 +21,9 @@ import {
} from "../error.js"; } from "../error.js";
class RequestWrapper { class RequestWrapper {
constructor(method, url, requestResult, responsePromise) { constructor(method, url, requestResult) {
this._requestResult = requestResult; this._requestResult = requestResult;
this._promise = responsePromise.then(response => { this._promise = requestResult.response().then(response => {
// ok? // ok?
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {
return response.body; return response.body;
@ -60,35 +60,6 @@ export class HomeServerApi {
return `${this._homeserver}/_matrix/client/r0${csPath}`; return `${this._homeserver}/_matrix/client/r0${csPath}`;
} }
_abortOnTimeout(timeoutAmount, requestResult, responsePromise) {
const timeout = this._createTimeout(timeoutAmount);
// abort request if timeout finishes first
let timedOut = false;
timeout.elapsed().then(
() => {
timedOut = true;
requestResult.abort();
},
() => {} // ignore AbortError
);
// abort timeout if request finishes first
return responsePromise.then(
response => {
timeout.abort();
return response;
},
err => {
timeout.abort();
// map error to TimeoutError
if (err instanceof AbortError && timedOut) {
throw new ConnectionError(`Request timed out after ${timeoutAmount}ms`, true);
} else {
throw err;
}
}
);
}
_encodeQueryParams(queryParams) { _encodeQueryParams(queryParams) {
return Object.entries(queryParams || {}) return Object.entries(queryParams || {})
.filter(([, value]) => value !== undefined) .filter(([, value]) => value !== undefined)
@ -118,19 +89,10 @@ export class HomeServerApi {
method, method,
headers, headers,
body: bodyString, body: bodyString,
timeout: options && options.timeout
}); });
let responsePromise = requestResult.response(); const wrapper = new RequestWrapper(method, url, requestResult);
if (options && options.timeout) {
responsePromise = this._abortOnTimeout(
options.timeout,
requestResult,
responsePromise
);
}
const wrapper = new RequestWrapper(method, url, requestResult, responsePromise);
if (this._reconnector) { if (this._reconnector) {
wrapper.response().catch(err => { wrapper.response().catch(err => {

View file

@ -18,6 +18,7 @@ import {
AbortError, AbortError,
ConnectionError ConnectionError
} from "../../error.js"; } from "../../error.js";
import {abortOnTimeout} from "../timeout.js";
class RequestResult { class RequestResult {
constructor(promise, controller) { constructor(promise, controller) {
@ -31,9 +32,9 @@ class RequestResult {
} }
}; };
}); });
this._promise = Promise.race([promise, abortPromise]); this.promise = Promise.race([promise, abortPromise]);
} else { } else {
this._promise = promise; this.promise = promise;
this._controller = controller; this._controller = controller;
} }
} }
@ -43,47 +44,55 @@ class RequestResult {
} }
response() { response() {
return this._promise; return this.promise;
} }
} }
export function fetchRequest(url, options) { export function createFetchRequest(createTimeout) {
const controller = typeof AbortController === "function" ? new AbortController() : null; return function fetchRequest(url, options) {
if (controller) { const controller = typeof AbortController === "function" ? new AbortController() : null;
if (controller) {
options = Object.assign(options, {
signal: controller.signal
});
}
options = Object.assign(options, { options = Object.assign(options, {
signal: controller.signal mode: "cors",
credentials: "omit",
referrer: "no-referrer",
cache: "no-cache",
}); });
} if (options.headers) {
options = Object.assign(options, { const headers = new Headers();
mode: "cors", for(const [name, value] of options.headers.entries()) {
credentials: "omit", headers.append(name, value);
referrer: "no-referrer", }
cache: "no-cache", options.headers = headers;
});
if (options.headers) {
const headers = new Headers();
for(const [name, value] of options.headers.entries()) {
headers.append(name, value);
} }
options.headers = headers; const promise = fetch(url, options).then(async response => {
} const {status} = response;
const promise = fetch(url, options).then(async response => { const body = await response.json();
const {status} = response; return {status, body};
const body = await response.json(); }, err => {
return {status, body}; if (err.name === "AbortError") {
}, err => { throw new AbortError();
if (err.name === "AbortError") { } else if (err instanceof TypeError) {
throw new AbortError(); // Network errors are reported as TypeErrors, see
} else if (err instanceof TypeError) { // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful
// Network errors are reported as TypeErrors, see // this can either mean user is offline, server is offline, or a CORS error (server misconfiguration).
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful //
// this can either mean user is offline, server is offline, or a CORS error (server misconfiguration). // One could check navigator.onLine to rule out the first
// // but the 2 latter ones are indistinguishable from javascript.
// One could check navigator.onLine to rule out the first throw new ConnectionError(`${options.method} ${url}: ${err.message}`);
// but the 2 latter ones are indistinguishable from javascript. }
throw new ConnectionError(`${options.method} ${url}: ${err.message}`); throw err;
});
const result = new RequestResult(promise, controller);
if (options.timeout) {
result.promise = abortOnTimeout(createTimeout, options.timeout, result, result.promise);
} }
throw err;
}); return result;
return new RequestResult(promise, controller); }
} }

28
src/matrix/net/timeout.js Normal file
View file

@ -0,0 +1,28 @@
export function abortOnTimeout(createTimeout, timeoutAmount, requestResult, responsePromise) {
const timeout = createTimeout(timeoutAmount);
// abort request if timeout finishes first
let timedOut = false;
timeout.elapsed().then(
() => {
timedOut = true;
requestResult.abort();
},
() => {} // ignore AbortError when timeout is aborted
);
// abort timeout if request finishes first
return responsePromise.then(
response => {
timeout.abort();
return response;
},
err => {
timeout.abort();
// map error to TimeoutError
if (err instanceof AbortError && timedOut) {
throw new ConnectionError(`Request timed out after ${timeoutAmount}ms`, true);
} else {
throw err;
}
}
);
}