From d47341d442b347fe379c1bee621ef1c0ae61bcbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20Cs=C3=A9csey?= Date: Wed, 4 Mar 2026 16:34:00 +0000 Subject: [PATCH 1/9] Clear sync-related Redux state when user logs out Dispatch a shared userLoggedOut action on logout and invalid token detection that resets sync slices, cancels active operations, stops pollers, and clears authenticated RTK Query caches. --- apps/studio/src/components/auth-provider.tsx | 10 +++-- apps/studio/src/stores/auth-actions.ts | 3 ++ apps/studio/src/stores/index.ts | 37 +++++++++++++++++++ .../studio/src/stores/sync/connected-sites.ts | 4 ++ .../src/stores/sync/sync-operations-slice.ts | 2 + apps/studio/src/stores/sync/sync-slice.ts | 2 + 6 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 apps/studio/src/stores/auth-actions.ts diff --git a/apps/studio/src/components/auth-provider.tsx b/apps/studio/src/components/auth-provider.tsx index 1c92f0c48c..015fabc67b 100644 --- a/apps/studio/src/components/auth-provider.tsx +++ b/apps/studio/src/components/auth-provider.tsx @@ -8,7 +8,8 @@ import { useIpcListener } from 'src/hooks/use-ipc-listener'; import { useOffline } from 'src/hooks/use-offline'; import { getIpcApi } from 'src/lib/get-ipc-api'; import { isInvalidTokenError } from 'src/lib/is-invalid-oauth-token-error'; -import { useI18nLocale } from 'src/stores'; +import { useAppDispatch, useI18nLocale } from 'src/stores'; +import { userLoggedOut } from 'src/stores/auth-actions'; import { setWpcomClient } from 'src/stores/wpcom-api'; export interface AuthContextType { @@ -45,11 +46,13 @@ const AuthProvider: React.FC< AuthProviderProps > = ( { children } ) => { const { __ } = useI18n(); const isOffline = useOffline(); + const dispatch = useAppDispatch(); const authenticate = useCallback( () => getIpcApi().authenticate(), [] ); const handleInvalidToken = useCallback( async () => { try { void getIpcApi().logRendererMessage( 'info', 'Detected invalid token. Logging out.' ); + dispatch( userLoggedOut() ); await getIpcApi().clearAuthenticationToken(); setIsAuthenticated( false ); setClient( undefined ); @@ -59,7 +62,7 @@ const AuthProvider: React.FC< AuthProviderProps > = ( { children } ) => { console.error( 'Failed to handle invalid token:', err ); Sentry.captureException( err ); } - }, [] ); + }, [ dispatch ] ); useIpcListener( 'auth-updated', ( _event, payload ) => { if ( 'error' in payload ) { @@ -110,6 +113,7 @@ const AuthProvider: React.FC< AuthProviderProps > = ( { children } ) => { } try { + dispatch( userLoggedOut() ); await getIpcApi().clearAuthenticationToken(); setIsAuthenticated( false ); setClient( undefined ); @@ -119,7 +123,7 @@ const AuthProvider: React.FC< AuthProviderProps > = ( { children } ) => { console.error( err ); Sentry.captureException( err ); } - }, [ client, isOffline ] ); + }, [ client, dispatch, isOffline ] ); useEffect( () => { async function run() { diff --git a/apps/studio/src/stores/auth-actions.ts b/apps/studio/src/stores/auth-actions.ts new file mode 100644 index 0000000000..bb746957ae --- /dev/null +++ b/apps/studio/src/stores/auth-actions.ts @@ -0,0 +1,3 @@ +import { createAction } from '@reduxjs/toolkit'; + +export const userLoggedOut = createAction( 'auth/userLoggedOut' ); diff --git a/apps/studio/src/stores/index.ts b/apps/studio/src/stores/index.ts index 1c23ef38ae..05e994511a 100644 --- a/apps/studio/src/stores/index.ts +++ b/apps/studio/src/stores/index.ts @@ -14,6 +14,7 @@ import { } from 'src/hooks/use-sync-states-progress-info'; import { getIpcApi } from 'src/lib/get-ipc-api'; import { appVersionApi } from 'src/stores/app-version-api'; +import { userLoggedOut } from 'src/stores/auth-actions'; import { betaFeaturesReducer, loadBetaFeatures } from 'src/stores/beta-features-slice'; import { certificateTrustApi } from 'src/stores/certificate-trust-api'; import { reducer as chatReducer } from 'src/stores/chat-slice'; @@ -184,6 +185,42 @@ startAppListening( { }, } ); +// Clear all sync state when user logs out +startAppListening( { + actionCreator: userLoggedOut, + effect( action, listenerApi ) { + const state = listenerApi.getState(); + + // Collect all operation IDs from state and pollers + const allStateIds = new Set( [ + ...Object.keys( state.syncOperations.pushStates ), + ...Object.keys( state.syncOperations.pullStates ), + ...PUSH_POLLERS.keys(), + ...PULL_POLLERS.keys(), + ] ); + + // Cancel main process operations + for ( const stateId of allStateIds ) { + getIpcApi().cancelSyncOperation( stateId ); + } + + // Stop renderer-side pollers + for ( const controller of PUSH_POLLERS.values() ) { + controller.abort(); + } + PUSH_POLLERS.clear(); + for ( const controller of PULL_POLLERS.values() ) { + controller.abort(); + } + PULL_POLLERS.clear(); + + // Reset authenticated RTK Query caches + listenerApi.dispatch( connectedSitesApi.util.resetApiState() ); + listenerApi.dispatch( wpcomSitesApi.util.resetApiState() ); + listenerApi.dispatch( wpcomApi.util.resetApiState() ); + }, +} ); + const PUSH_POLLING_KEYS = [ 'creatingRemoteBackup', 'applyingChanges', 'finishing' ]; const SYNC_POLLING_INTERVAL = 3000; diff --git a/apps/studio/src/stores/sync/connected-sites.ts b/apps/studio/src/stores/sync/connected-sites.ts index 6b2796eba1..8640ea14ec 100644 --- a/apps/studio/src/stores/sync/connected-sites.ts +++ b/apps/studio/src/stores/sync/connected-sites.ts @@ -2,6 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { getIpcApi } from 'src/lib/get-ipc-api'; import { RootState } from 'src/stores'; +import { userLoggedOut } from 'src/stores/auth-actions'; import type { SyncSite, SyncModalMode } from 'src/modules/sync/types'; type ConnectedSitesState = { @@ -42,6 +43,9 @@ const connectedSitesSlice = createSlice( { state.selectedRemoteSiteId = null; }, }, + extraReducers: ( builder ) => { + builder.addCase( userLoggedOut, () => getInitialState() ); + }, } ); export const connectedSitesActions = connectedSitesSlice.actions; diff --git a/apps/studio/src/stores/sync/sync-operations-slice.ts b/apps/studio/src/stores/sync/sync-operations-slice.ts index 25bc312bcd..8b93341006 100644 --- a/apps/studio/src/stores/sync/sync-operations-slice.ts +++ b/apps/studio/src/stores/sync/sync-operations-slice.ts @@ -8,6 +8,7 @@ import { generateStateId } from 'src/hooks/sync-sites/use-pull-push-states'; import { getIpcApi } from 'src/lib/get-ipc-api'; import { getHostnameFromUrl } from 'src/lib/url-utils'; import { store } from 'src/stores'; +import { userLoggedOut } from 'src/stores/auth-actions'; import { connectedSitesApi } from 'src/stores/sync/connected-sites'; import type { PullStateProgressInfo, @@ -155,6 +156,7 @@ const syncOperationsSlice = createSlice( { }, extraReducers: ( builder ) => { builder + .addCase( userLoggedOut, () => initialState ) .addCase( pushSiteThunk.rejected, ( state, action ) => { const { connectedSite, selectedSite } = action.meta.arg; const stateId = generateStateId( selectedSite.id, connectedSite.id ); diff --git a/apps/studio/src/stores/sync/sync-slice.ts b/apps/studio/src/stores/sync/sync-slice.ts index cc450453c8..7abe56e033 100644 --- a/apps/studio/src/stores/sync/sync-slice.ts +++ b/apps/studio/src/stores/sync/sync-slice.ts @@ -1,5 +1,6 @@ import { createSlice } from '@reduxjs/toolkit'; import { TreeNode } from 'src/components/tree-view'; +import { userLoggedOut } from 'src/stores/auth-actions'; import { fetchRemoteFileTree } from './sync-api'; type RemoteFileTreeState = { @@ -33,6 +34,7 @@ const syncSlice = createSlice( { }, extraReducers: ( builder ) => { builder + .addCase( userLoggedOut, () => initialState ) .addCase( fetchRemoteFileTree.pending, ( state ) => { state.remoteFileTrees.loading = true; state.remoteFileTrees.error = null; From c680af74f2c6cc43eba1ef8fbfad7620d8ac360a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20Cs=C3=A9csey?= Date: Fri, 13 Mar 2026 12:42:30 +0000 Subject: [PATCH 2/9] Treat failed import during sync pull as a pull error When importSite fails during a sync pull, the pull Redux state stays at 'importing' while the import context fires an error event. Since canCancelPull only allows cancellation for 'in-progress' and 'downloading' states, the user gets stuck with a disabled dismiss button. Add an isError flag to ImportProgressState and set it on import errors. In the sync UI, detect when a pull is in 'importing' state but the import has failed, and treat it as a pull error so the user can dismiss it and retry. --- apps/studio/src/hooks/use-import-export.tsx | 2 ++ .../modules/sync/components/sync-connected-sites.tsx | 12 ++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/studio/src/hooks/use-import-export.tsx b/apps/studio/src/hooks/use-import-export.tsx index af7c321726..75f9e89a1a 100644 --- a/apps/studio/src/hooks/use-import-export.tsx +++ b/apps/studio/src/hooks/use-import-export.tsx @@ -26,6 +26,7 @@ export type ImportProgressState = { statusMessage: string; progress: number; isNewSite?: boolean; + isError?: boolean; }; }; @@ -339,6 +340,7 @@ export const ImportExportProvider = ( { children }: { children: React.ReactNode [ siteId ]: { ...currentProgress, statusMessage: __( 'Import failed. Please try again.' ), + isError: true, }, } ) ); break; diff --git a/apps/studio/src/modules/sync/components/sync-connected-sites.tsx b/apps/studio/src/modules/sync/components/sync-connected-sites.tsx index 69c58ecca6..9c0dc98c88 100644 --- a/apps/studio/src/modules/sync/components/sync-connected-sites.tsx +++ b/apps/studio/src/modules/sync/components/sync-connected-sites.tsx @@ -221,11 +221,15 @@ const SyncConnectedSitesSectionItem = ( { const sitePullState = useRootSelector( syncOperationsSelectors.selectPullState( selectedSite.id, connectedSite.id ) ); + const pullImportFailed = + sitePullState?.status.key === 'importing' && + importState[ connectedSite.localSiteId ]?.isError === true; const isPulling = - sitePullState?.status.key === 'in-progress' || - sitePullState?.status.key === 'downloading' || - sitePullState?.status.key === 'importing'; - const isPullError = sitePullState?.status.key === 'failed'; + ( sitePullState?.status.key === 'in-progress' || + sitePullState?.status.key === 'downloading' || + sitePullState?.status.key === 'importing' ) && + ! pullImportFailed; + const isPullError = sitePullState?.status.key === 'failed' || pullImportFailed; const hasPullFinished = sitePullState?.status.key === 'finished'; const hasPullCancelled = sitePullState?.status.key === 'cancelled'; const pullImportState = importState[ connectedSite.localSiteId ]; From e667920b78cb0a30bdf1662a731255393c028cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20Cs=C3=A9csey?= Date: Fri, 13 Mar 2026 12:45:48 +0000 Subject: [PATCH 3/9] Simplify logout cleanup in ImportExportProvider to use direct isAuthenticated check --- apps/studio/src/hooks/use-import-export.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/studio/src/hooks/use-import-export.tsx b/apps/studio/src/hooks/use-import-export.tsx index 75f9e89a1a..7fc57eacf5 100644 --- a/apps/studio/src/hooks/use-import-export.tsx +++ b/apps/studio/src/hooks/use-import-export.tsx @@ -1,7 +1,8 @@ import * as Sentry from '@sentry/electron/renderer'; import { __, sprintf } from '@wordpress/i18n'; -import { createContext, useMemo, useState, useCallback, useContext } from 'react'; +import { createContext, useEffect, useMemo, useState, useCallback, useContext } from 'react'; import { WP_CLI_IMPORT_EXPORT_RESPONSE_TIMEOUT_IN_HRS } from 'src/constants'; +import { useAuth } from 'src/hooks/use-auth'; import { useIpcListener } from 'src/hooks/use-ipc-listener'; import { useSiteDetails } from 'src/hooks/use-site-details'; import { simplifyErrorForDisplay } from 'src/lib/error-formatting'; @@ -77,6 +78,14 @@ export const ImportExportProvider = ( { children }: { children: React.ReactNode const [ importState, setImportState ] = useState< ImportProgressState >( {} ); const [ exportState, setExportState ] = useState< ExportProgressState >( {} ); const { startServer, stopServer, updateSite } = useSiteDetails(); + const { isAuthenticated } = useAuth(); + + useEffect( () => { + if ( ! isAuthenticated ) { + setImportState( {} ); + setExportState( {} ); + } + }, [ isAuthenticated ] ); const importFile = useCallback( async ( From f8169954e29b265873997540411b0fbecb21d53d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20Cs=C3=A9csey?= Date: Mon, 16 Mar 2026 11:56:52 +0000 Subject: [PATCH 4/9] Use cancel thunks for sync cleanup on logout instead of manual cancellation --- apps/studio/src/stores/index.ts | 35 +++++++++---------- .../src/stores/sync/sync-operations-slice.ts | 31 ++++++++++------ 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/apps/studio/src/stores/index.ts b/apps/studio/src/stores/index.ts index 05e994511a..c3e3f9841a 100644 --- a/apps/studio/src/stores/index.ts +++ b/apps/studio/src/stores/index.ts @@ -191,28 +191,25 @@ startAppListening( { effect( action, listenerApi ) { const state = listenerApi.getState(); - // Collect all operation IDs from state and pollers - const allStateIds = new Set( [ - ...Object.keys( state.syncOperations.pushStates ), - ...Object.keys( state.syncOperations.pullStates ), - ...PUSH_POLLERS.keys(), - ...PULL_POLLERS.keys(), - ] ); - - // Cancel main process operations - for ( const stateId of allStateIds ) { - getIpcApi().cancelSyncOperation( stateId ); + for ( const pullState of Object.values( state.syncOperations.pullStates ) ) { + void store.dispatch( + syncOperationsThunks.cancelPull( { + selectedSiteId: pullState.selectedSite.id, + remoteSiteId: pullState.remoteSiteId, + displayNotification: false, + } ) + ); } - // Stop renderer-side pollers - for ( const controller of PUSH_POLLERS.values() ) { - controller.abort(); - } - PUSH_POLLERS.clear(); - for ( const controller of PULL_POLLERS.values() ) { - controller.abort(); + for ( const pushState of Object.values( state.syncOperations.pushStates ) ) { + void store.dispatch( + syncOperationsThunks.cancelPush( { + selectedSiteId: pushState.selectedSite.id, + remoteSiteId: pushState.remoteSiteId, + displayNotification: false, + } ) + ); } - PULL_POLLERS.clear(); // Reset authenticated RTK Query caches listenerApi.dispatch( connectedSitesApi.util.resetApiState() ); diff --git a/apps/studio/src/stores/sync/sync-operations-slice.ts b/apps/studio/src/stores/sync/sync-operations-slice.ts index e5c1c4efda..6a4d021663 100644 --- a/apps/studio/src/stores/sync/sync-operations-slice.ts +++ b/apps/studio/src/stores/sync/sync-operations-slice.ts @@ -272,11 +272,15 @@ const createTypedAsyncThunk = createAsyncThunk.withTypes< { type CancelOperationPayload = { selectedSiteId: string; remoteSiteId: number; + displayNotification?: boolean; }; const cancelPushThunk = createTypedAsyncThunk( 'syncOperations/cancelPush', - async ( { selectedSiteId, remoteSiteId }: CancelOperationPayload, { dispatch } ) => { + async ( + { selectedSiteId, remoteSiteId, displayNotification = true }: CancelOperationPayload, + { dispatch } + ) => { const operationId = generateStateId( selectedSiteId, remoteSiteId ); const abortCallback = PUSH_SITE_ABORT_CALLBACKS.get( operationId ); @@ -291,16 +295,21 @@ const cancelPushThunk = createTypedAsyncThunk( } ) ); - getIpcApi().showNotification( { - title: __( 'Push cancelled' ), - body: __( 'The push operation has been cancelled.' ), - } ); + if ( displayNotification ) { + getIpcApi().showNotification( { + title: __( 'Push cancelled' ), + body: __( 'The push operation has been cancelled.' ), + } ); + } } ); const cancelPullThunk = createTypedAsyncThunk( 'syncOperations/cancelPull', - async ( { selectedSiteId, remoteSiteId }: CancelOperationPayload, { dispatch } ) => { + async ( + { selectedSiteId, remoteSiteId, displayNotification = true }: CancelOperationPayload, + { dispatch } + ) => { const operationId = generateStateId( selectedSiteId, remoteSiteId ); getIpcApi().cancelSyncOperation( operationId ); @@ -318,10 +327,12 @@ const cancelPullThunk = createTypedAsyncThunk( // Ignore errors if file doesn't exist } ); - getIpcApi().showNotification( { - title: __( 'Pull cancelled' ), - body: __( 'The pull operation has been cancelled.' ), - } ); + if ( displayNotification ) { + getIpcApi().showNotification( { + title: __( 'Pull cancelled' ), + body: __( 'The pull operation has been cancelled.' ), + } ); + } } ); From 625fc86ec076d30af7e42cd060ca6391243128bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20Cs=C3=A9csey?= Date: Thu, 19 Mar 2026 13:46:51 +0000 Subject: [PATCH 5/9] Use getOriginalState() in userLoggedOut listener to fix cancel thunksnot firing --- apps/studio/src/stores/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/studio/src/stores/index.ts b/apps/studio/src/stores/index.ts index 81bed1f229..f54c1f91b7 100644 --- a/apps/studio/src/stores/index.ts +++ b/apps/studio/src/stores/index.ts @@ -187,7 +187,8 @@ startAppListening( { startAppListening( { actionCreator: userLoggedOut, effect( action, listenerApi ) { - const state = listenerApi.getState(); + // Use getOriginalState() to read state BEFORE the reducer cleared pullStates/pushStates + const state = listenerApi.getOriginalState(); for ( const pullState of Object.values( state.syncOperations.pullStates ) ) { void store.dispatch( From 677d49d9756d3407a989d806eccf195d34d85299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20Cs=C3=A9csey?= Date: Thu, 19 Mar 2026 13:46:55 +0000 Subject: [PATCH 6/9] Clear wpcomClient before dispatching userLoggedOut to prevent error modal --- apps/studio/src/components/auth-provider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/src/components/auth-provider.tsx b/apps/studio/src/components/auth-provider.tsx index 30c5c30dc5..1a4ecdcd04 100644 --- a/apps/studio/src/components/auth-provider.tsx +++ b/apps/studio/src/components/auth-provider.tsx @@ -52,11 +52,11 @@ const AuthProvider: React.FC< AuthProviderProps > = ( { children } ) => { const handleInvalidToken = useCallback( async () => { try { void getIpcApi().logRendererMessage( 'info', 'Detected invalid token. Logging out.' ); + setWpcomClient( undefined ); dispatch( userLoggedOut() ); await getIpcApi().clearAuthenticationToken(); setIsAuthenticated( false ); setClient( undefined ); - setWpcomClient( undefined ); setUser( undefined ); } catch ( err ) { console.error( 'Failed to handle invalid token:', err ); @@ -113,11 +113,11 @@ const AuthProvider: React.FC< AuthProviderProps > = ( { children } ) => { } try { + setWpcomClient( undefined ); dispatch( userLoggedOut() ); await getIpcApi().clearAuthenticationToken(); setIsAuthenticated( false ); setClient( undefined ); - setWpcomClient( undefined ); setUser( undefined ); } catch ( err ) { console.error( err ); From 34bd922df9817ad702b56f1fbe6d5bbc30a734d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20Cs=C3=A9csey?= Date: Thu, 19 Mar 2026 14:08:37 +0000 Subject: [PATCH 7/9] keep connected sites cache on logout --- apps/studio/src/stores/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/studio/src/stores/index.ts b/apps/studio/src/stores/index.ts index f54c1f91b7..c7fe37aadf 100644 --- a/apps/studio/src/stores/index.ts +++ b/apps/studio/src/stores/index.ts @@ -211,7 +211,6 @@ startAppListening( { } // Reset authenticated RTK Query caches - listenerApi.dispatch( connectedSitesApi.util.resetApiState() ); listenerApi.dispatch( wpcomSitesApi.util.resetApiState() ); listenerApi.dispatch( wpcomApi.util.resetApiState() ); }, From 58e5ecf7b73ee74f77654584278f3ebe03856b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20Cs=C3=A9csey?= Date: Thu, 19 Mar 2026 14:31:41 +0000 Subject: [PATCH 8/9] Abort poller signals in cancel thunks to prevent error modals during logout Extracted PUSH_POLLERS/PULL_POLLERS maps and stop functions into a shared sync-pollers module. Cancel thunks now call stopPushPoller/stopPullPoller directly, so in-flight poll requests see signal.aborted and exit silently instead of showing error modals. --- apps/studio/src/stores/index.ts | 19 ++++++------------ .../src/stores/sync/sync-operations-slice.ts | 3 +++ apps/studio/src/stores/sync/sync-pollers.ts | 20 +++++++++++++++++++ 3 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 apps/studio/src/stores/sync/sync-pollers.ts diff --git a/apps/studio/src/stores/index.ts b/apps/studio/src/stores/index.ts index c7fe37aadf..a26045a842 100644 --- a/apps/studio/src/stores/index.ts +++ b/apps/studio/src/stores/index.ts @@ -33,6 +33,12 @@ import { syncOperationsSelectors, syncOperationsThunks, } from 'src/stores/sync/sync-operations-slice'; +import { + PUSH_POLLERS, + PULL_POLLERS, + stopPushPoller, + stopPullPoller, +} from 'src/stores/sync/sync-pollers'; import { wpcomSitesApi } from 'src/stores/sync/wpcom-sites'; import uiReducer from 'src/stores/ui-slice'; import { getWpcomClient, wpcomApi, wpcomPublicApi } from 'src/stores/wpcom-api'; @@ -219,9 +225,6 @@ startAppListening( { const PUSH_POLLING_KEYS = [ 'creatingRemoteBackup', 'applyingChanges', 'finishing' ]; const SYNC_POLLING_INTERVAL = 3000; -const PUSH_POLLERS = new Map< string, AbortController >(); -const PULL_POLLERS = new Map< string, AbortController >(); - function isPushPollable( selectedSiteId: string, remoteSiteId: number ) { const pushState = syncOperationsSelectors.selectPushState( selectedSiteId, @@ -238,16 +241,6 @@ function isPullPollable( selectedSiteId: string, remoteSiteId: number ) { return pullState?.status.key === 'in-progress' && !! pullState.backupId; } -function stopPushPoller( stateId: string ) { - PUSH_POLLERS.get( stateId )?.abort(); - PUSH_POLLERS.delete( stateId ); -} - -function stopPullPoller( stateId: string ) { - PULL_POLLERS.get( stateId )?.abort(); - PULL_POLLERS.delete( stateId ); -} - async function startPushPoller( selectedSiteId: string, remoteSiteId: number ) { const stateId = generateStateId( selectedSiteId, remoteSiteId ); if ( PUSH_POLLERS.has( stateId ) ) { diff --git a/apps/studio/src/stores/sync/sync-operations-slice.ts b/apps/studio/src/stores/sync/sync-operations-slice.ts index 40601bb71e..84fc4a3b2e 100644 --- a/apps/studio/src/stores/sync/sync-operations-slice.ts +++ b/apps/studio/src/stores/sync/sync-operations-slice.ts @@ -9,6 +9,7 @@ import { getHostnameFromUrl } from 'src/lib/url-utils'; import { store } from 'src/stores'; import { userLoggedOut } from 'src/stores/auth-actions'; import { connectedSitesApi } from 'src/stores/sync/connected-sites'; +import { stopPullPoller, stopPushPoller } from 'src/stores/sync/sync-pollers'; import type { PullStateProgressInfo, PushStateProgressInfo, @@ -311,6 +312,7 @@ const cancelPushThunk = createTypedAsyncThunk( const operationId = generateStateId( selectedSiteId, remoteSiteId ); const abortCallback = PUSH_SITE_ABORT_CALLBACKS.get( operationId ); + stopPushPoller( operationId ); abortCallback?.(); getIpcApi().cancelSyncOperation( operationId ); @@ -338,6 +340,7 @@ const cancelPullThunk = createTypedAsyncThunk( { dispatch } ) => { const operationId = generateStateId( selectedSiteId, remoteSiteId ); + stopPullPoller( operationId ); getIpcApi().cancelSyncOperation( operationId ); dispatch( diff --git a/apps/studio/src/stores/sync/sync-pollers.ts b/apps/studio/src/stores/sync/sync-pollers.ts new file mode 100644 index 0000000000..b924ccf181 --- /dev/null +++ b/apps/studio/src/stores/sync/sync-pollers.ts @@ -0,0 +1,20 @@ +/** + * Shared poller abort controllers for sync push/pull operations. + * + * Extracted into its own module so both the poller loops (stores/index.ts) + * and the cancel thunks (sync-operations-slice.ts) can abort in-flight + * poll requests without circular imports. + */ + +export const PUSH_POLLERS = new Map< string, AbortController >(); +export const PULL_POLLERS = new Map< string, AbortController >(); + +export function stopPushPoller( stateId: string ) { + PUSH_POLLERS.get( stateId )?.abort(); + PUSH_POLLERS.delete( stateId ); +} + +export function stopPullPoller( stateId: string ) { + PULL_POLLERS.get( stateId )?.abort(); + PULL_POLLERS.delete( stateId ); +} From e09fa0291f16134ef552448cf8d22422a9b9658c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20Cs=C3=A9csey?= Date: Fri, 20 Mar 2026 16:02:43 +0000 Subject: [PATCH 9/9] Move setWpcomClient into userLoggedOut listener --- apps/studio/src/components/auth-provider.tsx | 2 -- apps/studio/src/stores/index.ts | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/studio/src/components/auth-provider.tsx b/apps/studio/src/components/auth-provider.tsx index 1a4ecdcd04..4f614630e9 100644 --- a/apps/studio/src/components/auth-provider.tsx +++ b/apps/studio/src/components/auth-provider.tsx @@ -52,7 +52,6 @@ const AuthProvider: React.FC< AuthProviderProps > = ( { children } ) => { const handleInvalidToken = useCallback( async () => { try { void getIpcApi().logRendererMessage( 'info', 'Detected invalid token. Logging out.' ); - setWpcomClient( undefined ); dispatch( userLoggedOut() ); await getIpcApi().clearAuthenticationToken(); setIsAuthenticated( false ); @@ -113,7 +112,6 @@ const AuthProvider: React.FC< AuthProviderProps > = ( { children } ) => { } try { - setWpcomClient( undefined ); dispatch( userLoggedOut() ); await getIpcApi().clearAuthenticationToken(); setIsAuthenticated( false ); diff --git a/apps/studio/src/stores/index.ts b/apps/studio/src/stores/index.ts index a26045a842..0eee72d75f 100644 --- a/apps/studio/src/stores/index.ts +++ b/apps/studio/src/stores/index.ts @@ -41,7 +41,7 @@ import { } from 'src/stores/sync/sync-pollers'; import { wpcomSitesApi } from 'src/stores/sync/wpcom-sites'; import uiReducer from 'src/stores/ui-slice'; -import { getWpcomClient, wpcomApi, wpcomPublicApi } from 'src/stores/wpcom-api'; +import { getWpcomClient, setWpcomClient, wpcomApi, wpcomPublicApi } from 'src/stores/wpcom-api'; import { wordpressVersionsApi } from './wordpress-versions-api'; import type { SupportedLocale } from '@studio/common/lib/locale'; @@ -193,6 +193,8 @@ startAppListening( { startAppListening( { actionCreator: userLoggedOut, effect( action, listenerApi ) { + setWpcomClient( undefined ); + // Use getOriginalState() to read state BEFORE the reducer cleared pullStates/pushStates const state = listenerApi.getOriginalState();