Skip to content

Commit 80de4b9

Browse files
committed
Fix push
1 parent 38ae719 commit 80de4b9

11 files changed

Lines changed: 175 additions & 21 deletions

File tree

‎apps/local/src/index.ts‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,10 +1212,22 @@ export async function startLocalServer( options: LocalServerOptions ): Promise<
12121212
executeCliCommand: execute,
12131213
accessToken: token.accessToken,
12141214
emit: ( output ) =>
1215-
sseSend( {
1216-
channel: 'sync',
1217-
payload: { ...output, siteId: req.params.id, remoteSiteId },
1218-
} ),
1215+
sseSend(
1216+
output.kind === 'import-progress'
1217+
? {
1218+
channel: 'sync-push',
1219+
payload: {
1220+
status: output.status,
1221+
progress: output.progress,
1222+
siteId: req.params.id,
1223+
remoteSiteId,
1224+
},
1225+
}
1226+
: {
1227+
channel: 'sync',
1228+
payload: { ...output, siteId: req.params.id, remoteSiteId },
1229+
}
1230+
),
12191231
},
12201232
{ sitePath: site.path, remoteSiteId, options }
12211233
);

‎apps/studio/src/ipc-utils.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { AgentRunEvent } from '@studio/common/ai/agent-events';
88
import type { AiSessionPlacementUpdatedEvent } from '@studio/common/ai/sessions/placement';
99
import type { RemoteSessionStatus } from '@studio/common/lib/remote-session';
1010
import type { StoredAuthToken } from '@studio/common/lib/shared-config';
11-
import type { PullSiteProgress } from '@studio/common/types/sync';
11+
import type { PullSiteProgress, PushImportProgress } from '@studio/common/types/sync';
1212

1313
type SnapshotEventData = {
1414
action: PreviewCommandLoggerAction;
@@ -40,6 +40,7 @@ export interface IpcEvents {
4040
'sync-upload-progress': [ { selectedSiteId: string; remoteSiteId: number; progress: number } ];
4141
'sync-upload-manually-paused': [ { selectedSiteId: string; remoteSiteId: number } ];
4242
'sync-pull-progress': [ PullSiteProgress & { siteId: string } ];
43+
'sync-push-progress': [ PushImportProgress & { siteId: string; remoteSiteId: number } ];
4344
'snapshot-error': [ { operationId: crypto.UUID; data: SnapshotEventData } ];
4445
'snapshot-fatal-error': [ { operationId: crypto.UUID; data: { message: string } } ];
4546
'snapshot-output': [ { operationId: crypto.UUID; data: SnapshotEventData } ];

‎apps/studio/src/modules/sync/lib/ipc-handlers.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,13 @@ export async function pushSiteToLive(
592592
selectedSiteId,
593593
remoteSiteId,
594594
} );
595+
} else if ( output.kind === 'import-progress' ) {
596+
void sendIpcEventToRenderer( 'sync-push-progress', {
597+
siteId: selectedSiteId,
598+
remoteSiteId,
599+
status: output.status,
600+
progress: output.progress,
601+
} );
595602
}
596603
},
597604
},

‎apps/ui/src/data/core/connectors/ipc/index.ts‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
LoadedAiSession,
2121
AppUpdateStatus,
2222
ProposedSitePath,
23+
PushImportProgress,
2324
QuitSitesBehavior,
2425
SelectedSiteFolder,
2526
SiteDetails,
@@ -543,12 +544,29 @@ export function createIpcConnector(): Connector {
543544
);
544545
},
545546

546-
async pushSiteToLive( siteId, remoteSiteId, options ): Promise< void > {
547+
async pushSiteToLive( siteId, remoteSiteId, options, onImportProgress ): Promise< void > {
547548
// The agentic UI pushes via the shared `pushSite` (export → TUS
548549
// upload → import) in both desktop and `studio ui`; the desktop runs
549-
// it behind this single IPC handler. Resolves once the import is
550-
// initiated (the remote import may still be running).
551-
await ipcApi.pushSiteToLive( siteId, remoteSiteId, options );
550+
// it behind this single IPC handler. Resolves once the remote import
551+
// has fully finished; import progress streams in between.
552+
const unsubscribe = onImportProgress
553+
? ipcListener.subscribe(
554+
'sync-push-progress',
555+
( _event: unknown, payload: { siteId: string; status: string; progress: number } ) => {
556+
if ( payload.siteId === siteId ) {
557+
onImportProgress( {
558+
status: payload.status as PushImportProgress[ 'status' ],
559+
progress: payload.progress,
560+
} );
561+
}
562+
}
563+
)
564+
: undefined;
565+
try {
566+
await ipcApi.pushSiteToLive( siteId, remoteSiteId, options );
567+
} finally {
568+
unsubscribe?.();
569+
}
552570
await markConnectedWpcomSiteSynced( siteId, remoteSiteId, 'push' );
553571
},
554572

‎apps/ui/src/data/core/connectors/local/index.ts‎

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
ProposedSitePath,
2121
QuitSitesBehavior,
2222
PullSiteProgress,
23+
PushImportProgress,
2324
SelectedSiteFolder,
2425
SiteDetails,
2526
Snapshot,
@@ -65,6 +66,11 @@ type SnapshotSseOutput =
6566
| { kind: 'success'; operationId: string }
6667
| { kind: 'output' | 'error'; operationId: string };
6768

69+
type PushProgressSseOutput = PushImportProgress & {
70+
siteId: string;
71+
remoteSiteId: number;
72+
};
73+
6874
type PullProgressSseOutput = PullSiteProgress & {
6975
siteId: string;
7076
remoteSiteId: number;
@@ -78,6 +84,7 @@ type ServerEvent =
7884
| { channel: 'placement'; payload: AiSessionPlacementUpdatedEvent }
7985
| { channel: 'snapshot'; payload: SnapshotSseOutput }
8086
| { channel: 'sync-pull'; payload: PullProgressSseOutput }
87+
| { channel: 'sync-push'; payload: PushProgressSseOutput }
8188
| { channel: 'import'; payload: ImportSseOutput }
8289
| { channel: 'sync-connect'; payload: { remoteSiteId: number; studioSiteId: string } };
8390

@@ -102,6 +109,7 @@ export function createLocalConnector( { apiBaseUrl }: LocalConnectorOptions ): C
102109
const placementListeners = new Set< ( event: AiSessionPlacementUpdatedEvent ) => void >();
103110
const snapshotListeners = new Set< ( output: SnapshotSseOutput ) => void >();
104111
const pullProgressListeners = new Set< ( output: PullProgressSseOutput ) => void >();
112+
const pushProgressListeners = new Set< ( output: PushProgressSseOutput ) => void >();
105113
const importListeners = new Set< ( output: ImportSseOutput ) => void >();
106114
const syncConnectListeners = new Set<
107115
( event: { remoteSiteId: number; studioSiteId: string } ) => void
@@ -226,6 +234,8 @@ export function createLocalConnector( { apiBaseUrl }: LocalConnectorOptions ): C
226234
snapshotListeners.forEach( ( listener ) => listener( parsed.payload ) );
227235
} else if ( parsed.channel === 'sync-pull' ) {
228236
pullProgressListeners.forEach( ( listener ) => listener( parsed.payload ) );
237+
} else if ( parsed.channel === 'sync-push' ) {
238+
pushProgressListeners.forEach( ( listener ) => listener( parsed.payload ) );
229239
} else if ( parsed.channel === 'import' ) {
230240
importListeners.forEach( ( listener ) => listener( parsed.payload ) );
231241
} else if ( parsed.channel === 'sync-connect' ) {
@@ -573,11 +583,23 @@ export function createLocalConnector( { apiBaseUrl }: LocalConnectorOptions ): C
573583
method: 'POST',
574584
} );
575585
},
576-
async pushSiteToLive( siteId, remoteSiteId, options ) {
577-
await api( `/sites/${ encodeURIComponent( siteId ) }/push`, {
578-
method: 'POST',
579-
body: JSON.stringify( { remoteSiteId, options } ),
580-
} );
586+
async pushSiteToLive( siteId, remoteSiteId, options, onImportProgress ) {
587+
const listener = ( output: PushProgressSseOutput ) => {
588+
if ( output.siteId === siteId ) {
589+
onImportProgress?.( { status: output.status, progress: output.progress } );
590+
}
591+
};
592+
if ( onImportProgress ) {
593+
pushProgressListeners.add( listener );
594+
}
595+
try {
596+
await api( `/sites/${ encodeURIComponent( siteId ) }/push`, {
597+
method: 'POST',
598+
body: JSON.stringify( { remoteSiteId, options } ),
599+
} );
600+
} finally {
601+
pushProgressListeners.delete( listener );
602+
}
581603
},
582604
async pullSiteFromLive( siteId, remoteSiteId, onProgress, options ) {
583605
const listener = ( output: PullProgressSseOutput ) => {

‎apps/ui/src/data/core/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type {
1616
ProposedSitePath,
1717
PullSiteProgress,
1818
PullSyncOptions,
19+
PushImportProgress,
1920
PushSyncOptions,
2021
QuitSitesBehavior,
2122
SelectedSiteFolder,

‎apps/ui/src/data/core/types.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type { Snapshot } from '@studio/common/types/snapshot';
1616
import type {
1717
PullSiteProgress,
1818
PullSyncOptions,
19+
PushImportProgress,
1920
PushSyncOptions,
2021
SyncSite,
2122
} from '@studio/common/types/sync';
@@ -42,6 +43,7 @@ export type { Snapshot } from '@studio/common/types/snapshot';
4243
export type {
4344
PullSiteProgress,
4445
PullSyncOptions,
46+
PushImportProgress,
4547
PushSyncOptions,
4648
SyncSite,
4749
} from '@studio/common/types/sync';
@@ -278,11 +280,14 @@ export interface Connector {
278280
disconnectWpcomSite( localSiteId: string, remoteSiteId: number ): Promise< void >;
279281
// Pushes the local site to a previously connected WordPress.com site.
280282
// Replaces the remote contents with the local database and wp-content,
281-
// or only the selection described by `options` when provided.
283+
// or only the selection described by `options` when provided. Resolves
284+
// once the remote import has fully finished; `onImportProgress` reports
285+
// the remote backup/import phases in between.
282286
pushSiteToLive(
283287
siteId: string,
284288
remoteSiteId: number,
285-
options?: PushSyncOptions
289+
options?: PushSyncOptions,
290+
onImportProgress?: ( progress: PushImportProgress ) => void
286291
): Promise< void >;
287292
// Pulls the connected WordPress.com site's database + wp-content back
288293
// into the local Studio site, or only the selection described by

‎apps/ui/src/data/queries/use-sync-site.ts‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import {
1010
reportSyncProgress,
1111
reportSyncSuccess,
1212
} from '@/data/sync-activity';
13-
import type { PullSiteProgress, PullSyncOptions, PushSyncOptions } from '@/data/core';
13+
import type {
14+
PullSiteProgress,
15+
PullSyncOptions,
16+
PushImportProgress,
17+
PushSyncOptions,
18+
} from '@/data/core';
1419

1520
// Mutation keys are exported so downstream consumers (e.g. a cross-page
1621
// activity indicator or future bulk-sync UI) can filter the react-query
@@ -24,13 +29,30 @@ type PushToLiveVariables = {
2429
options?: PushSyncOptions;
2530
};
2631

32+
// The remote applies a push in two phases (safety backup, then import); the
33+
// dropdown shows these while the push mutation stays pending.
34+
function getImportProgressMessage( status: PushImportProgress[ 'status' ] ): string {
35+
if ( status === 'finished' ) {
36+
return __( 'Finishing…' );
37+
}
38+
if ( status.startsWith( 'archive_import' ) ) {
39+
return __( 'Applying changes to the live site…' );
40+
}
41+
return __( 'Backing up the live site…' );
42+
}
43+
2744
export function usePushSiteToLive() {
2845
const connector = useConnector();
2946
const queryClient = useQueryClient();
3047
return useMutation( {
3148
mutationKey: PUSH_TO_LIVE_MUTATION_KEY,
3249
mutationFn: ( { siteId, remoteSiteId, options }: PushToLiveVariables ) =>
33-
connector.pushSiteToLive( siteId, remoteSiteId, options ),
50+
connector.pushSiteToLive( siteId, remoteSiteId, options, ( importProgress ) => {
51+
reportSyncProgress( siteId, 'push', {
52+
message: getImportProgressMessage( importProgress.status ),
53+
progress: importProgress.progress,
54+
} );
55+
} ),
3456
onMutate: ( { siteId } ) => {
3557
reportSyncPending( siteId, 'push' );
3658
},

‎apps/ui/src/data/sync-activity.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function reportSyncPending( siteId: string, direction: SyncDirection ): v
6262

6363
export function reportSyncProgress(
6464
siteId: string,
65-
direction: Extract< SyncDirection, 'pull' >,
65+
direction: Extract< SyncDirection, 'pull' | 'push' >,
6666
progress: PullSiteProgress
6767
): void {
6868
clearExpiryTimer( siteId );

‎packages/common/sites/sync.ts‎

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import crypto from 'node:crypto';
22
import fs from 'node:fs';
33
import os from 'node:os';
44
import path from 'node:path';
5-
import { initiateImport } from '@studio/common/lib/sync/sync-api';
5+
import {
6+
SYNC_MAX_STALLED_ATTEMPTS,
7+
SYNC_POLL_INTERVAL_MS,
8+
} from '@studio/common/lib/sync/constants';
9+
import { initiateImport, pollImportStatus } from '@studio/common/lib/sync/sync-api';
610
import { createTusUpload } from '@studio/common/lib/sync/tus-upload';
711
import type { ExecuteCliCommand } from '@studio/common/lib/cli-process';
812
import type {
913
PullSiteProgress,
1014
PullSyncOptions,
15+
PushImportProgress,
1116
PushSyncOptions,
1217
SyncOption,
1318
} from '@studio/common/types/sync';
@@ -22,7 +27,8 @@ import type {
2227
export type PushOutput =
2328
| { kind: 'upload-progress'; progress: number }
2429
| { kind: 'network-paused'; error: string }
25-
| { kind: 'resumed' };
30+
| { kind: 'resumed' }
31+
| ( { kind: 'import-progress' } & PushImportProgress );
2632

2733
export interface PushSiteContext {
2834
executeCliCommand: ExecuteCliCommand;
@@ -77,11 +83,63 @@ export async function pushSite(
7783
const attachmentId = await promise;
7884

7985
await initiateImport( ctx.accessToken, params.remoteSiteId, attachmentId, params.options );
86+
87+
await waitForImportCompletion( ctx, params.remoteSiteId );
8088
} finally {
8189
await fs.promises.rm( dir, { recursive: true, force: true } ).catch( () => undefined );
8290
}
8391
}
8492

93+
// Polls the remote import status after initiation until the import finishes,
94+
// mirroring the classic renderer's push lifecycle so callers only resolve once
95+
// the live site has actually been updated. Emits overall progress: the remote
96+
// backup phase maps to 0-50 and the import phase to 50-100.
97+
async function waitForImportCompletion(
98+
ctx: PushSiteContext,
99+
remoteSiteId: number
100+
): Promise< void > {
101+
let consecutiveErrors = 0;
102+
103+
for ( let attempt = 0; attempt < SYNC_MAX_STALLED_ATTEMPTS; attempt++ ) {
104+
await new Promise( ( resolve ) => setTimeout( resolve, SYNC_POLL_INTERVAL_MS ) );
105+
106+
let response;
107+
try {
108+
response = await pollImportStatus( ctx.accessToken, remoteSiteId );
109+
consecutiveErrors = 0;
110+
} catch ( error ) {
111+
// Tolerate transient network/API errors while the remote works.
112+
if ( ++consecutiveErrors >= 5 ) {
113+
throw error;
114+
}
115+
continue;
116+
}
117+
118+
if ( response.status === 'failed' ) {
119+
throw new Error( response.error || 'Import failed on the remote site.' );
120+
}
121+
if ( ! response.success ) {
122+
throw new Error( 'Import failed on the remote site.' );
123+
}
124+
125+
let progress = 0;
126+
if ( response.status === 'finished' ) {
127+
progress = 100;
128+
} else if ( response.status.startsWith( 'archive_import' ) ) {
129+
progress = 50 + ( response.import_progress ?? 0 ) / 2;
130+
} else {
131+
progress = ( response.backup_progress ?? 0 ) / 2;
132+
}
133+
ctx.emit?.( { kind: 'import-progress', status: response.status, progress } );
134+
135+
if ( response.status === 'finished' ) {
136+
return;
137+
}
138+
}
139+
140+
throw new Error( 'Import timed out' );
141+
}
142+
85143
// Mirrors the legacy renderer's export-mode derivation
86144
// (apps/studio/src/modules/sync/lib/ipc-handlers.ts `exportSiteForPush`):
87145
// no options (or 'all') exports the full site.

0 commit comments

Comments
 (0)