Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Defensively keep polling when a sync-status poll reads finished before the milestone write lands (100% progress, milestone still unset) instead of treating it as a stall. Not reachable on the default full-sync path; guards the legacy queue-based path against a one-poll false "Sync has stalled" error.
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,41 @@ describe( 'useSyncStatus', () => {
expect( result.current.error?.message ).toBe( 'down' );
} );

it( 'keeps polling when a poll reports finished before the milestone, then completes', async () => {
// Read gap: the first poll sees `finished` + 100% progress while the
// milestone is still 0. It must NOT surface a stall; the next poll exposes
// the milestone and the sync completes.
mockFetch.mockResolvedValueOnce(
rawStatus( {
finished: true,
progress: { woocommerce_analytics: { sent: 2, total: 2 } },
initial_full_sync_finished: 0,
} )
);
mockFetch.mockResolvedValue(
rawStatus( {
finished: true,
progress: { woocommerce_analytics: { sent: 2, total: 2 } },
initial_full_sync_finished: 1_700_000_000,
} )
);

const { result } = renderHook( () => useSyncStatus() );

// First poll: finishing — 100% progress, no stall, no error, polling continues.
await waitFor( () => expect( result.current.data?.percentage ).toBe( 100 ) );
expect( result.current.isStalled ).toBe( false );
expect( result.current.error ).toBeNull();

// Next tick exposes the milestone ⇒ complete.
await act( async () => {
jest.advanceTimersByTime( POLL_INTERVAL );
} );
await waitFor( () => expect( result.current.isComplete ).toBe( true ) );
expect( result.current.isStalled ).toBe( false );
expect( result.current.error ).toBeNull();
} );

it( 'updates the milestone live from the sync-status poll', async () => {
// Milestone unset at page load; the backend then exposes it on the poll.
mockFetch.mockResolvedValue( rawStatus( { initial_full_sync_finished: 1_700_000_500 } ) );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,29 @@ describe( 'isSyncStalled', () => {
} )
).toBe( false );
} );

it( 'is not stalled while finishing — 100% progress with the milestone write still in flight', () => {
// A poll can read `finished: true` before the milestone is persisted; that
// surfaces as 100% progress with the milestone unset. The sync is finishing,
// not stalled, and resolves on the next poll.
expect(
isSyncStalled( {
isStarted: true,
isRunning: false,
percentage: 100,
initialFullSyncFinished: 0,
} )
).toBe( false );
} );

it( 'stays stalled when finished below 100% (a genuine stall, not the read gap)', () => {
expect(
isSyncStalled( {
isStarted: true,
isRunning: false,
percentage: 50,
initialFullSyncFinished: 0,
} )
).toBe( true );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,15 @@ export function isSyncComplete( status: SyncStatus ): boolean {
/**
* Stalled = the sync started but is no longer running and hasn't completed. A
* sync that never started is NOT stalled — it just needs to be triggered.
*
* 100% progress with the milestone still unset means the sync is *finishing*,
* not stalled: a poll read `finished` before the milestone write landed. Keep
* polling so it self-heals on the next tick. A genuine stall sits below 100%.
*
* @param status - Normalized sync status.
* @return Whether the sync has stalled.
*/
export function isSyncStalled( status: SyncStatus ): boolean {
return status.isStarted && ! status.isRunning && ! isSyncComplete( status );
const isFinishing = status.percentage >= 100 && status.initialFullSyncFinished === 0;
return status.isStarted && ! status.isRunning && ! isSyncComplete( status ) && ! isFinishing;
}
Comment on lines +58 to 60
Loading