Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add PersistQuotaExceededError #51

Merged
merged 5 commits into from
Jan 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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