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 isProduction = process.env.NODE_ENV === 'production';
const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.includes('dev'); 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 WATCHMAN_BACKEND_ENDPOINT = 'https://watchman.na-backend.odysee.com/reports/playback';
const SEND_DATA_TO_WATCHMAN_INTERVAL = 10; // in seconds 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 { doSetPrefsReady, doPreferenceGet, doPopulateSharedUserState, syncInvalidated } from 'redux/actions/sync';
import { doAuthenticate } from 'redux/actions/user'; import { doAuthenticate } from 'redux/actions/user';
import { lbrySettings as config, version as appVersion } from 'package.json'; 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 { doSignOutCleanup } from 'util/saved-passwords';
import { LocalStorage, LS } from 'util/storage';
import { doNotificationSocketConnect } from 'redux/actions/websocket'; import { doNotificationSocketConnect } from 'redux/actions/websocket';
import { stringifyServerParam, shouldSetSetting } from 'util/sync-settings'; import { stringifyServerParam, shouldSetSetting } from 'util/sync-settings';
@ -348,7 +349,7 @@ export function doDaemonReady() {
const state = getState(); const state = getState();
// TODO: call doFetchDaemonSettings, then get usage data, and call doAuthenticate once they are loaded into the store // 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( dispatch(
doAuthenticate( doAuthenticate(

View file

@ -10,11 +10,9 @@ import {
filterUpcomingLiveStreamClaims, filterUpcomingLiveStreamClaims,
} from 'util/livestream'; } from 'util/livestream';
import moment from 'moment'; import moment from 'moment';
import { isLocalStorageAvailable } from 'util/storage'; import { LocalStorage, LS } from 'util/storage';
import { isEmpty } from 'util/object'; import { isEmpty } from 'util/object';
const localStorageAvailable = isLocalStorageAvailable();
export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dispatch, getState: GetState) => { export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dispatch, getState: GetState) => {
dispatch({ dispatch({
type: ACTIONS.FETCH_NO_SOURCE_CLAIMS_STARTED, type: ACTIONS.FETCH_NO_SOURCE_CLAIMS_STARTED,
@ -118,20 +116,20 @@ const findActiveStreams = async (
}; };
export const doFetchChannelLiveStatus = (channelId: string) => async (dispatch: Dispatch) => { export const doFetchChannelLiveStatus = (channelId: string) => async (dispatch: Dispatch) => {
const statusForId = `channel-live-status`; const statusForId = LS.CHANNEL_LIVE_STATUS;
const localStatus = localStorageAvailable && window.localStorage.getItem(statusForId); const localStatus = LocalStorage.getItem(statusForId);
try { try {
const { channelStatus, channelData } = await fetchLiveChannel(channelId); 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. // 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) { if (localStatus === LiveStatus.LIVE && channelStatus === LiveStatus.NOT_LIVE) {
localStorageAvailable && window.localStorage.removeItem(statusForId); LocalStorage.removeItem(statusForId);
return; return;
} }
if (channelStatus === LiveStatus.NOT_LIVE && !localStatus) { if (channelStatus === LiveStatus.NOT_LIVE && !localStatus) {
dispatch({ type: ACTIONS.REMOVE_CHANNEL_FROM_ACTIVE_LIVESTREAMS, data: { channelId } }); dispatch({ type: ACTIONS.REMOVE_CHANNEL_FROM_ACTIVE_LIVESTREAMS, data: { channelId } });
localStorageAvailable && window.localStorage.removeItem(statusForId); LocalStorage.removeItem(statusForId);
return; return;
} }
@ -148,10 +146,10 @@ export const doFetchChannelLiveStatus = (channelId: string) => async (dispatch:
dispatch({ type: ACTIONS.ADD_CHANNEL_TO_ACTIVE_LIVESTREAMS, data: { ...channelData } }); dispatch({ type: ACTIONS.ADD_CHANNEL_TO_ACTIVE_LIVESTREAMS, data: { ...channelData } });
} }
localStorageAvailable && window.localStorage.setItem(statusForId, channelStatus); LocalStorage.setItem(statusForId, channelStatus);
} catch (err) { } catch (err) {
dispatch({ type: ACTIONS.REMOVE_CHANNEL_FROM_ACTIVE_LIVESTREAMS, data: { channelId } }); 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 { Lbryio } from 'lbryinc';
import { DOMAIN, LOCALE_API } from 'config'; import { DOMAIN, LOCALE_API } from 'config';
import { getDefaultLanguage } from 'util/default-languages'; import { getDefaultLanguage } from 'util/default-languages';
import { LocalStorage, LS } from 'util/storage';
const AUTH_IN_PROGRESS = 'authInProgress';
export let sessionStorageAvailable = false; export let sessionStorageAvailable = false;
const CHECK_INTERVAL = 200; const CHECK_INTERVAL = 200;
const AUTH_WAIT_TIMEOUT = 10000; const AUTH_WAIT_TIMEOUT = 10000;
@ -91,9 +91,9 @@ function checkAuthBusy() {
if (!IS_WEB || !sessionStorageAvailable) { if (!IS_WEB || !sessionStorageAvailable) {
return resolve(); return resolve();
} }
const inProgress = window.sessionStorage.getItem(AUTH_IN_PROGRESS); const inProgress = LocalStorage.getItem(LS.AUTH_IN_PROGRESS);
if (!inProgress) { if (!inProgress) {
window.sessionStorage.setItem(AUTH_IN_PROGRESS, 'true'); LocalStorage.setItem(LS.AUTH_IN_PROGRESS, 'true');
return resolve(); return resolve();
} else { } else {
if (Date.now() - time < AUTH_WAIT_TIMEOUT) { if (Date.now() - time < AUTH_WAIT_TIMEOUT) {
@ -175,7 +175,7 @@ export function doAuthenticate(
return Lbryio.authenticate(DOMAIN, getDefaultLanguage()); return Lbryio.authenticate(DOMAIN, getDefaultLanguage());
}) })
.then((user) => { .then((user) => {
if (sessionStorageAvailable) window.sessionStorage.removeItem(AUTH_IN_PROGRESS); LocalStorage.removeItem(LS.AUTH_IN_PROGRESS);
Lbryio.getAuthToken().then((token) => { Lbryio.getAuthToken().then((token) => {
dispatch({ dispatch({
type: ACTIONS.AUTHENTICATION_SUCCESS, type: ACTIONS.AUTHENTICATION_SUCCESS,
@ -199,7 +199,7 @@ export function doAuthenticate(
}); });
}) })
.catch((error) => { .catch((error) => {
if (sessionStorageAvailable) window.sessionStorage.removeItem(AUTH_IN_PROGRESS); LocalStorage.removeItem(LS.AUTH_IN_PROGRESS);
dispatch({ dispatch({
type: ACTIONS.AUTHENTICATION_FAILURE, type: ACTIONS.AUTHENTICATION_FAILURE,

View file

@ -1,4 +1,5 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { LocalStorage } from 'util/storage';
export const selectState = (state) => state.user || {}; export const selectState = (state) => state.user || {};
@ -126,7 +127,7 @@ export const selectOdyseeMembershipIsPremiumPlus = (state) => {
*/ */
export const selectHasOdyseeMembership = (state) => { export const selectHasOdyseeMembership = (state) => {
// @if process.env.NODE_ENV!='production' // @if process.env.NODE_ENV!='production'
const override = window.localStorage.getItem('hasMembershipOverride'); const override = LocalStorage.getItem('hasMembershipOverride');
if (override) return override === 'true'; if (override) return override === 'true';
// @endif // @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() { export function isLocalStorageAvailable() {
try { try {
return Boolean(window.localStorage); 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(); const localStorageAvailable = isLocalStorageAvailable();
export function getLocalStorageItem(key) { export const LocalStorage = {
return localStorageAvailable ? window.localStorage.getItem(key) : undefined; setItem: (key: string, value: string) => {
} if (localStorageAvailable) window.localStorage.setItem(key, value);
},
export function setLocalStorageItem(key, value) { getItem: (key: string) => {
if (localStorageAvailable) { return localStorageAvailable ? window.localStorage.getItem(key) : undefined;
window.localStorage.setItem(key, value); },
}
} removeItem: (key: string) => {
if (localStorageAvailable) window.localStorage.removeItem(key);
},
};

View file

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

View file

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