fix(api): 180s publish timeout, "Still publishing..." after 30s
Save/load keep 30s; publish of a large site legitimately runs longer and was reported as failed at 30s while the server kept going. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6d6994f009
commit
ff826a03c4
4 files changed
+38
-5
No files matched your search
@@ -23,7 +23,7 @@ import { EditorConfigProvider } from '../state/EditorConfigContext';
|
||||
import { SaveGuardProvider, useSaveGuard } from '../state/SaveGuardContext';
|
||||
import { PageProvider } from '../state/PageContext';
|
||||
import { SiteDesignProvider } from '../state/SiteDesignContext';
|
||||
import { useWhpApi, API_TIMEOUT_MS } from './useWhpApi';
|
||||
import { useWhpApi, API_TIMEOUT_MS, PUBLISH_TIMEOUT_MS } from './useWhpApi';
|
||||
import { WhpConfig } from '../types';
|
||||
|
||||
const whpConfig: WhpConfig = {
|
||||
@@ -119,10 +119,19 @@ describe('request timeout (item 11)', () => {
|
||||
await act(async () => { void api.publish().then((r) => { res = r; }); });
|
||||
await flush();
|
||||
expect(calls('publish')).toHaveLength(1);
|
||||
// A large site legitimately takes longer than a save: 30s is NOT a failure.
|
||||
await act(async () => { vi.advanceTimersByTime(API_TIMEOUT_MS); });
|
||||
await flush();
|
||||
expect(res).toBe('pending');
|
||||
await act(async () => { vi.advanceTimersByTime(PUBLISH_TIMEOUT_MS - API_TIMEOUT_MS); });
|
||||
await flush();
|
||||
expect(res).toMatchObject({ success: false, error: 'timeout' });
|
||||
expect(res.message).toMatch(/^Publish failed: the server did not respond/);
|
||||
expect(res.message).toMatch(/^Publish failed: the server did not respond within 180 seconds/);
|
||||
});
|
||||
|
||||
test('timeouts: save/load 30s, publish 180s', () => {
|
||||
expect(API_TIMEOUT_MS).toBe(30_000);
|
||||
expect(PUBLISH_TIMEOUT_MS).toBe(180_000);
|
||||
});
|
||||
|
||||
test('a hung load rejects with a readable error', async () => {
|
||||
|
||||
@@ -263,8 +263,10 @@ export interface ApiResult {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** How long save / publish / load may take before the request is abandoned. */
|
||||
/** How long save / load may take before the request is abandoned. */
|
||||
export const API_TIMEOUT_MS = 30_000;
|
||||
/** Publish compiles and copies every page and asset: a large site legitimately takes minutes. */
|
||||
export const PUBLISH_TIMEOUT_MS = 180_000;
|
||||
|
||||
/** A request that got no complete answer within API_TIMEOUT_MS. */
|
||||
export class ApiTimeoutError extends Error {}
|
||||
@@ -527,7 +529,7 @@ export function useWhpApi() {
|
||||
},
|
||||
);
|
||||
return readApiResult(resp, 'Publish');
|
||||
});
|
||||
}, PUBLISH_TIMEOUT_MS);
|
||||
} catch (e) {
|
||||
if (e instanceof ApiTimeoutError) return timeoutResult(e);
|
||||
throw e;
|
||||
|
||||
@@ -344,6 +344,22 @@ describe('TopBar search button (#49 integration)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('TopBar slow publish', () => {
|
||||
test('after 30s the button reads "Still publishing..." instead of failing', async () => {
|
||||
vi.useFakeTimers();
|
||||
let resolve!: (v: unknown) => void;
|
||||
publishMock.mockImplementation(() => new Promise((r) => { resolve = r; }));
|
||||
render();
|
||||
await clickPublish();
|
||||
expect(publishButton().textContent).toContain('Publishing...');
|
||||
expect(publishButton().textContent).not.toContain('Still publishing');
|
||||
await act(async () => { vi.advanceTimersByTime(30_000); });
|
||||
expect(publishButton().textContent).toContain('Still publishing...');
|
||||
await act(async () => { resolve({ success: true }); await Promise.resolve(); await Promise.resolve(); });
|
||||
expect(publishButton().textContent).not.toContain('Still publishing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TopBar Preview head parity (item 14)', () => {
|
||||
const rootState = (style: Record<string, string> = {}) => JSON.stringify({
|
||||
ROOT: { type: { resolvedName: 'Container' }, isCanvas: true, props: { tag: 'div', style }, displayName: 'Container', custom: {}, hidden: false, nodes: [], linkedNodes: {} },
|
||||
|
||||
@@ -52,6 +52,8 @@ export const TopBar: React.FC<TopBarProps> = ({ showGuides, onToggleGuides }) =>
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveFailed, setSaveFailed] = useState(false);
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
// A big site can take well over 30s to publish: say so instead of looking stuck.
|
||||
const [publishSlow, setPublishSlow] = useState(false);
|
||||
const [published, setPublished] = useState(false);
|
||||
const [errorNotice, setErrorNotice] = useState<string | null>(null);
|
||||
const [publishWarnings, setPublishWarnings] = useState<string[]>([]);
|
||||
@@ -185,6 +187,8 @@ export const TopBar: React.FC<TopBarProps> = ({ showGuides, onToggleGuides }) =>
|
||||
const versionAtStart = getVersion();
|
||||
publishingRef.current = true;
|
||||
setPublishing(true);
|
||||
setPublishSlow(false);
|
||||
const slowTimer = setTimeout(() => setPublishSlow(true), 30_000);
|
||||
setPublishWarnings([]);
|
||||
try {
|
||||
const result = await publish();
|
||||
@@ -217,8 +221,10 @@ export const TopBar: React.FC<TopBarProps> = ({ showGuides, onToggleGuides }) =>
|
||||
console.error('Publish failed:', e);
|
||||
setErrorNotice(messageOf(e, 'Publish'));
|
||||
} finally {
|
||||
clearTimeout(slowTimer);
|
||||
publishingRef.current = false;
|
||||
setPublishing(false);
|
||||
setPublishSlow(false);
|
||||
}
|
||||
}, [publish, canSave, getVersion, setEmptyRefusal, pendingChecks, computeChecks]);
|
||||
|
||||
@@ -593,7 +599,7 @@ export const TopBar: React.FC<TopBarProps> = ({ showGuides, onToggleGuides }) =>
|
||||
aria-label="Publish to live site"
|
||||
>
|
||||
{publishing ? (
|
||||
<><i className="fa fa-spinner fa-spin" /> Publishing...</>
|
||||
<><i className="fa fa-spinner fa-spin" /> {publishSlow ? 'Still publishing...' : 'Publishing...'}</>
|
||||
) : (
|
||||
<><i className="fa fa-globe" /> Publish</>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user