import { EventEmitter } from 'events' ; import { set } from '@elastic/safer-lodash-set' ; import { isPlainObject , cloneDeep , get , isEqual , isString , merge , mergeWith , toPath } from 'lodash' ; function prepSetParams ( key , value , path ) { if ( value === undefined && ( isPlainObject ( key ) || path . length > 0 ) ) { value = key ; key = undefined ; } return { value : cloneDeep ( value ) , key , } ; } type PersistedStateKey = string | string [ ] | undefined ; type PersistedStatePath = string | string [ ] ; export class PersistedState extends EventEmitter { private readonly _path : PersistedStatePath ; private readonly _initialized : boolean ; private _changedState : any ; private _defaultState : any ; private _mergedState : any ; constructor ( value ? , path ? ) { super ( ) ; this . _path = this . setPath ( path ) ; if ( ! this . _path . length && value && ! isPlainObject ( value ) ) { throw new Error ( 'State value must be a plain object' ) ; } value = value || this . getDefault ( ) ; this . set ( value ) ; this . _initialized = true ; } get ( key ? , defaultValue ? ) { if ( ! this . hasPath ( ) && key === undefined ) { return this . _mergedState ; } return cloneDeep ( get ( this . _mergedState , this . getIndex ( key || '' ) , defaultValue ) ) ; } set ( key , value ? ) { const params = prepSetParams ( key , value , this . _path ) ; const val = this . setValue ( params . key , params . value ) ; this . emit ( 'set' ) ; return val ; } setSilent ( key , value ? ) { const params = prepSetParams ( key , value , this . _path ) ; if ( params . key || params . value ) { return this . setValue ( params . key , params . value , true ) ; } } clearAllKeys ( ) { Object . getOwnPropertyNames ( this . _changedState ) . forEach ( ( key ) => { this . set ( key , null ) ; } ) ; } reset ( path ) { const keyPath = this . getIndex ( path ) ; const origValue = get ( this . _defaultState , keyPath ) ; const currentValue = get ( this . _mergedState , keyPath ) ; if ( origValue === undefined ) { this . cleanPath ( path , this . _mergedState ) ; } else { set ( this . _mergedState , keyPath , origValue ) ; } this . cleanPath ( path , this . _changedState ) ; if ( ! isEqual ( currentValue , origValue ) ) this . emit ( 'change' ) ; } getChanges ( ) { return cloneDeep ( this . _changedState ) ; } toJSON ( ) { return this . get ( ) ; } toString ( ) { return JSON . stringify ( this . toJSON ( ) ) ; } fromString ( input ) { return this . set ( JSON . parse ( input ) ) ; } private getIndex ( key ) { if ( key === undefined ) return this . _path ; return [ ... ( this . _path || [ ] ) , ... toPath ( key ) ] ; } private getPartialIndex ( key ) { const keyPath = this . getIndex ( key ) ; return keyPath . slice ( this . _path . length ) ; } private cleanPath ( path , stateTree ) { const partialPath = this . getPartialIndex ( path ) ; let remove = true ; if ( Array . isArray ( partialPath ) ) { while ( partialPath . length > 0 ) { const lastKey = partialPath . splice ( partialPath . length - 1 , 1 ) [ 0 ] ; const statePath = [ ... this . _path , partialPath ] ; const stateVal = statePath . length > 0 ? get ( stateTree , statePath as string [ ] ) : stateTree ; if ( ! isPlainObject ( stateVal ) ) return ; if ( remove ) delete stateVal [ lastKey ] ; if ( Object . keys ( stateVal ) . length > 0 ) remove = false ; } } } private getDefault ( ) { return this . hasPath ( ) ? undefined : { } ; } private setPath ( path ? ) { if ( Array . isArray ( path ) ) { return path ; } if ( isString ( path ) ) { return [ ... this . getIndex ( path ) ] ; } return [ ] ; } private hasPath ( ) { return this . _path . length > 0 ; } private setValue ( key , value , silent = false ) { const self = this ; let stateChanged = false ; const initialState = ! this . _initialized ; const keyPath = this . getIndex ( key ) ; const hasKeyPath = keyPath . length > 0 ; if ( initialState ) { this . _changedState = { } ; if ( ! this . hasPath ( ) && key === undefined ) this . _defaultState = value ; else this . _defaultState = set ( { } , keyPath , value ) ; } if ( ! initialState ) { if ( ! this . hasPath ( ) && key === undefined ) { stateChanged = ! isEqual ( this . _changedState , value ) ; this . _changedState = value ; this . _mergedState = cloneDeep ( value ) ; } else { const curVal = hasKeyPath ? this . get ( keyPath ) : this . _mergedState ; stateChanged = ! isEqual ( curVal , value ) ; if ( Array . isArray ( get ( this . _mergedState , keyPath ) ) ) { if ( hasKeyPath ) { set ( this . _mergedState , keyPath , undefined ) ; } else { this . _mergedState = undefined ; } } if ( hasKeyPath ) { set ( this . _changedState , keyPath , value ) ; } else { this . _changedState = isPlainObject ( value ) ? value : { } ; } } } const targetObj = this . _mergedState || cloneDeep ( this . _defaultState ) ; const sourceObj = merge ( { } , this . _changedState ) ; const mergeMethod = function ( targetValue , sourceValue , mergeKey ) { if ( ! initialState && isEqual ( keyPath , self . getIndex ( mergeKey ) ) ) { return sourceValue === undefined ? targetValue : sourceValue ; } } ; this . _mergedState = mergeWith ( targetObj , sourceObj , mergeMethod ) ; if ( isEqual ( this . _mergedState , this . _defaultState ) ) this . _changedState = { } ; if ( ! silent && stateChanged ) this . emit ( 'change' , key ) ; return this ; } }