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