/**
* The `StorageManager` constructor. Initialises the storage namespace (key prefix) to relevant `env` parameters.
*
* Checks for (in order):
* - `env.REACT_APP_BASE_URL`
* - `env.REACT_APP_NAME`
* - `window.location.pathname`
*
* The storage manager builds a singleton instance, which is shared across the application. It can be imported and used
* as per the below example:
*
* ```js
* import { storage } from '@jcu/spark'
*
* ...
*
* storage.saveData('important_info', 'This is an important message', {persist: true})
* ```
*
* @category Services
*/
export class StorageManager {
//TODO: Update to allow user provided serialize/deserialize functions
localStorage: Storage = window.localStorage
sessionStorage: Storage = window.sessionStorage
_namespace: string
_isAvailable: boolean = undefined
/**
* Whether the browser has made Storage available to the application
*
* @returns {boolean} True if there is usable Storage, false otherwise
*/
get isAvailable() : boolean {
// Look if we've already checked, if we have, return that result
if (this._isAvailable !== undefined) {
return this._isAvailable
}
// Check if we can write to and delete from storage
// If storage isn't available, this will error instead
// NB: This can also return false if the application has exceeded it's storage quota, however that means the
// storage still isn't available
const test = 'test'
try {
this.sessionStorage.setItem(test, test)
this.sessionStorage.removeItem(test)
this._isAvailable = true
return this._isAvailable
} catch(e) {
this._isAvailable = false
return this._isAvailable
}
}
/**
* Save data to local or session storage depending on options provided. Contains a `JSON.stringify` call to allow the
* storage of more complex object data. If `JSON.parse(JSON.stringify(<your object>))` works then it can be saved.
* The `StorageManager` internally handles adding an application specific prefix to the storage keys to prevent the
* possible overlap of data spaces for co-hosted applications.
*
* The default behaviour of the save function is not persistent (session storage) and doesn't overwrite existing keys.
*
* @param {string} key The key you want to save the data under
* @param {*} value The data you want to save
* @param {Object} options Additional save options
* @param {boolean} options.update Used to overwrite data if the provided key already exists
* @param {boolean} options.persist Used to determine if session or local storage is used. True is local (persistent)
* @returns {Object} An Object containing data defining the outcome of the save request
*/
saveData(key: string, value: any, options: any = {}) {
// Results object
let results: any = {
success: false,
key: key,
value: value,
errors: []
}
if (key) {
// Check if the key already exists in storage
if (this.loadData(key) && !options.update) {
console.log("The key provided already exists in storage. If you'd like to overwrite the data, use 'updateData()'")
results.success = false
results.errors.push(`Key ${key} already exists in storage. To update, use updateData()`)
} else {
// Convert value into JSON string to preserve type and structure when written to storage
let jsonValue: string = JSON.stringify(value)
// Create app specific key for data
let scopedKey: string = `${this.namespace}-${key}`.toLowerCase()
if (options.persist) {
this.localStorage.setItem(scopedKey, jsonValue)
results.success = true
} else {
this.sessionStorage.setItem(scopedKey, jsonValue)
results.success = true
}
}
} else {
results.errors.push("Key not provided")
}
return results
}
/**
* A wrapper function for saveData where the `update` option is forced to true.
* Will overwrite existing keys or save the new key.
*
* @param {string} key The key you want update or save new data too
* @param {*} value The data you want to save
* @param {Object} options Additional update options
* @param {boolean} options.persist Used to determine if session or local storage is used. True is local (persistent)
*/
updateData(key: string, value: any, options: any = {}) {
return this.saveData(key, value, {...options, update: true})
}
/**
* Delete data from storage for a given key.
*
* Checks session then local storage for a key, deletes the first one it finds.
*
* @param {string} key The key you want to delete from storage
*/
deleteData(key: string) {
if (key) {
let scopedKey = `${this.namespace}-${key}`.toLowerCase()
if (this.sessionStorage.getItem(scopedKey)) {
this.sessionStorage.removeItem(scopedKey)
} else {
this.localStorage.removeItem(scopedKey)
}
}
}
/**
* Load data from storage for a given key. The `StorageManager` internally handles adding an application specific
* prefix to the key provided to prevent possible overlap of data spaces for co-hosted applications.
*
* There are currently no supported `options` for the `loadData` function.
*
* @param {string} key The key you want to load from storage
* @param {Object} options Additional load options
*/
loadData(key: string, options: any = {}) {
// Options is cast to any because otherwise it complains if you dont have every possible object key type defined
// TODO: Fix the ts-ignore blocks.
if (key) {
let scopedKey = `${this.namespace}-${key}`.toLowerCase()
if (this.sessionStorage.getItem(scopedKey)) {
// Session storage is first precedence (due to being cleared each time)
//@ts-ignore
return JSON.parse(this.sessionStorage.getItem(scopedKey))
} else if (this.localStorage.getItem(scopedKey)) {
// Local storage is checked second as the data persists across browser sessions
//@ts-ignore
return JSON.parse(this.localStorage.getItem(scopedKey))
}
}
return null
}
get namespace() {
return this._namespace
}
constructor() {
// Set up the namespace for app specific browser storage
if (process.env.REACT_APP_BASE_URL) {
// Replace spaces and underscores with hyphens
this._namespace = `jcu--${process.env.REACT_APP_BASE_URL.replace('/', '')}`
} else if (process.env.REACT_APP_NAME) {
// Replace spaces and underscores with hyphens
this._namespace = `jcu--${process.env.REACT_APP_NAME.replace('_', '-').replace(' ', '-')}`
} else {
this._namespace = `jcu-${window.location.pathname.replace('/', '-')}`
}
}
}
//TODO: Better name for export
export let storage = new StorageManager()
Source