Skip to content

Commit

Permalink
feat: add PersistQuotaExceededError (#51)
Browse files Browse the repository at this point in the history
* feat: add PersistQuotaExceededError

* test: test PersistQuotaExceededError

* fix: catch error in window env

* feat: show storage's values

* skip: apply review
  • Loading branch information
daybrush authored Jan 26, 2022
1 parent c44cbfb commit 975b9ce
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 116 deletions.
7 changes: 7 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
123 changes: 75 additions & 48 deletions src/Persist.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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;
}
}
}
Expand All @@ -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;
}
}

Expand All @@ -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) || [];

Expand All @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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,
Expand Down
69 changes: 69 additions & 0 deletions src/PersistQuotaExceededError.js
Original file line number Diff line number Diff line change
@@ -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 <ko>에러가 되는 키</ko>
* @property {string} message Error message <ko>에러 메시지</ko>
* @property {"SessionStorage" | "LocalStorage" | "History" | "None"} storageType The storage type in which the error occurred <ko>에러가 발생한 스토리지 타입</ko>
* @property {number} size The size of the value in which the error occurred <ko>에러가 발생한 값의 사이즈</ko>
* @property {Object} values Values of high size in storage. (maxLengh: 3) <ko>스토리지의 높은 사이즈의 값들. (최대 3개)</ko>
* @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<ko>에러 메시지</ko>
* @param value Error value<ko>에러 값</ko>
*/
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;
21 changes: 20 additions & 1 deletion src/consts.js
Original file line number Diff line number Diff line change
@@ -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}`;
Expand All @@ -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)));
})();
10 changes: 10 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Persist, {updateDepth} from "./Persist";
import PersistQuotaExceededError from "./PersistQuotaExceededError";


export {
updateDepth,
PersistQuotaExceededError,
};

export default Persist;
7 changes: 4 additions & 3 deletions src/index.umd.js
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 975b9ce

Please sign in to comment.