Skip to content

Commit

Permalink
fix(sync): ocasionally marked sites as deleted (#749)
Browse files Browse the repository at this point in the history
  • Loading branch information
nightnei authored Dec 19, 2024
1 parent ce42b2a commit dc7ef13
Show file tree
Hide file tree
Showing 14 changed files with 175 additions and 187 deletions.
2 changes: 1 addition & 1 deletion src/components/content-tab-sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { PropsWithChildren, useState, useEffect } from 'react';
import { CLIENT_ID, PROTOCOL_PREFIX, SCOPES, WP_AUTHORIZE_ENDPOINT } from '../constants';
import { useSyncSites } from '../hooks/sync-sites';
import { useAuth } from '../hooks/use-auth';
import { SyncSite } from '../hooks/use-fetch-wpcom-sites';
import { useOffline } from '../hooks/use-offline';
import { getIpcApi } from '../lib/get-ipc-api';
import { ArrowIcon } from './arrow-icon';
Expand All @@ -16,6 +15,7 @@ import { SyncSitesModalSelector } from './sync-sites-modal-selector';
import { SyncTabImage } from './sync-tab-image';
import { Tooltip } from './tooltip';
import { WordPressShortLogo } from './wordpress-short-logo';
import type { SyncSite } from '../hooks/use-fetch-wpcom-sites/types';

function SiteSyncDescription( { children }: PropsWithChildren ) {
const { __ } = useI18n();
Expand Down
2 changes: 1 addition & 1 deletion src/components/sync-connected-sites.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { useMemo } from 'react';
import { STUDIO_DOCS_URL_GET_HELP_UNSUPPORTED_SITES } from '../constants';
import { useSyncSites } from '../hooks/sync-sites';
import { useConfirmationDialog } from '../hooks/use-confirmation-dialog';
import { SyncSite } from '../hooks/use-fetch-wpcom-sites';
import { useOffline } from '../hooks/use-offline';
import { useSyncStatesProgressInfo } from '../hooks/use-sync-states-progress-info';
import { cx } from '../lib/cx';
Expand All @@ -23,6 +22,7 @@ import ProgressBar from './progress-bar';
import { SyncPullPushClear } from './sync-pull-push-clear';
import { Tooltip, DynamicTooltip } from './tooltip';
import { WordPressLogoCircle } from './wordpress-logo-circle';
import type { SyncSite } from '../hooks/use-fetch-wpcom-sites/types';

interface ConnectedSiteSection {
id: number;
Expand Down
2 changes: 1 addition & 1 deletion src/components/sync-sites-modal-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Icon, SearchControl as SearchControlWp } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { useI18n } from '@wordpress/react-i18n';
import { useState, useEffect } from 'react';
import { SyncSite } from '../hooks/use-fetch-wpcom-sites';
import { useOffline } from '../hooks/use-offline';
import { cx } from '../lib/cx';
import { getIpcApi } from '../lib/get-ipc-api';
Expand All @@ -11,6 +10,7 @@ import Button from './button';
import { CreateButton } from './connect-create-buttons';
import Modal from './modal';
import offlineIcon from './offline-icon';
import type { SyncSite } from '../hooks/use-fetch-wpcom-sites/types';

const SearchControl = process.env.NODE_ENV === 'test' ? () => null : SearchControlWp;

Expand Down
2 changes: 1 addition & 1 deletion src/hooks/sync-sites/sync-sites-context.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { __, sprintf } from '@wordpress/i18n';
import React, { createContext, useCallback, useContext, useState } from 'react';
import { getIpcApi } from '../../lib/get-ipc-api';
import { SyncSite } from '../use-fetch-wpcom-sites';
import { useFormatLocalizedTimestamps } from '../use-format-localized-timestamps';
import { useListenDeepLinkConnection } from './use-listen-deep-link-connection';
import { UseSiteSyncManagement, useSiteSyncManagement } from './use-site-sync-management';
import { PullStates, UseSyncPull, useSyncPull } from './use-sync-pull';
import { PushStates, UseSyncPush, useSyncPush } from './use-sync-push';
import type { SyncSite } from '../use-fetch-wpcom-sites/types';

type GetLastSyncTimeText = ( timestamp: string | null, type: 'pull' | 'push' ) => string;
type UpdateSiteTimestamp = (
Expand Down
145 changes: 4 additions & 141 deletions src/hooks/sync-sites/use-site-sync-management.ts
Original file line number Diff line number Diff line change
@@ -1,103 +1,9 @@
import { useEffect, useCallback } from 'react';
import { getIpcApi } from '../../lib/get-ipc-api';
import { useAuth } from '../use-auth';
import { FetchSites, SyncSite, useFetchWpComSites } from '../use-fetch-wpcom-sites';
import { FetchSites, useFetchWpComSites } from '../use-fetch-wpcom-sites';
import { useSiteDetails } from '../use-site-details';

/**
* Generate updated site data to be stored in `appdata-v1.json` in three steps:
* 1. Update the list of `connectedSites` with fresh data (name, URL, etc)
* 2. Find any staging sites that have been added to an already connected site
* 3. Find any connected staging sites that have been deleted on WordPress.com
*
* We treat staging sites differently from production sites because users can't connect staging
* sites separately from production sites (they're always connected together). So, while deleted
* production sites are still rendered in the UI (with a "deleted" notice), we need to automatically
* keep the list of staging sites up-to-date, which is where `stagingSitesToAdd` and
* `stagingSitesToDelete` comes in.
*/
export const reconcileConnectedSites = (
connectedSites: SyncSite[],
freshWpComSites: SyncSite[]
): {
updatedConnectedSites: SyncSite[];
stagingSitesToAdd: SyncSite[];
stagingSitesToDelete: { id: number; localSiteId: string }[];
} => {
const updatedConnectedSites = connectedSites.map( ( connectedSite ): SyncSite => {
const site = freshWpComSites.find( ( site ) => site.id === connectedSite.id );

if ( ! site ) {
return {
...connectedSite,
syncSupport: 'deleted',
};
}

return {
...connectedSite,
name: site.name,
url: site.url,
syncSupport: site.syncSupport,
stagingSiteIds: site.stagingSiteIds,
};
}, [] );

const stagingSitesToAdd = connectedSites.flatMap( ( connectedSite ) => {
const updatedConnectedSite = updatedConnectedSites.find(
( site ) => site.id === connectedSite.id
);

if ( ! updatedConnectedSite?.stagingSiteIds.length ) {
return [];
}

const addedStagingSiteIds = updatedConnectedSite.stagingSiteIds.filter(
( id ) => ! connectedSite.stagingSiteIds.includes( id )
);

return addedStagingSiteIds.flatMap( ( id ): SyncSite[] => {
const freshSite = freshWpComSites.find( ( site ) => site.id === id );

if ( ! freshSite ) {
return [];
}

return [
{
...freshSite,
localSiteId: connectedSite.localSiteId,
syncSupport: 'already-connected',
},
];
}, [] );
} );

const stagingSitesToDelete = connectedSites.flatMap( ( connectedSite ) => {
const updatedConnectedSite = updatedConnectedSites.find(
( site ) => site.id === connectedSite.id
);

if ( ! connectedSite?.stagingSiteIds.length ) {
return [];
}

return connectedSite.stagingSiteIds
.filter( ( id ) => ! updatedConnectedSite?.stagingSiteIds.includes( id ) )
.map( ( id ) => {
return {
id,
localSiteId: connectedSite.localSiteId,
};
} );
} );

return {
updatedConnectedSites,
stagingSitesToAdd,
stagingSitesToDelete,
};
};
import type { SyncSite } from '../use-fetch-wpcom-sites/types';

type ConnectedSites = SyncSite[];
type LoadConnectedSites = () => Promise< void >;
Expand All @@ -124,7 +30,7 @@ export const useSiteSyncManagement = ( {
setConnectedSites,
}: UseSiteSyncManagementProps ): UseSiteSyncManagement => {
const { isAuthenticated } = useAuth();
const { syncSites, isFetching, isInitialized, refetchSites } = useFetchWpComSites(
const { syncSites, isFetching, refetchSites } = useFetchWpComSites(
connectedSites.map( ( { id } ) => id )
);
const { selectedSite } = useSiteDetails();
Expand All @@ -149,50 +55,7 @@ export const useSiteSyncManagement = ( {
if ( isAuthenticated ) {
loadConnectedSites();
}
}, [ isAuthenticated, loadConnectedSites ] );

// whenever array of syncSites changes, we need to update connectedSites to keep them updated with wordpress.com
useEffect( () => {
if ( isFetching || ! isAuthenticated || ! isInitialized ) {
return;
}

getIpcApi()
.getConnectedWpcomSites()
.then( async ( allConnectedSites ) => {
const { updatedConnectedSites, stagingSitesToAdd, stagingSitesToDelete } =
reconcileConnectedSites( allConnectedSites, syncSites );

await getIpcApi().updateConnectedWpcomSites( updatedConnectedSites );

if ( stagingSitesToDelete.length ) {
const data = stagingSitesToDelete.map( ( { id, localSiteId } ) => ( {
siteIds: [ id ],
localSiteId,
} ) );

await getIpcApi().disconnectWpcomSites( data );
}

if ( stagingSitesToAdd.length ) {
const data = stagingSitesToAdd.map( ( site ) => ( {
sites: [ site ],
localSiteId: site.localSiteId,
} ) );

await getIpcApi().connectWpcomSites( data );
}

loadConnectedSites();
} );
}, [
isAuthenticated,
syncSites,
isFetching,
isInitialized,
setConnectedSites,
loadConnectedSites,
] );
}, [ isAuthenticated, syncSites, loadConnectedSites ] );

const connectSite = useCallback< ConnectSite >(
async ( site, overrideLocalSiteId ) => {
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/sync-sites/use-sync-pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useI18n } from '@wordpress/react-i18n';
import { useCallback, useEffect, useMemo } from 'react';
import { getIpcApi } from '../../lib/get-ipc-api';
import { useAuth } from '../use-auth';
import { SyncSite } from '../use-fetch-wpcom-sites';
import { useImportExport } from '../use-import-export';
import { useSiteDetails } from '../use-site-details';
import { useSyncStatesProgressInfo, PullStateProgressInfo } from '../use-sync-states-progress-info';
Expand All @@ -15,6 +14,7 @@ import {
UpdateState,
usePullPushStates,
} from './use-pull-push-states';
import type { SyncSite } from '../use-fetch-wpcom-sites/types';

export type SyncBackupState = {
remoteSiteId: number;
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/sync-sites/use-sync-push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useCallback, useEffect, useMemo } from 'react';
import { SYNC_PUSH_SIZE_LIMIT_BYTES } from '../../constants';
import { getIpcApi } from '../../lib/get-ipc-api';
import { useAuth } from '../use-auth';
import { SyncSite } from '../use-fetch-wpcom-sites';
import { useSyncStatesProgressInfo, PushStateProgressInfo } from '../use-sync-states-progress-info';
import {
ClearState,
Expand All @@ -14,6 +13,7 @@ import {
UpdateState,
usePullPushStates,
} from './use-pull-push-states';
import type { SyncSite } from '../use-fetch-wpcom-sites/types';

export type SyncPushState = {
remoteSiteId: number;
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/tests/reconcile-connected-sites.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { reconcileConnectedSites } from '../sync-sites/use-site-sync-management';
import { SyncSite } from '../use-fetch-wpcom-sites';
import { reconcileConnectedSites } from '../use-fetch-wpcom-sites/reconcile-connected-sites';
import type { SyncSite } from '../use-fetch-wpcom-sites/types';

describe( 'reconcileConnectedSites', () => {
test( 'should update name, url, syncSupport properties', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,10 @@
import * as Sentry from '@sentry/electron/renderer';
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { useAuth } from './use-auth';
import { useOffline } from './use-offline';

type SyncSupport =
| 'unsupported'
| 'syncable'
| 'needs-transfer'
| 'already-connected'
| 'jetpack-site'
| 'deleted';

export type SyncSite = {
id: number;
localSiteId: string;
name: string;
url: string;
isStaging: boolean;
stagingSiteIds: number[];
syncSupport: SyncSupport;
lastPullTimestamp: string | null;
lastPushTimestamp: string | null;
};
import { getIpcApi } from '../../lib/get-ipc-api';
import { useAuth } from '../use-auth';
import { useOffline } from '../use-offline';
import { reconcileConnectedSites } from './reconcile-connected-sites';
import type { SyncSite, SyncSupport } from './types';

type SitesEndpointSite = {
ID: number;
Expand Down Expand Up @@ -116,14 +99,13 @@ function transformSiteResponse(

export type FetchSites = () => Promise< SitesEndpointSite[] >;

export const useFetchWpComSites = ( connectedSiteIds: number[] ) => {
export const useFetchWpComSites = ( connectedSiteIdsOnlyForSelectedSite: number[] ) => {
const [ rawSyncSites, setRawSyncSites ] = useState< SitesEndpointSite[] >( [] );
const { isAuthenticated, client } = useAuth();
const isFetchingSites = useRef( false );
const isInitialized = useRef( false ); // By default syncSites are always empty array, so this flag helps to determine if we have fetched sites at least once
const isOffline = useOffline();

const joinedConnectedSiteIds = connectedSiteIds.join( ',' );
const joinedConnectedSiteIds = connectedSiteIdsOnlyForSelectedSite.join( ',' );
// we need this trick to avoid unnecessary re-renders,
// as a result different instances of the same array don't trigger refetching
const memoizedConnectedSiteIds: number[] = useMemo(
Expand All @@ -142,6 +124,8 @@ export const useFetchWpComSites = ( connectedSiteIds: number[] ) => {
isFetchingSites.current = true;

try {
const allConnectedSites = await getIpcApi().getConnectedWpcomSites();

const response = await client.req.get< SitesEndpointResponse >(
{
apiNamespace: 'rest/v1.2',
Expand All @@ -156,7 +140,34 @@ export const useFetchWpComSites = ( connectedSiteIds: number[] ) => {
}
);

isInitialized.current = true;
const syncSites = transformSiteResponse(
response.sites,
allConnectedSites.map( ( { id } ) => id )
);

// whenever array of syncSites changes, we need to update connectedSites to keep them updated with wordpress.com
const { updatedConnectedSites, stagingSitesToAdd, stagingSitesToDelete } =
reconcileConnectedSites( allConnectedSites, syncSites );

await getIpcApi().updateConnectedWpcomSites( updatedConnectedSites );

if ( stagingSitesToDelete.length ) {
const data = stagingSitesToDelete.map( ( { id, localSiteId } ) => ( {
siteIds: [ id ],
localSiteId,
} ) );

await getIpcApi().disconnectWpcomSites( data );
}

if ( stagingSitesToAdd.length ) {
const data = stagingSitesToAdd.map( ( site ) => ( {
sites: [ site ],
localSiteId: site.localSiteId,
} ) );

await getIpcApi().connectWpcomSites( data );
}

setRawSyncSites( response.sites );

Expand All @@ -174,19 +185,14 @@ export const useFetchWpComSites = ( connectedSiteIds: number[] ) => {
fetchSites();
}, [ fetchSites ] );

const refetchSites = useCallback( () => {
return fetchSites();
}, [ fetchSites ] );

const syncSites = useMemo(
const syncSitesWithSyncSupportForSelectedSite = useMemo(
() => transformSiteResponse( rawSyncSites, memoizedConnectedSiteIds ),
[ rawSyncSites, memoizedConnectedSiteIds ]
);

return {
syncSites,
syncSites: syncSitesWithSyncSupportForSelectedSite,
isFetching: isFetchingSites.current,
isInitialized: isInitialized.current,
refetchSites,
refetchSites: fetchSites,
};
};
Loading

0 comments on commit dc7ef13

Please sign in to comment.