import apiFetch from '@wordpress/api-fetch'; /** * "Fix with AI" runs as a job on the server: the request that starts it * answers at once with the job's key, the agent is called after that, and * the editor asks every few seconds whether the answer has come. No request * waits on the agent, whose rewrite can take longer than many hosts let a * request run before they cut it with an error page of their own. */ /** Where a job stands, as the plugin's job route says it. */ export type FixJobState = | { status: 'running'; seconds?: number } | { status: 'done'; preview: T } | { status: 'failed'; error: { code?: string; message: string; data?: { status?: number } }; }; /** How long to wait between two questions, and for the answer at most. */ export const POLL_MS = 3000; export const WAIT_MS = 7 * 60 * 1000; interface WaitOptions { /** Asks the job route; apiFetch by default. */ ask?: (path: string) => Promise>; sleep?: (ms: number) => Promise; now?: () => number; /** True once nobody waits for the answer any more: the dialog closed. */ cancelled?: () => boolean; } /** An error the fix dialogs read like apiFetch's: a message, a code and the status in its data. */ const failure = ( message: string, code: string, status: number ): Error & { code: string; data: { status: number } } => Object.assign(new Error(message), { code, data: { status } }); /** * The answer of a job, once it has come. * * @param job - The job's key, as the start route gave it. * @param options - What to ask with and how to wait; the defaults call the site. * @return The job's answer; a failed job throws its error. */ export const waitForJob = async ( job: string, { ask = path => apiFetch>({ path }), sleep = ms => new Promise(resolve => setTimeout(resolve, ms)), now = Date.now, cancelled = () => false, }: WaitOptions = {} ): Promise => { const deadline = now() + WAIT_MS; for (;;) { await sleep(POLL_MS); if (cancelled()) { throw failure('Closed before the answer came.', 'cancelled', 0); } // The time keeps a page cache from answering for the route. const state = await ask( `/recomaze/v1/seo/post-fix/job/${encodeURIComponent(job)}?_=${now()}` ); if (state.status === 'done') return state.preview; if (state.status === 'failed') { throw failure( state.error.message, state.error.code ?? '', Number(state.error.data?.status ?? 0) ); } if (now() > deadline) { throw failure( 'Recomaze has not answered after seven minutes. Try again in a moment; nothing was applied.', 'recomaze_seo_job_slow', 504 ); } } };