From 247ee757d1b8df8cfe0e7b034287ec6de8d22003 Mon Sep 17 00:00:00 2001 From: infinite-persistence <64950861+infinite-persistence@users.noreply.github.com> Date: Wed, 27 Oct 2021 23:07:06 +0800 Subject: [PATCH] ChunkLoadError: ask user to reload instead of automatically reloading (#139) ## Issue We previously automatically reload when there is a chunk error. This works fine if it's the case of new code was pushed recently while the user was active. But if the failure was caused by other things like network problems or the file IS actually missing, we end up in an infinite loop of refreshes. ## New approach Tell the user to reload instead of automatically doing it. --- ui/component/app/index.js | 2 ++ ui/component/app/view.jsx | 15 ++++++++++- ui/constants/action_types.js | 1 + ui/redux/reducers/app.js | 7 +++++ ui/redux/selectors/app.js | 1 + ui/util/lazyImport.js | 52 +++++++++++++++--------------------- 6 files changed, 47 insertions(+), 31 deletions(-) diff --git a/ui/component/app/index.js b/ui/component/app/index.js index 2802bbd2f..db2a7ec2f 100644 --- a/ui/component/app/index.js +++ b/ui/component/app/index.js @@ -21,6 +21,7 @@ import { selectAutoUpdateDownloaded, selectModal, selectActiveChannelClaim, + selectIsReloadRequired, } from 'redux/selectors/app'; import { doGetWalletSyncPreference, doSetLanguage, doSetHomepage } from 'redux/actions/settings'; import { doSyncLoop } from 'redux/actions/sync'; @@ -44,6 +45,7 @@ const select = (state) => ({ languages: selectLoadedLanguages(state), autoUpdateDownloaded: selectAutoUpdateDownloaded(state), isUpgradeAvailable: selectIsUpgradeAvailable(state), + isReloadRequired: selectIsReloadRequired(state), syncError: selectGetSyncErrorMessage(state), uploadCount: selectUploadCount(state), rewards: selectUnclaimedRewards(state), diff --git a/ui/component/app/view.jsx b/ui/component/app/view.jsx index 68604f599..e42b76f05 100644 --- a/ui/component/app/view.jsx +++ b/ui/component/app/view.jsx @@ -6,6 +6,7 @@ import classnames from 'classnames'; import analytics from 'analytics'; import { buildURI, parseURI } from 'util/lbryURI'; import { SIMPLE_SITE } from 'config'; +import Nag from 'component/common/nag'; import Router from 'component/router/index'; import ReactModal from 'react-modal'; import { openContextMenu } from 'util/context-menu'; @@ -17,10 +18,12 @@ import REWARDS from 'rewards'; import usePersistedState from 'effects/use-persisted-state'; import Spinner from 'component/spinner'; import LANGUAGES from 'constants/languages'; + // @if TARGET='app' import useZoom from 'effects/use-zoom'; import useHistoryNav from 'effects/use-history-nav'; // @endif + // @if TARGET='web' import { useDegradedPerformance, @@ -30,11 +33,11 @@ import { STATUS_DOWN, } from 'web/effects/use-degraded-performance'; // @endif + import LANGUAGE_MIGRATIONS from 'constants/language-migrations'; const FileDrop = lazyImport(() => import('component/fileDrop' /* webpackChunkName: "secondary" */)); const ModalRouter = lazyImport(() => import('modal/modalRouter' /* webpackChunkName: "secondary" */)); -const Nag = lazyImport(() => import('component/common/nag' /* webpackChunkName: "secondary" */)); const NagContinueFirstRun = lazyImport(() => import('component/nagContinueFirstRun' /* webpackChunkName: "secondary" */) ); @@ -92,6 +95,7 @@ type Props = { setLanguage: (string) => void, doSetHomepage: (string) => void, isUpgradeAvailable: boolean, + isReloadRequired: boolean, autoUpdateDownloaded: boolean, updatePreferences: () => Promise, getWalletSyncPref: () => Promise, @@ -126,6 +130,7 @@ function App(props: Props) { signIn, autoUpdateDownloaded, isUpgradeAvailable, + isReloadRequired, requestDownloadUpgrade, uploadCount, history, @@ -502,6 +507,14 @@ function App(props: Props) { {user === null && } {/* @endif */} + + {isReloadRequired && ( + window.location.reload()} + /> + )} )} diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index 249d28d7a..06c5ed663 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -32,6 +32,7 @@ export const TOGGLE_YOUTUBE_SYNC_INTEREST = 'TOGGLE_YOUTUBE_SYNC_INTEREST'; export const TOGGLE_SPLASH_ANIMATION = 'TOGGLE_SPLASH_ANIMATION'; export const SET_ACTIVE_CHANNEL = 'SET_ACTIVE_CHANNEL'; export const SET_INCOGNITO = 'SET_INCOGNITO'; +export const RELOAD_REQUIRED = 'RELOAD_REQUIRED'; // Navigation export const CHANGE_AFTER_AUTH_PATH = 'CHANGE_AFTER_AUTH_PATH'; diff --git a/ui/redux/reducers/app.js b/ui/redux/reducers/app.js index 4772ebb2d..c7abe855b 100644 --- a/ui/redux/reducers/app.js +++ b/ui/redux/reducers/app.js @@ -34,6 +34,7 @@ export type AppState = { checkUpgradeTimer: ?number, isUpgradeAvailable: ?boolean, isUpgradeSkipped: ?boolean, + isReloadRequired: ?boolean, hasClickedComment: boolean, enhancedLayout: boolean, splashAnimationEnabled: boolean, @@ -71,6 +72,7 @@ const defaultState: AppState = { checkUpgradeTimer: undefined, isUpgradeAvailable: undefined, isUpgradeSkipped: undefined, + isReloadRequired: undefined, enhancedLayout: false, splashAnimationEnabled: true, searchOptionsExpanded: false, @@ -213,6 +215,11 @@ reducers[ACTIONS.UPGRADE_DOWNLOAD_PROGRESSED] = (state, action) => downloadProgress: action.data.percent, }); +reducers[ACTIONS.RELOAD_REQUIRED] = (state, action) => + Object.assign({}, state, { + isReloadRequired: true, + }); + reducers[ACTIONS.DOWNLOADING_COMPLETED] = (state) => { const { badgeNumber } = state; diff --git a/ui/redux/selectors/app.js b/ui/redux/selectors/app.js index 59f2e80d8..923556e72 100644 --- a/ui/redux/selectors/app.js +++ b/ui/redux/selectors/app.js @@ -21,6 +21,7 @@ export const selectUpdateUrl = createSelector(selectPlatform, (platform) => { export const selectHasClickedComment = (state) => selectState(state).hasClickedComment; export const selectRemoteVersion = (state) => selectState(state).remoteVersion; export const selectIsUpgradeAvailable = (state) => selectState(state).isUpgradeAvailable; +export const selectIsReloadRequired = (state) => selectState(state).isReloadRequired; export const selectUpgradeFilename = createSelector(selectPlatform, selectRemoteVersion, (platform, version) => { switch (platform) { diff --git a/ui/util/lazyImport.js b/ui/util/lazyImport.js index da3e64b04..597fa67c8 100644 --- a/ui/util/lazyImport.js +++ b/ui/util/lazyImport.js @@ -1,34 +1,26 @@ import React from 'react'; +import * as ACTIONS from 'constants/action_types'; -let localStorageAvailable; -try { - localStorageAvailable = Boolean(window.localStorage); -} catch (e) { - localStorageAvailable = false; +const RETRY_DELAY_MS = 2000; +const RETRY_ATTEMPTS = 2; + +function componentLoader(lazyComponent, attemptsLeft) { + return new Promise((resolve, reject) => { + lazyComponent() + .then(resolve) + .catch((error) => { + setTimeout(() => { + if (attemptsLeft === 1) { + window.store.dispatch({ type: ACTIONS.RELOAD_REQUIRED }); + console.error(error.message); // Spew the error so users can report to us if reloading doesn't help. + } else { + componentLoader(lazyComponent, attemptsLeft - 1).then(resolve, reject); + } + }, RETRY_DELAY_MS); + }); + }); } -export const lazyImport = (componentImport) => - React.lazy(async () => { - const pageHasAlreadyBeenForceRefreshed = localStorageAvailable - ? JSON.parse(window.localStorage.getItem('page-has-been-force-refreshed') || 'false') - : false; - - try { - const component = await componentImport(); - if (localStorageAvailable) { - window.localStorage.setItem('page-has-been-force-refreshed', 'false'); - } - return component; - } catch (error) { - if (!pageHasAlreadyBeenForceRefreshed) { - // It's highly likely that the user's session is old. Try reloading once. - if (localStorageAvailable) { - window.localStorage.setItem('page-has-been-force-refreshed', 'true'); - } - return window.location.reload(); - } - - // If it still didn't work, then relay the error. - throw error; - } - }); +export function lazyImport(componentImport) { + return React.lazy(() => componentLoader(componentImport, RETRY_ATTEMPTS)); +}