diff --git a/projects/js-packages/publicize-components/changelog/fix-parallel-social-connections-requests-mixing-up b/projects/js-packages/publicize-components/changelog/fix-parallel-social-connections-requests-mixing-up new file mode 100644 index 0000000000000..c190e0e2e868d --- /dev/null +++ b/projects/js-packages/publicize-components/changelog/fix-parallel-social-connections-requests-mixing-up @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Social | Fixed parallel social connection requests messing up the UI state diff --git a/projects/js-packages/publicize-components/src/social-store/actions/connection-data.js b/projects/js-packages/publicize-components/src/social-store/actions/connection-data.js index 304dd43fc9a18..5e2c7ec0aae9d 100644 --- a/projects/js-packages/publicize-components/src/social-store/actions/connection-data.js +++ b/projects/js-packages/publicize-components/src/social-store/actions/connection-data.js @@ -14,6 +14,9 @@ import { TOGGLE_CONNECTIONS_MODAL, UPDATE_CONNECTION, UPDATING_CONNECTION, + REQUEST_TYPE_REFRESH_CONNECTIONS, + ADD_ABORT_CONTROLLER, + REMOVE_ABORT_CONTROLLERS, } from './constants'; /** @@ -107,6 +110,64 @@ export function mergeConnections( freshConnections ) { }; } +/** + * Create an abort controller. + * @param {AbortController} abortController - Abort controller. + * @param {string} requestType - Type of abort request. + * + * @returns {object} - an action object. + */ +export function createAbortController( abortController, requestType ) { + return { + type: ADD_ABORT_CONTROLLER, + requestType, + abortController, + }; +} + +/** + * Remove abort controllers. + * + * @param {string} requestType - Type of abort request. + * + * @returns {object} - an action object. + */ +export function removeAbortControllers( requestType ) { + return { + type: REMOVE_ABORT_CONTROLLERS, + requestType, + }; +} + +/** + * Abort a request. + * + * @param {string} requestType - Type of abort request. + * + * @returns {Function} - a function to abort a request. + */ +export function abortRequest( requestType ) { + return function ( { dispatch, select } ) { + const abortControllers = select.getAbortControllers( requestType ); + + for ( const controller of abortControllers ) { + controller.abort(); + } + + // Remove the abort controllers. + dispatch( removeAbortControllers( requestType ) ); + }; +} + +/** + * Abort the refresh connections request. + * + * @returns {Function} - a function to abort a request. + */ +export function abortRefreshConnectionsRequest() { + return abortRequest( REQUEST_TYPE_REFRESH_CONNECTIONS ); +} + /** * Effect handler which will refresh the connection test results. * @@ -118,7 +179,20 @@ export function refreshConnectionTestResults( syncToMeta = false ) { try { const path = select.connectionRefreshPath() || '/wpcom/v2/publicize/connection-test-results'; - const freshConnections = await apiFetch( { path } ); + // Wait until all connections are done updating/deleting. + while ( + select.getUpdatingConnections().length > 0 || + select.getDeletingConnections().length > 0 + ) { + await new Promise( resolve => setTimeout( resolve, 100 ) ); + } + + const abortController = new AbortController(); + + dispatch( createAbortController( abortController, REQUEST_TYPE_REFRESH_CONNECTIONS ) ); + + // Pass the abort controller signal to the fetch request. + const freshConnections = await apiFetch( { path, signal: abortController.signal } ); dispatch( mergeConnections( freshConnections ) ); @@ -126,7 +200,11 @@ export function refreshConnectionTestResults( syncToMeta = false ) { dispatch( syncConnectionsToPostMeta() ); } } catch ( e ) { - // Do nothing. + // If the request was aborted. + if ( 'AbortError' === e.name ) { + // Fire it again to run after the current operation that cancelled the request. + dispatch( refreshConnectionTestResults( syncToMeta ) ); + } } }; } @@ -210,6 +288,9 @@ export function deleteConnectionById( { connectionId, showSuccessNotice = true } try { const path = `/jetpack/v4/social/connections/${ connectionId }`; + // Abort the refresh connections request. + dispatch( abortRefreshConnectionsRequest() ); + dispatch( deletingConnection( connectionId ) ); await apiFetch( { method: 'DELETE', path } ); @@ -269,6 +350,9 @@ export function createConnection( data, optimisticData = {} ) { ...optimisticData, } ) ); + // Abort the refresh connections request. + dispatch( abortRefreshConnectionsRequest() ); + // Mark the connection as updating to show the spinner. dispatch( updatingConnection( tempId ) ); @@ -383,6 +467,9 @@ export function updateConnectionById( connectionId, data ) { try { const path = `/jetpack/v4/social/connections/${ connectionId }`; + // Abort the refresh connections request. + dispatch( abortRefreshConnectionsRequest() ); + // Optimistically update the connection. dispatch( updateConnection( connectionId, data ) ); diff --git a/projects/js-packages/publicize-components/src/social-store/actions/constants.ts b/projects/js-packages/publicize-components/src/social-store/actions/constants.ts index 6e2a9d56b686d..634acf1a23fdd 100644 --- a/projects/js-packages/publicize-components/src/social-store/actions/constants.ts +++ b/projects/js-packages/publicize-components/src/social-store/actions/constants.ts @@ -17,3 +17,11 @@ export const SET_RECONNECTING_ACCOUNT = 'SET_RECONNECTING_ACCOUNT'; export const SET_KEYRING_RESULT = 'SET_KEYRING_RESULT'; export const TOGGLE_CONNECTIONS_MODAL = 'TOGGLE_CONNECTIONS_MODAL'; + +export const ADD_ABORT_CONTROLLER = 'ADD_ABORT_CONTROLLER'; + +export const REMOVE_ABORT_CONTROLLERS = 'REMOVE_ABORT_CONTROLLERS'; + +export const REQUEST_TYPE_DEFAULT = 'DEFAULT'; + +export const REQUEST_TYPE_REFRESH_CONNECTIONS = 'REFRESH_CONNECTIONS'; diff --git a/projects/js-packages/publicize-components/src/social-store/reducer/connection-data.js b/projects/js-packages/publicize-components/src/social-store/reducer/connection-data.js index 0deb7fe39ae5e..b99683e85605e 100644 --- a/projects/js-packages/publicize-components/src/social-store/reducer/connection-data.js +++ b/projects/js-packages/publicize-components/src/social-store/reducer/connection-data.js @@ -9,6 +9,9 @@ import { TOGGLE_CONNECTIONS_MODAL, UPDATE_CONNECTION, UPDATING_CONNECTION, + ADD_ABORT_CONTROLLER, + REMOVE_ABORT_CONTROLLERS, + REQUEST_TYPE_DEFAULT, } from '../actions/constants'; /** @@ -92,6 +95,33 @@ const connectionData = ( state = {}, action ) => { }; } + case ADD_ABORT_CONTROLLER: { + const requestType = action.requestType || REQUEST_TYPE_DEFAULT; + + return { + ...state, + abortControllers: { + ...state.abortControllers, + [ requestType ]: [ + ...( state.abortControllers?.[ requestType ] || [] ), + action.abortController, + ], + }, + }; + } + + case REMOVE_ABORT_CONTROLLERS: { + const requestType = action.requestType || REQUEST_TYPE_DEFAULT; + + return { + ...state, + abortControllers: { + ...state.abortControllers, + [ requestType ]: [], + }, + }; + } + case SET_KEYRING_RESULT: return { ...state, diff --git a/projects/js-packages/publicize-components/src/social-store/selectors/connection-data.js b/projects/js-packages/publicize-components/src/social-store/selectors/connection-data.js index f51d4d29b006c..a9427c8333c78 100644 --- a/projects/js-packages/publicize-components/src/social-store/selectors/connection-data.js +++ b/projects/js-packages/publicize-components/src/social-store/selectors/connection-data.js @@ -1,3 +1,5 @@ +import { REQUEST_TYPE_DEFAULT } from '../actions/constants'; + /** * Returns the connections list from the store. * @@ -161,6 +163,18 @@ export function getReconnectingAccount( state ) { return state.connectionData?.reconnectingAccount ?? ''; } +/** + * Get the abort controllers for a specific request type. + * + * @param {import("../types").SocialStoreState} state - State object. + * @param {string} requestType - The request type. + * + * @returns {Array} The abort controllers. + */ +export function getAbortControllers( state, requestType = REQUEST_TYPE_DEFAULT ) { + return state.connectionData?.abortControllers?.[ requestType ] ?? []; +} + /** * Whether a mastodon account is already connected. *