343 lines
11 KiB
JavaScript
343 lines
11 KiB
JavaScript
import utils from './utils';
|
|
import store from '../store';
|
|
import constants from '../data/constants';
|
|
|
|
const scriptLoadingPromises = Object.create(null);
|
|
const authorizeTimeout = 6 * 60 * 1000; // 2 minutes
|
|
const silentAuthorizeTimeout = 15 * 1000; // 15 secondes (which will be reattempted)
|
|
const networkTimeout = 30 * 1000; // 30 sec
|
|
let isConnectionDown = false;
|
|
const userInactiveAfter = 3 * 60 * 1000; // 3 minutes (twice the default sync period)
|
|
let lastActivity = 0;
|
|
let lastFocus = 0;
|
|
let isConfLoading = false;
|
|
let isConfLoaded = false;
|
|
|
|
function parseHeaders(xhr) {
|
|
const pairs = xhr.getAllResponseHeaders().trim().split('\n');
|
|
const headers = {};
|
|
pairs.forEach((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);
|
|
}
|
|
|
|
export default {
|
|
async init() {
|
|
// Keep track of the last user activity
|
|
const setLastActivity = () => {
|
|
lastActivity = Date.now();
|
|
};
|
|
window.document.addEventListener('mousedown', setLastActivity);
|
|
window.document.addEventListener('keydown', setLastActivity);
|
|
window.document.addEventListener('touchstart', setLastActivity);
|
|
|
|
// Keep track of the last window focus
|
|
lastFocus = 0;
|
|
const setLastFocus = () => {
|
|
lastFocus = Date.now();
|
|
localStorage.setItem(store.getters['workspace/lastFocusKey'], lastFocus);
|
|
setLastActivity();
|
|
};
|
|
if (document.hasFocus()) {
|
|
setLastFocus();
|
|
}
|
|
window.addEventListener('focus', setLastFocus);
|
|
|
|
// Check that browser is online periodically
|
|
const checkOffline = async () => {
|
|
const isBrowserOffline = window.navigator.onLine === false;
|
|
if (!isBrowserOffline
|
|
&& store.state.lastOfflineCheck + networkTimeout + 5000 < Date.now()
|
|
&& this.isUserActive()
|
|
) {
|
|
store.commit('updateLastOfflineCheck');
|
|
const script = document.createElement('script');
|
|
let timeout;
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
script.onload = resolve;
|
|
script.onerror = reject;
|
|
script.src = `https://www.gstatic.cn/charts/loader.js?${Date.now()}`;
|
|
try {
|
|
document.head.appendChild(script); // This can fail with bad network
|
|
timeout = setTimeout(reject, networkTimeout);
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
isConnectionDown = false;
|
|
} catch (e) {
|
|
isConnectionDown = true;
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
document.head.removeChild(script);
|
|
}
|
|
}
|
|
const offline = isBrowserOffline || isConnectionDown;
|
|
if (store.state.offline !== offline) {
|
|
store.commit('setOffline', offline);
|
|
if (offline) {
|
|
store.dispatch('notification/error', '已离线!');
|
|
} else {
|
|
store.dispatch('notification/info', '恢复上线了!');
|
|
this.getServerConf();
|
|
}
|
|
}
|
|
};
|
|
|
|
utils.setInterval(checkOffline, 1000);
|
|
window.addEventListener('online', () => {
|
|
isConnectionDown = false;
|
|
checkOffline();
|
|
});
|
|
window.addEventListener('offline', checkOffline);
|
|
await checkOffline();
|
|
this.getServerConf();
|
|
},
|
|
async getServerConf() {
|
|
if (!store.state.offline && !isConfLoading && !isConfLoaded) {
|
|
try {
|
|
isConfLoading = true;
|
|
const res = await this.request({ url: 'conf' });
|
|
await store.dispatch('data/setServerConf', res.body);
|
|
isConfLoaded = true;
|
|
} finally {
|
|
isConfLoading = false;
|
|
}
|
|
}
|
|
},
|
|
isWindowFocused() {
|
|
// We don't use state.workspace.lastFocus as it's not reactive
|
|
const storedLastFocus = localStorage.getItem(store.getters['workspace/lastFocusKey']);
|
|
return parseInt(storedLastFocus, 10) === lastFocus;
|
|
},
|
|
isUserActive() {
|
|
return lastActivity > Date.now() - userInactiveAfter && this.isWindowFocused();
|
|
},
|
|
isConfLoaded() {
|
|
return !!Object.keys(store.getters['data/serverConf']).length;
|
|
},
|
|
async 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];
|
|
},
|
|
async startOauth2(url, params = {}, silent = false, reattempt = false) {
|
|
try {
|
|
// Build the authorize URL
|
|
const state = utils.uid();
|
|
const authorizeUrl = utils.addQueryParams(url, {
|
|
...params,
|
|
state,
|
|
redirect_uri: constants.oauth2RedirectUri,
|
|
});
|
|
|
|
let iframeElt;
|
|
let wnd;
|
|
if (silent) {
|
|
// Use an iframe as wnd for silent mode
|
|
iframeElt = utils.createHiddenIframe(authorizeUrl);
|
|
document.body.appendChild(iframeElt);
|
|
wnd = iframeElt.contentWindow;
|
|
} else {
|
|
// Open a tab otherwise
|
|
wnd = window.open(authorizeUrl);
|
|
if (!wnd) {
|
|
throw new Error('The authorize window was blocked.');
|
|
}
|
|
}
|
|
|
|
let checkClosedInterval;
|
|
let closeTimeout;
|
|
let msgHandler;
|
|
try {
|
|
return await new Promise((resolve, reject) => {
|
|
if (silent) {
|
|
iframeElt.onerror = () => {
|
|
reject(new Error('Unknown error.'));
|
|
};
|
|
closeTimeout = setTimeout(() => {
|
|
if (!reattempt) {
|
|
reject(new Error('REATTEMPT'));
|
|
} else {
|
|
isConnectionDown = true;
|
|
store.commit('setOffline', true);
|
|
store.commit('updateLastOfflineCheck');
|
|
reject(new Error('You are offline.'));
|
|
}
|
|
}, silentAuthorizeTimeout);
|
|
} else {
|
|
closeTimeout = setTimeout(() => {
|
|
reject(new Error('Timeout.'));
|
|
}, authorizeTimeout);
|
|
}
|
|
|
|
msgHandler = (event) => {
|
|
if (event.source === wnd && event.origin === constants.origin) {
|
|
const data = utils.parseQueryParams(`${event.data}`.slice(1));
|
|
if (data.error || data.state !== state) {
|
|
console.error(data); // eslint-disable-line no-console
|
|
reject(new Error('Could not get required authorization.'));
|
|
} else {
|
|
resolve({
|
|
accessToken: data.access_token,
|
|
code: data.code,
|
|
idToken: data.id_token,
|
|
expiresIn: data.expires_in,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener('message', msgHandler);
|
|
if (!silent) {
|
|
checkClosedInterval = setInterval(() => {
|
|
if (wnd.closed) {
|
|
reject(new Error('Authorize window was closed.'));
|
|
}
|
|
}, 250);
|
|
}
|
|
});
|
|
} finally {
|
|
clearInterval(checkClosedInterval);
|
|
if (!silent && !wnd.closed) {
|
|
wnd.close();
|
|
}
|
|
if (iframeElt) {
|
|
document.body.removeChild(iframeElt);
|
|
}
|
|
clearTimeout(closeTimeout);
|
|
window.removeEventListener('message', msgHandler);
|
|
}
|
|
} catch (e) {
|
|
if (e.message === 'REATTEMPT') {
|
|
return this.startOauth2(url, params, silent, true);
|
|
}
|
|
throw e;
|
|
}
|
|
},
|
|
async request(config, offlineCheck = false) {
|
|
let retryAfter = 500; // 500 ms
|
|
const maxRetryAfter = 10 * 1000; // 10 sec
|
|
const sanitizedConfig = Object.assign({}, config);
|
|
sanitizedConfig.timeout = sanitizedConfig.timeout || networkTimeout;
|
|
sanitizedConfig.headers = Object.assign({}, sanitizedConfig.headers);
|
|
if (sanitizedConfig.body && typeof sanitizedConfig.body === 'object') {
|
|
sanitizedConfig.body = JSON.stringify(sanitizedConfig.body);
|
|
sanitizedConfig.headers['Content-Type'] = 'application/json';
|
|
} else if (sanitizedConfig.formData) {
|
|
const data = new FormData();
|
|
Object.keys(sanitizedConfig.formData).forEach((key) => {
|
|
const formVal = sanitizedConfig.formData[key];
|
|
data.append(key, formVal);
|
|
});
|
|
sanitizedConfig.formData = data;
|
|
}
|
|
const attempt = async () => {
|
|
try {
|
|
return await new Promise((resolve, reject) => {
|
|
if (offlineCheck) {
|
|
store.commit('updateLastOfflineCheck');
|
|
}
|
|
|
|
const xhr = new window.XMLHttpRequest();
|
|
xhr.withCredentials = sanitizedConfig.withCredentials || false;
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
xhr.abort();
|
|
if (offlineCheck) {
|
|
isConnectionDown = true;
|
|
store.commit('setOffline', true);
|
|
reject(new Error('You are offline.'));
|
|
} else {
|
|
reject(new Error('Network request timeout.'));
|
|
}
|
|
}, sanitizedConfig.timeout);
|
|
|
|
xhr.onload = () => {
|
|
if (offlineCheck) {
|
|
isConnectionDown = false;
|
|
}
|
|
clearTimeout(timeoutId);
|
|
const result = {
|
|
status: xhr.status,
|
|
headers: parseHeaders(xhr),
|
|
body: sanitizedConfig.blob ? xhr.response : xhr.responseText,
|
|
};
|
|
if (!sanitizedConfig.raw && !sanitizedConfig.blob) {
|
|
try {
|
|
result.body = JSON.parse(result.body);
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
}
|
|
if (result.status >= 200 && result.status < 300) {
|
|
resolve(result);
|
|
} else {
|
|
reject(result);
|
|
}
|
|
};
|
|
|
|
xhr.onerror = () => {
|
|
clearTimeout(timeoutId);
|
|
if (offlineCheck) {
|
|
isConnectionDown = true;
|
|
store.commit('setOffline', true);
|
|
reject(new Error('You are offline.'));
|
|
} else {
|
|
reject(new Error('Network request failed.'));
|
|
}
|
|
};
|
|
|
|
const url = utils.addQueryParams(sanitizedConfig.url, sanitizedConfig.params);
|
|
xhr.open(sanitizedConfig.method || 'GET', url);
|
|
Object.entries(sanitizedConfig.headers).forEach(([key, value]) => {
|
|
if (value) {
|
|
xhr.setRequestHeader(key, `${value}`);
|
|
}
|
|
});
|
|
if (sanitizedConfig.blob) {
|
|
xhr.responseType = 'blob';
|
|
}
|
|
xhr.send(sanitizedConfig.body || sanitizedConfig.formData || null);
|
|
});
|
|
} catch (err) {
|
|
// Try again later in case of retriable error
|
|
if (isRetriable(err) && retryAfter < maxRetryAfter) {
|
|
await new Promise((resolve) => {
|
|
setTimeout(resolve, retryAfter);
|
|
// Exponential backoff
|
|
retryAfter *= 2;
|
|
});
|
|
return attempt();
|
|
}
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
return attempt();
|
|
},
|
|
};
|