Scope query client per DID (#3333)

* Move QueryProvider inside the key

* Pull useQueryClient-dependent code down in App.native

* Remove useQueryClient dependency from session provider

* Scope query client per DID
zio/stable
dan 2024-04-04 02:51:10 +01:00 committed by GitHub
parent db3cd3e821
commit e51ccb46b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 159 additions and 126 deletions

View File

@ -54,12 +54,10 @@ SplashScreen.preventAutoHideAsync()
function InnerApp() { function InnerApp() {
const {isInitialLoad, currentAccount} = useSession() const {isInitialLoad, currentAccount} = useSession()
const {resumeSession} = useSessionApi() const {resumeSession} = useSessionApi()
const queryClient = useQueryClient()
const theme = useColorModeTheme() const theme = useColorModeTheme()
const {_} = useLingui() const {_} = useLingui()
useIntentHandler() useIntentHandler()
useNotificationsListener(queryClient)
useOTAUpdates() useOTAUpdates()
// init // init
@ -79,25 +77,29 @@ function InnerApp() {
<React.Fragment <React.Fragment
// Resets the entire tree below when it changes: // Resets the entire tree below when it changes:
key={currentAccount?.did}> key={currentAccount?.did}>
<StatsigProvider> <QueryProvider currentDid={currentAccount?.did}>
<LabelDefsProvider> <PushNotificationsListener>
<LoggedOutViewProvider> <StatsigProvider>
<SelectedFeedProvider> <LabelDefsProvider>
<UnreadNotifsProvider> <LoggedOutViewProvider>
<ThemeProvider theme={theme}> <SelectedFeedProvider>
{/* All components should be within this provider */} <UnreadNotifsProvider>
<RootSiblingParent> <ThemeProvider theme={theme}>
<GestureHandlerRootView style={s.h100pct}> {/* All components should be within this provider */}
<TestCtrls /> <RootSiblingParent>
<Shell /> <GestureHandlerRootView style={s.h100pct}>
</GestureHandlerRootView> <TestCtrls />
</RootSiblingParent> <Shell />
</ThemeProvider> </GestureHandlerRootView>
</UnreadNotifsProvider> </RootSiblingParent>
</SelectedFeedProvider> </ThemeProvider>
</LoggedOutViewProvider> </UnreadNotifsProvider>
</LabelDefsProvider> </SelectedFeedProvider>
</StatsigProvider> </LoggedOutViewProvider>
</LabelDefsProvider>
</StatsigProvider>
</PushNotificationsListener>
</QueryProvider>
</React.Fragment> </React.Fragment>
</Splash> </Splash>
</Alf> </Alf>
@ -105,6 +107,12 @@ function InnerApp() {
) )
} }
function PushNotificationsListener({children}: {children: React.ReactNode}) {
const queryClient = useQueryClient()
useNotificationsListener(queryClient)
return children
}
function App() { function App() {
const [isReady, setReady] = useState(false) const [isReady, setReady] = useState(false)
@ -121,29 +129,27 @@ function App() {
* that is set up in the InnerApp component above. * that is set up in the InnerApp component above.
*/ */
return ( return (
<QueryProvider> <SessionProvider>
<SessionProvider> <ShellStateProvider>
<ShellStateProvider> <PrefsStateProvider>
<PrefsStateProvider> <MutedThreadsProvider>
<MutedThreadsProvider> <InvitesStateProvider>
<InvitesStateProvider> <ModalStateProvider>
<ModalStateProvider> <DialogStateProvider>
<DialogStateProvider> <LightboxStateProvider>
<LightboxStateProvider> <I18nProvider>
<I18nProvider> <PortalProvider>
<PortalProvider> <InnerApp />
<InnerApp /> </PortalProvider>
</PortalProvider> </I18nProvider>
</I18nProvider> </LightboxStateProvider>
</LightboxStateProvider> </DialogStateProvider>
</DialogStateProvider> </ModalStateProvider>
</ModalStateProvider> </InvitesStateProvider>
</InvitesStateProvider> </MutedThreadsProvider>
</MutedThreadsProvider> </PrefsStateProvider>
</PrefsStateProvider> </ShellStateProvider>
</ShellStateProvider> </SessionProvider>
</SessionProvider>
</QueryProvider>
) )
} }

View File

@ -54,25 +54,27 @@ function InnerApp() {
<React.Fragment <React.Fragment
// Resets the entire tree below when it changes: // Resets the entire tree below when it changes:
key={currentAccount?.did}> key={currentAccount?.did}>
<StatsigProvider> <QueryProvider currentDid={currentAccount?.did}>
<LabelDefsProvider> <StatsigProvider>
<LoggedOutViewProvider> <LabelDefsProvider>
<SelectedFeedProvider> <LoggedOutViewProvider>
<UnreadNotifsProvider> <SelectedFeedProvider>
<ThemeProvider theme={theme}> <UnreadNotifsProvider>
{/* All components should be within this provider */} <ThemeProvider theme={theme}>
<RootSiblingParent> {/* All components should be within this provider */}
<SafeAreaProvider> <RootSiblingParent>
<Shell /> <SafeAreaProvider>
</SafeAreaProvider> <Shell />
</RootSiblingParent> </SafeAreaProvider>
<ToastContainer /> </RootSiblingParent>
</ThemeProvider> <ToastContainer />
</UnreadNotifsProvider> </ThemeProvider>
</SelectedFeedProvider> </UnreadNotifsProvider>
</LoggedOutViewProvider> </SelectedFeedProvider>
</LabelDefsProvider> </LoggedOutViewProvider>
</StatsigProvider> </LabelDefsProvider>
</StatsigProvider>
</QueryProvider>
</React.Fragment> </React.Fragment>
</Alf> </Alf>
) )
@ -94,29 +96,27 @@ function App() {
* that is set up in the InnerApp component above. * that is set up in the InnerApp component above.
*/ */
return ( return (
<QueryProvider> <SessionProvider>
<SessionProvider> <ShellStateProvider>
<ShellStateProvider> <PrefsStateProvider>
<PrefsStateProvider> <MutedThreadsProvider>
<MutedThreadsProvider> <InvitesStateProvider>
<InvitesStateProvider> <ModalStateProvider>
<ModalStateProvider> <DialogStateProvider>
<DialogStateProvider> <LightboxStateProvider>
<LightboxStateProvider> <I18nProvider>
<I18nProvider> <PortalProvider>
<PortalProvider> <InnerApp />
<InnerApp /> </PortalProvider>
</PortalProvider> </I18nProvider>
</I18nProvider> </LightboxStateProvider>
</LightboxStateProvider> </DialogStateProvider>
</DialogStateProvider> </ModalStateProvider>
</ModalStateProvider> </InvitesStateProvider>
</InvitesStateProvider> </MutedThreadsProvider>
</MutedThreadsProvider> </PrefsStateProvider>
</PrefsStateProvider> </ShellStateProvider>
</ShellStateProvider> </SessionProvider>
</SessionProvider>
</QueryProvider>
) )
} }

View File

@ -1,4 +1,4 @@
import React from 'react' import React, {useRef, useState} from 'react'
import {AppState, AppStateStatus} from 'react-native' import {AppState, AppStateStatus} from 'react-native'
import AsyncStorage from '@react-native-async-storage/async-storage' import AsyncStorage from '@react-native-async-storage/async-storage'
import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister'
@ -39,31 +39,27 @@ focusManager.setEventListener(onFocus => {
} }
}) })
const queryClient = new QueryClient({ const createQueryClient = () =>
defaultOptions: { new QueryClient({
queries: { defaultOptions: {
// NOTE queries: {
// refetchOnWindowFocus breaks some UIs (like feeds) // NOTE
// so we only selectively want to enable this // refetchOnWindowFocus breaks some UIs (like feeds)
// -prf // so we only selectively want to enable this
refetchOnWindowFocus: false, // -prf
// Structural sharing between responses makes it impossible to rely on refetchOnWindowFocus: false,
// "first seen" timestamps on objects to determine if they're fresh. // Structural sharing between responses makes it impossible to rely on
// Disable this optimization so that we can rely on "first seen" timestamps. // "first seen" timestamps on objects to determine if they're fresh.
structuralSharing: false, // Disable this optimization so that we can rely on "first seen" timestamps.
// We don't want to retry queries by default, because in most cases we structuralSharing: false,
// want to fail early and show a response to the user. There are // We don't want to retry queries by default, because in most cases we
// exceptions, and those can be made on a per-query basis. For others, we // want to fail early and show a response to the user. There are
// should give users controls to retry. // exceptions, and those can be made on a per-query basis. For others, we
retry: false, // should give users controls to retry.
retry: false,
},
}, },
}, })
})
const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
key: 'queryCache',
})
const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehydrateOptions'] = const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehydrateOptions'] =
{ {
@ -73,12 +69,50 @@ const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehyd
}, },
} }
const persistOptions = { export function QueryProvider({
persister: asyncStoragePersister, children,
dehydrateOptions, currentDid,
}: {
children: React.ReactNode
currentDid: string | undefined
}) {
return (
<QueryProviderInner
// Enforce we never reuse cache between users.
// These two props MUST stay in sync.
key={currentDid}
currentDid={currentDid}>
{children}
</QueryProviderInner>
)
} }
export function QueryProvider({children}: {children: React.ReactNode}) { function QueryProviderInner({
children,
currentDid,
}: {
children: React.ReactNode
currentDid: string | undefined
}) {
const initialDid = useRef(currentDid)
if (currentDid !== initialDid.current) {
throw Error(
'Something is very wrong. Expected did to be stable due to key above.',
)
}
// We create the query client here so that it's scoped to a specific DID.
// Do not move the query client creation outside of this component.
const [queryClient, _setQueryClient] = useState(() => createQueryClient())
const [persistOptions, _setPersistOptions] = useState(() => {
const asyncPersister = createAsyncStoragePersister({
storage: AsyncStorage,
key: 'queryClient-' + (currentDid ?? 'logged-out'),
})
return {
persister: asyncPersister,
dehydrateOptions,
}
})
return ( return (
<PersistQueryClientProvider <PersistQueryClientProvider
client={queryClient} client={queryClient}

View File

@ -4,7 +4,6 @@ import {
BSKY_LABELER_DID, BSKY_LABELER_DID,
BskyAgent, BskyAgent,
} from '@atproto/api' } from '@atproto/api'
import {useQueryClient} from '@tanstack/react-query'
import {jwtDecode} from 'jwt-decode' import {jwtDecode} from 'jwt-decode'
import {track} from '#/lib/analytics/analytics' import {track} from '#/lib/analytics/analytics'
@ -178,7 +177,6 @@ function createPersistSessionHandler(
} }
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
const queryClient = useQueryClient()
const isDirty = React.useRef(false) const isDirty = React.useRef(false)
const [state, setState] = React.useState<SessionState>({ const [state, setState] = React.useState<SessionState>({
isInitialLoad: true, isInitialLoad: true,
@ -211,12 +209,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const clearCurrentAccount = React.useCallback(() => { const clearCurrentAccount = React.useCallback(() => {
logger.warn(`session: clear current account`) logger.warn(`session: clear current account`)
__globalAgent = PUBLIC_BSKY_AGENT __globalAgent = PUBLIC_BSKY_AGENT
queryClient.clear()
setStateAndPersist(s => ({ setStateAndPersist(s => ({
...s, ...s,
currentAccount: undefined, currentAccount: undefined,
})) }))
}, [setStateAndPersist, queryClient]) }, [setStateAndPersist])
const createAccount = React.useCallback<ApiContext['createAccount']>( const createAccount = React.useCallback<ApiContext['createAccount']>(
async ({ async ({
@ -286,14 +283,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
) )
__globalAgent = agent __globalAgent = agent
queryClient.clear()
upsertAccount(account) upsertAccount(account)
logger.debug(`session: created account`, {}, logger.DebugContext.session) logger.debug(`session: created account`, {}, logger.DebugContext.session)
track('Create Account') track('Create Account')
logEvent('account:create:success', {}) logEvent('account:create:success', {})
}, },
[upsertAccount, queryClient, clearCurrentAccount], [upsertAccount, clearCurrentAccount],
) )
const login = React.useCallback<ApiContext['login']>( const login = React.useCallback<ApiContext['login']>(
@ -334,7 +330,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
__globalAgent = agent __globalAgent = agent
// @ts-ignore // @ts-ignore
if (IS_DEV && isWeb) window.agent = agent if (IS_DEV && isWeb) window.agent = agent
queryClient.clear()
upsertAccount(account) upsertAccount(account)
logger.debug(`session: logged in`, {}, logger.DebugContext.session) logger.debug(`session: logged in`, {}, logger.DebugContext.session)
@ -342,7 +337,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
track('Sign In', {resumedSession: false}) track('Sign In', {resumedSession: false})
logEvent('account:loggedIn', {logContext, withPassword: true}) logEvent('account:loggedIn', {logContext, withPassword: true})
}, },
[upsertAccount, queryClient, clearCurrentAccount], [upsertAccount, clearCurrentAccount],
) )
const logout = React.useCallback<ApiContext['logout']>( const logout = React.useCallback<ApiContext['logout']>(
@ -411,7 +406,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
agent.session = prevSession agent.session = prevSession
__globalAgent = agent __globalAgent = agent
queryClient.clear()
upsertAccount(account) upsertAccount(account)
if (prevSession.deactivated) { if (prevSession.deactivated) {
@ -448,7 +442,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
try { try {
const freshAccount = await resumeSessionWithFreshAccount() const freshAccount = await resumeSessionWithFreshAccount()
__globalAgent = agent __globalAgent = agent
queryClient.clear()
upsertAccount(freshAccount) upsertAccount(freshAccount)
} catch (e) { } catch (e) {
/* /*
@ -489,7 +482,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
} }
} }
}, },
[upsertAccount, queryClient, clearCurrentAccount], [upsertAccount, clearCurrentAccount],
) )
const resumeSession = React.useCallback<ApiContext['resumeSession']>( const resumeSession = React.useCallback<ApiContext['resumeSession']>(