import { Disposable } from '@typed/disposable' ; import { catchFailure , Effects , Fiber , FiberFailure , fork , Fork , get , Kill , kill , PureEffect , runWith , } from '@typed/effects' ; import { Left } from '@typed/either' ; import { http , HttpEnv , HttpOptions } from '@typed/http' ; import { DecodeError , DecodeFailure , Type } from '@typed/io' ; import { validateHttpRequest } from '@typed/io' ; import { fromJust , isNothing , map , unpack } from '@typed/maybe' ; import * as RD from '@typed/remote-data' ; import { RemoteData } from '@typed/remote-data' ; import { first } from '@typed/tuple' ; import { getHookEnv } from './getHookEnv' ; import { HookEffects } from './types' ; import { useMemo } from './useMemo' ; import { useRef } from './useRef' ; import { useRemoteData } from './useRemoteData' ; const isLoadingOrRefreshing = ( data ) : boolean => RD . isLoading ( data ) || RD . isRefreshing ( data ) ; const DEFAULT_FETCH_TYPE = 'prefer-last' ; export type UseHttp < A > = readonly [ ( ) => PureEffect < RD . RemoteData < Error , A > > , ( ) => PureEffect < Fiber < RD . RemoteData < Error , A > > > , ( ) => PureEffect < RD . RemoteData < Error , A > > ] ; export function * useJsonRequest < A , B = A > ( url , options ) { const [ getRemoteData , { clear , set , loading , success , failure } ] = yield * useRemoteData < unknown , Error , B > ( ) ; const { getFiber , addFiber , killFiber } = yield * useFiberManager < RemoteData < Error , B > > ( ) ; const makeRequest = yield * useMemo ( ( e , url , opts ) => { return ( ) => runWith ( makeHttpRequest ( ) , e ) ; function * makeHttpRequest ( ) { const currentFiber = getFiber ( ) ; const remoteData = yield * getRemoteData ( ) ; if ( isNothing ( currentFiber ) || ( yield * shouldMakeRequest ( remoteData ) ) ) { const fiber = yield * startRequest ( ) ; yield * updateAsLoadingOrRefreshing ( remoteData ) ; return fiber ; } return fromJust ( currentFiber ) ; } function * shouldMakeRequest ( remoteData ) { const fetchType = opts . fetchType || DEFAULT_FETCH_TYPE ; if ( fetchType === 'prefer-current' && isLoadingOrRefreshing ( remoteData ) ) { return false ; } yield * killFiber ( ) ; return true ; } function * startRequest ( ) { const fetchType = opts . fetchType || DEFAULT_FETCH_TYPE ; function * myEffect ( ) { const effect = validateHttpRequest ( options . type , http ( url , opts ) ) ; const response = yield * catchFailure ( effect , DecodeFailure , ( e ) => Left . of ( new Error ( toErrorMessage ( e ) ) ) ) ; const data = RD . RemoteData . fromEither ( response ) ; yield * set ( data ) ; return data ; } const fiber = yield * fork ( myEffect ( ) ) ; if ( fetchType === 'prefer-last' ) { addFiber ( fiber ) ; } return fiber ; } function * updateAsLoadingOrRefreshing ( remoteData ) { if ( RD . isLoading ( remoteData ) ) { return ; } if ( RD . hasNoData ( remoteData ) ) { yield * loading ( ) ; } else if ( RD . isFailure ( remoteData ) || RD . isRefreshingFailure ( remoteData ) ) { yield * failure ( remoteData . value , true ) ; } else { yield * success ( remoteData . value , true ) ; } } } , [ yield * get ( ) , url , options ] as const ) ; return [ getRemoteData , makeRequest , clear ] as const ; } export type UseHttpOptions < A , B > = HttpOptions & { readonly type : Type < A , B > ; readonly fetchType ? : 'prefer-current' | 'prefer-last' ; } ; function * useFiberManager < A > ( ) { const env = yield * getHookEnv ( ) ; const [ ref , setRef ] = yield * useRef < unknown , readonly [ Fiber < A > , Disposable ] > ( ) ; const addFiber = yield * useMemo ( ( ) => ( fiber ) => setRef ( [ fiber , env . addDisposable ( fiber ) ] ) , [ ] ) ; const killFiber = yield * useMemo ( ( ) => function * ( ) { if ( isNothing ( ref . current ) ) { return ; } const [ fiber , disposable ] = fromJust ( ref . current ) ; setRef ( null ) ; fiber . dispose ( ) ; disposable . dispose ( ) ; return yield * kill ( fiber ) ; } , [ ] ) ; const getFiber = yield * useMemo ( ( r ) => ( ) => map ( first , r . current ) , [ ref ] ) ; return { getFiber , addFiber , killFiber } as const ; } function toErrorMessage ( { key , expected , actual } ) { return unpack ( ( k ) => `${ k } :: Expected ${ expected }, but received ${ actual }` , ( ) => `Expected ${ expected }, but received ${ actual }` , key ) ; }