Skip to content

Conversation

@gcsecsey
Copy link
Contributor

@gcsecsey gcsecsey commented Nov 7, 2025

Related issues

Fixes STU-711

Proposed Changes

  • Removed SyncSitesProvider completely
  • Added a new syncOperations Redux slice
  • Moved logic from use-sync-pull and use-sync-push hooks to Redux thunks
  • Kept the hooks as think wrappers of the tunks
  • Removed some unused callback props

Testing Instructions

  • Run npm start
  • Go to the Sync tab
  • Connect a site
  • Pull changes from the site and check that there are no regressions
  • Push changes to the site and check that there are no regressions

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

@gcsecsey gcsecsey requested a review from a team November 7, 2025 12:12
@github-actions
Copy link
Contributor

github-actions bot commented Nov 7, 2025

📊 Performance Test Results

Comparing fb2546e vs trunk

site-editor

Metric trunk fb2546e Diff Change
load 8207.00 ms 6809.00 ms -1398.00 ms 🟢 -17.0%

site-startup

Metric trunk fb2546e Diff Change
siteCreation 9069.00 ms 9086.00 ms +17.00 ms 🔴 0.2%
siteStartup 3953.00 ms 3956.00 ms +3.00 ms 🔴 0.1%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change

Copy link
Contributor

@nightnei nightnei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Push: it seems it stucks at "Creating backup..." step. Tried a few times and also tried in trunk - worked quickly for me.

Pull: not related to the changes, since I reproduce the error in trunk too, but if you are working there - take a look please at this error when pull is finished, maybe it's a quck win:

Screenshot 2025-11-18 at 19 07 39

@gcsecsey
Copy link
Contributor Author

Push: it seems it stucks at "Creating backup..." step. Tried a few times and also tried in trunk - worked quickly for me.

Pull: not related to the changes, since I reproduce the error in trunk too, but if you are working there - take a look please at this error when pull is finished, maybe it's a quck win: Screenshot 2025-11-18 at 19 07 39

Thanks for reviewing this @nightnei! I took another look at this, and found that earlier we used a ref to keep track of the state, which let us read it back immediately after updating it. For now, I added the ref to the hooks that were previously in the usePullPushStates hook.

I'm also testing a simpler approach that updated the state objects directly where needed. If it works reliably, I'll update the PR with this approach.

@fredrikekelund
Copy link
Contributor

@gcsecsey, I would challenge us to remove src/hooks/sync-sites/sync-sites-context.tsx completely as part of this refactor.

With this change, we are preserving functions like updatePushState in the React context, even though they are now essentially Redux action creators (or thunks, I guess). The logic around sync operations would be much easier to follow if we moved src/hooks/sync-sites/use-sync-push.ts and src/hooks/sync-sites/use-sync-pull.ts into src/stores/sync/sync-operations-slice.ts.

@gcsecsey
Copy link
Contributor Author

@gcsecsey, I would challenge us to remove src/hooks/sync-sites/sync-sites-context.tsx completely as part of this refactor.

With this change, we are preserving functions like updatePushState in the React context, even though they are now essentially Redux action creators (or thunks, I guess). The logic around sync operations would be much easier to follow if we moved src/hooks/sync-sites/use-sync-push.ts and src/hooks/sync-sites/use-sync-pull.ts into src/stores/sync/sync-operations-slice.ts.

That's a good point @fredrikekelund, thanks! I'll try to move as much as possible to the Redux slice, and ping for another review.

@gcsecsey gcsecsey marked this pull request as draft December 17, 2025 14:30
Remove the SyncSitesProvider React context layer and migrate all
components to use useSyncPull and useSyncPush hooks directly.

Changes:
- Extract getLastSyncTimeText to useLastSyncTimeText hook
- Extract initialization logic to useInitializeSyncStates hook
- Move useListenDeepLinkConnection and initialization to App component
- Update all components to use hooks directly instead of context
- Remove sync-sites-context.tsx
…dio-refactor-pull-and-push-states-from-syncsitesprovider
…dio-refactor-pull-and-push-states-from-syncsitesprovider
@gcsecsey
Copy link
Contributor Author

I carried out the changes to move most of the logic into thunks within src/stores/sync/sync-operations-slice.ts. I experimented with removing the src/hooks/sync-sites/use-sync-push.ts and src/hooks/sync-sites/use-sync-pull.ts hooks completely too, and update the components to directly use the thunks, but I found this needed quite a bit of boilerplate in each component, so for the time being, I kept the hooks as thin wrappers.

@fredrikekelund could you please take another look when you're back? Thanks!

@gcsecsey gcsecsey marked this pull request as ready for review December 23, 2025 15:11
Copy link
Contributor

@nightnei nightnei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing feedback, works well now, no regressions 👍

…s-from-syncsitesprovider

Resolve merge conflicts:
- use-sync-push.ts: Keep Redux-based approach with thunks and useSyncPolling
- use-add-site.test.tsx: Update mock to use separate useSyncPull hook
- sync-connected-sites.tsx: Merge isUploadingPaused feature with optional chaining
- index.test.tsx: Use useSyncPull mock instead of useSyncSites
- add-site.test.tsx: Update mock for separate hook pattern

Also fix:
- sync-operations-slice.ts: Add missing selectedSite.id parameter to pushArchive call

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@gcsecsey
Copy link
Contributor Author

gcsecsey commented Jan 5, 2026

I tested the changes again after resolving conflicts, the pull/push functionality still works as before. We can proceed with merging this as soon as the project process is 🟢 again.

@fredrikekelund
Copy link
Contributor

Trunk is open for merge again. I'll happily review this PR this afternoon, though

@gcsecsey
Copy link
Contributor Author

gcsecsey commented Jan 6, 2026

Trunk is open for merge again. I'll happily review this PR this afternoon, though

Thanks @fredrikekelund, that'd be great!

@fredrikekelund
Copy link
Contributor

Looking into this now. At first glance, I'm a little skeptical to keeping src/hooks/sync-sites/use-sync-pull.ts and src/hooks/sync-sites/use-sync-push.ts as thin wrappers, but maybe I'll be swayed when looking more closely at the code.

To me, the end goal is to move "business logic" into Redux slices and have components focused on consuming the Redux-derived state, with as little data crunching as possible. The useSyncPull and useSyncPush hooks don't exactly contradict that, so maybe it just irks me that they seem like leftovers at first glance…

Anyway, you mentioned the need for additional boilerplate in components if we removed those hooks, @gcsecsey. Is that stuff like useAppDispatch and useAuth calls, or something else? The boilerplate in useSyncPull and useSyncPush looks manageable to me, if that's anything to judge by.

@gcsecsey
Copy link
Contributor Author

gcsecsey commented Jan 6, 2026

Looking into this now. At first glance, I'm a little skeptical to keeping src/hooks/sync-sites/use-sync-pull.ts and src/hooks/sync-sites/use-sync-push.ts as thin wrappers, but maybe I'll be swayed when looking more closely at the code.

To me, the end goal is to move "business logic" into Redux slices and have components focused on consuming the Redux-derived state, with as little data crunching as possible. The useSyncPull and useSyncPush hooks don't exactly contradict that, so maybe it just irks me that they seem like leftovers at first glance…

Anyway, you mentioned the need for additional boilerplate in components if we removed those hooks, @gcsecsey. Is that stuff like useAppDispatch and useAuth calls, or something else? The boilerplate in useSyncPull and useSyncPush looks manageable to me, if that's anything to judge by.

Yes, exactly, I wanted to avoid having to add these as dependencies to each component. I think the boilerplate would be the useAppDispatch, useAuth, and pushStatesProgressInfo hooks, as these are needed when dispatching the pullSite or pushSite thunks currently. I also migrated some functions like isAnySitePushing to selectors, and the hook now just wraps this call to avoid the useRootSelector dependency on each call site.

Copy link
Contributor

@fredrikekelund fredrikekelund left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for not following up on my review last week, @gcsecsey. I'm coming back to this now!

I think there's a solid foundation in sync-operations-slice.ts, but there's room to simplify the implementation, not least by following some Redux patterns more closely.

I'll submit multiple reviews to make it a bit easier to digest. Starting with sync-operations-slice.ts and pushSiteThunk primarily.

Comment on lines +91 to +101
const isKeyCancelled = ( key: string | undefined ): boolean => {
return key === 'cancelled';
};

const isKeyFailed = ( key: string | undefined ): boolean => {
return key === 'failed';
};

const isKeyFinished = ( key: string | undefined ): boolean => {
return key === 'finished';
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions shouldn't be needed in this file. We should just rely on well-typed string props and compare against known values. I.e. state.pullStates[ key ].status.key already has an enum type.

Comment on lines +257 to +262
dispatch(
syncOperationsActions.clearPushState( { selectedSiteId: selectedSite.id, remoteSiteId } )
);
void dispatch(
syncOperationsThunks.clearPushState( { selectedSiteId: selectedSite.id, remoteSiteId } )
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clearPushState thunk could also be responsible for updating the state. Seems logical to consolidate that and not depend on the consumer to remember to also update the state if calling the thunk.

Comment on lines +265 to +277
updatePushStateWithIpc(
dispatch,
selectedSite.id,
remoteSiteId,
{
remoteSiteId,
status: pushStatesProgressInfo.creatingBackup,
selectedSite,
remoteSiteUrl,
},
isKeyFailed,
isKeyFinished
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
updatePushStateWithIpc(
dispatch,
selectedSite.id,
remoteSiteId,
{
remoteSiteId,
status: pushStatesProgressInfo.creatingBackup,
selectedSite,
remoteSiteUrl,
},
isKeyFailed,
isKeyFinished
);
dispatch(
syncOperationsActions.updatePushState( {
selectedSiteId: selectedSite.id,
remoteSiteId,
state: {
remoteSiteId,
status: pushStatesProgressInfo.creatingBackup,
selectedSite,
remoteSiteUrl,
},
} )
);

A couple of things here:

  1. Let's remove updatePushStateWithIpc.
  2. The calls to getIpcApi().addSyncOperation and getIpcApi().clearSyncOperation should be an effect of state updates, they shouldn't have to be managed by whoever is updating the state. In other words, we should subscribe to state changes and call getIpcApi().addSyncOperation and getIpcApi().clearSyncOperation as needed.
  3. Let's simplify the signature of updatePushState. Passing selectedSiteId and state.selectedSite, remoteSiteId twice, and remoteSiteUrl twice seems redundant. updatePushState could be smarter internally about passing the right data to the right place.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subscribe to state changes

Here's an example of that:

// Save chat messages to local storage
listenerMiddleware.startListening( {
predicate( action, currentState, previousState ) {
return currentState.chat.messagesDict !== previousState.chat.messagesDict;
},
effect( action, listenerApi ) {
const state = listenerApi.getState();
localStorage.setItem(
LOCAL_STORAGE_CHAT_MESSAGES_KEY,
JSON.stringify( state.chat.messagesDict )
);
},
} );

Comment on lines +279 to +318
let archivePath: string, archiveSizeInBytes: number;

try {
const result = await getIpcApi().exportSiteForPush( selectedSite.id, operationId, {
optionsToSync: options?.optionsToSync,
specificSelectionPaths: options?.specificSelectionPaths,
} );
( { archivePath, archiveSizeInBytes } = result );
} catch ( error ) {
if ( error instanceof Error && error.message === 'Export aborted' ) {
updatePushStateWithIpc(
dispatch,
selectedSite.id,
remoteSiteId,
{ status: pushStatesProgressInfo.cancelled },
isKeyFailed,
isKeyFinished
);
throw error; // Signal cancellation
}

Sentry.captureException( error );
updatePushStateWithIpc(
dispatch,
selectedSite.id,
remoteSiteId,
{ status: pushStatesProgressInfo.failed },
isKeyFailed,
isKeyFinished
);
getIpcApi().showErrorMessageBox( {
title: sprintf( __( 'Error pushing to %s' ), connectedSite.name ),
message: __(
'An error occurred while pushing the site. If this problem persists, please contact support.'
),
error,
showOpenLogs: true,
} );
throw error;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let archivePath: string, archiveSizeInBytes: number;
try {
const result = await getIpcApi().exportSiteForPush( selectedSite.id, operationId, {
optionsToSync: options?.optionsToSync,
specificSelectionPaths: options?.specificSelectionPaths,
} );
( { archivePath, archiveSizeInBytes } = result );
} catch ( error ) {
if ( error instanceof Error && error.message === 'Export aborted' ) {
updatePushStateWithIpc(
dispatch,
selectedSite.id,
remoteSiteId,
{ status: pushStatesProgressInfo.cancelled },
isKeyFailed,
isKeyFinished
);
throw error; // Signal cancellation
}
Sentry.captureException( error );
updatePushStateWithIpc(
dispatch,
selectedSite.id,
remoteSiteId,
{ status: pushStatesProgressInfo.failed },
isKeyFailed,
isKeyFinished
);
getIpcApi().showErrorMessageBox( {
title: sprintf( __( 'Error pushing to %s' ), connectedSite.name ),
message: __(
'An error occurred while pushing the site. If this problem persists, please contact support.'
),
error,
showOpenLogs: true,
} );
throw error;
}
const { archivePath, archiveSizeInBytes } = await getIpcApi().exportSiteForPush(
selectedSite.id,
operationId,
{
optionsToSync: options?.optionsToSync,
specificSelectionPaths: options?.specificSelectionPaths,
}
);

Let's unwrap this:

  1. The updatePushStateWithIpc calls could be moved to extraReducers. We would use builder.addCase( pushSiteThunk.rejected to listen for failures.
  2. The error modal (as triggered by getIpcApi().showErrorMessageBox) is also a side effect of the state update. Let's try moving it to a state subscription callback.

Comment on lines +330 to +335
getIpcApi().showErrorMessageBox( {
title: sprintf( __( 'Error pushing to %s' ), connectedSite.name ),
message: __(
'The site is too large to push. Please reduce the size of the site and try again.'
),
} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
getIpcApi().showErrorMessageBox( {
title: sprintf( __( 'Error pushing to %s' ), connectedSite.name ),
message: __(
'The site is too large to push. Please reduce the size of the site and try again.'
),
} );

This call isn't particularly bulky, but if we consider the error modals a side effect of state updates elsewhere (i.e., that they should be triggered by a state subscription listener), then let's be consistent in that approach.

Comment on lines +340 to +353
// Check if cancelled before upload
const state = getState();
const currentPushState = syncOperationsSelectors.selectPushState(
selectedSite.id,
remoteSiteId
)( state );
if (
! currentPushState ||
! currentPushState.status ||
isKeyCancelled( currentPushState.status.key )
) {
await getIpcApi().removeExportedSiteTmpFile( archivePath );
throw new Error( 'Push cancelled' );
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use something more similar to this approach? https://redux-toolkit.js.org/api/createAsyncThunk#canceling-while-running

Comment on lines +374 to +384
// Check if cancelled after upload
const stateAfterUpload = getState();
const pushStateAfterUpload = syncOperationsSelectors.selectPushState(
selectedSite.id,
remoteSiteId
)( stateAfterUpload );

if ( isKeyCancelled( pushStateAfterUpload?.status.key ) ) {
await getIpcApi().removeExportedSiteTmpFile( archivePath );
throw new Error( 'Push cancelled' );
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my previous comment about aborting the thunk.

remoteSiteUrl,
};
} else {
throw response;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw response;
throw new Error( response.error );

This seems more appropriate.

Comment on lines +411 to +419
Sentry.captureException( error );
updatePushStateWithIpc(
dispatch,
selectedSite.id,
remoteSiteId,
{ status: pushStatesProgressInfo.failed },
isKeyFailed,
isKeyFinished
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to an extraReducers handler.

Comment on lines +420 to +423
getIpcApi().showErrorMessageBox( {
title: sprintf( __( 'Error pushing to %s' ), connectedSite.name ),
message: getErrorFromResponse( error ),
} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to a state subscription handler.

@gcsecsey
Copy link
Contributor Author

gcsecsey commented Jan 14, 2026

Thanks @fredrikekelund for following up with the review! 🙌 First and foremost, I'll try to resolve all conflicts with trunk, then I'll start working on these changes.

Copy link
Contributor

@fredrikekelund fredrikekelund left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments related to pullSiteThunk

Comment on lines +457 to +463
// Clear existing state
dispatch(
syncOperationsActions.clearPullState( { selectedSiteId: selectedSite.id, remoteSiteId } )
);
void dispatch(
syncOperationsThunks.clearPullState( { selectedSiteId: selectedSite.id, remoteSiteId } )
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, let's make the clearPullState thunk update the state. Using the extraReducers API might be the best way to accomplish that.

Comment on lines +466 to +479
dispatch(
syncOperationsActions.updatePullState( {
selectedSiteId: selectedSite.id,
remoteSiteId,
state: {
backupId: null,
status: pullStatesProgressInfo[ 'in-progress' ],
downloadUrl: null,
remoteSiteId,
remoteSiteUrl,
selectedSite,
},
} )
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as for the updatePushState action creator, let's see if we can simplify the function signature to reduce the repetition here.

Comment on lines +481 to +483
// Add sync operation for tracking
const stateId = generateStateId( selectedSite.id, remoteSiteId );
getIpcApi().addSyncOperation( stateId );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to a state subscription handler.

Comment on lines +523 to +532
Sentry.captureException( error );
dispatch(
syncOperationsActions.updatePullState( {
selectedSiteId: selectedSite.id,
remoteSiteId,
state: {
status: pullStatesProgressInfo.failed,
},
} )
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to an extraReducers callback.

Comment on lines +534 to +537
getIpcApi().showErrorMessageBox( {
title: sprintf( __( 'Error pulling from %s' ), connectedSite.name ),
message: __( 'Studio was unable to connect to WordPress.com. Please try again.' ),
} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to a state subscription handler.

Comment on lines +617 to +618
let status: PushStateProgressInfo = pushStatesProgressInfo.creatingRemoteBackup;
if ( response.success && response.status === 'finished' ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let status: PushStateProgressInfo = pushStatesProgressInfo.creatingRemoteBackup;
if ( response.success && response.status === 'finished' ) {
if ( ! response.success ) {
throw new Error( response.error );
}
let status: PushStateProgressInfo;
switch ( response.status ) {
case 'initial_backup_started':
status = pushStatesProgressInfo.creatingRemoteBackup;
break;
case 'finished':

Let's make this a switch statement and add a case for initial_backup_started. This will allow TS to understand that we've exhausted all possible values of response.status when we call getPushStatusWithProgress

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT, the getPushStatusWithProgress function exported by this hook isn't used anywhere. Let's remove it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for getPullStatusWithProgress

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And for getBackupStatusWithProgress

Copy link
Contributor

@fredrikekelund fredrikekelund Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think we could remove isKeyPulling, isKeyPushing, isKeyFinished, isKeyFailed, isKeyCancelled, and isKeyUploadingPaused. They're only used in src/modules/sync/components/sync-connected-sites.tsx and could easily be replaced with plain equality comparisons.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isKeyImporting isn't used anywhere. Let's remove it.

Comment on lines +628 to +635
getIpcApi().showNotification( {
title: currentPushState.selectedSite.name,
body: sprintf(
// translators: %s is the site url without the protocol.
__( '%s has been updated' ),
getHostnameFromUrl( currentPushState.remoteSiteUrl )
),
} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side effect. Let's try to move this to a state subscription listener.

Comment on lines +544 to +579
// Helper function to calculate push status with progress (inlined from useSyncStatesProgressInfo)
const getPushStatusWithProgress = (
status: PushStateProgressInfo,
response: ImportResponse,
pushStatesProgressInfo: Record< PushStateProgressInfo[ 'key' ], PushStateProgressInfo >
): PushStateProgressInfo => {
if ( status.key === pushStatesProgressInfo.creatingRemoteBackup.key ) {
const progressRange =
pushStatesProgressInfo.applyingChanges.progress -
pushStatesProgressInfo.creatingRemoteBackup.progress;

// This step will increase the progress to the next step progressively based on the backup_progress
return {
...status,
progress:
pushStatesProgressInfo.creatingRemoteBackup.progress +
progressRange * ( response.backup_progress / 100 ),
};
}

// This step will increase the progress to the next step progressively based on the import_progress
if (
status.key === pushStatesProgressInfo.applyingChanges.key &&
response.import_progress < 100
) {
const progressRange =
pushStatesProgressInfo.finishing.progress - pushStatesProgressInfo.applyingChanges.progress;
return {
...status,
progress:
pushStatesProgressInfo.applyingChanges.progress +
progressRange * ( response.import_progress / 100 ),
};
}
return status;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's inline this logic in pollPushProgressThunk. I believe it will end up shorter and easier to read.

Comment on lines +674 to +681
updatePushStateWithIpc(
dispatch,
selectedSiteId,
remoteSiteId,
{ status },
isKeyFailed,
isKeyFinished
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
updatePushStateWithIpc(
dispatch,
selectedSiteId,
remoteSiteId,
{ status },
isKeyFailed,
isKeyFinished
);
dispatch(
syncOperationsActions.updatePushState( {
selectedSiteId,
remoteSiteId,
state: { status },
} )
);

Again, let's remove updatePushStateWithIpc

Copy link
Contributor

@fredrikekelund fredrikekelund left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments related to pollPullBackupThunk and completePullThunk

Comment on lines +728 to +741
// Check if state exists and is not cancelled
const state = getState();
const currentPullState = syncOperationsSelectors.selectPullState(
selectedSiteId,
remoteSiteId
)( state );

if (
! currentPullState ||
! currentPullState.status ||
isKeyCancelled( currentPullState.status.key )
) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as for pollPushProgressThunk, we should trigger (or perhaps register/unregister a setInterval/setTimeout callback) in a state subscription callback. If we do that, we should be able to remove this logic that starts the thunk by checking if we should cancel the operation.

Comment on lines +762 to +770
// Backup completed, trigger completion thunk
await dispatch(
syncOperationsThunks.completePull( {
selectedSiteId,
remoteSiteId,
downloadUrl,
pullStatesProgressInfo,
} )
).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

completePull doesn't actually stand on its own legs. I.e., this is the only place we call that thunk. I argue we should inline the logic from this function into this thunk. It'll clarify the overall API in this file.

Comment on lines +690 to +712
// Helper function to calculate backup status with progress (inlined from useSyncStatesProgressInfo)
const getBackupStatusWithProgress = (
hasBackupCompleted: boolean,
pullStatesProgressInfo: Record< PullStateProgressInfo[ 'key' ], PullStateProgressInfo >,
response: SyncBackupResponse
): PullStateProgressInfo => {
const frontendStatus = hasBackupCompleted
? pullStatesProgressInfo.downloading.key
: response.status;
let newProgressInfo: PullStateProgressInfo | null = null;
if ( response.status === 'in-progress' ) {
newProgressInfo = {
...pullStatesProgressInfo[ frontendStatus ],
// Update progress from the initial value to the new step proportionally to the response.progress
// on every update of the response.progress
progress:
IN_PROGRESS_INITIAL_VALUE + IN_PROGRESS_TO_DOWNLOADING_STEP * ( response.percent / 100 ),
};
}
const statusWithProgress = newProgressInfo || pullStatesProgressInfo[ frontendStatus ];

return statusWithProgress;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as with getPushStatusWithProgress, I would just inline this logic in pollPullBackupThunk.

Comment on lines +794 to +797
} catch ( error ) {
console.error( 'Failed to fetch backup status:', error );
throw error;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this error handling to extraReducers

Comment on lines +790 to +792
// Update IPC sync operation
const stateId = generateStateId( selectedSiteId, remoteSiteId );
getIpcApi().addSyncOperation( stateId );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side effect. Let's move to a state subscription callback.

Comment on lines +888 to +901
// Check if cancelled after download
const stateAfterDownload = getState();
const pullStateAfterDownload = syncOperationsSelectors.selectPullState(
selectedSiteId,
remoteSiteId
)( stateAfterDownload );

if (
! pullStateAfterDownload ||
! pullStateAfterDownload.status ||
isKeyCancelled( pullStateAfterDownload.status.key )
) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, let's try this strategy for aborting thunks.

Comment on lines +948 to +956
// Show notification
getIpcApi().showNotification( {
title: selectedSite.name,
body: sprintf(
// translators: %s is the site url without the protocol.
__( 'Studio site has been updated from %s' ),
getHostnameFromUrl( remoteSiteUrl )
),
} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side effect. Let's move to a state subscription listener.

Comment on lines +975 to +984
Sentry.captureException( error );
dispatch(
syncOperationsActions.updatePullState( {
selectedSiteId,
remoteSiteId,
state: {
status: pullStatesProgressInfo.failed,
},
} )
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to extraReducers. Again, using a notation like this: builder.addCase( pollPullBackupThunk.rejected

Comment on lines +985 to +988
getIpcApi().showErrorMessageBox( {
title: sprintf( __( 'Error pulling from %s' ), selectedSite.name ),
message: __( 'Failed to check backup file size. Please try again.' ),
} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side effect. Let's move to a state subscription handler.

Comment on lines +957 to +973
} catch ( error ) {
console.error( 'Backup completion failed:', error );

// Check if cancelled
const errorState = getState();
const pullStateOnError = syncOperationsSelectors.selectPullState(
selectedSiteId,
remoteSiteId
)( errorState );

if (
pullStateOnError &&
pullStateOnError.status &&
isKeyCancelled( pullStateOnError.status.key )
) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this. Errors can be handled per the strategy outlined below.

…dio-refactor-pull-and-push-states-from-syncsitesprovider
Migrate the changes of PR #2332
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this a thunk in src/stores/sync/sync-operations-slice.ts. Because the auth details are still not stored in Redux, we can dispatch the thunk from src/components/app.tsx and pass the client from the useAuth return value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can trigger the polling thunks from state subscription callbacks, then we should be able to remove this hook.

@fredrikekelund
Copy link
Contributor

If we move the logic for triggering polling thunks to state subscription callbacks, that should unblock us from removing useSyncPull and useSyncPush, as the remaining functions exported from those hooks are basically just Redux wrappers.

Let's focus on simplifying sync-operations-slice.ts and implementing the patterns I suggested in my reviews. Hopefully, that will unlock relatively straightforward removals of those hooks 🤞

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants