Last active
January 19, 2024 22:39
-
-
Save paceaux/da51a828c28cceed74fb0d614821d5b6 to your computer and use it in GitHub Desktop.
ClientStorage Module for saving things in a namespaced way to local or session storage.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ClientStorage { | |
/** | |
* Converts a string into a namespaced string | |
* @param {string} namespace the namespace | |
* @param {string} keyname keyname | |
* @returns {string} a string with namespace.keyname | |
*/ | |
static getNamespacedKeyName(namespace, keyname) { | |
let namespacedKeyName = ""; | |
if (namespace && !keyname.includes(`${namespace}.`)) { | |
namespacedKeyName = `${namespace}.${keyname}`; | |
} | |
return namespacedKeyName; | |
} | |
/** | |
* @param {*} value item to be stringified | |
* | |
* @returns {string} stringified item | |
*/ | |
static convertValue(value) { | |
let convertedValue = value; | |
if (value && typeof value === "object") { | |
convertedValue = JSON.stringify(value); | |
} | |
return convertedValue; | |
} | |
/** | |
* @param {string} value Item that should be parsed | |
* | |
* @returns {string|boolean|number|object|array} | |
*/ | |
static unconvertValue(value) { | |
let unconvertedValue = value; | |
if (value && (value.indexOf("{") === 0 || value.indexOf("[") === 0)) { | |
unconvertedValue = JSON.parse(value); | |
} | |
return unconvertedValue; | |
} | |
/** | |
* @typedef {"local" | "session"} StorageType | |
*/ | |
/** | |
* @param {string} namespace='' | |
* @param {StorageType} type='local' | |
*/ | |
constructor(namespace = "", type = "local") { | |
this.namespace = namespace; | |
this.observers = []; | |
const typeName = type.toLowerCase(); | |
if (typeName === "local" || typeName === "session") { | |
this.type = type; | |
} | |
if (namespace) { | |
this.registerNamespace(namespace); | |
} | |
window.addEventListener('storage', (evt) => { | |
const key = evt.key.replace(`${this.namespace}.`, ''); | |
const notifyObject = { | |
type: 'storageEvent', | |
oldValue: evt.oldValue, | |
value: evt.newValue, | |
key, | |
}; | |
this.notify(notifyObject); | |
}); | |
} | |
/** | |
* @property {Storage} storage either localStorage or sessionStorage | |
*/ | |
get storage() { | |
return window[`${this.type}Storage`]; | |
} | |
/** | |
* @property {Map<'key', value>} items de-namespaced map of items | |
*/ | |
get items() { | |
const items = new Map(); | |
const storageSize = this.storage.length; | |
let index = storageSize - 1; | |
// eslint-disable-next-line no-plusplus | |
while (--index > -1) { | |
const keyName = this.storage.key(index); | |
if (keyName.indexOf(this.namespace) === 0) { | |
const unnamespacedKey = keyName.replace(`${this.namespace}.`, ""); | |
items.set(unnamespacedKey, this.get(keyName)); | |
} | |
} | |
return items; | |
} | |
/** | |
* @property {number} size the number of items in storage | |
*/ | |
get size() { | |
return this.items.size; | |
} | |
/** | |
* @property {number} size the number of items in storage | |
*/ | |
get length() { | |
return this.items.size; | |
} | |
/** | |
* @property {string[]} namespaces the namespaces in storage | |
*/ | |
get namespaces() { | |
let namespaces = []; | |
const currentNamespaces = this.storage.getItem("ClientStorageNamespaces"); | |
if (currentNamespaces) { | |
namespaces = ClientStorage.unconvertValue(currentNamespaces); | |
} | |
return namespaces; | |
} | |
/** | |
* | |
* @param {string} namespace a namespace to register | |
*/ | |
registerNamespace(namespace) { | |
if (!namespace) return; | |
const currentNamespaces = this.namespaces; | |
if (currentNamespaces.length === 0) { | |
let namespaces = [namespace]; | |
this.storage.setItem("ClientStorageNamespaces", ClientStorage.convertValue(namespaces)); | |
} else { | |
if (!currentNamespaces.includes(namespace)) { | |
currentNamespaces.push(namespace); | |
this.storage.setItem("ClientStorageNamespaces", ClientStorage.unconvertValue(currentNamespaces)); | |
} | |
} | |
} | |
/** | |
* determines if a namespace already exists | |
* @param {string} namespace | |
* @returns {boolean} | |
*/ | |
hasNamespace(namespace) { | |
const namespaces = this.namespaces; | |
return namespaces?.includes(namespace); | |
} | |
/** | |
* Sets an item into storage | |
* @param {string} key unnamespaced key | |
* @param {string|number|boolean|object|array} value item to be serialized and stored | |
*/ | |
set(key, value) { | |
const keyName = ClientStorage.getNamespacedKeyName(this.namespace, key); | |
const convertedValue = ClientStorage.convertValue(value); | |
const notifyObj = {type: 'set', key, value: convertedValue}; | |
if (this.has(key)) { | |
console.log('key exists', this.get(key)); | |
notifyObj.oldValue = this.get(key); | |
} | |
this.storage.setItem(keyName, convertedValue); | |
this.notify(notifyObj); | |
} | |
/** | |
* Sets an object's keys and values into storage | |
* @param {object} key unnamespaced key | |
* @param {string|number|boolean|object|array} value item to be serialized and stored | |
*/ | |
setObject(object) { | |
const clone = JSON.parse(JSON.stringify(object)); | |
Object.keys(clone).forEach((key) => { | |
this.set(key, clone[key]); | |
}); | |
} | |
/** | |
* gets an item from storage | |
* @param {string} key unnamespaced key name | |
* @returns {*} | |
*/ | |
get(key) { | |
const keyName = ClientStorage.getNamespacedKeyName(this.namespace, key); | |
const item = this.storage.getItem(keyName); | |
return ClientStorage.unconvertValue(item); | |
} | |
/** | |
* removes item from storage | |
* @param {string} key unnamespaced keyname to remove | |
*/ | |
delete(key) { | |
const keyName = ClientStorage.getNamespacedKeyName(this.namespace, key); | |
const notifyObj = {type: 'delete', key}; | |
if (this.has(key)) { | |
notifyObj.oldValue = this.get(key); | |
} | |
this.storage.removeItem(keyName); | |
this.notify(notifyObj); | |
} | |
/** | |
* Determines if the item is in storage | |
* @param {string} key unnamespaced key name | |
* @returns {boolean} | |
*/ | |
has(key) { | |
const keyName = key.replace(`${this.namespace}.`, ""); | |
const keys = this.items.keys(); | |
let hasKey = false; | |
let item = keys.next(); | |
while (!item.done && !hasKey) { | |
if (item.value !== keyName) { | |
item = keys.next(); | |
} else { | |
hasKey = true; | |
} | |
} | |
return hasKey; | |
} | |
/** | |
* Deletes all items in the namespaced storage | |
*/ | |
clear() { | |
[...this.items.keys()].forEach((keyName) => { | |
this.delete(keyName); | |
}); | |
} | |
/** | |
* Adds a function to observables; allows it to receive a payload when storage changes | |
* @param {Function} observable | |
*/ | |
subscribe(observable) { | |
if (typeof observable !== 'function') { | |
throw new Error(`${typeof observable} is not a function`); | |
} | |
this.observers.push(observable); | |
} | |
/** | |
* Removes a function from observables | |
* @param {Function} observable | |
*/ | |
unsubscribe(observable) { | |
if (typeof observable !== 'function') { | |
throw new Error(`${typeof observable} is not a function`); | |
} | |
this.observers = this.observers.filter((observer) => observer !== observable); | |
} | |
/** | |
* Sends a payload to the observer | |
* @param {} data | |
*/ | |
notify(data) { | |
this.observers.forEach((observer) => { | |
observer(data); | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment