import type { GQtyClient , GQtyError , Selection } from 'gqty' ; import type { SchedulerPromiseValue } from 'gqty/Scheduler' ; import * as React from 'react' ; import { BuildSelections , isAnySelectionIncluded , isAnySelectionIncludedInMatrix , isSelectionIncluded , useBuildSelections , useIsomorphicLayoutEffect , } from '../common' ; import { areArraysEqual } from '../utils' ; export interface UseMetaStateOptions < T > { onStartFetching ? : ( ) => void ; onDoneFetching ? : ( ) => void ; onError ? : ( data : { newError : GQtyError ; selections : Selection [ ] ; isLastTry : boolean ; } ) => void ; onRetry ? : ( data : { retryPromise : Promise < SchedulerPromiseValue > ; selections : Set < Selection > ; } ) => void ; filterSelections ? : BuildSelections < T > ; } export interface MetaState { isFetching : boolean ; errors ? : GQtyError [ ] ; } export interface UseMetaState { < T > ( opts ? ) : MetaState ; } export function createUseMetaState ( client ) { const scheduler = client . scheduler ; const { accessorCache : { getProxySelection } , } = client ; const errorsMap = scheduler . errors . map ; const defaultEmptyOpts = { } ; const useMetaState = function useMetaState ( opts = defaultEmptyOpts ) { const { hasSpecifiedSelections : hasFilterSelections , selections : selectionsToFilter , } = useBuildSelections ( opts . filterSelections , getProxySelection , useMetaState ) ; const [ promisesInFly ] = React . useState ( ( ) => { return new Set < Promise < unknown > > ( ) ; } ) ; const isMountedRef = React . useRef ( true ) ; React . useEffect ( ( ) => { return ( ) => { isMountedRef . current = false ; } ; } , [ ] ) ; const getState = React . useCallback ( ( isMounted = isMountedRef ) : MetaState => { let isFetching ; if ( scheduler . pendingSelectionsGroups . size ) { if ( hasFilterSelections ) { isFetching = isAnySelectionIncludedInMatrix ( selectionsToFilter , scheduler . pendingSelectionsGroups ) ; } else { isFetching = true ; } if ( isFetching && scheduler . pendingSelectionsGroupsPromises . size ) { Promise . all ( scheduler . pendingSelectionsGroupsPromises . values ( ) ) . finally ( ( ) => setStateIfChanged ( isMounted ) ) ; } } else { isFetching = false ; } let errors ; if ( hasFilterSelections ) { const errorsSet = new Set < GQtyError > ( ) ; selectionsToFilter . forEach ( ( selection ) => { const error = errorsMap . get ( selection ) ; if ( error ) errorsSet . add ( error ) ; } ) ; if ( errorsSet . size ) errors = Array . from ( errorsSet ) ; } else if ( errorsMap . size ) { errors = Array . from ( new Set ( errorsMap . values ( ) ) ) ; } return errors ? { isFetching , errors } : { isFetching } ; } , [ hasFilterSelections , selectionsToFilter ] ) ; const setStateIfChanged = React . useCallback ( function setStateIfChanged ( isMounted ) { if ( ! isMounted . current ) return ; const prevState = stateRef . current ; const newState = getState ( isMounted ) ; if ( prevState . isFetching !== newState . isFetching || ! areArraysEqual ( prevState . errors , newState . errors ) ) { stateRef . current = newState ; setTimeout ( ( ) => { if ( isMounted . current ) setState ( newState ) ; } , 0 ) ; } } , [ ] ) ; const [ state , setState ] = React . useState ( getState ) ; const stateRef = React . useRef ( state ) ; stateRef . current = state ; const optsRef = React . useRef ( opts ) ; optsRef . current = opts ; useIsomorphicLayoutEffect ( ( ) => { const isMounted = { current : true } ; const unsubscribeIsFetching = scheduler . subscribeResolve ( ( promise , selection ) => { if ( promisesInFly . has ( promise ) ) return ; if ( hasFilterSelections && ! isSelectionIncluded ( selection , selectionsToFilter ) ) { return ; } if ( promisesInFly . size === 0 ) optsRef . current . onStartFetching ?. ( ) ; promisesInFly . add ( promise ) ; setStateIfChanged ( isMounted ) ; promise . then ( ( ) => { promisesInFly . delete ( promise ) ; if ( promisesInFly . size === 0 ) optsRef . current . onDoneFetching ?. ( ) ; setStateIfChanged ( isMounted ) ; } ) ; } ) ; const unsubscribeErrors = scheduler . errors . subscribeErrors ( ( data ) => { switch ( data . type ) { case 'new_error' : { if ( hasFilterSelections ) { if ( isAnySelectionIncluded ( selectionsToFilter , data . selections ) ) optsRef . current . onError ?. ( { newError : data . newError , selections : data . selections , isLastTry : data . isLastTry , } ) ; else return ; } else { optsRef . current . onError ?. ( { newError : data . newError , selections : data . selections , isLastTry : data . isLastTry , } ) ; } break ; } case 'retry' : { if ( hasFilterSelections ) { if ( isAnySelectionIncluded ( selectionsToFilter , data . selections ) ) { optsRef . current . onRetry ?. ( { retryPromise : data . retryPromise , selections : data . selections , } ) ; data . retryPromise . finally ( ( ) => { setTimeout ( ( ) => { setStateIfChanged ( isMounted ) ; } , 0 ) ; } ) ; } } else { optsRef . current . onRetry ?. ( { retryPromise : data . retryPromise , selections : data . selections , } ) ; data . retryPromise . finally ( ( ) => { setTimeout ( ( ) => { setStateIfChanged ( isMounted ) ; } , 0 ) ; } ) ; } break ; } case 'errors_clean' : { } } setStateIfChanged ( isMounted ) ; } ) ; return ( ) => { isMounted . current = false ; unsubscribeIsFetching ( ) ; unsubscribeErrors ( ) ; } ; } , [ getState , hasFilterSelections , setState , optsRef , selectionsToFilter ] ) ; return state ; } ; return useMetaState ; }