From e51ccb46b8673b7444b7cac0792da4a9f6a91c4b Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 4 Apr 2024 02:51:10 +0100 Subject: [PATCH] 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 --- src/App.native.tsx | 94 ++++++++++++++++++++----------------- src/App.web.tsx | 84 ++++++++++++++++----------------- src/lib/react-query.tsx | 92 ++++++++++++++++++++++++------------ src/state/session/index.tsx | 15 ++---- 4 files changed, 159 insertions(+), 126 deletions(-) diff --git a/src/App.native.tsx b/src/App.native.tsx index 57ebe495..9abe4a55 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -54,12 +54,10 @@ SplashScreen.preventAutoHideAsync() function InnerApp() { const {isInitialLoad, currentAccount} = useSession() const {resumeSession} = useSessionApi() - const queryClient = useQueryClient() const theme = useColorModeTheme() const {_} = useLingui() useIntentHandler() - useNotificationsListener(queryClient) useOTAUpdates() // init @@ -79,25 +77,29 @@ function InnerApp() { - - - - - - - {/* All components should be within this provider */} - - - - - - - - - - - - + + + + + + + + + {/* All components should be within this provider */} + + + + + + + + + + + + + + @@ -105,6 +107,12 @@ function InnerApp() { ) } +function PushNotificationsListener({children}: {children: React.ReactNode}) { + const queryClient = useQueryClient() + useNotificationsListener(queryClient) + return children +} + function App() { const [isReady, setReady] = useState(false) @@ -121,29 +129,27 @@ function App() { * that is set up in the InnerApp component above. */ return ( - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/src/App.web.tsx b/src/App.web.tsx index 2910bbba..ccf7ecb4 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -54,25 +54,27 @@ function InnerApp() { - - - - - - - {/* All components should be within this provider */} - - - - - - - - - - - - + + + + + + + + {/* All components should be within this provider */} + + + + + + + + + + + + + ) @@ -94,29 +96,27 @@ function App() { * that is set up in the InnerApp component above. */ return ( - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/src/lib/react-query.tsx b/src/lib/react-query.tsx index 08b61ee2..2fcd4694 100644 --- a/src/lib/react-query.tsx +++ b/src/lib/react-query.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {useRef, useState} from 'react' import {AppState, AppStateStatus} from 'react-native' import AsyncStorage from '@react-native-async-storage/async-storage' import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' @@ -39,31 +39,27 @@ focusManager.setEventListener(onFocus => { } }) -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - // NOTE - // refetchOnWindowFocus breaks some UIs (like feeds) - // so we only selectively want to enable this - // -prf - refetchOnWindowFocus: false, - // Structural sharing between responses makes it impossible to rely on - // "first seen" timestamps on objects to determine if they're fresh. - // Disable this optimization so that we can rely on "first seen" timestamps. - structuralSharing: false, - // We don't want to retry queries by default, because in most cases we - // want to fail early and show a response to the user. There are - // exceptions, and those can be made on a per-query basis. For others, we - // should give users controls to retry. - retry: false, +const createQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + // NOTE + // refetchOnWindowFocus breaks some UIs (like feeds) + // so we only selectively want to enable this + // -prf + refetchOnWindowFocus: false, + // Structural sharing between responses makes it impossible to rely on + // "first seen" timestamps on objects to determine if they're fresh. + // Disable this optimization so that we can rely on "first seen" timestamps. + structuralSharing: false, + // We don't want to retry queries by default, because in most cases we + // want to fail early and show a response to the user. There are + // exceptions, and those can be made on a per-query basis. For others, we + // should give users controls to retry. + retry: false, + }, }, - }, -}) - -const asyncStoragePersister = createAsyncStoragePersister({ - storage: AsyncStorage, - key: 'queryCache', -}) + }) const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehydrateOptions'] = { @@ -73,12 +69,50 @@ const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehyd }, } -const persistOptions = { - persister: asyncStoragePersister, - dehydrateOptions, +export function QueryProvider({ + children, + currentDid, +}: { + children: React.ReactNode + currentDid: string | undefined +}) { + return ( + + {children} + + ) } -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 ( ) { - const queryClient = useQueryClient() const isDirty = React.useRef(false) const [state, setState] = React.useState({ isInitialLoad: true, @@ -211,12 +209,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const clearCurrentAccount = React.useCallback(() => { logger.warn(`session: clear current account`) __globalAgent = PUBLIC_BSKY_AGENT - queryClient.clear() setStateAndPersist(s => ({ ...s, currentAccount: undefined, })) - }, [setStateAndPersist, queryClient]) + }, [setStateAndPersist]) const createAccount = React.useCallback( async ({ @@ -286,14 +283,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) __globalAgent = agent - queryClient.clear() upsertAccount(account) logger.debug(`session: created account`, {}, logger.DebugContext.session) track('Create Account') logEvent('account:create:success', {}) }, - [upsertAccount, queryClient, clearCurrentAccount], + [upsertAccount, clearCurrentAccount], ) const login = React.useCallback( @@ -334,7 +330,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) { __globalAgent = agent // @ts-ignore if (IS_DEV && isWeb) window.agent = agent - queryClient.clear() upsertAccount(account) logger.debug(`session: logged in`, {}, logger.DebugContext.session) @@ -342,7 +337,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { track('Sign In', {resumedSession: false}) logEvent('account:loggedIn', {logContext, withPassword: true}) }, - [upsertAccount, queryClient, clearCurrentAccount], + [upsertAccount, clearCurrentAccount], ) const logout = React.useCallback( @@ -411,7 +406,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) { agent.session = prevSession __globalAgent = agent - queryClient.clear() upsertAccount(account) if (prevSession.deactivated) { @@ -448,7 +442,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) { try { const freshAccount = await resumeSessionWithFreshAccount() __globalAgent = agent - queryClient.clear() upsertAccount(freshAccount) } catch (e) { /* @@ -489,7 +482,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } } }, - [upsertAccount, queryClient, clearCurrentAccount], + [upsertAccount, clearCurrentAccount], ) const resumeSession = React.useCallback(