Skip to content

Instantly share code, notes, and snippets.

@paceaux
Last active January 19, 2024 22:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save paceaux/da51a828c28cceed74fb0d614821d5b6 to your computer and use it in GitHub Desktop.
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.
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