import { useEffect , useLayoutEffect , useMemo , useReducer , useRef } from 'react' ; import { computeAsyncActionInput , createRef , isSyncAction , isAsyncAction , isSimpleSyncAction , TimeoutMap , PromiseMap , DEFAULT_PROMISE_ID , globalConfig , setGlobalConfig , GlobalConfig , ConflictActionsMap , buildOnMountInvocationContext , decorateSimpleSyncAction , decorateSyncAction , decorateAsyncAction , computeDerivedValues , DerivedState , onPropChange , runMiddlewares , buildLoadingMap , isLoadingImpl , } from './impl' ; import { DecoratedActions , StoreActions , AsyncAction , SyncAction , SimpleSyncAction , InternalLoadingMap , LoadingMap , LoadingParallelMap , StoreOptions , AsyncActionPromise , InvocationContext , GetPromiseInvocationContext , EffectsInvocationContext , InvocationContextActions , WarningNotifyFunc , ErrorEffectsInvocationContext , OnMountInvocationContext , ContextBase , OnPropsChangeEffectsContext , ContextState , ConflictPolicy , Middleware , AbortActionCallback , MiddlewareFactory , MiddlewareStoreContext , LoadingProps , } from './types' ; export { DecoratedActions , StoreActions , LoadingMap , StoreOptions , LoadingParallelMap , AsyncActionPromise as AsynchActionPromise , InvocationContext , GetPromiseInvocationContext , EffectsInvocationContext , InvocationContextActions , WarningNotifyFunc , ErrorEffectsInvocationContext , OnMountInvocationContext , ContextBase , OnPropsChangeEffectsContext , SyncAction , AsyncAction , SimpleSyncAction , ContextState , ConflictPolicy , GlobalConfig , Middleware , setGlobalConfig , LoadingProps , AbortActionCallback , } ; export type IsLoadingFunc < A > = ( ... props : ( keyof A | [ keyof A , string ] ) [ ] ) => boolean ; type StateListenerUnregister = ( ) => void ; export type StoreApi < S , A extends DecoratedActions , P , DS = { } > = { /**
     * The current store state (with derived state if any).
     */ readonly state : S & DS ; /**
     * The store decorator (ie. ready to use) actions.
     */ readonly actions : A ; /**
     * A boolean that indicates that at least one asynchronous action is loading.
     */ readonly loading : boolean ; /**
     * A map of loading actions map (computed on property access)
     */ readonly loadingMap : LoadingMap < A > ; /**
     * A map of parallel loading actions map (computed on property access)
     */ readonly loadingParallelMap : LoadingParallelMap < A > ; /**
     * Initializes the store with the specified props
     */ init : ( p : P ) => void ; /**
     * Update the store with the specified props
     */ setProps : ( p : P ) => void ; /**
     * Destroy the store
     */ destroy : ( ) => void ; /**
     * Abort an asynchronous action marked as abortable.
     */ abortAction : ( actionName : keyof A , promiseId ? : string ) => boolean ; /**
     * Add a store state change listener
     */ addStateListener : ( listener : StateListener < S , DS , A > ) => StateListenerUnregister ; /**
     * A function that takes a list of action names or a tuple of [action name, promiseId] and returns <code>true</code> if any action is loading.
     */ isLoading : IsLoadingFunc < A > ; getConfig : ( ) => { getInitialState : ( p : P ) => S ; options : StoreOptions < S , A , P , DS > ; actions : StoreActions < S , A , P , DS > ; } ; } ; export type StateListenerContext < S , DS , A extends DecoratedActions > = S & DS & A & Pick < StoreApi < S , A , any , DS > , 'loading' | 'loadingMap' | 'isLoading' | 'abortAction' > ; export type StateListener < S , DS , A extends DecoratedActions > = ( ctx : StateListenerContext < S , DS , A > ) => void ; param getInitialState param actionsImpl param options returns export function createStore < S , A extends DecoratedActions , P , DS = { } > ( getInitialState , actionsImpl , options = { } , storeMiddlewares = null ) { const stateRef = createRef < S > ( ) ; const derivedStateRef = createRef < DerivedState < DS > > ( ) ; const propsRef = createRef < P > ( ) ; const actionsRef = createRef < A > ( ) ; const timeoutRef = createRef < TimeoutMap < A > > ( ) ; const loadingMapRef = createRef < InternalLoadingMap < A > > ( ) ; const promisesRef = createRef < PromiseMap < A > > ( ) ; const conflictActionsRef = createRef < ConflictActionsMap < A > > ( ) ; const initializedRef = createRef ( false ) ; const middlewaresRef = createRef < Middleware < S , A , P > [ ] > ( [ ] ) ; let stateListeners = { } ; let stateListenerCount = 0 ; function addStateListener ( listener ) { const id = `${ stateListenerCount ++ }` ; stateListeners [ id ] = listener ; return ( ) => { delete stateListeners [ id ] ; } ; } function notifyStateListeners ( newState , derivedState ) { const s = { ... newState , ... derivedState , } ; const ctx = { ... s , ... actionsRef . current , isLoading , abortAction , loading : isAnyLoading ( ) , loadingMap : getLoadingMap ( ) , } ; Object . keys ( stateListeners ) . forEach ( ( listenerId ) => { stateListeners [ listenerId ] ( ctx ) ; } ) ; } function setProps ( p ) { const oldProps = propsRef . current ; propsRef . current = p ; if ( ! initializedRef . current ) { init ( p ) ; } else { onPropChange ( stateRef , derivedStateRef , propsRef , oldProps , actionsRef , options , setState , false ) ; } } /** Middleware only: sets the internal state, recompute derived state and notify listeners */ function setInternalState ( s ) { stateRef . current = s ; computeDerivedValues ( stateRef , propsRef , derivedStateRef , options ) ; notifyStateListeners ( stateRef . current , derivedStateRef . current . state ) ; } function init ( p ) { if ( ! initializedRef . current ) { propsRef . current = p ; initializedRef . current = true ; stateRef . current = getInitialState ( p ) ; const initialActionSet = new Set ( options ?. initialActionsMarkedLoading ) ; loadingMapRef . current = ( Object . keys ( actionsImpl ) as ( keyof A ) [ ] ) . reduce < InternalLoadingMap < A > > ( ( acc , name ) => { acc [ name ] = initialActionSet . has ( name ) ? { [ DEFAULT_PROMISE_ID ] : true } : { } ; return acc ; } , { } ) ; timeoutRef . current = { } ; promisesRef . current = { } ; conflictActionsRef . current = { } ; derivedStateRef . current = { state : null , deps : { } , } ; middlewaresRef . current = [ ] ; const ctx = { options , get state ( ) { return stateRef . current ; } , actions : actionsImpl , setState : setInternalState , } ; const mapper = ( m ) => { const middleware = m ( ) ; middlewaresRef . current . push ( middleware ) ; middleware . init ?. ( ctx ) ; } ; globalConfig . defaultMiddlewares ?. forEach ( mapper ) ; storeMiddlewares ?. forEach ( mapper ) ; computeDerivedValues ( stateRef , propsRef , derivedStateRef , options ) ; onPropChange ( stateRef , derivedStateRef , propsRef , null , actionsRef , options , setState , true ) ; if ( options ?. onMount ) { options . onMount ( buildOnMountInvocationContext ( stateRef , derivedStateRef , propsRef , actionsRef ) ) ; } notifyStateListeners ( stateRef . current , derivedStateRef . current . state ) ; } } function destroy ( ) { if ( initializedRef . current ) { options ?. onUnmount ?. ( buildOnMountInvocationContext ( stateRef , derivedStateRef , propsRef , actionsRef ) ) ; propsRef . current = null ; stateRef . current = null ; loadingMapRef . current = null ; promisesRef . current = null ; conflictActionsRef . current = null ; derivedStateRef . current = null ; Object . keys ( timeoutRef . current ) . forEach ( ( name ) => { clearTimeout ( timeoutRef . current [ name ] ) ; } ) ; timeoutRef . current = null ; stateListeners = { } ; initializedRef . current = false ; middlewaresRef . current . forEach ( ( m ) => { m . destroy ?. ( ) ; } ) ; middlewaresRef . current = [ ] ; } } function setState ( newStateIn , newLoadingMapIn , actionName , actionType , isAsync , actionCtx , propsChanged = false , init = false ) { let hasChanged = false ; if ( ! initializedRef . current ) { return ; } let newLoadingMap = newLoadingMapIn ; const oldLoading = isAsync ? isLoadingImpl ( newLoadingMap || loadingMapRef . current , [ actionName , actionCtx ?. promiseId ] ) : false ; const { newState , newLoading } = runMiddlewares ( middlewaresRef . current , stateRef . current , newStateIn , oldLoading , actionName , actionType , isAsync , actionCtx ) ; if ( newState != null ) { hasChanged = true ; stateRef . current = newState ; } if ( oldLoading !== newLoading ) { newLoadingMap = buildLoadingMap ( newLoadingMap , actionName , actionCtx ?. promiseId || DEFAULT_PROMISE_ID , newLoading ) ; if ( Object . keys ( newLoadingMap ) . length === 0 ) { newLoadingMap = undefined ; } } if ( newLoadingMap != null ) { hasChanged = true ; loadingMapRef . current = newLoadingMap ; } if ( hasChanged ) { computeDerivedValues ( stateRef , propsRef , derivedStateRef , options ) ; if ( ! init ) { notifyStateListeners ( newState || stateRef . current , derivedStateRef . current . state ) ; } } else if ( propsChanged ) { const derivedChanged = computeDerivedValues ( stateRef , propsRef , derivedStateRef , options ) ; if ( derivedChanged && ! init ) { notifyStateListeners ( newState || stateRef . current , derivedStateRef . current . state ) ; } } } const keys = Object . keys ( actionsImpl ) as ( keyof A ) [ ] ; actionsRef . current = keys . reduce < A > ( ( acc , actionName ) => { const action = actionsImpl [ actionName ] ; if ( isSimpleSyncAction ( action ) ) { acc [ actionName ] = decorateSimpleSyncAction ( actionName , action , stateRef , derivedStateRef , propsRef , initializedRef , setState ) as any ; } else if ( isAsyncAction ( action ) ) { acc [ actionName ] = decorateAsyncAction ( { actionName , stateRef , derivedStateRef , propsRef , loadingMapRef , actionsRef , promisesRef , conflictActionsRef , initializedRef , options , setState , action : computeAsyncActionInput ( action ) , } ) as any ; } else if ( isSyncAction ( action ) ) { acc [ actionName ] = decorateSyncAction ( actionName , action , stateRef , derivedStateRef , propsRef , actionsRef , initializedRef , timeoutRef , options , setState ) as any ; } return acc ; } , { } as A ) ; function isLoading < K extends keyof A > ( ... props ) { return isLoadingImpl ( loadingMapRef . current , ... props ) ; } function abortAction ( actionName , promiseId = DEFAULT_PROMISE_ID ) { const abortController = promisesRef . current [ actionName ] ?. [ promiseId ] ?. abortController ; let res = false ; if ( abortController ) { abortController . abort ( ) ; res = true ; } return res ; } function isAnyLoading ( ) { if ( initializedRef . current ) { const loadingMap = loadingMapRef . current ; return ( Object . keys ( loadingMap ) . find ( ( actionName ) => { const promises = loadingMap [ actionName ] ; return Object . keys ( promises ) . find ( ( pId ) => ! ! promises [ pId ] ) ; } ) != null ) ; } return null ; } function getLoadingMap ( ) { if ( initializedRef . current ) { const map = loadingMapRef . current ; const keys = Object . keys ( map ) as ( keyof A ) [ ] ; return keys . reduce < LoadingMap < A > > ( ( acc , actionName ) => { acc [ actionName ] = Object . keys ( map [ actionName ] ) . find ( ( pId ) => ! ! map [ actionName ] [ pId ] ) != null ; return acc ; } , { } ) ; } return null ; } function getConfig ( ) { return { getInitialState , options , actions : actionsImpl , } ; } return { getConfig , setProps , addStateListener , init , destroy , isLoading , abortAction , get actions ( ) { return actionsRef . current ; } , get state ( ) { if ( initializedRef . current ) { return { ... stateRef . current , ... derivedStateRef . current . state , } ; } return null ; } , get loading ( ) { return isAnyLoading ( ) ; } , get loadingMap ( ) { return getLoadingMap ( ) ; } , get loadingParallelMap ( ) { if ( initializedRef . current ) { return loadingMapRef . current ; } return null ; } , } ; } type useStoreSlice < S , A extends DecoratedActions , P , DS , SLICE > = ( store : StoreApi < S , A , P , DS > , slicer : ( ctx : StateListenerContext < S , DS , A > ) => SLICE ) => SLICE ; param store param slicerFunc param comparator returns export function useStoreSlice < S , A extends DecoratedActions , P , DS , SLICE > ( store , slicerFunc ) ; export function useStoreSlice < S , A extends DecoratedActions , P , DS , K extends keyof StateListenerContext < S , DS , A > > ( store , properties ) ; export function useStoreSlice < S , A extends DecoratedActions , P , DS > ( store , funcOrArr ) { const [ , forceRefresh ] = useReducer ( ( s ) => ( s > 100 ? 0 : s + 1 ) , 0 ) ; const slicerFunc = useMemo ( ( ) => ( funcOrArr instanceof Function ? funcOrArr : ( ctx ) => pick ( ctx , funcOrArr ) ) , [ funcOrArr ] ) ; const sliceRef = useRef < any > ( null ) ; if ( sliceRef . current === null ) { const s = store . state ; sliceRef . current = slicerFunc ( { ... s , ... store . actions , actions : store . actions , loading : store . loading , isLoading : store . isLoading , abortAction : store . abortAction , loadingMap : store . loadingMap , } ) ; } useLayoutEffect ( ( ) => { const unregister = store . addStateListener ( ( ctx ) => { let hasChanged = false ; const slice = slicerFunc ( ctx ) ; const prevSlice = sliceRef . current ; const compare = globalConfig . comparator ; hasChanged = prevSlice == null || ! compare ( slice , prevSlice ) ; sliceRef . current = slice ; if ( hasChanged ) { forceRefresh ( ) ; } } ) ; return unregister ; } , [ ] ) ; return sliceRef . current ; } param store param props returns export function useStore < S , A extends DecoratedActions , P , DS = { } > ( store , props = null ) { const [ , forceRefresh ] = useReducer ( ( s ) => ( s > 100 ? 0 : s + 1 ) , 0 ) ; const storeRef = useRef ( store ) ; store . setProps ( props ) ; useLayoutEffect ( ( ) => storeRef . current . addStateListener ( ( ) => { forceRefresh ( ) ; } ) , [ ] ) ; useEffect ( ( ) => ( ) => store . destroy ( ) , [ ] ) ; return { state : store . state , actions : store . actions , loading : store . loading , isLoading : store . isLoading , abortAction : store . abortAction , loadingMap : store . loadingMap , loadingParallelMap : store . loadingParallelMap , } ; } param store param props export function useBindStore < S , A extends DecoratedActions , P , DS = { } > ( store , props = null ) { useRef ( store ) ; store . setProps ( props ) ; useEffect ( ( ) => { return ( ) => store . destroy ( ) ; } , [ ] ) ; return store ; } param getInitialState param actionImpl param props param options returns export function useLocalStore < S , A extends DecoratedActions , P , DS = { } > ( getInitialState , actionImpl , props ? , options ? , middlewares ? ) { const storeRef = useRef < StoreApi < S , A , P , DS > > ( ) ; if ( storeRef . current == null ) { storeRef . current = createStore ( getInitialState , actionImpl , options , middlewares ) ; } return useStore ( storeRef . current , props ) ; } export default useLocalStore ; param obj param props returns export function pick < T , K extends keyof T > ( obj , props ) { return props . reduce ( ( acc , p ) => { if ( obj . hasOwnProperty ( p ) ) { acc [ p ] = obj [ p ] ; } return acc ; } , { } as any ) ; } param props returns export function slice < T , K extends keyof T > ( ... props ) { return ( store ) => pick ( store , props ) ; }