Bluesky app fork with some witchin' additions 馃挮
at post-text-option 136 lines 3.9 kB view raw
1import {useEffect, useState} from 'react' 2import EventEmitter from 'eventemitter3' 3 4import {networkRetry} from '#/lib/async/retry' 5import { 6 FALLBACK_GEOLOCATION_SERVICE_RESPONSE, 7 GEOLOCATION_SERVICE_URL, 8} from '#/geolocation/const' 9import * as debug from '#/geolocation/debug' 10import {logger} from '#/geolocation/logger' 11import {type Geolocation} from '#/geolocation/types' 12import {device} from '#/storage' 13 14const events = new EventEmitter() 15const EVENT = 'geolocation-service-response-updated' 16const emitGeolocationServiceResponseUpdate = (data: Geolocation) => { 17 events.emit(EVENT, data) 18} 19const onGeolocationServiceResponseUpdate = ( 20 listener: (data: Geolocation) => void, 21) => { 22 events.on(EVENT, listener) 23 return () => { 24 events.off(EVENT, listener) 25 } 26} 27 28async function fetchGeolocationServiceData( 29 url: string, 30): Promise<Geolocation | undefined> { 31 if (debug.enabled) return debug.resolve(debug.geolocation) 32 const res = await fetch(url) 33 if (!res.ok) { 34 throw new Error(`fetchGeolocationServiceData failed ${res.status}`) 35 } 36 return res.json() as Promise<Geolocation> 37} 38 39/** 40 * Local promise used within this file only. 41 */ 42let geolocationServicePromise: Promise<{success: boolean}> | undefined 43 44/** 45 * Begin the process of resolving geolocation config. This is called right away 46 * at app start, and the promise is awaited later before proceeding with app 47 * startup. 48 */ 49export async function resolve() { 50 if (geolocationServicePromise) { 51 const cached = device.get(['geolocationServiceResponse']) 52 if (cached) { 53 logger.debug(`resolve(): using cache`) 54 } else { 55 logger.debug(`resolve(): no cache`) 56 const {success} = await geolocationServicePromise 57 if (success) { 58 logger.debug(`resolve(): resolved`) 59 } else { 60 logger.info(`resolve(): failed`) 61 } 62 } 63 } else { 64 logger.debug(`resolve(): initiating`) 65 66 /** 67 * THIS PROMISE SHOULD NEVER `reject()`! We want the app to proceed with 68 * startup, even if geolocation resolution fails. 69 */ 70 geolocationServicePromise = new Promise(async resolvePromise => { 71 let success = false 72 73 function cacheResponseOrThrow(response: Geolocation | undefined) { 74 if (response) { 75 device.set(['geolocationServiceResponse'], response) 76 emitGeolocationServiceResponseUpdate(response) 77 } else { 78 // endpoint should throw on all failures, this is insurance 79 throw new Error(`fetchGeolocationServiceData returned no data`) 80 } 81 } 82 83 try { 84 // Try once, fail fast 85 const config = await fetchGeolocationServiceData( 86 GEOLOCATION_SERVICE_URL, 87 ) 88 cacheResponseOrThrow(config) 89 success = true 90 } catch (e: any) { 91 logger.debug( 92 `resolve(): fetchGeolocationServiceData failed initial request`, 93 { 94 safeMessage: e.message, 95 }, 96 ) 97 98 // retry 3 times, but don't await, proceed with default 99 networkRetry(3, () => 100 fetchGeolocationServiceData(GEOLOCATION_SERVICE_URL), 101 ) 102 .then(config => { 103 cacheResponseOrThrow(config) 104 }) 105 .catch((err: any) => { 106 // complete fail closed 107 logger.debug( 108 `resolve(): fetchGeolocationServiceData failed retries`, 109 { 110 safeMessage: err.message, 111 }, 112 ) 113 }) 114 } finally { 115 resolvePromise({success}) 116 } 117 }) 118 } 119} 120 121export function useGeolocationServiceResponse() { 122 const [config, setConfig] = useState(() => { 123 const initial = 124 device.get(['geolocationServiceResponse']) || 125 FALLBACK_GEOLOCATION_SERVICE_RESPONSE 126 return initial 127 }) 128 129 useEffect(() => { 130 return onGeolocationServiceResponseUpdate(responseConfig => { 131 setConfig(responseConfig!) 132 }) 133 }, []) 134 135 return config 136}