forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}