Fix localStorage crash

## Ticket
1572
Always check for availability before use.

## Changes
- Consolidated the keys into one place for easier tracking.
  - It'll also be easier to code using constant autocomplete.
- Cleaned up the wrapper to be as close as the original.
- Updated existing code to use the wrapper (even if they already handled the availability) to encourage future code to just use the wrapper.
This commit is contained in:
infinite-persistence 2022-05-26 10:44:26 +08:00 committed by Thomas Zarebczan
parent 3ed05e62a5
commit a73694deb4
9 changed files with 67 additions and 46 deletions

View file

@ -46,8 +46,6 @@ export const GA_DIMENSIONS = {
const isProduction = process.env.NODE_ENV === 'production';
const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.includes('dev');
export const SHARE_INTERNAL = 'shareInternal';
const WATCHMAN_BACKEND_ENDPOINT = 'https://watchman.na-backend.odysee.com/reports/playback';
const SEND_DATA_TO_WATCHMAN_INTERVAL = 10; // in seconds

View file

@ -1,4 +0,0 @@
// Local Storage keys
export const TUS_LOCKED_UPLOADS = 'tusLockedUploads';
export const TUS_REMOVED_UPLOADS = 'tusRemovedUploads';
export const TUS_REFRESH_LOCK = 'tusRefreshLock';

View file

@ -45,8 +45,9 @@ import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user';
import { doSetPrefsReady, doPreferenceGet, doPopulateSharedUserState, syncInvalidated } from 'redux/actions/sync';
import { doAuthenticate } from 'redux/actions/user';
import { lbrySettings as config, version as appVersion } from 'package.json';
import analytics, { SHARE_INTERNAL } from 'analytics';
import analytics from 'analytics';
import { doSignOutCleanup } from 'util/saved-passwords';
import { LocalStorage, LS } from 'util/storage';
import { doNotificationSocketConnect } from 'redux/actions/websocket';
import { stringifyServerParam, shouldSetSetting } from 'util/sync-settings';
@ -348,7 +349,7 @@ export function doDaemonReady() {
const state = getState();
// TODO: call doFetchDaemonSettings, then get usage data, and call doAuthenticate once they are loaded into the store
const shareUsageData = IS_WEB || window.localStorage.getItem(SHARE_INTERNAL) === 'true';
const shareUsageData = IS_WEB || LocalStorage.getItem(LS.SHARE_INTERNAL) === 'true';
dispatch(
doAuthenticate(

View file

@ -10,11 +10,9 @@ import {
filterUpcomingLiveStreamClaims,
} from 'util/livestream';
import moment from 'moment';
import { isLocalStorageAvailable } from 'util/storage';
import { LocalStorage, LS } from 'util/storage';
import { isEmpty } from 'util/object';
const localStorageAvailable = isLocalStorageAvailable();
export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dispatch, getState: GetState) => {
dispatch({
type: ACTIONS.FETCH_NO_SOURCE_CLAIMS_STARTED,
@ -118,20 +116,20 @@ const findActiveStreams = async (
};
export const doFetchChannelLiveStatus = (channelId: string) => async (dispatch: Dispatch) => {
const statusForId = `channel-live-status`;
const localStatus = localStorageAvailable && window.localStorage.getItem(statusForId);
const statusForId = LS.CHANNEL_LIVE_STATUS;
const localStatus = LocalStorage.getItem(statusForId);
try {
const { channelStatus, channelData } = await fetchLiveChannel(channelId);
// store live state locally, and force 2 non-live statuses before returninig NOT LIVE. This allows for the stream to finish before disposing player.
if (localStatus === LiveStatus.LIVE && channelStatus === LiveStatus.NOT_LIVE) {
localStorageAvailable && window.localStorage.removeItem(statusForId);
LocalStorage.removeItem(statusForId);
return;
}
if (channelStatus === LiveStatus.NOT_LIVE && !localStatus) {
dispatch({ type: ACTIONS.REMOVE_CHANNEL_FROM_ACTIVE_LIVESTREAMS, data: { channelId } });
localStorageAvailable && window.localStorage.removeItem(statusForId);
LocalStorage.removeItem(statusForId);
return;
}
@ -148,10 +146,10 @@ export const doFetchChannelLiveStatus = (channelId: string) => async (dispatch:
dispatch({ type: ACTIONS.ADD_CHANNEL_TO_ACTIVE_LIVESTREAMS, data: { ...channelData } });
}
localStorageAvailable && window.localStorage.setItem(statusForId, channelStatus);
LocalStorage.setItem(statusForId, channelStatus);
} catch (err) {
dispatch({ type: ACTIONS.REMOVE_CHANNEL_FROM_ACTIVE_LIVESTREAMS, data: { channelId } });
localStorageAvailable && window.localStorage.removeItem(statusForId);
LocalStorage.removeItem(statusForId);
}
};

View file

@ -15,8 +15,8 @@ import rewards from 'rewards';
import { Lbryio } from 'lbryinc';
import { DOMAIN, LOCALE_API } from 'config';
import { getDefaultLanguage } from 'util/default-languages';
import { LocalStorage, LS } from 'util/storage';
const AUTH_IN_PROGRESS = 'authInProgress';
export let sessionStorageAvailable = false;
const CHECK_INTERVAL = 200;
const AUTH_WAIT_TIMEOUT = 10000;
@ -91,9 +91,9 @@ function checkAuthBusy() {
if (!IS_WEB || !sessionStorageAvailable) {
return resolve();
}
const inProgress = window.sessionStorage.getItem(AUTH_IN_PROGRESS);
const inProgress = LocalStorage.getItem(LS.AUTH_IN_PROGRESS);
if (!inProgress) {
window.sessionStorage.setItem(AUTH_IN_PROGRESS, 'true');
LocalStorage.setItem(LS.AUTH_IN_PROGRESS, 'true');
return resolve();
} else {
if (Date.now() - time < AUTH_WAIT_TIMEOUT) {
@ -175,7 +175,7 @@ export function doAuthenticate(
return Lbryio.authenticate(DOMAIN, getDefaultLanguage());
})
.then((user) => {
if (sessionStorageAvailable) window.sessionStorage.removeItem(AUTH_IN_PROGRESS);
LocalStorage.removeItem(LS.AUTH_IN_PROGRESS);
Lbryio.getAuthToken().then((token) => {
dispatch({
type: ACTIONS.AUTHENTICATION_SUCCESS,
@ -199,7 +199,7 @@ export function doAuthenticate(
});
})
.catch((error) => {
if (sessionStorageAvailable) window.sessionStorage.removeItem(AUTH_IN_PROGRESS);
LocalStorage.removeItem(LS.AUTH_IN_PROGRESS);
dispatch({
type: ACTIONS.AUTHENTICATION_FAILURE,

View file

@ -1,4 +1,5 @@
import { createSelector } from 'reselect';
import { LocalStorage } from 'util/storage';
export const selectState = (state) => state.user || {};
@ -126,7 +127,7 @@ export const selectOdyseeMembershipIsPremiumPlus = (state) => {
*/
export const selectHasOdyseeMembership = (state) => {
// @if process.env.NODE_ENV!='production'
const override = window.localStorage.getItem('hasMembershipOverride');
const override = LocalStorage.getItem('hasMembershipOverride');
if (override) return override === 'true';
// @endif

View file

@ -1,3 +1,21 @@
// @flow
// ****************************************************************************
// ****************************************************************************
export const LS = Object.freeze({
AUTH_IN_PROGRESS: 'authInProgress',
CHANNEL_LIVE_STATUS: 'channel-live-status',
GDPR_REQUIRED: 'gdprRequired', // <-- should be using 'locale/get', right?
SHARE_INTERNAL: 'shareInternal',
TUS_LOCKED_UPLOADS: 'tusLockedUploads',
TUS_REFRESH_LOCK: 'tusRefreshLock',
TUS_REMOVED_UPLOADS: 'tusRemovedUploads',
});
// ****************************************************************************
// ****************************************************************************
export function isLocalStorageAvailable() {
try {
return Boolean(window.localStorage);
@ -24,14 +42,23 @@ export function getLocalStorageSummary() {
}
}
// ****************************************************************************
// LocalStorage (wrapper for 'window.localStorage')
// ****************************************************************************
// This assumes that local storage availability never changes after boot.
const localStorageAvailable = isLocalStorageAvailable();
export function getLocalStorageItem(key) {
return localStorageAvailable ? window.localStorage.getItem(key) : undefined;
}
export const LocalStorage = {
setItem: (key: string, value: string) => {
if (localStorageAvailable) window.localStorage.setItem(key, value);
},
export function setLocalStorageItem(key, value) {
if (localStorageAvailable) {
window.localStorage.setItem(key, value);
}
}
getItem: (key: string) => {
return localStorageAvailable ? window.localStorage.getItem(key) : undefined;
},
removeItem: (key: string) => {
if (localStorageAvailable) window.localStorage.removeItem(key);
},
};

View file

@ -9,8 +9,7 @@
*/
import { v4 as uuid } from 'uuid';
import { TUS_LOCKED_UPLOADS, TUS_REFRESH_LOCK, TUS_REMOVED_UPLOADS } from 'constants/storage';
import { isLocalStorageAvailable } from 'util/storage';
import { isLocalStorageAvailable, LocalStorage, LS } from 'util/storage';
import { doUpdateUploadRemove, doUpdateUploadProgress } from 'redux/actions/publish';
const localStorageAvailable = isLocalStorageAvailable();
@ -32,7 +31,7 @@ function getTabId() {
function getLockedUploads() {
if (localStorageAvailable) {
const storedValue = window.localStorage.getItem(TUS_LOCKED_UPLOADS);
const storedValue = LocalStorage.getItem(LS.TUS_LOCKED_UPLOADS);
return storedValue ? JSON.parse(storedValue) : {};
}
return {};
@ -47,7 +46,7 @@ export function tusLockAndNotify(guid: string) {
const lockedUploads = getLockedUploads();
if (!lockedUploads[guid] && localStorageAvailable) {
lockedUploads[guid] = getTabId();
window.localStorage.setItem(TUS_LOCKED_UPLOADS, JSON.stringify(lockedUploads));
LocalStorage.setItem(LS.TUS_LOCKED_UPLOADS, JSON.stringify(lockedUploads));
}
}
@ -75,9 +74,9 @@ export function tusUnlockAndNotify(guid?: string) {
}
if (Object.keys(lockedUploads).length > 0) {
window.localStorage.setItem(TUS_LOCKED_UPLOADS, JSON.stringify(lockedUploads));
LocalStorage.setItem(LS.TUS_LOCKED_UPLOADS, JSON.stringify(lockedUploads));
} else {
window.localStorage.removeItem(TUS_LOCKED_UPLOADS);
LocalStorage.removeItem(LS.TUS_LOCKED_UPLOADS);
}
}
@ -87,7 +86,7 @@ export function tusUnlockAndNotify(guid?: string) {
function getRemovedUploads() {
if (localStorageAvailable) {
const storedValue = window.localStorage.getItem(TUS_REMOVED_UPLOADS);
const storedValue = LocalStorage.getItem(LS.TUS_REMOVED_UPLOADS);
return storedValue ? storedValue.split(',') : [];
}
return [];
@ -97,18 +96,18 @@ export function tusRemoveAndNotify(guid: string) {
if (!localStorageAvailable) return;
const removedUploads = getRemovedUploads();
removedUploads.push(guid);
window.localStorage.setItem(TUS_REMOVED_UPLOADS, removedUploads.join(','));
LocalStorage.setItem(LS.TUS_REMOVED_UPLOADS, removedUploads.join(','));
}
export function tusClearRemovedUploads() {
if (!localStorageAvailable) return;
window.localStorage.removeItem(TUS_REMOVED_UPLOADS);
LocalStorage.removeItem(LS.TUS_REMOVED_UPLOADS);
}
export function tusClearLockedUploads() {
if (!localStorageAvailable) return;
window.localStorage.removeItem(TUS_LOCKED_UPLOADS);
window.localStorage.setItem(TUS_REFRESH_LOCK, Math.random());
LocalStorage.removeItem(LS.TUS_LOCKED_UPLOADS);
LocalStorage.setItem(LS.TUS_REFRESH_LOCK, String(Math.random()));
}
// ****************************************************************************
@ -117,17 +116,17 @@ export function tusClearLockedUploads() {
export function tusHandleTabUpdates(storageKey: string) {
switch (storageKey) {
case TUS_LOCKED_UPLOADS:
case LS.TUS_LOCKED_UPLOADS:
// The locked IDs are in localStorage, but related GUI is unaware.
// Send a redux update to force an update.
window.store.dispatch(doUpdateUploadProgress({ guid: 'force--update' }));
break;
case TUS_REFRESH_LOCK:
case LS.TUS_REFRESH_LOCK:
window.store.dispatch(doUpdateUploadProgress({ guid: 'refresh--lock' }));
break;
case TUS_REMOVED_UPLOADS:
case LS.TUS_REMOVED_UPLOADS:
// The other tab's store has removed this upload, so it's safe to do the
// same without affecting rehydration.
if (localStorageAvailable) {

View file

@ -9,6 +9,7 @@ import classnames from 'classnames';
import { platform } from 'util/platform';
import Icon from 'component/common/icon';
import * as ICONS from 'constants/icons';
import { LocalStorage, LS } from 'util/storage';
// prettier-ignore
const AD_CONFIGS = Object.freeze({
@ -64,7 +65,7 @@ function Ads(props: Props) {
const mobileAds = platform.isAndroid() || platform.isIOS();
// this is populated from app based on location
const isInEu = localStorage.getItem('gdprRequired') === 'true';
const isInEu = LocalStorage.getItem(LS.GDPR_REQUIRED) === 'true';
const adConfig = isInEu ? AD_CONFIGS.EU : mobileAds ? AD_CONFIGS.MOBILE : AD_CONFIGS.DEFAULT;
function resolveAdVisibility() {