import * as React from "react" ; import logger from "./logger" ; import { Comp , Connect , Dispatch , Node , Store , Watch } from "./types" ; import { joinPath , splitPath } from "./utils" ; import { subscribe , unsubscribe } from "./watch" ; export const ProdoContext = React . createContext < Store < any , any > > ( null as any ) ; const pathSymbol = Symbol ( "path" ) ; export const createUniverseWatcher = ( universePath ) => { const readProxy = ( path ) : any => new Proxy ( { } , { get : ( _target , key ) => key === pathSymbol ? path : readProxy ( path . concat ( [ key . toString ( ) ] ) ) , } ) ; return readProxy ( [ universePath ] ) ; } ; const getValue = ( path , obj ) : any => path . reduce ( ( x , y ) => x && x [ y ] , obj ) ; const valueExtractor = ( store , watched ) => ( x ) => { const path = x [ pathSymbol ] ; const pathKey = joinPath ( path ) ; const value = getValue ( path , store . universe ) ; watched [ pathKey ] = value ; return value ; } ; export const shallowEqual = ( objA , objB ) : boolean => { if ( Object . is ( objA , objB ) ) { return true ; } if ( typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null ) { return false ; } const keysA = Object . keys ( objA ) ; const keysB = Object . keys ( objB ) ; if ( keysA . length !== keysB . length ) { return false ; } for ( let i = 0 ; i < keysA . length ; i ++ ) { if ( ! Object . prototype . hasOwnProperty . call ( objB , keysA [ i ] ) || ! Object . is ( objA [ keysA [ i ] ] , objB [ keysA [ i ] ] ) ) { return false ; } } return true ; } ; let _compIdCnt = 1 ; export type Func < V , P = { } > = ( viewCtx : V ) => React . ComponentType < P > ; export const connect = < P extends { } > ( func , name = "(anonymous)" ) : React . ComponentType < P > => class ConnectComponent < P > extends React . Component < P , any > { public static contextType = ProdoContext ; public state : any ; private watched : { [ key ] : any ; } ; private prevWatched : { [ key ] : any ; } ; private pathNodes : { [ key ] : Node ; } ; private compId : number ; private comp : Comp ; private name : string ; private eventIdCnt : number ; private subscribe : ( path : string [ ] , unsubscribe ? : ( comp : Comp ) => void ) => void ; private unsubscribe : ( path : string [ ] ) => void ; private status : { unmounted : boolean ; } ; private store : Store < any , any > ; private _renderFunc : any ; private _watch : Watch ; private _dispatch : Dispatch ; private _state : any ; private _viewCtx : any ; constructor ( props ) { super ( props ) ; this . state = { } ; this . watched = { } ; this . prevWatched = { } ; this . pathNodes = { } ; this . compId = _compIdCnt ++ ; this . name = name + "." + this . compId ; this . store = this . context ; const setState = this . setState . bind ( this ) ; this . status = { unmounted : false } ; this . comp = { name : this . name , compId : this . compId , } ; this . eventIdCnt = 0 ; this . subscribe = ( path , unsubscribe ? ) => { const pathKey = joinPath ( path ) ; const node = this . pathNodes [ pathKey ] || { pathKey , status : this . status , setState , unsubscribe , ... this . comp , } ; this . pathNodes [ pathKey ] = node ; subscribe ( this . store , path , node ) ; } ; this . unsubscribe = ( path ) => { const pathKey = joinPath ( path ) ; const node = this . pathNodes [ pathKey ] ; if ( node != null ) { unsubscribe ( this . store , path , node ) ; delete this . pathNodes [ pathKey ] ; } } ; this . _watch = x => x ; this . _dispatch = func => ( ... args ) => this . store . exec ( { id : `${ this . comp . name }/event.${ this . eventIdCnt ++ }` , parentId : this . comp . name , } , func , ... args ) ; this . _renderFunc = ( props ) : any => { return ( func as ( ( args : any ) => ( props : any ) => any ) ) ( this . _viewCtx ) ( props ) ; } ; logger . info ( `[constructing] ${ this . name }` ) ; } public componentDidMount ( ) { logger . info ( `[did mount] ${ this . name }` ) ; Object . keys ( this . watched ) . forEach ( pathKey => { logger . info ( `[start watching] ${ this . name }: < ${ pathKey } >` ) ; this . subscribe ( splitPath ( pathKey ) ) ; } ) ; logger . debug ( "store" , this . store ) ; this . prevWatched = { ... this . watched } ; this . setState ( this . watched ) ; this . watched = { } ; } public shouldComponentUpdate ( nextProps , nextState ) { const test = ! shallowEqual ( this . props , nextProps ) || ! shallowEqual ( this . state , nextState ) ; logger . info ( `[should update] ${ this . name }` , test ) ; return test ; } public componentDidUpdate ( ) { logger . info ( `[did update] ${ this . name }` ) ; Object . keys ( this . watched ) . forEach ( pathKey => { const keyExisted = this . prevWatched . hasOwnProperty ( pathKey ) ; if ( ! keyExisted ) { logger . info ( `[update] ${ this . name }: now watching < ${ pathKey } >` ) ; this . subscribe ( splitPath ( pathKey ) ) ; } } ) ; Object . keys ( this . prevWatched ) . forEach ( pathKey => { const keyDeleted = ! this . watched . hasOwnProperty ( pathKey ) ; if ( keyDeleted ) { logger . info ( `[update] ${ this . name }: stop watching < ${ pathKey } >` ) ; this . unsubscribe ( splitPath ( pathKey ) ) ; } } ) ; this . prevWatched = { ... this . watched } ; this . watched = { } ; } public componentWillUnmount ( ) { logger . info ( `[will unmount]: ${ this . name }` , this . state ) ; Object . keys ( this . state ) . forEach ( pathKey => { logger . info ( `[unmount] ${ this . name }: stop watching < ${ pathKey } >` ) ; this . unsubscribe ( splitPath ( pathKey ) ) ; } ) ; logger . debug ( "store" , this . store ) ; this . status . unmounted = true ; } public render ( ) { this . createViewCtx ( ) ; const Comp = this . _renderFunc ; return < Comp { ... this . props } / > ; } private createViewCtx ( ) { this . store = this . context ; this . _state = createUniverseWatcher ( "state" ) ; this . _watch = valueExtractor ( this . store , this . watched ) ; const subscribe = ( path , unsubscribe ? ) : void => { const pathKey = joinPath ( path ) ; this . watched [ pathKey ] = getValue ( path , this . store . universe ) ; this . subscribe ( path , unsubscribe ) ; } ; const ctx = { dispatch : this . _dispatch , state : this . _state , watch : this . _watch , subscribe , } ; this . store . plugins . forEach ( p => { if ( p . _internals . viewCtx ) { ( ctx as any ) . dispatch = this . _dispatch ; p . _internals . viewCtx ( { ctx , universe : this . store . universe , comp : this . comp , } , this . store . config ) ; } } ) ; this . _viewCtx = ctx ; } } ;