Merge pull request #2 from vector-im/bwindels/ie11
Add IE11 support in Javascript
This commit is contained in:
commit
227e331435
11 changed files with 2597 additions and 116 deletions
2264
package-lock.json
generated
Normal file
2264
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -22,11 +22,20 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/vector-im/hydrogen-web/#readme",
|
"homepage": "https://github.com/vector-im/hydrogen-web/#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.11.1",
|
||||||
|
"@babel/preset-env": "^7.11.0",
|
||||||
|
"@rollup/plugin-babel": "^5.1.0",
|
||||||
|
"@rollup/plugin-commonjs": "^14.0.0",
|
||||||
|
"@rollup/plugin-multi-entry": "^3.0.1",
|
||||||
|
"@rollup/plugin-node-resolve": "^8.4.0",
|
||||||
"cheerio": "^1.0.0-rc.3",
|
"cheerio": "^1.0.0-rc.3",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
"finalhandler": "^1.1.1",
|
"finalhandler": "^1.1.1",
|
||||||
"impunity": "^0.0.11",
|
"impunity": "^0.0.11",
|
||||||
|
"mdn-polyfills": "^5.20.0",
|
||||||
"postcss": "^7.0.18",
|
"postcss": "^7.0.18",
|
||||||
"postcss-import": "^12.0.1",
|
"postcss-import": "^12.0.1",
|
||||||
|
"regenerator-runtime": "^0.13.7",
|
||||||
"rollup": "^1.15.6",
|
"rollup": "^1.15.6",
|
||||||
"serve-static": "^1.13.2"
|
"serve-static": "^1.13.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -23,15 +24,35 @@ import postcss from "postcss";
|
||||||
import postcssImport from "postcss-import";
|
import postcssImport from "postcss-import";
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
|
// needed for legacy bundle
|
||||||
|
import babel from '@rollup/plugin-babel';
|
||||||
|
// needed to find the polyfill modules in the main-legacy.js bundle
|
||||||
|
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||||
|
// needed because some of the polyfills are written as commonjs modules
|
||||||
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
// multi-entry plugin so we can add polyfill file to main
|
||||||
|
import multi from '@rollup/plugin-multi-entry';
|
||||||
|
|
||||||
|
const PROJECT_ID = "hydrogen";
|
||||||
|
const PROJECT_SHORT_NAME = "Hydrogen";
|
||||||
|
const PROJECT_NAME = "Hydrogen Chat";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
const projectDir = path.join(__dirname, "../");
|
const projectDir = path.join(__dirname, "../");
|
||||||
const targetDir = path.join(projectDir, "target");
|
const targetDir = path.join(projectDir, "target");
|
||||||
|
|
||||||
const debug = false;
|
const {debug, noOffline, legacy} = process.argv.reduce((params, param) => {
|
||||||
const offline = true;
|
if (param.startsWith("--")) {
|
||||||
|
params[param.substr(2)] = true;
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}, {
|
||||||
|
debug: false,
|
||||||
|
noOffline: false,
|
||||||
|
legacy: false
|
||||||
|
});
|
||||||
|
const offline = !noOffline;
|
||||||
|
|
||||||
async function build() {
|
async function build() {
|
||||||
// get version number
|
// get version number
|
||||||
|
@ -39,25 +60,32 @@ async function build() {
|
||||||
// clear target dir
|
// clear target dir
|
||||||
await removeDirIfExists(targetDir);
|
await removeDirIfExists(targetDir);
|
||||||
await fs.mkdir(targetDir);
|
await fs.mkdir(targetDir);
|
||||||
|
let bundleName = `${PROJECT_ID}.js`;
|
||||||
await buildHtml(version);
|
if (legacy) {
|
||||||
await buildJs();
|
bundleName = `${PROJECT_ID}-legacy.js`;
|
||||||
|
}
|
||||||
|
await buildHtml(version, bundleName);
|
||||||
|
if (legacy) {
|
||||||
|
await buildJsLegacy(bundleName);
|
||||||
|
} else {
|
||||||
|
await buildJs(bundleName);
|
||||||
|
}
|
||||||
await buildCss();
|
await buildCss();
|
||||||
if (offline) {
|
if (offline) {
|
||||||
await buildOffline(version);
|
await buildOffline(version, bundleName);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`built brawl ${version} successfully`);
|
console.log(`built ${PROJECT_ID}${legacy ? " legacy" : ""} ${version} successfully`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildHtml(version) {
|
async function buildHtml(version, bundleName) {
|
||||||
// transform html file
|
// transform html file
|
||||||
const devHtml = await fs.readFile(path.join(projectDir, "index.html"), "utf8");
|
const devHtml = await fs.readFile(path.join(projectDir, "index.html"), "utf8");
|
||||||
const doc = cheerio.load(devHtml);
|
const doc = cheerio.load(devHtml);
|
||||||
doc("link[rel=stylesheet]").attr("href", "brawl.css");
|
doc("link[rel=stylesheet]").attr("href", `${PROJECT_ID}.css`);
|
||||||
doc("script#main").replaceWith(
|
doc("script#main").replaceWith(
|
||||||
`<script type="text/javascript" src="brawl.js"></script>` +
|
`<script type="text/javascript" src="${bundleName}"></script>` +
|
||||||
`<script type="text/javascript">main(document.body);</script>`);
|
`<script type="text/javascript">${PROJECT_ID}Bundle.main(document.body);</script>`);
|
||||||
removeOrEnableScript(doc("script#phone-debug-pre"), debug);
|
removeOrEnableScript(doc("script#phone-debug-pre"), debug);
|
||||||
removeOrEnableScript(doc("script#phone-debug-post"), debug);
|
removeOrEnableScript(doc("script#phone-debug-post"), debug);
|
||||||
removeOrEnableScript(doc("script#service-worker"), offline);
|
removeOrEnableScript(doc("script#service-worker"), offline);
|
||||||
|
@ -75,23 +103,48 @@ async function buildHtml(version) {
|
||||||
await fs.writeFile(path.join(targetDir, "index.html"), doc.html(), "utf8");
|
await fs.writeFile(path.join(targetDir, "index.html"), doc.html(), "utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildJs() {
|
async function buildJs(bundleName) {
|
||||||
// create js bundle
|
// create js bundle
|
||||||
const rollupConfig = {
|
const bundle = await rollup.rollup({input: 'src/main.js'});
|
||||||
input: 'src/main.js',
|
await bundle.write({
|
||||||
output: {
|
file: path.join(targetDir, bundleName),
|
||||||
file: path.join(targetDir, "brawl.js"),
|
format: 'iife',
|
||||||
format: 'iife',
|
name: `${PROJECT_ID}Bundle`
|
||||||
name: 'main'
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
const bundle = await rollup.rollup(rollupConfig);
|
|
||||||
await bundle.write(rollupConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildOffline(version) {
|
async function buildJsLegacy(bundleName) {
|
||||||
|
// compile down to whatever IE 11 needs
|
||||||
|
const babelPlugin = babel.babel({
|
||||||
|
babelHelpers: 'bundled',
|
||||||
|
exclude: 'node_modules/**',
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
useBuiltIns: "entry",
|
||||||
|
corejs: "3",
|
||||||
|
targets: "IE 11"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
// create js bundle
|
||||||
|
const rollupConfig = {
|
||||||
|
input: ['src/legacy-polyfill.js', 'src/main.js'],
|
||||||
|
plugins: [multi(), commonjs(), nodeResolve(), babelPlugin]
|
||||||
|
};
|
||||||
|
const bundle = await rollup.rollup(rollupConfig);
|
||||||
|
await bundle.write({
|
||||||
|
file: path.join(targetDir, bundleName),
|
||||||
|
format: 'iife',
|
||||||
|
name: `${PROJECT_ID}Bundle`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildOffline(version, bundleName) {
|
||||||
// write offline availability
|
// write offline availability
|
||||||
const offlineFiles = ["brawl.js", "brawl.css", "index.html", "icon-192.png"];
|
const offlineFiles = [bundleName, `${PROJECT_ID}.css`, "index.html", "icon-192.png"];
|
||||||
|
|
||||||
// write appcache manifest
|
// write appcache manifest
|
||||||
const manifestLines = [
|
const manifestLines = [
|
||||||
|
@ -111,8 +164,8 @@ async function buildOffline(version) {
|
||||||
await fs.writeFile(path.join(targetDir, "sw.js"), swSource, "utf8");
|
await fs.writeFile(path.join(targetDir, "sw.js"), swSource, "utf8");
|
||||||
// write web manifest
|
// write web manifest
|
||||||
const webManifest = {
|
const webManifest = {
|
||||||
name: "Brawl Chat",
|
name:PROJECT_NAME,
|
||||||
short_name: "Brawl",
|
short_name: PROJECT_SHORT_NAME,
|
||||||
display: "fullscreen",
|
display: "fullscreen",
|
||||||
start_url: "index.html",
|
start_url: "index.html",
|
||||||
icons: [{"src": "icon-192.png", "sizes": "192x192", "type": "image/png"}],
|
icons: [{"src": "icon-192.png", "sizes": "192x192", "type": "image/png"}],
|
||||||
|
@ -129,7 +182,7 @@ async function buildCss() {
|
||||||
const preCss = await fs.readFile(cssMainFile, "utf8");
|
const preCss = await fs.readFile(cssMainFile, "utf8");
|
||||||
const cssBundler = postcss([postcssImport]);
|
const cssBundler = postcss([postcssImport]);
|
||||||
const result = await cssBundler.process(preCss, {from: cssMainFile});
|
const result = await cssBundler.process(preCss, {from: cssMainFile});
|
||||||
await fs.writeFile(path.join(targetDir, "brawl.css"), result.css, "utf8");
|
await fs.writeFile(path.join(targetDir, `${PROJECT_ID}.css`), result.css, "utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
26
src/legacy-polyfill.js
Normal file
26
src/legacy-polyfill.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// polyfills needed for IE11
|
||||||
|
import "core-js/stable";
|
||||||
|
import "regenerator-runtime/runtime";
|
||||||
|
import "mdn-polyfills/Element.prototype.closest";
|
||||||
|
// TODO: contribute this to mdn-polyfills
|
||||||
|
if (!Element.prototype.remove) {
|
||||||
|
Element.prototype.remove = function remove() {
|
||||||
|
this.parentNode.removeChild(this);
|
||||||
|
};
|
||||||
|
}
|
21
src/main.js
21
src/main.js
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,7 +16,8 @@ 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 {xhrRequest} from "./matrix/net/request/xhr.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";
|
||||||
|
@ -24,7 +26,10 @@ import {BrawlView} from "./ui/web/BrawlView.js";
|
||||||
import {Clock} from "./ui/web/dom/Clock.js";
|
import {Clock} from "./ui/web/dom/Clock.js";
|
||||||
import {OnlineStatus} from "./ui/web/dom/OnlineStatus.js";
|
import {OnlineStatus} from "./ui/web/dom/OnlineStatus.js";
|
||||||
|
|
||||||
export default async function main(container) {
|
// Don't use a default export here, as we use multiple entries during legacy build,
|
||||||
|
// which does not support default exports,
|
||||||
|
// see https://github.com/rollup/plugins/tree/master/packages/multi-entry
|
||||||
|
export async function main(container) {
|
||||||
try {
|
try {
|
||||||
// to replay:
|
// to replay:
|
||||||
// const fetchLog = await (await fetch("/fetchlogs/constrainterror.json")).json();
|
// const fetchLog = await (await fetch("/fetchlogs/constrainterror.json")).json();
|
||||||
|
@ -32,13 +37,17 @@ 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:
|
|
||||||
const request = fetchRequest;
|
|
||||||
const sessionInfoStorage = new SessionInfoStorage("brawl_sessions_v1");
|
|
||||||
const clock = new Clock();
|
const clock = new Clock();
|
||||||
|
let request;
|
||||||
|
if (typeof fetch === "function") {
|
||||||
|
request = createFetchRequest(clock.createTimeout);
|
||||||
|
} else {
|
||||||
|
request = xhrRequest;
|
||||||
|
}
|
||||||
|
const sessionInfoStorage = new SessionInfoStorage("brawl_sessions_v1");
|
||||||
const storageFactory = new StorageFactory();
|
const storageFactory = new StorageFactory();
|
||||||
|
|
||||||
const vm = new BrawlViewModel({
|
const vm = new BrawlViewModel({
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -18,6 +19,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 +33,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 +45,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);
|
}
|
||||||
}
|
}
|
||||||
|
|
97
src/matrix/net/request/xhr.js
Normal file
97
src/matrix/net/request/xhr.js
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
AbortError,
|
||||||
|
ConnectionError
|
||||||
|
} from "../../error.js";
|
||||||
|
|
||||||
|
class RequestResult {
|
||||||
|
constructor(promise, xhr) {
|
||||||
|
this._promise = promise;
|
||||||
|
this._xhr = xhr;
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
this._xhr.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
response() {
|
||||||
|
return this._promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function send(url, options) {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open(options.method, url);
|
||||||
|
if (options.headers) {
|
||||||
|
for(const [name, value] of options.headers.entries()) {
|
||||||
|
xhr.setRequestHeader(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.timeout) {
|
||||||
|
xhr.timeout = options.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.send(options.body || null);
|
||||||
|
|
||||||
|
return xhr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function xhrAsPromise(xhr, method, url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
xhr.addEventListener("load", () => resolve(xhr));
|
||||||
|
xhr.addEventListener("abort", () => reject(new AbortError()));
|
||||||
|
xhr.addEventListener("error", () => reject(new ConnectionError(`Error ${method} ${url}`)));
|
||||||
|
xhr.addEventListener("timeout", () => reject(new ConnectionError(`Timeout ${method} ${url}`, true)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCacheBuster(urlStr, random = Math.random) {
|
||||||
|
// XHR doesn't have a good way to disable cache,
|
||||||
|
// so add a random query param
|
||||||
|
// see https://davidtranscend.com/blog/prevent-ie11-cache-ajax-requests/
|
||||||
|
if (urlStr.includes("?")) {
|
||||||
|
urlStr = urlStr + "&";
|
||||||
|
} else {
|
||||||
|
urlStr = urlStr + "?";
|
||||||
|
}
|
||||||
|
return urlStr + `_cacheBuster=${Math.ceil(random() * Number.MAX_SAFE_INTEGER)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function xhrRequest(url, options) {
|
||||||
|
url = addCacheBuster(url);
|
||||||
|
const xhr = send(url, options);
|
||||||
|
const promise = xhrAsPromise(xhr, options.method, url).then(xhr => {
|
||||||
|
const {status} = xhr;
|
||||||
|
let body = xhr.responseText;
|
||||||
|
if (xhr.getResponseHeader("Content-Type") === "application/json") {
|
||||||
|
body = JSON.parse(body);
|
||||||
|
}
|
||||||
|
return {status, body};
|
||||||
|
});
|
||||||
|
return new RequestResult(promise, xhr);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tests() {
|
||||||
|
return {
|
||||||
|
"add cache buster": assert => {
|
||||||
|
const random = () => 0.5;
|
||||||
|
assert.equal(addCacheBuster("http://foo", random), "http://foo?_cacheBuster=4503599627370496");
|
||||||
|
assert.equal(addCacheBuster("http://foo?bar=baz", random), "http://foo?bar=baz&_cacheBuster=4503599627370496");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
src/matrix/net/timeout.js
Normal file
51
src/matrix/net/timeout.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
AbortError,
|
||||||
|
ConnectionError
|
||||||
|
} from "../error.js";
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -47,7 +47,7 @@ export class SwitchView {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
newRoot = errorToDOM(err);
|
newRoot = errorToDOM(err);
|
||||||
}
|
}
|
||||||
const parent = oldRoot.parentElement;
|
const parent = oldRoot.parentNode;
|
||||||
if (parent) {
|
if (parent) {
|
||||||
parent.replaceChild(newRoot, oldRoot);
|
parent.replaceChild(newRoot, oldRoot);
|
||||||
}
|
}
|
||||||
|
|
|
@ -242,8 +242,8 @@ class TemplateBuilder {
|
||||||
if (prevValue !== newValue) {
|
if (prevValue !== newValue) {
|
||||||
prevValue = newValue;
|
prevValue = newValue;
|
||||||
const newNode = renderNode(node);
|
const newNode = renderNode(node);
|
||||||
if (node.parentElement) {
|
if (node.parentNode) {
|
||||||
node.parentElement.replaceChild(newNode, node);
|
node.parentNode.replaceChild(newNode, node);
|
||||||
}
|
}
|
||||||
node = newNode;
|
node = newNode;
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue