import { AsyncStorage } from 'react-native' ; import CachePolicy from 'http-cache-semantics' ; import fromPairs from 'lodash/fromPairs' ; const ROOT = 'fp' ; const debug = false ; async function serializeResponse ( r ) { let { headers , status , statusText } = r ; let body = await r . clone ( ) . text ( ) ; if ( 'entries' in headers ) { headers = [ ... headers . entries ( ) ] ; } return { headers , status , statusText , body } ; } function headersInstanceToObject ( headers ) { return fromPairs ( [ ... Object . entries ( headers ) ] ) ; } function responseForCachePolicy ( { url , method , headers } ) { return { url , method , headers : headersInstanceToObject ( headers ) } ; } function requestForCachePolicy ( { status , headers } ) { return { status , headers : headersInstanceToObject ( headers ) } ; } export async function insertForUrl ( url , data ) { let key = `urlcache:${ url }` ; let { policy : oldPolicy } = await getItem ( key ) ; if ( oldPolicy ) { return ; } let req = new Request ( url ) ; let resp = new Response ( JSON . stringify ( data ) , { status : 200 } ) ; let policy = new CachePolicy ( requestForCachePolicy ( req ) , responseForCachePolicy ( resp ) ) ; return cacheItem ( { key , response : resp , policy , bundled : true } ) ; } type CacheItemArgs = { key : string ; response : Response ; policy : CachePolicy ; bundled ? : boolean ; } ; async function cacheItem ( { key , response , policy , bundled } ) { response = await serializeResponse ( response ) ; let strResponse = JSON . stringify ( response ) ; await AsyncStorage . multiSet ( [ [ `${ ROOT }:${ key }:response` , strResponse ] , [ `${ ROOT }:${ key }:policy` , JSON . stringify ( policy . toObject ( ) ) ] , [ `${ ROOT }:${ key }:ttl` , JSON . stringify ( policy . timeToLive ( ) ) ] , ] ) ; if ( bundled ) { await AsyncStorage . setItem ( `${ ROOT }:${ key }:bundled` , strResponse ) ; } } type GetItemResult = { response : Response ; policy ? : CachePolicy ; } ; async function getItem ( key ) { let [ [ , response ] , [ , policy ] ] = await AsyncStorage . multiGet ( [ `${ ROOT }:${ key }:response` , `${ ROOT }:${ key }:policy` , ] ) ; if ( ! response ) { return { response , policy : null } ; } let { body , ... init } = JSON . parse ( response ) ; return { response : new Response ( body , init ) , policy : CachePolicy . fromObject ( JSON . parse ( policy ) ) , } ; } async function handleInitialFetch ( args ) { let { request , key } = args ; debug && console . log ( `fetch(${ request . url }): no policy cached; fetching` ) ; let response = await fetch ( request ) ; let cachePolicyRequest = requestForCachePolicy ( request ) ; let cachePolicyResponse = responseForCachePolicy ( response ) ; let policy = new CachePolicy ( cachePolicyRequest , cachePolicyResponse ) ; if ( policy . storable ( ) ) { debug && console . log ( `fetch(${ request . url }): caching` ) ; await cacheItem ( { key , response , policy } ) ; } else { debug && console . log ( `fetch(${ request . url }): not cachable` ) ; } return response ; } type HandlePartialFetchArgs = { request : Request ; oldResponse : Response ; oldPolicy : CachePolicy ; key : string ; } ; function handleCachedButStillFresh ( args ) { let { request , oldResponse , oldPolicy } = args ; debug && console . log ( `fetch(${ request . url }): fresh; returning` ) ; oldResponse . headers = new Headers ( oldPolicy . responseHeaders ( ) ) ; return oldResponse ; } async function handleStale ( args ) { let { request , oldResponse , oldPolicy , key } = args ; debug && console . log ( `fetch(${ request . url }): stale; validating` ) ; let cachePolicyRequest = requestForCachePolicy ( request ) ; let newResponse = null ; try { let newHeaders = oldPolicy . revalidationHeaders ( cachePolicyRequest ) ; request . headers = new Headers ( newHeaders ) ; newResponse = await fetch ( request ) ; } catch ( error ) { debug && console . log ( `fetch(${ request . url }): offline; returning stale data` ) ; oldResponse . headers = new Headers ( oldPolicy . responseHeaders ( ) ) ; return oldResponse ; } let newCachePolicyResponse = responseForCachePolicy ( newResponse ) ; let { policy , modified } = oldPolicy . revalidatedPolicy ( cachePolicyRequest , newCachePolicyResponse ) ; if ( debug ) { if ( modified ) { console . log ( `fetch(${ request . url }): validated; did change` ) ; } else { console . log ( `fetch(${ request . url }): validated; 304 no change` ) ; } } let response = modified ? newResponse : oldResponse ; await cacheItem ( { key , policy , response } ) ; response . headers = new Headers ( policy . responseHeaders ( ) ) ; debug && console . log ( `fetch(${ request . url }): returning updated response` ) ; return response ; } function handleBundledInDev ( request , bundledResponse ) { debug && console . log ( `fetch(${ request . url }): in dev mode; returning bundled data` ) ; let { body , ... init } = JSON . parse ( bundledResponse ) ; return new Response ( body , init ) ; } export async function cachedFetch ( request ) { let { url } = request ; let key = `urlcache:${ url }` ; let { response : oldResponse , policy : oldPolicy } = await getItem ( key ) ; if ( process . env . NODE_ENV === 'development' ) { let bundledResponse = await AsyncStorage . getItem ( `${ ROOT }:${ key }:bundled` ) ; if ( bundledResponse ) { return handleBundledInDev ( request , bundledResponse ) ; } } if ( ! oldPolicy ) { return handleInitialFetch ( { request , key } ) ; } if ( oldPolicy . satisfiesWithoutRevalidation ( requestForCachePolicy ( request ) ) ) { return handleCachedButStillFresh ( { request , oldResponse , oldPolicy , key } ) ; } return handleStale ( { request , oldResponse , oldPolicy , key } ) ; }