Stackedit/src/services/networkSvc.js
2017-10-05 08:18:21 +01:00

272 lines
8.3 KiB
JavaScript

import utils from './utils';
import store from '../store';
const scriptLoadingPromises = Object.create(null);
const oauth2AuthorizationTimeout = 120 * 1000; // 2 minutes
const networkTimeout = 30 * 1000; // 30 sec
let isConnectionDown = false;
export default {
loadScript(url) {
if (!scriptLoadingPromises[url]) {
scriptLoadingPromises[url] = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.onload = resolve;
script.onerror = () => {
scriptLoadingPromises[url] = null;
reject();
};
script.src = url;
document.head.appendChild(script);
});
}
return scriptLoadingPromises[url];
},
startOauth2(url, params = {}, silent = false) {
// Build the authorize URL
const state = utils.uid();
params.state = state;
params.redirect_uri = utils.oauth2RedirectUri;
const authorizeUrl = utils.addQueryParams(url, params);
let iframeElt;
let wnd;
if (silent) {
// Use an iframe as wnd for silent mode
iframeElt = document.createElement('iframe');
iframeElt.style.position = 'absolute';
iframeElt.style.left = '-9999px';
iframeElt.src = authorizeUrl;
document.body.appendChild(iframeElt);
wnd = iframeElt.contentWindow;
} else {
// Open a tab otherwise
wnd = window.open(authorizeUrl);
if (!wnd) {
return Promise.reject('The authorize window was blocked.');
}
}
return new Promise((resolve, reject) => {
let checkClosedInterval;
let closeTimeout;
let msgHandler;
let clean = () => {
clearInterval(checkClosedInterval);
if (!silent && !wnd.closed) {
wnd.close();
}
if (iframeElt) {
document.body.removeChild(iframeElt);
}
clearTimeout(closeTimeout);
window.removeEventListener('message', msgHandler);
clean = () => Promise.resolve(); // Prevent from cleaning several times
return Promise.resolve();
};
if (silent) {
iframeElt.onerror = () => clean()
.then(() => reject('Unknown error.'));
closeTimeout = setTimeout(
() => clean()
.then(() => {
isConnectionDown = true;
store.commit('setOffline', true);
store.commit('updateLastOfflineCheck');
reject('You are offline.');
}),
networkTimeout);
} else {
closeTimeout = setTimeout(
() => clean()
.then(() => reject('Timeout.')),
oauth2AuthorizationTimeout);
}
msgHandler = event => event.source === wnd && event.origin === utils.origin && clean()
.then(() => {
const data = {};
`${event.data}`.slice(1).split('&').forEach((param) => {
const [key, value] = param.split('=').map(decodeURIComponent);
data[key] = value;
});
if (data.error || data.state !== state) {
reject('Could not get required authorization.');
} else {
resolve({
accessToken: data.access_token,
code: data.code,
expiresIn: data.expires_in,
});
}
});
window.addEventListener('message', msgHandler);
if (!silent) {
checkClosedInterval = setInterval(() => wnd.closed && clean()
.then(() => reject('Authorize window was closed.')), 250);
}
});
},
request(configParam, offlineCheck = false) {
let retryAfter = 500; // 500 ms
const maxRetryAfter = 30 * 1000; // 30 sec
const config = Object.assign({}, configParam);
config.timeout = config.timeout || networkTimeout;
config.headers = Object.assign({}, config.headers);
if (config.body && typeof config.body === 'object') {
config.body = JSON.stringify(config.body);
config.headers['Content-Type'] = 'application/json';
}
function parseHeaders(xhr) {
const pairs = xhr.getAllResponseHeaders().trim().split('\n');
return pairs.reduce((headers, header) => {
const split = header.trim().split(':');
const key = split.shift().trim().toLowerCase();
const value = split.join(':').trim();
headers[key] = value;
return headers;
}, {});
}
function isRetriable(err) {
if (err.status === 403) {
const googleReason = ((((err.body || {}).error || {}).errors || [])[0] || {}).reason;
return googleReason === 'rateLimitExceeded' || googleReason === 'userRateLimitExceeded';
}
return err.status === 429 || (err.status >= 500 && err.status < 600);
}
const attempt =
() => new Promise((resolve, reject) => {
if (offlineCheck) {
store.commit('updateLastOfflineCheck');
}
const xhr = new window.XMLHttpRequest();
let timeoutId;
xhr.onload = () => {
if (offlineCheck) {
isConnectionDown = false;
}
clearTimeout(timeoutId);
const result = {
status: xhr.status,
headers: parseHeaders(xhr),
body: xhr.responseText,
};
if (!config.raw) {
try {
result.body = JSON.parse(result.body);
} catch (e) {
// ignore
}
}
if (result.status >= 200 && result.status < 300) {
resolve(result);
return;
}
reject(result);
};
xhr.onerror = () => {
clearTimeout(timeoutId);
if (offlineCheck) {
isConnectionDown = true;
store.commit('setOffline', true);
reject('You are offline.');
} else {
reject('Network request failed.');
}
};
timeoutId = setTimeout(() => {
xhr.abort();
if (offlineCheck) {
isConnectionDown = true;
store.commit('setOffline', true);
reject('You are offline.');
} else {
reject('Network request timeout.');
}
}, config.timeout);
const url = utils.addQueryParams(config.url, config.params);
xhr.open(config.method || 'GET', url);
Object.keys(config.headers).forEach((key) => {
const value = config.headers[key];
if (value) {
xhr.setRequestHeader(key, `${value}`);
}
});
xhr.send(config.body || null);
})
.catch((err) => {
// Try again later in case of retriable error
if (isRetriable(err) && retryAfter < maxRetryAfter) {
return new Promise(
(resolve) => {
setTimeout(resolve, retryAfter);
// Exponential backoff
retryAfter *= 2;
})
.then(attempt);
}
throw err;
});
return attempt();
},
};
function checkOffline() {
const isBrowserOffline = window.navigator.onLine === false;
if (!isBrowserOffline &&
store.state.lastOfflineCheck + networkTimeout + 5000 < Date.now() &&
utils.isUserActive()
) {
store.commit('updateLastOfflineCheck');
new Promise((resolve, reject) => {
const script = document.createElement('script');
let timeout;
const cleaner = (cb, res) => () => {
clearTimeout(timeout);
cb(res);
document.head.removeChild(script);
};
script.onload = cleaner(resolve);
script.onerror = cleaner(reject);
script.src = `https://apis.google.com/js/api.js?${Date.now()}`;
try {
document.head.appendChild(script); // This can fail with bad network
timeout = setTimeout(cleaner(reject), networkTimeout);
} catch (e) {
reject(e);
}
})
.then(() => {
isConnectionDown = false;
}, () => {
isConnectionDown = true;
});
}
const offline = isBrowserOffline || isConnectionDown;
if (store.state.offline !== offline) {
store.commit('setOffline', offline);
if (offline) {
store.dispatch('notification/error', 'You are offline.');
} else {
store.dispatch('notification/info', 'You are back online!');
}
}
}
utils.setInterval(checkOffline, 1000);
window.addEventListener('online', () => {
isConnectionDown = false;
checkOffline();
});
window.addEventListener('offline', checkOffline);