/**
 * Copyright (c) 2018-Present, Nitrogen Labs, Inc.
 * Copyrights licensed under the MIT License. See the accompanying LICENSE file for terms.
 */ import ErrorStackParser from 'error-stack-parser' ; import { EventEmitter } from 'events' ; import debounce from 'lodash/debounce' ; import cloneDeep from 'lodash/fp/cloneDeep' ; import get from 'lodash/fp/get' ; import isEmpty from 'lodash/fp/isEmpty' ; import merge from 'lodash/fp/merge' ; import set from 'lodash/fp/set' ; import { ArkhamConstants } from '../constants/ArkhamConstants' ; import { FluxAction , FluxMiddlewareType , FluxOptions , FluxPluginType , FluxStore } from './Flux.types' ; type { EventEmitter } export class FluxFramework extends EventEmitter { static initFlux : boolean = false ; isInit : boolean = false ; pluginTypes : string [ ] = [ 'preDispatch' , 'postDispatch' ] ; private state : any = { } ; private storeActions : any = { } ; private defaultOptions : FluxOptions = { name : 'arkhamjs' , routerType : 'browser' , scrollToTop : true , state : null , storage : null , storageWait : 300 , stores : [ ] , title : 'ArkhamJS' } ; private middleware : any = { } ; private options : FluxOptions = this . defaultOptions ; constructor this { FluxFramework } constructor ( ) { super ( ) ; this . addMiddleware = this . addMiddleware . bind ( this ) ; this . addStores = this . addStores . bind ( this ) ; this . clearAppData = this . clearAppData . bind ( this ) ; this . clearMiddleware = this . clearMiddleware . bind ( this ) ; this . deregister = this . deregister . bind ( this ) ; this . dispatch = this . dispatch . bind ( this ) ; this . getOptions = this . getOptions . bind ( this ) ; this . getState = this . getState . bind ( this ) ; this . getStore = this . getStore . bind ( this ) ; this . init = this . init . bind ( this ) ; this . off = this . off . bind ( this ) ; this . register = this . register . bind ( this ) ; this . removeMiddleware = this . removeMiddleware . bind ( this ) ; this . removeStores = this . removeStores . bind ( this ) ; this . reset = this . reset . bind ( this ) ; this . setState = this . setState . bind ( this ) ; this . pluginTypes . forEach ( ( type ) => this . middleware [ `${ type }List` ] = [ ] ) ; } param { array } middleware addMiddleware ( middleware ) { middleware . forEach ( ( middleObj ) => { if ( ! ! middleObj && ( ( typeof middleObj === 'function' ) || ( typeof middleObj === 'object' ) ) ) { const middleName = middleObj . name || '' ; if ( ! middleName ) { throw Error ( 'Unknown middleware is not configured properly. Requires name property. Cannot add to Flux.' ) ; } this . pluginTypes . forEach ( ( type ) => { const method = middleObj [ type ] ; const plugin = { method , name : middleName } ; this . middleware [ `${ type }List` ] = this . addPlugin ( type , plugin ) ; } ) ; } else { throw Error ( 'Unknown middleware is not configured properly. Cannot add to Flux.' ) ; } } ) ; } returns { Promise < boolean > } clearAppData ( ) { Object . keys ( this . storeActions ) . forEach ( ( storeName ) => { const storeFn = this . storeActions [ storeName ] ; this . state [ storeFn . name ] = cloneDeep ( storeFn . initialState ) ; } ) ; const { name , storage } = this . options ; if ( storage ) { return storage . setStorageData ( name , this . state ) ; } return Promise . resolve ( true ) ; } returns { boolean } clearMiddleware ( ) { Object . keys ( this . middleware ) . forEach ( ( pluginType ) => { this . middleware [ pluginType ] = [ ] ; } ) ; return true ; } param { array } storeNames removeStores ( storeNames ) { storeNames . forEach ( ( name ) => this . deregister ( name ) ) ; } param { object } action param { boolean } silent returns { Promise } async dispatch ( action , silent = false ) { if ( ! action ) { throw new Error ( 'ArkhamJS Error: Flux.dispatch requires an action.' ) ; } let clonedAction = cloneDeep ( action ) ; const startTime = Date . now ( ) ; let stack = [ ] ; try { const stackProperty = 'stackTraceLimit' ; const { stackTraceLimit } = Error ; Error [ stackProperty ] = Infinity ; stack = ErrorStackParser . parse ( new Error ( ) ) ; Error [ stackProperty ] = stackTraceLimit ; } catch ( error ) { } const options = cloneDeep ( this . options ) ; const appInfo = { duration : 0 , options , stack } ; const { postDispatchList = [ ] , preDispatchList = [ ] } = this . middleware ; if ( preDispatchList . length ) { clonedAction = await Promise . all ( preDispatchList . map ( ( plugin ) => plugin . method ( cloneDeep ( clonedAction ) , cloneDeep ( this . state ) , appInfo ) ) ) . then ( ( actions ) => actions . reduce ( ( updatedAction , action ) => merge ( updatedAction , action ) , clonedAction ) as FluxAction ) . catch ( ( error ) => { throw error ; } ) ; } const { type , ... data } = clonedAction ; if ( ! type || type === '' ) { console . warn ( 'ArkhamJS Warning: Flux.dispatch is missing an action type for the payload:' , data ) ; return Promise . resolve ( clonedAction ) ; } Object . keys ( this . storeActions ) . forEach ( ( storeName ) => { const storeFn = this . storeActions [ storeName ] ; const state = cloneDeep ( this . state [ storeName ] ) || cloneDeep ( storeFn . initialState ) || { } ; this . state [ storeName ] = cloneDeep ( storeFn . action ( type , data , state ) ) || state ; } ) ; const { storage } = this . options ; if ( storage ) { try { await this . updateStorage ( ) ; } catch ( error ) { } } const endTime = + ( new Date ( ) ) ; const duration = endTime - startTime ; appInfo . duration = duration ; if ( postDispatchList . length ) { clonedAction = await Promise . all ( postDispatchList . map ( async ( plugin ) => plugin . method ( cloneDeep ( clonedAction ) , cloneDeep ( this . state ) , appInfo ) ) ) . then ( ( actions ) => actions . reduce ( ( updatedAction , action ) => merge ( updatedAction , action ) , clonedAction ) as FluxAction ) . catch ( ( error ) => { throw error ; } ) ; } if ( ! silent ) { this . emit ( type , clonedAction ) ; this . emit ( 'arkhamjs' , this . state ) ; } return Promise . resolve ( clonedAction ) ; } returns { FluxOptions } getOptions ( ) { return this . options ; } param { string | array } name param { any } defaultValue returns { any } getState ( path = '' , defaultValue ? ) { let storeValue ; if ( ! path ) { storeValue = this . state || { } ; } else { storeValue = get ( path , this . state ) ; } const value = storeValue ? cloneDeep ( storeValue ) : storeValue ; return value === undefined ? defaultValue : value ; } param { string } name returns { FluxStore } getStore ( name = '' ) { return this . storeActions [ name ] ; } param { object } options async init ( options = { } , reset = false ) { if ( reset ) { this . isInit = false ; this . reset ( false ) ; } const updatedOptions = { ... options } ; if ( this . isInit ) { updatedOptions . name = this . options . name ; } this . options = { ... this . defaultOptions , ... updatedOptions } ; const { debug , middleware , name , stores } = this . options ; try { await this . useStorage ( name ) ; } catch ( error ) { console . error ( 'Arkham Error: There was an error while using storage.' , name ) ; throw error ; } if ( ! ! stores && stores . length ) { try { await this . addStores ( stores ) ; } catch ( error ) { console . error ( 'Arkham Error: There was an error while adding stores.' , stores ) ; throw error ; } } if ( ! ! middleware && middleware . length ) { this . addMiddleware ( middleware ) ; } const windowProperty = 'arkhamjs' ; if ( debug ) { window [ windowProperty ] = this ; } else { delete window [ windowProperty ] ; } this . isInit = true ; this . emit ( ArkhamConstants . INIT ) ; return this ; } param { function } listener onInit ( listener ) { this . on ( ArkhamConstants . INIT , listener ) ; if ( this . isInit ) { listener ( ) ; } } param { function } listener offInit ( listener ) { this . off ( ArkhamConstants . INIT , listener ) ; } param { string } eventType param { function } listener off ( eventType , listener ) { return this . removeListener ( eventType , listener ) ; } param { string } eventType param { function } listener on ( eventType , listener ) { return this . addListener ( eventType , listener ) ; } param { array } stores returns { Promise < object [ ] > } async addStores ( stores ) { const registeredStores = stores . map ( ( store ) => this . register ( store ) ) ; const { name , storage } = this . options ; if ( storage ) { try { await storage . setStorageData ( name , this . state ) ; } catch ( error ) { throw error ; } } return registeredStores ; } param { array } string returns { Promise < object [ ] > } removeMiddleware ( names ) { names . forEach ( ( name ) => { this . pluginTypes . forEach ( ( type ) => { this . middleware [ `${ type }List` ] = this . removePlugin ( type , name ) ; } ) ; } ) ; } param { array } string returns { Promise < object [ ] > } async reset ( clearStorage = true ) { const { name , storage } = this . options ; if ( storage && clearStorage ) { try { await storage . setStorageData ( name , { } ) ; } catch ( error ) { throw error ; } } this . middleware = { } ; this . options = { ... this . defaultOptions } ; this . state = { } ; this . storeActions = { } ; this . isInit = false ; } param { string | array } name param { any } value setState ( path = '' , value ) { if ( ! ! path ) { this . state = set ( path , cloneDeep ( value ) , this . state ) ; } const { storage } = this . options ; if ( storage ) { return this . updateStorage ( ) ; } return Promise . resolve ( false ) ; } private addPlugin ( type , plugin ) { const list = this . middleware [ `${ type }List` ] || [ ] ; const { method , name } = plugin ; if ( method && typeof method === 'function' ) { const exists = ! ! list . filter ( ( obj ) => obj . name === name ) . length ; if ( ! exists ) { list . push ( { method , name } ) ; } } else if ( method !== undefined ) { throw Error ( `${ plugin . name } middleware is not configured properly. Method is not a function.` ) ; } return list ; } private deregister ( name = '' ) { delete this . storeActions [ name ] ; delete this . state [ name ] ; } private register ( storeFn ) { if ( ! storeFn ) { throw Error ( 'Store is undefined. Cannot register with Flux.' ) ; } const isFnc = typeof storeFn === 'function' ; if ( ! isFnc ) { throw Error ( `${ storeFn } is not a store function. Cannot register with Flux.` ) ; } const { name } = storeFn ; const initialState = storeFn ( ) ; const storeAction = { action : storeFn , initialState : storeFn ( ) , name } ; if ( ! isEmpty ( name ) && ! this . storeActions [ name ] ) { this . storeActions [ name ] = storeAction ; if ( ! this . state [ name ] ) { if ( initialState ) { this . state [ name ] = cloneDeep ( initialState ) ; } else { this . state [ name ] = { } ; } } } return this . storeActions [ name ] ; } private removePlugin ( type , name ) { const list = this . middleware [ `${ type }List` ] || [ ] ; return list . filter ( ( obj ) => obj . name !== name ) ; } private updateStorage = ( ) => Promise . resolve ( false ) ; private async useStorage ( name ) { const { storage , state , storageWait } = this . options ; if ( storage ) { try { this . state = state || await storage . getStorageData ( name ) || { } ; this . updateStorage = debounce ( ( ) => storage . setStorageData ( name , this . state ) , storageWait , { leading : true , trailing : true } ) ; } catch ( error ) { console . error ( `ArkhamJS Error: Using storage, "${ name }".` ) ; throw error ; } } else { this . state = state || { } ; } return null ; } } export const Flux = new FluxFramework ( ) ;