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