@aegis-framework/artemis
Version:
Aegis Framework Javascript Library
395 lines (358 loc) • 11.7 kB
JavaScript
/**
* ==============================
* Local Storage Adapter
* ==============================
*/
/**
* The Local Storage Adapter provides the Space Class the ability to interact
* with the localStorage api found in most modern browsers.
*
* @class
*/
export class LocalStorage {
/**
* Create a new LocalStorage. If no configuration is provided, the LocalStorage
* global object is used. The LocalStorage Adapter can provide independency
* by store name and space name.
*
* @constructor
* @param {Object} [configuration={name = '', version = '', store = ''}] - Configuration Object for the Adapter
* @param {string} configuration.name - Name of the Space
* @param {string} configuration.version - Version of the Space in Semantic versioning syntax
* @param {string} configuration.store - Name of the Object Store to use
*
*/
constructor ({name = '', version = '', store = ''}) {
this.name = name;
this.version = version;
this.store = store;
this.upgrades = {};
if (this.version === '') {
this.numericVersion = 0;
} else {
this.numericVersion = parseInt (version.replace (/\./g, ''));
}
if (name !== '' && version !== '' && store !== '') {
this.id = `${this.name}::${this.store}::${this.version}_`;
} else if (name !== '' && version !== '') {
this.id = `${this.name}::${this.version}_`;
} else if (name !== '') {
this.id = `${this.name}::_`;
} else {
this.id = '';
}
}
/**
* Open the Storage Object
*
* @return {Promise}
*/
open () {
if (typeof this.storage === 'object' && !(this.storage instanceof Promise)) {
return Promise.resolve (this);
} else if (this.storage instanceof Promise) {
return this.storage;
} else {
this.storage = new Promise ((resolve) => {
let upgradesToApply = [];
// Check if this space is versioned
if (this.version !== '') {
// Get the versionless part of the ID to check if an upgrade needs
// to ocurr based on the version available on storage and the current
// version.
let versionless = '';
if (this.name !== '' && this.version !== '' && this.store !== '') {
versionless = `${this.name}::${this.store}::`;
} else if (this.name !== '' && this.version !== '') {
versionless = `${this.name}::`;
}
// Get all the currently stored keys that contain the versionless
// ID, which means they belong to this space
const storedVersions = Object.keys (window.localStorage).filter ((key) => {
return key.indexOf (versionless) === 0;
}).map ((key) => {
// Remove the versionless part of the ID and keep only the
// part of the key belonging to the ID
return key.replace (versionless, '').split ('_')[0];
}). filter ((key) => {
// Filter all that didn't match the versionless part fully
return key.indexOf ('::') === -1;
}).sort ();
if (storedVersions.length > 0) {
// We'll only take the lowest one every time
const oldVersion = storedVersions[0];
const oldVersionNumeric = parseInt (oldVersion.replace (/\./g, ''));
if (oldVersionNumeric < this.numericVersion) {
// Check what upgrade functions have been declared in their respective order
const availableUpgrades = Object.keys (this.upgrades).sort ();
// Find the first update that needs to be applied to the database given
// the old version it currently has.
const startFrom = availableUpgrades.findIndex (u => {
const [old, ] = u.split ('::');
return parseInt (old) === oldVersionNumeric;
});
if (startFrom > -1) {
upgradesToApply = availableUpgrades.slice (startFrom).filter ((u) => {
const [old, next] = u.split ('::');
return parseInt (old) < this.numericVersion && parseInt (next) <= this.numericVersion;
});
}
// Get the previous ID using the old version
let previousId = `${this.name}::${oldVersion}_`;
if (this.name !== '' && this.version !== '' && this.store !== '') {
previousId = `${this.name}::${this.store}::${oldVersion}_`;
} else if (this.name !== '' && this.version !== '') {
previousId = `${this.name}::${oldVersion}_`;
}
// Get all keys from the previous version
const keys = Object.keys (window.localStorage).filter ((key) => {
return key.indexOf (previousId) === 0;
}).map ((key) => {
return key.replace (previousId, '');
});
for (const key of keys) {
// Get the value stored with the previous version
const previous = window.localStorage.getItem (`${previousId}${key}`);
// Re-insert the value using the new ID as a key
window.localStorage.setItem (this.id + key, previous);
// Delete the previous value.
window.localStorage.removeItem (`${previousId}${key}`);
}
}
}
}
resolve ({ upgrades: upgradesToApply });
}).then (({ upgrades }) => {
this.storage = window.localStorage;
return new Promise ((resolve) => {
const res = () => resolve (this);
this._upgrade (upgrades, res);
});
});
return this.storage;
}
}
/**
* Store a key-value pair
*
* @param {string} key - Key with which this value will be saved
* @param {Object|string|Number} - Value to save
* @return {Promise<{key, value}>}
*/
set (key, value) {
return this.open ().then (() => {
if (typeof value === 'object') {
this.storage.setItem (this.id + key, JSON.stringify (value));
} else {
this.storage.setItem (this.id + key, value);
}
return Promise.resolve ({key, value});
});
}
/**
* Update a key-value pair. In difference with the set () method, the update
* method will use an Object.assign () in the case of objects so no value is
* lost.
*
* @param {string} key - Key with which this value will be saved
* @param {Object|string|Number} - Value to save
* @return {Promise<{key, value}>}
*/
update (key, value) {
return this.get (key).then ((currentValue) => {
if (typeof currentValue === 'object') {
if (typeof value === 'object') {
value = Object.assign ({}, currentValue, value);
}
this.storage.setItem (this.id + key, JSON.stringify (value));
} else {
this.storage.setItem (this.id + key, value);
}
return Promise.resolve ({key, value});
}).catch (() => {
return this.set (key, value);
});
}
/**
* Retrieves a value from storage given it's key
*
* @param {string} - Key with which the value was saved
* @return {Promise<Object>|Promise<string>|Promise<Number>} - Resolves to the retreived value
* or its rejected if it doesn't exist
*/
get (key) {
return this.open ().then (() => {
return new Promise ((resolve, reject) => {
let value = null;
value = this.storage.getItem (this.id + key);
try {
const o = JSON.parse (value);
if (o && typeof o === 'object') {
value = o;
}
} catch (exception) {
// Unable to parse to JSON
}
if (typeof value !== 'undefined' && value !== null) {
resolve (value);
} else {
reject ();
}
});
});
}
/**
* Retrieves all the values in the space in a key-value JSON object
*
* @return {Promise<Object>} - Resolves to the retreived values
*/
getAll () {
return this.keys ().then ((keys) => {
const values = {};
const promises = [];
for (const key of keys) {
promises.push (this.get (key).then ((value) => {
values[key] = value;
}));
}
return Promise.all (promises).then (() => {
return values;
});
});
}
/**
* Check if the space contains a given key.
*
* @param {string} key - Key to look for.
* @return {Promise} Promise gets resolved if it exists and rejected if
* doesn't
*/
contains (key) {
return this.keys ().then ((keys) => {
if (keys.includes (key)) {
Promise.resolve ();
} else {
return Promise.reject ();
}
});
}
/**
* Upgrade a Space Version
*
* @param oldVersion {string} - The version of the storage to be upgraded
* @param newVersion {string} - The version to be upgraded to
* @param callback {function} - Function to transform the old stored values to the new version's format
* @returns {Promise}
*/
upgrade (oldVersion, newVersion, callback) {
this.upgrades[`${parseInt (oldVersion.replace (/\./g, ''))}::${parseInt (newVersion.replace (/\./g, ''))}`] = callback;
return Promise.resolve ();
}
// This function acts as a helper for the upgrade progress by executing the
// needed upgrade callbacks in the correct order and sychronously.
_upgrade (upgradesToApply, resolve) {
// Check if there are still upgrades to apply
if (upgradesToApply.length > 0) {
this.upgrades[upgradesToApply[0]].call (this, this).then (() => {
this._upgrade (upgradesToApply.slice (1), resolve);
}).catch ((e) => console.error (e));
} else {
resolve ();
}
}
/**
* Rename a Space
*
* @param {string} name - New name to be used.
* @returns {Promise} - Result of the rename operation
*/
rename (name) {
// Check if the name is different
if (this.name !== name) {
return this.keys ().then ((keys) => {
// Save the previous Space id
const oldId = this.id;
// Set new object properties with the new name
this.name = name;
if (this.name !== '' && this.version !== '' && this.store !== '') {
this.id = `${this.name}::${this.store}::${this.version}_`;
} else if (this.name !== '' && this.version !== '') {
this.id = `${this.name}::${this.version}_`;
} else if (this.name !== '') {
this.id = `${this.name}::_`;
} else {
this.id = '';
}
const promises = [];
for (const key of keys) {
promises.push (this.set (key, this.storage.getItem (`${oldId}${key}`)).then (() => {
this.storage.removeItem (`${oldId}${key}`);
}));
}
return Promise.all (promises);
});
} else {
return Promise.reject ();
}
}
/**
* Get the key that corresponds to a given index in the storage
*
* @param {Number} index - Index to get the key from
* @param {boolean} [full=false] - Whether to return the full key name including space id or just the key name
* @return {Promise<string>} - Resolves to the key's name
*/
key (index, full = false) {
return this.open ().then (() => {
if (full === true) {
return Promise.resolve (this.storage.key (index));
} else {
return Promise.resolve (this.storage.key (index).replace (this.id, ''));
}
});
}
/**
* Return all keys stored in the space.
*
* @param {boolean} [full=false] - Whether to return the full key name including space id or just the key name
* @return {Promise<string[]>} - Array of keys
*/
keys (full = false) {
return this.open ().then (() => {
return Promise.resolve (Object.keys (this.storage).filter ((key) => {
return key.indexOf (this.id) === 0;
}).map ((key) => {
if (full === true) {
return key;
} else {
return key.replace (this.id, '');
}
}));
});
}
/**
* Delete a value from the space given its key
*
* @param {string} key - Key of the item to delete
* @return {Promise<value>} - Resolves to the value of the deleted object
*/
remove (key) {
return this.get (key).then ((value) => {
this.storage.removeItem (this.id + key);
return Promise.resolve (value);
});
}
/**
* Clear the entire space
*
* @return {Promise} - Result of the clear operation
*/
clear () {
return this.keys ().then ((keys) => {
for (const key of keys) {
this.remove (key);
}
return Promise.resolve ();
});
}
}