import { useMemo } from 'react' ; import * as jsonStableStringify from 'json-stable-stringify' ; import { useSubscription } from 'use-subscription' ; import { deprecate } from '../util' ; import { ResultFunc , ResultFuncPure , Strategy , RegionOption , Listener , Props , } from '../types' ; const increase = ( v = 0 ) => ( v + 1 ) ; const decrease = ( v = 0 ) => ( v - 1 > 0 ? v - 1 : 0 ) ; const formatLoading = ( loading ? ) => { if ( loading === undefined ) { return true ; } return loading > 0 ; } ; const formatError = ( error ? ) : Error => { return typeof error === 'string' ? new Error ( error ) : ( error as Error ) ; } ; const getSetResult = < V > ( resultOrFunc , snapshot ) => { if ( typeof resultOrFunc === 'function' ) { return ( resultOrFunc as ResultFuncPure < V > ) ( snapshot ) ; } return resultOrFunc ; } ; interface LoadBy < K , V , Extend > { ( key , asyncFunction ) : ( ) => Promise < void > ; < TResult = unknown > ( key , asyncFunction , reducer ) : ( ) => Promise < void > ; < TParams = void > ( key , asyncFunction ) : ( params : TParams ) => Promise < void > ; < TParams = void , TResult = unknown > ( key , asyncFunction , reducer ) : ( params : TParams ) => Promise < void > ; } export interface CreateMappedRegionReturnValue < K , V > { private_getState_just_for_test : ( ) => any ; private_setState_just_for_test : ( value : any ) => void ; set : ( key : K , resultOrFunc : V | ResultFunc < V > ) => void ; reset : ( key : K ) => void ; resetAll : ( ) => void ; load : ( key : K , promise : Promise < V > ) => Promise < void > ; loadBy : LoadBy < K , V , undefined > ; getValue : ( key : K ) => V | undefined ; getLoading : ( key : K ) => boolean ; getError : ( key : K ) => Error | undefined ; getPromise : ( key : K ) => Promise < V > | undefined ; useValue : { ( key ) : V | undefined ; < TResult > ( key , selector ) : TResult ; } ; useLoading : ( key : K ) => boolean ; useError : ( key : K ) => Error | undefined ; useData : { ( key , fetcher ) : V ; < TResult > ( key , fetcher , selector ) : TResult ; } ; } export interface CreateMappedRegionPureReturnValue < K , V > extends Omit < CreateMappedRegionReturnValue < K , V > , 'set' | 'loadBy' | 'getValue' | 'useValue' > { set : ( key : K , resultOrFunc : V | ResultFuncPure < V > ) => void ; loadBy : LoadBy < K , V , never > ; getValue : ( key : K ) => V ; useValue : { ( key ) : V ; < TResult > ( key , selector ) : TResult ; } ; } function createMappedRegion < K , V > ( initialValue , option ? ) ; function createMappedRegion < K , V > ( initialValue , option ? ) ; function createMappedRegion < K , V > ( initialValue , option ? ) { type Result = CreateMappedRegionPureReturnValue < K , V > ; const strategy = option ?. strategy ?? 'acceptLatest' ; interface PrivateStoreState { [ key ] : Props < V > ; } interface PrivateStoreStateRef { current : PrivateStoreState ; } const private_stateRef = { current : { } , } ; const private_store_ensure = ( key ) : void => { if ( ! private_stateRef . current [ key ] ) { private_stateRef . current [ key ] = { } ; } } ; const private_store_emit = ( key ) : void => { const props = private_stateRef . current [ key ] ; if ( ! props ) { return ; } const { listeners } = props ; if ( ! listeners ) { return ; } listeners . forEach ( listener => listener ( ) ) ; } ; const private_store_getState = ( ) : PrivateStoreState => { return private_stateRef . current ; } ; const private_store_setState = ( value ) : void => { private_stateRef . current = value ; } ; const private_store_getAttribute = < A extends keyof Props < V > > ( key , attribute ) : Props < V > [ A ] => { const props = private_stateRef . current [ key ] ; if ( ! props ) { return undefined ; } return props [ attribute ] ; } ; const private_store_load = ( key , promise ) : void => { private_store_ensure ( key ) ; const props = private_stateRef . current [ key ] ; props . promise = promise ; props . pendingMutex = increase ( props . pendingMutex ) ; private_store_emit ( key ) ; } ; const private_store_loadEnd = ( key ) : void => { private_store_ensure ( key ) ; const props = private_stateRef . current [ key ] ; props . pendingMutex = decrease ( props . pendingMutex ) ; private_store_emit ( key ) ; } ; const private_store_set = ( key , value ) : void => { private_store_ensure ( key ) ; const props = private_stateRef . current [ key ] ; const snapshot = props . value ; const formatValue = typeof value === 'function' ? value ( snapshot ) : value ; props . pendingMutex = decrease ( props . pendingMutex ) ; props . value = formatValue ; props . error = undefined ; private_store_emit ( key ) ; } ; const private_store_setError = ( key , error ) : void => { private_store_ensure ( key ) ; const props = private_stateRef . current [ key ] ; props . pendingMutex = decrease ( props . pendingMutex ) ; props . error = error ; if ( error ) { console . error ( error ) ; } private_store_emit ( key ) ; } ; const private_store_reset = ( key ) : void => { private_stateRef . current [ key ] = { listeners : private_stateRef . current [ key ] . listeners } ; private_store_emit ( key ) ; } ; const private_store_resetAll = ( ) : void => { Object . keys ( private_stateRef . current ) . forEach ( ( key ) => { private_stateRef . current [ key ] = { listeners : private_stateRef . current [ key ] . listeners } ; private_store_emit ( key ) ; } ) ; } ; const private_store_subscribe = ( key , listener ) : ( ) => void => { private_store_ensure ( key ) ; const props = private_stateRef . current [ key ] ; if ( ! props . listeners ) { props . listeners = [ ] ; } if ( typeof listener === 'function' ) { props . listeners . push ( listener ) ; } else { console . warn ( `listener should be function, but received ${ listener }` ) ; } return ( ) => { private_store_ensure ( key ) ; const props = private_stateRef . current [ key ] ; if ( ! props . listeners ) { props . listeners = [ ] ; } props . listeners . splice ( props . listeners . indexOf ( listener ) , 1 ) ; } ; } ; const getKeyString = ( key ) : string => { if ( typeof key === 'string' ) { return key ; } return jsonStableStringify ( key ) ; } ; const getValueOrInitialValue = ( value ) : V => { if ( value !== undefined ) { return value ; } return initialValue as V ; } ; const set = ( key , resultOrFunc ) => { const keyString = getKeyString ( key ) ; const maybeSnapshot = private_store_getAttribute ( keyString , 'value' ) ; const snapshot = getValueOrInitialValue ( maybeSnapshot ) ; const result = getSetResult ( resultOrFunc , snapshot ) ; private_store_set ( keyString , result ) ; return result ; } ; const reset = ( key ) => { if ( key === undefined ) { deprecate ( 'reset should be called with key, use resetAll to reset all keys.' ) ; private_store_resetAll ( ) ; } const keyString = getKeyString ( key ) ; private_store_reset ( keyString ) ; } ; const resetAll = private_store_resetAll ; const toPromise = async < TParams , TResult > ( asyncFunction , reducer ? , params ? , getReducerState ) : Promise < V > => { const promise = typeof asyncFunction === 'function' ? asyncFunction ( params as TParams ) : asyncFunction ; if ( typeof reducer === 'function' ) { const result = await promise ; return reducer ( getReducerState ( ) , result , params as TParams ) ; } return promise as unknown as Promise < V > ; } ; const loadBy = < TParams = void , TResult = unknown > ( key , asyncFunction , reducer ? ) => { const loadByReturnFunction = async ( params ? ) => { const loadKey = typeof key === 'function' ? ( key as any ) ( params ) : key ; const keyString = getKeyString ( loadKey ) ; const promise = toPromise ( asyncFunction , reducer , params , ( ) => getValueOrInitialValue ( private_store_getAttribute ( keyString , 'value' ) ) ) ; private_store_load ( keyString , promise ) ; /**
         * note
         * 1. always get value after await, so it is the current one
         * 2. ensure if initialValue is gaven, every branch should return initialValueOfKey as T[K]
         */ try { const result = await promise ; const currentPromise = private_store_getAttribute ( keyString , 'promise' ) ; const snapshot = private_store_getAttribute ( keyString , 'value' ) ; if ( strategy === 'acceptLatest' && promise !== currentPromise ) { private_store_loadEnd ( keyString ) ; return getValueOrInitialValue ( snapshot ) as never ; } private_store_set ( keyString , result ) ; return getValueOrInitialValue ( result ) as never ; } catch ( error ) { const currentPromise = private_store_getAttribute ( keyString , 'promise' ) ; const snapshot = private_store_getAttribute ( keyString , 'value' ) ; if ( strategy === 'acceptLatest' && promise !== currentPromise ) { private_store_loadEnd ( keyString ) ; return getValueOrInitialValue ( snapshot ) as never ; } private_store_setError ( keyString , error ) ; return getValueOrInitialValue ( snapshot ) as never ; } } ; return loadByReturnFunction ; } ; const load = async ( key , promise ) => { if ( promise instanceof Promise || typeof promise === 'function' ) { return loadBy ( key , promise as unknown as any ) ( ) ; } console . warn ( 'set result directly' ) ; return set ( key , promise ) ; } ; const getValue = key => { const keyString = getKeyString ( key ) ; const value = private_store_getAttribute ( keyString , 'value' ) ; return getValueOrInitialValue ( value ) ; } ; const getLoading = key => { const keyString = getKeyString ( key ) ; return formatLoading ( private_store_getAttribute ( keyString , 'pendingMutex' ) ) ; } ; const getError = key => { const keyString = getKeyString ( key ) ; return formatError ( private_store_getAttribute ( keyString , 'error' ) ) ; } ; const getPromise = key => { const keyString = getKeyString ( key ) ; const promise = private_store_getAttribute ( keyString , 'promise' ) ; return promise ; } ; const createHooks = < TReturnType > ( getFn ) => { return ( key ) : TReturnType => { const subscription = useMemo ( ( ) => ( { getCurrentValue : ( ) => getFn ( key ) , subscribe : ( listener ) => private_store_subscribe ( getKeyString ( key ) , listener ) , } ) , [ getFn , getKeyString ( key ) ] ) ; return useSubscription ( subscription ) ; } ; } ; const useValue = < TResult > ( key , selector ? ) => { const getFn = getValue ; const subscription = useMemo ( ( ) => ( { getCurrentValue : ( ) => ( selector ? selector ( getFn ( key ) ) : getFn ( key ) ) , subscribe : ( listener ) => private_store_subscribe ( getKeyString ( key ) , listener ) , } ) , [ getFn , selector , getKeyString ( key ) ] ) ; return useSubscription ( subscription ) ; } ; const useData = < TResult > ( key , fetcher , selector ? ) => { const getFn = getValue ; const subscription = useMemo ( ( ) => ( { getCurrentValue : ( ) => ( selector ? selector ( getFn ( key ) ) : getFn ( key ) ) , subscribe : ( listener ) => private_store_subscribe ( getKeyString ( key ) , listener ) , } ) , [ getFn , selector , getKeyString ( key ) ] ) ; const currentPromise = useMemo ( ( ) => getPromise ( key ) , [ getPromise , getKeyString ( key ) ] ) ; if ( subscription . getCurrentValue ( ) === undefined ) { if ( currentPromise ) { throw currentPromise ; } else { throw fetcher ( ) ; } } return useSubscription ( subscription ) ; } ; const useLoading = createHooks ( getLoading ) ; const useError = createHooks ( getError ) ; return { private_getState_just_for_test : private_store_getState , private_setState_just_for_test : private_store_setState , set , reset , resetAll , load , loadBy , getValue , getLoading , getError , getPromise , useValue , useLoading , useError , useData , } ; } export default createMappedRegion ;