From 2e72be0f8aec67e74664a6772f0a493ad1527083 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Tue, 16 Feb 2021 11:47:23 +0530 Subject: [PATCH] Handle quota exceeded and running low on disk space errors while saving a key in localStorage (#2947) --- app/client/src/constants/AppConstants.ts | 4 +- app/client/src/constants/messages.ts | 5 ++ app/client/src/index.tsx | 1 + app/client/src/sagas/ActionExecutionSagas.ts | 2 + app/client/src/sagas/ThemeSaga.tsx | 1 + app/client/src/sagas/userSagas.tsx | 1 + app/client/src/utils/AppsmithUtils.tsx | 1 + app/client/src/utils/featureFlags.ts | 1 + app/client/src/utils/hooks/localstorage.tsx | 5 +- app/client/src/utils/localStorage.tsx | 66 ++++++++++++++++++++ 10 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 app/client/src/utils/localStorage.tsx diff --git a/app/client/src/constants/AppConstants.ts b/app/client/src/constants/AppConstants.ts index be0cf7b4ee..ddbe1a1f9c 100644 --- a/app/client/src/constants/AppConstants.ts +++ b/app/client/src/constants/AppConstants.ts @@ -1,3 +1,5 @@ +import localStorage from "utils/localStorage"; + export const CANVAS_DEFAULT_WIDTH_PX = 1242; export const CANVAS_DEFAULT_HEIGHT_PX = 1292; export const CANVAS_DEFAULT_GRID_HEIGHT_PX = 1; @@ -13,7 +15,7 @@ export const getAppStore = (appId: string) => { const appStoreName = getAppStoreName(appId); let storeString = "{}"; // Check if localStorage exists - if (localStorage) { + if (localStorage.isSupported()) { const appStore = localStorage.getItem(appStoreName); if (appStore) storeString = appStore; } diff --git a/app/client/src/constants/messages.ts b/app/client/src/constants/messages.ts index b0bf40aefd..6d5727320a 100644 --- a/app/client/src/constants/messages.ts +++ b/app/client/src/constants/messages.ts @@ -177,3 +177,8 @@ export const GOOGLE_RECAPTCHA_DOMAIN_ERROR = export const SERVER_API_TIMEOUT_ERROR = "Appsmith server is taking too long to respond. Please try again after some time"; export const DEFAULT_ERROR_MESSAGE = "There was an unexpected error"; + +export const LOCAL_STORAGE_QUOTA_EXCEEDED_MESSAGE = + "Error saving a key in localStorage. You have exceeded the allowed storage size limit"; +export const LOCAL_STORAGE_NO_SPACE_LEFT_ON_DEVICE_MESSAGE = + "Error saving a key in localStorage. You have run out of disk space"; diff --git a/app/client/src/index.tsx b/app/client/src/index.tsx index 97fa68dec0..d993a772a1 100755 --- a/app/client/src/index.tsx +++ b/app/client/src/index.tsx @@ -15,6 +15,7 @@ import { connect } from "react-redux"; import { AppState } from "reducers"; import { setThemeMode } from "actions/themeActions"; import { StyledToastContainer } from "components/ads/Toast"; +import localStorage from "utils/localStorage"; // enable autofreeze only in development import { setAutoFreeze } from "immer"; diff --git a/app/client/src/sagas/ActionExecutionSagas.ts b/app/client/src/sagas/ActionExecutionSagas.ts index 27c63fd088..41cc91aa58 100644 --- a/app/client/src/sagas/ActionExecutionSagas.ts +++ b/app/client/src/sagas/ActionExecutionSagas.ts @@ -88,6 +88,8 @@ import { import copy from "copy-to-clipboard"; import { EMPTY_RESPONSE } from "../components/editorComponents/ApiResponseView"; +import localStorage from "utils/localStorage"; + export enum NavigationTargetType { SAME_WINDOW = "SAME_WINDOW", NEW_WINDOW = "NEW_WINDOW", diff --git a/app/client/src/sagas/ThemeSaga.tsx b/app/client/src/sagas/ThemeSaga.tsx index fdf5dfe433..a50cdc7a2d 100644 --- a/app/client/src/sagas/ThemeSaga.tsx +++ b/app/client/src/sagas/ThemeSaga.tsx @@ -1,5 +1,6 @@ import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants"; import { takeLatest } from "redux-saga/effects"; +import localStorage from "utils/localStorage"; import { ThemeMode } from "../selectors/themeSelectors"; export function* setThemeSaga(actionPayload: ReduxAction) { diff --git a/app/client/src/sagas/userSagas.tsx b/app/client/src/sagas/userSagas.tsx index 8e6b01559b..25298ce4b1 100644 --- a/app/client/src/sagas/userSagas.tsx +++ b/app/client/src/sagas/userSagas.tsx @@ -37,6 +37,7 @@ import PerformanceTracker, { import { ERROR_CODES } from "constants/ApiConstants"; import { ANONYMOUS_USERNAME } from "constants/userConstants"; import { flushErrorsAndRedirect } from "actions/errorActions"; +import localStorage from "utils/localStorage"; export function* createUserSaga( action: ReduxActionWithPromise, diff --git a/app/client/src/utils/AppsmithUtils.tsx b/app/client/src/utils/AppsmithUtils.tsx index 3feff0051c..604e678dcc 100644 --- a/app/client/src/utils/AppsmithUtils.tsx +++ b/app/client/src/utils/AppsmithUtils.tsx @@ -13,6 +13,7 @@ import produce from "immer"; import { AppIconCollection, AppIconName } from "components/ads/AppIcon"; import { ERROR_CODES } from "constants/ApiConstants"; import { ERROR_500 } from "../constants/messages"; +import localStorage from "utils/localStorage"; export const createReducer = ( initialState: any, diff --git a/app/client/src/utils/featureFlags.ts b/app/client/src/utils/featureFlags.ts index cbe379a403..0026d71f82 100644 --- a/app/client/src/utils/featureFlags.ts +++ b/app/client/src/utils/featureFlags.ts @@ -1,4 +1,5 @@ import { FeatureFlagConfig, FeatureFlagsEnum } from "configs/types"; +import localStorage from "utils/localStorage"; const optimizelySDK = require("@optimizely/optimizely-sdk"); class FeatureFlag { diff --git a/app/client/src/utils/hooks/localstorage.tsx b/app/client/src/utils/hooks/localstorage.tsx index 78b3bb738e..8199de43c5 100644 --- a/app/client/src/utils/hooks/localstorage.tsx +++ b/app/client/src/utils/hooks/localstorage.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import localStorage from "utils/localStorage"; export function useLocalStorage(key: string, initialValue: string) { // State to store our value @@ -6,7 +7,7 @@ export function useLocalStorage(key: string, initialValue: string) { const [storedValue, setStoredValue] = useState(() => { try { // Get from local storage by key - const item = window.localStorage.getItem(key); + const item = localStorage.getItem(key); // Parse stored json or if none return initialValue return item ? JSON.parse(item) : initialValue; } catch (error) { @@ -26,7 +27,7 @@ export function useLocalStorage(key: string, initialValue: string) { // Save state setStoredValue(valueToStore); // Save to local storage - window.localStorage.setItem(key, JSON.stringify(valueToStore)); + localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { // A more advanced implementation would handle the error case console.log(error); diff --git a/app/client/src/utils/localStorage.tsx b/app/client/src/utils/localStorage.tsx new file mode 100644 index 0000000000..a7aa593a8c --- /dev/null +++ b/app/client/src/utils/localStorage.tsx @@ -0,0 +1,66 @@ +import { Variant } from "components/ads/common"; +import { Toaster } from "components/ads/Toast"; +import { + LOCAL_STORAGE_QUOTA_EXCEEDED_MESSAGE, + LOCAL_STORAGE_NO_SPACE_LEFT_ON_DEVICE_MESSAGE, +} from "constants/messages"; + +const getLocalStorage = () => { + const storage = window.localStorage; + + const handleError = (e: Error) => { + let message; + if (e.name === "QuotaExceededError") { + message = LOCAL_STORAGE_QUOTA_EXCEEDED_MESSAGE; + } else if (e.name === "NS_ERROR_FILE_NO_DEVICE_SPACE") { + message = LOCAL_STORAGE_NO_SPACE_LEFT_ON_DEVICE_MESSAGE; + } + + if (message) { + Toaster.show({ + text: message, + variant: Variant.danger, + }); + } else { + throw e; + } + }; + + const getItem = (key: string): string | null => { + try { + return storage.getItem(key); + } catch (e) { + handleError(e); + } + return null; + }; + + const setItem = (key: string, value: string) => { + try { + storage.setItem(key, value); + } catch (e) { + handleError(e); + } + }; + + const removeItem = (key: string) => { + try { + storage.removeItem(key); + } catch (e) { + handleError(e); + } + }; + + const isSupported = () => !!window.localStorage; + + return { + getItem, + setItem, + removeItem, + isSupported, + }; +}; + +const localStorage = getLocalStorage(); + +export default localStorage;