I am working on a React/TypeScript browser workflow that uploads one or more reference images, creates a long-running generation task, polls its status, restores up to eight recent tasks after reload, and later requests short-lived preview or download URLs.
Recent tasks are scoped to either a guest identity or a signed-in identity. Each task has its own polling controller:
const pollControllers = new Map<string, AbortController>();
async function pollTask(taskId: string) {
const controller = new AbortController();
pollControllers.set(taskId, controller);
while (!controller.signal.aborted) {
const task = await getTask(taskId); // signal is not currently passed here
applyAccountBalance(task);
setRecentTasks((items) =>
items.map((item) => item.taskId === taskId ? merge(item, task) : item),
);
await waitForNextPoll(3000, controller.signal);
}
}
When the identity scope changes, the effect aborts every controller, clears the controller map and task list, then loads the new identity’s saved tasks. The abort stops the delay between polls, but it does not cancel a getTask() request already in flight. A response from the old identity can therefore arrive after the new scope is active. Mapping by taskId prevents an old task from being inserted into an empty list, but other side effects such as applyAccountBalance(task) can still run for the wrong scope.
The reproduction I am guarding against is:
- Start an image generation and begin polling as a guest.
- Leave one status request in flight.
- Sign in, which migrates and reloads the recent-task scope.
- Let the guest request resolve after the signed-in state is active.
Would you pass the same abort signal into every status request, add a monotonically increasing identity epoch and reject responses whose captured epoch is stale, or use both defenses? I am leaning toward both: cancellation for efficiency and an epoch check before every stateful side effect for correctness.
I would also like to keep independent polling for restored tasks, so a single global “active request” flag is not enough. Is there a simpler invariant for this kind of multi-task polling UI?