import { useRef , useMemo , useCallback , useContext , createContext , ReactNode } from 'react' ; import { useSyncExternalStore } from 'use-sync-external-store/shim' ; import { Async , ResourceController , CacheController , AwaitableCacheController , LooseApi , ConstantAsync , ResourceState , } from './interface.js' ; import { ObservableCache } from './ObservableCache.js' ; import SuspenseError from './SuspenseError.js' ; import { stringifyKey } from './utils.js' ; interface ContextValue { cache : ObservableCache ; } const createCacheContextValue = ( ) => { const cache = new ObservableCache ( new WeakMap ( ) ) ; return { cache } ; } ; const Context = createContext < ContextValue > ( createCacheContextValue ( ) ) ; Context . displayName = 'BoundaryCacheContext' ; interface Props { children : ReactNode ; } export default function CacheProvider ( { children } ) { const ref = useRef ( createCacheContextValue ( ) ) ; return < Context . Provider value = { ref . current } > { children } < /Context.Provider>; } interface FetchToCacheOptions { computedKey ? : string ; workingType : 'init' | 'set' | 'refresh' | 'none' ; } const getWorkInProgress = ( state ) : Promise < unknown > | null => { switch ( state ?. kind ) { case 'pending' : case 'hasValuePending' : return state . promise ; default : return null ; } } ; const fetchToCache = ( cache , api , params , options ) => { const { computedKey , workingType } = options ; const key = computedKey ?? stringifyKey ( params ) ; const current = cache . get ( api , key ) ; const workingInProgress = getWorkInProgress ( current ) ; if ( workingInProgress ) { return workingInProgress ; } const promise = api ( params ) . then ( ( data ) => cache . set ( api , key , { data , kind : 'hasValue' } ) , ( error ) => cache . set ( api , key , { error , kind : 'hasError' } ) ) ; switch ( workingType ) { case 'init' : cache . init ( api , key , promise ) ; break ; case 'set' : cache . set ( api , key , { promise , kind : 'pending' } ) ; break ; case 'refresh' : { if ( current && ( current . kind === 'hasValue' || current . kind === 'hasValuePending' ) ) { cache . set ( api , key , { promise , data : current . data , kind : 'hasValuePending' } ) ; } } } return promise ; } ; export function useExpireCache ( ) { const { cache } = useContext ( Context ) ; const expire = useCallback ( ( api , params ) => { fetchToCache ( cache , api , params , { workingType : 'set' } ) ; } , [ cache ] ) ; return expire as CacheController ; } export function useRefreshCache ( ) { const { cache } = useContext ( Context ) ; const refresh = useCallback ( async ( api , params ) => { await fetchToCache ( cache , api , params , { workingType : 'refresh' } ) ; } , [ cache ] ) ; return refresh as AwaitableCacheController ; } function useResourceInternal < O > ( api , params ? ) { const { cache } = useContext ( Context ) ; const key = useMemo ( ( ) => stringifyKey ( params ) , [ params ] ) ; const state = useSyncExternalStore ( callback => cache . observe ( api , key , callback ) , ( ) => cache . get ( api , key ) ) ; const expireCache = useExpireCache ( ) ; const refreshCache = useRefreshCache ( ) ; const expire = useCallback ( ( ) => expireCache ( api , params ) , [ api , expireCache , params ] ) ; const refresh = useCallback ( ( ) => refreshCache ( api , params ) , [ api , refreshCache , params ] ) ; if ( ! state ) { const promise = fetchToCache ( cache , api , params , { computedKey : key , workingType : 'init' } ) ; throw promise ; } switch ( state . kind ) { case 'pending' : throw state . promise ; case 'hasValue' : return [ state . data as O , { expire , refresh , pending : false } ] ; case 'hasValuePending' : return [ state . data as O , { expire , refresh , pending : true } ] ; case 'hasError' : throw new SuspenseError ( api , params , state . error ) ; default : throw new Error ( `Unexpected state ${ ( state as any ) . kind } in resource.` ) ; } } export function useResource < I , O > ( api , params ) { return useResourceInternal ( api , params ) ; } export function useConstantResource < O > ( api ) { return useResourceInternal ( api ) ; } export function usePreloadResource < I , O > ( api , params ) { const { cache } = useContext ( Context ) ; const key = stringifyKey ( params ) ; if ( ! cache . has ( api , key ) ) { fetchToCache ( cache , api , params , { computedKey : key , workingType : 'init' } ) ; } } export function usePreloadConstantResource < O > ( api ) { const { cache } = useContext ( Context ) ; const key = stringifyKey ( undefined ) ; if ( ! cache . has ( api , key ) ) { fetchToCache ( cache , api , undefined , { computedKey : key , workingType : 'init' } ) ; } } export function usePreloadResourceCallback ( ) { const { cache } = useContext ( Context ) ; const preload = useCallback ( ( api , params ? ) => { const key = stringifyKey ( params ) ; if ( ! cache . has ( api , key ) ) { fetchToCache ( cache , api , params , { computedKey : key , workingType : 'init' } ) ; } } , [ cache ] ) ; return preload ; }