mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {useRef, useState} from 'react'
2import {AppState, AppStateStatus} from 'react-native'
3import AsyncStorage from '@react-native-async-storage/async-storage'
4import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister'
5import {focusManager, onlineManager, QueryClient} from '@tanstack/react-query'
6import {
7 PersistQueryClientProvider,
8 PersistQueryClientProviderProps,
9} from '@tanstack/react-query-persist-client'
10
11import {isNative} from '#/platform/detection'
12import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
13
14// any query keys in this array will be persisted to AsyncStorage
15export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info'
16const STORED_CACHE_QUERY_KEY_ROOTS = [labelersDetailedInfoQueryKeyRoot]
17
18async function checkIsOnline(): Promise<boolean> {
19 try {
20 const controller = new AbortController()
21 setTimeout(() => {
22 controller.abort()
23 }, 15e3)
24 const res = await fetch('https://public.api.bsky.app/xrpc/_health', {
25 cache: 'no-store',
26 signal: controller.signal,
27 })
28 const json = await res.json()
29 if (json.version) {
30 return true
31 } else {
32 return false
33 }
34 } catch (e) {
35 return false
36 }
37}
38
39let receivedNetworkLost = false
40let receivedNetworkConfirmed = false
41let isNetworkStateUnclear = false
42
43listenNetworkLost(() => {
44 receivedNetworkLost = true
45 onlineManager.setOnline(false)
46})
47
48listenNetworkConfirmed(() => {
49 receivedNetworkConfirmed = true
50 onlineManager.setOnline(true)
51})
52
53let checkPromise: Promise<void> | undefined
54function checkIsOnlineIfNeeded() {
55 if (checkPromise) {
56 return
57 }
58 receivedNetworkLost = false
59 receivedNetworkConfirmed = false
60 checkPromise = checkIsOnline().then(nextIsOnline => {
61 checkPromise = undefined
62 if (nextIsOnline && receivedNetworkLost) {
63 isNetworkStateUnclear = true
64 }
65 if (!nextIsOnline && receivedNetworkConfirmed) {
66 isNetworkStateUnclear = true
67 }
68 if (!isNetworkStateUnclear) {
69 onlineManager.setOnline(nextIsOnline)
70 }
71 })
72}
73
74setInterval(() => {
75 if (AppState.currentState === 'active') {
76 if (!onlineManager.isOnline() || isNetworkStateUnclear) {
77 checkIsOnlineIfNeeded()
78 }
79 }
80}, 2000)
81
82focusManager.setEventListener(onFocus => {
83 if (isNative) {
84 const subscription = AppState.addEventListener(
85 'change',
86 (status: AppStateStatus) => {
87 focusManager.setFocused(status === 'active')
88 },
89 )
90
91 return () => subscription.remove()
92 } else if (typeof window !== 'undefined' && window.addEventListener) {
93 // these handlers are a bit redundant but focus catches when the browser window
94 // is blurred/focused while visibilitychange seems to only handle when the
95 // window minimizes (both of them catch tab changes)
96 // there's no harm to redundant fires because refetchOnWindowFocus is only
97 // used with queries that employ stale data times
98 const handler = () => onFocus()
99 window.addEventListener('focus', handler, false)
100 window.addEventListener('visibilitychange', handler, false)
101 return () => {
102 window.removeEventListener('visibilitychange', handler)
103 window.removeEventListener('focus', handler)
104 }
105 }
106})
107
108const createQueryClient = () =>
109 new QueryClient({
110 defaultOptions: {
111 queries: {
112 // NOTE
113 // refetchOnWindowFocus breaks some UIs (like feeds)
114 // so we only selectively want to enable this
115 // -prf
116 refetchOnWindowFocus: false,
117 // Structural sharing between responses makes it impossible to rely on
118 // "first seen" timestamps on objects to determine if they're fresh.
119 // Disable this optimization so that we can rely on "first seen" timestamps.
120 structuralSharing: false,
121 // We don't want to retry queries by default, because in most cases we
122 // want to fail early and show a response to the user. There are
123 // exceptions, and those can be made on a per-query basis. For others, we
124 // should give users controls to retry.
125 retry: false,
126 },
127 },
128 })
129
130const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehydrateOptions'] =
131 {
132 shouldDehydrateMutation: (_: any) => false,
133 shouldDehydrateQuery: query => {
134 return STORED_CACHE_QUERY_KEY_ROOTS.includes(String(query.queryKey[0]))
135 },
136 }
137
138export function QueryProvider({
139 children,
140 currentDid,
141}: {
142 children: React.ReactNode
143 currentDid: string | undefined
144}) {
145 return (
146 <QueryProviderInner
147 // Enforce we never reuse cache between users.
148 // These two props MUST stay in sync.
149 key={currentDid}
150 currentDid={currentDid}>
151 {children}
152 </QueryProviderInner>
153 )
154}
155
156function QueryProviderInner({
157 children,
158 currentDid,
159}: {
160 children: React.ReactNode
161 currentDid: string | undefined
162}) {
163 const initialDid = useRef(currentDid)
164 if (currentDid !== initialDid.current) {
165 throw Error(
166 'Something is very wrong. Expected did to be stable due to key above.',
167 )
168 }
169 // We create the query client here so that it's scoped to a specific DID.
170 // Do not move the query client creation outside of this component.
171 const [queryClient, _setQueryClient] = useState(() => createQueryClient())
172 const [persistOptions, _setPersistOptions] = useState(() => {
173 const asyncPersister = createAsyncStoragePersister({
174 storage: AsyncStorage,
175 key: 'queryClient-' + (currentDid ?? 'logged-out'),
176 })
177 return {
178 persister: asyncPersister,
179 dehydrateOptions,
180 }
181 })
182 return (
183 <PersistQueryClientProvider
184 client={queryClient}
185 persistOptions={persistOptions}>
186 {children}
187 </PersistQueryClientProvider>
188 )
189}