Skip to content

Commit 18a6b06

Browse files
committed
fix: increase deploy preview polling and cancel on unmount
With editorial workflow, saving remounts the EditorToolbar component (navigates to the unpublished entry view), making componentDidMount the primary polling trigger rather than componentDidUpdate. The previous default of 3 attempts (15s) was too short for CI pipelines to finish building and posting a commit status. Changes: - Increase componentDidMount polling to 24 attempts (~2 min) - Cancel in-flight polling on component unmount via AbortSignal - Cancel previous poll before starting a new one on subsequent saves - Thread AbortSignal through action creator to backend polling loop - Add 5 tests for polling behavior and cancellation
1 parent 61d8b8b commit 18a6b06

File tree

4 files changed

+75
-6
lines changed

4 files changed

+75
-6
lines changed

packages/decap-cms-core/src/actions/deploys.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,17 @@ export function loadDeployPreview(
5555
slug: string,
5656
entry: Entry,
5757
published: boolean,
58-
opts?: { maxAttempts?: number; interval?: number },
58+
opts?: { maxAttempts?: number; interval?: number; signal?: AbortSignal },
5959
) {
6060
return async (dispatch: ThunkDispatch<State, undefined, AnyAction>, getState: () => State) => {
6161
const state = getState();
6262
const backend = currentBackend(state.config);
6363
const collectionName = collection.get('name');
6464

65-
// Exit if currently fetching
65+
// Exit if currently fetching, unless the caller provides a signal
66+
// (indicating it manages cancellation of the previous poll externally).
6667
const deployState = selectDeployPreview(state, collectionName, slug);
67-
if (deployState && deployState.isFetching) {
68+
if (deployState && deployState.isFetching && !opts?.signal) {
6869
return;
6970
}
7071

packages/decap-cms-core/src/backend.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1046,7 +1046,11 @@ export class Backend {
10461046
collection: Collection,
10471047
slug: string,
10481048
entry: EntryMap,
1049-
{ maxAttempts = 1, interval = 5000 } = {},
1049+
{
1050+
maxAttempts = 1,
1051+
interval = 5000,
1052+
signal,
1053+
}: { maxAttempts?: number; interval?: number; signal?: AbortSignal } = {},
10501054
) {
10511055
/**
10521056
* If the registered backend does not provide a `getDeployPreview` method, or
@@ -1063,6 +1067,9 @@ export class Backend {
10631067
let deployPreview,
10641068
count = 0;
10651069
while (!deployPreview && count < maxAttempts) {
1070+
if (signal?.aborted) {
1071+
return;
1072+
}
10661073
count++;
10671074
deployPreview = await this.implementation.getDeployPreview(collection.get('name'), slug);
10681075
if (!deployPreview) {

packages/decap-cms-core/src/components/Editor/EditorToolbar.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,17 +303,32 @@ export class EditorToolbar extends React.Component {
303303

304304
const { isNewEntry, loadDeployPreview } = this.props;
305305
if (!isNewEntry) {
306-
loadDeployPreview({ maxAttempts: 3 });
306+
// 24 attempts × 5s interval = ~2 min polling window.
307+
// With editorial workflow, saving remounts the component (navigates to
308+
// the unpublished entry view), so componentDidMount is the primary
309+
// polling trigger — not componentDidUpdate.
310+
this._pollController = new AbortController();
311+
loadDeployPreview({ maxAttempts: 24, signal: this._pollController.signal });
307312
}
308313
}
309314

310315
componentDidUpdate(prevProps) {
311316
const { isNewEntry, isPersisting, loadDeployPreview } = this.props;
312317
if (!isNewEntry && prevProps.isPersisting && !isPersisting) {
313-
loadDeployPreview({ maxAttempts: 3 });
318+
// Abort any in-flight poll before starting a new one.
319+
this._pollController?.abort();
320+
this._pollController = new AbortController();
321+
// Fires on subsequent saves when the component survives (no remount).
322+
// In editorial workflow the first save remounts, so this mainly
323+
// covers the second-save-and-beyond case.
324+
loadDeployPreview({ maxAttempts: 3, signal: this._pollController.signal });
314325
}
315326
}
316327

328+
componentWillUnmount() {
329+
this._pollController?.abort();
330+
}
331+
317332
renderSimpleControls = () => {
318333
const { collection, hasChanged, isNewEntry, showDelete, onDelete, t } = this.props;
319334
const canCreate = collection.get('create');

packages/decap-cms-core/src/components/Editor/__tests__/EditorToolbar.spec.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,50 @@ describe('EditorToolbar', () => {
117117
expect(asFragment()).toMatchSnapshot();
118118
});
119119
});
120+
121+
describe('deploy preview polling', () => {
122+
it('should poll with maxAttempts: 24 and an AbortSignal on mount for existing entries', () => {
123+
render(<EditorToolbar {...props} isNewEntry={false} />);
124+
expect(props.loadDeployPreview).toHaveBeenCalledTimes(1);
125+
const opts = props.loadDeployPreview.mock.calls[0][0];
126+
expect(opts.maxAttempts).toBe(24);
127+
expect(opts.signal).toBeInstanceOf(AbortSignal);
128+
});
129+
130+
it('should not poll on mount for new entries', () => {
131+
render(<EditorToolbar {...props} isNewEntry={true} />);
132+
expect(props.loadDeployPreview).not.toHaveBeenCalled();
133+
});
134+
135+
it('should poll with maxAttempts: 3 after a save completes', () => {
136+
const { rerender } = render(<EditorToolbar {...props} isPersisting={true} />);
137+
props.loadDeployPreview.mockClear();
138+
rerender(<EditorToolbar {...props} isPersisting={false} />);
139+
expect(props.loadDeployPreview).toHaveBeenCalledTimes(1);
140+
const opts = props.loadDeployPreview.mock.calls[0][0];
141+
expect(opts.maxAttempts).toBe(3);
142+
expect(opts.signal).toBeInstanceOf(AbortSignal);
143+
});
144+
145+
it('should abort polling on unmount', () => {
146+
const { unmount } = render(<EditorToolbar {...props} isNewEntry={false} />);
147+
const signal = props.loadDeployPreview.mock.calls[0][0].signal;
148+
expect(signal.aborted).toBe(false);
149+
unmount();
150+
expect(signal.aborted).toBe(true);
151+
});
152+
153+
it('should abort previous poll when a new save triggers a new poll', () => {
154+
const { rerender } = render(<EditorToolbar {...props} isPersisting={false} />);
155+
const firstSignal = props.loadDeployPreview.mock.calls[0][0].signal;
156+
157+
// Simulate save completing
158+
rerender(<EditorToolbar {...props} isPersisting={true} />);
159+
rerender(<EditorToolbar {...props} isPersisting={false} />);
160+
161+
expect(firstSignal.aborted).toBe(true);
162+
const secondSignal = props.loadDeployPreview.mock.calls[1][0].signal;
163+
expect(secondSignal.aborted).toBe(false);
164+
});
165+
});
120166
});

0 commit comments

Comments
 (0)