import { useState , useEffect , useRef } from 'react' ; import { useStore , Storage } from 'stook' ; import gql from 'graphql-tag' ; import compose from 'koa-compose' ; import { produce } from 'immer' ; import { GraphQLClient } from '@peajs/graphql-client' ; import { SubscriptionClient } from 'subscriptions-transport-ws' ; import isEqual from 'react-fast-compare' ; import { fetcher } from './fetcher' ; import { Options , RefetchOptions , QueryResult , FetcherItem , Middleware , GraphqlOptions , MutateResult , Variables , SubscribeResult , SubscriptionOption , FromSubscriptionOption , Observer , Start , } from './types' ; import { getDeps , getVariables , getDepsMaps , useUnmounted , useUnmount , isResolve , getOperationName , } from './utils' ; interface VarCurrent { value : any ; resolve : boolean ; } interface DepsCurrent { value : any ; resolve : boolean ; } const NULL_AS = null ; export class Context { headers : Record < string , string > = { } ; body : any = undefined ; valid : boolean = true ; } export class Client { graphqlOptions : GraphqlOptions ; middleware : Middleware [ ] = [ ] ; graphqlClient : GraphQLClient = NULL_AS ; subscriptionClient : SubscriptionClient = NULL_AS ; constructor ( opt = { endpoint : '/graphql' , headers : { } } ) { const { endpoint , headers , subscriptionsEndpoint } = opt ; this . graphqlOptions = opt ; this . graphqlClient = new GraphQLClient ( endpoint , { headers } as any ) ; if ( subscriptionsEndpoint ) { this . subscriptionClient = new SubscriptionClient ( subscriptionsEndpoint , { reconnect : true } ) ; } } config = ( opt = { endpoint : '/graphql' , headers : { } } ) => { this . graphqlOptions = { ... this . graphqlOptions , ... opt } ; const { endpoint , headers , subscriptionsEndpoint } = opt ; this . graphqlClient = new GraphQLClient ( endpoint , { headers } as any ) ; if ( subscriptionsEndpoint ) { this . subscriptionClient = new SubscriptionClient ( subscriptionsEndpoint , { reconnect : true } ) ; } } ; applyMiddleware = ( fn ) => { this . middleware . push ( fn ) ; } ; query = async < T = any > ( input , options = { } ) : Promise < T > => { const context = new Context ( ) ; const { variables = { } , endpoint } = options ; const opt = { } ; if ( options . headers ) { opt . headers = options . headers ; } const action = async ( ctx ) => { const queryOpt = { ... opt , headers : { ... ( opt . headers || { } ) , ... ( ctx . headers || { } ) } } ; if ( endpoint ) queryOpt . endpoint = endpoint ; try { ctx . body = await this . graphqlClient . query < T > ( input , variables , queryOpt ) ; } catch ( error ) { ctx . body = error ; ctx . valid = false ; } } ; await compose ( [ ... this . middleware ] ) ( context ) ; await compose ( [ ... this . middleware , action ] ) ( context ) ; if ( ! context . valid ) { throw context . body ; } return context . body as T ; } ; useQuery = < T = any , V = Variables > ( input , options = { } ) => { const isUnmouted = useUnmounted ( ) ; const { initialData : data , onUpdate , lazy = false , pollInterval } = options ; const fetcherName = options . key || input ; const initialState = { loading : true , data } as QueryResult < T > ; const deps = getDeps ( options ) ; const [ result , setState ] = useStore ( fetcherName , initialState ) ; const [ shouldStart , setShouldStart ] = useState ( ! lazy ) ; const update = ( nextState ) => { setState ( nextState ) ; onUpdate && onUpdate ( nextState ) ; } ; const makeFetch = async ( opt = { } ) : Promise < any > => { const key = opt . key ?? fetcherName ; try { if ( fetcher . get ( key ) ) fetcher . get ( key ) . called = true ; const resData = await this . query < T > ( input , opt || { } ) ; const nextState = produce ( result , ( draft ) => { draft . loading = false ; if ( opt . setData && typeof opt . setData === 'function' ) { opt . setData ( draft . data , resData ) ; } else { draft . data = resData ; } } ) ; update ( nextState ) ; return resData ; } catch ( error ) { update ( { loading : false , error } as QueryResult < T > ) ; } } ; param opt return const refetch = async ( opt = { } ) : Promise < T > => { const key = opt . key ?? fetcherName ; let showLoading = true ; if ( typeof opt . showLoading === 'boolean' && opt . showLoading === false ) { showLoading = false ; } if ( showLoading ) { update ( { loading : true } as QueryResult < T > ) ; } function getRefechVariables ( opt = { } ) { const fetcherVariables = fetcher . get ( key ) . variables ; const variables = opt . variables ; if ( ! variables ) { return fetcherVariables || { } ; } if ( typeof variables === 'function' ) { return variables ( fetcherVariables ) ; } return opt . variables ; } opt . variables = getRefechVariables ( opt ) ; fetcher . get ( key ) . variables = opt . variables ; const data = ( await makeFetch ( opt ) ) as any ; return data ; } ; const start = ( ) : any => { setShouldStart ( true ) ; } ; /**
         * handle variable
         */ const variables = getVariables ( options ) ; const varRef = useRef < VarCurrent > ( { value : getVariables ( options ) , resolve : isResolve ( options . variables ) } ) ; if ( ! varRef . current . resolve && getVariables ( options ) ) { varRef . current = { value : variables , resolve : true } ; } const timerRef = useRef < any > ( ) ; useEffect ( ( ) => { if ( ! fetcher . get ( fetcherName ) ) { fetcher . set ( fetcherName , { refetch , called : false } as FetcherItem < T > ) ; } const shouldFetch = varRef . current . resolve && ! fetcher . get ( fetcherName ) . called && ! isUnmouted ( ) && shouldStart ; if ( shouldFetch ) { fetcher . get ( fetcherName ) . variables = varRef . current . value ; makeFetch ( { ... options , variables : varRef . current . value } ) ; if ( pollInterval && ! fetcher . get ( fetcherName ) . polled ) { fetcher . get ( fetcherName ) . polled = true ; /** pollInterval */ timerRef . current = setInterval ( ( ) => { makeFetch ( { ... options , variables : varRef . current . value } ) ; } , pollInterval ) ; } } return ( ) => { if ( timerRef . current ) { clearInterval ( timerRef . current ) ; fetcher . get ( fetcherName ) . polled = false ; } } ; } , [ varRef . current , shouldStart ] ) ; /**
         * handle deps
         */ const depsMaps = getDepsMaps ( deps ) ; const depsRef = useRef < DepsCurrent > ( { value : depsMaps , resolve : false } ) ; if ( ! isEqual ( depsRef . current . value , depsMaps ) ) { depsRef . current = { value : depsMaps , resolve : true } ; } useEffect ( ( ) => { if ( depsRef . current . resolve ) { update ( { loading : true } as QueryResult < T > ) ; makeFetch ( { ... options , variables : varRef . current . value } ) ; } } , [ depsRef . current ] ) ; /** pollInterval */ useEffect ( ( ) => { if ( pollInterval && ! fetcher . get ( fetcherName ) . polled ) { fetcher . get ( fetcherName ) . polled = true ; const timer = setInterval ( ( ) => { makeFetch ( { ... options , variables : varRef . current . value } ) ; } , pollInterval ) ; return ( ) => { clearInterval ( timer ) ; } ; } return ( ) => { } ; } , [ ] ) ; useUnmount ( ( ) => { const store = Storage . get ( fetcherName ) ; if ( store && store . setters . length === 0 ) { fetcher . get ( fetcherName ) . called = false ; update ( { loading : true } as any ) ; } } ) ; const called = fetcher . get ( fetcherName ) && fetcher . get ( fetcherName ) . called ; return { ... result , refetch , start , called } ; } ; useMutation = < T = any , V = Variables > ( input , options = { } ) => { const { initialData : data , onUpdate } = options ; const initialState = { data , called : false } as MutateResult < T > ; const fetcherName = options . key || input ; const [ result , setState ] = useStore < MutateResult < T > > ( fetcherName , initialState ) ; const update = ( nextState ) => { setState ( nextState ) ; onUpdate && onUpdate ( nextState ) ; } ; const makeFetch = async ( opt = { } ) : Promise < any > => { try { const data = await this . query < T > ( input , { ... options , ... opt } ) ; update ( { loading : false , called : true , data } as MutateResult < T > ) ; return data ; } catch ( error ) { update ( { loading : false , called : true , error } as MutateResult < T > ) ; } } ; const mutate = async ( variables , opt = { } ) : Promise < T > => { update ( { loading : true } as MutateResult < T > ) ; return ( await makeFetch ( { ... opt , variables } ) ) as T ; } ; return [ mutate , result ] as [ ( variables : V , opt ? : Options ) => Promise < T > , MutateResult < T > ] ; } ; useSubscription = < T = any > ( input , options = { } ) => { const { variables = { } , operationName = '' , initialQuery = '' , onUpdate } = options ; const unmounted = useRef ( false ) ; const initialState = { loading : true } as SubscribeResult < T > ; const [ result , setState ] = useState ( initialState ) ; const context = new Context ( ) ; let unsubscribe = ( ) => { } ; function update ( nextState ) { setState ( nextState ) ; onUpdate && onUpdate ( nextState ) ; } function updateInitialQuery ( nextState ) { setState ( nextState ) ; if ( options . initialQuery && options . initialQuery . onUpdate ) { options . initialQuery . onUpdate ( nextState ) ; } } const initQuery = async ( ) => { if ( ! initialQuery ) return ; if ( unmounted . current ) return ; try { let data = await this . query < T > ( initialQuery . query , { variables : initialQuery . variables || { } } ) ; updateInitialQuery ( { loading : false , data } as SubscribeResult < T > ) ; return data ; } catch ( error ) { updateInitialQuery ( { loading : false , error } as SubscribeResult < T > ) ; return error ; } } ; const initSubscribe = async ( ) => { if ( unmounted . current ) return ; const node = gql `
        ${ input }
      ` ; const subResult = this . subscriptionClient . request ( { query : node , variables , operationName : getOperationName ( input ) || operationName } ) . subscribe ( { next : ( { data } ) => { const action = async ( ctx ) => { ctx . body = data ; } ; compose ( [ ... this . middleware , action ] ) ( context ) . then ( ( ) => { update ( { loading : false , data : context . body } as SubscribeResult < T > ) ; } ) ; } , error : ( error ) => { const action = async ( ctx ) => { ctx . body = error ; ctx . valid = false ; } ; compose ( [ ... this . middleware , action ] ) ( context ) . then ( ( ) => { update ( { loading : false , error : context . body } as SubscribeResult < T > ) ; } ) ; } , complete ( ) { console . log ( 'completed' ) ; } } ) ; unsubscribe = subResult . unsubscribe ; } ; useEffect ( ( ) => { if ( initialQuery ) initQuery ( ) ; initSubscribe ( ) ; return ( ) => { unmounted . current = true ; } ; } , [ ] ) ; return { ... result , unsubscribe } ; } ; fromSubscription = < T = any > ( input , options = { } ) => { const { variables = { } } = options ; if ( ! this . subscriptionClient ) { throw new Error ( 'require subscriptionsEndpoint config' ) ; } return { subscribe : ( observer ) => { const ob = { } as Observer < T > ; if ( observer . next ) { ob . next = ( data ) => { if ( observer . next ) observer . next ( data ) ; } ; } if ( observer . error ) ob . error = observer . error ; if ( observer . error ) ob . complete = observer . complete ; return this . subscriptionClient . request ( { query : gql `
              ${ input }
            ` , variables } ) . subscribe ( ob as any ) ; } } ; } ; }