diff --git a/index.d.ts b/index.d.ts
index 8e44106..1c7581a 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -24,6 +24,13 @@ declare module "@egjs/persist" {
*/
public remove(path: string): this;
}
+
+ export declare class PersistQuotaExceededError extends Error {
+ public name: string;
+ public storageType: "SessionStorage" | "LocalStorage" | "History" | "None";
+ public key: string;
+ public size: number;
+ }
export declare function updateDepth(type?: number): void;
export default Persist;
}
diff --git a/src/Persist.js b/src/Persist.js
index 6a2c131..7e6e1c1 100755
--- a/src/Persist.js
+++ b/src/Persist.js
@@ -5,7 +5,8 @@ import {
getStateByKey,
getStorage,
} from "./storageManager";
-import {isNeeded, getUrl, getStorageKey, getNavigationType} from "./utils";
+import PersistQuotaExceededError from "./PersistQuotaExceededError";
+import {isNeeded, getUrl, getStorageKey, getNavigationType, isQuotaExceededError} from "./utils";
import {console, window} from "./browser";
import {TYPE_BACK_FORWARD, TYPE_NAVIGATE, CONST_PERSIST_STATE, CONST_DEPTHS, CONST_LAST_URL} from "./consts";
@@ -37,15 +38,12 @@ function setPersistState(key, value) {
try {
setStateByKey(CONST_PERSIST_STATE, key, value);
} catch (e) {
- if (clearFirst()) {
+ if (catchQuotaExceededError(e, CONST_PERSIST_STATE, value)) {
if (key === CONST_LAST_URL) {
setPersistState(key, value);
} else if (key === CONST_DEPTHS) {
setPersistState(key, value && value.slice(1));
}
- } else {
- // There is no more size to fit in.
- throw e;
}
}
}
@@ -59,38 +57,56 @@ function updateDepth(type = 0) {
return;
}
// url is not the same for the first time, pushState, or replaceState.
- currentUrl = url;
- const depths = getPersistState(CONST_DEPTHS) || [];
+ const prevUrl = currentUrl;
- if (type === TYPE_BACK_FORWARD) {
- // Change current url only
- const currentIndex = depths.indexOf(currentUrl);
+ try {
+ currentUrl = url;
+ const depths = getPersistState(CONST_DEPTHS) || [];
- ~currentIndex && setPersistState(CONST_LAST_URL, currentUrl);
- } else {
- const prevLastUrl = getPersistState(CONST_LAST_URL);
+ if (type === TYPE_BACK_FORWARD) {
+ // Change current url only
+ const currentIndex = depths.indexOf(url);
+
+ ~currentIndex && setPersistState(CONST_LAST_URL, url);
+ } else {
+ const prevLastUrl = getPersistState(CONST_LAST_URL);
- reset(getStorageKey(currentUrl));
+ reset(getStorageKey(url));
- if (type === TYPE_NAVIGATE && url !== prevLastUrl) {
- // Remove all url lists with higher index than current index
- const prevLastIndex = depths.indexOf(prevLastUrl);
- const removedList = depths.splice(prevLastIndex + 1, depths.length);
+ if (type === TYPE_NAVIGATE && url !== prevLastUrl) {
+ // Remove all url lists with higher index than current index
+ const prevLastIndex = depths.indexOf(prevLastUrl);
+ const removedList = depths.splice(prevLastIndex + 1, depths.length);
- removedList.forEach(removedUrl => {
- reset(getStorageKey(removedUrl));
- });
- // If the type is NAVIGATE and there is information about current url, delete it.
- const currentIndex = depths.indexOf(currentUrl);
+ removedList.forEach(removedUrl => {
+ reset(getStorageKey(removedUrl));
+ });
+ // If the type is NAVIGATE and there is information about current url, delete it.
+ const currentIndex = depths.indexOf(url);
- ~currentIndex && depths.splice(currentIndex, 1);
- }
- // Add depth for new address.
- if (depths.indexOf(url) < 0) {
- depths.push(url);
+ ~currentIndex && depths.splice(currentIndex, 1);
+ }
+ // Add depth for new address.
+ if (depths.indexOf(url) < 0) {
+ depths.push(url);
+ }
+ setPersistState(CONST_DEPTHS, depths);
+ setPersistState(CONST_LAST_URL, url);
}
- setPersistState(CONST_DEPTHS, depths);
- setPersistState(CONST_LAST_URL, url);
+ } catch (e) {
+ // revert currentUrl
+ currentUrl = prevUrl;
+ throw e;
+ }
+}
+
+function catchQuotaExceededError(e, key, value) {
+ if (clearFirst()) {
+ return true;
+ } else if (isQuotaExceededError(e)) {
+ throw new PersistQuotaExceededError(key, value ? JSON.stringify(value) : "");
+ } else {
+ throw e;
}
}
@@ -117,6 +133,7 @@ function clearFirst() {
// Clear the previous record and try to add data again.
return true;
}
+
function clear() {
const depths = getPersistState(CONST_DEPTHS) || [];
@@ -128,12 +145,6 @@ function clear() {
currentUrl = "";
}
-if ("onpopstate" in window) {
- window.addEventListener("popstate", () => {
- // popstate event occurs when backward or forward
- updateDepth(TYPE_BACK_FORWARD);
- });
-}
/**
* Get or store the current state of the web page using JSON.
@@ -184,7 +195,7 @@ class Persist {
// find path
const urlKey = getStorageKey(getUrl());
- const globalState = getStateByKey(urlKey, this.key);
+ const globalState = getStateByKey(urlKey, this.key);
if (!path || path.length === 0) {
@@ -221,7 +232,7 @@ class Persist {
// find path
const key = this.key;
const urlKey = getStorageKey(getUrl());
- const globalState = getStateByKey(urlKey, key);
+ const globalState = getStateByKey(urlKey, key);
try {
if (path.length === 0) {
@@ -238,11 +249,8 @@ class Persist {
);
}
} catch (e) {
- if (clearFirst(e)) {
+ if (catchQuotaExceededError(e, urlKey, value)) {
this.set(path, value);
- } else {
- // There is no more size to fit in.
- throw e;
}
}
return this;
@@ -259,7 +267,7 @@ class Persist {
// find path
const key = this.key;
const urlKey = getStorageKey(getUrl());
- const globalState = getStateByKey(urlKey, key);
+ const globalState = getStateByKey(urlKey, key);
try {
if (path.length === 0) {
@@ -278,19 +286,38 @@ class Persist {
);
}
} catch (e) {
- if (clearFirst(e)) {
+ if (catchQuotaExceededError(e)) {
this.remove(path);
- } else {
- // There is no more size to fit in.
- throw e;
}
}
return this;
}
}
+
+if ("onpopstate" in window) {
+ window.addEventListener("popstate", () => {
+ // popstate event occurs when backward or forward
+ try {
+ updateDepth(TYPE_BACK_FORWARD);
+ } catch (e) {
+ // Global function calls prevent errors.
+ if (!isQuotaExceededError(e)) {
+ throw e;
+ }
+ }
+ });
+}
+
// If navigation's type is not TYPE_BACK_FORWARD, delete information about current url.
-updateDepth(getNavigationType());
+try {
+ updateDepth(getNavigationType());
+} catch (e) {
+ // Global function calls prevent errors.
+ if (!isQuotaExceededError(e)) {
+ throw e;
+ }
+}
export {
updateDepth,
diff --git a/src/PersistQuotaExceededError.js b/src/PersistQuotaExceededError.js
new file mode 100644
index 0000000..21aa09a
--- /dev/null
+++ b/src/PersistQuotaExceededError.js
@@ -0,0 +1,69 @@
+import {getStorage, getStorageType} from "./storageManager";
+
+const setPrototypeOf = Object.setPrototypeOf || ((obj, proto) => {
+ // eslint-disable-next-line no-proto
+ obj.__proto__ = proto;
+ return obj;
+});
+
+
+/**
+ * Special type of known error that {@link Persist} throws.
+ * @ko Persist 내부에서 알려진 오류 발생시 throw되는 에러
+ * @property {string} key Error key 에러가 되는 키
+ * @property {string} message Error message 에러 메시지
+ * @property {"SessionStorage" | "LocalStorage" | "History" | "None"} storageType The storage type in which the error occurred 에러가 발생한 스토리지 타입
+ * @property {number} size The size of the value in which the error occurred 에러가 발생한 값의 사이즈
+ * @property {Object} values Values of high size in storage. (maxLengh: 3) 스토리지의 높은 사이즈의 값들. (최대 3개)
+ * @example
+ * ```ts
+ * import Persist, { PersistQuotaExceededError } from "@egjs/persist";
+ * try {
+ * const persist = new Persist("key");
+ * } catch (e) {
+ * if (e instanceof PersistQuotaExceededError) {
+ * console.error("size", e.size);
+ * }
+ * }
+ * ```
+ */
+class PersistQuotaExceededError extends Error {
+ /**
+ * @param key Error message에러 메시지
+ * @param value Error value에러 값
+ */
+ constructor(key, value) {
+ const size = value.length;
+ const storageType = getStorageType();
+ const storage = getStorage();
+ let valuesText = "";
+ let values = [];
+
+ if (storage) {
+ const length = storage.length;
+
+ for (let i = 0; i < length; ++i) {
+ const itemKey = storage.key(i);
+ const item = storage.getItem(itemKey) || "";
+
+ values.push({key: itemKey, size: item.length});
+ }
+ values = values.sort((a, b) => b.size - a.size).slice(0, 3);
+
+ if (values.length) {
+ valuesText = ` The highest values of ${storageType} are ${values.map(item => JSON.stringify({[item.key]: item.size})).join(", ")}.`;
+ }
+ }
+
+ super(`Setting the value (size: ${size}) of '${key}' exceeded the ${storageType}'s quota.${valuesText}`);
+
+ setPrototypeOf(this, PersistQuotaExceededError.prototype);
+ this.name = "PersistQuotaExceededError";
+ this.storageType = storageType;
+ this.key = key;
+ this.size = size;
+ this.values = values;
+ }
+}
+
+export default PersistQuotaExceededError;
diff --git a/src/consts.js b/src/consts.js
index 4404475..b3d61b0 100644
--- a/src/consts.js
+++ b/src/consts.js
@@ -1,4 +1,4 @@
-import {performance} from "./browser";
+import {performance, navigator, parseFloat} from "./browser";
export const CONST_PERSIST = "___persist___";
export const CONST_PERSIST_STATE = `state${CONST_PERSIST}`;
@@ -9,3 +9,22 @@ const navigation = performance && performance.navigation;
export const TYPE_NAVIGATE = (navigation && navigation.TYPE_NAVIGATE) || 0;
export const TYPE_RELOAD = (navigation && navigation.TYPE_RELOAD) || 1;
export const TYPE_BACK_FORWARD = (navigation && navigation.TYPE_BACK_FORWARD) || 2;
+
+const userAgent = navigator ? navigator.userAgent : "";
+
+export const IS_PERSIST_NEEDED = (function() {
+ const isIOS = (new RegExp("iPhone|iPad", "i")).test(userAgent);
+ const isMacSafari = (new RegExp("Mac", "i")).test(userAgent) &&
+ !(new RegExp("Chrome", "i")).test(userAgent) &&
+ (new RegExp("Apple", "i")).test(userAgent);
+ const isAndroid = (new RegExp("Android ", "i")).test(userAgent);
+ const isWebview = (new RegExp("wv; |inapp;", "i")).test(userAgent);
+ const androidVersion = isAndroid ? parseFloat(new RegExp(
+ "(Android)\\s([\\d_\\.]+|\\d_0)", "i"
+ ).exec(userAgent)[2]) : undefined;
+
+ return !(isIOS ||
+ isMacSafari ||
+ (isAndroid &&
+ ((androidVersion <= 4.3 && isWebview) || androidVersion < 3)));
+})();
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..ece1520
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,10 @@
+import Persist, {updateDepth} from "./Persist";
+import PersistQuotaExceededError from "./PersistQuotaExceededError";
+
+
+export {
+ updateDepth,
+ PersistQuotaExceededError,
+};
+
+export default Persist;
diff --git a/src/index.umd.js b/src/index.umd.js
index c00d15e..337d5c7 100755
--- a/src/index.umd.js
+++ b/src/index.umd.js
@@ -1,6 +1,7 @@
-import Persist, {updateDepth} from "./Persist";
+import Persist, * as modules from "./index";
-// eslint-disable-next-line import/no-named-as-default-member
-Persist.updateDepth = updateDepth;
+for (const name in modules) {
+ Persist[name] = modules[name];
+}
export default Persist;
diff --git a/src/storageManager.js b/src/storageManager.js
index 7a03cab..38293dd 100644
--- a/src/storageManager.js
+++ b/src/storageManager.js
@@ -2,6 +2,7 @@ import {window, history, location, sessionStorage, localStorage} from "./browser
import {CONST_PERSIST} from "./consts";
const isSupportState = history && "replaceState" in history && "state" in history;
+let storageType = "None";
function isStorageAvailable(storage) {
if (!storage) {
@@ -26,8 +27,12 @@ const storage = (function() {
if (isStorageAvailable(sessionStorage)) {
strg = sessionStorage;
+ storageType = "SessionStorage";
} else if (isStorageAvailable(localStorage)) {
strg = localStorage;
+ storageType = "LocalStorage";
+ } else if (history.state) {
+ storageType = "History";
}
return strg;
@@ -40,10 +45,6 @@ function warnInvalidStorageValue() {
/* eslint-enable no-console */
}
-function getStorage() {
- return storage;
-}
-
/*
* Get state value
*/
@@ -91,20 +92,6 @@ function getState(key) {
return state;
}
-function getStateByKey(key, valueKey) {
- if (!isSupportState && !storage) {
- return undefined;
- }
-
- let result = getState(key)[valueKey];
-
- // some device returns "null" or undefined
- if (result === "null" || typeof result === "undefined") {
- result = null;
- }
- return result;
-}
-
/*
* Set state value
*/
@@ -142,7 +129,30 @@ function setState(key, state) {
state ? window[CONST_PERSIST] = true : delete window[CONST_PERSIST];
}
-function setStateByKey(key, valueKey, data) {
+
+export function getStorage() {
+ return storage;
+}
+
+export function getStorageType() {
+ return storageType;
+}
+
+export function getStateByKey(key, valueKey) {
+ if (!isSupportState && !storage) {
+ return undefined;
+ }
+
+ let result = getState(key)[valueKey];
+
+ // some device returns "null" or undefined
+ if (result === "null" || typeof result === "undefined") {
+ result = null;
+ }
+ return result;
+}
+
+export function setStateByKey(key, valueKey, data) {
if (!isSupportState && !storage) {
return;
}
@@ -156,13 +166,6 @@ function setStateByKey(key, valueKey, data) {
/*
* flush current history state
*/
-function reset(key) {
+export function reset(key) {
setState(key, null);
}
-
-export {
- reset,
- setStateByKey,
- getStateByKey,
- getStorage,
-};
diff --git a/src/utils.js b/src/utils.js
index 527a171..fda2eff 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -3,7 +3,7 @@ import {CONST_PERSIST} from "./consts";
const userAgent = navigator ? navigator.userAgent : "";
-const isNeeded = (function() {
+export const isNeeded = (function() {
const isIOS = (new RegExp("iPhone|iPad", "i")).test(userAgent);
const isMacSafari = (new RegExp("Mac", "i")).test(userAgent) &&
!(new RegExp("Chrome", "i")).test(userAgent) &&
@@ -21,20 +21,19 @@ const isNeeded = (function() {
})();
// In case of IE8, TYPE_BACK_FORWARD is undefined.
-function getNavigationType() {
+export function getNavigationType() {
return performance && performance.navigation &&
performance.navigation.type;
}
-function getUrl() {
+
+export function getUrl() {
return location ? location.href.split("#")[0] : "";
}
-function getStorageKey(name) {
+
+export function getStorageKey(name) {
return name + CONST_PERSIST;
}
-export {
- getUrl,
- getStorageKey,
- getNavigationType,
- isNeeded,
-};
+export function isQuotaExceededError(e) {
+ return e.name === "QuotaExceededError" || e.name === "PersistQuotaExceededError";
+}
diff --git a/test/manual/exceeded.html b/test/manual/exceeded.html
new file mode 100755
index 0000000..14195f7
--- /dev/null
+++ b/test/manual/exceeded.html
@@ -0,0 +1,14 @@
+
+
+
\ No newline at end of file
diff --git a/test/unit/TestHelper.js b/test/unit/TestHelper.js
index 2b12f5f..eada5af 100644
--- a/test/unit/TestHelper.js
+++ b/test/unit/TestHelper.js
@@ -5,6 +5,7 @@ import {CONST_PERSIST_STATE, CONST_DEPTHS} from "../../src/consts";
import * as StorageManager from "../../src/storageManager";
import * as browser from "../../src/browser";
+
const DEFAULT_HREF = location.href;
export const INJECT_URL = "https://inject.com";
@@ -18,6 +19,14 @@ export function wait(time = 100) {
});
}
+export function throwQuotaExceedError() {
+ const err = new Error(`Failed to execute 'setItem' on 'Storage': Setting the value of 'URL' exceeded the quota.`);
+
+ err.name = "QuotaExceededError";
+
+ throw err;
+}
+
export function injectBrowser(href = DEFAULT_HREF) {
const location = {
pathname: INJECT_URL,
@@ -67,6 +76,9 @@ export function injectBrowser(href = DEFAULT_HREF) {
history,
};
}
+export function getDepths() {
+ return StorageManager.getStateByKey(CONST_PERSIST_STATE, CONST_DEPTHS) || [];
+}
export function sessionStorageForLimit(limit, limit2 = limit) {
// Compare with limit when adding depth and limit2 when adding value.
return {
@@ -79,13 +91,13 @@ export function sessionStorageForLimit(limit, limit2 = limit) {
if (key === CONST_PERSIST_STATE) {
isExceed = JSON.parse(value || "{depths: []}")[CONST_DEPTHS].length > limit;
} else {
- isExceed = StorageManager.getStateByKey(CONST_PERSIST_STATE, CONST_DEPTHS).length > limit2;
+ isExceed = getDepths().length > limit2;
}
} catch (e) {
}
if (isExceed) {
- throw new Error("exceed storage");
+ throwQuotaExceedError();
}
window.sessionStorage.setItem(key, value);
},
diff --git a/test/unit/persist.spec.js b/test/unit/persist.spec.js
index ce6fc47..15016bc 100755
--- a/test/unit/persist.spec.js
+++ b/test/unit/persist.spec.js
@@ -6,7 +6,8 @@ import Persist from "../../src/Persist";
import * as utils from "../../src/utils";
import * as StorageManager from "../../src/storageManager";
import {CONST_PERSIST_STATE, CONST_DEPTHS, CONST_LAST_URL} from "../../src/consts";
-import {wait, storageManagerForLimit, injectBrowser, injectPersistModules, INJECT_URL, injectPersist} from "./TestHelper";
+import {wait, storageManagerForLimit, injectBrowser, injectPersistModules, INJECT_URL, injectPersist, throwQuotaExceedError, getDepths} from "./TestHelper";
+import { PersistQuotaExceededError } from "../../src";
const StorageManagerUsingHistory = StorageManagerInjector(
{
@@ -605,6 +606,11 @@ describe("Persist", () => {
const pathname = location.pathname;
beforeEach(() => {
+ const length = sessionStorage.length;
+
+ for (let i = 0; i < length; ++i) {
+ sessionStorage.removeItem(sessionStorage.key(i));
+ }
Persist.clear();
});
afterEach(() => {
@@ -612,10 +618,9 @@ describe("Persist", () => {
});
it(`test depth test for exceed test (depths limit: 0)`, () => {
- // Given
try {
- // When
- new(injectPersist(
+ // Given
+ const persist = new(injectPersist(
{
"./storageManager": storageManagerForLimit(0),
"./browser": {
@@ -624,33 +629,46 @@ describe("Persist", () => {
},
}
))("");
+
+ // When
+ persist.set("a", "");
} catch (e) {
// Then
// An unconditional error occurs.
- expect(e.message).to.be.equals("exceed storage");
+ expect(e).to.be.an.instanceof(PersistQuotaExceededError);
return;
}
throw new Error("Errors should occur unconditionally, but they ignored them.");
});
- it(`test depth test for exceed test (depths limit: 1, value limit: 0)`, () => {
- // Given
- const persist = new(injectPersist(
- {
- "./storageManager": storageManagerForLimit(1, 0),
- "./browser": {
- window: {},
- console,
- },
- }
- ))("");
-
- // When
+ it(`should check if other values of sessionStorage are displayed`, () => {
try {
- persist.set("a", 1);
+ // Given
+ const persist = new(injectPersist(
+ {
+ "./storageManager": storageManagerForLimit(0),
+ "./browser": {
+ window: {},
+ console: window,
+ },
+ }
+ ))("");
+
+ sessionStorage.setItem("test1", "22");
+ sessionStorage.setItem("test2", "1114");
+
+ // When
+ persist.set("a", "1");
} catch (e) {
// Then
- // An unconditional error occurs.
- expect(e.message).to.be.equals("exceed storage");
+ expect(e).to.be.an.instanceof(PersistQuotaExceededError);
+ expect(e.message).to.have.string("test1");
+ expect(e.message).to.have.string("test2");
+ // 0: tmp__state__
+ expect(e.values[1].key).to.be.equals("test2");
+ expect(e.values[1].size).to.be.equals(4);
+
+ expect(e.values[2].key).to.be.equals("test1");
+ expect(e.values[2].size).to.be.equals(2);
return;
}
throw new Error("Errors should occur unconditionally, but they ignored them.");
@@ -675,7 +693,7 @@ describe("Persist", () => {
persist.set("a", "1");
// Then
- const state1 = StorageManager.getStateByKey(CONST_PERSIST_STATE, CONST_DEPTHS).length;
+ const state1 = getDepths().length;
expect(state1).to.be.equals(limit - 1);
@@ -684,7 +702,7 @@ describe("Persist", () => {
persist.set("a", "1");
// Then
- const state2 = StorageManager.getStateByKey(CONST_PERSIST_STATE, CONST_DEPTHS).length;
+ const state2 = getDepths().length;
expect(state2).to.be.equals(limit - 1);
} else {
@@ -698,7 +716,7 @@ describe("Persist", () => {
// start, a0
// start, a0 , a1
// a0 , a1, a2
- const state = StorageManager.getStateByKey(CONST_PERSIST_STATE, CONST_DEPTHS).length;
+ const state = getDepths().length;
expect(state).to.be.equals(j + 2);
}
@@ -710,7 +728,7 @@ describe("Persist", () => {
persist.set("a", 1);
// Then
- const currentState = StorageManager.getStateByKey(CONST_PERSIST_STATE, CONST_DEPTHS).length;
+ const currentState = getDepths().length;
expect(currentState).to.be.equals(limit - 1);
}