An AI video job can keep running after the user refreshes the page. The browser loses its component state, but the generation service still has work to do. When the page comes back, the UI needs to find that job, resume status checks, and eventually replace the pending card with a result or a useful error.
This article uses EaseGen, an AI image and video creation app, as its example. It is published on behalf of EaseGen. AI was used to draft the article, and the technical details were checked against the project's current implementation. The code below is a reduced example of the reconciliation logic, rather than a copy of the full application.
Save enough information to resume the job
A prompt and a model name are enough to describe what the user asked for. They are not enough to reconnect to a running job.
EaseGen's video interface restores pending attempts from account history. The recovery data includes the history record ID, the generation task ID, the provider routing value, and the credit reservation metadata returned by the server. The resumed status request carries the task ID and those associated fields back to the application backend.
These fields have different jobs. The history ID identifies the card. The task ID identifies the running generation. The routing value lets the backend query the correct service. Credit reservation metadata lets the backend associate a terminal failure with the original reservation.
The server still needs to validate the signed-in user's access to the task. A task ID or a client-supplied credit value is not authorization, and the browser must not decide whether a refund is owed.
The start time also belongs in durable history. A restored card should calculate elapsed time from the original attempt, rather than resetting its timer when the component mounts.
A recent history response is a partial view
The video interface requests the latest 20 history records. That is useful for rendering recent activity, but it cannot answer whether every older job still exists.
Suppose the browser is tracking job A and a new response contains jobs B through U. Removing A because it is absent would make a running generation disappear. A local upload can also be missing because it has not yet become a server record.
The reconciliation rule is:
- Keep existing pending entries unless a matching record explicitly reports success or failure.
- Add pending entries recovered from history, without duplicating a known history ID or task ID.
Here is a small JavaScript implementation. In this example, both local and server entries use pending for unfinished work; an application may map that to a different UI status.
function reconcilePending(previous, snapshot) {
const terminal = snapshot.filter(
item => item.status === 'succeeded' || item.status === 'failed'
);
const terminalIds = new Set(terminal.map(item => item.id));
const terminalTasks = new Set(
terminal.map(item => item.taskId).filter(Boolean)
);
const next = previous.filter(item =>
!terminalIds.has(item.id) &&
(!item.taskId || !terminalTasks.has(item.taskId))
);
const seenIds = new Set(next.map(item => item.id));
const seenTasks = new Set(next.map(item => item.taskId).filter(Boolean));
for (const item of snapshot) {
if (item.status !== 'pending') continue;
if (seenIds.has(item.id)) continue;
if (item.taskId && seenTasks.has(item.taskId)) continue;
if (terminalIds.has(item.id)) continue;
if (item.taskId && terminalTasks.has(item.taskId)) continue;
next.push(item);
seenIds.add(item.id);
if (item.taskId) seenTasks.add(item.taskId);
}
return next;
}
Matching by task ID matters when a temporary local card and its durable history record have different record IDs. Matching by record ID also covers entries that do not yet have a task ID.
Keeping a card is only half of recovery. EaseGen also retains recovered task descriptors in a map keyed by task ID. If a task falls out of the latest history page, that map still supplies the information needed for the next status check. Explicit terminal records remove it from the map.
Give each task one polling owner in the component
A newly submitted task may already have a status loop. The history refresh can discover that same task while the original loop is still running.
Before querying a recovered task, the video interface checks a set of task IDs already handled by the local submission flow. If the ID is present, the recovery flow leaves it to that loop. A separate in-flight guard prevents overlapping history refreshes when a timer and a focus event fire close together.
Those guards apply within the current component instance. They do not coordinate every browser tab, and they do not provide exactly-once generation. Backend task access checks and idempotent financial operations still need their own protection.
Resume on focus, preserve uncertainty on network errors
EaseGen refreshes video history when the window receives focus or the document's visibility changes. Its recovery loop avoids starting history queries while the document is hidden. On unmount, it removes the event listeners, clears its retry timer, and stops applying results to that component.
A failed status request does not necessarily mean the generation failed. A disconnected browser cannot know the outcome. The recovery flow preserves the last known pending state and retries later.
When the backend explicitly reports failure, the interface removes the pending entry and displays the returned failure reason. When it reports success, the flow persists the output media and reconciles the completed history record.
Do not automatically resubmit a generation because a status check timed out. The original job may still complete, and a new submission can create another job with another charge.
Test competing tasks and incomplete snapshots
The useful checks go beyond a single successful refresh:
| Scenario | Expected behavior |
|---|---|
| A pending job appears in history after reload | Restore its card and resume status checks with its saved fields. |
| The latest history page omits a tracked job | Keep the card and its recovered query descriptor. |
| An upload has no server record yet | Preserve the local upload entry. |
| A terminal record has a different history ID but the same task ID | Remove the matching pending card. |
| The original submission loop already owns the task | Avoid a second recovery status loop. |
| A temporary status request fails | Keep the last known state and allow another check. |
| The page becomes visible again | Resume synchronization without overlapping an in-flight refresh. |
One boundary remains: this recovery approach depends on having persisted a task identifier. If the generation service accepts a create request but the response is lost before that identifier is recorded, refreshing the UI alone cannot resolve the ambiguity. That requires a backend request identifier and a way to look up the accepted operation; blindly retrying is unsafe.
For an asynchronous generation interface, the history feed is part of the recovery contract. Its response needs to distinguish a terminal outcome from an incomplete view, and the client needs to keep enough information to continue checking work that has fallen outside that view.
Top comments (0)