import { fromJS } from "immutable" ; import raf from "raf" ; import ReactDOM from "react-dom" ; import Actor from "./actor" ; import { QueryLang } from "./ql" ; import { RxLang } from "./rx" ; import { isArray , isString } from "./type" ; import { IMap , IOptions , IViewActionMapper , TViewAction } from "./typing" ; export type TDispatch = ( ) => void ; export type TRollback = ( ) => void ; export type TSubscribeHandler = ( state : IMap ) => void ; /**
 * 是不是可以批量处理
 * ReactDOM'sunstable_batchedUpdates可以很酷的解决父子组件级联渲染的问题
 * 可惜Preact不支持，只能靠Immutable的不可变这个特性来挡着了
 * 在react-native环境中，unstable_batchedUpdates是在react-native对象中
 * 所以我们的babel-plugin-plume2就是去解决这个问题
 */ const batchedUpdates = ReactDOM . unstable_batchedUpdates || function ( cb ) { cb ( ) ; } ; /**
 * Store状态容器
 * 整个应用中心的状态管理 控制整个应用的状态控制
 * Store = f(Actor, ViewAction)
 */ export default class Store < T = { } > { constructor ( props ? ) { this . _opts = props || { debug : false } ; this . _cacheQL = { } ; this . _uiSubscribers = [ ] ; this . _isInTranstion = false ; console . groupCollapsed && console . groupCollapsed ( `Store init 🚀` ) ; this . _state = fromJS ( { } ) ; this . _reduceActorState ( ) ; this . viewAction = { } as TViewAction < T > ; this . _initViewAction ( ) ; this . _initRx ( ) ; this . bindGlobalState ( ) ; if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . groupEnd && console . groupEnd ( ) ; } } public readonly viewAction : TViewAction < T > ; private _opts : IOptions ; private _state : IMap ; private _rxSubscribers : Array < Function > ; private _uiSubscribers : Array < TSubscribeHandler > ; private _actors : Array < Actor > ; private _actorsState : Array < IMap > ; private _cacheQL : { [ name ] : Array < any > ; } ; private _isInTranstion : boolean ; bindActor ( ) { return [ ] ; } bindViewAction ( ) { return { } ; } bindRx ( ) { return { } ; } bindGlobalState ( ) { } param msg param params dispatch ( msg , params ? ) { if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . groupCollapsed && console . groupCollapsed ( `store dispatch => '${ msg }'` ) ; if ( typeof params !== "undefined" ) { if ( typeof params === "object" ) { console . log ( `params|> immutable[${ params . toJS ? "yes" : "no" }] ` ) ; console . dir && console . dir ( params . toJS ? params . toJS ( ) : params ) ; } else { console . log ( `params|> ${ params }` ) ; } } } const newStoreState = this . _dispatchActor ( msg , params ) ; if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . groupEnd && console . groupEnd ( ) ; } if ( newStoreState != this . _state ) { this . _state = newStoreState ; if ( ! this . _isInTranstion ) { this . _notifier ( ) ; } } } param dispatch param rollBack return transaction ( dispatch , rollBack ? ) { let isRollback = false ; if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . groupCollapsed && console . groupCollapsed ( "::::::::::::::::🚀 open new transaction 🚀::::::::::::::::::" ) ; } this . _isInTranstion = true ; const currentStoreState = this . _state ; try { dispatch ( ) ; } catch ( err ) { if ( rollBack ) { rollBack ( ) ; } else { this . _state = currentStoreState ; } isRollback = true ; if ( process . env . NODE_ENV != "production" ) { console . warn ( "😭, some exception occur in transaction, store state roll back" ) ; console . log ( err ) ; } } if ( currentStoreState != this . _state ) { this . _notifier ( ) ; } this . _isInTranstion = false ; if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . groupEnd && console . groupEnd ( ) ; } return isRollback ; } param ql bigQuery = ( ql ) : any => { if ( isString ( ql ) ) { return this . _state . get ( ql as string ) ; } if ( isArray ( ql ) ) { return this . _state . getIn ( ql as Array < any > ) ; } if ( ! ( ql instanceof QueryLang ) ) { throw new Error ( "invalid QL" ) ; } let outdate = false ; const id = ql . id ( ) ; const name = ql . name ( ) ; this . _cacheQL [ id ] = this . _cacheQL [ id ] || [ ] ; const lang = ql . lang ( ) . slice ( ) ; const rxFn = lang . pop ( ) ; if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . groupCollapsed && console . groupCollapsed ( `🔥:tracing: QL(${ name })` ) ; } let args = lang . map ( ( elem , index ) => { if ( elem instanceof QueryLang ) { const value = this . bigQuery ( elem ) ; if ( value != this . _cacheQL [ id ] [ index ] ) { outdate = true ; this . _cacheQL [ id ] [ index ] = value ; } if ( process . env . NODE_ENV != "production" ) { if ( this . _opts . debug ) { console . log ( `dep:${ elem . name ( ) }, cache:${ ! outdate },value:${ JSON . stringify ( value , null , 2 ) }` ) ; } } return value ; } else { const value = isArray ( elem ) ? this . _state . getIn ( elem ) : this . _state . get ( elem ) ; if ( this . _cacheQL [ id ] . length == 0 || value != this . _cacheQL [ id ] [ index ] ) { outdate = true ; this . _cacheQL [ id ] [ index ] = value ; } if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . log ( `dep:${ elem }, cache:${ ! outdate }, value:${ JSON . stringify ( value , null , 2 ) }` ) ; } return value ; } } ) ; if ( outdate ) { const result = rxFn . apply ( null , args ) ; this . _cacheQL [ id ] [ args . length ] = result ; if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . log ( `QL(${ name })|> ${ JSON . stringify ( result , null , 2 ) }` ) ; console . groupEnd && console . groupEnd ( ) ; } return result ; } else { if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . log ( `🚀:QL(${ name }), cache: true, result: ${ JSON . stringify ( this . _cacheQL [ id ] [ args . length ] , null , 2 ) }` ) ; console . groupEnd && console . groupEnd ( ) ; } return this . _cacheQL [ id ] [ args . length ] ; } } ; /**
     * 获取store容器的数据状态
     */ state ( ) { return this . _state ; } /**
     *获取数据的快捷方式
     */ get ( path ) { return this . bigQuery ( path ) ; } param state setState ( state ) { this . _state = state ; } param cb subscribe ( cb ) { if ( typeof cb != "function" || this . _uiSubscribers . indexOf ( cb ) != - 1 ) { return ; } this . _uiSubscribers . push ( cb ) ; } param cb unsubscribe ( cb ) { const index = this . _uiSubscribers . indexOf ( cb ) ; if ( typeof cb != "function" || index == - 1 ) { return ; } this . _uiSubscribers . splice ( index , 1 ) ; } destroy ( ) { this . _cacheQL = null ; this . _state = null ; this . _rxSubscribers = null ; this . _actors = null ; this . _actorsState = null ; } private _initViewAction = ( ) => { const viewActionMapper = this . bindViewAction ( ) || { } ; const keys = Object . keys ( viewActionMapper ) ; for ( let key of keys ) { const ViewAction = viewActionMapper [ key ] ; const viewAction = new ViewAction ( ) ; if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . log ( `viewAction:merge ${ ( viewAction . constructor as any ) . name }` ) ; } ( viewAction as any ) . _bindStore ( this ) ; this . viewAction [ key ] = viewAction ; } } ; private _reduceActorState ( ) { this . _actors = [ ] ; this . _actorsState = [ ] ; const actors = this . bindActor ( ) || [ ] ; this . _state = this . _state . withMutations ( state => { for ( let actor of actors ) { if ( typeof actor === "function" ) { actor = new actor ( ) ; } if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . log ( `Actor:reduce ${ ( actor . constructor as any ) . name }` ) ; } this . _actors . push ( actor ) ; let initState = fromJS ( actor . defaultState ( ) ) ; this . _actorsState . push ( initState ) ; state = state . merge ( initState ) ; } return state ; } ) ; } private _initRx ( ) { this . _rxSubscribers = [ ] ; const rxs = this . bindRx ( ) || [ ] ; for ( let rx in rxs ) { this . _rxSubscribers . push ( this . _parseRL ( rxs [ rx ] ) ) ; } } private _parseRL ( rl ) { const cache = [ ] ; const name = rl . name ( ) ; const lang = rl . lang ( ) . slice ( ) ; const rxFn = lang . pop ( ) ; if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . log ( `parse:rl -> ${ name }` ) ; } for ( let dep of lang ) { cache . push ( this . bigQuery ( dep ) ) ; } return ( ) => { let isChanged = false ; lang . forEach ( ( l , i ) => { const v = this . bigQuery ( l ) ; if ( v != cache [ i ] ) { isChanged = true ; cache [ i ] = v ; } } ) ; if ( isChanged ) { if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . log ( `${ name } => 🔥` ) ; } rxFn . apply ( null , cache ) ; } } ; } private _notifier ( ) { batchedUpdates ( ( ) => { this . _uiSubscribers . forEach ( cb => cb ( this . _state ) ) ; if ( this . _rxSubscribers . length > 0 ) { raf ( ( ) => { if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . groupCollapsed && console . groupCollapsed ( "Reactive 🚀" ) ; } this . _rxSubscribers . forEach ( cb => cb ( ) ) ; if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . groupEnd && console . groupEnd ( ) ; } } ) ; } } ) ; } private _dispatchActor ( msg , params ? ) { let _state = this . _state ; for ( let i = 0 , len = this . _actors . length ; i < len ; i ++ ) { let actor = this . _actors [ i ] ; const fn = ( actor as any ) . _route [ msg ] ; if ( ! fn ) { continue ; } if ( process . env . NODE_ENV != "production" && this . _opts . debug ) { console . log ( `${ ( actor . constructor as any ) . name } receive => '${ msg }'` ) ; } let preActorState = this . _actorsState [ i ] ; const newActorState = actor . receive ( { msg , state : preActorState , params } ) ; if ( preActorState != newActorState ) { this . _actorsState [ i ] = newActorState ; _state = _state . merge ( newActorState ) ; } } return _state ; } /**
     * 打印store中的数据状态
     */ pprint ( ) { if ( process . env . NODE_ENV != "production" ) { console . log ( JSON . stringify ( this . _state , null , 2 ) ) ; } } /**
     * 打印store中的数据状态是从哪些Actor中聚合
     */ pprintActor ( ) { if ( process . env . NODE_ENV != "production" ) { const stateObj = { } ; this . _actors . forEach ( ( actor , index ) => { const name = ( actor . constructor as any ) . name ; stateObj [ name ] = this . _actorsState [ index ] . toJS ( ) ; } ) ; console . log ( JSON . stringify ( stateObj , null , 2 ) ) ; } } }