UNPKG

@aegis-framework/artemis

Version:

Aegis Framework Javascript Library

395 lines (358 loc) 11.7 kB
/** * ============================== * 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 (); }); } }