import { debounce , isEmpty , isString , isNil } from 'lodash-es' ; import { Observable } from 'rxjs' ; import { Exception } from '../errors/exception' ; const NOTIFICATION_DEBOUNCE = 300 ; const DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/ ; export enum StorageType { LocalStorage , SessionStorage , InMemoryStorage } export interface Subscription { closed : boolean ; unsubscribe ( ) : void ; } /**
 * Helper for creating and querying Local Storage or Session Storage.
 * Uses  Dictionary so all the data is encapsulated in a single
 *  storage namespace. Writes update the actual storage.
  export class Storage < T > { private _storage : typeof localStorage | typeof sessionStorage ; private _observable : Observable < string > = null ; private _containerRegex : RegExp = null ; constructor param { string } container param { StorageType } type constructor ( public container , private _type = StorageType . LocalStorage ) { this . _validateKey ( container ) ; this . _containerRegex = new RegExp ( `^@${ this . container }\/` ) ; this . switchStorage ( this . _type ) ; } type { StorageType } switchStorage ( type ) { switch ( type ) { case StorageType . LocalStorage : this . _storage = window . localStorage ; break ; case StorageType . SessionStorage : this . _storage = window . sessionStorage ; break ; case StorageType . InMemoryStorage : this . _storage = new InMemoryStorage ( ) as any ; break ; } if ( isNil ( this . _storage ) ) { throw new Exception ( 'Browser local or session storage is not supported.' ) ; } if ( ! this . _storage . hasOwnProperty ( this . container ) ) { this . _storage [ this . container ] = null ; } } param { string } key return { object } get ( key ) { const scopedKey = this . _scope ( key ) ; const item = this . _storage . getItem ( scopedKey ) ; try { return JSON . parse ( item , this . _reviver . bind ( this ) ) ; } catch ( _error ) { return item as any ; } } param { string } key param { object } value return { object } set ( key , value ) { this . _validateKey ( key ) ; try { const scopedKey = this . _scope ( key ) ; const item = JSON . stringify ( value ) ; this . _storage . setItem ( scopedKey , item ) ; return value ; } catch ( error ) { throw new Exception ( `Unable to serialize value for: ${ key } ` , error ) ; } } param { string } key return { object } delete ( key ) { try { let value = this . get ( key ) ; if ( value === undefined ) { throw new ReferenceError ( `Key: ${ key } not found.` ) ; } const scopedKey = this . _scope ( key ) ; this . _storage . removeItem ( scopedKey ) ; return value ; } catch ( error ) { throw new Exception ( `Unable to delete '${ key }' from storage` , error ) ; } } /**
     * Clear the storage.
     */ clear ( ) { this . _storage . removeItem ( this . container ) ; } param { string } key return { boolean } has ( key ) { this . _validateKey ( key ) ; return this . get ( key ) !== undefined ; } return { array } keys ( ) { try { return Object . keys ( this . _storage ) . filter ( key => this . _containerRegex . test ( key ) ) ; } catch ( error ) { throw new Exception ( `Unable to get keys from storage` , error ) ; } } return { array } values ( ) { try { return this . keys ( ) . map ( key => this . get ( key ) ) ; } catch ( error ) { throw new Exception ( `Unable to get values from storage` , error ) ; } } return { number } get count ( ) : number { try { return this . keys ( ) . length ; } catch ( error ) { throw new Exception ( `Unable to get size of localStorage` , error ) ; } } /**
     * Clear all storages.
     * Completely clears both the localStorage and sessionStorage.
     */ static clearAll ( ) { window . localStorage . clear ( ) ; window . sessionStorage . clear ( ) ; } /**
     * Returns an observable that triggers every time there's a Storage Event
     * or if the collection is modified in a different tab.
     */ notify ( next , error ? , complete ? ) { if ( ! ( this . _observable == null ) ) { return this . _observable . subscribe ( next , error , complete ) ; } this . _observable = new Observable < string > ( ( observer ) => { let debouncedUpdate = debounce ( ( event ) => { try { if ( this . _containerRegex . test ( event . key ) ) { observer . next ( event . key ) ; } } catch ( e ) { observer . error ( e ) ; } } , NOTIFICATION_DEBOUNCE ) ; window . addEventListener ( 'storage' , debouncedUpdate , false ) ; return ( ) => { window . removeEventListener ( 'storage' , debouncedUpdate , false ) ; this . _observable = null ; } ; } ) ; return this . _observable . subscribe ( next , error , complete ) ; } private _validateKey ( key ) { if ( ! isString ( key ) || isEmpty ( key ) ) { throw new TypeError ( 'Key needs to be a string' ) ; } } /**
     * Determine if the value was a Date type and if so return a Date object instead.
     * https://blog.mariusschulz.com/2016/04/28/deserializing-json-strings-as-javascript-date-objects
     */ private _reviver ( _key , value ) { if ( isString ( value ) && DATE_REGEX . test ( value ) ) { return new Date ( value ) ; } return value ; } param key private _scope ( key ) { if ( isEmpty ( this . container ) ) { return key ; } return `@${ this . container }/${ key }` ; } } /**
 * Creating a mock for folks who don't want to use localStorage.
 * This will still allow them to use the APIs.
*/ class InMemoryStorage { private _map : Map < string , string > ; constructor ( ) { console . warn ( `Using non persistent storage. Data will be lost when browser is refreshed/closed` ) ; this . _map = new Map ( ) ; } get length ( ) : number { return this . _map . size ; } clear ( ) { this . _map . clear ( ) ; } getItem ( key ) { return this . _map . get ( key ) ; } removeItem ( key ) { return this . _map . delete ( key ) ; } setItem ( key , data ) { this . _map . set ( key , data ) ; } key ( index ) { let result = undefined ; let ctr = 0 ; this . _map . forEach ( ( _val , key ) => { if ( ++ ctr === index ) { result = key ; } } ) ; return result ; } }